Update.
Some checks failed
Build and Test / build-framework (push) Failing after 1m16s
Build and Test / test-framework (push) Has been skipped
Build and Test / test-docdb-full (push) Has been skipped
Build and Test / memory-check (push) Has been skipped

This commit is contained in:
retoor 2025-11-25 21:11:23 +01:00
commit bde988cd6a
12 changed files with 6812 additions and 0 deletions

131
.gitea/workflows/build.yaml Normal file
View File

@ -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

34
.gitignore vendored Normal file
View File

@ -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/

21
LICENSE Normal file
View File

@ -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.

38
Makefile Normal file
View File

@ -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)

88
Makefile.docdb Normal file
View File

@ -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"

256
README.md Normal file
View File

@ -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

2679
celerityapi.c Normal file

File diff suppressed because it is too large Load Diff

481
celerityapi.h Normal file
View File

@ -0,0 +1,481 @@
#ifndef CELERITYAPI_H
#define CELERITYAPI_H
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdarg.h>
#include <time.h>
#include <ctype.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#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

922
docclient.c Normal file
View File

@ -0,0 +1,922 @@
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#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 <host> Server host (default: 127.0.0.1)\n");
printf(" -p, --port <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;
}

1243
docserver.c Normal file

File diff suppressed because it is too large Load Diff

392
example.c Normal file
View File

@ -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;
}

527
loadtest.c Normal file
View File

@ -0,0 +1,527 @@
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdatomic.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <signal.h>
#include <math.h>
#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] <url>\n\n", program);
printf("Options:\n");
printf(" -c, --concurrency <n> Number of concurrent connections (default: 10)\n");
printf(" -d, --duration <n> Test duration in seconds (default: 10)\n");
printf(" -m, --method <method> HTTP method (GET, POST, PUT, DELETE) (default: GET)\n");
printf(" -b, --body <data> Request body for POST/PUT\n");
printf(" -H, --header <header> Add custom header (can be used multiple times)\n");
printf(" -t, --timeout <ms> 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;
}