From bde988cd6ab1159f5040e0985e8870f4777eb902 Mon Sep 17 00:00:00 2001 From: retoor Date: Tue, 25 Nov 2025 21:11:23 +0100 Subject: [PATCH] Update. --- .gitea/workflows/build.yaml | 131 ++ .gitignore | 34 + LICENSE | 21 + Makefile | 38 + Makefile.docdb | 88 ++ README.md | 256 ++++ celerityapi.c | 2679 +++++++++++++++++++++++++++++++++++ celerityapi.h | 481 +++++++ docclient.c | 922 ++++++++++++ docserver.c | 1243 ++++++++++++++++ example.c | 392 +++++ loadtest.c | 527 +++++++ 12 files changed, 6812 insertions(+) create mode 100644 .gitea/workflows/build.yaml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 Makefile.docdb create mode 100644 README.md create mode 100644 celerityapi.c create mode 100644 celerityapi.h create mode 100644 docclient.c create mode 100644 docserver.c create mode 100644 example.c create mode 100644 loadtest.c diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..1f60bfb --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,131 @@ +name: Build and Test + +on: + push: + branches: [main, master, develop] + pull_request: + branches: [main, master] + +jobs: + build-framework: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential uuid-dev valgrind + + - name: Build CelerityAPI Example + run: make all + + - name: Build DocDB + run: make -f Makefile.docdb all + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: binaries + path: | + example + loadtest + docserver + docclient + + test-framework: + runs-on: ubuntu-latest + needs: build-framework + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential uuid-dev + + - name: Build + run: | + make all + make -f Makefile.docdb all + + - name: Test CelerityAPI + run: | + ./example --port 9000 & + sleep 2 + ./loadtest -c 10 -d 3 http://127.0.0.1:9000/health + pkill -f "./example --port 9000" || true + + - name: Test DocDB + run: | + ./docserver --port 9080 & + sleep 2 + ./docclient --port 9080 --quick + pkill -f "./docserver --port 9080" || true + + test-docdb-full: + runs-on: ubuntu-latest + needs: build-framework + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential uuid-dev + + - name: Build DocDB + run: make -f Makefile.docdb all + + - name: Run full test suite + run: | + ./docserver --port 9080 & + sleep 2 + ./docclient --port 9080 + pkill -f "./docserver --port 9080" || true + + - name: Test persistence + run: | + rm -rf ./docdb_data + ./docserver --port 9080 & + sleep 1 + curl -X POST http://127.0.0.1:9080/collections -H "Content-Type: application/json" -d '{"name":"ci_test"}' + curl -X POST http://127.0.0.1:9080/collections/ci_test/documents -H "Content-Type: application/json" -d '{"test":"data"}' + pkill -f "./docserver --port 9080" || true + sleep 1 + ./docserver --port 9080 & + sleep 1 + curl -s http://127.0.0.1:9080/collections/ci_test | grep -q "ci_test" + pkill -f "./docserver --port 9080" || true + + memory-check: + runs-on: ubuntu-latest + needs: build-framework + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential uuid-dev valgrind + + - name: Build with debug symbols + run: | + make debug + make -f Makefile.docdb debug + + - name: Memory check CelerityAPI + run: | + timeout 10 valgrind --leak-check=full --error-exitcode=1 ./example --port 9000 & + sleep 3 + curl http://127.0.0.1:9000/health || true + curl http://127.0.0.1:9000/users || true + pkill -f "./example" || true + + - name: Memory check DocDB + run: | + timeout 10 valgrind --leak-check=full --error-exitcode=1 ./docserver --port 9080 & + sleep 3 + curl -X POST http://127.0.0.1:9080/collections -H "Content-Type: application/json" -d '{"name":"valgrind_test"}' || true + curl http://127.0.0.1:9080/collections || true + pkill -f "./docserver" || true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b15f89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +example +loadtest +docserver +docclient + +*.o +*.a +*.so +*.dylib + +*.dSYM/ + +docdb_data/ + +*.log +*.tmp +*.bak +*.swp +*.swo +*~ + +.DS_Store +Thumbs.db + +.vscode/ +.idea/ +*.sublime-* + +core +vgcore.* +*.core + +compile_commands.json +.cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c13f991 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a228f72 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +CC = gcc +CFLAGS = -std=c11 -Wall -Wextra -O2 +LDFLAGS = -lpthread + +TARGET = example +LOADTEST = loadtest +SOURCES = celerityapi.c example.c +HEADERS = celerityapi.h + +.PHONY: all clean run debug test + +all: $(TARGET) $(LOADTEST) + +$(TARGET): $(SOURCES) $(HEADERS) + $(CC) $(CFLAGS) -o $(TARGET) $(SOURCES) $(LDFLAGS) + +$(LOADTEST): loadtest.c + $(CC) $(CFLAGS) -o $(LOADTEST) loadtest.c $(LDFLAGS) -lm + +debug: CFLAGS += -g -DDEBUG +debug: clean all + +clean: + rm -f $(TARGET) $(LOADTEST) + +run: $(TARGET) + ./$(TARGET) + +test: $(TARGET) $(LOADTEST) + @echo "Starting server..." + @./$(TARGET) --port 9000 & + @sleep 2 + @echo "Running load test..." + @./$(LOADTEST) -c 50 -d 5 http://127.0.0.1:9000/health + @pkill -f "./$(TARGET) --port 9000" || true + +valgrind: debug + valgrind --leak-check=full --show-leak-kinds=all ./$(TARGET) diff --git a/Makefile.docdb b/Makefile.docdb new file mode 100644 index 0000000..4ff683d --- /dev/null +++ b/Makefile.docdb @@ -0,0 +1,88 @@ +CC = gcc +CFLAGS = -std=c11 -Wall -Wextra -O2 +LDFLAGS = -lpthread -luuid + +DOCSERVER = docserver +DOCCLIENT = docclient +SERVER_SOURCES = celerityapi.c docserver.c +CLIENT_SOURCES = docclient.c +HEADERS = celerityapi.h + +.PHONY: all clean server client run test test-quick test-stress valgrind help + +all: $(DOCSERVER) $(DOCCLIENT) + +$(DOCSERVER): $(SERVER_SOURCES) $(HEADERS) + $(CC) $(CFLAGS) -o $(DOCSERVER) $(SERVER_SOURCES) $(LDFLAGS) + +$(DOCCLIENT): $(CLIENT_SOURCES) + $(CC) $(CFLAGS) -o $(DOCCLIENT) $(CLIENT_SOURCES) + +server: $(DOCSERVER) + +client: $(DOCCLIENT) + +debug: CFLAGS += -g -DDEBUG +debug: clean all + +clean: + rm -f $(DOCSERVER) $(DOCCLIENT) + rm -rf ./docdb_data + +run: $(DOCSERVER) + ./$(DOCSERVER) + +test: $(DOCSERVER) $(DOCCLIENT) + @echo "Starting DocDB server..." + @rm -rf ./docdb_data + @./$(DOCSERVER) --port 9080 & + @sleep 1 + @echo "Running test client..." + @./$(DOCCLIENT) --port 9080 || true + @pkill -f "./$(DOCSERVER) --port 9080" || true + +test-quick: $(DOCSERVER) $(DOCCLIENT) + @echo "Starting DocDB server..." + @rm -rf ./docdb_data + @./$(DOCSERVER) --port 9080 & + @sleep 1 + @echo "Running quick tests..." + @./$(DOCCLIENT) --port 9080 --quick || true + @pkill -f "./$(DOCSERVER) --port 9080" || true + +test-stress: $(DOCSERVER) $(DOCCLIENT) + @echo "Starting DocDB server..." + @rm -rf ./docdb_data + @./$(DOCSERVER) --port 9080 & + @sleep 1 + @echo "Running stress tests..." + @./$(DOCCLIENT) --port 9080 --stress-only || true + @pkill -f "./$(DOCSERVER) --port 9080" || true + +test-persistence: $(DOCSERVER) $(DOCCLIENT) + @./$(DOCCLIENT) --port 9080 --test-persistence + +valgrind: debug + valgrind --leak-check=full --show-leak-kinds=all ./$(DOCSERVER) + +help: + @echo "DocDB Build System" + @echo "" + @echo "Targets:" + @echo " all Build server and client" + @echo " server Build server only" + @echo " client Build client only" + @echo " debug Build with debug symbols" + @echo " clean Remove binaries and data" + @echo " run Build and run server" + @echo " test Run full test suite" + @echo " test-quick Run basic tests only" + @echo " test-stress Run stress tests only" + @echo " valgrind Run with memory leak detection" + @echo " help Show this help" + @echo "" + @echo "Server options:" + @echo " ./docserver --host 0.0.0.0 --port 8080 --data ./data" + @echo "" + @echo "Client options:" + @echo " ./docclient --host 127.0.0.1 --port 8080 --verbose" diff --git a/README.md b/README.md new file mode 100644 index 0000000..61a7ece --- /dev/null +++ b/README.md @@ -0,0 +1,256 @@ +# CelerityAPI + +A high-performance HTTP web framework written in pure C, inspired by FastAPI. Zero external dependencies beyond standard C library and POSIX APIs. + +## Features + +- Non-blocking I/O with epoll +- Trie-based URL routing with path parameters +- JSON parsing and serialization +- Middleware pipeline (logging, CORS, timing, auth) +- Dependency injection system +- Request validation with schemas +- WebSocket support +- OpenAPI schema generation +- Thread-safe design + +## Requirements + +- GCC with C11 support +- POSIX-compliant system (Linux, macOS, BSD) +- uuid-dev (for DocDB) +- pthread + +## Build + +```bash +make all +``` + +## Quick Start + +```c +#include "celerityapi.h" + +static http_response_t* handle_hello(http_request_t *req, dependency_context_t *deps) { + (void)req; (void)deps; + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "message", json_string("Hello, World")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +int main(void) { + app_context_t *app = app_create("MyAPI", "1.0.0"); + ROUTE_GET(app, "/hello", handle_hello); + app_run(app, "127.0.0.1", 8000); + app_destroy(app); + return 0; +} +``` + +## API Reference + +### Application + +```c +app_context_t* app_create(const char *title, const char *version); +void app_add_route(app_context_t *app, http_method_t method, const char *path, + route_handler_fn handler, field_schema_t *schema); +void app_add_middleware(app_context_t *app, middleware_fn middleware, void *ctx); +void app_add_dependency(app_context_t *app, const char *name, dep_factory_fn factory, + dep_cleanup_fn cleanup, dependency_scope_t scope); +void app_run(app_context_t *app, const char *host, int port); +void app_destroy(app_context_t *app); +``` + +### Route Macros + +```c +ROUTE_GET(app, "/path", handler); +ROUTE_POST(app, "/path", handler, schema); +ROUTE_PUT(app, "/path", handler, schema); +ROUTE_DELETE(app, "/path", handler); +ROUTE_PATCH(app, "/path", handler, schema); +``` + +### JSON + +```c +json_value_t* json_parse(const char *json_str); +char* json_serialize(json_value_t *value); +json_value_t* json_object(void); +json_value_t* json_array(void); +json_value_t* json_string(const char *value); +json_value_t* json_number(double value); +json_value_t* json_bool(bool value); +json_value_t* json_null(void); +void json_object_set(json_value_t *obj, const char *key, json_value_t *value); +json_value_t* json_object_get(json_value_t *obj, const char *key); +void json_array_append(json_value_t *arr, json_value_t *value); +void json_value_free(json_value_t *value); +``` + +### Response + +```c +http_response_t* http_response_create(int status_code); +void http_response_set_header(http_response_t *resp, const char *key, const char *value); +void http_response_set_body(http_response_t *resp, const char *body, size_t length); +void http_response_set_json(http_response_t *resp, json_value_t *json); +void http_response_free(http_response_t *resp); +``` + +### Built-in Middleware + +```c +http_response_t* logging_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx); +http_response_t* cors_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx); +http_response_t* timing_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx); +http_response_t* auth_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx); +``` + +## Path Parameters + +```c +ROUTE_GET(app, "/users/{id}", handle_get_user); + +static http_response_t* handle_get_user(http_request_t *req, dependency_context_t *deps) { + char *user_id = dict_get(req->path_params, "id"); + // ... +} +``` + +## Request Validation + +```c +field_schema_t *schema = schema_create("user", TYPE_OBJECT); + +field_schema_t *name = schema_create("name", TYPE_STRING); +schema_set_required(name, true); +schema_set_min(name, 3); +schema_set_max(name, 100); +schema_add_field(schema, name); + +field_schema_t *age = schema_create("age", TYPE_INT); +schema_set_min(age, 0); +schema_set_max(age, 150); +schema_add_field(schema, age); + +ROUTE_POST(app, "/users", handle_create_user, schema); +``` + +## Dependency Injection + +```c +static void* database_factory(dependency_context_t *ctx) { + return create_db_connection(); +} + +static void database_cleanup(void *instance) { + close_db_connection(instance); +} + +app_add_dependency(app, "database", database_factory, database_cleanup, DEP_SINGLETON); + +static http_response_t* handler(http_request_t *req, dependency_context_t *deps) { + db_t *db = dep_resolve(deps, "database"); + // ... +} +``` + +--- + +# DocDB + +A document database built on CelerityAPI with file-based persistence. + +## Build + +```bash +make -f Makefile.docdb all +``` + +## Run + +```bash +./docserver --host 127.0.0.1 --port 8080 --data ./data +``` + +## Test + +```bash +./docclient --host 127.0.0.1 --port 8080 +./docclient --quick # Basic tests only +./docclient --stress-only # Performance tests +``` + +## REST API + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | /health | Health check | +| GET | /stats | Database statistics | +| GET | /collections | List collections | +| POST | /collections | Create collection | +| GET | /collections/{name} | Get collection info | +| DELETE | /collections/{name} | Delete collection | +| GET | /collections/{name}/documents | List documents | +| POST | /collections/{name}/documents | Create document | +| GET | /collections/{name}/documents/{id} | Get document | +| PUT | /collections/{name}/documents/{id} | Update document | +| DELETE | /collections/{name}/documents/{id} | Delete document | +| POST | /collections/{name}/query | Query documents | +| POST | /collections/{name}/bulk | Bulk insert | +| DELETE | /collections/{name}/bulk | Bulk delete | + +## Examples + +Create collection: +```bash +curl -X POST http://localhost:8080/collections \ + -H "Content-Type: application/json" \ + -d '{"name":"users"}' +``` + +Create document: +```bash +curl -X POST http://localhost:8080/collections/users/documents \ + -H "Content-Type: application/json" \ + -d '{"name":"Alice","email":"alice@example.com"}' +``` + +Query documents: +```bash +curl -X POST http://localhost:8080/collections/users/query \ + -H "Content-Type: application/json" \ + -d '{"filter":{"name":"Alice"},"limit":10}' +``` + +Bulk insert: +```bash +curl -X POST http://localhost:8080/collections/users/bulk \ + -H "Content-Type: application/json" \ + -d '{"documents":[{"name":"Bob"},{"name":"Carol"}]}' +``` + +--- + +## Project Structure + +``` +celerityapi.h Framework header +celerityapi.c Framework implementation +example.c Framework usage example +loadtest.c HTTP load testing tool +docserver.c Document database server +docclient.c DocDB test client +Makefile Framework build +Makefile.docdb DocDB build +``` + +## License + +MIT diff --git a/celerityapi.c b/celerityapi.c new file mode 100644 index 0000000..9b21e3c --- /dev/null +++ b/celerityapi.c @@ -0,0 +1,2679 @@ +#include "celerityapi.h" + +static volatile sig_atomic_t g_shutdown_requested = 0; +static app_context_t *g_app = NULL; + +static void signal_handler(int sig) { + (void)sig; + g_shutdown_requested = 1; + if (g_app) { + g_app->running = false; + if (g_app->server) { + g_app->server->running = false; + } + } +} + +void celerity_log(const char *level, const char *format, ...) { + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + char time_buf[32]; + strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm_info); + fprintf(stderr, "[%s] [%s] ", time_buf, level); + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); + fprintf(stderr, "\n"); +} + +static uint32_t hash_string(const char *str) { + uint32_t hash = 5381; + int c; + while ((c = *str++)) { + hash = ((hash << 5) + hash) + c; + } + return hash; +} + +dict_t* dict_create(size_t initial_size) { + dict_t *dict = calloc(1, sizeof(dict_t)); + if (!dict) return NULL; + dict->size = initial_size > 0 ? initial_size : DICT_INITIAL_SIZE; + dict->buckets = calloc(dict->size, sizeof(dict_entry_t*)); + if (!dict->buckets) { + free(dict); + return NULL; + } + pthread_mutex_init(&dict->lock, NULL); + return dict; +} + +void dict_set(dict_t *dict, const char *key, void *value) { + if (!dict || !key) return; + pthread_mutex_lock(&dict->lock); + uint32_t index = hash_string(key) % dict->size; + dict_entry_t *entry = dict->buckets[index]; + while (entry) { + if (strcmp(entry->key, key) == 0) { + entry->value = value; + pthread_mutex_unlock(&dict->lock); + return; + } + entry = entry->next; + } + entry = calloc(1, sizeof(dict_entry_t)); + if (!entry) { + pthread_mutex_unlock(&dict->lock); + return; + } + entry->key = str_duplicate(key); + entry->value = value; + entry->next = dict->buckets[index]; + dict->buckets[index] = entry; + dict->count++; + pthread_mutex_unlock(&dict->lock); +} + +void* dict_get(dict_t *dict, const char *key) { + if (!dict || !key) return NULL; + pthread_mutex_lock(&dict->lock); + uint32_t index = hash_string(key) % dict->size; + dict_entry_t *entry = dict->buckets[index]; + while (entry) { + if (strcmp(entry->key, key) == 0) { + void *value = entry->value; + pthread_mutex_unlock(&dict->lock); + return value; + } + entry = entry->next; + } + pthread_mutex_unlock(&dict->lock); + return NULL; +} + +bool dict_has(dict_t *dict, const char *key) { + if (!dict || !key) return false; + pthread_mutex_lock(&dict->lock); + uint32_t index = hash_string(key) % dict->size; + dict_entry_t *entry = dict->buckets[index]; + while (entry) { + if (strcmp(entry->key, key) == 0) { + pthread_mutex_unlock(&dict->lock); + return true; + } + entry = entry->next; + } + pthread_mutex_unlock(&dict->lock); + return false; +} + +void dict_remove(dict_t *dict, const char *key) { + if (!dict || !key) return; + pthread_mutex_lock(&dict->lock); + uint32_t index = hash_string(key) % dict->size; + dict_entry_t *entry = dict->buckets[index]; + dict_entry_t *prev = NULL; + while (entry) { + if (strcmp(entry->key, key) == 0) { + if (prev) { + prev->next = entry->next; + } else { + dict->buckets[index] = entry->next; + } + free(entry->key); + free(entry); + dict->count--; + pthread_mutex_unlock(&dict->lock); + return; + } + prev = entry; + entry = entry->next; + } + pthread_mutex_unlock(&dict->lock); +} + +void dict_free(dict_t *dict) { + if (!dict) return; + for (size_t i = 0; i < dict->size; i++) { + dict_entry_t *entry = dict->buckets[i]; + while (entry) { + dict_entry_t *next = entry->next; + free(entry->key); + free(entry); + entry = next; + } + } + pthread_mutex_destroy(&dict->lock); + free(dict->buckets); + free(dict); +} + +void dict_free_with_values(dict_t *dict, void (*free_fn)(void*)) { + if (!dict) return; + for (size_t i = 0; i < dict->size; i++) { + dict_entry_t *entry = dict->buckets[i]; + while (entry) { + dict_entry_t *next = entry->next; + if (free_fn && entry->value) { + free_fn(entry->value); + } + free(entry->key); + free(entry); + entry = next; + } + } + pthread_mutex_destroy(&dict->lock); + free(dict->buckets); + free(dict); +} + +char** dict_keys(dict_t *dict, size_t *count) { + if (!dict || !count) return NULL; + pthread_mutex_lock(&dict->lock); + char **keys = calloc(dict->count + 1, sizeof(char*)); + if (!keys) { + pthread_mutex_unlock(&dict->lock); + *count = 0; + return NULL; + } + size_t idx = 0; + for (size_t i = 0; i < dict->size && idx < dict->count; i++) { + dict_entry_t *entry = dict->buckets[i]; + while (entry) { + keys[idx++] = str_duplicate(entry->key); + entry = entry->next; + } + } + *count = idx; + pthread_mutex_unlock(&dict->lock); + return keys; +} + +array_t* array_create(size_t initial_capacity) { + array_t *arr = calloc(1, sizeof(array_t)); + if (!arr) return NULL; + arr->capacity = initial_capacity > 0 ? initial_capacity : 16; + arr->items = calloc(arr->capacity, sizeof(void*)); + if (!arr->items) { + free(arr); + return NULL; + } + return arr; +} + +void array_append(array_t *arr, void *item) { + if (!arr) return; + if (arr->count >= arr->capacity) { + size_t new_capacity = arr->capacity * 2; + void **new_items = realloc(arr->items, new_capacity * sizeof(void*)); + if (!new_items) return; + arr->items = new_items; + arr->capacity = new_capacity; + } + arr->items[arr->count++] = item; +} + +void* array_get(array_t *arr, size_t index) { + if (!arr || index >= arr->count) return NULL; + return arr->items[index]; +} + +void array_free(array_t *arr) { + if (!arr) return; + free(arr->items); + free(arr); +} + +void array_free_with_items(array_t *arr, void (*free_fn)(void*)) { + if (!arr) return; + if (free_fn) { + for (size_t i = 0; i < arr->count; i++) { + if (arr->items[i]) { + free_fn(arr->items[i]); + } + } + } + free(arr->items); + free(arr); +} + +string_builder_t* sb_create(void) { + string_builder_t *sb = calloc(1, sizeof(string_builder_t)); + if (!sb) return NULL; + sb->capacity = 256; + sb->data = calloc(sb->capacity, 1); + if (!sb->data) { + free(sb); + return NULL; + } + return sb; +} + +static void sb_ensure_capacity(string_builder_t *sb, size_t additional) { + if (sb->length + additional >= sb->capacity) { + size_t new_capacity = (sb->capacity + additional) * 2; + char *new_data = realloc(sb->data, new_capacity); + if (!new_data) return; + sb->data = new_data; + sb->capacity = new_capacity; + } +} + +void sb_append(string_builder_t *sb, const char *str) { + if (!sb || !str) return; + size_t len = strlen(str); + sb_ensure_capacity(sb, len + 1); + memcpy(sb->data + sb->length, str, len); + sb->length += len; + sb->data[sb->length] = '\0'; +} + +void sb_append_char(string_builder_t *sb, char c) { + if (!sb) return; + sb_ensure_capacity(sb, 2); + sb->data[sb->length++] = c; + sb->data[sb->length] = '\0'; +} + +void sb_append_int(string_builder_t *sb, int value) { + if (!sb) return; + char buf[32]; + snprintf(buf, sizeof(buf), "%d", value); + sb_append(sb, buf); +} + +void sb_append_double(string_builder_t *sb, double value) { + if (!sb) return; + char buf[64]; + if (value == (int)value) { + snprintf(buf, sizeof(buf), "%.0f", value); + } else { + snprintf(buf, sizeof(buf), "%g", value); + } + sb_append(sb, buf); +} + +char* sb_to_string(string_builder_t *sb) { + if (!sb) return NULL; + char *result = str_duplicate(sb->data); + return result; +} + +void sb_free(string_builder_t *sb) { + if (!sb) return; + free(sb->data); + free(sb); +} + +char* str_duplicate(const char *str) { + if (!str) return NULL; + size_t len = strlen(str); + char *dup = malloc(len + 1); + if (!dup) return NULL; + memcpy(dup, str, len + 1); + return dup; +} + +char* str_trim(const char *str) { + if (!str) return NULL; + while (*str && isspace((unsigned char)*str)) str++; + if (*str == '\0') return str_duplicate(""); + const char *end = str + strlen(str) - 1; + while (end > str && isspace((unsigned char)*end)) end--; + size_t len = end - str + 1; + char *result = malloc(len + 1); + if (!result) return NULL; + memcpy(result, str, len); + result[len] = '\0'; + return result; +} + +char** str_split(const char *str, char delimiter, size_t *count) { + if (!str || !count) return NULL; + *count = 0; + size_t capacity = 8; + char **parts = calloc(capacity, sizeof(char*)); + if (!parts) return NULL; + const char *start = str; + const char *p = str; + while (*p) { + if (*p == delimiter) { + if (*count >= capacity) { + capacity *= 2; + char **new_parts = realloc(parts, capacity * sizeof(char*)); + if (!new_parts) { + str_split_free(parts, *count); + return NULL; + } + parts = new_parts; + } + size_t len = p - start; + parts[*count] = malloc(len + 1); + if (parts[*count]) { + memcpy(parts[*count], start, len); + parts[*count][len] = '\0'; + } + (*count)++; + start = p + 1; + } + p++; + } + if (*count >= capacity) { + capacity++; + char **new_parts = realloc(parts, capacity * sizeof(char*)); + if (!new_parts) { + str_split_free(parts, *count); + return NULL; + } + parts = new_parts; + } + size_t len = p - start; + parts[*count] = malloc(len + 1); + if (parts[*count]) { + memcpy(parts[*count], start, len); + parts[*count][len] = '\0'; + } + (*count)++; + return parts; +} + +void str_split_free(char **parts, size_t count) { + if (!parts) return; + for (size_t i = 0; i < count; i++) { + free(parts[i]); + } + free(parts); +} + +bool str_starts_with(const char *str, const char *prefix) { + if (!str || !prefix) return false; + return strncmp(str, prefix, strlen(prefix)) == 0; +} + +bool str_ends_with(const char *str, const char *suffix) { + if (!str || !suffix) return false; + size_t str_len = strlen(str); + size_t suffix_len = strlen(suffix); + if (suffix_len > str_len) return false; + return strcmp(str + str_len - suffix_len, suffix) == 0; +} + +char* url_decode(const char *str) { + if (!str) return NULL; + size_t len = strlen(str); + char *result = malloc(len + 1); + if (!result) return NULL; + char *out = result; + while (*str) { + if (*str == '%' && str[1] && str[2]) { + char hex[3] = {str[1], str[2], '\0'}; + *out++ = (char)strtol(hex, NULL, 16); + str += 3; + } else if (*str == '+') { + *out++ = ' '; + str++; + } else { + *out++ = *str++; + } + } + *out = '\0'; + return result; +} + +char* url_encode(const char *str) { + if (!str) return NULL; + size_t len = strlen(str); + char *result = malloc(len * 3 + 1); + if (!result) return NULL; + char *out = result; + while (*str) { + if (isalnum((unsigned char)*str) || *str == '-' || *str == '_' || + *str == '.' || *str == '~') { + *out++ = *str; + } else if (*str == ' ') { + *out++ = '+'; + } else { + sprintf(out, "%%%02X", (unsigned char)*str); + out += 3; + } + str++; + } + *out = '\0'; + return result; +} + +static const char base64_chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +char* base64_encode(const unsigned char *data, size_t len) { + if (!data) return NULL; + size_t out_len = 4 * ((len + 2) / 3); + char *result = malloc(out_len + 1); + if (!result) return NULL; + char *out = result; + size_t i; + for (i = 0; i + 2 < len; i += 3) { + *out++ = base64_chars[(data[i] >> 2) & 0x3F]; + *out++ = base64_chars[((data[i] & 0x3) << 4) | ((data[i + 1] >> 4) & 0xF)]; + *out++ = base64_chars[((data[i + 1] & 0xF) << 2) | ((data[i + 2] >> 6) & 0x3)]; + *out++ = base64_chars[data[i + 2] & 0x3F]; + } + if (i < len) { + *out++ = base64_chars[(data[i] >> 2) & 0x3F]; + if (i + 1 < len) { + *out++ = base64_chars[((data[i] & 0x3) << 4) | ((data[i + 1] >> 4) & 0xF)]; + *out++ = base64_chars[(data[i + 1] & 0xF) << 2]; + } else { + *out++ = base64_chars[(data[i] & 0x3) << 4]; + *out++ = '='; + } + *out++ = '='; + } + *out = '\0'; + return result; +} + +static int base64_decode_char(char c) { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; +} + +unsigned char* base64_decode(const char *str, size_t *out_len) { + if (!str || !out_len) return NULL; + size_t len = strlen(str); + if (len % 4 != 0) return NULL; + *out_len = len / 4 * 3; + if (str[len - 1] == '=') (*out_len)--; + if (str[len - 2] == '=') (*out_len)--; + unsigned char *result = malloc(*out_len + 1); + if (!result) return NULL; + size_t j = 0; + for (size_t i = 0; i < len; i += 4) { + int a = base64_decode_char(str[i]); + int b = base64_decode_char(str[i + 1]); + int c = str[i + 2] == '=' ? 0 : base64_decode_char(str[i + 2]); + int d = str[i + 3] == '=' ? 0 : base64_decode_char(str[i + 3]); + if (a < 0 || b < 0 || c < 0 || d < 0) { + free(result); + return NULL; + } + result[j++] = (a << 2) | (b >> 4); + if (str[i + 2] != '=') result[j++] = ((b & 0xF) << 4) | (c >> 2); + if (str[i + 3] != '=') result[j++] = ((c & 0x3) << 6) | d; + } + result[*out_len] = '\0'; + return result; +} + +typedef struct { + uint32_t state[5]; + uint32_t count[2]; + unsigned char buffer[64]; +} sha1_context_t; + +static void sha1_transform(uint32_t state[5], const unsigned char buffer[64]) { + uint32_t a, b, c, d, e, temp; + uint32_t w[80]; + for (int i = 0; i < 16; i++) { + w[i] = ((uint32_t)buffer[i * 4] << 24) | + ((uint32_t)buffer[i * 4 + 1] << 16) | + ((uint32_t)buffer[i * 4 + 2] << 8) | + ((uint32_t)buffer[i * 4 + 3]); + } + for (int i = 16; i < 80; i++) { + temp = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; + w[i] = (temp << 1) | (temp >> 31); + } + a = state[0]; b = state[1]; c = state[2]; d = state[3]; e = state[4]; + for (int i = 0; i < 80; i++) { + uint32_t f, k; + if (i < 20) { + f = (b & c) | ((~b) & d); + k = 0x5A827999; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + temp = ((a << 5) | (a >> 27)) + f + e + k + w[i]; + e = d; d = c; c = (b << 30) | (b >> 2); b = a; a = temp; + } + state[0] += a; state[1] += b; state[2] += c; state[3] += d; state[4] += e; +} + +static void sha1_init(sha1_context_t *ctx) { + ctx->state[0] = 0x67452301; + ctx->state[1] = 0xEFCDAB89; + ctx->state[2] = 0x98BADCFE; + ctx->state[3] = 0x10325476; + ctx->state[4] = 0xC3D2E1F0; + ctx->count[0] = ctx->count[1] = 0; +} + +static void sha1_update(sha1_context_t *ctx, const unsigned char *data, size_t len) { + size_t i, j; + j = (ctx->count[0] >> 3) & 63; + if ((ctx->count[0] += (uint32_t)(len << 3)) < (len << 3)) ctx->count[1]++; + ctx->count[1] += (uint32_t)(len >> 29); + if ((j + len) > 63) { + memcpy(&ctx->buffer[j], data, (i = 64 - j)); + sha1_transform(ctx->state, ctx->buffer); + for (; i + 63 < len; i += 64) { + sha1_transform(ctx->state, &data[i]); + } + j = 0; + } else { + i = 0; + } + memcpy(&ctx->buffer[j], &data[i], len - i); +} + +static void sha1_final(unsigned char digest[20], sha1_context_t *ctx) { + unsigned char finalcount[8]; + for (int i = 0; i < 8; i++) { + finalcount[i] = (unsigned char)((ctx->count[(i >= 4 ? 0 : 1)] >> + ((3 - (i & 3)) * 8)) & 255); + } + unsigned char c = 0x80; + sha1_update(ctx, &c, 1); + while ((ctx->count[0] & 504) != 448) { + c = 0x00; + sha1_update(ctx, &c, 1); + } + sha1_update(ctx, finalcount, 8); + for (int i = 0; i < 20; i++) { + digest[i] = (unsigned char)((ctx->state[i >> 2] >> ((3 - (i & 3)) * 8)) & 255); + } +} + +static char* compute_websocket_accept(const char *key) { + const char *magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + size_t key_len = strlen(key); + size_t magic_len = strlen(magic); + char *combined = malloc(key_len + magic_len + 1); + if (!combined) return NULL; + memcpy(combined, key, key_len); + memcpy(combined + key_len, magic, magic_len + 1); + sha1_context_t ctx; + sha1_init(&ctx); + sha1_update(&ctx, (unsigned char*)combined, key_len + magic_len); + unsigned char digest[20]; + sha1_final(digest, &ctx); + free(combined); + return base64_encode(digest, 20); +} + +static const char* skip_whitespace(const char *str) { + while (*str && isspace((unsigned char)*str)) str++; + return str; +} + +static json_value_t* json_parse_value(const char **str); + +static char* json_parse_string_internal(const char **str) { + if (**str != '"') return NULL; + (*str)++; + string_builder_t *sb = sb_create(); + if (!sb) return NULL; + while (**str && **str != '"') { + if (**str == '\\') { + (*str)++; + switch (**str) { + case '"': sb_append_char(sb, '"'); break; + case '\\': sb_append_char(sb, '\\'); break; + case '/': sb_append_char(sb, '/'); break; + case 'b': sb_append_char(sb, '\b'); break; + case 'f': sb_append_char(sb, '\f'); break; + case 'n': sb_append_char(sb, '\n'); break; + case 'r': sb_append_char(sb, '\r'); break; + case 't': sb_append_char(sb, '\t'); break; + case 'u': { + (*str)++; + char hex[5] = {0}; + for (int i = 0; i < 4 && **str; i++) { + hex[i] = *(*str)++; + } + (*str)--; + int code = (int)strtol(hex, NULL, 16); + if (code < 0x80) { + sb_append_char(sb, (char)code); + } else if (code < 0x800) { + sb_append_char(sb, (char)(0xC0 | (code >> 6))); + sb_append_char(sb, (char)(0x80 | (code & 0x3F))); + } else { + sb_append_char(sb, (char)(0xE0 | (code >> 12))); + sb_append_char(sb, (char)(0x80 | ((code >> 6) & 0x3F))); + sb_append_char(sb, (char)(0x80 | (code & 0x3F))); + } + break; + } + default: sb_append_char(sb, **str); break; + } + } else { + sb_append_char(sb, **str); + } + (*str)++; + } + if (**str == '"') (*str)++; + char *result = sb_to_string(sb); + sb_free(sb); + return result; +} + +static json_value_t* json_parse_object(const char **str) { + if (**str != '{') return NULL; + (*str)++; + *str = skip_whitespace(*str); + json_value_t *obj = json_object(); + if (!obj) return NULL; + if (**str == '}') { + (*str)++; + return obj; + } + while (**str) { + *str = skip_whitespace(*str); + if (**str != '"') { + json_value_free(obj); + return NULL; + } + char *key = json_parse_string_internal(str); + if (!key) { + json_value_free(obj); + return NULL; + } + *str = skip_whitespace(*str); + if (**str != ':') { + free(key); + json_value_free(obj); + return NULL; + } + (*str)++; + *str = skip_whitespace(*str); + json_value_t *value = json_parse_value(str); + if (!value) { + free(key); + json_value_free(obj); + return NULL; + } + json_object_set(obj, key, value); + free(key); + *str = skip_whitespace(*str); + if (**str == '}') { + (*str)++; + return obj; + } + if (**str != ',') { + json_value_free(obj); + return NULL; + } + (*str)++; + } + json_value_free(obj); + return NULL; +} + +static json_value_t* json_parse_array(const char **str) { + if (**str != '[') return NULL; + (*str)++; + *str = skip_whitespace(*str); + json_value_t *arr = json_array(); + if (!arr) return NULL; + if (**str == ']') { + (*str)++; + return arr; + } + while (**str) { + *str = skip_whitespace(*str); + json_value_t *value = json_parse_value(str); + if (!value) { + json_value_free(arr); + return NULL; + } + json_array_append(arr, value); + *str = skip_whitespace(*str); + if (**str == ']') { + (*str)++; + return arr; + } + if (**str != ',') { + json_value_free(arr); + return NULL; + } + (*str)++; + } + json_value_free(arr); + return NULL; +} + +static json_value_t* json_parse_value(const char **str) { + *str = skip_whitespace(*str); + if (**str == '"') { + char *s = json_parse_string_internal(str); + if (!s) return NULL; + json_value_t *v = json_string(s); + free(s); + return v; + } + if (**str == '{') { + return json_parse_object(str); + } + if (**str == '[') { + return json_parse_array(str); + } + if (strncmp(*str, "true", 4) == 0) { + *str += 4; + return json_bool(true); + } + if (strncmp(*str, "false", 5) == 0) { + *str += 5; + return json_bool(false); + } + if (strncmp(*str, "null", 4) == 0) { + *str += 4; + return json_null(); + } + if (**str == '-' || isdigit((unsigned char)**str)) { + char *end; + double num = strtod(*str, &end); + if (end == *str) return NULL; + *str = end; + return json_number(num); + } + return NULL; +} + +json_value_t* json_parse(const char *json_str) { + if (!json_str) return NULL; + const char *str = json_str; + return json_parse_value(&str); +} + +static void json_serialize_string(string_builder_t *sb, const char *str) { + sb_append_char(sb, '"'); + while (*str) { + switch (*str) { + case '"': sb_append(sb, "\\\""); break; + case '\\': sb_append(sb, "\\\\"); break; + case '\b': sb_append(sb, "\\b"); break; + case '\f': sb_append(sb, "\\f"); break; + case '\n': sb_append(sb, "\\n"); break; + case '\r': sb_append(sb, "\\r"); break; + case '\t': sb_append(sb, "\\t"); break; + default: + if ((unsigned char)*str < 32) { + char buf[8]; + snprintf(buf, sizeof(buf), "\\u%04x", (unsigned char)*str); + sb_append(sb, buf); + } else { + sb_append_char(sb, *str); + } + break; + } + str++; + } + sb_append_char(sb, '"'); +} + +static void json_serialize_value(string_builder_t *sb, json_value_t *value) { + if (!value) { + sb_append(sb, "null"); + return; + } + switch (value->type) { + case JSON_NULL: + sb_append(sb, "null"); + break; + case JSON_BOOL: + sb_append(sb, value->bool_value ? "true" : "false"); + break; + case JSON_NUMBER: + sb_append_double(sb, value->number_value); + break; + case JSON_STRING: + json_serialize_string(sb, value->string_value ? value->string_value : ""); + break; + case JSON_ARRAY: + sb_append_char(sb, '['); + for (size_t i = 0; i < value->array_value.count; i++) { + if (i > 0) sb_append_char(sb, ','); + json_serialize_value(sb, value->array_value.items[i]); + } + sb_append_char(sb, ']'); + break; + case JSON_OBJECT: + sb_append_char(sb, '{'); + for (size_t i = 0; i < value->object_value.count; i++) { + if (i > 0) sb_append_char(sb, ','); + json_serialize_string(sb, value->object_value.keys[i]); + sb_append_char(sb, ':'); + json_serialize_value(sb, value->object_value.values[i]); + } + sb_append_char(sb, '}'); + break; + } +} + +char* json_serialize(json_value_t *value) { + string_builder_t *sb = sb_create(); + if (!sb) return NULL; + json_serialize_value(sb, value); + char *result = sb_to_string(sb); + sb_free(sb); + return result; +} + +json_value_t* json_object_get(json_value_t *obj, const char *key) { + if (!obj || obj->type != JSON_OBJECT || !key) return NULL; + for (size_t i = 0; i < obj->object_value.count; i++) { + if (strcmp(obj->object_value.keys[i], key) == 0) { + return obj->object_value.values[i]; + } + } + return NULL; +} + +void json_object_set(json_value_t *obj, const char *key, json_value_t *value) { + if (!obj || obj->type != JSON_OBJECT || !key) return; + for (size_t i = 0; i < obj->object_value.count; i++) { + if (strcmp(obj->object_value.keys[i], key) == 0) { + json_value_free(obj->object_value.values[i]); + obj->object_value.values[i] = value; + return; + } + } + size_t new_count = obj->object_value.count + 1; + char **new_keys = realloc(obj->object_value.keys, new_count * sizeof(char*)); + json_value_t **new_values = realloc(obj->object_value.values, new_count * sizeof(json_value_t*)); + if (!new_keys || !new_values) return; + obj->object_value.keys = new_keys; + obj->object_value.values = new_values; + obj->object_value.keys[obj->object_value.count] = str_duplicate(key); + obj->object_value.values[obj->object_value.count] = value; + obj->object_value.count = new_count; +} + +json_value_t* json_array_get(json_value_t *arr, size_t index) { + if (!arr || arr->type != JSON_ARRAY || index >= arr->array_value.count) return NULL; + return arr->array_value.items[index]; +} + +void json_array_append(json_value_t *arr, json_value_t *value) { + if (!arr || arr->type != JSON_ARRAY) return; + size_t new_count = arr->array_value.count + 1; + json_value_t **new_items = realloc(arr->array_value.items, new_count * sizeof(json_value_t*)); + if (!new_items) return; + arr->array_value.items = new_items; + arr->array_value.items[arr->array_value.count] = value; + arr->array_value.count = new_count; +} + +json_value_t* json_null(void) { + json_value_t *v = calloc(1, sizeof(json_value_t)); + if (v) v->type = JSON_NULL; + return v; +} + +json_value_t* json_bool(bool value) { + json_value_t *v = calloc(1, sizeof(json_value_t)); + if (v) { + v->type = JSON_BOOL; + v->bool_value = value; + } + return v; +} + +json_value_t* json_number(double value) { + json_value_t *v = calloc(1, sizeof(json_value_t)); + if (v) { + v->type = JSON_NUMBER; + v->number_value = value; + } + return v; +} + +json_value_t* json_string(const char *value) { + json_value_t *v = calloc(1, sizeof(json_value_t)); + if (v) { + v->type = JSON_STRING; + v->string_value = str_duplicate(value ? value : ""); + } + return v; +} + +json_value_t* json_array(void) { + json_value_t *v = calloc(1, sizeof(json_value_t)); + if (v) { + v->type = JSON_ARRAY; + v->array_value.items = NULL; + v->array_value.count = 0; + } + return v; +} + +json_value_t* json_object(void) { + json_value_t *v = calloc(1, sizeof(json_value_t)); + if (v) { + v->type = JSON_OBJECT; + v->object_value.keys = NULL; + v->object_value.values = NULL; + v->object_value.count = 0; + } + return v; +} + +void json_value_free(json_value_t *value) { + if (!value) return; + switch (value->type) { + case JSON_STRING: + free(value->string_value); + break; + case JSON_ARRAY: + for (size_t i = 0; i < value->array_value.count; i++) { + json_value_free(value->array_value.items[i]); + } + free(value->array_value.items); + break; + case JSON_OBJECT: + for (size_t i = 0; i < value->object_value.count; i++) { + free(value->object_value.keys[i]); + json_value_free(value->object_value.values[i]); + } + free(value->object_value.keys); + free(value->object_value.values); + break; + default: + break; + } + free(value); +} + +json_value_t* json_deep_copy(json_value_t *value) { + if (!value) return NULL; + json_value_t *copy = calloc(1, sizeof(json_value_t)); + if (!copy) return NULL; + copy->type = value->type; + switch (value->type) { + case JSON_NULL: + break; + case JSON_BOOL: + copy->bool_value = value->bool_value; + break; + case JSON_NUMBER: + copy->number_value = value->number_value; + break; + case JSON_STRING: + copy->string_value = str_duplicate(value->string_value); + break; + case JSON_ARRAY: + copy->array_value.count = value->array_value.count; + if (value->array_value.count > 0) { + copy->array_value.items = calloc(value->array_value.count, sizeof(json_value_t*)); + for (size_t i = 0; i < value->array_value.count; i++) { + copy->array_value.items[i] = json_deep_copy(value->array_value.items[i]); + } + } + break; + case JSON_OBJECT: + copy->object_value.count = value->object_value.count; + if (value->object_value.count > 0) { + copy->object_value.keys = calloc(value->object_value.count, sizeof(char*)); + copy->object_value.values = calloc(value->object_value.count, sizeof(json_value_t*)); + for (size_t i = 0; i < value->object_value.count; i++) { + copy->object_value.keys[i] = str_duplicate(value->object_value.keys[i]); + copy->object_value.values[i] = json_deep_copy(value->object_value.values[i]); + } + } + break; + } + return copy; +} + +http_method_t http_method_from_string(const char *method) { + if (!method) return HTTP_UNKNOWN; + if (strcmp(method, "GET") == 0) return HTTP_GET; + if (strcmp(method, "POST") == 0) return HTTP_POST; + if (strcmp(method, "PUT") == 0) return HTTP_PUT; + if (strcmp(method, "DELETE") == 0) return HTTP_DELETE; + if (strcmp(method, "PATCH") == 0) return HTTP_PATCH; + if (strcmp(method, "HEAD") == 0) return HTTP_HEAD; + if (strcmp(method, "OPTIONS") == 0) return HTTP_OPTIONS; + return HTTP_UNKNOWN; +} + +const char* http_method_to_string(http_method_t method) { + switch (method) { + case HTTP_GET: return "GET"; + case HTTP_POST: return "POST"; + case HTTP_PUT: return "PUT"; + case HTTP_DELETE: return "DELETE"; + case HTTP_PATCH: return "PATCH"; + case HTTP_HEAD: return "HEAD"; + case HTTP_OPTIONS: return "OPTIONS"; + default: return "UNKNOWN"; + } +} + +const char* http_status_message(int status_code) { + switch (status_code) { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 200: return "OK"; + case 201: return "Created"; + case 204: return "No Content"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 304: return "Not Modified"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 409: return "Conflict"; + case 422: return "Unprocessable Entity"; + case 429: return "Too Many Requests"; + case 500: return "Internal Server Error"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + default: return "Unknown"; + } +} + +http_request_t* http_parse_request(const char *raw_request, size_t length) { + if (!raw_request || length == 0) return NULL; + http_request_t *req = calloc(1, sizeof(http_request_t)); + if (!req) return NULL; + req->headers = calloc(MAX_HEADERS, sizeof(header_t)); + if (!req->headers) { + free(req); + return NULL; + } + char *request_copy = malloc(length + 1); + if (!request_copy) { + free(req->headers); + free(req); + return NULL; + } + memcpy(request_copy, raw_request, length); + request_copy[length] = '\0'; + char *line_end = strstr(request_copy, "\r\n"); + if (!line_end) { + free(request_copy); + free(req->headers); + free(req); + return NULL; + } + *line_end = '\0'; + char *method = strtok(request_copy, " "); + char *path = strtok(NULL, " "); + char *version = strtok(NULL, " "); + if (!method || !path || !version) { + free(request_copy); + free(req->headers); + free(req); + return NULL; + } + req->method = http_method_from_string(method); + char *query = strchr(path, '?'); + if (query) { + *query = '\0'; + req->query_string = str_duplicate(query + 1); + req->query_params = http_parse_query_string(req->query_string); + } else { + req->query_params = dict_create(16); + } + req->path = url_decode(path); + req->http_version = str_duplicate(version); + char *header_start = line_end + 2; + char *header_end = strstr(header_start, "\r\n\r\n"); + if (header_end) { + *header_end = '\0'; + char *header_line = header_start; + while (*header_line && req->header_count < MAX_HEADERS) { + char *next_line = strstr(header_line, "\r\n"); + if (next_line) *next_line = '\0'; + char *colon = strchr(header_line, ':'); + if (colon) { + *colon = '\0'; + char *value = colon + 1; + while (*value == ' ') value++; + req->headers[req->header_count].key = str_duplicate(header_line); + req->headers[req->header_count].value = str_duplicate(value); + req->header_count++; + } + if (!next_line) break; + header_line = next_line + 2; + } + char *body_start = header_end + 4; + size_t body_offset = body_start - request_copy; + if (body_offset < length) { + req->body_length = length - body_offset; + req->body = malloc(req->body_length + 1); + if (req->body) { + memcpy(req->body, raw_request + body_offset, req->body_length); + req->body[req->body_length] = '\0'; + } + } + } + free(request_copy); + for (size_t i = 0; i < req->header_count; i++) { + if (strcasecmp(req->headers[i].key, "Content-Type") == 0) { + if (strstr(req->headers[i].value, "application/json") && req->body) { + req->json_body = json_parse(req->body); + } + break; + } + } + return req; +} + +dict_t* http_parse_query_string(const char *query) { + dict_t *params = dict_create(16); + if (!query || !params) return params; + char *query_copy = str_duplicate(query); + if (!query_copy) return params; + char *pair = strtok(query_copy, "&"); + while (pair) { + char *equals = strchr(pair, '='); + if (equals) { + *equals = '\0'; + char *key = url_decode(pair); + char *value = url_decode(equals + 1); + if (key && value) { + dict_set(params, key, value); + } + free(key); + } else { + char *key = url_decode(pair); + if (key) { + dict_set(params, key, str_duplicate("")); + } + free(key); + } + pair = strtok(NULL, "&"); + } + free(query_copy); + return params; +} + +void http_request_free(http_request_t *req) { + if (!req) return; + free(req->path); + free(req->query_string); + free(req->http_version); + free(req->body); + for (size_t i = 0; i < req->header_count; i++) { + free(req->headers[i].key); + free(req->headers[i].value); + } + free(req->headers); + if (req->query_params) { + dict_free_with_values(req->query_params, free); + } + if (req->path_params) { + dict_free_with_values(req->path_params, free); + } + if (req->json_body) { + json_value_free(req->json_body); + } + free(req); +} + +http_response_t* http_response_create(int status_code) { + http_response_t *resp = calloc(1, sizeof(http_response_t)); + if (!resp) return NULL; + resp->status_code = status_code; + resp->status_message = str_duplicate(http_status_message(status_code)); + resp->header_capacity = 16; + resp->headers = calloc(resp->header_capacity, sizeof(header_t)); + return resp; +} + +void http_response_set_header(http_response_t *resp, const char *key, const char *value) { + if (!resp || !key || !value) return; + for (size_t i = 0; i < resp->header_count; i++) { + if (strcasecmp(resp->headers[i].key, key) == 0) { + free(resp->headers[i].value); + resp->headers[i].value = str_duplicate(value); + return; + } + } + if (resp->header_count >= resp->header_capacity) { + resp->header_capacity *= 2; + header_t *new_headers = realloc(resp->headers, resp->header_capacity * sizeof(header_t)); + if (!new_headers) return; + resp->headers = new_headers; + } + resp->headers[resp->header_count].key = str_duplicate(key); + resp->headers[resp->header_count].value = str_duplicate(value); + resp->header_count++; +} + +void http_response_set_body(http_response_t *resp, const char *body, size_t length) { + if (!resp) return; + free(resp->body); + if (body && length > 0) { + resp->body = malloc(length + 1); + if (resp->body) { + memcpy(resp->body, body, length); + resp->body[length] = '\0'; + resp->body_length = length; + } + } else { + resp->body = NULL; + resp->body_length = 0; + } +} + +void http_response_set_json(http_response_t *resp, json_value_t *json) { + if (!resp || !json) return; + char *body = json_serialize(json); + if (body) { + http_response_set_header(resp, "Content-Type", "application/json"); + http_response_set_body(resp, body, strlen(body)); + free(body); + } +} + +char* http_serialize_response(http_response_t *response, size_t *out_length) { + if (!response) return NULL; + string_builder_t *sb = sb_create(); + if (!sb) return NULL; + sb_append(sb, "HTTP/1.1 "); + sb_append_int(sb, response->status_code); + sb_append_char(sb, ' '); + sb_append(sb, response->status_message ? response->status_message : "OK"); + sb_append(sb, "\r\n"); + bool has_content_length = false; + for (size_t i = 0; i < response->header_count; i++) { + sb_append(sb, response->headers[i].key); + sb_append(sb, ": "); + sb_append(sb, response->headers[i].value); + sb_append(sb, "\r\n"); + if (strcasecmp(response->headers[i].key, "Content-Length") == 0) { + has_content_length = true; + } + } + if (!has_content_length) { + sb_append(sb, "Content-Length: "); + sb_append_int(sb, (int)response->body_length); + sb_append(sb, "\r\n"); + } + sb_append(sb, "\r\n"); + if (response->body && response->body_length > 0) { + sb_ensure_capacity(sb, response->body_length + 1); + memcpy(sb->data + sb->length, response->body, response->body_length); + sb->length += response->body_length; + sb->data[sb->length] = '\0'; + } + if (out_length) *out_length = sb->length; + char *result = sb->data; + sb->data = NULL; + sb_free(sb); + return result; +} + +void http_response_free(http_response_t *resp) { + if (!resp) return; + free(resp->status_message); + free(resp->body); + for (size_t i = 0; i < resp->header_count; i++) { + free(resp->headers[i].key); + free(resp->headers[i].value); + } + free(resp->headers); + free(resp); +} + +static route_node_t* route_node_create(const char *segment) { + route_node_t *node = calloc(1, sizeof(route_node_t)); + if (!node) return NULL; + if (segment) { + if (segment[0] == '{' && segment[strlen(segment) - 1] == '}') { + node->is_parameter = true; + size_t len = strlen(segment) - 2; + node->param_name = malloc(len + 1); + if (node->param_name) { + memcpy(node->param_name, segment + 1, len); + node->param_name[len] = '\0'; + } + node->segment = str_duplicate("*"); + } else { + node->segment = str_duplicate(segment); + } + } + node->children = calloc(MAX_CHILDREN, sizeof(route_node_t*)); + return node; +} + +static void route_node_free(route_node_t *node) { + if (!node) return; + free(node->segment); + free(node->param_name); + free(node->operation_id); + free(node->summary); + free(node->description); + for (size_t i = 0; i < node->tag_count; i++) { + free(node->tags[i]); + } + free(node->tags); + for (size_t i = 0; i < node->child_count; i++) { + route_node_free(node->children[i]); + } + free(node->children); + if (node->request_schema) { + schema_free(node->request_schema); + } + free(node); +} + +router_t* router_create(void) { + router_t *router = calloc(1, sizeof(router_t)); + if (!router) return NULL; + router->root = route_node_create(NULL); + router->routes = array_create(16); + return router; +} + +void router_add_route(router_t *router, http_method_t method, const char *path, + route_handler_fn handler, field_schema_t *schema) { + if (!router || !path || !handler) return; + size_t segment_count; + char **segments = str_split(path + 1, '/', &segment_count); + if (!segments) return; + route_node_t *current = router->root; + for (size_t i = 0; i < segment_count; i++) { + if (strlen(segments[i]) == 0) continue; + route_node_t *found = NULL; + bool is_param = (segments[i][0] == '{'); + for (size_t j = 0; j < current->child_count; j++) { + if (is_param && current->children[j]->is_parameter) { + found = current->children[j]; + break; + } else if (!is_param && !current->children[j]->is_parameter && + strcmp(current->children[j]->segment, segments[i]) == 0) { + found = current->children[j]; + break; + } + } + if (!found) { + found = route_node_create(segments[i]); + if (current->child_count < MAX_CHILDREN) { + current->children[current->child_count++] = found; + } + } + current = found; + } + current->handlers[method] = handler; + current->request_schema = schema; + route_info_t *info = calloc(1, sizeof(route_info_t)); + if (info) { + info->path = str_duplicate(path); + info->method = method; + info->handler = handler; + info->request_schema = schema; + array_append(router->routes, info); + } + str_split_free(segments, segment_count); + LOG_INFO("Registered route: %s %s", http_method_to_string(method), path); +} + +route_node_t* router_match(router_t *router, http_method_t method, const char *path, + dict_t **path_params) { + if (!router || !path) return NULL; + *path_params = dict_create(8); + if (!*path_params) return NULL; + size_t segment_count; + char **segments = str_split(path + 1, '/', &segment_count); + if (!segments) { + dict_free(*path_params); + *path_params = NULL; + return NULL; + } + route_node_t *current = router->root; + for (size_t i = 0; i < segment_count; i++) { + if (strlen(segments[i]) == 0) continue; + route_node_t *found = NULL; + for (size_t j = 0; j < current->child_count; j++) { + if (!current->children[j]->is_parameter && + strcmp(current->children[j]->segment, segments[i]) == 0) { + found = current->children[j]; + break; + } + } + if (!found) { + for (size_t j = 0; j < current->child_count; j++) { + if (current->children[j]->is_parameter) { + found = current->children[j]; + dict_set(*path_params, found->param_name, str_duplicate(segments[i])); + break; + } + } + } + if (!found) { + str_split_free(segments, segment_count); + dict_free_with_values(*path_params, free); + *path_params = NULL; + return NULL; + } + current = found; + } + str_split_free(segments, segment_count); + if (current->handlers[method]) { + return current; + } + dict_free_with_values(*path_params, free); + *path_params = NULL; + return NULL; +} + +void router_free(router_t *router) { + if (!router) return; + route_node_free(router->root); + for (size_t i = 0; i < router->routes->count; i++) { + route_info_t *info = router->routes->items[i]; + free(info->path); + free(info); + } + array_free(router->routes); + free(router); +} + +field_schema_t* schema_create(const char *name, field_type_t type) { + field_schema_t *schema = calloc(1, sizeof(field_schema_t)); + if (!schema) return NULL; + schema->name = str_duplicate(name); + schema->type = type; + return schema; +} + +void schema_set_required(field_schema_t *schema, bool required) { + if (schema) schema->required = required; +} + +void schema_set_min(field_schema_t *schema, double min) { + if (schema) { + schema->min_value = min; + schema->has_min = true; + } +} + +void schema_set_max(field_schema_t *schema, double max) { + if (schema) { + schema->max_value = max; + schema->has_max = true; + } +} + +void schema_set_pattern(field_schema_t *schema, const char *pattern) { + if (schema) { + free(schema->regex_pattern); + schema->regex_pattern = str_duplicate(pattern); + } +} + +void schema_set_enum(field_schema_t *schema, const char **values, size_t count) { + if (!schema || !values) return; + for (size_t i = 0; i < schema->enum_count; i++) { + free(schema->enum_values[i]); + } + free(schema->enum_values); + schema->enum_values = calloc(count, sizeof(char*)); + if (!schema->enum_values) return; + for (size_t i = 0; i < count; i++) { + schema->enum_values[i] = str_duplicate(values[i]); + } + schema->enum_count = count; +} + +void schema_add_field(field_schema_t *parent, field_schema_t *child) { + if (!parent || !child) return; + size_t new_count = parent->nested_count + 1; + field_schema_t **new_fields = realloc(parent->nested_schema, + new_count * sizeof(field_schema_t*)); + if (!new_fields) return; + parent->nested_schema = new_fields; + parent->nested_schema[parent->nested_count] = child; + parent->nested_count = new_count; +} + +void schema_free(field_schema_t *schema) { + if (!schema) return; + free(schema->name); + free(schema->regex_pattern); + for (size_t i = 0; i < schema->enum_count; i++) { + free(schema->enum_values[i]); + } + free(schema->enum_values); + for (size_t i = 0; i < schema->nested_count; i++) { + schema_free(schema->nested_schema[i]); + } + free(schema->nested_schema); + free(schema); +} + +static validation_error_t* validation_error_create(const char *path, const char *message) { + validation_error_t *err = calloc(1, sizeof(validation_error_t)); + if (!err) return NULL; + err->field_path = str_duplicate(path); + err->error_message = str_duplicate(message); + return err; +} + +static void validation_error_free(validation_error_t *err) { + if (!err) return; + free(err->field_path); + free(err->error_message); + free(err); +} + +static void add_validation_error(validation_result_t *result, const char *path, const char *message) { + size_t new_count = result->error_count + 1; + validation_error_t **new_errors = realloc(result->errors, + new_count * sizeof(validation_error_t*)); + if (!new_errors) return; + result->errors = new_errors; + result->errors[result->error_count] = validation_error_create(path, message); + result->error_count = new_count; + result->valid = false; +} + +static void validate_field(json_value_t *data, field_schema_t *schema, + const char *path, validation_result_t *result) { + if (!data) { + if (schema->required) { + add_validation_error(result, path, "Field is required"); + } + return; + } + switch (schema->type) { + case TYPE_STRING: + if (data->type != JSON_STRING) { + add_validation_error(result, path, "Expected string"); + return; + } + if (schema->has_min && strlen(data->string_value) < (size_t)schema->min_value) { + char msg[128]; + snprintf(msg, sizeof(msg), "String length must be at least %.0f", schema->min_value); + add_validation_error(result, path, msg); + } + if (schema->has_max && strlen(data->string_value) > (size_t)schema->max_value) { + char msg[128]; + snprintf(msg, sizeof(msg), "String length must be at most %.0f", schema->max_value); + add_validation_error(result, path, msg); + } + if (schema->enum_count > 0) { + bool found = false; + for (size_t i = 0; i < schema->enum_count; i++) { + if (strcmp(data->string_value, schema->enum_values[i]) == 0) { + found = true; + break; + } + } + if (!found) { + add_validation_error(result, path, "Value not in allowed values"); + } + } + break; + case TYPE_INT: + case TYPE_FLOAT: + if (data->type != JSON_NUMBER) { + add_validation_error(result, path, "Expected number"); + return; + } + if (schema->type == TYPE_INT && data->number_value != (int)data->number_value) { + add_validation_error(result, path, "Expected integer"); + return; + } + if (schema->has_min && data->number_value < schema->min_value) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value must be at least %.0f", schema->min_value); + add_validation_error(result, path, msg); + } + if (schema->has_max && data->number_value > schema->max_value) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value must be at most %.0f", schema->max_value); + add_validation_error(result, path, msg); + } + break; + case TYPE_BOOL: + if (data->type != JSON_BOOL) { + add_validation_error(result, path, "Expected boolean"); + } + break; + case TYPE_ARRAY: + if (data->type != JSON_ARRAY) { + add_validation_error(result, path, "Expected array"); + return; + } + if (schema->nested_count > 0) { + for (size_t i = 0; i < data->array_value.count; i++) { + char item_path[256]; + snprintf(item_path, sizeof(item_path), "%s[%zu]", path, i); + validate_field(data->array_value.items[i], schema->nested_schema[0], + item_path, result); + } + } + break; + case TYPE_OBJECT: + if (data->type != JSON_OBJECT) { + add_validation_error(result, path, "Expected object"); + return; + } + for (size_t i = 0; i < schema->nested_count; i++) { + char field_path[256]; + snprintf(field_path, sizeof(field_path), "%s.%s", path, schema->nested_schema[i]->name); + json_value_t *field_data = json_object_get(data, schema->nested_schema[i]->name); + validate_field(field_data, schema->nested_schema[i], field_path, result); + } + break; + case TYPE_NULL: + if (data->type != JSON_NULL) { + add_validation_error(result, path, "Expected null"); + } + break; + } +} + +validation_result_t* validate_json(json_value_t *data, field_schema_t *schema) { + validation_result_t *result = calloc(1, sizeof(validation_result_t)); + if (!result) return NULL; + result->valid = true; + if (schema->type == TYPE_OBJECT) { + if (!data || data->type != JSON_OBJECT) { + add_validation_error(result, "", "Expected object"); + return result; + } + for (size_t i = 0; i < schema->nested_count; i++) { + json_value_t *field_data = json_object_get(data, schema->nested_schema[i]->name); + validate_field(field_data, schema->nested_schema[i], schema->nested_schema[i]->name, result); + } + } else { + validate_field(data, schema, schema->name ? schema->name : "", result); + } + if (result->valid) { + result->validated_data = json_deep_copy(data); + } + return result; +} + +char* validation_errors_to_json(validation_result_t *result) { + if (!result) return NULL; + json_value_t *obj = json_object(); + json_value_t *errors = json_array(); + for (size_t i = 0; i < result->error_count; i++) { + json_value_t *err = json_object(); + json_object_set(err, "field", json_string(result->errors[i]->field_path)); + json_object_set(err, "message", json_string(result->errors[i]->error_message)); + json_array_append(errors, err); + } + json_object_set(obj, "detail", errors); + char *str = json_serialize(obj); + json_value_free(obj); + return str; +} + +void validation_result_free(validation_result_t *result) { + if (!result) return; + for (size_t i = 0; i < result->error_count; i++) { + validation_error_free(result->errors[i]); + } + free(result->errors); + if (result->validated_data) { + json_value_free(result->validated_data); + } + free(result); +} + +middleware_pipeline_t* middleware_pipeline_create(void) { + return calloc(1, sizeof(middleware_pipeline_t)); +} + +void middleware_add(middleware_pipeline_t *pipeline, middleware_fn fn, void *ctx) { + if (!pipeline || !fn) return; + middleware_node_t *node = calloc(1, sizeof(middleware_node_t)); + if (!node) return; + node->handler = fn; + node->context = ctx; + if (!pipeline->head) { + pipeline->head = node; + pipeline->tail = node; + } else { + pipeline->tail->next = node; + pipeline->tail = node; + } + pipeline->count++; +} + +typedef struct { + middleware_node_t *current; + http_request_t *request; + route_handler_fn final_handler; + dependency_context_t *deps; +} middleware_context_t; + +static http_response_t* middleware_next(void *ctx) { + middleware_context_t *mw_ctx = ctx; + if (!mw_ctx->current) { + if (mw_ctx->final_handler) { + return mw_ctx->final_handler(mw_ctx->request, mw_ctx->deps); + } + return http_response_create(404); + } + middleware_node_t *current = mw_ctx->current; + mw_ctx->current = mw_ctx->current->next; + return current->handler(mw_ctx->request, middleware_next, mw_ctx, current->context); +} + +http_response_t* middleware_execute(middleware_pipeline_t *pipeline, http_request_t *req, + route_handler_fn final_handler, dependency_context_t *deps) { + middleware_context_t ctx = { + .current = pipeline ? pipeline->head : NULL, + .request = req, + .final_handler = final_handler, + .deps = deps + }; + return middleware_next(&ctx); +} + +void middleware_pipeline_free(middleware_pipeline_t *pipeline) { + if (!pipeline) return; + middleware_node_t *node = pipeline->head; + while (node) { + middleware_node_t *next = node->next; + free(node); + node = next; + } + free(pipeline); +} + +http_response_t* cors_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx) { + (void)mw_ctx; + http_response_t *resp; + if (req->method == HTTP_OPTIONS) { + resp = http_response_create(204); + } else { + resp = next(next_ctx); + } + if (resp) { + http_response_set_header(resp, "Access-Control-Allow-Origin", "*"); + http_response_set_header(resp, "Access-Control-Allow-Methods", + "GET, POST, PUT, DELETE, PATCH, OPTIONS"); + http_response_set_header(resp, "Access-Control-Allow-Headers", + "Content-Type, Authorization"); + } + return resp; +} + +http_response_t* logging_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx) { + (void)mw_ctx; + struct timespec start, end; + clock_gettime(CLOCK_MONOTONIC, &start); + http_response_t *resp = next(next_ctx); + clock_gettime(CLOCK_MONOTONIC, &end); + double duration = (end.tv_sec - start.tv_sec) * 1000.0 + + (end.tv_nsec - start.tv_nsec) / 1000000.0; + LOG_INFO("%s %s %d %.2fms", + http_method_to_string(req->method), + req->path, + resp ? resp->status_code : 0, + duration); + return resp; +} + +http_response_t* timing_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx) { + (void)mw_ctx; + struct timespec start, end; + clock_gettime(CLOCK_MONOTONIC, &start); + http_response_t *resp = next(next_ctx); + clock_gettime(CLOCK_MONOTONIC, &end); + double duration = (end.tv_sec - start.tv_sec) * 1000.0 + + (end.tv_nsec - start.tv_nsec) / 1000000.0; + if (resp) { + char timing[64]; + snprintf(timing, sizeof(timing), "%.3f", duration); + http_response_set_header(resp, "X-Response-Time", timing); + } + (void)req; + return resp; +} + +http_response_t* auth_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx) { + const char *token = mw_ctx; + for (size_t i = 0; i < req->header_count; i++) { + if (strcasecmp(req->headers[i].key, "Authorization") == 0) { + if (str_starts_with(req->headers[i].value, "Bearer ")) { + const char *provided_token = req->headers[i].value + 7; + if (token && strcmp(provided_token, token) == 0) { + return next(next_ctx); + } + } + } + } + http_response_t *resp = http_response_create(401); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Unauthorized")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +dependency_context_t* dep_context_create(http_request_t *req, dict_t *registry) { + dependency_context_t *ctx = calloc(1, sizeof(dependency_context_t)); + if (!ctx) return NULL; + ctx->instances = dict_create(16); + ctx->registry = registry; + ctx->request = req; + ctx->cleanup_list = array_create(8); + return ctx; +} + +void dep_register(dict_t *registry, const char *name, dep_factory_fn factory, + dep_cleanup_fn cleanup, dependency_scope_t scope) { + if (!registry || !name || !factory) return; + dependency_t *dep = calloc(1, sizeof(dependency_t)); + if (!dep) return; + dep->name = str_duplicate(name); + dep->factory = factory; + dep->cleanup = cleanup; + dep->scope = scope; + dict_set(registry, name, dep); +} + +void* dep_resolve(dependency_context_t *ctx, const char *name) { + if (!ctx || !name) return NULL; + void *instance = dict_get(ctx->instances, name); + if (instance) return instance; + dependency_t *dep = dict_get(ctx->registry, name); + if (!dep) return NULL; + if (ctx->is_resolving) { + LOG_ERROR("Circular dependency detected for: %s", name); + return NULL; + } + ctx->is_resolving = true; + instance = dep->factory(ctx); + ctx->is_resolving = false; + if (instance) { + dict_set(ctx->instances, name, instance); + if (dep->cleanup && dep->scope == DEP_REQUEST_SCOPED) { + typedef struct { + void *instance; + dep_cleanup_fn cleanup; + } cleanup_entry_t; + cleanup_entry_t *entry = malloc(sizeof(cleanup_entry_t)); + if (entry) { + entry->instance = instance; + entry->cleanup = dep->cleanup; + array_append(ctx->cleanup_list, entry); + } + } + } + return instance; +} + +void dep_context_cleanup(dependency_context_t *ctx) { + if (!ctx || !ctx->cleanup_list) return; + typedef struct { + void *instance; + dep_cleanup_fn cleanup; + } cleanup_entry_t; + for (size_t i = ctx->cleanup_list->count; i > 0; i--) { + cleanup_entry_t *entry = ctx->cleanup_list->items[i - 1]; + if (entry && entry->cleanup) { + entry->cleanup(entry->instance); + } + free(entry); + } +} + +void dep_context_free(dependency_context_t *ctx) { + if (!ctx) return; + dep_context_cleanup(ctx); + dict_free(ctx->instances); + array_free(ctx->cleanup_list); + free(ctx); +} + +static int set_nonblocking(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + if (flags == -1) return -1; + return fcntl(fd, F_SETFL, flags | O_NONBLOCK); +} + +socket_server_t* socket_server_create(const char *host, int port) { + socket_server_t *server = calloc(1, sizeof(socket_server_t)); + if (!server) return NULL; + server->fd = socket(AF_INET, SOCK_STREAM, 0); + if (server->fd < 0) { + free(server); + return NULL; + } + int opt = 1; + setsockopt(server->fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + server->addr.sin_family = AF_INET; + server->addr.sin_port = htons(port); + if (host && strcmp(host, "0.0.0.0") != 0) { + inet_pton(AF_INET, host, &server->addr.sin_addr); + } else { + server->addr.sin_addr.s_addr = INADDR_ANY; + } + if (bind(server->fd, (struct sockaddr*)&server->addr, sizeof(server->addr)) < 0) { + close(server->fd); + free(server); + return NULL; + } + if (listen(server->fd, SOMAXCONN) < 0) { + close(server->fd); + free(server); + return NULL; + } + set_nonblocking(server->fd); + server->epoll_fd = epoll_create1(0); + if (server->epoll_fd < 0) { + close(server->fd); + free(server); + return NULL; + } + struct epoll_event ev; + ev.events = EPOLLIN; + ev.data.fd = server->fd; + epoll_ctl(server->epoll_fd, EPOLL_CTL_ADD, server->fd, &ev); + server->max_events = MAX_EVENTS; + server->events = calloc(MAX_EVENTS, sizeof(struct epoll_event)); + server->max_connections = MAX_CONNECTIONS; + server->connections = calloc(MAX_CONNECTIONS, sizeof(connection_t*)); + return server; +} + +connection_t* socket_server_accept(socket_server_t *server) { + struct sockaddr_in client_addr; + socklen_t client_len = sizeof(client_addr); + int client_fd = accept(server->fd, (struct sockaddr*)&client_addr, &client_len); + if (client_fd < 0) return NULL; + set_nonblocking(client_fd); + connection_t *conn = calloc(1, sizeof(connection_t)); + if (!conn) { + close(client_fd); + return NULL; + } + conn->fd = client_fd; + conn->buffer_size = BUFFER_SIZE; + conn->request_buffer = malloc(BUFFER_SIZE); + if (!conn->request_buffer) { + close(client_fd); + free(conn); + return NULL; + } + conn->last_activity = time(NULL); + struct epoll_event ev; + ev.events = EPOLLIN | EPOLLET; + ev.data.ptr = conn; + epoll_ctl(server->epoll_fd, EPOLL_CTL_ADD, client_fd, &ev); + if ((size_t)client_fd < server->max_connections) { + server->connections[client_fd] = conn; + } + return conn; +} + +ssize_t socket_read(connection_t *conn) { + if (!conn) return -1; + ssize_t total = 0; + while (1) { + if (conn->buffer_used >= conn->buffer_size - 1) { + size_t new_size = conn->buffer_size * 2; + char *new_buf = realloc(conn->request_buffer, new_size); + if (!new_buf) return -1; + conn->request_buffer = new_buf; + conn->buffer_size = new_size; + } + ssize_t n = read(conn->fd, conn->request_buffer + conn->buffer_used, + conn->buffer_size - conn->buffer_used - 1); + if (n > 0) { + conn->buffer_used += n; + total += n; + conn->request_buffer[conn->buffer_used] = '\0'; + } else if (n == 0) { + return total > 0 ? total : 0; + } else { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + break; + } + return -1; + } + } + conn->last_activity = time(NULL); + return total; +} + +ssize_t socket_write(connection_t *conn, const char *buf, size_t len) { + if (!conn || !buf) return -1; + size_t total = 0; + while (total < len) { + ssize_t n = write(conn->fd, buf + total, len - total); + if (n > 0) { + total += n; + } else if (n == 0) { + break; + } else { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + continue; + } + return -1; + } + } + return total; +} + +void connection_free(connection_t *conn) { + if (!conn) return; + if (conn->ws) { + websocket_free(conn->ws); + } + free(conn->request_buffer); + close(conn->fd); + free(conn); +} + +static bool is_request_complete(const char *buffer, size_t length) { + const char *header_end = strstr(buffer, "\r\n\r\n"); + if (!header_end) return false; + const char *content_length = strcasestr(buffer, "Content-Length:"); + if (!content_length || content_length > header_end) { + return true; + } + size_t cl = atoi(content_length + 15); + size_t header_len = (header_end - buffer) + 4; + return length >= header_len + cl; +} + +static void handle_request(app_context_t *app, connection_t *conn) { + if (!is_request_complete(conn->request_buffer, conn->buffer_used)) { + return; + } + http_request_t *req = http_parse_request(conn->request_buffer, conn->buffer_used); + if (!req) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Bad Request")); + http_response_set_json(resp, body); + json_value_free(body); + size_t resp_len; + char *resp_str = http_serialize_response(resp, &resp_len); + socket_write(conn, resp_str, resp_len); + free(resp_str); + http_response_free(resp); + conn->buffer_used = 0; + return; + } + req->conn = conn; + for (size_t i = 0; i < req->header_count; i++) { + if (strcasecmp(req->headers[i].key, "Upgrade") == 0 && + strcasecmp(req->headers[i].value, "websocket") == 0) { + if (websocket_handshake(conn, req)) { + conn->is_websocket = true; + if (app->ws_handler && conn->ws) { + conn->ws->on_message = app->ws_handler; + conn->ws->user_data = app->ws_user_data; + } + } + http_request_free(req); + conn->buffer_used = 0; + return; + } + } + dict_t *path_params = NULL; + route_node_t *route = router_match(app->router, req->method, req->path, &path_params); + http_response_t *resp = NULL; + if (route) { + req->path_params = path_params; + if (route->request_schema && req->json_body) { + validation_result_t *validation = validate_json(req->json_body, route->request_schema); + if (!validation->valid) { + resp = http_response_create(422); + char *errors = validation_errors_to_json(validation); + http_response_set_header(resp, "Content-Type", "application/json"); + http_response_set_body(resp, errors, strlen(errors)); + free(errors); + validation_result_free(validation); + } else { + validation_result_free(validation); + } + } + if (!resp) { + dependency_context_t *deps = dep_context_create(req, app->dep_registry); + resp = middleware_execute(app->middleware, req, route->handlers[req->method], deps); + dep_context_free(deps); + } + } else { + resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Not Found")); + http_response_set_json(resp, body); + json_value_free(body); + } + if (resp) { + size_t resp_len; + char *resp_str = http_serialize_response(resp, &resp_len); + socket_write(conn, resp_str, resp_len); + free(resp_str); + http_response_free(resp); + } + http_request_free(req); + conn->buffer_used = 0; +} + +static void handle_websocket_data(app_context_t *app, connection_t *conn) { + (void)app; + if (!conn->ws) return; + char *message = NULL; + size_t length = 0; + ws_opcode_t opcode; + int result = websocket_receive(conn->ws, &message, &length, &opcode); + if (result < 0) { + return; + } + switch (opcode) { + case WS_OPCODE_TEXT: + case WS_OPCODE_BINARY: + if (conn->ws->on_message && message) { + conn->ws->on_message(conn->ws, message, length); + } + break; + case WS_OPCODE_PING: + websocket_send_pong(conn->ws, message, length); + break; + case WS_OPCODE_CLOSE: + websocket_close(conn->ws, 1000, "Normal closure"); + break; + default: + break; + } + free(message); +} + +void socket_server_run(socket_server_t *server, app_context_t *app) { + server->running = true; + LOG_INFO("Server listening on http://%s:%d", + inet_ntoa(server->addr.sin_addr), ntohs(server->addr.sin_port)); + LOG_INFO("Press Ctrl+C to stop"); + while (server->running && !g_shutdown_requested) { + int nfds = epoll_wait(server->epoll_fd, server->events, server->max_events, 100); + if (nfds < 0) { + if (errno == EINTR) continue; + break; + } + for (int i = 0; i < nfds; i++) { + if (server->events[i].data.fd == server->fd) { + while (1) { + connection_t *conn = socket_server_accept(server); + if (!conn) break; + } + } else { + connection_t *conn = server->events[i].data.ptr; + if (server->events[i].events & (EPOLLERR | EPOLLHUP)) { + if ((size_t)conn->fd < server->max_connections) { + server->connections[conn->fd] = NULL; + } + epoll_ctl(server->epoll_fd, EPOLL_CTL_DEL, conn->fd, NULL); + connection_free(conn); + continue; + } + if (server->events[i].events & EPOLLIN) { + if (conn->is_websocket) { + ssize_t n = read(conn->fd, conn->ws->receive_buffer + conn->ws->buffer_used, + conn->ws->buffer_size - conn->ws->buffer_used); + if (n > 0) { + conn->ws->buffer_used += n; + handle_websocket_data(app, conn); + } else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) { + if ((size_t)conn->fd < server->max_connections) { + server->connections[conn->fd] = NULL; + } + epoll_ctl(server->epoll_fd, EPOLL_CTL_DEL, conn->fd, NULL); + connection_free(conn); + } + } else { + ssize_t n = socket_read(conn); + if (n > 0) { + handle_request(app, conn); + } else if (n == 0) { + if ((size_t)conn->fd < server->max_connections) { + server->connections[conn->fd] = NULL; + } + epoll_ctl(server->epoll_fd, EPOLL_CTL_DEL, conn->fd, NULL); + connection_free(conn); + } + } + } + } + } + } +} + +void socket_server_stop(socket_server_t *server) { + if (server) { + server->running = false; + } +} + +void socket_server_free(socket_server_t *server) { + if (!server) return; + for (size_t i = 0; i < server->max_connections; i++) { + if (server->connections[i]) { + connection_free(server->connections[i]); + } + } + free(server->connections); + free(server->events); + close(server->epoll_fd); + close(server->fd); + free(server); +} + +bool websocket_handshake(connection_t *conn, http_request_t *req) { + const char *ws_key = NULL; + for (size_t i = 0; i < req->header_count; i++) { + if (strcasecmp(req->headers[i].key, "Sec-WebSocket-Key") == 0) { + ws_key = req->headers[i].value; + break; + } + } + if (!ws_key) return false; + char *accept_key = compute_websocket_accept(ws_key); + if (!accept_key) return false; + string_builder_t *sb = sb_create(); + sb_append(sb, "HTTP/1.1 101 Switching Protocols\r\n"); + sb_append(sb, "Upgrade: websocket\r\n"); + sb_append(sb, "Connection: Upgrade\r\n"); + sb_append(sb, "Sec-WebSocket-Accept: "); + sb_append(sb, accept_key); + sb_append(sb, "\r\n\r\n"); + char *response = sb_to_string(sb); + sb_free(sb); + free(accept_key); + socket_write(conn, response, strlen(response)); + free(response); + conn->ws = websocket_create(conn->fd); + return conn->ws != NULL; +} + +websocket_t* websocket_create(int client_fd) { + websocket_t *ws = calloc(1, sizeof(websocket_t)); + if (!ws) return NULL; + ws->client_fd = client_fd; + ws->is_upgraded = true; + ws->buffer_size = WS_FRAME_MAX_SIZE; + ws->receive_buffer = malloc(WS_FRAME_MAX_SIZE); + if (!ws->receive_buffer) { + free(ws); + return NULL; + } + return ws; +} + +static int websocket_send_frame(websocket_t *ws, ws_opcode_t opcode, + const unsigned char *data, size_t length) { + if (!ws) return -1; + unsigned char header[10]; + size_t header_len = 2; + header[0] = 0x80 | opcode; + if (length <= 125) { + header[1] = length; + } else if (length <= 65535) { + header[1] = 126; + header[2] = (length >> 8) & 0xFF; + header[3] = length & 0xFF; + header_len = 4; + } else { + header[1] = 127; + for (int i = 0; i < 8; i++) { + header[2 + i] = (length >> (56 - i * 8)) & 0xFF; + } + header_len = 10; + } + if (write(ws->client_fd, header, header_len) < 0) return -1; + if (length > 0 && data) { + if (write(ws->client_fd, data, length) < 0) return -1; + } + return 0; +} + +int websocket_send_text(websocket_t *ws, const char *message, size_t length) { + return websocket_send_frame(ws, WS_OPCODE_TEXT, (const unsigned char*)message, length); +} + +int websocket_send_binary(websocket_t *ws, const unsigned char *data, size_t length) { + return websocket_send_frame(ws, WS_OPCODE_BINARY, data, length); +} + +int websocket_receive(websocket_t *ws, char **message, size_t *length, ws_opcode_t *opcode) { + if (!ws || ws->buffer_used < 2) return -1; + unsigned char *buf = (unsigned char*)ws->receive_buffer; + bool fin = (buf[0] & 0x80) != 0; + *opcode = buf[0] & 0x0F; + bool masked = (buf[1] & 0x80) != 0; + uint64_t payload_len = buf[1] & 0x7F; + size_t header_len = 2; + if (payload_len == 126) { + if (ws->buffer_used < 4) return -1; + payload_len = ((uint64_t)buf[2] << 8) | buf[3]; + header_len = 4; + } else if (payload_len == 127) { + if (ws->buffer_used < 10) return -1; + payload_len = 0; + for (int i = 0; i < 8; i++) { + payload_len = (payload_len << 8) | buf[2 + i]; + } + header_len = 10; + } + if (masked) header_len += 4; + if (ws->buffer_used < header_len + payload_len) return -1; + *message = malloc(payload_len + 1); + if (!*message) return -1; + if (masked) { + unsigned char *mask = buf + header_len - 4; + for (uint64_t i = 0; i < payload_len; i++) { + (*message)[i] = buf[header_len + i] ^ mask[i % 4]; + } + } else { + memcpy(*message, buf + header_len, payload_len); + } + (*message)[payload_len] = '\0'; + *length = payload_len; + size_t consumed = header_len + payload_len; + memmove(ws->receive_buffer, ws->receive_buffer + consumed, ws->buffer_used - consumed); + ws->buffer_used -= consumed; + (void)fin; + return 0; +} + +void websocket_send_ping(websocket_t *ws) { + websocket_send_frame(ws, WS_OPCODE_PING, NULL, 0); +} + +void websocket_send_pong(websocket_t *ws, const char *data, size_t length) { + websocket_send_frame(ws, WS_OPCODE_PONG, (const unsigned char*)data, length); +} + +void websocket_close(websocket_t *ws, uint16_t code, const char *reason) { + if (!ws) return; + size_t reason_len = reason ? strlen(reason) : 0; + size_t payload_len = 2 + reason_len; + unsigned char *payload = malloc(payload_len); + if (!payload) return; + payload[0] = (code >> 8) & 0xFF; + payload[1] = code & 0xFF; + if (reason && reason_len > 0) { + memcpy(payload + 2, reason, reason_len); + } + websocket_send_frame(ws, WS_OPCODE_CLOSE, payload, payload_len); + free(payload); +} + +void websocket_free(websocket_t *ws) { + if (!ws) return; + free(ws->receive_buffer); + free(ws); +} + +openapi_generator_t* openapi_create(const char *title, const char *version, const char *description) { + openapi_generator_t *gen = calloc(1, sizeof(openapi_generator_t)); + if (!gen) return NULL; + gen->info.title = str_duplicate(title); + gen->info.version = str_duplicate(version); + gen->info.description = str_duplicate(description); + gen->schemas = dict_create(16); + gen->security_schemes = dict_create(8); + return gen; +} + +static json_value_t* schema_to_json(field_schema_t *schema) { + if (!schema) return NULL; + json_value_t *obj = json_object(); + const char *type_str = NULL; + switch (schema->type) { + case TYPE_STRING: type_str = "string"; break; + case TYPE_INT: type_str = "integer"; break; + case TYPE_FLOAT: type_str = "number"; break; + case TYPE_BOOL: type_str = "boolean"; break; + case TYPE_ARRAY: type_str = "array"; break; + case TYPE_OBJECT: type_str = "object"; break; + case TYPE_NULL: type_str = "null"; break; + } + json_object_set(obj, "type", json_string(type_str)); + if (schema->has_min) { + if (schema->type == TYPE_STRING) { + json_object_set(obj, "minLength", json_number(schema->min_value)); + } else { + json_object_set(obj, "minimum", json_number(schema->min_value)); + } + } + if (schema->has_max) { + if (schema->type == TYPE_STRING) { + json_object_set(obj, "maxLength", json_number(schema->max_value)); + } else { + json_object_set(obj, "maximum", json_number(schema->max_value)); + } + } + if (schema->enum_count > 0) { + json_value_t *enum_arr = json_array(); + for (size_t i = 0; i < schema->enum_count; i++) { + json_array_append(enum_arr, json_string(schema->enum_values[i])); + } + json_object_set(obj, "enum", enum_arr); + } + if (schema->type == TYPE_OBJECT && schema->nested_count > 0) { + json_value_t *properties = json_object(); + json_value_t *required = json_array(); + for (size_t i = 0; i < schema->nested_count; i++) { + json_object_set(properties, schema->nested_schema[i]->name, + schema_to_json(schema->nested_schema[i])); + if (schema->nested_schema[i]->required) { + json_array_append(required, json_string(schema->nested_schema[i]->name)); + } + } + json_object_set(obj, "properties", properties); + if (required->array_value.count > 0) { + json_object_set(obj, "required", required); + } else { + json_value_free(required); + } + } + if (schema->type == TYPE_ARRAY && schema->nested_count > 0) { + json_object_set(obj, "items", schema_to_json(schema->nested_schema[0])); + } + return obj; +} + +char* openapi_generate_schema(openapi_generator_t *gen) { + if (!gen || !gen->router) return NULL; + json_value_t *root = json_object(); + json_object_set(root, "openapi", json_string("3.1.0")); + json_value_t *info = json_object(); + json_object_set(info, "title", json_string(gen->info.title)); + json_object_set(info, "version", json_string(gen->info.version)); + if (gen->info.description) { + json_object_set(info, "description", json_string(gen->info.description)); + } + json_object_set(root, "info", info); + json_value_t *paths = json_object(); + for (size_t i = 0; i < gen->router->routes->count; i++) { + route_info_t *route = gen->router->routes->items[i]; + json_value_t *path_obj = json_object_get(paths, route->path); + if (!path_obj) { + path_obj = json_object(); + json_object_set(paths, route->path, path_obj); + } + json_value_t *method_obj = json_object(); + const char *method_lower = NULL; + switch (route->method) { + case HTTP_GET: method_lower = "get"; break; + case HTTP_POST: method_lower = "post"; break; + case HTTP_PUT: method_lower = "put"; break; + case HTTP_DELETE: method_lower = "delete"; break; + case HTTP_PATCH: method_lower = "patch"; break; + case HTTP_HEAD: method_lower = "head"; break; + case HTTP_OPTIONS: method_lower = "options"; break; + default: method_lower = "get"; break; + } + json_value_t *responses = json_object(); + json_value_t *resp_200 = json_object(); + json_object_set(resp_200, "description", json_string("Successful Response")); + json_object_set(responses, "200", resp_200); + json_object_set(method_obj, "responses", responses); + char *path_copy = str_duplicate(route->path); + json_value_t *parameters = json_array(); + char *p = path_copy; + while (*p) { + if (*p == '{') { + char *end = strchr(p, '}'); + if (end) { + *end = '\0'; + json_value_t *param = json_object(); + json_object_set(param, "name", json_string(p + 1)); + json_object_set(param, "in", json_string("path")); + json_object_set(param, "required", json_bool(true)); + json_value_t *schema_obj = json_object(); + json_object_set(schema_obj, "type", json_string("string")); + json_object_set(param, "schema", schema_obj); + json_array_append(parameters, param); + p = end + 1; + continue; + } + } + p++; + } + free(path_copy); + if (parameters->array_value.count > 0) { + json_object_set(method_obj, "parameters", parameters); + } else { + json_value_free(parameters); + } + if (route->request_schema && + (route->method == HTTP_POST || route->method == HTTP_PUT || route->method == HTTP_PATCH)) { + json_value_t *request_body = json_object(); + json_object_set(request_body, "required", json_bool(true)); + json_value_t *content = json_object(); + json_value_t *json_content = json_object(); + json_object_set(json_content, "schema", schema_to_json(route->request_schema)); + json_object_set(content, "application/json", json_content); + json_object_set(request_body, "content", content); + json_object_set(method_obj, "requestBody", request_body); + } + json_object_set(path_obj, method_lower, method_obj); + } + json_object_set(root, "paths", paths); + char *result = json_serialize(root); + json_value_free(root); + return result; +} + +void openapi_add_schema(openapi_generator_t *gen, const char *name, field_schema_t *schema) { + if (!gen || !name || !schema) return; + dict_set(gen->schemas, name, schema); +} + +void openapi_free(openapi_generator_t *gen) { + if (!gen) return; + free(gen->info.title); + free(gen->info.version); + free(gen->info.description); + free(gen->info.base_url); + dict_free(gen->schemas); + dict_free(gen->security_schemes); + free(gen); +} + +app_context_t* app_create(const char *title, const char *version) { + app_context_t *app = calloc(1, sizeof(app_context_t)); + if (!app) return NULL; + app->router = router_create(); + app->middleware = middleware_pipeline_create(); + app->dep_registry = dict_create(16); + app->openapi = openapi_create(title, version, NULL); + app->config = dict_create(16); + app->openapi->router = app->router; + LOG_INFO("CelerityAPI v%s starting...", CELERITY_VERSION); + return app; +} + +void app_add_route(app_context_t *app, http_method_t method, const char *path, + route_handler_fn handler, field_schema_t *schema) { + if (!app) return; + router_add_route(app->router, method, path, handler, schema); +} + +void app_add_middleware(app_context_t *app, middleware_fn middleware, void *ctx) { + if (!app) return; + middleware_add(app->middleware, middleware, ctx); +} + +void app_add_dependency(app_context_t *app, const char *name, dep_factory_fn factory, + dep_cleanup_fn cleanup, dependency_scope_t scope) { + if (!app) return; + dep_register(app->dep_registry, name, factory, cleanup, scope); +} + +void app_set_websocket_handler(app_context_t *app, ws_message_handler_fn handler, void *user_data) { + if (!app) return; + app->ws_handler = handler; + app->ws_user_data = user_data; +} + +void app_run(app_context_t *app, const char *host, int port) { + if (!app) return; + g_app = app; + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + signal(SIGPIPE, SIG_IGN); + app->server = socket_server_create(host, port); + if (!app->server) { + LOG_ERROR("Failed to create server on %s:%d", host, port); + return; + } + app->running = true; + socket_server_run(app->server, app); +} + +void app_stop(app_context_t *app) { + if (!app) return; + app->running = false; + if (app->server) { + socket_server_stop(app->server); + } +} + +void app_destroy(app_context_t *app) { + if (!app) return; + if (app->server) { + socket_server_free(app->server); + } + router_free(app->router); + middleware_pipeline_free(app->middleware); + size_t key_count; + char **keys = dict_keys(app->dep_registry, &key_count); + for (size_t i = 0; i < key_count; i++) { + dependency_t *dep = dict_get(app->dep_registry, keys[i]); + if (dep) { + free(dep->name); + free(dep); + } + free(keys[i]); + } + free(keys); + dict_free(app->dep_registry); + openapi_free(app->openapi); + dict_free(app->config); + free(app); + g_app = NULL; + LOG_INFO("Server stopped"); +} diff --git a/celerityapi.h b/celerityapi.h new file mode 100644 index 0000000..75fa41b --- /dev/null +++ b/celerityapi.h @@ -0,0 +1,481 @@ +#ifndef CELERITYAPI_H +#define CELERITYAPI_H + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CELERITY_VERSION "1.0.0" +#define MAX_EVENTS 1024 +#define BUFFER_SIZE 65536 +#define MAX_HEADERS 100 +#define MAX_PATH_SEGMENTS 32 +#define MAX_CHILDREN 64 +#define DICT_INITIAL_SIZE 64 +#define MAX_CONNECTIONS 10000 +#define WS_FRAME_MAX_SIZE 65535 +#define SHA1_BLOCK_SIZE 64 +#define SHA1_DIGEST_SIZE 20 + +typedef struct dict_entry dict_entry_t; +typedef struct dict dict_t; +typedef struct array array_t; +typedef struct string_builder string_builder_t; +typedef struct json_value json_value_t; +typedef struct http_request http_request_t; +typedef struct http_response http_response_t; +typedef struct route_node route_node_t; +typedef struct router router_t; +typedef struct dependency dependency_t; +typedef struct dependency_context dependency_context_t; +typedef struct field_schema field_schema_t; +typedef struct validation_error validation_error_t; +typedef struct validation_result validation_result_t; +typedef struct middleware_node middleware_node_t; +typedef struct middleware_pipeline middleware_pipeline_t; +typedef struct connection connection_t; +typedef struct socket_server socket_server_t; +typedef struct websocket websocket_t; +typedef struct openapi_generator openapi_generator_t; +typedef struct app_context app_context_t; + +typedef http_response_t* (*route_handler_fn)(http_request_t *req, dependency_context_t *deps); +typedef http_response_t* (*next_fn)(void *ctx); +typedef http_response_t* (*middleware_fn)(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx); +typedef void* (*dep_factory_fn)(dependency_context_t *ctx); +typedef void (*dep_cleanup_fn)(void *instance); +typedef void (*ws_message_handler_fn)(websocket_t *ws, const char *message, size_t length); + +typedef enum { + HTTP_GET, + HTTP_POST, + HTTP_PUT, + HTTP_DELETE, + HTTP_PATCH, + HTTP_HEAD, + HTTP_OPTIONS, + HTTP_UNKNOWN +} http_method_t; + +typedef enum { + JSON_NULL, + JSON_BOOL, + JSON_NUMBER, + JSON_STRING, + JSON_ARRAY, + JSON_OBJECT +} json_type_t; + +typedef enum { + TYPE_STRING, + TYPE_INT, + TYPE_FLOAT, + TYPE_BOOL, + TYPE_ARRAY, + TYPE_OBJECT, + TYPE_NULL +} field_type_t; + +typedef enum { + DEP_FACTORY, + DEP_SINGLETON, + DEP_REQUEST_SCOPED +} dependency_scope_t; + +typedef enum { + WS_OPCODE_CONTINUATION = 0x0, + WS_OPCODE_TEXT = 0x1, + WS_OPCODE_BINARY = 0x2, + WS_OPCODE_CLOSE = 0x8, + WS_OPCODE_PING = 0x9, + WS_OPCODE_PONG = 0xA +} ws_opcode_t; + +struct dict_entry { + char *key; + void *value; + dict_entry_t *next; +}; + +struct dict { + dict_entry_t **buckets; + size_t size; + size_t count; + pthread_mutex_t lock; +}; + +struct array { + void **items; + size_t count; + size_t capacity; +}; + +struct string_builder { + char *data; + size_t length; + size_t capacity; +}; + +struct json_value { + json_type_t type; + union { + bool bool_value; + double number_value; + char *string_value; + struct { + json_value_t **items; + size_t count; + } array_value; + struct { + char **keys; + json_value_t **values; + size_t count; + } object_value; + }; +}; + +typedef struct { + char *key; + char *value; +} header_t; + +struct http_request { + http_method_t method; + char *path; + char *query_string; + char *http_version; + header_t *headers; + size_t header_count; + char *body; + size_t body_length; + dict_t *query_params; + dict_t *path_params; + json_value_t *json_body; + connection_t *conn; +}; + +struct http_response { + int status_code; + char *status_message; + header_t *headers; + size_t header_count; + size_t header_capacity; + char *body; + size_t body_length; +}; + +struct route_node { + char *segment; + bool is_parameter; + char *param_name; + route_node_t **children; + size_t child_count; + route_handler_fn handlers[8]; + field_schema_t *request_schema; + char *operation_id; + char *summary; + char *description; + char **tags; + size_t tag_count; +}; + +struct router { + route_node_t *root; + array_t *routes; +}; + +struct dependency { + char *name; + dep_factory_fn factory; + dep_cleanup_fn cleanup; + dependency_scope_t scope; + char **dependencies; + size_t dep_count; +}; + +struct dependency_context { + dict_t *instances; + dict_t *registry; + http_request_t *request; + array_t *cleanup_list; + bool is_resolving; +}; + +struct field_schema { + char *name; + field_type_t type; + bool required; + void *default_value; + double min_value; + double max_value; + bool has_min; + bool has_max; + char *regex_pattern; + char **enum_values; + size_t enum_count; + field_schema_t **nested_schema; + size_t nested_count; +}; + +struct validation_error { + char *field_path; + char *error_message; +}; + +struct validation_result { + bool valid; + validation_error_t **errors; + size_t error_count; + json_value_t *validated_data; +}; + +struct middleware_node { + middleware_fn handler; + void *context; + middleware_node_t *next; +}; + +struct middleware_pipeline { + middleware_node_t *head; + middleware_node_t *tail; + size_t count; +}; + +struct connection { + int fd; + char *request_buffer; + size_t buffer_size; + size_t buffer_used; + time_t last_activity; + bool is_websocket; + websocket_t *ws; +}; + +struct socket_server { + int fd; + struct sockaddr_in addr; + int epoll_fd; + struct epoll_event *events; + int max_events; + bool running; + connection_t **connections; + size_t max_connections; +}; + +struct websocket { + int client_fd; + bool is_upgraded; + char *receive_buffer; + size_t buffer_size; + size_t buffer_used; + ws_message_handler_fn on_message; + void *user_data; +}; + +typedef struct { + char *title; + char *version; + char *description; + char *base_url; +} openapi_info_t; + +struct openapi_generator { + openapi_info_t info; + router_t *router; + dict_t *schemas; + dict_t *security_schemes; +}; + +typedef struct { + char *path; + http_method_t method; + route_handler_fn handler; + field_schema_t *request_schema; + char *operation_id; + char *summary; + char *description; + char **tags; + size_t tag_count; +} route_info_t; + +struct app_context { + socket_server_t *server; + router_t *router; + middleware_pipeline_t *middleware; + dict_t *dep_registry; + openapi_generator_t *openapi; + dict_t *config; + ws_message_handler_fn ws_handler; + void *ws_user_data; + volatile bool running; +}; + +dict_t* dict_create(size_t initial_size); +void dict_set(dict_t *dict, const char *key, void *value); +void* dict_get(dict_t *dict, const char *key); +bool dict_has(dict_t *dict, const char *key); +void dict_remove(dict_t *dict, const char *key); +void dict_free(dict_t *dict); +void dict_free_with_values(dict_t *dict, void (*free_fn)(void*)); +char** dict_keys(dict_t *dict, size_t *count); + +array_t* array_create(size_t initial_capacity); +void array_append(array_t *arr, void *item); +void* array_get(array_t *arr, size_t index); +void array_free(array_t *arr); +void array_free_with_items(array_t *arr, void (*free_fn)(void*)); + +string_builder_t* sb_create(void); +void sb_append(string_builder_t *sb, const char *str); +void sb_append_char(string_builder_t *sb, char c); +void sb_append_int(string_builder_t *sb, int value); +void sb_append_double(string_builder_t *sb, double value); +char* sb_to_string(string_builder_t *sb); +void sb_free(string_builder_t *sb); + +char* url_decode(const char *str); +char* url_encode(const char *str); +char* base64_encode(const unsigned char *data, size_t len); +unsigned char* base64_decode(const char *str, size_t *out_len); +char* str_trim(const char *str); +char** str_split(const char *str, char delimiter, size_t *count); +void str_split_free(char **parts, size_t count); +char* str_duplicate(const char *str); +bool str_starts_with(const char *str, const char *prefix); +bool str_ends_with(const char *str, const char *suffix); + +json_value_t* json_parse(const char *json_str); +char* json_serialize(json_value_t *value); +json_value_t* json_object_get(json_value_t *obj, const char *key); +void json_object_set(json_value_t *obj, const char *key, json_value_t *value); +json_value_t* json_array_get(json_value_t *arr, size_t index); +void json_array_append(json_value_t *arr, json_value_t *value); +json_value_t* json_null(void); +json_value_t* json_bool(bool value); +json_value_t* json_number(double value); +json_value_t* json_string(const char *value); +json_value_t* json_array(void); +json_value_t* json_object(void); +void json_value_free(json_value_t *value); +json_value_t* json_deep_copy(json_value_t *value); + +http_request_t* http_parse_request(const char *raw_request, size_t length); +char* http_serialize_response(http_response_t *response, size_t *out_length); +dict_t* http_parse_query_string(const char *query); +http_method_t http_method_from_string(const char *method); +const char* http_method_to_string(http_method_t method); +const char* http_status_message(int status_code); +void http_request_free(http_request_t *req); +http_response_t* http_response_create(int status_code); +void http_response_set_header(http_response_t *resp, const char *key, const char *value); +void http_response_set_body(http_response_t *resp, const char *body, size_t length); +void http_response_set_json(http_response_t *resp, json_value_t *json); +void http_response_free(http_response_t *resp); + +router_t* router_create(void); +void router_add_route(router_t *router, http_method_t method, const char *path, + route_handler_fn handler, field_schema_t *schema); +route_node_t* router_match(router_t *router, http_method_t method, const char *path, + dict_t **path_params); +void router_free(router_t *router); + +field_schema_t* schema_create(const char *name, field_type_t type); +void schema_set_required(field_schema_t *schema, bool required); +void schema_set_min(field_schema_t *schema, double min); +void schema_set_max(field_schema_t *schema, double max); +void schema_set_pattern(field_schema_t *schema, const char *pattern); +void schema_set_enum(field_schema_t *schema, const char **values, size_t count); +void schema_add_field(field_schema_t *parent, field_schema_t *child); +validation_result_t* validate_json(json_value_t *data, field_schema_t *schema); +char* validation_errors_to_json(validation_result_t *result); +void validation_result_free(validation_result_t *result); +void schema_free(field_schema_t *schema); + +middleware_pipeline_t* middleware_pipeline_create(void); +void middleware_add(middleware_pipeline_t *pipeline, middleware_fn fn, void *ctx); +http_response_t* middleware_execute(middleware_pipeline_t *pipeline, http_request_t *req, + route_handler_fn final_handler, dependency_context_t *deps); +void middleware_pipeline_free(middleware_pipeline_t *pipeline); + +http_response_t* cors_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx); +http_response_t* logging_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx); +http_response_t* timing_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx); +http_response_t* auth_middleware(http_request_t *req, next_fn next, void *next_ctx, void *mw_ctx); + +dependency_context_t* dep_context_create(http_request_t *req, dict_t *registry); +void dep_register(dict_t *registry, const char *name, dep_factory_fn factory, + dep_cleanup_fn cleanup, dependency_scope_t scope); +void* dep_resolve(dependency_context_t *ctx, const char *name); +void dep_context_cleanup(dependency_context_t *ctx); +void dep_context_free(dependency_context_t *ctx); + +socket_server_t* socket_server_create(const char *host, int port); +connection_t* socket_server_accept(socket_server_t *server); +ssize_t socket_read(connection_t *conn); +ssize_t socket_write(connection_t *conn, const char *buf, size_t len); +void socket_server_run(socket_server_t *server, app_context_t *app); +void socket_server_stop(socket_server_t *server); +void socket_server_free(socket_server_t *server); +void connection_free(connection_t *conn); + +bool websocket_handshake(connection_t *conn, http_request_t *req); +websocket_t* websocket_create(int client_fd); +int websocket_send_text(websocket_t *ws, const char *message, size_t length); +int websocket_send_binary(websocket_t *ws, const unsigned char *data, size_t length); +int websocket_receive(websocket_t *ws, char **message, size_t *length, ws_opcode_t *opcode); +void websocket_send_ping(websocket_t *ws); +void websocket_send_pong(websocket_t *ws, const char *data, size_t length); +void websocket_close(websocket_t *ws, uint16_t code, const char *reason); +void websocket_free(websocket_t *ws); + +openapi_generator_t* openapi_create(const char *title, const char *version, const char *description); +char* openapi_generate_schema(openapi_generator_t *gen); +void openapi_add_schema(openapi_generator_t *gen, const char *name, field_schema_t *schema); +void openapi_free(openapi_generator_t *gen); + +app_context_t* app_create(const char *title, const char *version); +void app_add_route(app_context_t *app, http_method_t method, const char *path, + route_handler_fn handler, field_schema_t *schema); +void app_add_middleware(app_context_t *app, middleware_fn middleware, void *ctx); +void app_add_dependency(app_context_t *app, const char *name, dep_factory_fn factory, + dep_cleanup_fn cleanup, dependency_scope_t scope); +void app_set_websocket_handler(app_context_t *app, ws_message_handler_fn handler, void *user_data); +void app_run(app_context_t *app, const char *host, int port); +void app_stop(app_context_t *app); +void app_destroy(app_context_t *app); + +void celerity_log(const char *level, const char *format, ...); + +#define LOG_INFO(fmt, ...) celerity_log("INFO", fmt, ##__VA_ARGS__) +#define LOG_ERROR(fmt, ...) celerity_log("ERROR", fmt, ##__VA_ARGS__) +#define LOG_DEBUG(fmt, ...) celerity_log("DEBUG", fmt, ##__VA_ARGS__) +#define LOG_WARN(fmt, ...) celerity_log("WARN", fmt, ##__VA_ARGS__) + +#define ROUTE_GET(app, path, handler) \ + app_add_route(app, HTTP_GET, path, handler, NULL) +#define ROUTE_POST(app, path, handler, schema) \ + app_add_route(app, HTTP_POST, path, handler, schema) +#define ROUTE_PUT(app, path, handler, schema) \ + app_add_route(app, HTTP_PUT, path, handler, schema) +#define ROUTE_DELETE(app, path, handler) \ + app_add_route(app, HTTP_DELETE, path, handler, NULL) +#define ROUTE_PATCH(app, path, handler, schema) \ + app_add_route(app, HTTP_PATCH, path, handler, schema) + +#endif diff --git a/docclient.c b/docclient.c new file mode 100644 index 0000000..68c6703 --- /dev/null +++ b/docclient.c @@ -0,0 +1,922 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BUFFER_SIZE 65536 +#define MAX_RESPONSE_SIZE 1048576 + +typedef struct { + char host[256]; + int port; +} server_config_t; + +typedef struct { + int status_code; + char *body; + size_t body_length; +} http_response_t; + +typedef struct { + int total; + int passed; + int failed; +} test_stats_t; + +static server_config_t g_server = {"127.0.0.1", 8080}; +static test_stats_t g_stats = {0, 0, 0}; +static bool g_verbose = false; + +static double get_time_ms(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0; +} + +static int connect_to_server(void) { + struct addrinfo hints, *result; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + char port_str[16]; + snprintf(port_str, sizeof(port_str), "%d", g_server.port); + if (getaddrinfo(g_server.host, port_str, &hints, &result) != 0) { + return -1; + } + int sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol); + if (sock < 0) { + freeaddrinfo(result); + return -1; + } + struct timeval tv; + tv.tv_sec = 5; + tv.tv_usec = 0; + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + tv.tv_sec = 0; + tv.tv_usec = 500000; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + int opt = 1; + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); + if (connect(sock, result->ai_addr, result->ai_addrlen) < 0) { + close(sock); + freeaddrinfo(result); + return -1; + } + freeaddrinfo(result); + return sock; +} + +static http_response_t* http_request(const char *method, const char *path, + const char *body, size_t body_len) { + int sock = connect_to_server(); + if (sock < 0) return NULL; + char request[BUFFER_SIZE]; + int req_len; + if (body && body_len > 0) { + req_len = snprintf(request, sizeof(request), + "%s %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Content-Type: application/json\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n", + method, path, g_server.host, g_server.port, body_len); + if (req_len + (int)body_len < (int)sizeof(request)) { + memcpy(request + req_len, body, body_len); + req_len += body_len; + } + } else { + req_len = snprintf(request, sizeof(request), + "%s %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Connection: close\r\n" + "\r\n", + method, path, g_server.host, g_server.port); + } + if (send(sock, request, req_len, 0) != req_len) { + close(sock); + return NULL; + } + char *response_buf = malloc(MAX_RESPONSE_SIZE); + if (!response_buf) { + close(sock); + return NULL; + } + size_t total_read = 0; + ssize_t n; + size_t content_length = 0; + char *header_end = NULL; + bool headers_parsed = false; + while ((n = recv(sock, response_buf + total_read, + MAX_RESPONSE_SIZE - total_read - 1, 0)) > 0) { + total_read += n; + response_buf[total_read] = '\0'; + if (!headers_parsed) { + header_end = strstr(response_buf, "\r\n\r\n"); + if (header_end) { + headers_parsed = true; + char *cl = strcasestr(response_buf, "content-length:"); + if (cl && cl < header_end) { + content_length = (size_t)atoi(cl + 15); + } + } + } + if (headers_parsed && header_end) { + size_t body_received = total_read - (header_end + 4 - response_buf); + if (content_length > 0 && body_received >= content_length) break; + if (content_length == 0 && body_received > 0) break; + } + if (total_read >= MAX_RESPONSE_SIZE - 1) break; + } + close(sock); + response_buf[total_read] = '\0'; + http_response_t *resp = calloc(1, sizeof(http_response_t)); + if (!resp) { + free(response_buf); + return NULL; + } + char *status_line = strstr(response_buf, "HTTP/"); + if (status_line) { + char *space = strchr(status_line, ' '); + if (space) resp->status_code = atoi(space + 1); + } + char *body_start = strstr(response_buf, "\r\n\r\n"); + if (body_start) { + body_start += 4; + resp->body_length = total_read - (body_start - response_buf); + resp->body = malloc(resp->body_length + 1); + if (resp->body) { + memcpy(resp->body, body_start, resp->body_length); + resp->body[resp->body_length] = '\0'; + } + } + free(response_buf); + return resp; +} + +static void http_response_free(http_response_t *resp) { + if (!resp) return; + free(resp->body); + free(resp); +} + +static char* json_get_string(const char *json, const char *key) { + char pattern[256]; + snprintf(pattern, sizeof(pattern), "\"%s\":", key); + char *pos = strstr(json, pattern); + if (!pos) return NULL; + pos += strlen(pattern); + while (*pos == ' ' || *pos == '\t') pos++; + if (*pos != '"') return NULL; + pos++; + char *end = strchr(pos, '"'); + if (!end) return NULL; + size_t len = end - pos; + char *result = malloc(len + 1); + if (!result) return NULL; + memcpy(result, pos, len); + result[len] = '\0'; + return result; +} + +static double json_get_number(const char *json, const char *key) { + char pattern[256]; + snprintf(pattern, sizeof(pattern), "\"%s\":", key); + char *pos = strstr(json, pattern); + if (!pos) return 0; + pos += strlen(pattern); + while (*pos == ' ' || *pos == '\t') pos++; + return atof(pos); +} + +static bool json_get_bool(const char *json, const char *key) { + char pattern[256]; + snprintf(pattern, sizeof(pattern), "\"%s\":", key); + char *pos = strstr(json, pattern); + if (!pos) return false; + pos += strlen(pattern); + while (*pos == ' ' || *pos == '\t') pos++; + return strncmp(pos, "true", 4) == 0; +} + +static void print_separator(void) { + printf("════════════════════════════════════════════════════════════════\n"); +} + +static void print_header(const char *title) { + printf("\n"); + print_separator(); + printf(" %s\n", title); + print_separator(); +} + +static void print_test_result(const char *name, bool passed, double duration_ms, + const char *details) { + g_stats.total++; + if (passed) { + g_stats.passed++; + printf(" [PASS] %-40s (%.1fms)\n", name, duration_ms); + } else { + g_stats.failed++; + printf(" [FAIL] %-40s (%.1fms)\n", name, duration_ms); + if (details) printf(" └─ %s\n", details); + } +} + +static bool test_health_check(void) { + double start = get_time_ms(); + http_response_t *resp = http_request("GET", "/health", NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Health Check", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + if (passed && resp->body) { + char *status = json_get_string(resp->body, "status"); + passed = status && strcmp(status, "healthy") == 0; + free(status); + } + print_test_result("Health Check", passed, duration, + passed ? NULL : "Unexpected response"); + http_response_free(resp); + return passed; +} + +static bool test_stats_endpoint(void) { + double start = get_time_ms(); + http_response_t *resp = http_request("GET", "/stats", NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Stats Endpoint", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + print_test_result("Stats Endpoint", passed, duration, + passed ? NULL : "Unexpected status code"); + http_response_free(resp); + return passed; +} + +static bool test_create_collection(const char *name) { + char body[256]; + snprintf(body, sizeof(body), "{\"name\":\"%s\"}", name); + double start = get_time_ms(); + http_response_t *resp = http_request("POST", "/collections", body, strlen(body)); + double duration = get_time_ms() - start; + if (!resp) { + char msg[256]; + snprintf(msg, sizeof(msg), "Create Collection '%s'", name); + print_test_result(msg, false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 201; + char test_name[256]; + snprintf(test_name, sizeof(test_name), "Create Collection '%s'", name); + print_test_result(test_name, passed, duration, + passed ? NULL : "Failed to create collection"); + http_response_free(resp); + return passed; +} + +static bool test_create_collection_duplicate(const char *name) { + char body[256]; + snprintf(body, sizeof(body), "{\"name\":\"%s\"}", name); + double start = get_time_ms(); + http_response_t *resp = http_request("POST", "/collections", body, strlen(body)); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Create Duplicate Collection", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 409; + print_test_result("Create Duplicate Collection (expect 409)", passed, duration, + passed ? NULL : "Should return 409 Conflict"); + http_response_free(resp); + return passed; +} + +static bool test_get_collection(const char *name) { + char path[256]; + snprintf(path, sizeof(path), "/collections/%s", name); + double start = get_time_ms(); + http_response_t *resp = http_request("GET", path, NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Get Collection", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + char test_name[256]; + snprintf(test_name, sizeof(test_name), "Get Collection '%s'", name); + print_test_result(test_name, passed, duration, + passed ? NULL : "Collection not found"); + http_response_free(resp); + return passed; +} + +static bool test_list_collections(void) { + double start = get_time_ms(); + http_response_t *resp = http_request("GET", "/collections", NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("List Collections", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + print_test_result("List Collections", passed, duration, + passed ? NULL : "Failed to list collections"); + if (g_verbose && resp->body) { + printf(" └─ Response: %s\n", resp->body); + } + http_response_free(resp); + return passed; +} + +static char* test_create_document(const char *collection, const char *doc_json) { + char path[256]; + snprintf(path, sizeof(path), "/collections/%s/documents", collection); + double start = get_time_ms(); + http_response_t *resp = http_request("POST", path, doc_json, strlen(doc_json)); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Create Document", false, duration, "Connection failed"); + return NULL; + } + bool passed = resp->status_code == 201; + char *doc_id = NULL; + if (passed && resp->body) { + doc_id = json_get_string(resp->body, "_id"); + } + print_test_result("Create Document", passed, duration, + passed ? NULL : "Failed to create document"); + if (g_verbose && resp->body) { + printf(" └─ Response: %s\n", resp->body); + } + http_response_free(resp); + return doc_id; +} + +static char* test_create_document_with_id(const char *collection, const char *doc_id, + const char *doc_json) { + char path[256]; + snprintf(path, sizeof(path), "/collections/%s/documents", collection); + char body[4096]; + size_t json_len = strlen(doc_json); + if (doc_json[json_len - 1] == '}') { + snprintf(body, sizeof(body), "{\"_id\":\"%s\",%s", doc_id, doc_json + 1); + } else { + snprintf(body, sizeof(body), "%s", doc_json); + } + double start = get_time_ms(); + http_response_t *resp = http_request("POST", path, body, strlen(body)); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Create Document with Custom ID", false, duration, "Connection failed"); + return NULL; + } + bool passed = resp->status_code == 201; + char *returned_id = NULL; + if (passed && resp->body) { + returned_id = json_get_string(resp->body, "_id"); + if (returned_id && strcmp(returned_id, doc_id) != 0) { + passed = false; + free(returned_id); + returned_id = NULL; + } + } + char test_name[256]; + snprintf(test_name, sizeof(test_name), "Create Document with ID '%s'", doc_id); + print_test_result(test_name, passed, duration, + passed ? NULL : "Custom ID not preserved"); + http_response_free(resp); + return returned_id; +} + +static bool test_get_document(const char *collection, const char *doc_id) { + char path[512]; + snprintf(path, sizeof(path), "/collections/%s/documents/%s", collection, doc_id); + double start = get_time_ms(); + http_response_t *resp = http_request("GET", path, NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Get Document", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + char test_name[256]; + snprintf(test_name, sizeof(test_name), "Get Document '%s'", doc_id); + print_test_result(test_name, passed, duration, + passed ? NULL : "Document not found"); + if (g_verbose && resp->body) { + printf(" └─ Response: %s\n", resp->body); + } + http_response_free(resp); + return passed; +} + +static bool test_get_document_not_found(const char *collection) { + char path[512]; + snprintf(path, sizeof(path), "/collections/%s/documents/nonexistent-id-12345", collection); + double start = get_time_ms(); + http_response_t *resp = http_request("GET", path, NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Get Nonexistent Document", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 404; + print_test_result("Get Nonexistent Document (expect 404)", passed, duration, + passed ? NULL : "Should return 404"); + http_response_free(resp); + return passed; +} + +static bool test_update_document(const char *collection, const char *doc_id, + const char *update_json) { + char path[512]; + snprintf(path, sizeof(path), "/collections/%s/documents/%s", collection, doc_id); + double start = get_time_ms(); + http_response_t *resp = http_request("PUT", path, update_json, strlen(update_json)); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Update Document", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + print_test_result("Update Document", passed, duration, + passed ? NULL : "Failed to update document"); + if (g_verbose && resp->body) { + printf(" └─ Response: %s\n", resp->body); + } + http_response_free(resp); + return passed; +} + +static bool test_delete_document(const char *collection, const char *doc_id) { + char path[512]; + snprintf(path, sizeof(path), "/collections/%s/documents/%s", collection, doc_id); + double start = get_time_ms(); + http_response_t *resp = http_request("DELETE", path, NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Delete Document", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + char test_name[256]; + snprintf(test_name, sizeof(test_name), "Delete Document '%s'", doc_id); + print_test_result(test_name, passed, duration, + passed ? NULL : "Failed to delete document"); + http_response_free(resp); + return passed; +} + +static bool test_list_documents(const char *collection) { + char path[256]; + snprintf(path, sizeof(path), "/collections/%s/documents", collection); + double start = get_time_ms(); + http_response_t *resp = http_request("GET", path, NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("List Documents", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + print_test_result("List Documents", passed, duration, + passed ? NULL : "Failed to list documents"); + if (g_verbose && resp->body) { + printf(" └─ Response: %s\n", resp->body); + } + http_response_free(resp); + return passed; +} + +static bool test_list_documents_with_pagination(const char *collection) { + char path[256]; + snprintf(path, sizeof(path), "/collections/%s/documents?skip=0&limit=5", collection); + double start = get_time_ms(); + http_response_t *resp = http_request("GET", path, NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("List Documents with Pagination", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + print_test_result("List Documents with Pagination", passed, duration, + passed ? NULL : "Pagination failed"); + http_response_free(resp); + return passed; +} + +static bool test_query_documents(const char *collection, const char *query_json) { + char path[256]; + snprintf(path, sizeof(path), "/collections/%s/query", collection); + double start = get_time_ms(); + http_response_t *resp = http_request("POST", path, query_json, strlen(query_json)); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Query Documents", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + print_test_result("Query Documents", passed, duration, + passed ? NULL : "Query failed"); + if (g_verbose && resp->body) { + printf(" └─ Response: %s\n", resp->body); + } + http_response_free(resp); + return passed; +} + +static bool test_bulk_insert(const char *collection) { + char path[256]; + snprintf(path, sizeof(path), "/collections/%s/bulk", collection); + const char *body = "{\"documents\":[" + "{\"name\":\"Bulk User 1\",\"email\":\"bulk1@test.com\",\"score\":85}," + "{\"name\":\"Bulk User 2\",\"email\":\"bulk2@test.com\",\"score\":92}," + "{\"name\":\"Bulk User 3\",\"email\":\"bulk3@test.com\",\"score\":78}," + "{\"name\":\"Bulk User 4\",\"email\":\"bulk4@test.com\",\"score\":95}," + "{\"name\":\"Bulk User 5\",\"email\":\"bulk5@test.com\",\"score\":88}" + "]}"; + double start = get_time_ms(); + http_response_t *resp = http_request("POST", path, body, strlen(body)); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Bulk Insert (5 documents)", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 201; + if (passed && resp->body) { + double count = json_get_number(resp->body, "inserted_count"); + passed = count == 5; + } + print_test_result("Bulk Insert (5 documents)", passed, duration, + passed ? NULL : "Not all documents inserted"); + http_response_free(resp); + return passed; +} + +static bool test_bulk_delete(const char *collection) { + char path[256]; + snprintf(path, sizeof(path), "/collections/%s/bulk", collection); + const char *body = "{\"filter\":{\"score\":78}}"; + double start = get_time_ms(); + http_response_t *resp = http_request("DELETE", path, body, strlen(body)); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Bulk Delete by Query", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + print_test_result("Bulk Delete by Query", passed, duration, + passed ? NULL : "Bulk delete failed"); + if (g_verbose && resp->body) { + printf(" └─ Response: %s\n", resp->body); + } + http_response_free(resp); + return passed; +} + +static bool test_delete_collection(const char *name) { + char path[256]; + snprintf(path, sizeof(path), "/collections/%s", name); + double start = get_time_ms(); + http_response_t *resp = http_request("DELETE", path, NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Delete Collection", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + char test_name[256]; + snprintf(test_name, sizeof(test_name), "Delete Collection '%s'", name); + print_test_result(test_name, passed, duration, + passed ? NULL : "Failed to delete collection"); + http_response_free(resp); + return passed; +} + +static bool test_delete_nonexistent_collection(void) { + double start = get_time_ms(); + http_response_t *resp = http_request("DELETE", "/collections/nonexistent_collection_xyz", NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Delete Nonexistent Collection", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 404; + print_test_result("Delete Nonexistent Collection (expect 404)", passed, duration, + passed ? NULL : "Should return 404"); + http_response_free(resp); + return passed; +} + +static void run_basic_tests(void) { + print_header("BASIC CONNECTIVITY TESTS"); + test_health_check(); + test_stats_endpoint(); +} + +static void run_collection_tests(void) { + print_header("COLLECTION MANAGEMENT TESTS"); + test_create_collection("test_users"); + test_create_collection("test_products"); + test_create_collection("test_orders"); + test_create_collection_duplicate("test_users"); + test_get_collection("test_users"); + test_list_collections(); + test_delete_nonexistent_collection(); +} + +static void run_document_crud_tests(void) { + print_header("DOCUMENT CRUD TESTS"); + char *doc1_id = test_create_document("test_users", + "{\"name\":\"Alice Smith\",\"email\":\"alice@example.com\",\"age\":28,\"active\":true}"); + char *doc2_id = test_create_document("test_users", + "{\"name\":\"Bob Johnson\",\"email\":\"bob@example.com\",\"age\":35,\"active\":true}"); + char *doc3_id = test_create_document_with_id("test_users", "custom-id-001", + "{\"name\":\"Charlie Brown\",\"email\":\"charlie@example.com\",\"age\":42,\"active\":false}"); + if (doc1_id) { + test_get_document("test_users", doc1_id); + test_update_document("test_users", doc1_id, "{\"age\":29,\"verified\":true}"); + test_get_document("test_users", doc1_id); + } + test_get_document_not_found("test_users"); + if (doc2_id) { + test_delete_document("test_users", doc2_id); + free(doc2_id); + } + test_list_documents("test_users"); + free(doc1_id); + free(doc3_id); +} + +static void run_query_tests(void) { + print_header("QUERY AND SEARCH TESTS"); + test_create_document("test_products", + "{\"name\":\"Laptop\",\"category\":\"Electronics\",\"price\":999.99,\"in_stock\":true}"); + test_create_document("test_products", + "{\"name\":\"Phone\",\"category\":\"Electronics\",\"price\":699.99,\"in_stock\":true}"); + test_create_document("test_products", + "{\"name\":\"Desk\",\"category\":\"Furniture\",\"price\":299.99,\"in_stock\":false}"); + test_create_document("test_products", + "{\"name\":\"Chair\",\"category\":\"Furniture\",\"price\":149.99,\"in_stock\":true}"); + test_query_documents("test_products", "{\"filter\":{\"category\":\"Electronics\"}}"); + test_query_documents("test_products", "{\"filter\":{\"in_stock\":true}}"); + test_query_documents("test_products", "{\"filter\":{\"category\":\"Furniture\"},\"limit\":1}"); + test_list_documents_with_pagination("test_products"); +} + +static void run_bulk_operation_tests(void) { + print_header("BULK OPERATION TESTS"); + test_bulk_insert("test_orders"); + test_list_documents("test_orders"); + test_bulk_delete("test_orders"); + test_list_documents("test_orders"); +} + +static void run_cleanup_tests(void) { + print_header("CLEANUP TESTS"); + test_delete_collection("test_users"); + test_delete_collection("test_products"); + test_delete_collection("test_orders"); +} + +static void run_stress_tests(void) { + print_header("STRESS TESTS"); + test_create_collection("stress_test"); + double start = get_time_ms(); + int success_count = 0; + int total_count = 100; + for (int i = 0; i < total_count; i++) { + char doc[256]; + snprintf(doc, sizeof(doc), + "{\"index\":%d,\"name\":\"Stress Test Item %d\",\"value\":%d}", + i, i, i * 10); + char path[256]; + snprintf(path, sizeof(path), "/collections/stress_test/documents"); + http_response_t *resp = http_request("POST", path, doc, strlen(doc)); + if (resp && resp->status_code == 201) success_count++; + http_response_free(resp); + } + double duration = get_time_ms() - start; + double rps = (total_count * 1000.0) / duration; + char test_name[256]; + snprintf(test_name, sizeof(test_name), "Insert %d Documents", total_count); + bool passed = success_count == total_count; + g_stats.total++; + if (passed) g_stats.passed++; else g_stats.failed++; + printf(" [%s] %-40s (%.1fms, %.1f req/s)\n", + passed ? "PASS" : "FAIL", test_name, duration, rps); + if (!passed) { + printf(" └─ Only %d/%d succeeded\n", success_count, total_count); + } + start = get_time_ms(); + success_count = 0; + for (int i = 0; i < total_count; i++) { + char path[256]; + snprintf(path, sizeof(path), "/collections/stress_test/documents?skip=%d&limit=10", i); + http_response_t *resp = http_request("GET", path, NULL, 0); + if (resp && resp->status_code == 200) success_count++; + http_response_free(resp); + } + duration = get_time_ms() - start; + rps = (total_count * 1000.0) / duration; + snprintf(test_name, sizeof(test_name), "Read %d Times with Pagination", total_count); + passed = success_count == total_count; + g_stats.total++; + if (passed) g_stats.passed++; else g_stats.failed++; + printf(" [%s] %-40s (%.1fms, %.1f req/s)\n", + passed ? "PASS" : "FAIL", test_name, duration, rps); + test_delete_collection("stress_test"); +} + +static void run_edge_case_tests(void) { + print_header("EDGE CASE TESTS"); + double start = get_time_ms(); + http_response_t *resp = http_request("POST", "/collections", "{}", 2); + double duration = get_time_ms() - start; + bool passed = resp && resp->status_code == 400; + print_test_result("Create Collection without Name", passed, duration, + passed ? NULL : "Should return 400"); + http_response_free(resp); + start = get_time_ms(); + resp = http_request("POST", "/collections", "{\"name\":\"test!@#$\"}", 22); + duration = get_time_ms() - start; + passed = resp && resp->status_code == 400; + print_test_result("Create Collection with Invalid Name", passed, duration, + passed ? NULL : "Should return 400"); + http_response_free(resp); + test_create_collection("edge_cases"); + start = get_time_ms(); + resp = http_request("POST", "/collections/edge_cases/documents", "{}", 2); + duration = get_time_ms() - start; + passed = resp && resp->status_code == 201; + print_test_result("Create Empty Document", passed, duration, + passed ? NULL : "Should allow empty documents"); + http_response_free(resp); + char large_doc[32768]; + int pos = snprintf(large_doc, sizeof(large_doc), "{\"data\":\""); + for (int i = 0; i < 30000 && pos < (int)sizeof(large_doc) - 10; i++) { + large_doc[pos++] = 'A' + (i % 26); + } + pos += snprintf(large_doc + pos, sizeof(large_doc) - pos, "\"}"); + start = get_time_ms(); + resp = http_request("POST", "/collections/edge_cases/documents", large_doc, pos); + duration = get_time_ms() - start; + passed = resp && resp->status_code == 201; + print_test_result("Create Large Document (30KB)", passed, duration, + passed ? NULL : "Large document failed"); + http_response_free(resp); + test_delete_collection("edge_cases"); +} + +static void run_persistence_test(void) { + print_header("PERSISTENCE VERIFICATION"); + test_create_collection("persist_test"); + char *doc_id = test_create_document("persist_test", + "{\"name\":\"Persistent Data\",\"important\":true}"); + printf("\n NOTE: To verify persistence, restart the server and run:\n"); + printf(" ./docclient --test-persistence\n"); + if (doc_id) { + printf(" Document ID to verify: %s\n", doc_id); + free(doc_id); + } +} + +static bool test_persistence_verification(void) { + print_header("PERSISTENCE VERIFICATION TEST"); + double start = get_time_ms(); + http_response_t *resp = http_request("GET", "/collections/persist_test", NULL, 0); + double duration = get_time_ms() - start; + if (!resp) { + print_test_result("Verify Persisted Collection", false, duration, "Connection failed"); + return false; + } + bool passed = resp->status_code == 200; + print_test_result("Verify Persisted Collection Exists", passed, duration, + passed ? NULL : "Collection not found after restart"); + http_response_free(resp); + if (passed) { + start = get_time_ms(); + resp = http_request("GET", "/collections/persist_test/documents", NULL, 0); + duration = get_time_ms() - start; + passed = resp && resp->status_code == 200; + if (passed && resp->body) { + double count = json_get_number(resp->body, "count"); + passed = count >= 1; + } + print_test_result("Verify Persisted Documents", passed, duration, + passed ? NULL : "Documents not found"); + http_response_free(resp); + } + test_delete_collection("persist_test"); + return passed; +} + +static void print_summary(void) { + printf("\n"); + print_separator(); + printf(" TEST SUMMARY\n"); + print_separator(); + printf(" Total Tests: %d\n", g_stats.total); + printf(" Passed: %d (%.1f%%)\n", g_stats.passed, + g_stats.total > 0 ? (g_stats.passed * 100.0 / g_stats.total) : 0); + printf(" Failed: %d (%.1f%%)\n", g_stats.failed, + g_stats.total > 0 ? (g_stats.failed * 100.0 / g_stats.total) : 0); + print_separator(); + if (g_stats.failed == 0) { + printf(" STATUS: ALL TESTS PASSED\n"); + } else { + printf(" STATUS: SOME TESTS FAILED\n"); + } + print_separator(); + printf("\n"); +} + +static void print_usage(const char *program) { + printf("DocDB Test Client\n\n"); + printf("Usage: %s [options]\n\n", program); + printf("Options:\n"); + printf(" -h, --host Server host (default: 127.0.0.1)\n"); + printf(" -p, --port Server port (default: 8080)\n"); + printf(" -v, --verbose Show response bodies\n"); + printf(" --test-persistence Run persistence verification test\n"); + printf(" --stress-only Run only stress tests\n"); + printf(" --quick Run only basic and CRUD tests\n"); + printf(" --help Show this help message\n"); +} + +int main(int argc, char *argv[]) { + bool test_persistence = false; + bool stress_only = false; + bool quick_mode = false; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--host") == 0) { + if (++i < argc) strncpy(g_server.host, argv[i], sizeof(g_server.host) - 1); + } else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) { + if (++i < argc) g_server.port = atoi(argv[i]); + } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) { + g_verbose = true; + } else if (strcmp(argv[i], "--test-persistence") == 0) { + test_persistence = true; + } else if (strcmp(argv[i], "--stress-only") == 0) { + stress_only = true; + } else if (strcmp(argv[i], "--quick") == 0) { + quick_mode = true; + } else if (strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return 0; + } + } + printf("\n"); + print_separator(); + printf(" DocDB Test Client v1.0.0\n"); + printf(" Target: http://%s:%d\n", g_server.host, g_server.port); + print_separator(); + http_response_t *resp = http_request("GET", "/health", NULL, 0); + if (!resp || resp->status_code != 200) { + printf("\n ERROR: Cannot connect to DocDB server at %s:%d\n", + g_server.host, g_server.port); + printf(" Make sure the server is running.\n\n"); + http_response_free(resp); + return 1; + } + http_response_free(resp); + if (test_persistence) { + test_persistence_verification(); + print_summary(); + return g_stats.failed > 0 ? 1 : 0; + } + if (stress_only) { + run_stress_tests(); + print_summary(); + return g_stats.failed > 0 ? 1 : 0; + } + run_basic_tests(); + run_collection_tests(); + run_document_crud_tests(); + if (!quick_mode) { + run_query_tests(); + run_bulk_operation_tests(); + run_edge_case_tests(); + run_stress_tests(); + run_persistence_test(); + } + run_cleanup_tests(); + print_summary(); + return g_stats.failed > 0 ? 1 : 0; +} diff --git a/docserver.c b/docserver.c new file mode 100644 index 0000000..266d444 --- /dev/null +++ b/docserver.c @@ -0,0 +1,1243 @@ +#include "celerityapi.h" +#include +#include +#include + +#define DATA_DIR "./docdb_data" +#define MAX_PATH_LEN 4096 +#define MAX_DOC_SIZE 1048576 +#define MAX_QUERY_RESULTS 1000 +#define INDEX_BUCKET_COUNT 1024 + +typedef struct doc_index_entry { + char *doc_id; + char *file_path; + time_t created_at; + time_t updated_at; + struct doc_index_entry *next; +} doc_index_entry_t; + +typedef struct collection_index { + char *name; + doc_index_entry_t **buckets; + size_t bucket_count; + size_t doc_count; + pthread_rwlock_t lock; + time_t created_at; +} collection_index_t; + +typedef struct { + dict_t *collections; + pthread_mutex_t global_lock; + char *data_dir; +} doc_database_t; + +static doc_database_t *g_db = NULL; + +static unsigned int hash_string(const char *str) { + unsigned int hash = 5381; + int c; + while ((c = *str++)) { + hash = ((hash << 5) + hash) + c; + } + return hash; +} + +static char* generate_uuid(void) { + uuid_t uuid; + char *str = malloc(37); + if (!str) return NULL; + uuid_generate(uuid); + uuid_unparse_lower(uuid, str); + return str; +} + +static int ensure_directory(const char *path) { + struct stat st; + if (stat(path, &st) == 0) { + return S_ISDIR(st.st_mode) ? 0 : -1; + } + return mkdir(path, 0755); +} + +static int file_exists(const char *path) { + struct stat st; + return stat(path, &st) == 0 && S_ISREG(st.st_mode); +} + +static char* read_file_contents(const char *path, size_t *out_size) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + if (size <= 0 || size > MAX_DOC_SIZE) { + fclose(f); + return NULL; + } + char *content = malloc(size + 1); + if (!content) { + fclose(f); + return NULL; + } + size_t read = fread(content, 1, size, f); + fclose(f); + if (read != (size_t)size) { + free(content); + return NULL; + } + content[size] = '\0'; + if (out_size) *out_size = size; + return content; +} + +static int write_file_atomic(const char *path, const char *content, size_t size) { + char temp_path[MAX_PATH_LEN]; + snprintf(temp_path, sizeof(temp_path), "%s.tmp.%d", path, getpid()); + FILE *f = fopen(temp_path, "wb"); + if (!f) return -1; + size_t written = fwrite(content, 1, size, f); + if (fflush(f) != 0 || fsync(fileno(f)) != 0) { + fclose(f); + unlink(temp_path); + return -1; + } + fclose(f); + if (written != size) { + unlink(temp_path); + return -1; + } + if (rename(temp_path, path) != 0) { + unlink(temp_path); + return -1; + } + return 0; +} + +static collection_index_t* collection_index_create(const char *name) { + collection_index_t *idx = calloc(1, sizeof(collection_index_t)); + if (!idx) return NULL; + idx->name = str_duplicate(name); + idx->bucket_count = INDEX_BUCKET_COUNT; + idx->buckets = calloc(INDEX_BUCKET_COUNT, sizeof(doc_index_entry_t*)); + if (!idx->buckets) { + free(idx->name); + free(idx); + return NULL; + } + pthread_rwlock_init(&idx->lock, NULL); + idx->created_at = time(NULL); + return idx; +} + +static void collection_index_free(collection_index_t *idx) { + if (!idx) return; + for (size_t i = 0; i < idx->bucket_count; i++) { + doc_index_entry_t *entry = idx->buckets[i]; + while (entry) { + doc_index_entry_t *next = entry->next; + free(entry->doc_id); + free(entry->file_path); + free(entry); + entry = next; + } + } + pthread_rwlock_destroy(&idx->lock); + free(idx->buckets); + free(idx->name); + free(idx); +} + +static doc_index_entry_t* collection_index_get(collection_index_t *idx, const char *doc_id) { + unsigned int hash = hash_string(doc_id) % idx->bucket_count; + doc_index_entry_t *entry = idx->buckets[hash]; + while (entry) { + if (strcmp(entry->doc_id, doc_id) == 0) return entry; + entry = entry->next; + } + return NULL; +} + +static int collection_index_add(collection_index_t *idx, const char *doc_id, + const char *file_path, time_t created, time_t updated) { + if (collection_index_get(idx, doc_id)) return -1; + doc_index_entry_t *entry = calloc(1, sizeof(doc_index_entry_t)); + if (!entry) return -1; + entry->doc_id = str_duplicate(doc_id); + entry->file_path = str_duplicate(file_path); + entry->created_at = created; + entry->updated_at = updated; + unsigned int hash = hash_string(doc_id) % idx->bucket_count; + entry->next = idx->buckets[hash]; + idx->buckets[hash] = entry; + idx->doc_count++; + return 0; +} + +static int collection_index_remove(collection_index_t *idx, const char *doc_id) { + unsigned int hash = hash_string(doc_id) % idx->bucket_count; + doc_index_entry_t **prev = &idx->buckets[hash]; + doc_index_entry_t *entry = *prev; + while (entry) { + if (strcmp(entry->doc_id, doc_id) == 0) { + *prev = entry->next; + free(entry->doc_id); + free(entry->file_path); + free(entry); + idx->doc_count--; + return 0; + } + prev = &entry->next; + entry = entry->next; + } + return -1; +} + +static int load_collection_from_disk(doc_database_t *db, const char *name) { + char coll_path[MAX_PATH_LEN]; + snprintf(coll_path, sizeof(coll_path), "%s/%s", db->data_dir, name); + DIR *dir = opendir(coll_path); + if (!dir) return -1; + collection_index_t *idx = collection_index_create(name); + if (!idx) { + closedir(dir); + return -1; + } + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + if (ent->d_name[0] == '.') continue; + size_t len = strlen(ent->d_name); + if (len < 6 || strcmp(ent->d_name + len - 5, ".json") != 0) continue; + if (strcmp(ent->d_name, "_meta.json") == 0) continue; + char doc_id[256]; + strncpy(doc_id, ent->d_name, len - 5); + doc_id[len - 5] = '\0'; + char doc_path[MAX_PATH_LEN]; + snprintf(doc_path, sizeof(doc_path), "%s/%s", coll_path, ent->d_name); + struct stat st; + if (stat(doc_path, &st) == 0) { + collection_index_add(idx, doc_id, doc_path, st.st_ctime, st.st_mtime); + } + } + closedir(dir); + char meta_path[MAX_PATH_LEN]; + snprintf(meta_path, sizeof(meta_path), "%s/_meta.json", coll_path); + char *meta_content = read_file_contents(meta_path, NULL); + if (meta_content) { + json_value_t *meta = json_parse(meta_content); + if (meta) { + json_value_t *created = json_object_get(meta, "created_at"); + if (created && created->type == JSON_NUMBER) { + idx->created_at = (time_t)created->number_value; + } + json_value_free(meta); + } + free(meta_content); + } + dict_set(db->collections, name, idx); + return 0; +} + +static int save_collection_meta(doc_database_t *db, collection_index_t *idx) { + char meta_path[MAX_PATH_LEN]; + snprintf(meta_path, sizeof(meta_path), "%s/%s/_meta.json", db->data_dir, idx->name); + json_value_t *meta = json_object(); + json_object_set(meta, "name", json_string(idx->name)); + json_object_set(meta, "created_at", json_number((double)idx->created_at)); + json_object_set(meta, "doc_count", json_number((double)idx->doc_count)); + json_object_set(meta, "updated_at", json_number((double)time(NULL))); + char *content = json_serialize(meta); + json_value_free(meta); + if (!content) return -1; + int ret = write_file_atomic(meta_path, content, strlen(content)); + free(content); + return ret; +} + +static doc_database_t* database_create(const char *data_dir) { + doc_database_t *db = calloc(1, sizeof(doc_database_t)); + if (!db) return NULL; + db->data_dir = str_duplicate(data_dir); + db->collections = dict_create(64); + pthread_mutex_init(&db->global_lock, NULL); + if (ensure_directory(data_dir) != 0) { + free(db->data_dir); + dict_free(db->collections); + free(db); + return NULL; + } + DIR *dir = opendir(data_dir); + if (dir) { + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + if (ent->d_name[0] == '.') continue; + char path[MAX_PATH_LEN]; + snprintf(path, sizeof(path), "%s/%s", data_dir, ent->d_name); + struct stat st; + if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { + load_collection_from_disk(db, ent->d_name); + } + } + closedir(dir); + } + return db; +} + +static void database_free(doc_database_t *db) { + if (!db) return; + size_t count; + char **keys = dict_keys(db->collections, &count); + for (size_t i = 0; i < count; i++) { + collection_index_t *idx = dict_get(db->collections, keys[i]); + collection_index_free(idx); + free(keys[i]); + } + free(keys); + dict_free(db->collections); + pthread_mutex_destroy(&db->global_lock); + free(db->data_dir); + free(db); +} + +static int database_create_collection(doc_database_t *db, const char *name) { + pthread_mutex_lock(&db->global_lock); + if (dict_has(db->collections, name)) { + pthread_mutex_unlock(&db->global_lock); + return -1; + } + char coll_path[MAX_PATH_LEN]; + snprintf(coll_path, sizeof(coll_path), "%s/%s", db->data_dir, name); + if (ensure_directory(coll_path) != 0) { + pthread_mutex_unlock(&db->global_lock); + return -1; + } + collection_index_t *idx = collection_index_create(name); + if (!idx) { + pthread_mutex_unlock(&db->global_lock); + return -1; + } + dict_set(db->collections, name, idx); + save_collection_meta(db, idx); + pthread_mutex_unlock(&db->global_lock); + return 0; +} + +static int database_delete_collection(doc_database_t *db, const char *name) { + pthread_mutex_lock(&db->global_lock); + collection_index_t *idx = dict_get(db->collections, name); + if (!idx) { + pthread_mutex_unlock(&db->global_lock); + return -1; + } + char coll_path[MAX_PATH_LEN]; + snprintf(coll_path, sizeof(coll_path), "%s/%s", db->data_dir, name); + DIR *dir = opendir(coll_path); + if (dir) { + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + if (ent->d_name[0] == '.') continue; + char file_path[MAX_PATH_LEN]; + snprintf(file_path, sizeof(file_path), "%s/%s", coll_path, ent->d_name); + unlink(file_path); + } + closedir(dir); + } + rmdir(coll_path); + dict_remove(db->collections, name); + collection_index_free(idx); + pthread_mutex_unlock(&db->global_lock); + return 0; +} + +static json_value_t* database_insert_document(doc_database_t *db, const char *collection, + json_value_t *doc, const char *custom_id) { + collection_index_t *idx = dict_get(db->collections, collection); + if (!idx) return NULL; + pthread_rwlock_wrlock(&idx->lock); + char *doc_id = custom_id ? str_duplicate(custom_id) : generate_uuid(); + if (!doc_id) { + pthread_rwlock_unlock(&idx->lock); + return NULL; + } + if (collection_index_get(idx, doc_id)) { + free(doc_id); + pthread_rwlock_unlock(&idx->lock); + return NULL; + } + time_t now = time(NULL); + json_value_t *result = json_deep_copy(doc); + json_object_set(result, "_id", json_string(doc_id)); + json_object_set(result, "_created_at", json_number((double)now)); + json_object_set(result, "_updated_at", json_number((double)now)); + char *content = json_serialize(result); + if (!content) { + free(doc_id); + json_value_free(result); + pthread_rwlock_unlock(&idx->lock); + return NULL; + } + char doc_path[MAX_PATH_LEN]; + snprintf(doc_path, sizeof(doc_path), "%s/%s/%s.json", db->data_dir, collection, doc_id); + if (write_file_atomic(doc_path, content, strlen(content)) != 0) { + free(doc_id); + free(content); + json_value_free(result); + pthread_rwlock_unlock(&idx->lock); + return NULL; + } + free(content); + collection_index_add(idx, doc_id, doc_path, now, now); + free(doc_id); + save_collection_meta(db, idx); + pthread_rwlock_unlock(&idx->lock); + return result; +} + +static json_value_t* database_get_document(doc_database_t *db, const char *collection, + const char *doc_id) { + collection_index_t *idx = dict_get(db->collections, collection); + if (!idx) return NULL; + pthread_rwlock_rdlock(&idx->lock); + doc_index_entry_t *entry = collection_index_get(idx, doc_id); + if (!entry) { + pthread_rwlock_unlock(&idx->lock); + return NULL; + } + char *content = read_file_contents(entry->file_path, NULL); + pthread_rwlock_unlock(&idx->lock); + if (!content) return NULL; + json_value_t *doc = json_parse(content); + free(content); + return doc; +} + +static json_value_t* database_update_document(doc_database_t *db, const char *collection, + const char *doc_id, json_value_t *updates) { + collection_index_t *idx = dict_get(db->collections, collection); + if (!idx) return NULL; + pthread_rwlock_wrlock(&idx->lock); + doc_index_entry_t *entry = collection_index_get(idx, doc_id); + if (!entry) { + pthread_rwlock_unlock(&idx->lock); + return NULL; + } + char *content = read_file_contents(entry->file_path, NULL); + if (!content) { + pthread_rwlock_unlock(&idx->lock); + return NULL; + } + json_value_t *doc = json_parse(content); + free(content); + if (!doc) { + pthread_rwlock_unlock(&idx->lock); + return NULL; + } + if (updates && updates->type == JSON_OBJECT) { + for (size_t i = 0; i < updates->object_value.count; i++) { + const char *key = updates->object_value.keys[i]; + if (key[0] == '_') continue; + json_value_t *val = json_deep_copy(updates->object_value.values[i]); + json_object_set(doc, key, val); + } + } + time_t now = time(NULL); + json_object_set(doc, "_updated_at", json_number((double)now)); + char *new_content = json_serialize(doc); + if (!new_content) { + json_value_free(doc); + pthread_rwlock_unlock(&idx->lock); + return NULL; + } + if (write_file_atomic(entry->file_path, new_content, strlen(new_content)) != 0) { + free(new_content); + json_value_free(doc); + pthread_rwlock_unlock(&idx->lock); + return NULL; + } + free(new_content); + entry->updated_at = now; + save_collection_meta(db, idx); + pthread_rwlock_unlock(&idx->lock); + return doc; +} + +static int database_delete_document(doc_database_t *db, const char *collection, + const char *doc_id) { + collection_index_t *idx = dict_get(db->collections, collection); + if (!idx) return -1; + pthread_rwlock_wrlock(&idx->lock); + doc_index_entry_t *entry = collection_index_get(idx, doc_id); + if (!entry) { + pthread_rwlock_unlock(&idx->lock); + return -1; + } + unlink(entry->file_path); + collection_index_remove(idx, doc_id); + save_collection_meta(db, idx); + pthread_rwlock_unlock(&idx->lock); + return 0; +} + +static bool json_value_matches(json_value_t *doc_val, json_value_t *query_val) { + if (!doc_val || !query_val) return false; + if (doc_val->type != query_val->type) { + if (doc_val->type == JSON_NUMBER && query_val->type == JSON_NUMBER) { + return doc_val->number_value == query_val->number_value; + } + return false; + } + switch (doc_val->type) { + case JSON_NULL: + return true; + case JSON_BOOL: + return doc_val->bool_value == query_val->bool_value; + case JSON_NUMBER: + return doc_val->number_value == query_val->number_value; + case JSON_STRING: + return strcmp(doc_val->string_value, query_val->string_value) == 0; + default: + return false; + } +} + +static bool document_matches_query(json_value_t *doc, json_value_t *query) { + if (!query || query->type != JSON_OBJECT) return true; + if (!doc || doc->type != JSON_OBJECT) return false; + for (size_t i = 0; i < query->object_value.count; i++) { + const char *key = query->object_value.keys[i]; + json_value_t *query_val = query->object_value.values[i]; + json_value_t *doc_val = json_object_get(doc, key); + if (!json_value_matches(doc_val, query_val)) return false; + } + return true; +} + +static json_value_t* database_query_documents(doc_database_t *db, const char *collection, + json_value_t *query, int skip, int limit) { + collection_index_t *idx = dict_get(db->collections, collection); + if (!idx) return NULL; + if (limit <= 0) limit = MAX_QUERY_RESULTS; + if (limit > MAX_QUERY_RESULTS) limit = MAX_QUERY_RESULTS; + pthread_rwlock_rdlock(&idx->lock); + json_value_t *results = json_array(); + int skipped = 0; + int added = 0; + for (size_t i = 0; i < idx->bucket_count && added < limit; i++) { + doc_index_entry_t *entry = idx->buckets[i]; + while (entry && added < limit) { + char *content = read_file_contents(entry->file_path, NULL); + if (content) { + json_value_t *doc = json_parse(content); + free(content); + if (doc && document_matches_query(doc, query)) { + if (skipped < skip) { + skipped++; + json_value_free(doc); + } else { + json_array_append(results, doc); + added++; + } + } else if (doc) { + json_value_free(doc); + } + } + entry = entry->next; + } + } + pthread_rwlock_unlock(&idx->lock); + return results; +} + +static json_value_t* database_bulk_insert(doc_database_t *db, const char *collection, + json_value_t *docs) { + if (!docs || docs->type != JSON_ARRAY) return NULL; + json_value_t *results = json_array(); + json_value_t *errors = json_array(); + for (size_t i = 0; i < docs->array_value.count; i++) { + json_value_t *doc = docs->array_value.items[i]; + json_value_t *id_val = json_object_get(doc, "_id"); + const char *custom_id = (id_val && id_val->type == JSON_STRING) ? + id_val->string_value : NULL; + json_value_t *inserted = database_insert_document(db, collection, doc, custom_id); + if (inserted) { + json_value_t *id = json_object_get(inserted, "_id"); + if (id) json_array_append(results, json_deep_copy(id)); + json_value_free(inserted); + } else { + json_value_t *err = json_object(); + json_object_set(err, "index", json_number((double)i)); + json_object_set(err, "error", json_string("Insert failed")); + json_array_append(errors, err); + } + } + json_value_t *response = json_object(); + json_object_set(response, "inserted_ids", results); + json_object_set(response, "errors", errors); + json_object_set(response, "inserted_count", json_number((double)results->array_value.count)); + json_object_set(response, "error_count", json_number((double)errors->array_value.count)); + return response; +} + +static int database_bulk_delete(doc_database_t *db, const char *collection, + json_value_t *query, int *deleted_count) { + collection_index_t *idx = dict_get(db->collections, collection); + if (!idx) return -1; + pthread_rwlock_wrlock(&idx->lock); + array_t *to_delete = array_create(64); + for (size_t i = 0; i < idx->bucket_count; i++) { + doc_index_entry_t *entry = idx->buckets[i]; + while (entry) { + char *content = read_file_contents(entry->file_path, NULL); + if (content) { + json_value_t *doc = json_parse(content); + free(content); + if (doc && document_matches_query(doc, query)) { + array_append(to_delete, str_duplicate(entry->doc_id)); + } + if (doc) json_value_free(doc); + } + entry = entry->next; + } + } + *deleted_count = 0; + for (size_t i = 0; i < to_delete->count; i++) { + char *doc_id = to_delete->items[i]; + doc_index_entry_t *entry = collection_index_get(idx, doc_id); + if (entry) { + unlink(entry->file_path); + collection_index_remove(idx, doc_id); + (*deleted_count)++; + } + free(doc_id); + } + array_free(to_delete); + save_collection_meta(db, idx); + pthread_rwlock_unlock(&idx->lock); + return 0; +} + +static void* db_factory(dependency_context_t *ctx) { + (void)ctx; + return g_db; +} + +static http_response_t* handle_list_collections(http_request_t *req, dependency_context_t *deps) { + (void)req; + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + pthread_mutex_lock(&db->global_lock); + json_value_t *collections = json_array(); + size_t count; + char **keys = dict_keys(db->collections, &count); + for (size_t i = 0; i < count; i++) { + collection_index_t *idx = dict_get(db->collections, keys[i]); + json_value_t *coll = json_object(); + json_object_set(coll, "name", json_string(keys[i])); + json_object_set(coll, "doc_count", json_number((double)idx->doc_count)); + json_object_set(coll, "created_at", json_number((double)idx->created_at)); + json_array_append(collections, coll); + free(keys[i]); + } + free(keys); + pthread_mutex_unlock(&db->global_lock); + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "collections", collections); + json_object_set(body, "count", json_number((double)count)); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_create_collection(http_request_t *req, dependency_context_t *deps) { + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + if (!req->json_body) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Request body required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + json_value_t *name_val = json_object_get(req->json_body, "name"); + if (!name_val || name_val->type != JSON_STRING) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection name required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + const char *name = name_val->string_value; + for (size_t i = 0; name[i]; i++) { + char c = name[i]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-')) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Invalid collection name")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + } + if (database_create_collection(db, name) != 0) { + http_response_t *resp = http_response_create(409); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection already exists")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(201); + json_value_t *body = json_object(); + json_object_set(body, "name", json_string(name)); + json_object_set(body, "created", json_bool(true)); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_get_collection(http_request_t *req, dependency_context_t *deps) { + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *name = dict_get(req->path_params, "collection"); + if (!name) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection name required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + pthread_mutex_lock(&db->global_lock); + collection_index_t *idx = dict_get(db->collections, name); + if (!idx) { + pthread_mutex_unlock(&db->global_lock); + http_response_t *resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection not found")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "name", json_string(idx->name)); + json_object_set(body, "doc_count", json_number((double)idx->doc_count)); + json_object_set(body, "created_at", json_number((double)idx->created_at)); + pthread_mutex_unlock(&db->global_lock); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_delete_collection(http_request_t *req, dependency_context_t *deps) { + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *name = dict_get(req->path_params, "collection"); + if (!name) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection name required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + if (database_delete_collection(db, name) != 0) { + http_response_t *resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection not found")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "deleted", json_bool(true)); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_list_documents(http_request_t *req, dependency_context_t *deps) { + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *collection = dict_get(req->path_params, "collection"); + if (!collection) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection name required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + int skip = 0; + int limit = 100; + if (req->query_params) { + char *skip_str = dict_get(req->query_params, "skip"); + char *limit_str = dict_get(req->query_params, "limit"); + if (skip_str) skip = atoi(skip_str); + if (limit_str) limit = atoi(limit_str); + } + json_value_t *docs = database_query_documents(db, collection, NULL, skip, limit); + if (!docs) { + http_response_t *resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection not found")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "documents", docs); + json_object_set(body, "count", json_number((double)docs->array_value.count)); + json_object_set(body, "skip", json_number((double)skip)); + json_object_set(body, "limit", json_number((double)limit)); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_create_document(http_request_t *req, dependency_context_t *deps) { + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *collection = dict_get(req->path_params, "collection"); + if (!collection) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection name required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + if (!req->json_body) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Document body required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + json_value_t *id_val = json_object_get(req->json_body, "_id"); + const char *custom_id = (id_val && id_val->type == JSON_STRING) ? + id_val->string_value : NULL; + json_value_t *inserted = database_insert_document(db, collection, req->json_body, custom_id); + if (!inserted) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Failed to insert document")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(201); + http_response_set_json(resp, inserted); + json_value_free(inserted); + return resp; +} + +static http_response_t* handle_get_document(http_request_t *req, dependency_context_t *deps) { + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *collection = dict_get(req->path_params, "collection"); + char *doc_id = dict_get(req->path_params, "id"); + if (!collection || !doc_id) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection and document ID required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + json_value_t *doc = database_get_document(db, collection, doc_id); + if (!doc) { + http_response_t *resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Document not found")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(200); + http_response_set_json(resp, doc); + json_value_free(doc); + return resp; +} + +static http_response_t* handle_update_document(http_request_t *req, dependency_context_t *deps) { + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *collection = dict_get(req->path_params, "collection"); + char *doc_id = dict_get(req->path_params, "id"); + if (!collection || !doc_id) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection and document ID required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + if (!req->json_body) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Update body required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + json_value_t *updated = database_update_document(db, collection, doc_id, req->json_body); + if (!updated) { + http_response_t *resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Document not found")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(200); + http_response_set_json(resp, updated); + json_value_free(updated); + return resp; +} + +static http_response_t* handle_delete_document(http_request_t *req, dependency_context_t *deps) { + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *collection = dict_get(req->path_params, "collection"); + char *doc_id = dict_get(req->path_params, "id"); + if (!collection || !doc_id) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection and document ID required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + if (database_delete_document(db, collection, doc_id) != 0) { + http_response_t *resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Document not found")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "deleted", json_bool(true)); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_query_documents(http_request_t *req, dependency_context_t *deps) { + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *collection = dict_get(req->path_params, "collection"); + if (!collection) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection name required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + json_value_t *query = NULL; + int skip = 0; + int limit = 100; + if (req->json_body) { + query = json_object_get(req->json_body, "filter"); + json_value_t *skip_val = json_object_get(req->json_body, "skip"); + json_value_t *limit_val = json_object_get(req->json_body, "limit"); + if (skip_val && skip_val->type == JSON_NUMBER) skip = (int)skip_val->number_value; + if (limit_val && limit_val->type == JSON_NUMBER) limit = (int)limit_val->number_value; + } + json_value_t *docs = database_query_documents(db, collection, query, skip, limit); + if (!docs) { + http_response_t *resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection not found")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "documents", docs); + json_object_set(body, "count", json_number((double)docs->array_value.count)); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_bulk_insert(http_request_t *req, dependency_context_t *deps) { + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *collection = dict_get(req->path_params, "collection"); + if (!collection) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection name required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + if (!req->json_body) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Documents array required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + json_value_t *docs = json_object_get(req->json_body, "documents"); + if (!docs || docs->type != JSON_ARRAY) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Documents must be an array")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + json_value_t *result = database_bulk_insert(db, collection, docs); + if (!result) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Bulk insert failed")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(201); + http_response_set_json(resp, result); + json_value_free(result); + return resp; +} + +static http_response_t* handle_bulk_delete(http_request_t *req, dependency_context_t *deps) { + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *collection = dict_get(req->path_params, "collection"); + if (!collection) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection name required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + json_value_t *query = NULL; + if (req->json_body) { + query = json_object_get(req->json_body, "filter"); + } + int deleted_count = 0; + if (database_bulk_delete(db, collection, query, &deleted_count) != 0) { + http_response_t *resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Collection not found")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "deleted_count", json_number((double)deleted_count)); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_health(http_request_t *req, dependency_context_t *deps) { + (void)req; + doc_database_t *db = dep_resolve(deps, "database"); + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "status", json_string("healthy")); + json_object_set(body, "service", json_string("DocDB")); + json_object_set(body, "version", json_string("1.0.0")); + json_object_set(body, "timestamp", json_number((double)time(NULL))); + if (db) { + size_t count; + char **keys = dict_keys(db->collections, &count); + json_object_set(body, "collection_count", json_number((double)count)); + for (size_t i = 0; i < count; i++) free(keys[i]); + free(keys); + } + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_stats(http_request_t *req, dependency_context_t *deps) { + (void)req; + doc_database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "error", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + pthread_mutex_lock(&db->global_lock); + json_value_t *stats = json_object(); + size_t total_docs = 0; + size_t count; + char **keys = dict_keys(db->collections, &count); + json_value_t *collections = json_array(); + for (size_t i = 0; i < count; i++) { + collection_index_t *idx = dict_get(db->collections, keys[i]); + total_docs += idx->doc_count; + json_value_t *coll = json_object(); + json_object_set(coll, "name", json_string(keys[i])); + json_object_set(coll, "doc_count", json_number((double)idx->doc_count)); + json_array_append(collections, coll); + free(keys[i]); + } + free(keys); + pthread_mutex_unlock(&db->global_lock); + json_object_set(stats, "collection_count", json_number((double)count)); + json_object_set(stats, "total_documents", json_number((double)total_docs)); + json_object_set(stats, "data_directory", json_string(db->data_dir)); + json_object_set(stats, "collections", collections); + http_response_t *resp = http_response_create(200); + http_response_set_json(resp, stats); + json_value_free(stats); + return resp; +} + +int main(int argc, char *argv[]) { + int port = 8080; + const char *host = "127.0.0.1"; + const char *data_dir = DATA_DIR; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) { + port = atoi(argv[++i]); + } else if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) { + host = argv[++i]; + } else if (strcmp(argv[i], "--data") == 0 && i + 1 < argc) { + data_dir = argv[++i]; + } + } + g_db = database_create(data_dir); + if (!g_db) { + LOG_ERROR("Failed to initialize database"); + return 1; + } + app_context_t *app = app_create("DocDB", "1.0.0"); + if (!app) { + LOG_ERROR("Failed to create application"); + database_free(g_db); + return 1; + } + app_add_dependency(app, "database", db_factory, NULL, DEP_SINGLETON); + app_add_middleware(app, logging_middleware, NULL); + app_add_middleware(app, timing_middleware, NULL); + app_add_middleware(app, cors_middleware, NULL); + ROUTE_GET(app, "/health", handle_health); + ROUTE_GET(app, "/stats", handle_stats); + ROUTE_GET(app, "/collections", handle_list_collections); + ROUTE_POST(app, "/collections", handle_create_collection, NULL); + ROUTE_GET(app, "/collections/{collection}", handle_get_collection); + ROUTE_DELETE(app, "/collections/{collection}", handle_delete_collection); + ROUTE_GET(app, "/collections/{collection}/documents", handle_list_documents); + ROUTE_POST(app, "/collections/{collection}/documents", handle_create_document, NULL); + ROUTE_GET(app, "/collections/{collection}/documents/{id}", handle_get_document); + ROUTE_PUT(app, "/collections/{collection}/documents/{id}", handle_update_document, NULL); + ROUTE_DELETE(app, "/collections/{collection}/documents/{id}", handle_delete_document); + app_add_route(app, HTTP_POST, "/collections/{collection}/query", handle_query_documents, NULL); + app_add_route(app, HTTP_POST, "/collections/{collection}/bulk", handle_bulk_insert, NULL); + app_add_route(app, HTTP_DELETE, "/collections/{collection}/bulk", handle_bulk_delete, NULL); + LOG_INFO("DocDB starting on http://%s:%d", host, port); + LOG_INFO("Data directory: %s", data_dir); + app_run(app, host, port); + app_destroy(app); + database_free(g_db); + return 0; +} diff --git a/example.c b/example.c new file mode 100644 index 0000000..cc4f8af --- /dev/null +++ b/example.c @@ -0,0 +1,392 @@ +#include "celerityapi.h" + +typedef struct { + int next_id; + dict_t *users; +} database_t; + +typedef struct { + int id; + char *name; + char *email; + int age; +} user_t; + +static database_t *g_database = NULL; + +static void user_free(void *ptr) { + user_t *user = ptr; + if (!user) return; + free(user->name); + free(user->email); + free(user); +} + +static void* database_factory(dependency_context_t *ctx) { + (void)ctx; + if (g_database) return g_database; + g_database = calloc(1, sizeof(database_t)); + if (!g_database) return NULL; + g_database->next_id = 1; + g_database->users = dict_create(64); + return g_database; +} + +static void database_cleanup(void *instance) { + database_t *db = instance; + if (!db) return; + dict_free_with_values(db->users, user_free); + free(db); + g_database = NULL; +} + +static http_response_t* handle_root(http_request_t *req, dependency_context_t *deps) { + (void)req; + (void)deps; + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "message", json_string("Welcome to CelerityAPI")); + json_object_set(body, "version", json_string(CELERITY_VERSION)); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_health(http_request_t *req, dependency_context_t *deps) { + (void)req; + (void)deps; + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "status", json_string("healthy")); + json_object_set(body, "timestamp", json_number((double)time(NULL))); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_get_user(http_request_t *req, dependency_context_t *deps) { + database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *id_str = dict_get(req->path_params, "id"); + if (!id_str) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Missing user ID")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + user_t *user = dict_get(db->users, id_str); + if (!user) { + http_response_t *resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("User not found")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "id", json_number(user->id)); + json_object_set(body, "name", json_string(user->name)); + json_object_set(body, "email", json_string(user->email)); + json_object_set(body, "age", json_number(user->age)); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_list_users(http_request_t *req, dependency_context_t *deps) { + (void)req; + database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(200); + json_value_t *users_array = json_array(); + size_t key_count; + char **keys = dict_keys(db->users, &key_count); + for (size_t i = 0; i < key_count; i++) { + user_t *user = dict_get(db->users, keys[i]); + if (user) { + json_value_t *user_obj = json_object(); + json_object_set(user_obj, "id", json_number(user->id)); + json_object_set(user_obj, "name", json_string(user->name)); + json_object_set(user_obj, "email", json_string(user->email)); + json_object_set(user_obj, "age", json_number(user->age)); + json_array_append(users_array, user_obj); + } + free(keys[i]); + } + free(keys); + http_response_set_json(resp, users_array); + json_value_free(users_array); + return resp; +} + +static http_response_t* handle_create_user(http_request_t *req, dependency_context_t *deps) { + database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + if (!req->json_body) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Request body required")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + json_value_t *name_val = json_object_get(req->json_body, "name"); + json_value_t *email_val = json_object_get(req->json_body, "email"); + json_value_t *age_val = json_object_get(req->json_body, "age"); + user_t *user = calloc(1, sizeof(user_t)); + user->id = db->next_id++; + user->name = str_duplicate(name_val && name_val->type == JSON_STRING ? + name_val->string_value : "Unknown"); + user->email = str_duplicate(email_val && email_val->type == JSON_STRING ? + email_val->string_value : "unknown@example.com"); + user->age = age_val && age_val->type == JSON_NUMBER ? (int)age_val->number_value : 0; + char id_str[32]; + snprintf(id_str, sizeof(id_str), "%d", user->id); + dict_set(db->users, id_str, user); + http_response_t *resp = http_response_create(201); + json_value_t *body = json_object(); + json_object_set(body, "id", json_number(user->id)); + json_object_set(body, "name", json_string(user->name)); + json_object_set(body, "email", json_string(user->email)); + json_object_set(body, "age", json_number(user->age)); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_update_user(http_request_t *req, dependency_context_t *deps) { + database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *id_str = dict_get(req->path_params, "id"); + if (!id_str) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Missing user ID")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + user_t *user = dict_get(db->users, id_str); + if (!user) { + http_response_t *resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("User not found")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + if (req->json_body) { + json_value_t *name_val = json_object_get(req->json_body, "name"); + json_value_t *email_val = json_object_get(req->json_body, "email"); + json_value_t *age_val = json_object_get(req->json_body, "age"); + if (name_val && name_val->type == JSON_STRING) { + free(user->name); + user->name = str_duplicate(name_val->string_value); + } + if (email_val && email_val->type == JSON_STRING) { + free(user->email); + user->email = str_duplicate(email_val->string_value); + } + if (age_val && age_val->type == JSON_NUMBER) { + user->age = (int)age_val->number_value; + } + } + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "id", json_number(user->id)); + json_object_set(body, "name", json_string(user->name)); + json_object_set(body, "email", json_string(user->email)); + json_object_set(body, "age", json_number(user->age)); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static http_response_t* handle_delete_user(http_request_t *req, dependency_context_t *deps) { + database_t *db = dep_resolve(deps, "database"); + if (!db) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Database unavailable")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *id_str = dict_get(req->path_params, "id"); + if (!id_str) { + http_response_t *resp = http_response_create(400); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Missing user ID")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + user_t *user = dict_get(db->users, id_str); + if (!user) { + http_response_t *resp = http_response_create(404); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("User not found")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + user_free(user); + dict_remove(db->users, id_str); + http_response_t *resp = http_response_create(204); + return resp; +} + +static http_response_t* handle_openapi(http_request_t *req, dependency_context_t *deps) { + (void)req; + (void)deps; + return NULL; +} + +static http_response_t* handle_ws_upgrade(http_request_t *req, dependency_context_t *deps) { + (void)req; + (void)deps; + http_response_t *resp = http_response_create(200); + json_value_t *body = json_object(); + json_object_set(body, "message", json_string("WebSocket endpoint - use ws:// protocol")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; +} + +static void ws_echo_handler(websocket_t *ws, const char *message, size_t length) { + LOG_INFO("WebSocket received: %.*s", (int)length, message); + json_value_t *response = json_object(); + json_object_set(response, "type", json_string("echo")); + json_object_set(response, "message", json_string(message)); + json_object_set(response, "timestamp", json_number((double)time(NULL))); + char *json_str = json_serialize(response); + json_value_free(response); + if (json_str) { + websocket_send_text(ws, json_str, strlen(json_str)); + free(json_str); + } +} + +static app_context_t *g_app_ref = NULL; + +static http_response_t* handle_openapi_actual(http_request_t *req, dependency_context_t *deps) { + (void)req; + (void)deps; + if (!g_app_ref || !g_app_ref->openapi) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("OpenAPI not available")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + char *schema = openapi_generate_schema(g_app_ref->openapi); + if (!schema) { + http_response_t *resp = http_response_create(500); + json_value_t *body = json_object(); + json_object_set(body, "detail", json_string("Failed to generate schema")); + http_response_set_json(resp, body); + json_value_free(body); + return resp; + } + http_response_t *resp = http_response_create(200); + http_response_set_header(resp, "Content-Type", "application/json"); + http_response_set_body(resp, schema, strlen(schema)); + free(schema); + return resp; +} + +int main(int argc, char *argv[]) { + int port = 8000; + const char *host = "127.0.0.1"; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) { + port = atoi(argv[++i]); + } else if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) { + host = argv[++i]; + } + } + + app_context_t *app = app_create("CelerityAPI Example", "1.0.0"); + if (!app) { + LOG_ERROR("Failed to create application"); + return 1; + } + g_app_ref = app; + + app_add_dependency(app, "database", database_factory, database_cleanup, DEP_SINGLETON); + + app_add_middleware(app, logging_middleware, NULL); + app_add_middleware(app, timing_middleware, NULL); + app_add_middleware(app, cors_middleware, NULL); + + field_schema_t *user_schema = schema_create("user", TYPE_OBJECT); + + field_schema_t *name_field = schema_create("name", TYPE_STRING); + schema_set_required(name_field, true); + schema_set_min(name_field, 3); + schema_set_max(name_field, 100); + schema_add_field(user_schema, name_field); + + field_schema_t *email_field = schema_create("email", TYPE_STRING); + schema_set_required(email_field, true); + schema_add_field(user_schema, email_field); + + field_schema_t *age_field = schema_create("age", TYPE_INT); + schema_set_required(age_field, false); + schema_set_min(age_field, 0); + schema_set_max(age_field, 150); + schema_add_field(user_schema, age_field); + + ROUTE_GET(app, "/", handle_root); + ROUTE_GET(app, "/health", handle_health); + ROUTE_GET(app, "/users", handle_list_users); + ROUTE_GET(app, "/users/{id}", handle_get_user); + ROUTE_POST(app, "/users", handle_create_user, user_schema); + ROUTE_PUT(app, "/users/{id}", handle_update_user, NULL); + ROUTE_DELETE(app, "/users/{id}", handle_delete_user); + ROUTE_GET(app, "/ws", handle_ws_upgrade); + ROUTE_GET(app, "/openapi.json", handle_openapi_actual); + + app_set_websocket_handler(app, ws_echo_handler, NULL); + + (void)handle_openapi; + + app_run(app, host, port); + + app_destroy(app); + + return 0; +} diff --git a/loadtest.c b/loadtest.c new file mode 100644 index 0000000..6dd5ce5 --- /dev/null +++ b/loadtest.c @@ -0,0 +1,527 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_URL_LENGTH 2048 +#define MAX_BODY_LENGTH 65536 +#define BUFFER_SIZE 8192 +#define MAX_LATENCIES 1000000 + +typedef struct { + char host[256]; + int port; + char path[1024]; +} parsed_url_t; + +typedef struct { + atomic_uint_fast64_t total_requests; + atomic_uint_fast64_t successful_requests; + atomic_uint_fast64_t failed_requests; + atomic_uint_fast64_t bytes_received; + atomic_uint_fast64_t connect_errors; + atomic_uint_fast64_t timeout_errors; + atomic_uint_fast64_t read_errors; + atomic_uint_fast64_t status_2xx; + atomic_uint_fast64_t status_3xx; + atomic_uint_fast64_t status_4xx; + atomic_uint_fast64_t status_5xx; + double *latencies; + atomic_uint_fast64_t latency_count; + pthread_mutex_t latency_lock; +} stats_t; + +typedef struct { + int thread_id; + parsed_url_t *url; + char *method; + char *body; + char *headers; + int duration_seconds; + int timeout_ms; + stats_t *stats; + volatile bool *running; +} worker_config_t; + +static volatile bool g_running = true; +static stats_t g_stats; + +static void signal_handler(int sig) { + (void)sig; + g_running = false; +} + +static double get_time_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; +} + +static int parse_url(const char *url, parsed_url_t *parsed) { + memset(parsed, 0, sizeof(parsed_url_t)); + parsed->port = 80; + strcpy(parsed->path, "/"); + const char *start = url; + if (strncmp(url, "http://", 7) == 0) { + start = url + 7; + } + const char *path_start = strchr(start, '/'); + const char *port_start = strchr(start, ':'); + if (port_start && (!path_start || port_start < path_start)) { + size_t host_len = port_start - start; + if (host_len >= sizeof(parsed->host)) return -1; + strncpy(parsed->host, start, host_len); + parsed->port = atoi(port_start + 1); + } else if (path_start) { + size_t host_len = path_start - start; + if (host_len >= sizeof(parsed->host)) return -1; + strncpy(parsed->host, start, host_len); + } else { + strncpy(parsed->host, start, sizeof(parsed->host) - 1); + } + if (path_start) { + strncpy(parsed->path, path_start, sizeof(parsed->path) - 1); + } + return 0; +} + +static int connect_to_server(parsed_url_t *url, int timeout_ms) { + struct addrinfo hints, *result, *rp; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + char port_str[16]; + snprintf(port_str, sizeof(port_str), "%d", url->port); + int ret = getaddrinfo(url->host, port_str, &hints, &result); + if (ret != 0) { + return -1; + } + int sock = -1; + for (rp = result; rp != NULL; rp = rp->ai_next) { + sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (sock == -1) continue; + int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); + ret = connect(sock, rp->ai_addr, rp->ai_addrlen); + if (ret == 0) { + break; + } + if (errno == EINPROGRESS) { + fd_set write_fds; + FD_ZERO(&write_fds); + FD_SET(sock, &write_fds); + struct timeval tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + ret = select(sock + 1, NULL, &write_fds, NULL, &tv); + if (ret > 0) { + int error = 0; + socklen_t len = sizeof(error); + getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len); + if (error == 0) { + break; + } + } + } + close(sock); + sock = -1; + } + freeaddrinfo(result); + if (sock != -1) { + int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags & ~O_NONBLOCK); + int opt = 1; + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); + struct timeval tv; + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + } + return sock; +} + +static int send_request(int sock, parsed_url_t *url, const char *method, + const char *body, const char *extra_headers) { + char request[BUFFER_SIZE]; + int len; + if (body && strlen(body) > 0) { + len = snprintf(request, sizeof(request), + "%s %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Connection: close\r\n" + "Content-Length: %zu\r\n" + "%s" + "\r\n" + "%s", + method, url->path, url->host, url->port, + strlen(body), + extra_headers ? extra_headers : "", + body); + } else { + len = snprintf(request, sizeof(request), + "%s %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Connection: close\r\n" + "%s" + "\r\n", + method, url->path, url->host, url->port, + extra_headers ? extra_headers : ""); + } + ssize_t sent = send(sock, request, len, 0); + return sent == len ? 0 : -1; +} + +static int read_response(int sock, int *status_code, size_t *bytes_read, int timeout_ms) { + char buffer[BUFFER_SIZE]; + *bytes_read = 0; + *status_code = 0; + bool headers_parsed = false; + size_t content_length = 0; + size_t header_end_pos = 0; + size_t body_received = 0; + fd_set read_fds; + struct timeval tv; + while (1) { + FD_ZERO(&read_fds); + FD_SET(sock, &read_fds); + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + int sel = select(sock + 1, &read_fds, NULL, NULL, &tv); + if (sel <= 0) { + if (sel == 0) return *status_code > 0 ? 0 : -2; + return -1; + } + ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + if (*status_code > 0) return 0; + return -2; + } + return -1; + } + if (n == 0) break; + *bytes_read += n; + buffer[n] = '\0'; + if (!headers_parsed) { + char *status_line = strstr(buffer, "HTTP/"); + if (status_line) { + char *space = strchr(status_line, ' '); + if (space) { + *status_code = atoi(space + 1); + } + } + char *header_end = strstr(buffer, "\r\n\r\n"); + if (header_end) { + headers_parsed = true; + header_end_pos = (header_end - buffer) + 4; + body_received = n - header_end_pos; + char *cl = strcasestr(buffer, "Content-Length:"); + if (cl && cl < header_end) { + content_length = atoi(cl + 15); + } + if (content_length > 0 && body_received >= content_length) { + break; + } + if (content_length == 0) { + break; + } + } + } else { + body_received += n; + if (content_length > 0 && body_received >= content_length) { + break; + } + } + } + return 0; +} + +static void record_latency(stats_t *stats, double latency_ms) { + pthread_mutex_lock(&stats->latency_lock); + uint64_t idx = atomic_load(&stats->latency_count); + if (idx < MAX_LATENCIES) { + stats->latencies[idx] = latency_ms; + atomic_fetch_add(&stats->latency_count, 1); + } + pthread_mutex_unlock(&stats->latency_lock); +} + +static void* worker_thread(void *arg) { + worker_config_t *config = (worker_config_t*)arg; + while (*config->running) { + double start_time = get_time_ms(); + int sock = connect_to_server(config->url, config->timeout_ms); + if (sock < 0) { + atomic_fetch_add(&config->stats->connect_errors, 1); + atomic_fetch_add(&config->stats->failed_requests, 1); + atomic_fetch_add(&config->stats->total_requests, 1); + continue; + } + if (send_request(sock, config->url, config->method, + config->body, config->headers) < 0) { + close(sock); + atomic_fetch_add(&config->stats->failed_requests, 1); + atomic_fetch_add(&config->stats->total_requests, 1); + continue; + } + int status_code; + size_t bytes_read; + int ret = read_response(sock, &status_code, &bytes_read, config->timeout_ms); + close(sock); + double end_time = get_time_ms(); + double latency = end_time - start_time; + atomic_fetch_add(&config->stats->total_requests, 1); + atomic_fetch_add(&config->stats->bytes_received, bytes_read); + if (ret == -2) { + atomic_fetch_add(&config->stats->timeout_errors, 1); + atomic_fetch_add(&config->stats->failed_requests, 1); + } else if (ret < 0) { + atomic_fetch_add(&config->stats->read_errors, 1); + atomic_fetch_add(&config->stats->failed_requests, 1); + } else { + atomic_fetch_add(&config->stats->successful_requests, 1); + record_latency(config->stats, latency); + if (status_code >= 200 && status_code < 300) { + atomic_fetch_add(&config->stats->status_2xx, 1); + } else if (status_code >= 300 && status_code < 400) { + atomic_fetch_add(&config->stats->status_3xx, 1); + } else if (status_code >= 400 && status_code < 500) { + atomic_fetch_add(&config->stats->status_4xx, 1); + } else if (status_code >= 500) { + atomic_fetch_add(&config->stats->status_5xx, 1); + } + } + } + return NULL; +} + +static int compare_double(const void *a, const void *b) { + double da = *(const double*)a; + double db = *(const double*)b; + if (da < db) return -1; + if (da > db) return 1; + return 0; +} + +static void calculate_percentiles(stats_t *stats, double *p50, double *p90, + double *p99, double *min, double *max, double *avg) { + uint64_t count = atomic_load(&stats->latency_count); + if (count == 0) { + *p50 = *p90 = *p99 = *min = *max = *avg = 0; + return; + } + qsort(stats->latencies, count, sizeof(double), compare_double); + *min = stats->latencies[0]; + *max = stats->latencies[count - 1]; + double sum = 0; + for (uint64_t i = 0; i < count; i++) { + sum += stats->latencies[i]; + } + *avg = sum / count; + *p50 = stats->latencies[(size_t)(count * 0.50)]; + *p90 = stats->latencies[(size_t)(count * 0.90)]; + *p99 = stats->latencies[(size_t)(count * 0.99)]; +} + +static void print_progress(stats_t *stats, double elapsed_seconds) { + uint64_t total = atomic_load(&stats->total_requests); + uint64_t success = atomic_load(&stats->successful_requests); + uint64_t failed = atomic_load(&stats->failed_requests); + double rps = elapsed_seconds > 0 ? total / elapsed_seconds : 0; + printf("\r[%.1fs] Requests: %lu | Success: %lu | Failed: %lu | RPS: %.1f ", + elapsed_seconds, total, success, failed, rps); + fflush(stdout); +} + +static void print_results(stats_t *stats, double total_time_seconds, int concurrency) { + uint64_t total = atomic_load(&stats->total_requests); + uint64_t success = atomic_load(&stats->successful_requests); + uint64_t failed = atomic_load(&stats->failed_requests); + uint64_t bytes = atomic_load(&stats->bytes_received); + double p50, p90, p99, min, max, avg; + calculate_percentiles(stats, &p50, &p90, &p99, &min, &max, &avg); + printf("\n\n"); + printf("╔══════════════════════════════════════════════════════════════╗\n"); + printf("║ LOAD TEST RESULTS ║\n"); + printf("╠══════════════════════════════════════════════════════════════╣\n"); + printf("║ Duration: %10.2f seconds ║\n", total_time_seconds); + printf("║ Concurrency: %10d threads ║\n", concurrency); + printf("╠══════════════════════════════════════════════════════════════╣\n"); + printf("║ Total Requests: %10lu ║\n", total); + printf("║ Successful: %10lu (%.1f%%) ║\n", + success, total > 0 ? (success * 100.0 / total) : 0); + printf("║ Failed: %10lu (%.1f%%) ║\n", + failed, total > 0 ? (failed * 100.0 / total) : 0); + printf("╠══════════════════════════════════════════════════════════════╣\n"); + printf("║ Requests/sec: %10.2f ║\n", + total / total_time_seconds); + printf("║ Transfer/sec: %10.2f KB ║\n", + (bytes / 1024.0) / total_time_seconds); + printf("║ Total Transfer: %10.2f MB ║\n", + bytes / (1024.0 * 1024.0)); + printf("╠══════════════════════════════════════════════════════════════╣\n"); + printf("║ LATENCY DISTRIBUTION ║\n"); + printf("║ ───────────────────── ║\n"); + printf("║ Min: %10.2f ms ║\n", min); + printf("║ Avg: %10.2f ms ║\n", avg); + printf("║ Max: %10.2f ms ║\n", max); + printf("║ p50: %10.2f ms ║\n", p50); + printf("║ p90: %10.2f ms ║\n", p90); + printf("║ p99: %10.2f ms ║\n", p99); + printf("╠══════════════════════════════════════════════════════════════╣\n"); + printf("║ HTTP STATUS CODES ║\n"); + printf("║ ───────────────── ║\n"); + printf("║ 2xx: %10lu ║\n", + atomic_load(&stats->status_2xx)); + printf("║ 3xx: %10lu ║\n", + atomic_load(&stats->status_3xx)); + printf("║ 4xx: %10lu ║\n", + atomic_load(&stats->status_4xx)); + printf("║ 5xx: %10lu ║\n", + atomic_load(&stats->status_5xx)); + printf("╠══════════════════════════════════════════════════════════════╣\n"); + printf("║ ERROR BREAKDOWN ║\n"); + printf("║ ─────────────── ║\n"); + printf("║ Connect Errors: %10lu ║\n", + atomic_load(&stats->connect_errors)); + printf("║ Timeout Errors: %10lu ║\n", + atomic_load(&stats->timeout_errors)); + printf("║ Read Errors: %10lu ║\n", + atomic_load(&stats->read_errors)); + printf("╚══════════════════════════════════════════════════════════════╝\n"); +} + +static void print_usage(const char *program) { + printf("Usage: %s [options] \n\n", program); + printf("Options:\n"); + printf(" -c, --concurrency Number of concurrent connections (default: 10)\n"); + printf(" -d, --duration Test duration in seconds (default: 10)\n"); + printf(" -m, --method HTTP method (GET, POST, PUT, DELETE) (default: GET)\n"); + printf(" -b, --body Request body for POST/PUT\n"); + printf(" -H, --header
Add custom header (can be used multiple times)\n"); + printf(" -t, --timeout Request timeout in milliseconds (default: 5000)\n"); + printf(" -h, --help Show this help message\n"); + printf("\nExamples:\n"); + printf(" %s -c 100 -d 30 http://localhost:8000/\n", program); + printf(" %s -c 50 -m POST -b '{\"name\":\"test\"}' -H 'Content-Type: application/json' http://localhost:8000/users\n", program); +} + +int main(int argc, char *argv[]) { + int concurrency = 10; + int duration = 10; + int timeout_ms = 5000; + char *method = "GET"; + char *body = NULL; + char headers[4096] = ""; + char *url_str = NULL; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--concurrency") == 0) { + if (++i < argc) concurrency = atoi(argv[i]); + } else if (strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--duration") == 0) { + if (++i < argc) duration = atoi(argv[i]); + } else if (strcmp(argv[i], "-m") == 0 || strcmp(argv[i], "--method") == 0) { + if (++i < argc) method = argv[i]; + } else if (strcmp(argv[i], "-b") == 0 || strcmp(argv[i], "--body") == 0) { + if (++i < argc) body = argv[i]; + } else if (strcmp(argv[i], "-H") == 0 || strcmp(argv[i], "--header") == 0) { + if (++i < argc) { + strcat(headers, argv[i]); + strcat(headers, "\r\n"); + } + } else if (strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--timeout") == 0) { + if (++i < argc) timeout_ms = atoi(argv[i]); + } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return 0; + } else if (argv[i][0] != '-') { + url_str = argv[i]; + } + } + if (!url_str) { + fprintf(stderr, "Error: URL is required\n\n"); + print_usage(argv[0]); + return 1; + } + parsed_url_t url; + if (parse_url(url_str, &url) < 0) { + fprintf(stderr, "Error: Invalid URL format\n"); + return 1; + } + if (concurrency < 1) concurrency = 1; + if (concurrency > 10000) concurrency = 10000; + if (duration < 1) duration = 1; + memset(&g_stats, 0, sizeof(g_stats)); + g_stats.latencies = calloc(MAX_LATENCIES, sizeof(double)); + if (!g_stats.latencies) { + fprintf(stderr, "Error: Failed to allocate memory for latencies\n"); + return 1; + } + pthread_mutex_init(&g_stats.latency_lock, NULL); + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + signal(SIGPIPE, SIG_IGN); + printf("╔══════════════════════════════════════════════════════════════╗\n"); + printf("║ LOAD TEST STARTING ║\n"); + printf("╠══════════════════════════════════════════════════════════════╣\n"); + printf("║ Target: http://%s:%d%s\n", url.host, url.port, url.path); + printf("║ Method: %s\n", method); + printf("║ Threads: %d\n", concurrency); + printf("║ Duration: %d seconds\n", duration); + printf("║ Timeout: %d ms\n", timeout_ms); + printf("╚══════════════════════════════════════════════════════════════╝\n\n"); + printf("Running load test... Press Ctrl+C to stop early.\n\n"); + pthread_t *threads = calloc(concurrency, sizeof(pthread_t)); + worker_config_t *configs = calloc(concurrency, sizeof(worker_config_t)); + if (!threads || !configs) { + fprintf(stderr, "Error: Failed to allocate memory for threads\n"); + free(g_stats.latencies); + return 1; + } + double start_time = get_time_ms(); + for (int i = 0; i < concurrency; i++) { + configs[i].thread_id = i; + configs[i].url = &url; + configs[i].method = method; + configs[i].body = body; + configs[i].headers = strlen(headers) > 0 ? headers : NULL; + configs[i].duration_seconds = duration; + configs[i].timeout_ms = timeout_ms; + configs[i].stats = &g_stats; + configs[i].running = &g_running; + pthread_create(&threads[i], NULL, worker_thread, &configs[i]); + } + double elapsed = 0; + while (g_running && elapsed < duration) { + usleep(100000); + elapsed = (get_time_ms() - start_time) / 1000.0; + print_progress(&g_stats, elapsed); + } + g_running = false; + for (int i = 0; i < concurrency; i++) { + pthread_join(threads[i], NULL); + } + double total_time = (get_time_ms() - start_time) / 1000.0; + print_results(&g_stats, total_time, concurrency); + free(threads); + free(configs); + free(g_stats.latencies); + pthread_mutex_destroy(&g_stats.latency_lock); + return 0; +}