Files
libakgl/include/akgl/json_helpers.h
Andrew Kesterson 19530f6a97 Fix the type, macro and state-table defects, and the leftover debris
Closes internal-consistency items 19 through 36, 38 and 41. Item 37, the ~180
redundant casts, is deliberately left open with its reasoning in TODO.md: the
benefit only arrives once the build turns on the warnings those casts suppress,
and doing it before that is churn across the two files with the most
outstanding functional defects.

The two that were real bugs are in the actor state table.
AKGL_ACTOR_STATE_STRING_NAMES was declared [AKGL_ACTOR_MAX_STATES+1] and
defined [32], so a consumer trusting the declared bound read past the object;
and indices 11 and 12 were named UNDEFINED_11 and UNDEFINED_12 where actor.h
has MOVING_IN and MOVING_OUT, so no character JSON could bind a sprite to
either state. tests/registry.c now walks the whole table -- every entry
non-NULL, every entry resolving to its own bit, no two entries sharing a name.

The bitmask macros are parenthesized and AKGL_BITMASK_CLEAR has lost the
semicolon inside its body. Writing tests/bitmasks.c for that turned up
something worth knowing: the obvious test does not catch it. For a bit that is
set, the misparse `!(mask & bit) == bit` gives the same answer as the correct
one. It only diverges for an unset bit whose value is not 1, and that is the
shape the suite uses now.

akgl_draw_background was the last public function outside the error protocol.
It takes a backend like everything else in draw.h, restores the draw colour it
found, and is tested -- TODO.md had it filed under "needs the offscreen
renderer harness", which was never true; what it needed was to stop reading the
global.

All eight registry initializers go through one helper, so the seven that leaked
an SDL_PropertiesID on every call after the first no longer do. Fixed in the
same place because it is the same function: akgl_registry_init never called
akgl_registry_init_properties, which made akgl_set_property a silent no-op for
anyone not going through akgl_game_init -- Defects, Known and still open item 3.

Also: AKGL_COLLIDE_RECTANGLES (three open parens, two closes) and akgl_Frame
deleted, float32_t/float64_t used consistently, the developer-specific debug
logging removed from the controller inner loop, the abandoned SDL_GetBasePath
comments removed, nine unused locals removed, and dst renamed to dest.

akgl_game_update's default flags no longer OR the same bit twice. That changes
nothing today, and the reason is Performance item 32: the loop never reads
either bit, which is why every actor is updated sixteen times a frame. Still
open.

25/25 pass, memcheck clean, reindent --check clean.

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

239 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 <akgl/types.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, float32_t *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, float64_t *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_