diff --git a/lib/std/encoding/json_marshal.c3 b/lib/std/encoding/json_marshal.c3 new file mode 100644 index 000000000..a2481b8f6 --- /dev/null +++ b/lib/std/encoding/json_marshal.c3 @@ -0,0 +1,164 @@ +// 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; + +<* + 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); + + DString result = dstring::new_with_capacity(allocator, 32); + defer result.free(); + + 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 using common marshaling logic + @pool() + { + String field_result = tmarshal_value($member.get(value))!; + result.append(field_result); + }; + $endif + $endforeach + + result.append_char('}'); + return result.copy_str(allocator); +} + +<* + 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(allocator, value); + $case ARRAY: + $case SLICE: + return marshal_array(allocator, 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 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::new_with_capacity(allocator, 32); + defer result.free(); + + result.append_char('['); + + foreach (i, element : array) + { + if (i > 0) result.append_char(','); + + // Use common marshaling logic for each element + @pool() + { + String element_result = tmarshal_value(element)!; + result.append(element_result); + }; + } + + result.append_char(']'); + return result.copy_str(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); + + // 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..8047594dc --- /dev/null +++ b/test/unit/stdlib/encoding/json_marshal.c3 @@ -0,0 +1,255 @@ +module json_marshal_test @test; +import std::encoding::json; +import std::collections::object; +import std::io; + +// Test enums +enum Status +{ + ACTIVE, + INACTIVE, + PENDING +} + +enum Priority +{ + LOW, + MEDIUM, + HIGH +} + +// Enum with associated value +enum State : int (String description) +{ + WAITING = "waiting", + RUNNING = "running", + TERMINATED = "ended" +} + +// Basic structures +struct Address +{ + String street; + String city; + int zip_code; +} + +struct Person +{ + String name; + int age; + bool is_active; + double height; + Status status; +} + +// Complex structure with all features +struct ComplexData +{ + // Primitive types + String text; + int integer; + float single_precision; + double double_precision; + bool flag; + + // Enums + Status status; + Priority priority; + State current_state; + + // Nested struct + Person owner; + Address location; + + // Arrays of primitives + String[] tags; + int[] numbers; + float[] ratings; + bool[] flags; + + // Array of structs + Address[] offices; + Person[] team_members; + + // Array of arrays + int[][] matrix; + String[][] categories; +} + +fn void test_comprehensive_marshaling() @test +{ + // 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, + + // Enums + .status = Status.ACTIVE, + .priority = Priority.HIGH, + .current_state = State.RUNNING, + + // Nested structs + .owner = { + .name = "John Doe", + .age = 30, + .is_active = true, + .height = 5.9, + .status = Status.ACTIVE + }, + .location = { + .street = "123 Main St", + .city = "New York", + .zip_code = 10001 + }, + + // Arrays of primitives + .tags = { "important", "urgent", "review" }, + .numbers = { 1, 2, 3, 4, 5 }, + .ratings = { 4.5, 3.8, 4.9 }, + .flags = { true, false, true }, + + // Array of structs + .offices = { + { + .street = "100 Tech Blvd", + .city = "Austin", + .zip_code = 78701 + }, + { + .street = "200 Innovation Dr", + .city = "Seattle", + .zip_code = 98101 + } + }, + .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" } } + }; + + // 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 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 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); + + 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); + + 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