From 4894890ee866be85d96d1a6376487db588d343bd Mon Sep 17 00:00:00 2001 From: Disheng Su Date: Sun, 29 Jun 2025 19:52:07 -0700 Subject: [PATCH 1/6] Add JSON marshaling library with enum support - Add json_marshal.c3 with Go-style API (json::marshal, json::marshal_value, json::marshal_array) - Support all primitive types: String, int, float, double, bool - Support enums (always marshaled as enum names, regardless of associated values) - Support nested structs and arrays of any supported type - Use temp allocator for memory-safe operation - Comprehensive test suite with 15 test cases covering all features - Follow C3 coding style with proper and kindof usage --- lib/std/encoding/json_marshal.c3 | 193 ++++++++ test/unit/stdlib/encoding/json_marshal.c3 | 543 ++++++++++++++++++++++ 2 files changed, 736 insertions(+) create mode 100644 lib/std/encoding/json_marshal.c3 create mode 100644 test/unit/stdlib/encoding/json_marshal.c3 diff --git a/lib/std/encoding/json_marshal.c3 b/lib/std/encoding/json_marshal.c3 new file mode 100644 index 000000000..b047891d7 --- /dev/null +++ b/lib/std/encoding/json_marshal.c3 @@ -0,0 +1,193 @@ +// Copyright (c) 2024 C3 Community. All rights reserved. +// Use of this source code is governed by the MIT license +// a copy of which can be found in the LICENSE_STDLIB file. +module std::encoding::json; +import std::core::string; + +faultdef UNSUPPORTED_TYPE; + +/** + * JSON marshaling for structs containing primitive types, enums, and nested structs + * Supports: String, int, float, double, bool, enums (always marshaled as enum names), nested structs + * Uses temp allocator to avoid memory management issues + */ + +/** + * Marshal a struct with primitive fields and nested structs to JSON + * @param [in] value The struct value to marshal + * @return The JSON string representation (using temp allocator) + */ +macro String? marshal(value) +{ + var $Type = $typeof(value); + + // Only handle structs + $if $Type.kindof != STRUCT: + return UNSUPPORTED_TYPE?; + $endif + + DString result = dstring::temp(); + + result.append_char('{'); + + var $first = true; + $foreach $member : $Type.membersof: + $if $member.nameof != "": + $if !$first: + result.append_char(','); + $endif + $first = false; + + // Add field name (always quoted) + result.append_char('"'); + result.append($member.nameof); + result.append(`":`); + + // Add field value based on type + var $FieldType = $member.typeid; + $switch $FieldType.kindof: + $case SIGNED_INT: + $case UNSIGNED_INT: + // Integer field + result.appendf("%d", $member.get(value)); + $case FLOAT: + // Float field + result.appendf("%g", $member.get(value)); + $case BOOL: + // Boolean field + result.append($member.get(value) ? "true" : "false"); + $case ENUM: + // Enum field - marshal as string name + @pool() + { + String? enum_result = marshal_enum($member.get(value)); + if (catch err = enum_result) return err?; + result.append(enum_result); + }; + $case STRUCT: + // Nested struct field - recursively marshal + @pool() + { + String? nested_result = marshal($member.get(value)); + if (catch err = nested_result) return err?; + result.append(nested_result); + }; + $case ARRAY: + $case SLICE: + // Array field - marshal as array + @pool() + { + String? array_result = marshal_array($member.get(value)); + if (catch err = array_result) return err?; + result.append(array_result); + }; + $default: + $if $FieldType.typeid == String.typeid: + // String field - use the string escape functionality + result.append($member.get(value).tescape(false)); + $endif + $endswitch + $endif + $endforeach + + result.append_char('}'); + return result.str_view(); +} + +/** + * Marshal a primitive value to JSON + * @param [in] value The value to marshal + * @return The JSON string representation (using temp allocator) + */ +macro String? marshal_value(value) +{ + var $Type = $typeof(value); + + $switch $Type.kindof: + $case STRUCT: + return marshal(value); + $case SIGNED_INT: + $case UNSIGNED_INT: + return string::tformat("%d", value); + $case FLOAT: + return string::tformat("%g", value); + $case BOOL: + return value ? "true" : "false"; + $case ENUM: + return marshal_enum(value); + $default: + $if $Type.typeid == String.typeid: + return value.tescape(false); + $endif + return UNSUPPORTED_TYPE?; + $endswitch +} + +/** + * Marshal an array of primitive values to JSON + * @param [in] array The array to marshal + * @return The JSON array string representation (using temp allocator) + */ +macro String? marshal_array(array) +{ + var $Type = $typeof(array); + var $ElementType = $Type.inner; + + DString result = dstring::temp(); + + result.append_char('['); + + foreach (i, element : array) + { + if (i > 0) result.append_char(','); + + $switch $ElementType.kindof: + $case SIGNED_INT: + $case UNSIGNED_INT: + result.appendf("%d", element); + $case FLOAT: + result.appendf("%g", element); + $case BOOL: + result.append(element ? "true" : "false"); + $case ENUM: + @pool() + { + String? enum_result = marshal_enum(element); + if (catch err = enum_result) return err?; + result.append(enum_result); + }; + $case STRUCT: + @pool() + { + String? nested_result = marshal(element); + if (catch err = nested_result) return err?; + result.append(nested_result); + }; + $default: + $if $ElementType.typeid == String.typeid: + result.append(element.tescape(false)); + $endif + $endswitch + } + + result.append_char(']'); + return result.str_view(); +} + +/** + * Marshal an enum value to JSON as a quoted string + * Always uses the enum name, regardless of associated values + * @param [in] enum_value The enum value to marshal + * @return The JSON string representation (using temp allocator) + */ +macro String? marshal_enum(enum_value) +{ + var $Type = $typeof(enum_value); + + // Convert enum to ordinal and get the name + usz ordinal = types::any_to_enum_ordinal(&enum_value, usz)!!; + assert(ordinal < $Type.names.len, "Illegal enum value found, numerical value was %d.", ordinal); + + // Always use enum names for JSON marshaling + return $Type.names[ordinal].tescape(false); +} diff --git a/test/unit/stdlib/encoding/json_marshal.c3 b/test/unit/stdlib/encoding/json_marshal.c3 new file mode 100644 index 000000000..ab3bb57df --- /dev/null +++ b/test/unit/stdlib/encoding/json_marshal.c3 @@ -0,0 +1,543 @@ +module json_marshal_test @test; +import std::encoding::json; +import std::io; + +// Test enums +enum Status +{ + ACTIVE, + INACTIVE, + PENDING, + SUSPENDED +} + +enum Priority +{ + LOW, + MEDIUM, + HIGH, + CRITICAL +} + +// Enum with single String associated value +enum State : int (String description) +{ + WAITING = "waiting", + RUNNING = "running", + TERMINATED = "ended" +} + +// Enum with multiple associated values (marshaled using enum name) +enum ComplexState : int (String desc, bool active) +{ + IDLE = { "idle", false }, + BUSY = { "busy", true } +} + +// Test structures with various primitive types +struct Person +{ + String name; + int age; + bool is_active; + double height; + Status status; +} + +struct Product +{ + String title; + int price; + String category; + float rating; + bool in_stock; +} + +// Test struct with all supported primitive types +struct AllTypes +{ + String text; + int integer; + float single_precision; + double double_precision; + bool flag; +} + +// Nested struct test structures +struct Address +{ + String street; + String city; + int zip_code; +} + +struct Company +{ + String name; + Address headquarters; + int employee_count; +} + +struct Employee +{ + Person personal_info; + Company employer; + float salary; + bool is_remote; +} + +struct Department +{ + String name; + String[] skills_required; + Address[] office_locations; + int budget; + Priority priority; +} + +struct Task +{ + String title; + Status status; + Priority priority; + int estimated_hours; +} + +struct Process +{ + String name; + State current_state; + ComplexState complex_state; + int pid; +} + +fn void test_primitive_marshaling() @test +{ + // Test integers + String? result = json::marshal_value(42); + assert(result!! == "42"); + + // Test floats + result = json::marshal_value(3.14); + assert(result!!.starts_with("3.14")); + + // Test doubles + result = json::marshal_value(2.718281828); + assert(result!!.starts_with("2.718")); + + // Test booleans + result = json::marshal_value(true); + assert(result!! == "true"); + + result = json::marshal_value(false); + assert(result!! == "false"); + + // Test strings + result = json::marshal_value("hello world"); + assert(result!! == `"hello world"`); + + // Test string with special characters + result = json::marshal_value("Hello \"world\""); + assert(result!! == `"Hello \"world\""`); +} + +fn void test_enum_marshaling() @test +{ + // Test individual enum values + String? result = json::marshal_value(Status.ACTIVE); + assert(result!! == `"ACTIVE"`); + + result = json::marshal_value(Status.INACTIVE); + assert(result!! == `"INACTIVE"`); + + result = json::marshal_value(Status.PENDING); + assert(result!! == `"PENDING"`); + + result = json::marshal_value(Status.SUSPENDED); + assert(result!! == `"SUSPENDED"`); + + // Test Priority enum + result = json::marshal_value(Priority.LOW); + assert(result!! == `"LOW"`); + + result = json::marshal_value(Priority.MEDIUM); + assert(result!! == `"MEDIUM"`); + + result = json::marshal_value(Priority.HIGH); + assert(result!! == `"HIGH"`); + + result = json::marshal_value(Priority.CRITICAL); + assert(result!! == `"CRITICAL"`); +} + +fn void test_enum_with_associated_values() @test +{ + // Test enum with single String associated value (always uses enum names) + String? result = json::marshal_value(State.WAITING); + assert(result!! == `"WAITING"`); + + result = json::marshal_value(State.RUNNING); + assert(result!! == `"RUNNING"`); + + result = json::marshal_value(State.TERMINATED); + assert(result!! == `"TERMINATED"`); + + // Test enum with multiple associated values (always uses enum names) + result = json::marshal_value(ComplexState.IDLE); + assert(result!! == `"IDLE"`); + + result = json::marshal_value(ComplexState.BUSY); + assert(result!! == `"BUSY"`); +} + +fn void test_struct_with_associated_value_enums() @test +{ + Process process = { + .name = "web_server", + .current_state = State.RUNNING, + .complex_state = ComplexState.BUSY, + .pid = 1234 + }; + + String? result = json::marshal(process); + String json = result!!; + + // Check that enums always use the enum name + assert(json.contains(`"name":"web_server"`)); + assert(json.contains(`"current_state":"RUNNING"`)); // Always uses enum name + assert(json.contains(`"complex_state":"BUSY"`)); // Always uses enum name + assert(json.contains(`"pid":1234`)); + + // Should be valid JSON format + assert(json.starts_with("{")); + assert(json.ends_with("}")); +} + +fn void test_struct_with_enums() @test +{ + Task task = { + .title = "Implement JSON marshaling", + .status = Status.ACTIVE, + .priority = Priority.HIGH, + .estimated_hours = 8 + }; + + String? result = json::marshal(task); + String json = result!!; + + // Check that enum fields are marshaled as strings + assert(json.contains(`"title":"Implement JSON marshaling"`)); + assert(json.contains(`"status":"ACTIVE"`)); + assert(json.contains(`"priority":"HIGH"`)); + assert(json.contains(`"estimated_hours":8`)); + + // Should be valid JSON format + assert(json.starts_with("{")); + assert(json.ends_with("}")); +} + +fn void test_simple_struct_marshaling() @test +{ + Person person = { + .name = "John Doe", + .age = 30, + .is_active = true, + .height = 5.9, + .status = Status.ACTIVE + }; + + String? result = json::marshal(person); + String json = result!!; + + // Check that all fields are present + assert(json.contains(`"name":"John Doe"`)); + assert(json.contains(`"age":30`)); + assert(json.contains(`"is_active":true`)); + assert(json.contains(`"height":5.9`)); + assert(json.contains(`"status":"ACTIVE"`)); + + // Should start and end with braces + assert(json.starts_with("{")); + assert(json.ends_with("}")); +} + +fn void test_struct_with_multiple_fields() @test +{ + Product product = { + .title = "C3 Programming Book", + .price = 2999, + .category = "Programming", + .rating = 4.8, + .in_stock = true + }; + + String? result = json::marshal(product); + String json = result!!; + + // Check that all fields are present + assert(json.contains(`"title":"C3 Programming Book"`)); + assert(json.contains(`"price":2999`)); + assert(json.contains(`"category":"Programming"`)); + assert(json.contains(`"rating":4.8`)); + assert(json.contains(`"in_stock":true`)); + + // Should start and end with braces + assert(json.starts_with("{")); + assert(json.ends_with("}")); +} + +fn void test_array_marshaling() @test +{ + // Test integer arrays + int[] numbers = { 1, 2, 3, 4, 5 }; + String? result = json::marshal_array(numbers); + assert(result!! == "[1,2,3,4,5]"); + + // Test string arrays + String[] words = { "hello", "world", "test" }; + result = json::marshal_array(words); + assert(result!! == `["hello","world","test"]`); + + // Test float arrays + float[] prices = { 1.99, 2.50, 3.14 }; + result = json::marshal_array(prices); + String json = result!!; + assert(json.starts_with("[")); + assert(json.ends_with("]")); + assert(json.contains("1.99")); + assert(json.contains("2.5")); + assert(json.contains("3.14")); + + // Test boolean arrays + bool[] flags = { true, false, true }; + result = json::marshal_array(flags); + assert(result!! == "[true,false,true]"); + + // Test enum arrays + Status[] statuses = { Status.ACTIVE, Status.PENDING, Status.INACTIVE }; + result = json::marshal_array(statuses); + assert(result!! == `["ACTIVE","PENDING","INACTIVE"]`); + + Priority[] priorities = { Priority.LOW, Priority.HIGH }; + result = json::marshal_array(priorities); + assert(result!! == `["LOW","HIGH"]`); + + // Test enum arrays with associated values (always uses enum names) + State[] states = { State.WAITING, State.RUNNING, State.TERMINATED }; + result = json::marshal_array(states); + assert(result!! == `["WAITING","RUNNING","TERMINATED"]`); // Always uses enum names + + ComplexState[] complex_states = { ComplexState.IDLE, ComplexState.BUSY }; + result = json::marshal_array(complex_states); + assert(result!! == `["IDLE","BUSY"]`); // Always uses enum names +} + +fn void test_string_escaping() @test +{ + Person person = { + .name = "John \"The Coder\" Doe", + .age = 25, + .is_active = true, + .height = 5.8, + .status = Status.ACTIVE + }; + + String? result = json::marshal(person); + String json = result!!; + + // Should properly escape quotes in the name + assert(json.contains(`"name":"John \"The Coder\" Doe"`)); + assert(json.contains(`"age":25`)); + assert(json.contains(`"status":"ACTIVE"`)); +} + +fn void test_empty_strings() @test +{ + Person person = { + .name = "", + .age = 0, + .is_active = false, + .height = 0.0, + .status = Status.INACTIVE + }; + + String? result = json::marshal(person); + String json = result!!; + + // Should handle empty strings and zero values properly + assert(json.contains(`"name":""`)); + assert(json.contains(`"age":0`)); + assert(json.contains(`"is_active":false`)); + assert(json.contains(`"height":0`)); + assert(json.contains(`"status":"INACTIVE"`)); +} + +fn void test_all_primitive_types() @test +{ + AllTypes data = { + .text = "test string", + .integer = 42, + .single_precision = 3.14, + .double_precision = 2.718281828, + .flag = true + }; + + String? result = json::marshal(data); + String json = result!!; + + // Verify all types are marshaled correctly + assert(json.contains(`"text":"test string"`)); + assert(json.contains(`"integer":42`)); + assert(json.contains(`"single_precision":3.14`)); + assert(json.contains(`"double_precision":2.718`)); + assert(json.contains(`"flag":true`)); + + // Should be valid JSON format + assert(json.starts_with("{")); + assert(json.ends_with("}")); +} + +fn void test_nested_struct_marshaling() @test +{ + Address address = { + .street = "123 Main St", + .city = "New York", + .zip_code = 10001 + }; + + Company company = { + .name = "Tech Corp", + .headquarters = address, + .employee_count = 500 + }; + + String? result = json::marshal(company); + String json = result!!; + + // Check that nested struct is properly marshaled + assert(json.contains(`"name":"Tech Corp"`)); + assert(json.contains(`"headquarters":{`)); + assert(json.contains(`"street":"123 Main St"`)); + assert(json.contains(`"city":"New York"`)); + assert(json.contains(`"zip_code":10001`)); + assert(json.contains(`"employee_count":500`)); + + // Should be valid JSON format + assert(json.starts_with("{")); + assert(json.ends_with("}")); +} + +fn void test_deeply_nested_struct_marshaling() @test +{ + Employee employee = { + .personal_info = { + .name = "Alice Johnson", + .age = 28, + .is_active = true, + .height = 5.6, + .status = Status.ACTIVE + }, + .employer = { + .name = "Innovation Labs", + .headquarters = { + .street = "456 Tech Ave", + .city = "San Francisco", + .zip_code = 94105 + }, + .employee_count = 250 + }, + .salary = 85000.0, + .is_remote = true + }; + + String? result = json::marshal(employee); + String json = result!!; + + // Check deeply nested structure + assert(json.contains(`"personal_info":{`)); + assert(json.contains(`"name":"Alice Johnson"`)); + assert(json.contains(`"age":28`)); + + assert(json.contains(`"employer":{`)); + assert(json.contains(`"name":"Innovation Labs"`)); + assert(json.contains(`"headquarters":{`)); + assert(json.contains(`"street":"456 Tech Ave"`)); + assert(json.contains(`"city":"San Francisco"`)); + assert(json.contains(`"zip_code":94105`)); + + assert(json.contains(`"salary":85000`)); + assert(json.contains(`"is_remote":true`)); + + // Should be valid JSON format + assert(json.starts_with("{")); + assert(json.ends_with("}")); +} + +fn void test_array_of_structs() @test +{ + Address[] addresses = { + { + .street = "100 First St", + .city = "Boston", + .zip_code = 2101 + }, + { + .street = "200 Second Ave", + .city = "Chicago", + .zip_code = 60601 + } + }; + + String? result = json::marshal_array(addresses); + String json = result!!; + + // Check array of structs + assert(json.starts_with("[")); + assert(json.ends_with("]")); + assert(json.contains(`"street":"100 First St"`)); + assert(json.contains(`"city":"Boston"`)); + assert(json.contains(`"zip_code":2101`)); + assert(json.contains(`"street":"200 Second Ave"`)); + assert(json.contains(`"city":"Chicago"`)); + assert(json.contains(`"zip_code":60601`)); +} + +fn void test_struct_with_nested_arrays() @test +{ + Department dept = { + .name = "Engineering", + .skills_required = { "C3", "Rust", "Go" }, + .office_locations = { + { + .street = "100 Tech Blvd", + .city = "Austin", + .zip_code = 78701 + }, + { + .street = "200 Innovation Dr", + .city = "Seattle", + .zip_code = 98101 + } + }, + .budget = 1000000, + .priority = Priority.HIGH + }; + + String? result = json::marshal(dept); + String json = result!!; + + // Check that both primitive and struct arrays are marshaled correctly + assert(json.contains(`"name":"Engineering"`)); + assert(json.contains(`"skills_required":["C3","Rust","Go"]`)); + assert(json.contains(`"office_locations":[`)); + assert(json.contains(`"street":"100 Tech Blvd"`)); + assert(json.contains(`"city":"Austin"`)); + assert(json.contains(`"street":"200 Innovation Dr"`)); + assert(json.contains(`"city":"Seattle"`)); + assert(json.contains(`"budget":1000000`)); + assert(json.contains(`"priority":"HIGH"`)); + + // Should be valid JSON format + assert(json.starts_with("{")); + assert(json.ends_with("}")); +} From 504fcd1334537a57927bf8a755a45b629ff17204 Mon Sep 17 00:00:00 2001 From: Disheng Su Date: Mon, 30 Jun 2025 11:03:22 -0700 Subject: [PATCH 2/6] Refactor JSON marshal functions to eliminate code duplication - Extract common marshaling logic into marshal_value function - Simplify marshal() to use marshal_value for all struct fields - Simplify marshal_array() to use marshal_value for all array elements - Add array/slice support to marshal_value function - Reduce code duplication by ~60 lines while maintaining all functionality - All existing tests continue to pass --- lib/std/encoding/json_marshal.c3 | 115 ++++++++----------------------- 1 file changed, 29 insertions(+), 86 deletions(-) diff --git a/lib/std/encoding/json_marshal.c3 b/lib/std/encoding/json_marshal.c3 index b047891d7..6c131e67d 100644 --- a/lib/std/encoding/json_marshal.c3 +++ b/lib/std/encoding/json_marshal.c3 @@ -20,16 +20,16 @@ faultdef UNSUPPORTED_TYPE; macro String? marshal(value) { var $Type = $typeof(value); - + // Only handle structs $if $Type.kindof != STRUCT: return UNSUPPORTED_TYPE?; $endif - + DString result = dstring::temp(); - + result.append_char('{'); - + var $first = true; $foreach $member : $Type.membersof: $if $member.nameof != "": @@ -37,59 +37,22 @@ macro String? marshal(value) result.append_char(','); $endif $first = false; - + // Add field name (always quoted) result.append_char('"'); result.append($member.nameof); result.append(`":`); - - // Add field value based on type - var $FieldType = $member.typeid; - $switch $FieldType.kindof: - $case SIGNED_INT: - $case UNSIGNED_INT: - // Integer field - result.appendf("%d", $member.get(value)); - $case FLOAT: - // Float field - result.appendf("%g", $member.get(value)); - $case BOOL: - // Boolean field - result.append($member.get(value) ? "true" : "false"); - $case ENUM: - // Enum field - marshal as string name - @pool() - { - String? enum_result = marshal_enum($member.get(value)); - if (catch err = enum_result) return err?; - result.append(enum_result); - }; - $case STRUCT: - // Nested struct field - recursively marshal - @pool() - { - String? nested_result = marshal($member.get(value)); - if (catch err = nested_result) return err?; - result.append(nested_result); - }; - $case ARRAY: - $case SLICE: - // Array field - marshal as array - @pool() - { - String? array_result = marshal_array($member.get(value)); - if (catch err = array_result) return err?; - result.append(array_result); - }; - $default: - $if $FieldType.typeid == String.typeid: - // String field - use the string escape functionality - result.append($member.get(value).tescape(false)); - $endif - $endswitch + + // Add field value using common marshaling logic + @pool() + { + String? field_result = marshal_value($member.get(value)); + if (catch err = field_result) return err?; + result.append(field_result); + }; $endif $endforeach - + result.append_char('}'); return result.str_view(); } @@ -102,10 +65,13 @@ macro String? marshal(value) macro String? marshal_value(value) { var $Type = $typeof(value); - + $switch $Type.kindof: $case STRUCT: return marshal(value); + $case ARRAY: + $case SLICE: + return marshal_array(value); $case SIGNED_INT: $case UNSIGNED_INT: return string::tformat("%d", value); @@ -130,46 +96,23 @@ macro String? marshal_value(value) */ macro String? marshal_array(array) { - var $Type = $typeof(array); - var $ElementType = $Type.inner; - DString result = dstring::temp(); - + result.append_char('['); - + foreach (i, element : array) { if (i > 0) result.append_char(','); - - $switch $ElementType.kindof: - $case SIGNED_INT: - $case UNSIGNED_INT: - result.appendf("%d", element); - $case FLOAT: - result.appendf("%g", element); - $case BOOL: - result.append(element ? "true" : "false"); - $case ENUM: - @pool() - { - String? enum_result = marshal_enum(element); - if (catch err = enum_result) return err?; - result.append(enum_result); - }; - $case STRUCT: - @pool() - { - String? nested_result = marshal(element); - if (catch err = nested_result) return err?; - result.append(nested_result); - }; - $default: - $if $ElementType.typeid == String.typeid: - result.append(element.tescape(false)); - $endif - $endswitch + + // Use common marshaling logic for each element + @pool() + { + String? element_result = marshal_value(element); + if (catch err = element_result) return err?; + result.append(element_result); + }; } - + result.append_char(']'); return result.str_view(); } From 6fab56588194271409396f6e7ed988132dbed68a Mon Sep 17 00:00:00 2001 From: Disheng Su Date: Tue, 1 Jul 2025 21:57:58 -0700 Subject: [PATCH 3/6] Address comments --- lib/std/encoding/json_marshal.c3 | 116 +++++++++------ test/unit/stdlib/encoding/json_marshal.c3 | 166 ++++++++++------------ 2 files changed, 148 insertions(+), 134 deletions(-) diff --git a/lib/std/encoding/json_marshal.c3 b/lib/std/encoding/json_marshal.c3 index 6c131e67d..2d41b422c 100644 --- a/lib/std/encoding/json_marshal.c3 +++ b/lib/std/encoding/json_marshal.c3 @@ -1,32 +1,30 @@ // Copyright (c) 2024 C3 Community. All rights reserved. // Use of this source code is governed by the MIT license // a copy of which can be found in the LICENSE_STDLIB file. + +<* + JSON marshaling for structs containing primitive types, enums, and nested structs. + Supports: String, int, float, double, bool, enums (always marshaled as enum names), nested structs +*> + module std::encoding::json; import std::core::string; faultdef UNSUPPORTED_TYPE; -/** - * JSON marshaling for structs containing primitive types, enums, and nested structs - * Supports: String, int, float, double, bool, enums (always marshaled as enum names), nested structs - * Uses temp allocator to avoid memory management issues - */ - -/** - * Marshal a struct with primitive fields and nested structs to JSON - * @param [in] value The struct value to marshal - * @return The JSON string representation (using temp allocator) - */ -macro String? marshal(value) +<* + Marshal a struct with primitive fields and nested structs to JSON. + + @param allocator : "The allocator to use for the result" + @param value: "The struct value to marshal" + @require @typekind(value) == STRUCT + @return "The JSON string representation" +*> +macro String? marshal(Allocator allocator, value) { var $Type = $typeof(value); - // Only handle structs - $if $Type.kindof != STRUCT: - return UNSUPPORTED_TYPE?; - $endif - - DString result = dstring::temp(); + DString result = dstring::new_with_capacity(allocator, 32); result.append_char('{'); @@ -46,32 +44,42 @@ macro String? marshal(value) // Add field value using common marshaling logic @pool() { - String? field_result = marshal_value($member.get(value)); - if (catch err = field_result) return err?; + String field_result = marshal_value(allocator, $member.get(value))!; result.append(field_result); }; $endif $endforeach result.append_char('}'); - return result.str_view(); + return result.copy_str(allocator); } -/** - * Marshal a primitive value to JSON - * @param [in] value The value to marshal - * @return The JSON string representation (using temp allocator) - */ -macro String? marshal_value(value) +<* + Marshal a struct with primitive fields and nested structs to JSON using the temp allocator. + + @param value: "The struct value to marshal" + @require @typekind(value) == STRUCT + @return "The JSON string representation" +*> +macro String? tmarshal(value) => marshal(tmem, value); + +<* + Marshal a primitive value to JSON. + + @param allocator : "The allocator to use for the result" + @param value: "The value to marshal" + @return "The JSON string representation" +*> +macro String? marshal_value(Allocator allocator, value) { var $Type = $typeof(value); $switch $Type.kindof: $case STRUCT: - return marshal(value); + return marshal(allocator, value); $case ARRAY: $case SLICE: - return marshal_array(value); + return marshal_array(allocator, value); $case SIGNED_INT: $case UNSIGNED_INT: return string::tformat("%d", value); @@ -89,14 +97,24 @@ macro String? marshal_value(value) $endswitch } -/** - * Marshal an array of primitive values to JSON - * @param [in] array The array to marshal - * @return The JSON array string representation (using temp allocator) - */ -macro String? marshal_array(array) +<* + Marshal a primitive value to JSON using the temp allocator. + + @param value: "The value to marshal" + @return "The JSON string representation" +*> +macro String? tmarshal_value(value) => marshal_value(tmem, value); + +<* + Marshal an array of primitive values to JSON. + + @param allocator : "The allocator to use for the result" + @param array: "The array to marshal" + @return "The JSON array string representation" +*> +macro String? marshal_array(Allocator allocator, array) { - DString result = dstring::temp(); + DString result = dstring::new_with_capacity(allocator, 32); result.append_char('['); @@ -107,22 +125,30 @@ macro String? marshal_array(array) // Use common marshaling logic for each element @pool() { - String? element_result = marshal_value(element); - if (catch err = element_result) return err?; + String element_result = marshal_value(allocator, element)!; result.append(element_result); }; } result.append_char(']'); - return result.str_view(); + return result.copy_str(allocator); } -/** - * Marshal an enum value to JSON as a quoted string - * Always uses the enum name, regardless of associated values - * @param [in] enum_value The enum value to marshal - * @return The JSON string representation (using temp allocator) - */ +<* + Marshal an array of primitive values to JSON using the temp allocator. + + @param array: "The array to marshal" + @return "The JSON array string representation" +*> +macro String? tmarshal_array(array) => marshal_array(tmem, array); + +<* + Marshal an enum value to JSON as a quoted string. + Always uses the enum name, regardless of associated values. + + @param enum_value: "The enum value to marshal" + @return "The JSON string representation" +*> macro String? marshal_enum(enum_value) { var $Type = $typeof(enum_value); diff --git a/test/unit/stdlib/encoding/json_marshal.c3 b/test/unit/stdlib/encoding/json_marshal.c3 index ab3bb57df..3cad9e7cd 100644 --- a/test/unit/stdlib/encoding/json_marshal.c3 +++ b/test/unit/stdlib/encoding/json_marshal.c3 @@ -114,80 +114,80 @@ struct Process fn void test_primitive_marshaling() @test { // Test integers - String? result = json::marshal_value(42); - assert(result!! == "42"); + String result = json::tmarshal_value(42)!!; + assert(result == "42"); // Test floats - result = json::marshal_value(3.14); - assert(result!!.starts_with("3.14")); + result = json::tmarshal_value(3.14)!!; + assert(result.starts_with("3.14")); // Test doubles - result = json::marshal_value(2.718281828); - assert(result!!.starts_with("2.718")); + result = json::tmarshal_value(2.718281828)!!; + assert(result.starts_with("2.718")); // Test booleans - result = json::marshal_value(true); - assert(result!! == "true"); + result = json::tmarshal_value(true)!!; + assert(result == "true"); - result = json::marshal_value(false); - assert(result!! == "false"); + result = json::tmarshal_value(false)!!; + assert(result == "false"); // Test strings - result = json::marshal_value("hello world"); - assert(result!! == `"hello world"`); + result = json::tmarshal_value("hello world")!!; + assert(result == `"hello world"`); // Test string with special characters - result = json::marshal_value("Hello \"world\""); - assert(result!! == `"Hello \"world\""`); + result = json::tmarshal_value("Hello \"world\"")!!; + assert(result == `"Hello \"world\""`); } fn void test_enum_marshaling() @test { // Test individual enum values - String? result = json::marshal_value(Status.ACTIVE); - assert(result!! == `"ACTIVE"`); + String result = json::tmarshal_value(Status.ACTIVE)!!; + assert(result == `"ACTIVE"`); - result = json::marshal_value(Status.INACTIVE); - assert(result!! == `"INACTIVE"`); + result = json::tmarshal_value(Status.INACTIVE)!!; + assert(result == `"INACTIVE"`); - result = json::marshal_value(Status.PENDING); - assert(result!! == `"PENDING"`); + result = json::tmarshal_value(Status.PENDING)!!; + assert(result == `"PENDING"`); - result = json::marshal_value(Status.SUSPENDED); - assert(result!! == `"SUSPENDED"`); + result = json::tmarshal_value(Status.SUSPENDED)!!; + assert(result == `"SUSPENDED"`); // Test Priority enum - result = json::marshal_value(Priority.LOW); - assert(result!! == `"LOW"`); + result = json::tmarshal_value(Priority.LOW)!!; + assert(result == `"LOW"`); - result = json::marshal_value(Priority.MEDIUM); - assert(result!! == `"MEDIUM"`); + result = json::tmarshal_value(Priority.MEDIUM)!!; + assert(result == `"MEDIUM"`); - result = json::marshal_value(Priority.HIGH); - assert(result!! == `"HIGH"`); + result = json::tmarshal_value(Priority.HIGH)!!; + assert(result == `"HIGH"`); - result = json::marshal_value(Priority.CRITICAL); - assert(result!! == `"CRITICAL"`); + result = json::tmarshal_value(Priority.CRITICAL)!!; + assert(result == `"CRITICAL"`); } fn void test_enum_with_associated_values() @test { // Test enum with single String associated value (always uses enum names) - String? result = json::marshal_value(State.WAITING); - assert(result!! == `"WAITING"`); + String result = json::tmarshal_value(State.WAITING)!!; + assert(result == `"WAITING"`); - result = json::marshal_value(State.RUNNING); - assert(result!! == `"RUNNING"`); + result = json::tmarshal_value(State.RUNNING)!!; + assert(result == `"RUNNING"`); - result = json::marshal_value(State.TERMINATED); - assert(result!! == `"TERMINATED"`); + result = json::tmarshal_value(State.TERMINATED)!!; + assert(result == `"TERMINATED"`); // Test enum with multiple associated values (always uses enum names) - result = json::marshal_value(ComplexState.IDLE); - assert(result!! == `"IDLE"`); + result = json::tmarshal_value(ComplexState.IDLE)!!; + assert(result == `"IDLE"`); - result = json::marshal_value(ComplexState.BUSY); - assert(result!! == `"BUSY"`); + result = json::tmarshal_value(ComplexState.BUSY)!!; + assert(result == `"BUSY"`); } fn void test_struct_with_associated_value_enums() @test @@ -199,8 +199,7 @@ fn void test_struct_with_associated_value_enums() @test .pid = 1234 }; - String? result = json::marshal(process); - String json = result!!; + String json = json::tmarshal(process)!!; // Check that enums always use the enum name assert(json.contains(`"name":"web_server"`)); @@ -222,8 +221,7 @@ fn void test_struct_with_enums() @test .estimated_hours = 8 }; - String? result = json::marshal(task); - String json = result!!; + String json = json::tmarshal(task)!!; // Check that enum fields are marshaled as strings assert(json.contains(`"title":"Implement JSON marshaling"`)); @@ -246,8 +244,7 @@ fn void test_simple_struct_marshaling() @test .status = Status.ACTIVE }; - String? result = json::marshal(person); - String json = result!!; + String json = json::tmarshal(person)!!; // Check that all fields are present assert(json.contains(`"name":"John Doe"`)); @@ -271,9 +268,8 @@ fn void test_struct_with_multiple_fields() @test .in_stock = true }; - String? result = json::marshal(product); - String json = result!!; - + String json = json::tmarshal(product)!!; + // Check that all fields are present assert(json.contains(`"title":"C3 Programming Book"`)); assert(json.contains(`"price":2999`)); @@ -290,46 +286,45 @@ fn void test_array_marshaling() @test { // Test integer arrays int[] numbers = { 1, 2, 3, 4, 5 }; - String? result = json::marshal_array(numbers); - assert(result!! == "[1,2,3,4,5]"); + String result = json::tmarshal_array(numbers)!!; + assert(result == "[1,2,3,4,5]"); // Test string arrays String[] words = { "hello", "world", "test" }; - result = json::marshal_array(words); - assert(result!! == `["hello","world","test"]`); + result = json::tmarshal_array(words)!!; + assert(result == `["hello","world","test"]`); // Test float arrays float[] prices = { 1.99, 2.50, 3.14 }; - result = json::marshal_array(prices); - String json = result!!; - assert(json.starts_with("[")); - assert(json.ends_with("]")); - assert(json.contains("1.99")); - assert(json.contains("2.5")); - assert(json.contains("3.14")); + result = json::tmarshal_array(prices)!!; + assert(result.starts_with("[")); + assert(result.ends_with("]")); + assert(result.contains("1.99")); + assert(result.contains("2.5")); + assert(result.contains("3.14")); // Test boolean arrays bool[] flags = { true, false, true }; - result = json::marshal_array(flags); - assert(result!! == "[true,false,true]"); + result = json::tmarshal_array(flags)!!; + assert(result == "[true,false,true]"); // Test enum arrays Status[] statuses = { Status.ACTIVE, Status.PENDING, Status.INACTIVE }; - result = json::marshal_array(statuses); - assert(result!! == `["ACTIVE","PENDING","INACTIVE"]`); + result = json::tmarshal_array(statuses)!!; + assert(result == `["ACTIVE","PENDING","INACTIVE"]`); Priority[] priorities = { Priority.LOW, Priority.HIGH }; - result = json::marshal_array(priorities); - assert(result!! == `["LOW","HIGH"]`); + result = json::tmarshal_array(priorities)!!; + assert(result == `["LOW","HIGH"]`); // Test enum arrays with associated values (always uses enum names) State[] states = { State.WAITING, State.RUNNING, State.TERMINATED }; - result = json::marshal_array(states); - assert(result!! == `["WAITING","RUNNING","TERMINATED"]`); // Always uses enum names + result = json::tmarshal_array(states)!!; + assert(result == `["WAITING","RUNNING","TERMINATED"]`); // Always uses enum names ComplexState[] complex_states = { ComplexState.IDLE, ComplexState.BUSY }; - result = json::marshal_array(complex_states); - assert(result!! == `["IDLE","BUSY"]`); // Always uses enum names + result = json::tmarshal_array(complex_states)!!; + assert(result == `["IDLE","BUSY"]`); // Always uses enum names } fn void test_string_escaping() @test @@ -342,8 +337,7 @@ fn void test_string_escaping() @test .status = Status.ACTIVE }; - String? result = json::marshal(person); - String json = result!!; + String json = json::tmarshal(person)!!; // Should properly escape quotes in the name assert(json.contains(`"name":"John \"The Coder\" Doe"`)); @@ -361,8 +355,7 @@ fn void test_empty_strings() @test .status = Status.INACTIVE }; - String? result = json::marshal(person); - String json = result!!; + String json = json::tmarshal(person)!!; // Should handle empty strings and zero values properly assert(json.contains(`"name":""`)); @@ -382,9 +375,8 @@ fn void test_all_primitive_types() @test .flag = true }; - String? result = json::marshal(data); - String json = result!!; - + String json = json::tmarshal(data)!!; + // Verify all types are marshaled correctly assert(json.contains(`"text":"test string"`)); assert(json.contains(`"integer":42`)); @@ -411,9 +403,8 @@ fn void test_nested_struct_marshaling() @test .employee_count = 500 }; - String? result = json::marshal(company); - String json = result!!; - + String json = json::tmarshal(company)!!; + // Check that nested struct is properly marshaled assert(json.contains(`"name":"Tech Corp"`)); assert(json.contains(`"headquarters":{`)); @@ -450,9 +441,8 @@ fn void test_deeply_nested_struct_marshaling() @test .is_remote = true }; - String? result = json::marshal(employee); - String json = result!!; - + String json = json::tmarshal(employee)!!; + // Check deeply nested structure assert(json.contains(`"personal_info":{`)); assert(json.contains(`"name":"Alice Johnson"`)); @@ -488,9 +478,8 @@ fn void test_array_of_structs() @test } }; - String? result = json::marshal_array(addresses); - String json = result!!; - + String json = json::tmarshal_array(addresses)!!; + // Check array of structs assert(json.starts_with("[")); assert(json.ends_with("]")); @@ -523,9 +512,8 @@ fn void test_struct_with_nested_arrays() @test .priority = Priority.HIGH }; - String? result = json::marshal(dept); - String json = result!!; - + String json = json::tmarshal(dept)!!; + // Check that both primitive and struct arrays are marshaled correctly assert(json.contains(`"name":"Engineering"`)); assert(json.contains(`"skills_required":["C3","Rust","Go"]`)); From 627e856fdb50186fa8b4c6679dcf89b55c685e55 Mon Sep 17 00:00:00 2001 From: Disheng Su Date: Tue, 1 Jul 2025 22:04:40 -0700 Subject: [PATCH 4/6] Fix ut --- test/unit/stdlib/encoding/json_marshal.c3 | 259 +++++++++++++++------- 1 file changed, 178 insertions(+), 81 deletions(-) diff --git a/test/unit/stdlib/encoding/json_marshal.c3 b/test/unit/stdlib/encoding/json_marshal.c3 index 3cad9e7cd..e183b7f57 100644 --- a/test/unit/stdlib/encoding/json_marshal.c3 +++ b/test/unit/stdlib/encoding/json_marshal.c3 @@ -1,5 +1,6 @@ module json_marshal_test @test; import std::encoding::json; +import std::collections::object; import std::io; // Test enums @@ -223,11 +224,15 @@ fn void test_struct_with_enums() @test String json = json::tmarshal(task)!!; - // Check that enum fields are marshaled as strings - assert(json.contains(`"title":"Implement JSON marshaling"`)); - assert(json.contains(`"status":"ACTIVE"`)); - assert(json.contains(`"priority":"HIGH"`)); - assert(json.contains(`"estimated_hours":8`)); + // Parse the marshaled JSON back to verify it's valid and correct + Object* parsed = json::tparse_string(json)!!; + defer parsed.free(); + + // Verify all fields through the JSON Object API + assert(parsed.get_string("title")!! == "Implement JSON marshaling"); + assert(parsed.get_string("status")!! == "ACTIVE"); // Enum marshaled as string + assert(parsed.get_string("priority")!! == "HIGH"); // Enum marshaled as string + assert(parsed.get_int("estimated_hours")!! == 8); // Should be valid JSON format assert(json.starts_with("{")); @@ -246,14 +251,18 @@ fn void test_simple_struct_marshaling() @test String json = json::tmarshal(person)!!; - // Check that all fields are present - assert(json.contains(`"name":"John Doe"`)); - assert(json.contains(`"age":30`)); - assert(json.contains(`"is_active":true`)); - assert(json.contains(`"height":5.9`)); - assert(json.contains(`"status":"ACTIVE"`)); + // Parse the marshaled JSON back to verify it's valid and correct + Object* parsed = json::tparse_string(json)!!; + defer parsed.free(); + + // Verify all fields through the JSON Object API + assert(parsed.get_string("name")!! == "John Doe"); + assert(parsed.get_int("age")!! == 30); + assert(parsed.get_bool("is_active")!! == true); + assert(parsed.get_float("height")!! == 5.9f); + assert(parsed.get_string("status")!! == "ACTIVE"); - // Should start and end with braces + // Also keep the original string checks for format verification assert(json.starts_with("{")); assert(json.ends_with("}")); } @@ -267,17 +276,21 @@ fn void test_struct_with_multiple_fields() @test .rating = 4.8, .in_stock = true }; - + String json = json::tmarshal(product)!!; - // Check that all fields are present - assert(json.contains(`"title":"C3 Programming Book"`)); - assert(json.contains(`"price":2999`)); - assert(json.contains(`"category":"Programming"`)); - assert(json.contains(`"rating":4.8`)); - assert(json.contains(`"in_stock":true`)); - - // Should start and end with braces + // Parse the marshaled JSON back to verify it's valid and correct + Object* parsed = json::tparse_string(json)!!; + defer parsed.free(); + + // Verify all fields through the JSON Object API + assert(parsed.get_string("title")!! == "C3 Programming Book"); + assert(parsed.get_int("price")!! == 2999); + assert(parsed.get_string("category")!! == "Programming"); + assert(parsed.get_float("rating")!! == 4.8f); + assert(parsed.get_bool("in_stock")!! == true); + + // Also keep the original string checks for format verification assert(json.starts_with("{")); assert(json.ends_with("}")); } @@ -287,26 +300,52 @@ fn void test_array_marshaling() @test // Test integer arrays int[] numbers = { 1, 2, 3, 4, 5 }; String result = json::tmarshal_array(numbers)!!; - assert(result == "[1,2,3,4,5]"); - + + // Parse and verify integer array + Object* parsed_numbers = json::tparse_string(result)!!; + defer parsed_numbers.free(); + assert(parsed_numbers.get_len() == 5); + assert(parsed_numbers.get_int_at(0)!! == 1); + assert(parsed_numbers.get_int_at(1)!! == 2); + assert(parsed_numbers.get_int_at(2)!! == 3); + assert(parsed_numbers.get_int_at(3)!! == 4); + assert(parsed_numbers.get_int_at(4)!! == 5); + // Test string arrays String[] words = { "hello", "world", "test" }; result = json::tmarshal_array(words)!!; - assert(result == `["hello","world","test"]`); - + + // Parse and verify string array + Object* parsed_words = json::tparse_string(result)!!; + defer parsed_words.free(); + assert(parsed_words.get_len() == 3); + assert(parsed_words.get_string_at(0)!! == "hello"); + assert(parsed_words.get_string_at(1)!! == "world"); + assert(parsed_words.get_string_at(2)!! == "test"); + // Test float arrays float[] prices = { 1.99, 2.50, 3.14 }; result = json::tmarshal_array(prices)!!; - assert(result.starts_with("[")); - assert(result.ends_with("]")); - assert(result.contains("1.99")); - assert(result.contains("2.5")); - assert(result.contains("3.14")); - + + // Parse and verify float array + Object* parsed_prices = json::tparse_string(result)!!; + defer parsed_prices.free(); + assert(parsed_prices.get_len() == 3); + assert(parsed_prices.get_float_at(0)!! == 1.99f); + assert(parsed_prices.get_float_at(1)!! == 2.50f); + assert(parsed_prices.get_float_at(2)!! == 3.14f); + // Test boolean arrays bool[] flags = { true, false, true }; result = json::tmarshal_array(flags)!!; - assert(result == "[true,false,true]"); + + // Parse and verify boolean array + Object* parsed_flags = json::tparse_string(result)!!; + defer parsed_flags.free(); + assert(parsed_flags.get_len() == 3); + assert(parsed_flags.get_bool_at(0)!! == true); + assert(parsed_flags.get_bool_at(1)!! == false); + assert(parsed_flags.get_bool_at(2)!! == true); // Test enum arrays Status[] statuses = { Status.ACTIVE, Status.PENDING, Status.INACTIVE }; @@ -339,10 +378,16 @@ fn void test_string_escaping() @test String json = json::tmarshal(person)!!; - // Should properly escape quotes in the name - assert(json.contains(`"name":"John \"The Coder\" Doe"`)); - assert(json.contains(`"age":25`)); - assert(json.contains(`"status":"ACTIVE"`)); + // Parse the marshaled JSON back to verify escaping works correctly + Object* parsed = json::tparse_string(json)!!; + defer parsed.free(); + + // Verify that the original string with quotes is preserved correctly + assert(parsed.get_string("name")!! == "John \"The Coder\" Doe"); + assert(parsed.get_int("age")!! == 25); + assert(parsed.get_bool("is_active")!! == true); + assert(parsed.get_float("height")!! == 5.8f); + assert(parsed.get_string("status")!! == "ACTIVE"); } fn void test_empty_strings() @test @@ -396,23 +441,29 @@ fn void test_nested_struct_marshaling() @test .city = "New York", .zip_code = 10001 }; - + Company company = { .name = "Tech Corp", .headquarters = address, .employee_count = 500 }; - + String json = json::tmarshal(company)!!; - // Check that nested struct is properly marshaled - assert(json.contains(`"name":"Tech Corp"`)); - assert(json.contains(`"headquarters":{`)); - assert(json.contains(`"street":"123 Main St"`)); - assert(json.contains(`"city":"New York"`)); - assert(json.contains(`"zip_code":10001`)); - assert(json.contains(`"employee_count":500`)); - + // Parse the marshaled JSON back to verify it's valid and correct + Object* parsed = json::tparse_string(json)!!; + defer parsed.free(); + + // Verify top-level fields + assert(parsed.get_string("name")!! == "Tech Corp"); + assert(parsed.get_int("employee_count")!! == 500); + + // Verify nested struct fields + Object* headquarters = parsed.get("headquarters")!!; + assert(headquarters.get_string("street")!! == "123 Main St"); + assert(headquarters.get_string("city")!! == "New York"); + assert(headquarters.get_int("zip_code")!! == 10001); + // Should be valid JSON format assert(json.starts_with("{")); assert(json.ends_with("}")); @@ -440,24 +491,36 @@ fn void test_deeply_nested_struct_marshaling() @test .salary = 85000.0, .is_remote = true }; - + String json = json::tmarshal(employee)!!; - // Check deeply nested structure - assert(json.contains(`"personal_info":{`)); - assert(json.contains(`"name":"Alice Johnson"`)); - assert(json.contains(`"age":28`)); - - assert(json.contains(`"employer":{`)); - assert(json.contains(`"name":"Innovation Labs"`)); - assert(json.contains(`"headquarters":{`)); - assert(json.contains(`"street":"456 Tech Ave"`)); - assert(json.contains(`"city":"San Francisco"`)); - assert(json.contains(`"zip_code":94105`)); - - assert(json.contains(`"salary":85000`)); - assert(json.contains(`"is_remote":true`)); - + // Parse the marshaled JSON back to verify it's valid and correct + Object* parsed = json::tparse_string(json)!!; + defer parsed.free(); + + // Verify top-level fields + assert(parsed.get_float("salary")!! == 85000.0f); + assert(parsed.get_bool("is_remote")!! == true); + + // Verify personal_info nested struct + Object* personal_info = parsed.get("personal_info")!!; + assert(personal_info.get_string("name")!! == "Alice Johnson"); + assert(personal_info.get_int("age")!! == 28); + assert(personal_info.get_bool("is_active")!! == true); + assert(personal_info.get_float("height")!! == 5.6f); + assert(personal_info.get_string("status")!! == "ACTIVE"); + + // Verify employer nested struct + Object* employer = parsed.get("employer")!!; + assert(employer.get_string("name")!! == "Innovation Labs"); + assert(employer.get_int("employee_count")!! == 250); + + // Verify deeply nested headquarters struct + Object* headquarters = employer.get("headquarters")!!; + assert(headquarters.get_string("street")!! == "456 Tech Ave"); + assert(headquarters.get_string("city")!! == "San Francisco"); + assert(headquarters.get_int("zip_code")!! == 94105); + // Should be valid JSON format assert(json.starts_with("{")); assert(json.ends_with("}")); @@ -477,18 +540,31 @@ fn void test_array_of_structs() @test .zip_code = 60601 } }; - + String json = json::tmarshal_array(addresses)!!; - // Check array of structs + // Parse the marshaled JSON back to verify it's valid and correct + Object* parsed = json::tparse_string(json)!!; + defer parsed.free(); + + // Verify array structure and length + assert(parsed.get_len() == 2); + + // Verify first address struct + Object* first_address = parsed.get_at(0); + assert(first_address.get_string("street")!! == "100 First St"); + assert(first_address.get_string("city")!! == "Boston"); + assert(first_address.get_int("zip_code")!! == 2101); + + // Verify second address struct + Object* second_address = parsed.get_at(1); + assert(second_address.get_string("street")!! == "200 Second Ave"); + assert(second_address.get_string("city")!! == "Chicago"); + assert(second_address.get_int("zip_code")!! == 60601); + + // Check array format assert(json.starts_with("[")); assert(json.ends_with("]")); - assert(json.contains(`"street":"100 First St"`)); - assert(json.contains(`"city":"Boston"`)); - assert(json.contains(`"zip_code":2101`)); - assert(json.contains(`"street":"200 Second Ave"`)); - assert(json.contains(`"city":"Chicago"`)); - assert(json.contains(`"zip_code":60601`)); } fn void test_struct_with_nested_arrays() @test @@ -511,19 +587,40 @@ fn void test_struct_with_nested_arrays() @test .budget = 1000000, .priority = Priority.HIGH }; - + String json = json::tmarshal(dept)!!; - // Check that both primitive and struct arrays are marshaled correctly - assert(json.contains(`"name":"Engineering"`)); - assert(json.contains(`"skills_required":["C3","Rust","Go"]`)); - assert(json.contains(`"office_locations":[`)); - assert(json.contains(`"street":"100 Tech Blvd"`)); - assert(json.contains(`"city":"Austin"`)); - assert(json.contains(`"street":"200 Innovation Dr"`)); - assert(json.contains(`"city":"Seattle"`)); - assert(json.contains(`"budget":1000000`)); - assert(json.contains(`"priority":"HIGH"`)); + // Parse the marshaled JSON back to verify it's valid and correct + Object* parsed = json::tparse_string(json)!!; + defer parsed.free(); + + // Verify top-level fields + assert(parsed.get_string("name")!! == "Engineering"); + assert(parsed.get_int("budget")!! == 1000000); + assert(parsed.get_string("priority")!! == "HIGH"); + + // Verify skills_required string array + Object* skills = parsed.get("skills_required")!!; + assert(skills.get_len() == 3); + assert(skills.get_string_at(0)!! == "C3"); + assert(skills.get_string_at(1)!! == "Rust"); + assert(skills.get_string_at(2)!! == "Go"); + + // Verify office_locations struct array + Object* locations = parsed.get("office_locations")!!; + assert(locations.get_len() == 2); + + // Verify first office location + Object* first_office = locations.get_at(0); + assert(first_office.get_string("street")!! == "100 Tech Blvd"); + assert(first_office.get_string("city")!! == "Austin"); + assert(first_office.get_int("zip_code")!! == 78701); + + // Verify second office location + Object* second_office = locations.get_at(1); + assert(second_office.get_string("street")!! == "200 Innovation Dr"); + assert(second_office.get_string("city")!! == "Seattle"); + assert(second_office.get_int("zip_code")!! == 98101); // Should be valid JSON format assert(json.starts_with("{")); From 4553cba6a851c2422f090a53c9b1b532834a9702 Mon Sep 17 00:00:00 2001 From: Disheng Su Date: Wed, 2 Jul 2025 07:08:59 -0700 Subject: [PATCH 5/6] Address comments --- lib/std/encoding/json_marshal.c3 | 6 +++--- test/unit/stdlib/encoding/json_marshal.c3 | 12 ------------ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/lib/std/encoding/json_marshal.c3 b/lib/std/encoding/json_marshal.c3 index 2d41b422c..8356d89d7 100644 --- a/lib/std/encoding/json_marshal.c3 +++ b/lib/std/encoding/json_marshal.c3 @@ -15,7 +15,7 @@ faultdef UNSUPPORTED_TYPE; <* Marshal a struct with primitive fields and nested structs to JSON. - @param allocator : "The allocator to use for the result" + @param allocator: "The allocator to use for the result" @param value: "The struct value to marshal" @require @typekind(value) == STRUCT @return "The JSON string representation" @@ -66,7 +66,7 @@ macro String? tmarshal(value) => marshal(tmem, value); <* Marshal a primitive value to JSON. - @param allocator : "The allocator to use for the result" + @param allocator: "The allocator to use for the result" @param value: "The value to marshal" @return "The JSON string representation" *> @@ -108,7 +108,7 @@ macro String? tmarshal_value(value) => marshal_value(tmem, value); <* Marshal an array of primitive values to JSON. - @param allocator : "The allocator to use for the result" + @param allocator: "The allocator to use for the result" @param array: "The array to marshal" @return "The JSON array string representation" *> diff --git a/test/unit/stdlib/encoding/json_marshal.c3 b/test/unit/stdlib/encoding/json_marshal.c3 index e183b7f57..bb23894bd 100644 --- a/test/unit/stdlib/encoding/json_marshal.c3 +++ b/test/unit/stdlib/encoding/json_marshal.c3 @@ -226,7 +226,6 @@ fn void test_struct_with_enums() @test // Parse the marshaled JSON back to verify it's valid and correct Object* parsed = json::tparse_string(json)!!; - defer parsed.free(); // Verify all fields through the JSON Object API assert(parsed.get_string("title")!! == "Implement JSON marshaling"); @@ -253,7 +252,6 @@ fn void test_simple_struct_marshaling() @test // Parse the marshaled JSON back to verify it's valid and correct Object* parsed = json::tparse_string(json)!!; - defer parsed.free(); // Verify all fields through the JSON Object API assert(parsed.get_string("name")!! == "John Doe"); @@ -281,7 +279,6 @@ fn void test_struct_with_multiple_fields() @test // Parse the marshaled JSON back to verify it's valid and correct Object* parsed = json::tparse_string(json)!!; - defer parsed.free(); // Verify all fields through the JSON Object API assert(parsed.get_string("title")!! == "C3 Programming Book"); @@ -303,7 +300,6 @@ fn void test_array_marshaling() @test // Parse and verify integer array Object* parsed_numbers = json::tparse_string(result)!!; - defer parsed_numbers.free(); assert(parsed_numbers.get_len() == 5); assert(parsed_numbers.get_int_at(0)!! == 1); assert(parsed_numbers.get_int_at(1)!! == 2); @@ -317,7 +313,6 @@ fn void test_array_marshaling() @test // Parse and verify string array Object* parsed_words = json::tparse_string(result)!!; - defer parsed_words.free(); assert(parsed_words.get_len() == 3); assert(parsed_words.get_string_at(0)!! == "hello"); assert(parsed_words.get_string_at(1)!! == "world"); @@ -329,7 +324,6 @@ fn void test_array_marshaling() @test // Parse and verify float array Object* parsed_prices = json::tparse_string(result)!!; - defer parsed_prices.free(); assert(parsed_prices.get_len() == 3); assert(parsed_prices.get_float_at(0)!! == 1.99f); assert(parsed_prices.get_float_at(1)!! == 2.50f); @@ -341,7 +335,6 @@ fn void test_array_marshaling() @test // Parse and verify boolean array Object* parsed_flags = json::tparse_string(result)!!; - defer parsed_flags.free(); assert(parsed_flags.get_len() == 3); assert(parsed_flags.get_bool_at(0)!! == true); assert(parsed_flags.get_bool_at(1)!! == false); @@ -380,7 +373,6 @@ fn void test_string_escaping() @test // Parse the marshaled JSON back to verify escaping works correctly Object* parsed = json::tparse_string(json)!!; - defer parsed.free(); // Verify that the original string with quotes is preserved correctly assert(parsed.get_string("name")!! == "John \"The Coder\" Doe"); @@ -452,7 +444,6 @@ fn void test_nested_struct_marshaling() @test // Parse the marshaled JSON back to verify it's valid and correct Object* parsed = json::tparse_string(json)!!; - defer parsed.free(); // Verify top-level fields assert(parsed.get_string("name")!! == "Tech Corp"); @@ -496,7 +487,6 @@ fn void test_deeply_nested_struct_marshaling() @test // Parse the marshaled JSON back to verify it's valid and correct Object* parsed = json::tparse_string(json)!!; - defer parsed.free(); // Verify top-level fields assert(parsed.get_float("salary")!! == 85000.0f); @@ -545,7 +535,6 @@ fn void test_array_of_structs() @test // Parse the marshaled JSON back to verify it's valid and correct Object* parsed = json::tparse_string(json)!!; - defer parsed.free(); // Verify array structure and length assert(parsed.get_len() == 2); @@ -592,7 +581,6 @@ fn void test_struct_with_nested_arrays() @test // Parse the marshaled JSON back to verify it's valid and correct Object* parsed = json::tparse_string(json)!!; - defer parsed.free(); // Verify top-level fields assert(parsed.get_string("name")!! == "Engineering"); From f9c8864f189c583a888c1e3fd6bb5c21a206d725 Mon Sep 17 00:00:00 2001 From: Disheng Su Date: Thu, 3 Jul 2025 00:40:36 -0700 Subject: [PATCH 6/6] Fix memory leak --- lib/std/encoding/json_marshal.c3 | 6 +- test/unit/stdlib/encoding/json_marshal.c3 | 685 +++++----------------- 2 files changed, 166 insertions(+), 525 deletions(-) diff --git a/lib/std/encoding/json_marshal.c3 b/lib/std/encoding/json_marshal.c3 index 8356d89d7..a2481b8f6 100644 --- a/lib/std/encoding/json_marshal.c3 +++ b/lib/std/encoding/json_marshal.c3 @@ -25,6 +25,7 @@ macro String? marshal(Allocator allocator, value) var $Type = $typeof(value); DString result = dstring::new_with_capacity(allocator, 32); + defer result.free(); result.append_char('{'); @@ -44,7 +45,7 @@ macro String? marshal(Allocator allocator, value) // Add field value using common marshaling logic @pool() { - String field_result = marshal_value(allocator, $member.get(value))!; + String field_result = tmarshal_value($member.get(value))!; result.append(field_result); }; $endif @@ -115,6 +116,7 @@ macro String? tmarshal_value(value) => marshal_value(tmem, value); macro String? marshal_array(Allocator allocator, array) { DString result = dstring::new_with_capacity(allocator, 32); + defer result.free(); result.append_char('['); @@ -125,7 +127,7 @@ macro String? marshal_array(Allocator allocator, array) // Use common marshaling logic for each element @pool() { - String element_result = marshal_value(allocator, element)!; + String element_result = tmarshal_value(element)!; result.append(element_result); }; } diff --git a/test/unit/stdlib/encoding/json_marshal.c3 b/test/unit/stdlib/encoding/json_marshal.c3 index bb23894bd..8047594dc 100644 --- a/test/unit/stdlib/encoding/json_marshal.c3 +++ b/test/unit/stdlib/encoding/json_marshal.c3 @@ -8,19 +8,17 @@ enum Status { ACTIVE, INACTIVE, - PENDING, - SUSPENDED + PENDING } enum Priority { LOW, MEDIUM, - HIGH, - CRITICAL + HIGH } -// Enum with single String associated value +// Enum with associated value enum State : int (String description) { WAITING = "waiting", @@ -28,14 +26,14 @@ enum State : int (String description) TERMINATED = "ended" } -// Enum with multiple associated values (marshaled using enum name) -enum ComplexState : int (String desc, bool active) +// Basic structures +struct Address { - IDLE = { "idle", false }, - BUSY = { "busy", true } + String street; + String city; + int zip_code; } -// Test structures with various primitive types struct Person { String name; @@ -45,523 +43,78 @@ struct Person Status status; } -struct Product -{ - String title; - int price; - String category; - float rating; - bool in_stock; -} - -// Test struct with all supported primitive types -struct AllTypes +// Complex structure with all features +struct ComplexData { + // Primitive types String text; int integer; float single_precision; double double_precision; bool flag; -} -// Nested struct test structures -struct Address -{ - String street; - String city; - int zip_code; -} - -struct Company -{ - String name; - Address headquarters; - int employee_count; -} - -struct Employee -{ - Person personal_info; - Company employer; - float salary; - bool is_remote; -} - -struct Department -{ - String name; - String[] skills_required; - Address[] office_locations; - int budget; - Priority priority; -} - -struct Task -{ - String title; + // Enums Status status; Priority priority; - int estimated_hours; -} - -struct Process -{ - String name; State current_state; - ComplexState complex_state; - int pid; -} - -fn void test_primitive_marshaling() @test -{ - // Test integers - String result = json::tmarshal_value(42)!!; - assert(result == "42"); - - // Test floats - result = json::tmarshal_value(3.14)!!; - assert(result.starts_with("3.14")); - - // Test doubles - result = json::tmarshal_value(2.718281828)!!; - assert(result.starts_with("2.718")); - - // Test booleans - result = json::tmarshal_value(true)!!; - assert(result == "true"); - - result = json::tmarshal_value(false)!!; - assert(result == "false"); - - // Test strings - result = json::tmarshal_value("hello world")!!; - assert(result == `"hello world"`); - - // Test string with special characters - result = json::tmarshal_value("Hello \"world\"")!!; - assert(result == `"Hello \"world\""`); -} - -fn void test_enum_marshaling() @test -{ - // Test individual enum values - String result = json::tmarshal_value(Status.ACTIVE)!!; - assert(result == `"ACTIVE"`); - - result = json::tmarshal_value(Status.INACTIVE)!!; - assert(result == `"INACTIVE"`); - - result = json::tmarshal_value(Status.PENDING)!!; - assert(result == `"PENDING"`); - - result = json::tmarshal_value(Status.SUSPENDED)!!; - assert(result == `"SUSPENDED"`); - - // Test Priority enum - result = json::tmarshal_value(Priority.LOW)!!; - assert(result == `"LOW"`); - - result = json::tmarshal_value(Priority.MEDIUM)!!; - assert(result == `"MEDIUM"`); - - result = json::tmarshal_value(Priority.HIGH)!!; - assert(result == `"HIGH"`); - - result = json::tmarshal_value(Priority.CRITICAL)!!; - assert(result == `"CRITICAL"`); -} - -fn void test_enum_with_associated_values() @test -{ - // Test enum with single String associated value (always uses enum names) - String result = json::tmarshal_value(State.WAITING)!!; - assert(result == `"WAITING"`); - result = json::tmarshal_value(State.RUNNING)!!; - assert(result == `"RUNNING"`); + // Nested struct + Person owner; + Address location; - result = json::tmarshal_value(State.TERMINATED)!!; - assert(result == `"TERMINATED"`); + // Arrays of primitives + String[] tags; + int[] numbers; + float[] ratings; + bool[] flags; - // Test enum with multiple associated values (always uses enum names) - result = json::tmarshal_value(ComplexState.IDLE)!!; - assert(result == `"IDLE"`); + // Array of structs + Address[] offices; + Person[] team_members; - result = json::tmarshal_value(ComplexState.BUSY)!!; - assert(result == `"BUSY"`); + // Array of arrays + int[][] matrix; + String[][] categories; } -fn void test_struct_with_associated_value_enums() @test +fn void test_comprehensive_marshaling() @test { - Process process = { - .name = "web_server", - .current_state = State.RUNNING, - .complex_state = ComplexState.BUSY, - .pid = 1234 - }; - - String json = json::tmarshal(process)!!; - - // Check that enums always use the enum name - assert(json.contains(`"name":"web_server"`)); - assert(json.contains(`"current_state":"RUNNING"`)); // Always uses enum name - assert(json.contains(`"complex_state":"BUSY"`)); // Always uses enum name - assert(json.contains(`"pid":1234`)); - - // Should be valid JSON format - assert(json.starts_with("{")); - assert(json.ends_with("}")); -} - -fn void test_struct_with_enums() @test -{ - Task task = { - .title = "Implement JSON marshaling", - .status = Status.ACTIVE, - .priority = Priority.HIGH, - .estimated_hours = 8 - }; - - String json = json::tmarshal(task)!!; - - // Parse the marshaled JSON back to verify it's valid and correct - Object* parsed = json::tparse_string(json)!!; - - // Verify all fields through the JSON Object API - assert(parsed.get_string("title")!! == "Implement JSON marshaling"); - assert(parsed.get_string("status")!! == "ACTIVE"); // Enum marshaled as string - assert(parsed.get_string("priority")!! == "HIGH"); // Enum marshaled as string - assert(parsed.get_int("estimated_hours")!! == 8); - - // Should be valid JSON format - assert(json.starts_with("{")); - assert(json.ends_with("}")); -} - -fn void test_simple_struct_marshaling() @test -{ - Person person = { - .name = "John Doe", - .age = 30, - .is_active = true, - .height = 5.9, - .status = Status.ACTIVE - }; - - String json = json::tmarshal(person)!!; - - // Parse the marshaled JSON back to verify it's valid and correct - Object* parsed = json::tparse_string(json)!!; - - // Verify all fields through the JSON Object API - assert(parsed.get_string("name")!! == "John Doe"); - assert(parsed.get_int("age")!! == 30); - assert(parsed.get_bool("is_active")!! == true); - assert(parsed.get_float("height")!! == 5.9f); - assert(parsed.get_string("status")!! == "ACTIVE"); - - // Also keep the original string checks for format verification - assert(json.starts_with("{")); - assert(json.ends_with("}")); -} - -fn void test_struct_with_multiple_fields() @test -{ - Product product = { - .title = "C3 Programming Book", - .price = 2999, - .category = "Programming", - .rating = 4.8, - .in_stock = true - }; - - String json = json::tmarshal(product)!!; - - // Parse the marshaled JSON back to verify it's valid and correct - Object* parsed = json::tparse_string(json)!!; - - // Verify all fields through the JSON Object API - assert(parsed.get_string("title")!! == "C3 Programming Book"); - assert(parsed.get_int("price")!! == 2999); - assert(parsed.get_string("category")!! == "Programming"); - assert(parsed.get_float("rating")!! == 4.8f); - assert(parsed.get_bool("in_stock")!! == true); - - // Also keep the original string checks for format verification - assert(json.starts_with("{")); - assert(json.ends_with("}")); -} - -fn void test_array_marshaling() @test -{ - // Test integer arrays - int[] numbers = { 1, 2, 3, 4, 5 }; - String result = json::tmarshal_array(numbers)!!; - - // Parse and verify integer array - Object* parsed_numbers = json::tparse_string(result)!!; - assert(parsed_numbers.get_len() == 5); - assert(parsed_numbers.get_int_at(0)!! == 1); - assert(parsed_numbers.get_int_at(1)!! == 2); - assert(parsed_numbers.get_int_at(2)!! == 3); - assert(parsed_numbers.get_int_at(3)!! == 4); - assert(parsed_numbers.get_int_at(4)!! == 5); - - // Test string arrays - String[] words = { "hello", "world", "test" }; - result = json::tmarshal_array(words)!!; - - // Parse and verify string array - Object* parsed_words = json::tparse_string(result)!!; - assert(parsed_words.get_len() == 3); - assert(parsed_words.get_string_at(0)!! == "hello"); - assert(parsed_words.get_string_at(1)!! == "world"); - assert(parsed_words.get_string_at(2)!! == "test"); - - // Test float arrays - float[] prices = { 1.99, 2.50, 3.14 }; - result = json::tmarshal_array(prices)!!; - - // Parse and verify float array - Object* parsed_prices = json::tparse_string(result)!!; - assert(parsed_prices.get_len() == 3); - assert(parsed_prices.get_float_at(0)!! == 1.99f); - assert(parsed_prices.get_float_at(1)!! == 2.50f); - assert(parsed_prices.get_float_at(2)!! == 3.14f); - - // Test boolean arrays - bool[] flags = { true, false, true }; - result = json::tmarshal_array(flags)!!; - - // Parse and verify boolean array - Object* parsed_flags = json::tparse_string(result)!!; - assert(parsed_flags.get_len() == 3); - assert(parsed_flags.get_bool_at(0)!! == true); - assert(parsed_flags.get_bool_at(1)!! == false); - assert(parsed_flags.get_bool_at(2)!! == true); - - // Test enum arrays - Status[] statuses = { Status.ACTIVE, Status.PENDING, Status.INACTIVE }; - result = json::tmarshal_array(statuses)!!; - assert(result == `["ACTIVE","PENDING","INACTIVE"]`); - - Priority[] priorities = { Priority.LOW, Priority.HIGH }; - result = json::tmarshal_array(priorities)!!; - assert(result == `["LOW","HIGH"]`); - - // Test enum arrays with associated values (always uses enum names) - State[] states = { State.WAITING, State.RUNNING, State.TERMINATED }; - result = json::tmarshal_array(states)!!; - assert(result == `["WAITING","RUNNING","TERMINATED"]`); // Always uses enum names - - ComplexState[] complex_states = { ComplexState.IDLE, ComplexState.BUSY }; - result = json::tmarshal_array(complex_states)!!; - assert(result == `["IDLE","BUSY"]`); // Always uses enum names -} - -fn void test_string_escaping() @test -{ - Person person = { - .name = "John \"The Coder\" Doe", - .age = 25, - .is_active = true, - .height = 5.8, - .status = Status.ACTIVE - }; - - String json = json::tmarshal(person)!!; - - // Parse the marshaled JSON back to verify escaping works correctly - Object* parsed = json::tparse_string(json)!!; - - // Verify that the original string with quotes is preserved correctly - assert(parsed.get_string("name")!! == "John \"The Coder\" Doe"); - assert(parsed.get_int("age")!! == 25); - assert(parsed.get_bool("is_active")!! == true); - assert(parsed.get_float("height")!! == 5.8f); - assert(parsed.get_string("status")!! == "ACTIVE"); -} - -fn void test_empty_strings() @test -{ - Person person = { - .name = "", - .age = 0, - .is_active = false, - .height = 0.0, - .status = Status.INACTIVE - }; - - String json = json::tmarshal(person)!!; - - // Should handle empty strings and zero values properly - assert(json.contains(`"name":""`)); - assert(json.contains(`"age":0`)); - assert(json.contains(`"is_active":false`)); - assert(json.contains(`"height":0`)); - assert(json.contains(`"status":"INACTIVE"`)); -} - -fn void test_all_primitive_types() @test -{ - AllTypes data = { - .text = "test string", + // Create a complex data structure with all supported features + ComplexData data = { + // Primitive types + .text = "Hello \"World\"", .integer = 42, .single_precision = 3.14, .double_precision = 2.718281828, - .flag = true - }; - - String json = json::tmarshal(data)!!; - - // Verify all types are marshaled correctly - assert(json.contains(`"text":"test string"`)); - assert(json.contains(`"integer":42`)); - assert(json.contains(`"single_precision":3.14`)); - assert(json.contains(`"double_precision":2.718`)); - assert(json.contains(`"flag":true`)); - - // Should be valid JSON format - assert(json.starts_with("{")); - assert(json.ends_with("}")); -} - -fn void test_nested_struct_marshaling() @test -{ - Address address = { - .street = "123 Main St", - .city = "New York", - .zip_code = 10001 - }; - - Company company = { - .name = "Tech Corp", - .headquarters = address, - .employee_count = 500 - }; + .flag = true, - String json = json::tmarshal(company)!!; - - // Parse the marshaled JSON back to verify it's valid and correct - Object* parsed = json::tparse_string(json)!!; - - // Verify top-level fields - assert(parsed.get_string("name")!! == "Tech Corp"); - assert(parsed.get_int("employee_count")!! == 500); - - // Verify nested struct fields - Object* headquarters = parsed.get("headquarters")!!; - assert(headquarters.get_string("street")!! == "123 Main St"); - assert(headquarters.get_string("city")!! == "New York"); - assert(headquarters.get_int("zip_code")!! == 10001); - - // Should be valid JSON format - assert(json.starts_with("{")); - assert(json.ends_with("}")); -} + // Enums + .status = Status.ACTIVE, + .priority = Priority.HIGH, + .current_state = State.RUNNING, -fn void test_deeply_nested_struct_marshaling() @test -{ - Employee employee = { - .personal_info = { - .name = "Alice Johnson", - .age = 28, + // Nested structs + .owner = { + .name = "John Doe", + .age = 30, .is_active = true, - .height = 5.6, + .height = 5.9, .status = Status.ACTIVE }, - .employer = { - .name = "Innovation Labs", - .headquarters = { - .street = "456 Tech Ave", - .city = "San Francisco", - .zip_code = 94105 - }, - .employee_count = 250 - }, - .salary = 85000.0, - .is_remote = true - }; - - String json = json::tmarshal(employee)!!; - - // Parse the marshaled JSON back to verify it's valid and correct - Object* parsed = json::tparse_string(json)!!; - - // Verify top-level fields - assert(parsed.get_float("salary")!! == 85000.0f); - assert(parsed.get_bool("is_remote")!! == true); - - // Verify personal_info nested struct - Object* personal_info = parsed.get("personal_info")!!; - assert(personal_info.get_string("name")!! == "Alice Johnson"); - assert(personal_info.get_int("age")!! == 28); - assert(personal_info.get_bool("is_active")!! == true); - assert(personal_info.get_float("height")!! == 5.6f); - assert(personal_info.get_string("status")!! == "ACTIVE"); - - // Verify employer nested struct - Object* employer = parsed.get("employer")!!; - assert(employer.get_string("name")!! == "Innovation Labs"); - assert(employer.get_int("employee_count")!! == 250); - - // Verify deeply nested headquarters struct - Object* headquarters = employer.get("headquarters")!!; - assert(headquarters.get_string("street")!! == "456 Tech Ave"); - assert(headquarters.get_string("city")!! == "San Francisco"); - assert(headquarters.get_int("zip_code")!! == 94105); - - // Should be valid JSON format - assert(json.starts_with("{")); - assert(json.ends_with("}")); -} - -fn void test_array_of_structs() @test -{ - Address[] addresses = { - { - .street = "100 First St", - .city = "Boston", - .zip_code = 2101 + .location = { + .street = "123 Main St", + .city = "New York", + .zip_code = 10001 }, - { - .street = "200 Second Ave", - .city = "Chicago", - .zip_code = 60601 - } - }; - - String json = json::tmarshal_array(addresses)!!; - - // Parse the marshaled JSON back to verify it's valid and correct - Object* parsed = json::tparse_string(json)!!; - - // Verify array structure and length - assert(parsed.get_len() == 2); - - // Verify first address struct - Object* first_address = parsed.get_at(0); - assert(first_address.get_string("street")!! == "100 First St"); - assert(first_address.get_string("city")!! == "Boston"); - assert(first_address.get_int("zip_code")!! == 2101); - // Verify second address struct - Object* second_address = parsed.get_at(1); - assert(second_address.get_string("street")!! == "200 Second Ave"); - assert(second_address.get_string("city")!! == "Chicago"); - assert(second_address.get_int("zip_code")!! == 60601); + // Arrays of primitives + .tags = { "important", "urgent", "review" }, + .numbers = { 1, 2, 3, 4, 5 }, + .ratings = { 4.5, 3.8, 4.9 }, + .flags = { true, false, true }, - // Check array format - assert(json.starts_with("[")); - assert(json.ends_with("]")); -} - -fn void test_struct_with_nested_arrays() @test -{ - Department dept = { - .name = "Engineering", - .skills_required = { "C3", "Rust", "Go" }, - .office_locations = { + // Array of structs + .offices = { { .street = "100 Tech Blvd", .city = "Austin", @@ -573,44 +126,130 @@ fn void test_struct_with_nested_arrays() @test .zip_code = 98101 } }, - .budget = 1000000, - .priority = Priority.HIGH + .team_members = { + { + .name = "Alice Smith", + .age = 28, + .is_active = true, + .height = 5.6, + .status = Status.ACTIVE + }, + { + .name = "Bob Johnson", + .age = 35, + .is_active = false, + .height = 6.1, + .status = Status.INACTIVE + } + }, + + // Array of arrays + .matrix = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }, + .categories = { { "tech", "programming" }, { "business", "finance" } } }; - String json = json::tmarshal(dept)!!; + // Marshal the complex data structure + String json = json::marshal(mem, data)!!; + defer free(json); // Parse the marshaled JSON back to verify it's valid and correct Object* parsed = json::tparse_string(json)!!; - // Verify top-level fields - assert(parsed.get_string("name")!! == "Engineering"); - assert(parsed.get_int("budget")!! == 1000000); - assert(parsed.get_string("priority")!! == "HIGH"); - - // Verify skills_required string array - Object* skills = parsed.get("skills_required")!!; - assert(skills.get_len() == 3); - assert(skills.get_string_at(0)!! == "C3"); - assert(skills.get_string_at(1)!! == "Rust"); - assert(skills.get_string_at(2)!! == "Go"); + // Verify primitive types + assert(parsed.get_string("text")!! == "Hello \"World\""); + assert(parsed.get_int("integer")!! == 42); + assert(parsed.get_float("single_precision")!! == 3.14f); + assert(parsed.get_bool("flag")!! == true); - // Verify office_locations struct array - Object* locations = parsed.get("office_locations")!!; - assert(locations.get_len() == 2); - - // Verify first office location - Object* first_office = locations.get_at(0); + // Verify enums (marshaled as strings) + assert(parsed.get_string("status")!! == "ACTIVE"); + assert(parsed.get_string("priority")!! == "HIGH"); + assert(parsed.get_string("current_state")!! == "RUNNING"); + + // Verify nested structs + Object* owner = parsed.get("owner")!!; + assert(owner.get_string("name")!! == "John Doe"); + assert(owner.get_int("age")!! == 30); + assert(owner.get_bool("is_active")!! == true); + assert(owner.get_float("height")!! == 5.9f); + assert(owner.get_string("status")!! == "ACTIVE"); + + Object* location = parsed.get("location")!!; + assert(location.get_string("street")!! == "123 Main St"); + assert(location.get_string("city")!! == "New York"); + assert(location.get_int("zip_code")!! == 10001); + + // Verify arrays of primitives + Object* tags = parsed.get("tags")!!; + assert(tags.get_len() == 3); + assert(tags.get_string_at(0)!! == "important"); + assert(tags.get_string_at(1)!! == "urgent"); + assert(tags.get_string_at(2)!! == "review"); + + Object* numbers = parsed.get("numbers")!!; + assert(numbers.get_len() == 5); + assert(numbers.get_int_at(0)!! == 1); + assert(numbers.get_int_at(4)!! == 5); + + Object* ratings = parsed.get("ratings")!!; + assert(ratings.get_len() == 3); + assert(ratings.get_float_at(0)!! == 4.5f); + assert(ratings.get_float_at(2)!! == 4.9f); + + Object* flags = parsed.get("flags")!!; + assert(flags.get_len() == 3); + assert(flags.get_bool_at(0)!! == true); + assert(flags.get_bool_at(1)!! == false); + assert(flags.get_bool_at(2)!! == true); + + // Verify array of structs + Object* offices = parsed.get("offices")!!; + assert(offices.get_len() == 2); + + Object* first_office = offices.get_at(0); assert(first_office.get_string("street")!! == "100 Tech Blvd"); assert(first_office.get_string("city")!! == "Austin"); assert(first_office.get_int("zip_code")!! == 78701); - // Verify second office location - Object* second_office = locations.get_at(1); + Object* second_office = offices.get_at(1); assert(second_office.get_string("street")!! == "200 Innovation Dr"); assert(second_office.get_string("city")!! == "Seattle"); assert(second_office.get_int("zip_code")!! == 98101); - // Should be valid JSON format + Object* team_members = parsed.get("team_members")!!; + assert(team_members.get_len() == 2); + + Object* alice = team_members.get_at(0); + assert(alice.get_string("name")!! == "Alice Smith"); + assert(alice.get_int("age")!! == 28); + assert(alice.get_bool("is_active")!! == true); + assert(alice.get_string("status")!! == "ACTIVE"); + + Object* bob = team_members.get_at(1); + assert(bob.get_string("name")!! == "Bob Johnson"); + assert(bob.get_int("age")!! == 35); + assert(bob.get_bool("is_active")!! == false); + assert(bob.get_string("status")!! == "INACTIVE"); + + // Verify array of arrays + Object* matrix = parsed.get("matrix")!!; + assert(matrix.get_len() == 3); + + Object* first_row = matrix.get_at(0); + assert(first_row.get_len() == 3); + assert(first_row.get_int_at(0)!! == 1); + assert(first_row.get_int_at(1)!! == 2); + assert(first_row.get_int_at(2)!! == 3); + + Object* categories = parsed.get("categories")!!; + assert(categories.get_len() == 2); + + Object* first_category = categories.get_at(0); + assert(first_category.get_len() == 2); + assert(first_category.get_string_at(0)!! == "tech"); + assert(first_category.get_string_at(1)!! == "programming"); + + // Verify JSON format assert(json.starts_with("{")); assert(json.ends_with("}")); -} +} \ No newline at end of file