diff --git a/n_cjson_helpers.c b/n_cjson_helpers.c index ebd27a6b..e76dc286 100644 --- a/n_cjson_helpers.c +++ b/n_cjson_helpers.c @@ -12,6 +12,7 @@ */ #include +#include #include #include #include "n_lib.h" @@ -465,3 +466,685 @@ int JBaseItemType(int type) } return type; } + +void JMerge(J *target, J *source) +{ + if (source == NULL) { + return; + } + if (target == NULL) { + // Target is NULL but source is not - delete source to avoid leak + JDelete(source); + return; + } + J *item = source->child; + while (item != NULL) { + J *next = item->next; + const char *name = item->string; + + // Remove from source + JDetachItemViaPointer(source, item); + + // Delete existing field in target if present + JDeleteItemFromObject(target, name); + + // Add to target + JAddItemToObject(target, name, item); + + item = next; + } + JDelete(source); +} + +/***************************************************************************** + * + * JObjectf / JAddf - printf-style JSON object construction + * + ***************************************************************************** + * + * OVERVIEW + * -------- + * JObjectf creates a JSON object using a printf-inspired format string syntax. + * This provides a concise, readable way to construct JSON objects inline, + * similar to how printf provides a concise way to format strings. + * + * Just as printf("name: %s, age: %d", name, 42) formats a string, + * JObjectf("name:%s age:%d", name, 42) creates {"name":"...","age":42} + * + * BASIC SYNTAX + * ------------ + * The format string is a whitespace-separated list of field definitions. + * Each field definition has the form: name:value + * + * Whitespace includes: space, tab, newline (\n), carriage return (\r), comma (,) + * This means you can write formats in multiple styles: + * + * "name:%s age:%d" // space-separated + * "name:%s, age:%d" // comma-separated (JSON-like) + * "name:%s,age:%d" // compact + * "name:%s\nage:%d" // multi-line in source + * + * FIELD NAMES + * ----------- + * Field names can be: + * - A literal identifier: letters, digits, underscore (e.g., "myField") + * - A format specifier %s: the name is taken from the next argument + * + * Examples: + * JObjectf("status:%s", "ok") // {"status":"ok"} + * JObjectf("%s:%s", "status", "ok") // {"status":"ok"} + * JObjectf("%s:%d", fieldName, 42) // {:42} + * + * VALUE SPECIFIERS + * ---------------- + * Values can be format specifiers (pulling from arguments) or literals: + * + * Format specifiers (consume an argument): + * %s - String (const char*) + * %d - Integer (JINTEGER, typically long long) + * %f - Floating point number (JNUMBER, typically double) + * %b - Boolean (int: 0=false, non-zero=true) + * %o - JSON object/array (J*) - object is MOVED, not copied + * %a - Synonym for %o (for "array", but accepts any J*) + * + * Literal values (no argument consumed): + * true - Boolean true + * false - Boolean false + * 123 - Integer (decimal digits, optional leading minus) + * 123.45 - Floating point (contains decimal point) + * 'text' - String literal with single quotes (escape: \' and \\) + * "text" - String literal with double quotes (escape: \" and \\) + * word - Unquoted string (letters/digits/underscore/dot, ends at whitespace) + * e.g., status:ok, mode:continuous, file:data.qo + * + * EXAMPLES + * -------- + * + * Basic usage with format specifiers: + * + * J *obj = JObjectf("name:%s age:%d active:%b", + * "Alice", 30, true); + * // Creates: {"name":"Alice","age":30,"active":true} + * + * Using literal values (no arguments needed): + * + * J *obj = JObjectf("status:'pending' count:0 enabled:false"); + * // Creates: {"status":"pending","count":0,"enabled":false} + * + * Unquoted string values (simple words without quotes): + * + * J *obj = JObjectf("status:ok mode:continuous file:data.qo"); + * // Creates: {"status":"ok","mode":"continuous","file":"data.qo"} + * + * Mixing literals and format specifiers: + * + * J *obj = JObjectf("type:'sensor' id:%s reading:%f", + * sensorId, temperature); + * // Creates: {"type":"sensor","id":"...","reading":23.5} + * + * Dynamic field names: + * + * J *obj = JObjectf("%s:%d %s:%d", "x", 10, "y", 20); + * // Creates: {"x":10,"y":20} + * + * Nested objects (object is MOVED into result): + * + * J *inner = JObjectf("lat:%f lon:%f", 40.7128, -74.0060); + * J *obj = JObjectf("name:%s location:%o", "NYC", inner); + * // Creates: {"name":"NYC","location":{"lat":40.7128,"lon":-74.006}} + * // NOTE: 'inner' is now owned by 'obj', do not JDelete(inner)! + * + * String literals with embedded quotes (single or double quotes work): + * + * J *obj = JObjectf("message:'It\\'s working!'"); + * // Creates: {"message":"It's working!"} + * + * J *obj = JObjectf("message:\"It's working!\""); + * // Creates: {"message":"It's working!"} (no escape needed) + * + * J *obj = JObjectf("quote:\"She said \\\"Hello\\\"\""); + * // Creates: {"quote":"She said \"Hello\""} + * + * Multi-line format for readability: + * + * J *obj = JObjectf( + * "req:note.add" + * "file:%s" + * "body:%o", + * filename, bodyObj); + * + * Building Notecard requests and commands: + * + * // A Notecard request (expects response) + * J *req = JObjectf("req:hub.set product:com.blues.app mode:continuous"); + * // Creates: {"req":"hub.set","product":"com.blues.app","mode":"continuous"} + * + * // A Notecard command (no response expected) + * J *cmd = JObjectf("cmd:card.led mode:breathe"); + * // Creates: {"cmd":"card.led","mode":"breathe"} + * + * // Full request example with mixed values + * J *req = JObjectf("req:note.add file:sensors.qo body:%o", sensorData); + * // Creates: {"req":"note.add","file":"sensors.qo","body":{...}} + * + * USING JAddf + * ----------- + * JAddf is a convenience macro that merges fields into an existing object: + * + * J *obj = JCreateObject(); + * JAddStringToObject(obj, "base", "value"); + * JAddf(obj, "extra:%d flag:%b", 42, true); + * // obj now has: {"base":"value","extra":42,"flag":true} + * + * JAddf overwrites existing fields with the same name: + * + * J *obj = JObjectf("status:'pending'"); + * JAddf(obj, "status:'complete'"); + * // obj now has: {"status":"complete"} + * + * ERROR HANDLING + * -------------- + * - If format is NULL, returns an empty object. + * - If memory allocation fails, returns NULL. + * - If format is malformed (e.g., missing ':'), parsing stops and returns + * a partial object containing all fields successfully parsed so far. + * - If %o/%a argument is NULL, that field is silently skipped. + * - If a duplicate field name appears, the later value overwrites the earlier. + * + * MEMORY MANAGEMENT + * ----------------- + * - The returned J* must be freed with JDelete() when no longer needed. + * - Objects passed via %o/%a are MOVED into the result and should NOT be + * freed separately. They become owned by the returned object. + * - String arguments (%s for values) are copied; the original strings + * are not modified and can be freed independently. + * + *****************************************************************************/ + +/*! + * @brief Check if a character is a field separator (whitespace or comma). + * + * Field separators in the JObjectf format string include all traditional + * whitespace characters plus comma, allowing flexible formatting styles. + * + * @param c The character to check. + * @return true if c is a separator, false otherwise. + */ +static bool _jObjectf_isSeparator(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ','; +} + +/*! + * @brief Skip past any separator characters in the format string. + * + * @param p Pointer to current position in format string. + * @return Pointer to next non-separator character (or end of string). + */ +static const char *_jObjectf_skipSeparators(const char *p) +{ + while (*p && _jObjectf_isSeparator(*p)) { + p++; + } + return p; +} + +/*! + * @brief Check if a character is valid in a literal field name. + * + * Valid characters are: letters (a-z, A-Z), digits (0-9), underscore (_). + * + * @param c The character to check. + * @return true if c is valid in a field name, false otherwise. + */ +static bool _jObjectf_isNameChar(char c) +{ + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '_'; +} + +/*! + * @brief Check if a character is valid in an unquoted string value. + * + * Unquoted string values can contain letters, digits, underscore, and dot. + * This allows values like: ok, continuous, data.qo, hub.set, com.blues.app + * + * @param c The character to check. + * @return true if c is valid in an unquoted string value, false otherwise. + */ +static bool _jObjectf_isUnquotedStringChar(char c) +{ + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '_' || + c == '.'; +} + +/*! + * @brief Parse a field name from the format string. + * + * The field name is either: + * - A literal identifier (letters, digits, underscore) + * - The format specifier %s (name taken from va_list argument) + * + * @param p Pointer to current position in format string. + * @param nameBuf Buffer to store the parsed field name. + * @param bufSize Size of nameBuf. + * @param args Pointer to va_list for extracting %s argument. + * @return Pointer to character after the parsed name, or NULL on error. + */ +static const char *_jObjectf_parseName(const char *p, char *nameBuf, + size_t bufSize, va_list *args) +{ + // Check for %s format specifier (name from argument) + if (p[0] == '%' && p[1] == 's') { + const char *argName = va_arg(*args, const char *); + if (argName == NULL) { + nameBuf[0] = '\0'; + return NULL; + } + size_t len = strlen(argName); + if (len >= bufSize) { + len = bufSize - 1; + } + memcpy(nameBuf, argName, len); + nameBuf[len] = '\0'; + return p + 2; + } + + // Parse literal identifier + size_t i = 0; + while (_jObjectf_isNameChar(*p) && i < bufSize - 1) { + nameBuf[i++] = *p++; + } + nameBuf[i] = '\0'; + + if (i == 0) { + // No valid name characters found + return NULL; + } + + return p; +} + +/*! + * @brief Parse a literal number from the format string. + * + * Parses decimal integers or floating point numbers. + * Integer: optional minus, followed by digits (e.g., "123", "-42") + * Float: integer part, decimal point, fractional part (e.g., "3.14", "-0.5") + * + * @param p Pointer to current position in format string. + * @param value Pointer to store the parsed J* value. + * @param endPtr Pointer to store position after parsed number. + * @return true if a number was successfully parsed, false otherwise. + */ +static bool _jObjectf_parseNumber(const char *p, J **value, const char **endPtr) +{ + const char *start = p; + bool isFloat = false; + bool hasDigits = false; + + // Optional leading minus + if (*p == '-') { + p++; + } + + // Integer part + while (*p >= '0' && *p <= '9') { + p++; + hasDigits = true; + } + + // Check for decimal point + if (*p == '.') { + isFloat = true; + p++; + // Fractional part + while (*p >= '0' && *p <= '9') { + p++; + hasDigits = true; + } + } + + if (!hasDigits) { + return false; + } + + // Parse the number + if (isFloat) { + JNUMBER num = JAtoN(start, NULL); + *value = JCreateNumber(num); + } else { + JINTEGER num = JAtoI(start); + *value = JCreateInteger(num); + } + + *endPtr = p; + return (*value != NULL); +} + +/*! + * @brief Parse a quoted string literal from the format string. + * + * String literals can be enclosed in either single quotes ('text') or + * double quotes ("text"). The opening and closing quote must match. + * + * Escape sequences: + * - \' for single quote (in single-quoted strings) + * - \" for double quote (in double-quoted strings) + * - \\ for backslash (in either) + * + * @param p Pointer to opening quote character (' or "). + * @param value Pointer to store the parsed J* string value. + * @param endPtr Pointer to store position after closing quote. + * @return true if string was successfully parsed, false otherwise. + */ +static bool _jObjectf_parseQuotedString(const char *p, J **value, + const char **endPtr) +{ + char quoteChar = *p; + if (quoteChar != '\'' && quoteChar != '"') { + return false; + } + p++; // Skip opening quote + + // First pass: calculate length needed + const char *scan = p; + size_t len = 0; + while (*scan && *scan != quoteChar) { + if (*scan == '\\' && (scan[1] == quoteChar || scan[1] == '\\')) { + scan += 2; + } else { + scan++; + } + len++; + } + + if (*scan != quoteChar) { + // Unterminated string + return false; + } + + // Allocate and copy with escape processing + char *buf = _Malloc(len + 1); + if (buf == NULL) { + return false; + } + + size_t i = 0; + while (*p && *p != quoteChar) { + if (*p == '\\' && (p[1] == quoteChar || p[1] == '\\')) { + buf[i++] = p[1]; + p += 2; + } else { + buf[i++] = *p++; + } + } + buf[i] = '\0'; + + // Skip closing quote + if (*p == quoteChar) { + p++; + } + + *value = JCreateString(buf); + _Free(buf); + *endPtr = p; + + return (*value != NULL); +} + +/*! + * @brief Parse a value from the format string. + * + * Values can be: + * - Format specifiers: %s (string), %d (int), %f (float), %b (bool), %o/%a (object) + * - Literals: true, false, numbers (123, -45.67), quoted strings ('text' or "text") + * - Unquoted strings: words starting with a letter (e.g., ok, continuous, hub.set) + * + * @param p Pointer to current position in format string. + * @param value Pointer to store the parsed J* value (NULL if skipped). + * @param args Pointer to va_list for extracting arguments. + * @param endPtr Pointer to store position after parsed value. + * @return true if value was successfully parsed (even if skipped), false on error. + */ +static bool _jObjectf_parseValue(const char *p, J **value, va_list *args, + const char **endPtr) +{ + *value = NULL; + + // Check for format specifiers + if (*p == '%') { + char spec = p[1]; + p += 2; + *endPtr = p; + + switch (spec) { + case 's': { + const char *str = va_arg(*args, const char *); + if (str != NULL) { + *value = JCreateString(str); + } + return true; + } + case 'd': { + JINTEGER num = va_arg(*args, JINTEGER); + *value = JCreateInteger(num); + return (*value != NULL); + } + case 'f': { + JNUMBER num = (JNUMBER)va_arg(*args, double); + *value = JCreateNumber(num); + return (*value != NULL); + } + case 'b': { + int bval = va_arg(*args, int); + *value = JCreateBool(bval ? true : false); + return (*value != NULL); + } + case 'o': + case 'a': { + J *obj = va_arg(*args, J *); + // If NULL, we skip this field (value stays NULL, return true) + *value = obj; + return true; + } + default: + // Unknown format specifier + return false; + } + } + + // Check for boolean literals + if (strncmp(p, "true", 4) == 0 && + !_jObjectf_isNameChar(p[4])) { + *value = JCreateBool(true); + *endPtr = p + 4; + return (*value != NULL); + } + if (strncmp(p, "false", 5) == 0 && + !_jObjectf_isNameChar(p[5])) { + *value = JCreateBool(false); + *endPtr = p + 5; + return (*value != NULL); + } + + // Check for quoted string literal (single or double quotes) + if (*p == '\'' || *p == '"') { + return _jObjectf_parseQuotedString(p, value, endPtr); + } + + // Check for numeric literal + if ((*p >= '0' && *p <= '9') || (*p == '-' && p[1] >= '0' && p[1] <= '9')) { + return _jObjectf_parseNumber(p, value, endPtr); + } + + // Check for unquoted string (must start with a letter) + // This allows values like: ok, continuous, hub.set, com.blues.app, data.qo + if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')) { + const char *start = p; + while (_jObjectf_isUnquotedStringChar(*p)) { + p++; + } + size_t len = p - start; + char *buf = _Malloc(len + 1); + if (buf == NULL) { + return false; + } + memcpy(buf, start, len); + buf[len] = '\0'; + *value = JCreateString(buf); + _Free(buf); + *endPtr = p; + return (*value != NULL); + } + + // Unrecognized value format + return false; +} + +/*! + @brief Create a JSON object using a printf-style format string. + + JObjectf provides a concise way to construct JSON objects inline, similar to + how printf formats strings. The format string contains field definitions + separated by whitespace (space, tab, newline, carriage return, or comma). + + Each field definition has the form `name:value` where: + - name is a literal identifier or %s (from argument) + - value is a format specifier (%s, %d, %f, %b, %o, %a) or a literal + + Field name specifiers: + - literal: Letters, digits, underscore (e.g., myField) + - %s: Name taken from argument (const char*) + + Value format specifiers: + - %s: String (const char*) + - %d: Integer (JINTEGER) + - %f: Floating point (JNUMBER) + - %b: Boolean (int, 0=false, non-zero=true) + - %o: JSON object/array (J*) - object is MOVED, not copied + - %a: Synonym for %o + + Literal values: + - true / false: Boolean literals + - 123 or -45: Integer literals + - 3.14 or -0.5: Floating point literals + - 'text': String literal with single quotes (use backslash-quote for embedded quote) + - "text": String literal with double quotes (use backslash-quote for embedded quote) + - word: Unquoted string starting with letter (e.g., ok, hub.set, data.qo) + + @param format The format string describing the object structure. + @param ... Arguments corresponding to format specifiers. + + @return A newly allocated J* object, or NULL on memory allocation failure. + The caller is responsible for calling JDelete() on the returned object. + + @note Objects passed via %o/%a are MOVED into the result and should not be + freed separately. + @note If the format string is malformed, returns a partial object with all + fields successfully parsed before the error. + @note If a %o/%a argument is NULL, that field is silently skipped. + + Example usage: + @code + // Simple object with mixed types + J *obj = JObjectf("name:%s age:%d active:%b", "Alice", 30, true); + + // Using unquoted string values + J *obj = JObjectf("status:ok mode:continuous file:data.qo"); + + // Notecard request + J *req = JObjectf("req:hub.set product:com.blues.app mode:continuous"); + + // Nested object + J *loc = JObjectf("lat:%f lon:%f", 40.7128, -74.0060); + J *obj = JObjectf("city:%s location:%o", "NYC", loc); + @endcode + */ +J *JObjectf(const char *format, ...) +{ + va_list args; + va_start(args, format); + J *result = JObjectfv(format, args); + va_end(args); + return result; +} + +/*! + @brief Create a JSON object using a printf-style format string (va_list version). + + This is the va_list variant of JObjectf, useful when wrapping JObjectf + in another variadic function. + + @param format The format string describing the object structure. + @param args The va_list containing arguments for format specifiers. + + @return A newly allocated J* object, or NULL on memory allocation failure. + + @see JObjectf for detailed documentation and examples. + */ +J *JObjectfv(const char *format, va_list args) +{ + J *result = JCreateObject(); + if (result == NULL) { + return NULL; + } + + if (format == NULL) { + return result; + } + + // Buffer for field names - sized to be reasonable for typical use + // Field names in JSON are typically short identifiers + char nameBuf[256]; + + const char *p = format; + + while (*p) { + // Skip leading separators + p = _jObjectf_skipSeparators(p); + if (*p == '\0') { + break; + } + + // Parse field name + const char *afterName = _jObjectf_parseName(p, nameBuf, + sizeof(nameBuf), (va_list *)&args); + if (afterName == NULL || nameBuf[0] == '\0') { + // Parse error or empty name - stop parsing + break; + } + p = afterName; + + // Expect colon separator + if (*p != ':') { + // Malformed - stop parsing + break; + } + p++; // Skip colon + + // Parse value + J *value = NULL; + const char *afterValue; + if (!_jObjectf_parseValue(p, &value, (va_list *)&args, &afterValue)) { + // Parse error - stop parsing + break; + } + p = afterValue; + + // Add field to object (if value is not NULL) + // For %o/%a with NULL argument, we skip the field + if (value != NULL) { + // Delete any existing field with same name (to support overwrites) + JDeleteItemFromObject(result, nameBuf); + JAddItemToObject(result, nameBuf, value); + } + } + + return result; +} diff --git a/note.h b/note.h index b9997ebd..9be8833f 100644 --- a/note.h +++ b/note.h @@ -27,6 +27,7 @@ // In case they're not yet defined #include #include +#include #include #include @@ -1421,6 +1422,151 @@ int JGetItemType(J *item); @returns The base type code. */ int JBaseItemType(int type); +/*! + @brief Merge all fields from source object into target object. + + Moves all fields from the source JSON object into the target object. + If a field with the same name already exists in the target, it is replaced. + The source object is deleted after the merge. + + @param target The target JSON object to merge fields into. + @param source The source JSON object to merge fields from (will be deleted). + */ +void JMerge(J *target, J *source); + +/***************************************************************************** + * JObjectf / JAddf - printf-style JSON object construction + ***************************************************************************** + * + * JObjectf creates JSON objects using a printf-inspired format string. + * Just as printf("name: %s", name) formats a string, JObjectf("name:%s", name) + * creates a JSON object {"name":"..."}. + * + * FORMAT SYNTAX: + * Whitespace-separated field definitions: "field1:value1 field2:value2" + * Whitespace includes: space, tab, newline, carriage return, comma + * + * FIELD NAMES: + * literal - Unquoted identifier (letters, digits, underscore) + * %s - Name taken from argument (const char*) + * + * VALUE SPECIFIERS (consume arguments): + * %s - String (const char*) + * %d - Integer (JINTEGER) + * %f - Floating point (JNUMBER) + * %b - Boolean (int: 0=false, non-zero=true) + * %o - JSON object/array (J*) - MOVED into result, not copied + * %a - Synonym for %o + * + * LITERAL VALUES (no argument): + * true / false - Boolean + * 123 / -45 - Integer + * 3.14 / -0.5 - Float + * 'text' - String with single quotes (escape: \' and \\) + * "text" - String with double quotes (escape: \" and \\) + * word - Unquoted string (letters/digits/underscore/dot) + * e.g., ok, continuous, hub.set, com.blues.app + * + * EXAMPLES: + * + * // Basic types with format specifiers + * JObjectf("name:%s age:%d", "Alice", 30) -> {"name":"Alice","age":30} + * JObjectf("temp:%f", 98.6) -> {"temp":98.6} + * JObjectf("active:%b", true) -> {"active":true} + * + * // Unquoted string values (simple identifiers) + * JObjectf("status:ok") -> {"status":"ok"} + * JObjectf("mode:continuous") -> {"mode":"continuous"} + * JObjectf("file:data.qo") -> {"file":"data.qo"} + * + * // Quoted string values + * JObjectf("msg:'hello world'") -> {"msg":"hello world"} + * JObjectf("msg:\"hello world\"") -> {"msg":"hello world"} + * JObjectf("text:'it\\'s ok'") -> {"text":"it's ok"} + * + * // Literal numbers and booleans + * JObjectf("count:42 rate:3.14") -> {"count":42,"rate":3.14} + * JObjectf("enabled:true disabled:false") -> {"enabled":true,...} + * JObjectf("offset:-10") -> {"offset":-10} + * + * // Dynamic field names with %s + * JObjectf("%s:%d", "x", 10) -> {"x":10} + * JObjectf("%s:%s", key, value) -> {:} + * + * // Notecard requests and commands + * JObjectf("req:hub.set product:com.blues.app mode:continuous") + * -> {"req":"hub.set","product":"com.blues.app","mode":"continuous"} + * JObjectf("cmd:card.led mode:breathe") + * -> {"cmd":"card.led","mode":"breathe"} + * + * // Nested objects with %o + * J *body = JObjectf("temp:%f", 72.5); + * J *req = JObjectf("req:note.add file:sensors.qo body:%o", body); + * -> {"req":"note.add","file":"sensors.qo","body":{"temp":72.5}} + * + * // Inline nested object construction + * J *req = JObjectf("req:note.add file:sensors.qo body:%o", + * JObjectf("temp:%f humidity:%f", 72.5, 45.0)); + * -> {"req":"note.add","file":"sensors.qo","body":{"temp":72.5,"humidity":45.0}} + * + * // Arrays with %a (same as %o) + * J *arr = JCreateArray(); + * JAddItemToArray(arr, JCreateNumber(1)); + * JObjectf("values:%a", arr) -> {"values":[1]} + * + * // Comma separators (JSON-like style) + * JObjectf("a:1, b:2, c:3") -> {"a":1,"b":2,"c":3} + * + * // JAddf to add fields to existing object + * J *obj = JObjectf("base:1"); + * JAddf(obj, "extra:2 more:3"); -> {"base":1,"extra":2,"more":3} + * + * MEMORY: Caller must JDelete() the result. Objects passed via %o/%a become + * owned by the result and should NOT be freed separately. + * + * ERRORS: Returns partial object on parse error, empty object if format is NULL. + * If %o/%a argument is NULL, that field is skipped. + * + * See implementation in n_cjson_helpers.c for comprehensive documentation. + *****************************************************************************/ + +/*! + @brief Create a JSON object using a printf-style format string. + + @return A newly allocated J* object. Caller must call JDelete() when done. + Returns NULL only on memory allocation failure. + + @see JObjectf() in n_cjson_helpers.c for full documentation. + */ +J *JObjectf(const char *format, ...); + +/*! + @brief Create a JSON object using a printf-style format string (va_list version). + + @return A newly allocated J* object. Caller must call JDelete() when done. + + @see JObjectfv() in n_cjson_helpers.c for full documentation. + */ +J *JObjectfv(const char *format, va_list args); + +/*! + @brief Merge printf-style formatted fields into an existing JSON object. + + Convenience macro equivalent to JMerge(obj, JObjectf(fmt, ...)). + Fields with duplicate names in obj will be overwritten. + + @param obj The target JSON object to add fields to. + @param fmt The format string with field definitions. + @param ... Arguments corresponding to format specifiers. + + Example: + @code + J *obj = JCreateObject(); + JAddf(obj, "name:%s age:%d", "Alice", 30); + @endcode + */ +#define JAddf(obj, fmt, ...) JMerge(obj, JObjectf(fmt, ##__VA_ARGS__)) + #define JGetObjectItemName(j) (j->string) // Helper functions for apps that wish to limit their C library dependencies diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f4a0c38a..4f8bbcec 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -108,6 +108,8 @@ add_test(JIsExactString_test) add_test(JIsNullString_test) add_test(JIsPresent_test) add_test(JItoA_test) +add_test(JMerge_test) +add_test(JObjectf_test) add_test(JNtoA_test) add_test(JNumberValue_test) add_test(JPrintUnformatted_test) diff --git a/test/src/JMerge_test.cpp b/test/src/JMerge_test.cpp new file mode 100644 index 00000000..272f331a --- /dev/null +++ b/test/src/JMerge_test.cpp @@ -0,0 +1,196 @@ +/*! + * @file JMerge_test.cpp + * + * Written by the Blues Inc. team. + * + * Copyright (c) 2024 Blues Inc. MIT License. Use of this source code is + * governed by licenses granted by the copyright holder including that found in + * the + * LICENSE + * file. + * + */ + +#include + +#include "n_lib.h" + +namespace +{ + +SCENARIO("JMerge") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("NULL target") { + J *source = JCreateObject(); + REQUIRE(source != NULL); + JAddStringToObject(source, "key", "value"); + + JMerge(NULL, source); + // Should not crash, source is deleted + } + + SECTION("NULL source") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + JAddStringToObject(target, "key", "value"); + + JMerge(target, NULL); + // Should not crash, target unchanged + CHECK(JIsPresent(target, "key")); + CHECK(strcmp(JGetString(target, "key"), "value") == 0); + + JDelete(target); + } + + SECTION("Both NULL") { + JMerge(NULL, NULL); + // Should not crash + } + + SECTION("Merge into empty target") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + + J *source = JCreateObject(); + REQUIRE(source != NULL); + JAddStringToObject(source, "name", "test"); + JAddNumberToObject(source, "count", 42); + JAddBoolToObject(source, "active", true); + + JMerge(target, source); + // source is now deleted, don't use it + + CHECK(JIsPresent(target, "name")); + CHECK(strcmp(JGetString(target, "name"), "test") == 0); + CHECK(JGetNumber(target, "count") == 42); + CHECK(JGetBool(target, "active") == true); + + JDelete(target); + } + + SECTION("Merge from empty source") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + JAddStringToObject(target, "existing", "value"); + + J *source = JCreateObject(); + REQUIRE(source != NULL); + + JMerge(target, source); + // source is now deleted + + CHECK(JIsPresent(target, "existing")); + CHECK(strcmp(JGetString(target, "existing"), "value") == 0); + + JDelete(target); + } + + SECTION("Merge with no overlapping fields") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + JAddStringToObject(target, "targetKey", "targetValue"); + + J *source = JCreateObject(); + REQUIRE(source != NULL); + JAddStringToObject(source, "sourceKey", "sourceValue"); + + JMerge(target, source); + + CHECK(JIsPresent(target, "targetKey")); + CHECK(strcmp(JGetString(target, "targetKey"), "targetValue") == 0); + CHECK(JIsPresent(target, "sourceKey")); + CHECK(strcmp(JGetString(target, "sourceKey"), "sourceValue") == 0); + + JDelete(target); + } + + SECTION("Merge overwrites existing fields") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + JAddStringToObject(target, "shared", "oldValue"); + JAddNumberToObject(target, "count", 10); + + J *source = JCreateObject(); + REQUIRE(source != NULL); + JAddStringToObject(source, "shared", "newValue"); + JAddNumberToObject(source, "count", 99); + + JMerge(target, source); + + CHECK(strcmp(JGetString(target, "shared"), "newValue") == 0); + CHECK(JGetNumber(target, "count") == 99); + + JDelete(target); + } + + SECTION("Merge with nested objects") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + JAddStringToObject(target, "top", "level"); + + J *source = JCreateObject(); + REQUIRE(source != NULL); + J *nested = JAddObjectToObject(source, "nested"); + REQUIRE(nested != NULL); + JAddStringToObject(nested, "inner", "value"); + + JMerge(target, source); + + CHECK(JIsPresent(target, "top")); + J *mergedNested = JGetObject(target, "nested"); + CHECK(mergedNested != NULL); + CHECK(strcmp(JGetString(mergedNested, "inner"), "value") == 0); + + JDelete(target); + } + + SECTION("Merge with arrays") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + + J *source = JCreateObject(); + REQUIRE(source != NULL); + J *arr = JAddArrayToObject(source, "items"); + REQUIRE(arr != NULL); + JAddItemToArray(arr, JCreateNumber(1)); + JAddItemToArray(arr, JCreateNumber(2)); + JAddItemToArray(arr, JCreateNumber(3)); + + JMerge(target, source); + + J *mergedArr = JGetArray(target, "items"); + CHECK(mergedArr != NULL); + CHECK(JGetArraySize(mergedArr) == 3); + + JDelete(target); + } + + SECTION("Merge multiple fields") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + JAddStringToObject(target, "keep", "this"); + + J *source = JCreateObject(); + REQUIRE(source != NULL); + JAddStringToObject(source, "field1", "value1"); + JAddStringToObject(source, "field2", "value2"); + JAddStringToObject(source, "field3", "value3"); + JAddNumberToObject(source, "num", 123); + JAddBoolToObject(source, "flag", false); + + JMerge(target, source); + + CHECK(strcmp(JGetString(target, "keep"), "this") == 0); + CHECK(strcmp(JGetString(target, "field1"), "value1") == 0); + CHECK(strcmp(JGetString(target, "field2"), "value2") == 0); + CHECK(strcmp(JGetString(target, "field3"), "value3") == 0); + CHECK(JGetNumber(target, "num") == 123); + CHECK(JGetBool(target, "flag") == false); + + JDelete(target); + } +} + +} diff --git a/test/src/JObjectf_test.cpp b/test/src/JObjectf_test.cpp new file mode 100644 index 00000000..98b2ce73 --- /dev/null +++ b/test/src/JObjectf_test.cpp @@ -0,0 +1,1339 @@ +/*! + * @file JObjectf_test.cpp + * + * Written by the Blues Inc. team. + * + * Copyright (c) 2024 Blues Inc. MIT License. Use of this source code is + * governed by licenses granted by the copyright holder including that found in + * the + * LICENSE + * file. + * + */ + +#include +#include +#include + +#include "n_lib.h" + +namespace +{ + +// ========================================================================== +// NULL AND EMPTY INPUT HANDLING +// ========================================================================== + +SCENARIO("JObjectf: NULL and empty input") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("NULL format returns empty object") { + J *obj = JObjectf(NULL); + REQUIRE(obj != NULL); + CHECK(obj->child == NULL); + JDelete(obj); + } + + SECTION("Empty format string returns empty object") { + J *obj = JObjectf(""); + REQUIRE(obj != NULL); + CHECK(obj->child == NULL); + JDelete(obj); + } + + SECTION("Whitespace-only format returns empty object") { + J *obj = JObjectf(" \t\n\r "); + REQUIRE(obj != NULL); + CHECK(obj->child == NULL); + JDelete(obj); + } +} + +// ========================================================================== +// VALUE FORMAT SPECIFIER: %s (STRING) +// ========================================================================== + +SCENARIO("JObjectf: %s string format specifier") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Valid string") { + J *obj = JObjectf("name:%s", "Alice"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "name"), "Alice") == 0); + JDelete(obj); + } + + SECTION("Empty string") { + J *obj = JObjectf("name:%s", ""); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "name"), "") == 0); + JDelete(obj); + } + + SECTION("String with spaces") { + J *obj = JObjectf("msg:%s", "hello world"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "hello world") == 0); + JDelete(obj); + } + + SECTION("NULL string argument skips field") { + J *obj = JObjectf("before:%d name:%s after:%d", (JINTEGER)1, (const char *)NULL, (JINTEGER)2); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "before") == 1); + CHECK(!JIsPresent(obj, "name")); + CHECK(JGetInt(obj, "after") == 2); + JDelete(obj); + } + + SECTION("Multiple strings") { + J *obj = JObjectf("first:%s last:%s", "John", "Doe"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "first"), "John") == 0); + CHECK(strcmp(JGetString(obj, "last"), "Doe") == 0); + JDelete(obj); + } +} + +// ========================================================================== +// VALUE FORMAT SPECIFIER: %d (INTEGER) +// ========================================================================== + +SCENARIO("JObjectf: %d integer format specifier") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Positive integer") { + J *obj = JObjectf("count:%d", (JINTEGER)42); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "count") == 42); + JDelete(obj); + } + + SECTION("Negative integer") { + J *obj = JObjectf("offset:%d", (JINTEGER)-100); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "offset") == -100); + JDelete(obj); + } + + SECTION("Zero") { + J *obj = JObjectf("zero:%d", (JINTEGER)0); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "zero") == 0); + JDelete(obj); + } + + SECTION("Large positive integer") { + J *obj = JObjectf("big:%d", (JINTEGER)1000000000LL); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "big") == 1000000000LL); + JDelete(obj); + } + + SECTION("Large negative integer") { + J *obj = JObjectf("neg:%d", (JINTEGER)-1000000000LL); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "neg") == -1000000000LL); + JDelete(obj); + } +} + +// ========================================================================== +// VALUE FORMAT SPECIFIER: %f (FLOATING POINT) +// ========================================================================== + +SCENARIO("JObjectf: %f floating point format specifier") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Positive float") { + J *obj = JObjectf("temp:%f", (JNUMBER)98.6); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "temp") - 98.6) < 0.0001); + JDelete(obj); + } + + SECTION("Negative float") { + J *obj = JObjectf("temp:%f", (JNUMBER)-40.5); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "temp") - (-40.5)) < 0.0001); + JDelete(obj); + } + + SECTION("Zero float") { + J *obj = JObjectf("zero:%f", (JNUMBER)0.0); + REQUIRE(obj != NULL); + CHECK(JGetNumber(obj, "zero") == 0.0); + JDelete(obj); + } + + SECTION("Small decimal") { + J *obj = JObjectf("small:%f", (JNUMBER)0.001); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "small") - 0.001) < 0.00001); + JDelete(obj); + } + + SECTION("Pi approximation") { + J *obj = JObjectf("pi:%f", (JNUMBER)3.14159265359); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "pi") - 3.14159265359) < 0.0000001); + JDelete(obj); + } +} + +// ========================================================================== +// VALUE FORMAT SPECIFIER: %b (BOOLEAN) +// ========================================================================== + +SCENARIO("JObjectf: %b boolean format specifier") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Boolean true (1)") { + J *obj = JObjectf("active:%b", 1); + REQUIRE(obj != NULL); + CHECK(JGetBool(obj, "active") == true); + JDelete(obj); + } + + SECTION("Boolean false (0)") { + J *obj = JObjectf("active:%b", 0); + REQUIRE(obj != NULL); + CHECK(JGetBool(obj, "active") == false); + JDelete(obj); + } + + SECTION("Non-zero treated as true") { + J *obj = JObjectf("active:%b", 42); + REQUIRE(obj != NULL); + CHECK(JGetBool(obj, "active") == true); + JDelete(obj); + } + + SECTION("Negative non-zero treated as true") { + J *obj = JObjectf("active:%b", -1); + REQUIRE(obj != NULL); + CHECK(JGetBool(obj, "active") == true); + JDelete(obj); + } +} + +// ========================================================================== +// VALUE FORMAT SPECIFIER: %o and %a (OBJECT/ARRAY) +// ========================================================================== + +SCENARIO("JObjectf: %o object format specifier") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Valid object is moved") { + J *inner = JCreateObject(); + REQUIRE(inner != NULL); + JAddStringToObject(inner, "key", "value"); + + J *obj = JObjectf("data:%o", inner); + REQUIRE(obj != NULL); + + J *data = JGetObject(obj, "data"); + REQUIRE(data != NULL); + CHECK(strcmp(JGetString(data, "key"), "value") == 0); + + // inner is now owned by obj, only delete obj + JDelete(obj); + } + + SECTION("NULL object skips field") { + J *obj = JObjectf("before:%d data:%o after:%d", (JINTEGER)1, (J *)NULL, (JINTEGER)2); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "before") == 1); + CHECK(!JIsPresent(obj, "data")); + CHECK(JGetInt(obj, "after") == 2); + JDelete(obj); + } + + SECTION("Array via %o") { + J *arr = JCreateArray(); + REQUIRE(arr != NULL); + JAddItemToArray(arr, JCreateNumber(1)); + JAddItemToArray(arr, JCreateNumber(2)); + JAddItemToArray(arr, JCreateNumber(3)); + + J *obj = JObjectf("items:%o", arr); + REQUIRE(obj != NULL); + + J *items = JGetArray(obj, "items"); + REQUIRE(items != NULL); + CHECK(JGetArraySize(items) == 3); + + JDelete(obj); + } + + SECTION("Nested objects") { + J *level2 = JObjectf("c:3"); + J *level1 = JObjectf("b:2 nested:%o", level2); + J *obj = JObjectf("a:1 inner:%o", level1); + + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + + J *inner = JGetObject(obj, "inner"); + REQUIRE(inner != NULL); + CHECK(JGetInt(inner, "b") == 2); + + J *nested = JGetObject(inner, "nested"); + REQUIRE(nested != NULL); + CHECK(JGetInt(nested, "c") == 3); + + JDelete(obj); + } +} + +SCENARIO("JObjectf: %a array format specifier (synonym for %o)") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Array via %a") { + J *arr = JCreateArray(); + REQUIRE(arr != NULL); + JAddItemToArray(arr, JCreateString("one")); + JAddItemToArray(arr, JCreateString("two")); + + J *obj = JObjectf("values:%a", arr); + REQUIRE(obj != NULL); + + J *values = JGetArray(obj, "values"); + REQUIRE(values != NULL); + CHECK(JGetArraySize(values) == 2); + + JDelete(obj); + } + + SECTION("NULL via %a skips field") { + J *obj = JObjectf("arr:%a", (J *)NULL); + REQUIRE(obj != NULL); + CHECK(!JIsPresent(obj, "arr")); + JDelete(obj); + } +} + +// ========================================================================== +// LITERAL BOOLEAN VALUES +// ========================================================================== + +SCENARIO("JObjectf: literal boolean values") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("true literal") { + J *obj = JObjectf("flag:true"); + REQUIRE(obj != NULL); + J *item = JGetObjectItem(obj, "flag"); + REQUIRE(item != NULL); + CHECK(JIsBool(item)); + CHECK(JIsTrue(item)); + JDelete(obj); + } + + SECTION("false literal") { + J *obj = JObjectf("flag:false"); + REQUIRE(obj != NULL); + J *item = JGetObjectItem(obj, "flag"); + REQUIRE(item != NULL); + CHECK(JIsBool(item)); + CHECK(JIsFalse(item)); + JDelete(obj); + } + + SECTION("true followed by other chars is string") { + J *obj = JObjectf("word:trueish"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "word"), "trueish") == 0); + JDelete(obj); + } + + SECTION("false followed by other chars is string") { + J *obj = JObjectf("word:falsehood"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "word"), "falsehood") == 0); + JDelete(obj); + } + + SECTION("TRUE (uppercase) is unquoted string, not boolean") { + J *obj = JObjectf("word:TRUE"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "word"), "TRUE") == 0); + JDelete(obj); + } + + SECTION("FALSE (uppercase) is unquoted string, not boolean") { + J *obj = JObjectf("word:FALSE"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "word"), "FALSE") == 0); + JDelete(obj); + } + + SECTION("Multiple booleans") { + J *obj = JObjectf("a:true b:false c:true"); + REQUIRE(obj != NULL); + CHECK(JGetBool(obj, "a") == true); + CHECK(JGetBool(obj, "b") == false); + CHECK(JGetBool(obj, "c") == true); + JDelete(obj); + } +} + +// ========================================================================== +// LITERAL INTEGER VALUES +// ========================================================================== + +SCENARIO("JObjectf: literal integer values") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Zero") { + J *obj = JObjectf("n:0"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "n") == 0); + JDelete(obj); + } + + SECTION("Single digit") { + J *obj = JObjectf("n:7"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "n") == 7); + JDelete(obj); + } + + SECTION("Multi-digit") { + J *obj = JObjectf("n:12345"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "n") == 12345); + JDelete(obj); + } + + SECTION("Negative integer") { + J *obj = JObjectf("n:-42"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "n") == -42); + JDelete(obj); + } + + SECTION("Negative multi-digit") { + J *obj = JObjectf("n:-98765"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "n") == -98765); + JDelete(obj); + } + + SECTION("Multiple integers") { + J *obj = JObjectf("a:1 b:2 c:3"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "b") == 2); + CHECK(JGetInt(obj, "c") == 3); + JDelete(obj); + } +} + +// ========================================================================== +// LITERAL FLOAT VALUES +// ========================================================================== + +SCENARIO("JObjectf: literal float values") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Simple decimal") { + J *obj = JObjectf("n:3.14"); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "n") - 3.14) < 0.001); + JDelete(obj); + } + + SECTION("Zero point something") { + J *obj = JObjectf("n:0.5"); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "n") - 0.5) < 0.001); + JDelete(obj); + } + + SECTION("Negative float") { + J *obj = JObjectf("n:-2.718"); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "n") - (-2.718)) < 0.001); + JDelete(obj); + } + + SECTION("Zero point zero") { + J *obj = JObjectf("n:0.0"); + REQUIRE(obj != NULL); + CHECK(JGetNumber(obj, "n") == 0.0); + JDelete(obj); + } + + SECTION("Integer with decimal is float") { + J *obj = JObjectf("n:42.0"); + REQUIRE(obj != NULL); + CHECK(JGetNumber(obj, "n") == 42.0); + JDelete(obj); + } + + SECTION("Multiple floats") { + J *obj = JObjectf("lat:40.7128 lon:-74.0060"); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "lat") - 40.7128) < 0.0001); + CHECK(std::abs(JGetNumber(obj, "lon") - (-74.0060)) < 0.0001); + JDelete(obj); + } +} + +// ========================================================================== +// SINGLE-QUOTED STRING LITERALS +// ========================================================================== + +SCENARIO("JObjectf: single-quoted string literals") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Simple string") { + J *obj = JObjectf("msg:'hello'"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "hello") == 0); + JDelete(obj); + } + + SECTION("String with spaces") { + J *obj = JObjectf("msg:'hello world'"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "hello world") == 0); + JDelete(obj); + } + + SECTION("Empty string") { + J *obj = JObjectf("msg:''"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "") == 0); + JDelete(obj); + } + + SECTION("Escaped single quote") { + J *obj = JObjectf("msg:'it\\'s ok'"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "it's ok") == 0); + JDelete(obj); + } + + SECTION("Escaped backslash") { + J *obj = JObjectf("path:'c:\\\\dir'"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "path"), "c:\\dir") == 0); + JDelete(obj); + } + + SECTION("Multiple escaped quotes") { + J *obj = JObjectf("msg:'\\'a\\' and \\'b\\''"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "'a' and 'b'") == 0); + JDelete(obj); + } + + SECTION("Contains double quote (no escape needed)") { + J *obj = JObjectf("msg:'say \"hi\"'"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "say \"hi\"") == 0); + JDelete(obj); + } +} + +// ========================================================================== +// DOUBLE-QUOTED STRING LITERALS +// ========================================================================== + +SCENARIO("JObjectf: double-quoted string literals") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Simple string") { + J *obj = JObjectf("msg:\"hello\""); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "hello") == 0); + JDelete(obj); + } + + SECTION("String with spaces") { + J *obj = JObjectf("msg:\"hello world\""); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "hello world") == 0); + JDelete(obj); + } + + SECTION("Empty string") { + J *obj = JObjectf("msg:\"\""); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "") == 0); + JDelete(obj); + } + + SECTION("Escaped double quote") { + J *obj = JObjectf("msg:\"say \\\"hi\\\"\""); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "say \"hi\"") == 0); + JDelete(obj); + } + + SECTION("Escaped backslash") { + J *obj = JObjectf("path:\"c:\\\\dir\""); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "path"), "c:\\dir") == 0); + JDelete(obj); + } + + SECTION("Contains single quote (no escape needed)") { + J *obj = JObjectf("msg:\"it's ok\""); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "it's ok") == 0); + JDelete(obj); + } +} + +// ========================================================================== +// UNQUOTED STRING VALUES +// ========================================================================== + +SCENARIO("JObjectf: unquoted string values") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Simple word") { + J *obj = JObjectf("status:ok"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "status"), "ok") == 0); + JDelete(obj); + } + + SECTION("Longer word") { + J *obj = JObjectf("mode:continuous"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "mode"), "continuous") == 0); + JDelete(obj); + } + + SECTION("With dot (domain-style)") { + J *obj = JObjectf("product:com.blues.app"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "product"), "com.blues.app") == 0); + JDelete(obj); + } + + SECTION("With dot (file extension)") { + J *obj = JObjectf("file:data.qo"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "file"), "data.qo") == 0); + JDelete(obj); + } + + SECTION("API endpoint style") { + J *obj = JObjectf("req:hub.set"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "req"), "hub.set") == 0); + JDelete(obj); + } + + SECTION("With underscore") { + J *obj = JObjectf("key:my_value"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "key"), "my_value") == 0); + JDelete(obj); + } + + SECTION("With trailing digits") { + J *obj = JObjectf("name:sensor123"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "name"), "sensor123") == 0); + JDelete(obj); + } + + SECTION("Mixed dots and underscores") { + J *obj = JObjectf("id:device_123.sensor_a"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "id"), "device_123.sensor_a") == 0); + JDelete(obj); + } + + SECTION("Multiple unquoted strings") { + J *obj = JObjectf("a:one b:two c:three"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "a"), "one") == 0); + CHECK(strcmp(JGetString(obj, "b"), "two") == 0); + CHECK(strcmp(JGetString(obj, "c"), "three") == 0); + JDelete(obj); + } +} + +// ========================================================================== +// FIELD NAME PARSING +// ========================================================================== + +SCENARIO("JObjectf: field name parsing") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Simple literal name") { + J *obj = JObjectf("name:value"); + REQUIRE(obj != NULL); + CHECK(JIsPresent(obj, "name")); + JDelete(obj); + } + + SECTION("Name with underscore") { + J *obj = JObjectf("my_field:value"); + REQUIRE(obj != NULL); + CHECK(JIsPresent(obj, "my_field")); + JDelete(obj); + } + + SECTION("Name starting with underscore") { + J *obj = JObjectf("_private:%d _internal:%s", (JINTEGER)42, "data"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "_private") == 42); + CHECK(strcmp(JGetString(obj, "_internal"), "data") == 0); + JDelete(obj); + } + + SECTION("Name with multiple underscores") { + J *obj = JObjectf("__dunder__:true _a_b_c_:%d", (JINTEGER)99); + REQUIRE(obj != NULL); + CHECK(JGetBool(obj, "__dunder__") == true); + CHECK(JGetInt(obj, "_a_b_c_") == 99); + JDelete(obj); + } + + SECTION("Name with digits") { + J *obj = JObjectf("field1:value"); + REQUIRE(obj != NULL); + CHECK(JIsPresent(obj, "field1")); + JDelete(obj); + } + + SECTION("Name starting with uppercase") { + J *obj = JObjectf("MyField:value"); + REQUIRE(obj != NULL); + CHECK(JIsPresent(obj, "MyField")); + JDelete(obj); + } + + SECTION("%s field name from argument") { + J *obj = JObjectf("%s:%d", "dynamic", (JINTEGER)42); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "dynamic") == 42); + JDelete(obj); + } + + SECTION("Multiple %s field names") { + J *obj = JObjectf("%s:%d %s:%d", "x", (JINTEGER)10, "y", (JINTEGER)20); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "x") == 10); + CHECK(JGetInt(obj, "y") == 20); + JDelete(obj); + } + + SECTION("Mixed literal and %s field names") { + J *obj = JObjectf("literal:%d %s:%d", (JINTEGER)1, "dynamic", (JINTEGER)2); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "literal") == 1); + CHECK(JGetInt(obj, "dynamic") == 2); + JDelete(obj); + } + + SECTION("Very long %s field name is truncated") { + // Buffer is 256 bytes, create a name longer than that + char longName[300]; + memset(longName, 'x', 299); + longName[299] = '\0'; + + J *obj = JObjectf("%s:%d", longName, (JINTEGER)42); + REQUIRE(obj != NULL); + // Name should be truncated to 255 chars + J *child = obj->child; + REQUIRE(child != NULL); + CHECK(strlen(child->string) == 255); + CHECK(JGetInt(obj, child->string) == 42); + JDelete(obj); + } +} + +// ========================================================================== +// SEPARATOR AND WHITESPACE HANDLING +// ========================================================================== + +SCENARIO("JObjectf: separator and whitespace handling") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Space separator") { + J *obj = JObjectf("a:1 b:2"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "b") == 2); + JDelete(obj); + } + + SECTION("Tab separator") { + J *obj = JObjectf("a:1\tb:2"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "b") == 2); + JDelete(obj); + } + + SECTION("Newline separator") { + J *obj = JObjectf("a:1\nb:2"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "b") == 2); + JDelete(obj); + } + + SECTION("Carriage return separator") { + J *obj = JObjectf("a:1\rb:2"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "b") == 2); + JDelete(obj); + } + + SECTION("Comma separator") { + J *obj = JObjectf("a:1,b:2"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "b") == 2); + JDelete(obj); + } + + SECTION("Comma with space (JSON-like)") { + J *obj = JObjectf("a:1, b:2, c:3"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "b") == 2); + CHECK(JGetInt(obj, "c") == 3); + JDelete(obj); + } + + SECTION("Multiple spaces") { + J *obj = JObjectf("a:1 b:2"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "b") == 2); + JDelete(obj); + } + + SECTION("Mixed separators") { + J *obj = JObjectf("a:1, \t\n b:2"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "b") == 2); + JDelete(obj); + } + + SECTION("Leading whitespace") { + J *obj = JObjectf(" a:1"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + JDelete(obj); + } + + SECTION("Trailing whitespace") { + J *obj = JObjectf("a:1 "); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + JDelete(obj); + } +} + +// ========================================================================== +// ERROR CASES AND EDGE CASES +// ========================================================================== + +SCENARIO("JObjectf: error and edge cases") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Missing colon stops parsing, returns partial") { + J *obj = JObjectf("a:1 badfield b:2"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(!JIsPresent(obj, "badfield")); + CHECK(!JIsPresent(obj, "b")); + JDelete(obj); + } + + SECTION("Missing value stops parsing") { + J *obj = JObjectf("a:1 b:"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(!JIsPresent(obj, "b")); + JDelete(obj); + } + + SECTION("Unknown format specifier stops parsing") { + J *obj = JObjectf("a:1 b:%x", 42); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(!JIsPresent(obj, "b")); + JDelete(obj); + } + + SECTION("Duplicate field names - later overwrites") { + J *obj = JObjectf("x:1 x:2 x:3"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "x") == 3); + JDelete(obj); + } + + SECTION("Single field") { + J *obj = JObjectf("single:42"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "single") == 42); + JDelete(obj); + } + + SECTION("Many fields") { + J *obj = JObjectf("a:1 b:2 c:3 d:4 e:5 f:6 g:7 h:8 i:9 j:10"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "e") == 5); + CHECK(JGetInt(obj, "j") == 10); + JDelete(obj); + } + + SECTION("Unterminated single-quoted string stops parsing") { + J *obj = JObjectf("a:1 b:'unterminated"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(!JIsPresent(obj, "b")); + JDelete(obj); + } + + SECTION("Unterminated double-quoted string stops parsing") { + J *obj = JObjectf("a:1 b:\"unterminated"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(!JIsPresent(obj, "b")); + JDelete(obj); + } + + SECTION("Just minus sign as value stops parsing") { + J *obj = JObjectf("a:1 b:-"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(!JIsPresent(obj, "b")); + JDelete(obj); + } + + SECTION("Minus followed by non-digit is unquoted string") { + // Actually, looking at the parser, a leading minus only works with digits + // So "b:-x" should fail to parse as number and fall through + J *obj = JObjectf("a:1 b:-x"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + // -x is not a valid unquoted string (doesn't start with letter) + CHECK(!JIsPresent(obj, "b")); + JDelete(obj); + } + + SECTION("Lone decimal point as value stops parsing") { + J *obj = JObjectf("a:1 b:."); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(!JIsPresent(obj, "b")); + JDelete(obj); + } + + SECTION("Number with trailing dot parsed as float") { + J *obj = JObjectf("n:42."); + REQUIRE(obj != NULL); + // 42. is a valid float literal + CHECK(JGetNumber(obj, "n") == 42.0); + JDelete(obj); + } + + SECTION("Backslash at end of quoted string") { + // Backslash at end with no following char - unterminated + J *obj = JObjectf("a:1 b:'test\\"); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + CHECK(!JIsPresent(obj, "b")); + JDelete(obj); + } + + SECTION("Double backslash in quoted string") { + J *obj = JObjectf("msg:'path\\\\to\\\\file'"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "msg"), "path\\to\\file") == 0); + JDelete(obj); + } + + SECTION("NULL %s field name stops parsing") { + J *obj = JObjectf("a:1 %s:2", (const char *)NULL); + REQUIRE(obj != NULL); + CHECK(JGetInt(obj, "a") == 1); + // Parsing stopped after NULL field name + JDelete(obj); + } +} + +// ========================================================================== +// COMPLEX REAL-WORLD EXAMPLES +// ========================================================================== + +SCENARIO("JObjectf: complex real-world examples") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Notecard hub.set request") { + J *obj = JObjectf("req:hub.set product:com.blues.app mode:continuous outbound:60"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "req"), "hub.set") == 0); + CHECK(strcmp(JGetString(obj, "product"), "com.blues.app") == 0); + CHECK(strcmp(JGetString(obj, "mode"), "continuous") == 0); + CHECK(JGetInt(obj, "outbound") == 60); + JDelete(obj); + } + + SECTION("Notecard note.add request") { + J *body = JObjectf("temp:%f humidity:%f", (JNUMBER)72.5, (JNUMBER)45.0); + REQUIRE(body != NULL); + + J *req = JObjectf("req:note.add file:sensors.qo body:%o", body); + REQUIRE(req != NULL); + + CHECK(strcmp(JGetString(req, "req"), "note.add") == 0); + CHECK(strcmp(JGetString(req, "file"), "sensors.qo") == 0); + + J *bodyObj = JGetObject(req, "body"); + REQUIRE(bodyObj != NULL); + CHECK(std::abs(JGetNumber(bodyObj, "temp") - 72.5) < 0.01); + CHECK(std::abs(JGetNumber(bodyObj, "humidity") - 45.0) < 0.01); + + JDelete(req); + } + + SECTION("Notecard command") { + J *obj = JObjectf("cmd:card.led mode:breathe"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "cmd"), "card.led") == 0); + CHECK(strcmp(JGetString(obj, "mode"), "breathe") == 0); + JDelete(obj); + } + + SECTION("Mixed value types") { + J *obj = JObjectf("name:%s age:%d active:%b score:%f status:ok", + "Alice", (JINTEGER)30, 1, (JNUMBER)95.5); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "name"), "Alice") == 0); + CHECK(JGetInt(obj, "age") == 30); + CHECK(JGetBool(obj, "active") == true); + CHECK(std::abs(JGetNumber(obj, "score") - 95.5) < 0.01); + CHECK(strcmp(JGetString(obj, "status"), "ok") == 0); + JDelete(obj); + } + + SECTION("Using comma separators for readability") { + J *obj = JObjectf("req:hub.set, product:com.blues.test, mode:periodic, outbound:120"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "req"), "hub.set") == 0); + CHECK(strcmp(JGetString(obj, "product"), "com.blues.test") == 0); + CHECK(strcmp(JGetString(obj, "mode"), "periodic") == 0); + CHECK(JGetInt(obj, "outbound") == 120); + JDelete(obj); + } + + SECTION("Multi-line C string concatenation style") { + J *obj = JObjectf( + "req:note.add " + "file:data.qo " + "sync:%b", + 1); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "req"), "note.add") == 0); + CHECK(strcmp(JGetString(obj, "file"), "data.qo") == 0); + CHECK(JGetBool(obj, "sync") == true); + JDelete(obj); + } +} + +// ========================================================================== +// JAddf MACRO TESTS +// ========================================================================== + +SCENARIO("JAddf: add fields to existing object") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Add to empty object") { + J *obj = JCreateObject(); + REQUIRE(obj != NULL); + + JAddf(obj, "a:1 b:2"); + + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "b") == 2); + JDelete(obj); + } + + SECTION("Add to object with existing fields") { + J *obj = JObjectf("existing:42"); + REQUIRE(obj != NULL); + + JAddf(obj, "new:100"); + + CHECK(JGetInt(obj, "existing") == 42); + CHECK(JGetInt(obj, "new") == 100); + JDelete(obj); + } + + SECTION("Overwrite existing field") { + J *obj = JObjectf("value:old"); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "value"), "old") == 0); + + JAddf(obj, "value:new"); + + CHECK(strcmp(JGetString(obj, "value"), "new") == 0); + JDelete(obj); + } + + SECTION("Add with format specifiers") { + J *obj = JObjectf("base:1"); + REQUIRE(obj != NULL); + + JAddf(obj, "name:%s count:%d", "test", (JINTEGER)99); + + CHECK(JGetInt(obj, "base") == 1); + CHECK(strcmp(JGetString(obj, "name"), "test") == 0); + CHECK(JGetInt(obj, "count") == 99); + JDelete(obj); + } + + SECTION("Multiple JAddf calls") { + J *obj = JCreateObject(); + REQUIRE(obj != NULL); + + JAddf(obj, "a:1"); + JAddf(obj, "b:2"); + JAddf(obj, "c:3"); + + CHECK(JGetInt(obj, "a") == 1); + CHECK(JGetInt(obj, "b") == 2); + CHECK(JGetInt(obj, "c") == 3); + JDelete(obj); + } + + SECTION("JAddf with NULL target does not crash") { + // This should not crash - JMerge handles NULL target + JAddf(NULL, "a:1"); + // If we get here without crashing, test passes + CHECK(true); + } +} + +// ========================================================================== +// JMerge EDGE CASES +// ========================================================================== + +SCENARIO("JMerge: NULL handling") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("NULL source does nothing") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + JAddStringToObject(target, "existing", "value"); + + // This should do nothing (source is NULL) + JMerge(target, NULL); + + // Target should be unchanged + CHECK(strcmp(JGetString(target, "existing"), "value") == 0); + JDelete(target); + } + + SECTION("NULL target deletes source to prevent memory leak") { + J *source = JCreateObject(); + REQUIRE(source != NULL); + JAddStringToObject(source, "key", "value"); + + // This should delete source to prevent leak + JMerge(NULL, source); + + // If we get here without leak detector complaining, test passes + // (source was deleted inside JMerge) + CHECK(true); + } + + SECTION("Both NULL does nothing") { + // Should not crash + JMerge(NULL, NULL); + CHECK(true); + } + + SECTION("Empty source object") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + JAddStringToObject(target, "existing", "value"); + + J *source = JCreateObject(); + REQUIRE(source != NULL); + // source has no children + + JMerge(target, source); + + // Target should be unchanged + CHECK(strcmp(JGetString(target, "existing"), "value") == 0); + JDelete(target); + } + + SECTION("Multiple fields merged") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + JAddStringToObject(target, "a", "1"); + + J *source = JCreateObject(); + REQUIRE(source != NULL); + JAddStringToObject(source, "b", "2"); + JAddStringToObject(source, "c", "3"); + + JMerge(target, source); + + CHECK(strcmp(JGetString(target, "a"), "1") == 0); + CHECK(strcmp(JGetString(target, "b"), "2") == 0); + CHECK(strcmp(JGetString(target, "c"), "3") == 0); + JDelete(target); + } + + SECTION("Overwrites existing field") { + J *target = JCreateObject(); + REQUIRE(target != NULL); + JAddStringToObject(target, "key", "old"); + + J *source = JCreateObject(); + REQUIRE(source != NULL); + JAddStringToObject(source, "key", "new"); + + JMerge(target, source); + + CHECK(strcmp(JGetString(target, "key"), "new") == 0); + JDelete(target); + } +} + +// ========================================================================== +// JObjectfv TESTS +// ========================================================================== + +// Helper function to test JObjectfv +static J* testJObjectfvWrapper(const char *format, ...) +{ + va_list args; + va_start(args, format); + J *result = JObjectfv(format, args); + va_end(args); + return result; +} + +SCENARIO("JObjectfv: va_list version") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + SECTION("Basic usage via wrapper") { + J *obj = testJObjectfvWrapper("name:%s age:%d", "Bob", (JINTEGER)25); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "name"), "Bob") == 0); + CHECK(JGetInt(obj, "age") == 25); + JDelete(obj); + } + + SECTION("NULL format") { + J *obj = testJObjectfvWrapper(NULL); + REQUIRE(obj != NULL); + CHECK(obj->child == NULL); + JDelete(obj); + } + + SECTION("Mixed types via wrapper") { + J *obj = testJObjectfvWrapper("s:%s d:%d f:%f b:%b", + "str", (JINTEGER)42, (JNUMBER)3.14, 1); + REQUIRE(obj != NULL); + CHECK(strcmp(JGetString(obj, "s"), "str") == 0); + CHECK(JGetInt(obj, "d") == 42); + CHECK(std::abs(JGetNumber(obj, "f") - 3.14) < 0.001); + CHECK(JGetBool(obj, "b") == true); + JDelete(obj); + } +} + +// ========================================================================== +// VARARG FLOAT/DOUBLE PROMOTION +// ========================================================================== + +SCENARIO("JObjectf: vararg float promotion to double") +{ + NoteSetFnDefault(malloc, free, NULL, NULL); + + // When floats are passed through varargs (...), they are promoted to double. + // The implementation must use va_arg with double, not float/JNUMBER. + + SECTION("Double literal passed directly") { + // 3.14159 is a double literal, tests double extraction + J *obj = JObjectf("val:%f", 3.14159); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "val") - 3.14159) < 0.00001); + JDelete(obj); + } + + SECTION("Float cast to double") { + float f = 2.5f; + J *obj = JObjectf("val:%f", (double)f); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "val") - 2.5) < 0.0001); + JDelete(obj); + } + + SECTION("Multiple double values") { + J *obj = JObjectf("a:%f b:%f c:%f", 1.1, 2.2, 3.3); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "a") - 1.1) < 0.0001); + CHECK(std::abs(JGetNumber(obj, "b") - 2.2) < 0.0001); + CHECK(std::abs(JGetNumber(obj, "c") - 3.3) < 0.0001); + JDelete(obj); + } + + SECTION("Large double value") { + J *obj = JObjectf("big:%f", 1.0e30); + REQUIRE(obj != NULL); + CHECK(JGetNumber(obj, "big") > 9.9e29); + JDelete(obj); + } + + SECTION("Small double value") { + J *obj = JObjectf("small:%f", 1.0e-30); + REQUIRE(obj != NULL); + CHECK(JGetNumber(obj, "small") < 1.1e-30); + CHECK(JGetNumber(obj, "small") > 0.0); + JDelete(obj); + } + + SECTION("JObjectfv with double values") { + J *obj = testJObjectfvWrapper("x:%f y:%f", 100.5, 200.75); + REQUIRE(obj != NULL); + CHECK(std::abs(JGetNumber(obj, "x") - 100.5) < 0.001); + CHECK(std::abs(JGetNumber(obj, "y") - 200.75) < 0.001); + JDelete(obj); + } +} + +}