cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
graphql.c
Go to the documentation of this file.
4#include <string.h>
5#include <ctype.h>
6#include <stdlib.h>
7#include <stdio.h>
8
10
11typedef struct graphql_field {
12 char *name;
14 void *ctx;
15 struct graphql_field *next;
17
18struct cwist_graphql_schema {
19 graphql_field_t *queries;
20 graphql_field_t *mutations;
21};
22
28static cwist_error_t gql_error(int code) {
29 return (cwist_error_t){.errtype = CWIST_ERR_INT16, .error.err_i16 = code};
30}
31
40static bool gql_name_char(char c, bool first) {
41 if (first) return (c == '_' || isalpha((unsigned char)c));
42 return (c == '_' || isalnum((unsigned char)c));
43}
44
51static bool gql_name_valid(const char *s) {
52 if (!s || !gql_name_char(*s, true)) return false;
53 for (++s; *s; ++s) {
54 if (!gql_name_char(*s, false)) return false;
55 }
56 return true;
57}
58
65static void gql_add_error(cJSON *errors, const char *message) {
67 if (e) {
68 cJSON_AddStringToObject(e, "message", message);
69 cJSON_AddItemToArray(errors, e);
70 }
71}
72
80 cwist_graphql_schema_t *schema = (cwist_graphql_schema_t *)cwist_alloc(sizeof(*schema));
81 if (schema) {
82 schema->queries = NULL;
83 schema->mutations = NULL;
84 }
85 return schema;
86}
87
92static void free_fields(graphql_field_t *f) {
93 while (f) {
94 graphql_field_t *n = f->next;
95 if (f->name) cwist_free(f->name);
97 f = n;
98 }
99}
100
106 if (!schema) return;
107 free_fields(schema->queries);
108 free_fields(schema->mutations);
109 cwist_free(schema);
110}
111
124static bool add_field(graphql_field_t **head, const char *field, cwist_graphql_resolver_fn resolver,
125 void *ctx) {
126 if (!head || !gql_name_valid(field) || !resolver) return false;
127 for (graphql_field_t *f = *head; f; f = f->next) {
128 if (strcmp(f->name, field) == 0) {
129 f->fn = resolver;
130 f->ctx = ctx;
131 return true;
132 }
133 }
135 if (!f) return false;
136 f->name = cwist_strdup(field);
137 if (!f->name) {
138 cwist_free(f);
139 return false;
140 }
141 f->fn = resolver;
142 f->ctx = ctx;
143 f->next = *head;
144 *head = f;
145 return true;
146}
147
157bool cwist_graphql_add_query(cwist_graphql_schema_t *schema, const char *field,
159 if (!schema) return false;
160 return add_field(&schema->queries, field, resolver, ctx);
161}
162
174 if (!schema) return false;
175 return add_field(&schema->mutations, field, resolver, ctx);
176}
177
184static graphql_field_t *gql_lookup(graphql_field_t *head, const char *name) {
185 for (graphql_field_t *f = head; f; f = f->next) {
186 if (strcmp(f->name, name) == 0) return f;
187 }
188 return NULL;
189}
190
196static const char *skip_ws_comments(const char *p) {
197 while (*p) {
198 if (isspace((unsigned char)*p) || *p == ',') {
199 p++;
200 } else if (*p == '#') {
201 while (*p && *p != '\n' && *p != '\r') p++;
202 } else {
203 break;
204 }
205 }
206 return p;
207}
208
221static const char *parse_arguments(const char *p, const cJSON *variables, cJSON **out_args,
222 cJSON *errors) {
223 *out_args = NULL;
224 p = skip_ws_comments(p);
225 if (*p != '(') return p;
226 p++; /* skip '(' */
227
229 if (!args) {
230 gql_add_error(errors, "Internal memory error parsing arguments");
231 return p;
232 }
233
234 while (*p) {
235 p = skip_ws_comments(p);
236 if (*p == ')' || *p == '\0') {
237 if (*p == ')') p++;
238 break;
239 }
240
241 /* Read argument name */
242 const char *name_start = p;
243 if (!gql_name_char(*p, true)) {
244 gql_add_error(errors, "Invalid argument name");
246 return p;
247 }
248 while (*p && gql_name_char(*p, false)) p++;
249 size_t name_len = (size_t)(p - name_start);
250 char arg_name[128];
251 if (name_len >= sizeof(arg_name)) name_len = sizeof(arg_name) - 1;
253 arg_name[name_len] = '\0';
254
255 p = skip_ws_comments(p);
256 if (*p != ':') {
257 gql_add_error(errors, "Expected ':' after argument name");
259 return p;
260 }
261 p++; /* skip ':' */
262 p = skip_ws_comments(p);
263
264 /* Parse argument value */
265 cJSON *val = NULL;
266 if (*p == '"') {
267 p++;
268 const char *str_start = p;
269 while (*p && *p != '"') {
270 if (*p == '\\' && *(p + 1))
271 p += 2;
272 else
273 p++;
274 }
275 if (*p != '"') {
276 gql_add_error(errors, "Unterminated string in argument");
278 return p;
279 }
280 size_t str_len = (size_t)(p - str_start);
282 char *str_val = cwist_scratch_alloc(&str_val_s, str_len + 1);
283 if (str_val) {
285 str_val[str_len] = '\0';
287 }
288 p++; /* skip closing '"' */
289 } else if (*p == '$') { /* Variable reference */
290 p++;
291 const char *var_start = p;
292 while (*p && gql_name_char(*p, false)) p++;
293 size_t var_len = (size_t)(p - var_start);
294 char var_name[128];
295 if (var_len >= sizeof(var_name)) var_len = sizeof(var_name) - 1;
297 var_name[var_len] = '\0';
298
301 if (v) val = cJSON_Duplicate(v, true);
302 }
303 if (!val) val = cJSON_CreateNull();
304 } else if (isdigit((unsigned char)*p) || *p == '-') {
305 char *endptr = NULL;
306 double num = strtod(p, &endptr);
308 p = endptr;
309 } else if (strncmp(p, "true", 4) == 0 && !gql_name_char(p[4], false)) {
310 val = cJSON_CreateBool(true);
311 p += 4;
312 } else if (strncmp(p, "false", 5) == 0 && !gql_name_char(p[5], false)) {
313 val = cJSON_CreateBool(false);
314 p += 5;
315 } else if (strncmp(p, "null", 4) == 0 && !gql_name_char(p[4], false)) {
317 p += 4;
318 } else {
319 gql_add_error(errors, "Unsupported argument value format");
321 return p;
322 }
323
324 if (val) {
326 }
327 }
328
329 *out_args = args;
330 return p;
331}
332
347static const char *parse_selection_set(const char *p, graphql_field_t *schema_fields,
348 const cJSON *variables, cJSON *parent_data, cJSON *errors,
349 unsigned int depth) {
351 gql_add_error(errors, "Query selection depth exceeds maximum allowed");
352 return p;
353 }
354
355 p = skip_ws_comments(p);
356 if (*p != '{') return p;
357 p++; /* skip '{' */
358
359 size_t field_count = 0;
360 while (*p) {
361 p = skip_ws_comments(p);
362 if (*p == '}' || *p == '\0') {
363 if (*p == '}') p++;
364 break;
365 }
366
367 if (++field_count > CWIST_GRAPHQL_MAX_FIELDS) {
368 gql_add_error(errors, "Field count limit exceeded");
369 break;
370 }
371
372 /* Read Alias or Field Name */
373 const char *name_start = p;
374 if (!gql_name_char(*p, true)) {
375 gql_add_error(errors, "Invalid field name syntax");
376 break;
377 }
378 while (*p && gql_name_char(*p, false)) p++;
379 size_t name_len = (size_t)(p - name_start);
380 char token1[128];
381 if (name_len >= sizeof(token1)) name_len = sizeof(token1) - 1;
383 token1[name_len] = '\0';
384
385 char field_name[128];
386 char output_key[128];
389
390 p = skip_ws_comments(p);
391 if (*p == ':') { /* Alias: output_key: field_name */
392 p++;
393 p = skip_ws_comments(p);
394 const char *field_start = p;
395 if (!gql_name_char(*p, true)) {
396 gql_add_error(errors, "Invalid field name after alias");
397 break;
398 }
399 while (*p && gql_name_char(*p, false)) p++;
400 size_t f_len = (size_t)(p - field_start);
401 if (f_len >= sizeof(field_name)) f_len = sizeof(field_name) - 1;
403 field_name[f_len] = '\0';
404 }
405
406 /* Parse Field Arguments if present */
407 cJSON *args = NULL;
408 p = parse_arguments(p, variables, &args, errors);
409
410 /* Resolve Field Value */
411 cJSON *val = NULL;
413 if (!f) {
414 char err_buf[256];
415 snprintf(err_buf, sizeof(err_buf), "Cannot query field '%s' on schema", field_name);
416 gql_add_error(errors, err_buf);
418 } else {
419 val = f->fn(args, variables, f->ctx);
420 if (!val) {
422 }
423 }
424 if (args) cJSON_Delete(args);
425
426 /* Parse nested selection set if present and value is object */
427 p = skip_ws_comments(p);
428 if (*p == '{') {
429 if (cJSON_IsObject(val)) {
430 p = parse_selection_set(p, schema_fields, variables, val, errors, depth + 1);
431 } else {
432 /* Skip sub-selection if non-object or null */
433 int brace_count = 0;
434 while (*p) {
435 if (*p == '{')
436 brace_count++;
437 else if (*p == '}') {
438 brace_count--;
439 if (brace_count == 0) {
440 p++;
441 break;
442 }
443 }
444 p++;
445 }
446 }
447 }
448
450 }
451
452 return p;
453}
454
469 if (!schema || !request_json || !out_json) return gql_error(-1);
470 *out_json = NULL;
471
474 cJSON *data = cJSON_CreateObject();
475 cJSON *errors = cJSON_CreateArray();
476
477 if (!request || !root || !data || !errors) {
480 cJSON_Delete(data);
481 cJSON_Delete(errors);
482 return gql_error(-1);
483 }
484 cJSON_AddItemToObject(root, "data", data);
485 cJSON_AddItemToObject(root, "errors", errors);
486
489
490 if (!cJSON_IsString(query) || !query->valuestring) {
491 gql_add_error(errors, "GraphQL request must contain a string 'query'");
492 } else if (strlen(query->valuestring) > CWIST_GRAPHQL_MAX_QUERY_SIZE) {
493 gql_add_error(errors, "Query string exceeds maximum allowed size");
494 } else {
495 const char *p = skip_ws_comments(query->valuestring);
497
498 if (strncmp(p, "mutation", 8) == 0 && !gql_name_char(p[8], false)) {
500 p += 8;
501 p = skip_ws_comments(p);
502 /* Skip optional mutation name / variable defs until '{' */
503 while (*p && *p != '{') p++;
504 } else if (strncmp(p, "query", 5) == 0 && !gql_name_char(p[5], false)) {
506 p += 5;
507 p = skip_ws_comments(p);
508 while (*p && *p != '{') p++;
509 }
510
511 if (*p != '{') {
512 gql_add_error(errors, "Expected '{' to begin selection set");
513 } else {
515 (op_type == GQL_OP_MUTATION) ? schema->mutations : schema->queries;
516 parse_selection_set(p, target_schema, variables, data, errors, 1);
517 }
518 }
519
520 if (cJSON_GetArraySize(errors) == 0) {
522 }
523
527
528 if (!printed) return gql_error(-1);
529
531 if (!*out_json) {
532 free(printed);
533 return gql_error(-1);
534 }
536 free(printed);
537
538 return gql_error(0);
539}
540
557bool cwist_graphql_parse_subscription(const char *query, const cJSON *variables, char *field_out,
558 size_t field_cap, cJSON **args_out) {
559 if (!query || !field_out || field_cap == 0 || !args_out) return false;
560 *args_out = NULL;
561 field_out[0] = '\0';
562
563 const char *p = skip_ws_comments(query);
564 if (strncmp(p, "subscription", 12) != 0 || gql_name_char(p[12], false)) return false;
565 p += 12;
566 p = skip_ws_comments(p);
567 /* Skip optional operation name / variable definitions until '{' (same
568 * treatment as the query/mutation keyword in cwist_graphql_execute). */
569 while (*p && *p != '{') p++;
570 if (*p != '{') return false;
571 p++;
572 p = skip_ws_comments(p);
573 if (*p == '}' || !gql_name_char(*p, true)) return false;
574
575 char token[128];
576 const char *name_start = p;
577 while (*p && gql_name_char(*p, false)) p++;
578 size_t name_len = (size_t)(p - name_start);
579 if (name_len >= sizeof(token)) name_len = sizeof(token) - 1;
581 token[name_len] = '\0';
582
583 p = skip_ws_comments(p);
584 if (*p == ':') { /* Alias: the resolvable name follows the alias. */
585 p++;
586 p = skip_ws_comments(p);
587 name_start = p;
588 if (!gql_name_char(*p, true)) return false;
589 while (*p && gql_name_char(*p, false)) p++;
591 if (name_len >= sizeof(token)) name_len = sizeof(token) - 1;
593 token[name_len] = '\0';
594 }
595
596 size_t out_len = strlen(token);
597 if (out_len >= field_cap) return false;
598 memcpy(field_out, token, out_len + 1);
599
600 cJSON *errors = cJSON_CreateArray();
601 if (!errors) return false;
602 parse_arguments(p, variables, args_out, errors);
603 cJSON_Delete(errors);
604 /* *args_out stays NULL when the field carries no arguments. */
605 return true;
606}
607
619 cwist_http_response *res) {
620 if (!schema || !req || !res || req->method != CWIST_HTTP_POST) {
621 if (res) res->status_code = CWIST_HTTP_BAD_REQUEST;
622 return;
623 }
624
625 cwist_sstring *json = NULL;
626 const char *body = req->body ? req->body->data : NULL;
627
628 if (cwist_graphql_execute(schema, body, &json).error.err_i16 != 0) {
630 cwist_sstring_assign(res->body, "{\"errors\":[{\"message\":\"invalid GraphQL request\"}]}");
631 } else {
633 cwist_sstring_assign_len(res->body, json->data, json->size);
635 }
636 cwist_http_header_add(&res->headers, "Content-Type", "application/json");
637}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
Definition alloc.c:32
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
#define CWIST_SCRATCH_DEFER
Attach to a cwist_scratch_t variable's declaration so its allocation (arena- or heap-backed,...
Definition alloc.h:190
Safe string type with helper methods.
cwist_sstring * cwist_sstring_create(void)
Create a new sstring instance.
Definition sstring.c:802
cwist_error_t cwist_sstring_assign_len(cwist_sstring *str, const char *data, size_t len)
Assign raw bytes with length.
Definition sstring.c:88
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
Definition sstring.c:504
void cwist_sstring_destroy(cwist_sstring *str)
Destroy an sstring instance.
Definition sstring.c:823
@ CWIST_ERR_INT16
used when checking common errcodes in Unix/Linux
Definition cwist_err.h:16
bool cwist_graphql_add_mutation(cwist_graphql_schema_t *schema, const char *field, cwist_graphql_resolver_fn resolver, void *ctx)
Register a resolver for a GraphQL mutation field.
Definition graphql.c:172
bool cwist_graphql_parse_subscription(const char *query, const cJSON *variables, char *field_out, size_t field_cap, cJSON **args_out)
Extract the root field name and parsed arguments of a subscription operation.
Definition graphql.c:557
struct graphql_field graphql_field_t
bool cwist_graphql_add_query(cwist_graphql_schema_t *schema, const char *field, cwist_graphql_resolver_fn resolver, void *ctx)
Register a resolver for a GraphQL query field.
Definition graphql.c:157
void cwist_graphql_schema_destroy(cwist_graphql_schema_t *schema)
Destroy a schema and all registered query/mutation fields.
Definition graphql.c:105
gql_op_type_t
Definition graphql.c:9
@ GQL_OP_MUTATION
Definition graphql.c:9
@ GQL_OP_QUERY
Definition graphql.c:9
cwist_error_t cwist_graphql_execute(cwist_graphql_schema_t *schema, const char *request_json, cwist_sstring **out_json)
Execute a GraphQL query or mutation from a JSON request body.
Definition graphql.c:467
cwist_graphql_schema_t * cwist_graphql_schema_create(void)
Allocate an empty GraphQL schema with no query or mutation fields.
Definition graphql.c:79
void cwist_graphql_serve(cwist_graphql_schema_t *schema, cwist_http_request *req, cwist_http_response *res)
HTTP handler serving GraphQL requests over POST.
Definition graphql.c:618
@ CWIST_HTTP_BAD_REQUEST
Definition http.h:31
@ CWIST_HTTP_OK
Definition http.h:28
@ CWIST_HTTP_POST
Definition http.h:18
cwist_error_t cwist_http_header_add(cwist_http_header_node **head, const char *key, const char *value)
Add a header to the list.
Definition http.c:1861
Full-featured, production-ready GraphQL query & mutation execution engine.
#define CWIST_GRAPHQL_MAX_FIELDS
Definition graphql.h:16
struct cwist_graphql_schema cwist_graphql_schema_t
Definition graphql.h:14
cJSON *(* cwist_graphql_resolver_fn)(const cJSON *args, const cJSON *variables, void *ctx)
Definition graphql.h:26
#define CWIST_GRAPHQL_MAX_QUERY_SIZE
Definition graphql.h:15
#define CWIST_GRAPHQL_MAX_DEPTH
Definition graphql.h:17
size_t head
Definition otel.c:57
size_t cap
Definition otel.c:56
Integrated error structure used throughout the system.
Definition cwist_err.h:72
cwist_errtype_t errtype
Definition cwist_err.h:73
cwist_sstring * body
Definition http.h:73
cwist_http_method_t method
Definition http.h:66
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
cwist_sstring * body
Definition http.h:87
cwist_http_status_t status_code
Definition http.h:84
cwist_http_header_node * headers
Definition http.h:86
Backing state for cwist_scratch_alloc()/CWIST_SCRATCH_DEFER: a single block-scoped allocation that pr...
Definition alloc.h:131
size_t size
Definition sstring.h:20
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15