DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
graphql.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file graphql.cpp
6 * @brief GraphQL query subset - parser + executor (implementation).
7 *
8 * Recursive-descent parse into fixed node/arg pools, then a recursive emit that
9 * mirrors the selection set into a JSON `data` object, calling the resolver for
10 * leaf fields with the arguments collected along the path. No heap; all state is
11 * file-static (single-accessor, like the other services).
12 */
13
15
16#if DETWS_ENABLE_GRAPHQL
17
18#include <stdio.h>
19#include <string.h>
20
21struct DetwsGqlArgs
22{
23 const int *idx; // indices into s_gql.args that are in scope
24 int count;
25};
26
27namespace
28{
29struct Node
30{
31 char name[DETWS_GQL_NAME_MAX];
32 int first_arg;
33 int n_args;
34 int first_child; // -1 if leaf
35 int next_sib; // -1 if last
36};
37struct Arg
38{
39 char name[DETWS_GQL_NAME_MAX];
40 DetwsGqlValue val;
41};
42
43// All GraphQL parser + executor state, owned by one instance (internal linkage): the node /
44// arg / string pools and their cursors, the parse root + error, and the executor's arg-scope
45// stack, resolver, and dotted path. Grouped so it is one named owner, unreachable cross-TU;
46// single-accessor (never reentrant). The recursive parser/executor is a single-owner state
47// machine, so its helpers reach this owner directly.
48struct GqlCtx
49{
50 Node nodes[DETWS_GQL_MAX_NODES];
51 Arg args[DETWS_GQL_MAX_ARGS];
52 char strbuf[DETWS_GQL_STRBUF];
53 int nnodes;
54 int nargs;
55 int str_len;
56 int root;
57 DetwsGqlResult err;
58 // executor: scope stack of in-scope arg indices, resolver, and dotted path
59 int scope[DETWS_GQL_MAX_ARGS];
60 int scope_n;
61 detws_gql_resolver_fn resolver;
62 char path[DETWS_GQL_PATH_MAX];
63};
64GqlCtx s_gql;
65
66int new_node()
67{
68 if (s_gql.nnodes >= DETWS_GQL_MAX_NODES)
69 {
70 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_LIMIT;
71 return -1;
72 }
73 Node *n = &s_gql.nodes[s_gql.nnodes];
74 n->name[0] = '\0';
75 n->first_arg = -1;
76 n->n_args = 0;
77 n->first_child = -1;
78 n->next_sib = -1;
79 return s_gql.nnodes++;
80}
81
82// ---- lexer helpers --------------------------------------------------------
83struct Lex
84{
85 const char *p;
86 const char *e;
87};
88
89void skipws(Lex &L)
90{
91 while (L.p < L.e)
92 {
93 char c = *L.p;
94 if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',')
95 L.p++;
96 else if (c == '#')
97 {
98 while (L.p < L.e && *L.p != '\n')
99 L.p++;
100 }
101 else
102 break;
103 }
104}
105
106char peek(Lex &L)
107{
108 skipws(L);
109 return L.p < L.e ? *L.p : '\0';
110}
111
112// Record a generic parse error, preserving any more specific error already set.
113void gql_flag_parse_err()
114{
115 if (s_gql.err == DetwsGqlResult::DETWS_GQL_OK)
116 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_PARSE;
117}
118
119bool is_name_start(char c)
120{
121 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
122}
123bool is_name(char c)
124{
125 return is_name_start(c) || (c >= '0' && c <= '9');
126}
127
128bool parse_name(Lex &L, char *out)
129{
130 skipws(L);
131 if (L.p >= L.e || !is_name_start(*L.p))
132 return false;
133 int i = 0;
134 while (L.p < L.e && is_name(*L.p))
135 {
136 if (i >= DETWS_GQL_NAME_MAX - 1)
137 {
138 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_LIMIT;
139 return false;
140 }
141 out[i++] = *L.p++;
142 }
143 out[i] = '\0';
144 return true;
145}
146
147// Copy a decoded string into the strbuf pool; returns pointer or nullptr.
148const char *intern(const char *s, int len)
149{
150 if (s_gql.str_len + len + 1 > DETWS_GQL_STRBUF)
151 {
152 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_LIMIT;
153 return nullptr;
154 }
155 char *dst = s_gql.strbuf + s_gql.str_len;
156 memcpy(dst, s, len);
157 dst[len] = '\0';
158 s_gql.str_len += len + 1;
159 return dst;
160}
161
162bool parse_value(Lex &L, DetwsGqlValue *v)
163{
164 char c = peek(L);
165 if (c == '"')
166 {
167 L.p++; // opening quote
168 char tmp[DETWS_GQL_STRBUF];
169 int n = 0;
170 while (L.p < L.e && *L.p != '"')
171 {
172 char ch = *L.p++;
173 if (ch == '\\' && L.p < L.e)
174 {
175 char esc = *L.p++;
176 switch (esc)
177 {
178 case 'n':
179 ch = '\n';
180 break;
181 case 't':
182 ch = '\t';
183 break;
184 case 'r':
185 ch = '\r';
186 break;
187 case '"':
188 ch = '"';
189 break;
190 case '\\':
191 ch = '\\';
192 break;
193 case '/':
194 ch = '/';
195 break;
196 default:
197 ch = esc;
198 break;
199 }
200 }
201 if (n >= (int)sizeof(tmp) - 1)
202 {
203 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_LIMIT;
204 return false;
205 }
206 tmp[n++] = ch;
207 }
208 if (L.p >= L.e)
209 {
210 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_PARSE;
211 return false;
212 }
213 L.p++; // closing quote
214 const char *s = intern(tmp, n);
215 if (!s)
216 return false;
217 v->type = DetwsGqlType::DETWS_GQL_STR;
218 v->s = s;
219 return true;
220 }
221 if (c == '-' || (c >= '0' && c <= '9'))
222 {
223 // Manual number parse (no stdlib): integer, optional fraction, optional
224 // exponent. Builds an int64 for plain integers and a double otherwise.
225 bool neg = false;
226 if (*L.p == '-')
227 {
228 neg = true;
229 L.p++;
230 }
231 bool any = false;
232 bool is_float = false;
233 unsigned long long ipart = 0; // accumulate unsigned: signed overflow on a huge literal is UB
234 double fval = 0.0;
235 while (L.p < L.e && *L.p >= '0' && *L.p <= '9')
236 {
237 ipart = ipart * 10ULL + (unsigned)(*L.p - '0');
238 L.p++;
239 any = true;
240 }
241 fval = (double)ipart;
242 if (L.p < L.e && *L.p == '.')
243 {
244 is_float = true;
245 L.p++;
246 double scale = 1.0;
247 while (L.p < L.e && *L.p >= '0' && *L.p <= '9')
248 {
249 scale *= 10.0;
250 fval += (double)(*L.p - '0') / scale;
251 L.p++;
252 any = true;
253 }
254 }
255 if (L.p < L.e && (*L.p == 'e' || *L.p == 'E'))
256 {
257 is_float = true;
258 L.p++;
259 bool eneg = false;
260 if (L.p < L.e && (*L.p == '+' || *L.p == '-'))
261 eneg = (*L.p++ == '-');
262 int ex = 0;
263 while (L.p < L.e && *L.p >= '0' && *L.p <= '9')
264 {
265 // clamp: 10^400 overflows the double to inf, and bounds the exponent below
266 ex = (ex < 400) ? ex * 10 + (*L.p - '0') : ex;
267 L.p++;
268 }
269 double m = 1.0;
270 for (int k = 0; k < ex; k++)
271 m *= 10.0;
272 fval = eneg ? fval / m : fval * m;
273 }
274 if (!any)
275 {
276 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_PARSE;
277 return false;
278 }
279 if (is_float)
280 {
281 v->type = DetwsGqlType::DETWS_GQL_FLOAT;
282 v->f = neg ? -fval : fval;
283 }
284 else
285 {
286 v->type = DetwsGqlType::DETWS_GQL_INT;
287 // Negate in signed space: ipart is unsigned (to dodge signed-overflow UB while
288 // accumulating), so -ipart would be a modular unsigned negation, not arithmetic negation.
289 v->i = neg ? -(long long)ipart : (long long)ipart;
290 }
291 return true;
292 }
293 // keyword: true / false / null
294 char kw[8];
295 if (parse_name(L, kw))
296 {
297 if (strcmp(kw, "true") == 0)
298 {
299 v->type = DetwsGqlType::DETWS_GQL_BOOL;
300 v->b = true;
301 return true;
302 }
303 if (strcmp(kw, "false") == 0)
304 {
305 v->type = DetwsGqlType::DETWS_GQL_BOOL;
306 v->b = false;
307 return true;
308 }
309 if (strcmp(kw, "null") == 0)
310 {
311 v->type = DetwsGqlType::DETWS_GQL_NULL;
312 return true;
313 }
314 }
315 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_PARSE;
316 return false;
317}
318
319int parse_selection(Lex &L, int depth);
320
321int parse_field(Lex &L, int depth)
322{
323 int idx = new_node();
324 if (idx < 0)
325 return -1;
326 if (!parse_name(L, s_gql.nodes[idx].name))
327 {
328 gql_flag_parse_err();
329 return -1;
330 }
331 // arguments
332 if (peek(L) == '(')
333 {
334 L.p++; // '('
335 int first = -1;
336 int count = 0;
337 while (peek(L) != ')')
338 {
339 if (s_gql.nargs >= DETWS_GQL_MAX_ARGS)
340 {
341 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_LIMIT;
342 return -1;
343 }
344 Arg *a = &s_gql.args[s_gql.nargs];
345 if (!parse_name(L, a->name))
346 {
347 gql_flag_parse_err();
348 return -1;
349 }
350 if (peek(L) != ':')
351 {
352 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_PARSE;
353 return -1;
354 }
355 L.p++; // ':'
356 if (!parse_value(L, &a->val))
357 return -1;
358 if (first < 0)
359 first = s_gql.nargs;
360 count++;
361 s_gql.nargs++;
362 }
363 L.p++; // ')'
364 s_gql.nodes[idx].first_arg = first;
365 s_gql.nodes[idx].n_args = count;
366 }
367 // sub-selection
368 if (peek(L) == '{')
369 s_gql.nodes[idx].first_child = parse_selection(L, depth + 1);
370 return s_gql.err != DetwsGqlResult::DETWS_GQL_OK ? -1 : idx;
371}
372
373int parse_selection(Lex &L, int depth)
374{
375 if (depth > DETWS_GQL_MAX_DEPTH)
376 {
377 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_LIMIT;
378 return -1;
379 }
380 if (peek(L) != '{')
381 {
382 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_PARSE;
383 return -1;
384 }
385 L.p++; // '{'
386 int first = -1;
387 int prev = -1;
388 while (peek(L) != '}')
389 {
390 if (L.p >= L.e)
391 {
392 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_PARSE;
393 return -1;
394 }
395 int f = parse_field(L, depth);
396 if (f < 0)
397 return -1;
398 if (first < 0)
399 first = f;
400 else
401 s_gql.nodes[prev].next_sib = f;
402 prev = f;
403 }
404 L.p++; // '}'
405 return first;
406}
407
408bool parse_document(Lex &L)
409{
410 char c = peek(L);
411 if (c != '{')
412 {
413 char kw[DETWS_GQL_NAME_MAX];
414 if (!parse_name(L, kw) || strcmp(kw, "query") != 0)
415 {
416 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_PARSE; // only anonymous or `query` operations
417 return false;
418 }
419 if (peek(L) != '{') // optional operation name
420 {
421 char opname[DETWS_GQL_NAME_MAX];
422 if (!parse_name(L, opname))
423 {
424 gql_flag_parse_err();
425 return false;
426 }
427 }
428 }
429 s_gql.root = parse_selection(L, 1);
430 if (s_gql.err != DetwsGqlResult::DETWS_GQL_OK)
431 return false;
432 if (peek(L) != '\0') // trailing junk after the operation
433 {
434 s_gql.err = DetwsGqlResult::DETWS_GQL_ERR_PARSE;
435 return false;
436 }
437 return true;
438}
439
440// ---- writer + executor ----------------------------------------------------
441struct Writer
442{
443 char *o;
444 size_t cap;
445 size_t n;
446 bool ovf;
447};
448void w_raw(Writer &w, const char *s, size_t len)
449{
450 if (w.ovf)
451 return;
452 if (w.n + len > w.cap)
453 {
454 w.ovf = true;
455 return;
456 }
457 memcpy(w.o + w.n, s, len);
458 w.n += len;
459}
460void w_str(Writer &w, const char *s)
461{
462 w_raw(w, s, strnlen(s, w.cap + 1));
463}
464void w_json_str(Writer &w, const char *s)
465{
466 w_raw(w, "\"", 1);
467 for (const char *p = s; *p; p++)
468 {
469 unsigned char ch = (unsigned char)*p;
470 if (ch == '"')
471 w_raw(w, "\\\"", 2);
472 else if (ch == '\\')
473 w_raw(w, "\\\\", 2);
474 else if (ch == '\n')
475 w_raw(w, "\\n", 2);
476 else if (ch == '\r')
477 w_raw(w, "\\r", 2);
478 else if (ch == '\t')
479 w_raw(w, "\\t", 2);
480 else if (ch < 0x20)
481 {
482 char u[7];
483 snprintf(u, sizeof(u), "\\u%04x", ch);
484 w_raw(w, u, 6);
485 }
486 else
487 w_raw(w, (const char *)&ch, 1);
488 }
489 w_raw(w, "\"", 1);
490}
491void w_scalar(Writer &w, const DetwsGqlValue *v)
492{
493 char b[40];
494 switch (v->type)
495 {
496 case DetwsGqlType::DETWS_GQL_INT:
497 snprintf(b, sizeof(b), "%lld", v->i);
498 w_str(w, b);
499 break;
500 case DetwsGqlType::DETWS_GQL_FLOAT:
501 snprintf(b, sizeof(b), "%g", v->f);
502 w_str(w, b);
503 break;
504 case DetwsGqlType::DETWS_GQL_BOOL:
505 w_str(w, v->b ? "true" : "false");
506 break;
507 case DetwsGqlType::DETWS_GQL_STR:
508 w_json_str(w, v->s ? v->s : "");
509 break;
510 default:
511 w_str(w, "null");
512 break;
513 }
514}
515
516void emit_field(Writer &w, int idx, int path_len)
517{
518 Node *node = &s_gql.nodes[idx];
519
520 // extend the dotted path: [parent].name
521 int plen = path_len;
522 if (plen > 0)
523 {
524 if (plen + 1 >= DETWS_GQL_PATH_MAX)
525 {
526 w.ovf = true;
527 return;
528 }
529 s_gql.path[plen++] = '.';
530 }
531 int nl = (int)strnlen(node->name, DETWS_GQL_PATH_MAX);
532 if (plen + nl >= DETWS_GQL_PATH_MAX)
533 {
534 w.ovf = true;
535 return;
536 }
537 memcpy(s_gql.path + plen, node->name, nl);
538 plen += nl;
539 s_gql.path[plen] = '\0';
540
541 // push this field's args into scope
542 int pushed = 0;
543 for (int a = 0; a < node->n_args; a++)
544 if (s_gql.scope_n < DETWS_GQL_MAX_ARGS)
545 {
546 s_gql.scope[s_gql.scope_n++] = node->first_arg + a;
547 pushed++;
548 }
549
550 w_json_str(w, node->name);
551 w_raw(w, ":", 1);
552
553 if (node->first_child >= 0)
554 {
555 w_raw(w, "{", 1);
556 bool first = true;
557 for (int c = node->first_child; c >= 0; c = s_gql.nodes[c].next_sib)
558 {
559 if (!first)
560 w_raw(w, ",", 1);
561 first = false;
562 emit_field(w, c, plen);
563 }
564 w_raw(w, "}", 1);
565 }
566 else
567 {
568 DetwsGqlValue v;
569 v.type = DetwsGqlType::DETWS_GQL_NULL;
570 DetwsGqlArgs view = {s_gql.scope, s_gql.scope_n};
571 if (s_gql.resolver && s_gql.resolver(s_gql.path, &view, &v))
572 w_scalar(w, &v);
573 else
574 w_str(w, "null");
575 }
576
577 s_gql.scope_n -= pushed; // pop
578 s_gql.path[path_len] = '\0';
579}
580} // namespace
581
582bool detws_gql_arg_int(const DetwsGqlArgs *args, const char *name, long long *out)
583{
584 if (!args)
585 return false;
586 for (int k = 0; k < args->count; k++)
587 {
588 Arg *a = &s_gql.args[args->idx[k]];
589 if (strcmp(a->name, name) == 0 && a->val.type == DetwsGqlType::DETWS_GQL_INT)
590 {
591 *out = a->val.i;
592 return true;
593 }
594 }
595 return false;
596}
597bool detws_gql_arg_str(const DetwsGqlArgs *args, const char *name, const char **out)
598{
599 if (!args)
600 return false;
601 for (int k = 0; k < args->count; k++)
602 {
603 Arg *a = &s_gql.args[args->idx[k]];
604 if (strcmp(a->name, name) == 0 && a->val.type == DetwsGqlType::DETWS_GQL_STR)
605 {
606 *out = a->val.s;
607 return true;
608 }
609 }
610 return false;
611}
612bool detws_gql_arg_bool(const DetwsGqlArgs *args, const char *name, bool *out)
613{
614 if (!args)
615 return false;
616 for (int k = 0; k < args->count; k++)
617 {
618 Arg *a = &s_gql.args[args->idx[k]];
619 if (strcmp(a->name, name) == 0 && a->val.type == DetwsGqlType::DETWS_GQL_BOOL)
620 {
621 *out = a->val.b;
622 return true;
623 }
624 }
625 return false;
626}
627
628DetwsGqlResult detws_graphql_execute(const char *query, size_t len, detws_gql_resolver_fn resolver, char *out,
629 size_t cap)
630{
631 s_gql.nnodes = 0;
632 s_gql.nargs = 0;
633 s_gql.str_len = 0;
634 s_gql.scope_n = 0;
635 s_gql.root = -1;
636 s_gql.err = DetwsGqlResult::DETWS_GQL_OK;
637 s_gql.resolver = resolver;
638 s_gql.path[0] = '\0';
639
640 Lex L = {query, query + (query ? len : 0)};
641 if (!query || !out || cap == 0)
642 return DetwsGqlResult::DETWS_GQL_ERR_PARSE;
643
644 if (!parse_document(L))
645 {
646 const char *msg =
647 (s_gql.err == DetwsGqlResult::DETWS_GQL_ERR_LIMIT) ? "query exceeds a configured limit" : "syntax error";
648 Writer w = {out, cap, 0, false};
649 w_str(w, "{\"errors\":[{\"message\":");
650 w_json_str(w, msg);
651 w_str(w, "}]}");
652 if (!w.ovf && w.n < cap)
653 out[w.n] = '\0';
654 return s_gql.err != DetwsGqlResult::DETWS_GQL_OK ? s_gql.err : DetwsGqlResult::DETWS_GQL_ERR_PARSE;
655 }
656
657 Writer w = {out, cap, 0, false};
658 w_str(w, "{\"data\":{");
659 bool first = true;
660 for (int c = s_gql.root; c >= 0; c = s_gql.nodes[c].next_sib)
661 {
662 if (!first)
663 w_raw(w, ",", 1);
664 first = false;
665 emit_field(w, c, 0);
666 }
667 w_str(w, "}}");
668 if (w.ovf || w.n >= cap)
669 return DetwsGqlResult::DETWS_GQL_ERR_OVERFLOW;
670 out[w.n] = '\0';
671 return DetwsGqlResult::DETWS_GQL_OK;
672}
673
674#endif // DETWS_ENABLE_GRAPHQL
Zero-heap GraphQL query subset - parser + executor (DETWS_ENABLE_GRAPHQL).