commit bde988cd6ab1159f5040e0985e8870f4777eb902 Author: retoor Date: Tue Nov 25 21:11:23 2025 +0100 Update. 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; +}