Files
libakgl/include/akgl/json_helpers.h
Andrew Kesterson 9924d74dcc Namespace every exported symbol, and bump to 0.5.0
Closes internal-consistency items 1 through 6, 12 and 13. Every include guard
is _AKGL_<FILE>_H_, every in-project header include is angled, and every
exported function, type and global carries the akgl_ prefix. This is an ABI
break; the soname goes to libakgl.so.0.5. TODO.md carries the full rename
table.

The renames were driven by renaming each declaration and letting the compiler
find the uses, not by pattern substitution: renderer, physics and camera are
also parameter and struct-member names, and a sed would have rewritten
map->physics and every akgl_RenderBackend *renderer parameter without a word.

Item 4 turned out not to be cosmetic. The library exported a global called
renderer and tests/character.c defined an SDL_Renderer *renderer of its own;
the executable's definition preempted the library's, akgl_sprite_load_json
read a SDL_Renderer * through an akgl_RenderBackend *, and every texture load
in that suite failed. The suite reported success anyway, because libakerror's
unhandled-error handler ends in exit(errctx->status), exit keeps only the low
byte, and AKGL_ERR_SDL is exactly 256. So character had been green while
running one of its four tests, and every suite in the tree was unable to fail
on the most common status in a library built on SDL.

Both are fixed. tests/testutil.h gains TEST_TRAP_UNHANDLED_ERRORS(), which
collapses any status a byte cannot carry onto 1, and every suite installs it.
character binds a real backend with akgl_render_2d_bind. Its fourth test then
runs for the first time and fails on a defect it has asserted all along, so
akgl_heap_release_character now walks state_sprites with
AKGL_ITERATOR_OP_RELEASE and destroys the property set before zeroing the slot
-- TODO.md Defects item 21 and half of Carried over item 1.

AKGL_TIME_ONESEC_MS said "one second in milliseconds" and held 1000000, so
akgl_game_state_lock waited roughly sixteen minutes rather than one second. It
is AKGL_TIME_ONEMS_NS now, the budget is its own named constant, and
tests/game.c holds the mutex from a second thread to assert the wait -- the
contended path had no coverage at all.

Headers are self-contained and it is enforced: AKGL_PUBLIC_HEADERS drives both
install() and a generated translation unit per header, so a header that ships
is a header that is checked. Writing that found registry.h, which used
SDL_PropertiesID in eight declarations and included no SDL header.

23/23 suites pass, memcheck is clean, reindent --check is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:35 -04:00

238 lines
12 KiB
C

