first version of rtjson.h
This commit is contained in:
parent
02b55a6f35
commit
e244af9c38
554
rtjson.h
Normal file
554
rtjson.h
Normal file
@ -0,0 +1,554 @@
|
||||
#ifndef RT_JSON_H
|
||||
#define RT_JSON_H
|
||||
|
||||
/*
|
||||
* rtjson.h - Simple json parser for C
|
||||
* Copyright (C) 2026 Kevin Trogant
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "rtcore.h"
|
||||
|
||||
#ifndef RTJ_API
|
||||
#define RTJ_API RTC_API
|
||||
#endif
|
||||
|
||||
typedef enum
|
||||
{
|
||||
JSON_TYPE_OBJECT,
|
||||
JSON_TYPE_ARRAY,
|
||||
JSON_TYPE_STRING,
|
||||
JSON_TYPE_INTEGER,
|
||||
JSON_TYPE_FLOAT,
|
||||
} json_type;
|
||||
|
||||
/* Represents a json value */
|
||||
typedef struct json
|
||||
{
|
||||
/* The key, if this value is part of an object.
|
||||
* Empty otherwise. */
|
||||
s8 key;
|
||||
|
||||
json_type type;
|
||||
|
||||
/* The value. */
|
||||
union
|
||||
{
|
||||
s8 s;
|
||||
i64 i;
|
||||
f64 f;
|
||||
|
||||
/* The first subobject.
|
||||
* Element 0 of an array,
|
||||
* one member of an object. */
|
||||
struct json *first_child;
|
||||
} value;
|
||||
|
||||
/* Next in the list of a parents children */
|
||||
struct json *next;
|
||||
} json;
|
||||
|
||||
/* Parses the given json text.
|
||||
* The returned json struct will be allocated on a.
|
||||
* file is only used for error output and can be set to {0} */
|
||||
RTJ_API json *ParseJSON(s8 text, s8 file, arena *a);
|
||||
|
||||
/* Returns a pointer to the objects member with the given key.
|
||||
* Returns NULL, if no such member exists */
|
||||
RTJ_API json *GetJSONMember(json *o, s8 key);
|
||||
|
||||
/* Returns a pointer to the arrays i-th element.
|
||||
* Returns NULL if no such element exists */
|
||||
RTJ_API json *GetJSONArrayElement(json *a, isize i);
|
||||
|
||||
/* Returns the length of the given json array or -1 if the
|
||||
* passed pointer is not an array */
|
||||
RTJ_API isize GetJSONArrayLength(json *a);
|
||||
|
||||
/* Iterates over every member of an object or array */
|
||||
#define JsonForEach(_ChildVarName, _ParentPtr) \
|
||||
for (json *_ChildVarName = (_ParentPtr)->value.first_child; _ChildVarName != 0; _ChildVarName = _ChildVarName->next)
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef RT_JSON_IMPLEMENTATION
|
||||
#undef RT_JSON_IMPLEMENTATION
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
typedef enum
|
||||
{
|
||||
NOT_A_NUMBER,
|
||||
DOUBLE,
|
||||
INT,
|
||||
} parse_number_result;
|
||||
|
||||
static parse_number_result
|
||||
ParseNumber(s8 text, isize *_at, isize *current_line, s8 file, i64 *_i, f64 *_f)
|
||||
{
|
||||
isize at = *_at;
|
||||
if ((text.data[at] < '0' || text.data[at] > '9') && text.data[at] != '-')
|
||||
return NOT_A_NUMBER;
|
||||
int dot_count = 0, exp_count = 0;
|
||||
isize len = 0;
|
||||
while ((text.data[at + len] >= '0' && text.data[at + len] <= '9') || text.data[at + len] == '.' ||
|
||||
text.data[at + len] == 'e' || text.data[at + len] == 'E' || text.data[at + len] == '+' ||
|
||||
text.data[at + len] == '-')
|
||||
{
|
||||
++len;
|
||||
if (text.data[at + len] == '.')
|
||||
++dot_count;
|
||||
if (text.data[at + len] == 'e' || text.data[at + len] == 'E')
|
||||
++exp_count;
|
||||
}
|
||||
if (dot_count > 1 || exp_count > 1)
|
||||
{
|
||||
if (file.data)
|
||||
printf("%*.s:%zu expected a number\n", (int)file.length, file.data, *current_line);
|
||||
else
|
||||
printf("%zu: expected a number\n", *current_line);
|
||||
return NOT_A_NUMBER;
|
||||
}
|
||||
if (dot_count || exp_count)
|
||||
{
|
||||
s8 n = {.data = &text.data[at], .length = len};
|
||||
s8_parse_f64_result parsed = S8ParseF64(n);
|
||||
*_f = parsed.f;
|
||||
return parsed.ok ? DOUBLE : NOT_A_NUMBER;
|
||||
}
|
||||
else
|
||||
{
|
||||
s8 n = {.data = &text.data[at], .length = len};
|
||||
s8_parse_i64_result parsed = S8ParseI64(n, 10);
|
||||
*_i = parsed.i;
|
||||
return parsed.ok ? INT : NOT_A_NUMBER;
|
||||
}
|
||||
}
|
||||
|
||||
static s8
|
||||
ParseString(s8 text, isize *_at, isize *current_line, s8 file, arena *a)
|
||||
{
|
||||
isize at = *_at;
|
||||
if (text.data[at] != '\"')
|
||||
{
|
||||
if (file.data)
|
||||
printf("%.*s:%zu expected string\n", (int)file.length, file.data, *current_line);
|
||||
else
|
||||
printf("%zu: expected string\n", *current_line);
|
||||
return (s8){0};
|
||||
}
|
||||
++at;
|
||||
isize first_char = at;
|
||||
s8 str = {.data = &text.data[first_char], .length = 0};
|
||||
/* If the string contains control codes, we need to copy it into owned
|
||||
* memory to be able to modify it.
|
||||
*/
|
||||
b32 owns_memory = 0;
|
||||
isize str_at = 0;
|
||||
while (text.data[at] != '\"' && at < text.length)
|
||||
{
|
||||
if (text.data[at] == '\\')
|
||||
{
|
||||
/* Control codes.
|
||||
* If this string is still unowned, copy it into owned memory to be able
|
||||
* to modify it. */
|
||||
if (!owns_memory)
|
||||
{
|
||||
isize total_length = at - first_char;
|
||||
isize i = at;
|
||||
while (text.data[i] != '\"' && i < text.length)
|
||||
++total_length;
|
||||
if (i == text.length)
|
||||
{
|
||||
if (file.data)
|
||||
printf("%.*s:%zu unexpected end of file\n", (int)file.length, file.data, *current_line);
|
||||
else
|
||||
printf("%zu: unexpected end of file\n", *current_line);
|
||||
return (s8){0};
|
||||
}
|
||||
u8 *mem = alloc(a, u8, total_length);
|
||||
if (at > first_char)
|
||||
memcpy(mem, str.data, at - first_char);
|
||||
str.data = mem;
|
||||
owns_memory = 1;
|
||||
}
|
||||
|
||||
++at;
|
||||
if (at == text.length)
|
||||
{
|
||||
if (file.data)
|
||||
printf("%.*s:%zu unexpected end of file\n", (int)file.length, file.data, *current_line);
|
||||
else
|
||||
printf("%zu: unexpected end of file\n", *current_line);
|
||||
return (s8){0};
|
||||
}
|
||||
switch (text.data[at])
|
||||
{
|
||||
case '\"':
|
||||
case '\\':
|
||||
str.data[str_at] = text.data[at];
|
||||
++str_at;
|
||||
++at;
|
||||
break;
|
||||
case 'b':
|
||||
str.data[str_at] = '\b';
|
||||
++str_at;
|
||||
++at;
|
||||
break;
|
||||
case 'f':
|
||||
str.data[str_at] = '\f';
|
||||
++str_at;
|
||||
++at;
|
||||
break;
|
||||
case 'n':
|
||||
str.data[str_at] = '\n';
|
||||
++str_at;
|
||||
++at;
|
||||
break;
|
||||
case 'r':
|
||||
str.data[str_at] = '\r';
|
||||
++str_at;
|
||||
++at;
|
||||
break;
|
||||
case 't':
|
||||
str.data[str_at] = '\t';
|
||||
++str_at;
|
||||
++at;
|
||||
break;
|
||||
case 'u':
|
||||
/* 4 hex digits unicode codepoint
|
||||
* NOT IMPLEMENTED YET. */
|
||||
default:
|
||||
if (file.data)
|
||||
printf("%.*s:%zu unsupported control code '%c'\n",
|
||||
(int)file.length,
|
||||
file.data,
|
||||
*current_line,
|
||||
text.data[at]);
|
||||
else
|
||||
printf("%zu: unsupported control code '%c'\n", *current_line, text.data[at]);
|
||||
return (s8){0};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Normal character */
|
||||
if (owns_memory)
|
||||
str.data[str_at] = text.data[at];
|
||||
++str_at;
|
||||
++at;
|
||||
}
|
||||
}
|
||||
/* Consume terminating quotes */
|
||||
++at;
|
||||
str.length = str_at;
|
||||
|
||||
*_at = at;
|
||||
return str;
|
||||
}
|
||||
|
||||
/* Return 1 if it reaches the ned of the file */
|
||||
static b32
|
||||
ConsumeWhitespace(s8 text, isize *_at, isize *current_line)
|
||||
{
|
||||
isize at = *_at;
|
||||
while (at < text.length)
|
||||
{
|
||||
if (text.data[at] == ' ' || text.data[at] == '\t' || text.data[at] == '\r')
|
||||
{
|
||||
++at;
|
||||
}
|
||||
else if (text.data[at] == '\n')
|
||||
{
|
||||
++at;
|
||||
++*current_line;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
*_at = at;
|
||||
return at == text.length;
|
||||
}
|
||||
|
||||
#define ConsumeWhitespaceNoEof(_Text, _PtrAt, _CurLine, _File, _ErrRet) \
|
||||
if (ConsumeWhitespace((_Text), (_PtrAt), (_CurLine))) \
|
||||
{ \
|
||||
if ((_File).data) \
|
||||
printf("%.*s:%zu unexpected end of file\n", (int)(_File).length, (_File).data, *(_CurLine)); \
|
||||
else \
|
||||
printf("%zu: unexpected end of file\n", *(_CurLine)); \
|
||||
return (_ErrRet); \
|
||||
}
|
||||
|
||||
#define ExpectCharacter(_Char, _Text, _At, _CurLine, _File, _ErrRet) \
|
||||
if ((_Text).data[(_At)] != (_Char)) \
|
||||
{ \
|
||||
if ((_File).data) \
|
||||
printf("%.*s:%zu expected '%c', got '%c'\n", \
|
||||
(int)(_File).length, \
|
||||
(_File).data, \
|
||||
*(_CurLine), \
|
||||
(_Char), \
|
||||
(_Text).data[(_At)]); \
|
||||
else \
|
||||
printf("%zu: expected '%c' got '%c'\n", *(_CurLine), (_Char), (_Text).data[(_At)]); \
|
||||
return (_ErrRet); \
|
||||
} \
|
||||
(_At) += 1;
|
||||
|
||||
static json *
|
||||
ParseJSONImpl(s8 text, isize *_at, json *sibling, isize *current_line, s8 file, arena *a)
|
||||
{
|
||||
isize at = *_at;
|
||||
|
||||
json *j = alloc(a, json);
|
||||
j->next = sibling;
|
||||
|
||||
if (text.data[at] == '{')
|
||||
{
|
||||
/* Parse object */
|
||||
++at;
|
||||
|
||||
j->type = JSON_TYPE_OBJECT;
|
||||
|
||||
ConsumeWhitespaceNoEof(text, &at, current_line, file, 0);
|
||||
|
||||
while (text.data[at] != '}' && at < text.length)
|
||||
{
|
||||
/* "string" : value [,] */
|
||||
s8 key = ParseString(text, &at, current_line, file, a);
|
||||
if (!key.data)
|
||||
return 0;
|
||||
|
||||
ConsumeWhitespaceNoEof(text, &at, current_line, file, 0);
|
||||
ExpectCharacter(':', text, at, current_line, file, 0);
|
||||
ConsumeWhitespaceNoEof(text, &at, current_line, file, 0);
|
||||
|
||||
/* Value */
|
||||
if (text.data[at] == '{' || text.data[at] == '[')
|
||||
{
|
||||
/* Object or array, recurse into it.
|
||||
* JSON objects are unordered, so it does not matter that we
|
||||
* effectively reverse the order here */
|
||||
j->value.first_child = ParseJSONImpl(text, &at, j->value.first_child, current_line, file, a);
|
||||
if (!j->value.first_child)
|
||||
return 0;
|
||||
j->value.first_child->key = key;
|
||||
}
|
||||
else if (text.data[at] == '\"')
|
||||
{
|
||||
json *child = alloc(a, json);
|
||||
child->key = key;
|
||||
child->type = JSON_TYPE_STRING;
|
||||
child->value.s = ParseString(text, &at, current_line, file, a);
|
||||
child->next = j->value.first_child;
|
||||
j->value.first_child = child;
|
||||
}
|
||||
else if ((text.data[at] >= '0' && text.data[at] <= '9') || text.data[at] == '-')
|
||||
{
|
||||
/* Number. */
|
||||
f64 f;
|
||||
i64 i;
|
||||
parse_number_result res = ParseNumber(text, &at, current_line, file, &i, &f);
|
||||
if (res == NOT_A_NUMBER)
|
||||
return 0;
|
||||
json *child = alloc(a, json);
|
||||
child->key = key;
|
||||
if (res == INT)
|
||||
{
|
||||
child->type = JSON_TYPE_INTEGER;
|
||||
child->value.i = i;
|
||||
}
|
||||
else /* DOUBLE */
|
||||
{
|
||||
child->type = JSON_TYPE_FLOAT;
|
||||
child->value.f = f;
|
||||
}
|
||||
child->next = j->value.first_child;
|
||||
j->value.first_child = child;
|
||||
}
|
||||
|
||||
ConsumeWhitespaceNoEof(text, &at, current_line, file, 0);
|
||||
|
||||
/* Consume ',' We deviate from the spec: We actually support trailing commas,
|
||||
* because a) thats simpler code and b) not allowing trailing commas is stupid. */
|
||||
if (text.data[at] == ',')
|
||||
{
|
||||
++at;
|
||||
ConsumeWhitespaceNoEof(text, &at, current_line, file, 0);
|
||||
}
|
||||
else if (text.data[at] != '}')
|
||||
{
|
||||
if (file.data)
|
||||
printf("%.*s:%zu unexpected character '%c' after value\n",
|
||||
(int)file.length,
|
||||
file.data,
|
||||
*current_line,
|
||||
text.data[at]);
|
||||
else
|
||||
printf("%zu: unexpected character '%c' after value\n", *current_line, text.data[at]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
++at;
|
||||
}
|
||||
else if (text.data[at] == '[')
|
||||
{
|
||||
/* Parse array */
|
||||
++at;
|
||||
j->type = JSON_TYPE_ARRAY;
|
||||
|
||||
json *last_child = NULL;
|
||||
while (text.data[at] != ']' && at < text.length)
|
||||
{
|
||||
/* value [,] ... */
|
||||
ConsumeWhitespaceNoEof(text, &at, current_line, file, 0);
|
||||
if (text.data[at] == '{' || text.data[at] == '[')
|
||||
{
|
||||
/* Object or array, recurse into it. */
|
||||
json *child = ParseJSONImpl(text, &at, NULL, current_line, file, a);
|
||||
if (!child)
|
||||
return 0;
|
||||
if (!j->value.first_child)
|
||||
j->value.first_child = child;
|
||||
if (last_child)
|
||||
last_child->next = child;
|
||||
last_child = child;
|
||||
}
|
||||
else if (text.data[at] == '\"')
|
||||
{
|
||||
json *child = alloc(a, json);
|
||||
child->value.s = ParseString(text, &at, current_line, file, a);
|
||||
child->type = JSON_TYPE_STRING;
|
||||
if (!j->value.first_child)
|
||||
j->value.first_child = child;
|
||||
if (last_child)
|
||||
last_child->next = child;
|
||||
last_child = child;
|
||||
}
|
||||
else if ((text.data[at] >= '0' && text.data[at] <= '9') || text.data[at] == '-')
|
||||
{
|
||||
/* Number. */
|
||||
f64 f;
|
||||
i64 i;
|
||||
parse_number_result res = ParseNumber(text, &at, current_line, file, &i, &f);
|
||||
if (res == NOT_A_NUMBER)
|
||||
return 0;
|
||||
json *child = alloc(a, json);
|
||||
if (res == INT)
|
||||
{
|
||||
child->value.i = i;
|
||||
child->type = JSON_TYPE_INTEGER;
|
||||
}
|
||||
else /* DOUBLE */
|
||||
{
|
||||
child->value.f = f;
|
||||
child->type = JSON_TYPE_FLOAT;
|
||||
}
|
||||
if (!j->value.first_child)
|
||||
j->value.first_child = child;
|
||||
if (last_child)
|
||||
last_child->next = child;
|
||||
last_child = child;
|
||||
}
|
||||
|
||||
ConsumeWhitespaceNoEof(text, &at, current_line, file, 0);
|
||||
|
||||
/* Consume ',' We deviate from the spec: We actually support trailing commas,
|
||||
* because a) thats simpler code and b) not allowing trailing commas is stupid. */
|
||||
if (text.data[at] == ',')
|
||||
{
|
||||
++at;
|
||||
ConsumeWhitespaceNoEof(text, &at, current_line, file, 0);
|
||||
}
|
||||
else if (text.data[at] != ']')
|
||||
{
|
||||
if (file.data)
|
||||
printf("%.*s:%zu unexpected character '%c' after value\n",
|
||||
(int)file.length,
|
||||
file.data,
|
||||
*current_line,
|
||||
text.data[at]);
|
||||
else
|
||||
printf("%zu: unexpected character '%c' after value\n", *current_line, text.data[at]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
++at;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (file.data)
|
||||
printf("%.*s:%zu unexpected character %c\n", (int)file.length, file.data, *current_line, text.data[0]);
|
||||
else
|
||||
printf("%zu: unexpected character %c\n", *current_line, text.data[0]);
|
||||
}
|
||||
*_at = at;
|
||||
return j;
|
||||
}
|
||||
|
||||
RTJ_API json *
|
||||
ParseJSON(s8 text, s8 file, arena *a)
|
||||
{
|
||||
isize line = 1, at = 0;
|
||||
return ParseJSONImpl(text, &at, NULL, &line, file, a);
|
||||
}
|
||||
|
||||
RTJ_API json *
|
||||
GetJSONMember(json *o, s8 key)
|
||||
{
|
||||
if (o->type != JSON_TYPE_OBJECT)
|
||||
return NULL;
|
||||
JsonForEach(child, o)
|
||||
{
|
||||
if (S8Equals(child->key, key))
|
||||
return child;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RTJ_API json *
|
||||
GetJSONArrayElement(json *a, isize i)
|
||||
{
|
||||
if (a->type != JSON_TYPE_ARRAY)
|
||||
return NULL;
|
||||
isize at = 0;
|
||||
JsonForEach(child, a)
|
||||
{
|
||||
if (at == i)
|
||||
return child;
|
||||
++at;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RTJ_API isize
|
||||
GetJSONArrayLength(json *a)
|
||||
{
|
||||
if (a->type != JSON_TYPE_ARRAY)
|
||||
return -1;
|
||||
isize at = 0;
|
||||
JsonForEach(child, a) { ++at; }
|
||||
return at;
|
||||
}
|
||||
|
||||
#endif
|
||||
Loading…
Reference in New Issue
Block a user