/**
* @file json_helpers.h
* @brief Typed jansson accessors that report "missing" and "wrong type" as errors.
*
* Every asset in this library is described by a JSON document, and jansson's own
* accessors answer "missing key", "wrong type", and "index past the end" all
* with the same `NULL`. These wrappers split those apart -- AKERR_KEY,
* AKERR_TYPE, AKERR_OUTOFBOUNDS -- so a malformed asset file produces a message
* naming the key and what was wrong with it, instead of a `NULL` dereference
* three frames later.
*
* Conventions that run through the whole set, so they need not be repeated per
* function:
*
* - **Absence is an error here.** Unlike the search functions in libakstdlib,
* these treat a missing key as AKERR_KEY. An *optional* key is expressed by
* passing the resulting error to akgl_get_json_with_default() rather than by
* the accessor staying quiet.
* - **The result comes back through @p dest**, because the return value is the
* error context.
* - **`json_t *` results are borrowed, not owned.** Objects and arrays are
* returned as pointers into the document; they are valid until the document is
* freed and must not be `json_decref`'d.
* - **@p dest is not `NULL`-checked** except where noted, so a `NULL` there is a
* crash rather than an error context.
* - **The document itself is never modified.**
*/
#ifndef _AKGL_JSON_HELPERS_H_
#define _AKGL_JSON_HELPERS_H_
#include <akerror.h>
#include <jansson.h>
#include <akgl/staticstring.h>
/**
* @brief Read a nested object out of a JSON object.
* @param obj The object to read from. Required.
* @param key The member name to look up. A `NULL` key is reported as a missing
* key rather than as a `NULL` pointer.
* @param dest Receives a borrowed pointer to the nested object; not written on
* any failure path. Not checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent. The message names it.
* @throws AKERR_TYPE If @p key is present but is not an object.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest);
/**
* @brief Read a boolean out of a JSON object.
* @param obj The object to read from. Required.
* @param key The member name to look up.
* @param dest Receives the value; not written on any failure path. Not checked
* for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not `true` or `false`. A `0`
* or `1` is a number in JSON, and is refused here.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest);
/**
* @brief Read an integer out of a JSON object.
* @param obj The object to read from. Required.
* @param key The member name to look up.
* @param dest Receives the value, narrowed to `int`. Not written on any failure
* path, and not checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not an integer. This is strict:
* `3.0` is a real in JSON and is refused, not truncated. Use
* akgl_get_json_number_value() where either spelling should be accepted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_integer_value(json_t *obj, char *key, int *dest);
/**
* @brief Read a number out of a JSON object as a `float`.
* @param obj The object to read from. Required.
* @param key The member name to look up.
* @param dest Receives the value, narrowed to `float`. Not written on any
* failure path, and not checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not a number. Integers and
* reals are both accepted, so `1` and `1.0` behave alike.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char *key, float *dest);
/**
* @brief Read a number out of a JSON object as a `double`.
*
* The full-precision form of akgl_get_json_number_value(), for the physics
* constants, which are `double`.
*
* @param obj The object to read from. Required.
* @param key The member name to look up.
* @param dest Receives the value. Not written on any failure path, and not
* checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not a number.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest);
/**
* @brief Read a string out of a JSON object into a pooled akgl_String.
*
* Unlike the other accessors this copies, because the caller wants a buffer it
* can keep rather than a pointer into the document. `*dest` doubles as an input:
* `NULL` means "claim one for me", non-`NULL` means "write into this one".
*
* @param obj The object to read from. Required.
* @param key The member name to look up. Required -- checked here, unlike the
* other accessors in this file.
* @param dest Address of the destination string. Required, and it must be
* *initialized*: set `*dest` to `NULL` to have a pool string claimed
* for you, or to a claimed string to write in place. An
* indeterminate `*dest` is dereferenced. Either way the caller
* releases it with akgl_heap_release_string().
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj, @p key, or @p dest is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not a string.
* @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
*
* @note A value longer than #AKGL_MAX_STRING_LENGTH is truncated silently, and
* is left without a terminator -- this is `strncpy`, not `strlcpy`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest);
/**
* @brief Read an array out of a JSON object.
* @param obj The object to read from. Required.
* @param key The member name to look up.
* @param dest Receives a borrowed pointer to the array; not written on any
* failure path. Not checked for `NULL`. Use `json_array_size()` on
* it and the `akgl_get_json_array_index_*` family to walk it.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
* @throws AKERR_KEY If @p key is absent.
* @throws AKERR_TYPE If @p key is present but is not an array.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest);
/**
* @brief Read one element of a JSON array as an object.
* @param array The array to read from. Required.
* @param index Zero-based element index. A negative index is reported the same
* way as one past the end.
* @param dest Receives a borrowed pointer to the element; not written on any
* failure path. Not checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p array is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array. The message
* reports the index.
* @throws AKERR_TYPE If the element is not an object.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest);
/**
* @brief Read one element of a JSON array as an integer.
* @param array The array to read from. Required.
* @param index Zero-based element index.
* @param dest Receives the value, narrowed to `int`; not written on any failure
* path. Not checked for `NULL`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p array is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array.
* @throws AKERR_TYPE If the element is not an integer. Strict, as in
* akgl_get_json_integer_value().
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_integer(json_t *array, int index, int *dest);
/**
* @brief Read one element of a JSON array into a pooled akgl_String.
* @param array The array to read from. Required.
* @param index Zero-based element index.
* @param dest Address of the destination string, with the same claim-or-reuse
* contract as akgl_get_json_string_value(): required, must be
* initialized, and released by the caller.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p array or @p dest is `NULL`.
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array.
* @throws AKERR_TYPE If the element is not a string.
* @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
*
* @note Truncates silently at #AKGL_MAX_STRING_LENGTH, as
* akgl_get_json_string_value() does.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest);
/**
* @brief Turn a "key not found" error from one of the accessors above into a default value.
*
* This is how an optional key is spelled. Run the accessor, hand its error
* context here along with the fallback, and a missing key becomes @p dest
* holding @p defval and a `NULL` return; anything else propagates untouched:
*
* ```c
* int width = 0;
* int width_default = 32;
* PASS(errctx,
* akgl_get_json_with_default(
* akgl_get_json_integer_value(json, "width", &width),
* &width_default,
* &width,
* sizeof(int)
* ));
* ```
*
* A `NULL` @p e -- the accessor succeeded -- is the ordinary case and returns at
* once without touching @p dest. When the error *is* consumed it is also
* released, so the caller must not release it again.
*
* @param e The error context to inspect, straight from an accessor.
* `NULL` means "no error", which is not itself an error.
* @param defval The fallback value to copy. Required when @p e is non-`NULL`.
* @param dest Where to copy it. Required when @p e is non-`NULL`. Must be the
* same destination the accessor was given, and at least
* @p defsize bytes.
* @param defsize Bytes to copy out of @p defval. Trusted, not derived -- it must
* match the type both sides actually are, since this is a
* `memcpy` through `void *` with no type information.
* @return `NULL` when the error was consumed or there was none, otherwise an
* error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p e is non-`NULL` and @p defval or @p dest is
* `NULL`.
* @throws AKERR_* Whatever @p e carried, if it is not one of the statuses this
* defaults on -- an AKERR_TYPE from a key that exists but is the wrong
* type propagates, which is right: that is a malformed document, not an
* omitted setting.
*
* @note It defaults on AKERR_KEY and AKERR_INDEX, but *not* on
* AKERR_OUTOFBOUNDS -- which is the status the
* `akgl_get_json_array_index_*` family actually raises for a short array.
* So this pairs with the object accessors and does not currently give an
* array index a default.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize);
#endif // _AKGL_JSON_HELPERS_H_