From ffc6f6d294228f456cc45872604a4c8657685fc9 Mon Sep 17 00:00:00 2001 From: Vincent van Ecchi Date: Wed, 25 Dec 2024 21:26:06 +0100 Subject: [PATCH 1/3] Add json.unparse for json.Value --- core/encoding/json/unparse.odin | 116 +++++++++++++++++++ tests/core/encoding/json/test_core_json.odin | 56 ++++++++- 2 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 core/encoding/json/unparse.odin diff --git a/core/encoding/json/unparse.odin b/core/encoding/json/unparse.odin new file mode 100644 index 00000000000..06c61ed35a8 --- /dev/null +++ b/core/encoding/json/unparse.odin @@ -0,0 +1,116 @@ +package encoding_json + +import "base:runtime" +import "core:strings" +import "core:io" +import "core:slice" + +unparse :: proc(v: Value, opt: Marshal_Options = {}, allocator := context.allocator, loc := #caller_location) -> (data: []u8, err: io.Error) { + b := strings.builder_make(allocator, loc) + defer if err != nil { + strings.builder_destroy(&b) + } + + // temp guard in case we are sorting map keys, which will use temp allocations + runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD(ignore = allocator == context.temp_allocator) + + opt := opt + unparse_to_builder(&b, v, &opt) or_return + + if len(b.buf) != 0 { + data = b.buf[:] + } + + return data, nil +} + +unparse_to_builder :: proc(b: ^strings.Builder, v: Value, opt: ^Marshal_Options) -> io.Error { + return unparse_to_writer(strings.to_writer(b), v, opt) +} + +unparse_to_writer :: proc(w: io.Writer, v: Value, opt: ^Marshal_Options) -> io.Error { + switch uv in v { + case Null: return unparse_null_to_writer(w, uv, opt) + case Integer: return unparse_integer_to_writer(w, uv, opt) + case Float: return unparse_float_to_writer(w, uv, opt) + case Boolean: return unparse_boolean_to_writer(w, uv, opt) + case String: return unparse_string_to_writer(w, uv, opt) + case Array: return unparse_array_to_writer(w, uv, opt) + case Object: return unparse_object_to_writer(w, uv, opt) + } + return nil +} + +unparse_null_to_writer :: proc(w: io.Writer, v: Null, opt: ^Marshal_Options) -> io.Error { + io.write_string(w, "null") or_return + return nil +} + +unparse_integer_to_writer :: proc(w: io.Writer, v: Integer, opt: ^Marshal_Options) -> io.Error { + base := 16 if opt.write_uint_as_hex && (opt.spec == .JSON5 || opt.spec == .MJSON) else 10 + io.write_i64(w, v, base) or_return + return nil +} + +unparse_float_to_writer :: proc(w: io.Writer, v: Float, opt: ^Marshal_Options) -> io.Error { + io.write_f64(w, v) or_return + return nil +} + +unparse_boolean_to_writer :: proc(w: io.Writer, v: Boolean, opt: ^Marshal_Options) -> io.Error { + io.write_string(w, v ? "true" : "false") or_return + return nil +} + +unparse_string_to_writer :: proc(w: io.Writer, v: String, opt: ^Marshal_Options) -> io.Error { + io.write_quoted_string(w, v, '"', nil, true) or_return + return nil +} + +unparse_array_to_writer :: proc(w: io.Writer, v: Array, opt: ^Marshal_Options) -> io.Error { + opt_write_start(w, opt, '[') or_return + for i in 0.. io.Error { + if !opt.sort_maps_by_key { + opt_write_start(w, opt, '{') or_return + + first_iteration := true + for k,v in m { + opt_write_iteration(w, opt, first_iteration) or_return + opt_write_key(w, opt, k) or_return + unparse_to_writer(w, v, opt) or_return + first_iteration = false + } + + opt_write_end(w, opt, '}') or_return + } + else { + Entry :: struct { + key: string, + value: Value + } + + entries := make([dynamic]Entry, 0, len(m), context.temp_allocator) + for k, v in m { + append(&entries, Entry{k, v}) + } + + slice.sort_by(entries[:], proc(i, j: Entry) -> bool { return i.key < j.key }) + + opt_write_start(w, opt, '{') or_return + for e, i in entries { + opt_write_iteration(w, opt, i == 0) or_return + opt_write_key(w, opt, e.key) or_return + unparse_to_writer(w, e.value, opt) or_return + } + opt_write_end(w, opt, '}') or_return + } + return nil +} diff --git a/tests/core/encoding/json/test_core_json.odin b/tests/core/encoding/json/test_core_json.odin index 27cce7faa46..4cc998e3797 100644 --- a/tests/core/encoding/json/test_core_json.odin +++ b/tests/core/encoding/json/test_core_json.odin @@ -482,4 +482,58 @@ map_with_integer_keys :: proc(t: ^testing.T) { testing.expectf(t, runtime.string_eq(item, my_map2[key]), "Expected value %s to be present in unmarshaled map", key) } } -} \ No newline at end of file +} + +@test +unparse_json_schema :: proc(t: ^testing.T) { + + json_schema: json.Value = json.Object{ + "title" = "example", + "description" = "example json schema for unparse test", + "type" = "object", + "properties" = json.Object{ + "id" = json.Object{"type" = "integer"}, + "name" = json.Object{"type" = "string"}, + "is_valid" = json.Object{"type" = "boolean"}, + "tags" = json.Object{ + "type" = "array", + "items" = json.Object{"type" = "string"} + } + } + } + + // having fun cleaning up json literals + defer { + delete(json_schema.(json.Object)["properties"].(json.Object)["tags"].(json.Object)["items"].(json.Object)) + for k, &v in json_schema.(json.Object)["properties"].(json.Object) { + delete(v.(json.Object)) + } + delete(json_schema.(json.Object)["properties"].(json.Object)) + delete(json_schema.(json.Object)) + } + + is_error :: proc(t: ^testing.T, E: $Error_Type, fn: string) -> bool { + testing.expectf(t, E == nil, "%s failed with error:", fn, E) + return E != nil + } + + unparsed_json_schema, unparse_err := json.unparse(json_schema, json.Marshal_Options{sort_maps_by_key=true}) + if is_error(t, unparse_err, "json.unparse(json_schema)") do return + defer delete(unparsed_json_schema) + + parsed_json_schema, parse_err := json.parse(unparsed_json_schema, parse_integers=true) + if is_error(t, parse_err, "json.parse(unparsed_json_schema)") do return + defer json.destroy_value(parsed_json_schema) + + buf1, marshal_err1 := json.marshal(json_schema, json.Marshal_Options{sort_maps_by_key=true}) + if is_error(t, marshal_err1, "json.marshal(json_schema)") do return + defer delete(buf1) + + buf2, marshal_err2 := json.marshal(parsed_json_schema, json.Marshal_Options{sort_maps_by_key=true}) + if is_error(t, marshal_err2, "json.marshal(parsed_json_schema)") do return + defer delete(buf2) + + marshaled_parsed_json_schema := string(buf2) + testing.expect_value(t, marshaled_parsed_json_schema, string(buf1)) + testing.expect_value(t, string(unparsed_json_schema), string(buf1)) +} From e8e05a57aa9605aac84c3fce9f9012d37ba141e8 Mon Sep 17 00:00:00 2001 From: Vincent van Ecchi Date: Wed, 25 Dec 2024 22:07:22 +0100 Subject: [PATCH 2/3] Fix null/nil handling in json.marshal and json.unparse --- core/encoding/json/marshal.odin | 4 ++++ core/encoding/json/unparse.odin | 8 ++++++-- tests/core/encoding/json/test_core_json.odin | 10 +++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/core/encoding/json/marshal.odin b/core/encoding/json/marshal.odin index f0f0927a124..37c2859740d 100644 --- a/core/encoding/json/marshal.odin +++ b/core/encoding/json/marshal.odin @@ -176,6 +176,10 @@ marshal_to_writer :: proc(w: io.Writer, v: any, opt: ^Marshal_Options) -> (err: return .Unsupported_Type case runtime.Type_Info_Pointer: + if a.(rawptr) == nil { + io.write_string(w, "null") or_return + return + } return .Unsupported_Type case runtime.Type_Info_Multi_Pointer: diff --git a/core/encoding/json/unparse.odin b/core/encoding/json/unparse.odin index 06c61ed35a8..a8cd43af63f 100644 --- a/core/encoding/json/unparse.odin +++ b/core/encoding/json/unparse.odin @@ -29,8 +29,12 @@ unparse_to_builder :: proc(b: ^strings.Builder, v: Value, opt: ^Marshal_Options) } unparse_to_writer :: proc(w: io.Writer, v: Value, opt: ^Marshal_Options) -> io.Error { + if v == nil { + return unparse_null_to_writer(w, opt) + } + switch uv in v { - case Null: return unparse_null_to_writer(w, uv, opt) + case Null: return unparse_null_to_writer(w, opt) case Integer: return unparse_integer_to_writer(w, uv, opt) case Float: return unparse_float_to_writer(w, uv, opt) case Boolean: return unparse_boolean_to_writer(w, uv, opt) @@ -41,7 +45,7 @@ unparse_to_writer :: proc(w: io.Writer, v: Value, opt: ^Marshal_Options) -> io.E return nil } -unparse_null_to_writer :: proc(w: io.Writer, v: Null, opt: ^Marshal_Options) -> io.Error { +unparse_null_to_writer :: proc(w: io.Writer, opt: ^Marshal_Options) -> io.Error { io.write_string(w, "null") or_return return nil } diff --git a/tests/core/encoding/json/test_core_json.odin b/tests/core/encoding/json/test_core_json.odin index 4cc998e3797..b3ea917a4bf 100644 --- a/tests/core/encoding/json/test_core_json.odin +++ b/tests/core/encoding/json/test_core_json.odin @@ -498,12 +498,20 @@ unparse_json_schema :: proc(t: ^testing.T) { "tags" = json.Object{ "type" = "array", "items" = json.Object{"type" = "string"} + }, + "also" = json.Object{ + "integer" = 42, + "float" = 3.1415, + "bool" = false, + "null" = nil, + "array" = json.Array{42, 3.1415, false, nil, "string"} } } } // having fun cleaning up json literals defer { + delete(json_schema.(json.Object)["properties"].(json.Object)["also"].(json.Object)["array"].(json.Array)) delete(json_schema.(json.Object)["properties"].(json.Object)["tags"].(json.Object)["items"].(json.Object)) for k, &v in json_schema.(json.Object)["properties"].(json.Object) { delete(v.(json.Object)) @@ -513,7 +521,7 @@ unparse_json_schema :: proc(t: ^testing.T) { } is_error :: proc(t: ^testing.T, E: $Error_Type, fn: string) -> bool { - testing.expectf(t, E == nil, "%s failed with error:", fn, E) + testing.expectf(t, E == nil, "%s failed with error: %v", fn, E) return E != nil } From 95fb5922eade6938738d39b1cac5600d231173f9 Mon Sep 17 00:00:00 2001 From: Vincent van Ecchi Date: Fri, 10 Jan 2025 14:37:29 +0100 Subject: [PATCH 3/3] fix code --strict-style issues --- core/encoding/json/unparse.odin | 30 ++++++++++++-------- tests/core/encoding/json/test_core_json.odin | 8 +++--- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/core/encoding/json/unparse.odin b/core/encoding/json/unparse.odin index a8cd43af63f..3e6bc4cf6d9 100644 --- a/core/encoding/json/unparse.odin +++ b/core/encoding/json/unparse.odin @@ -34,13 +34,20 @@ unparse_to_writer :: proc(w: io.Writer, v: Value, opt: ^Marshal_Options) -> io.E } switch uv in v { - case Null: return unparse_null_to_writer(w, opt) - case Integer: return unparse_integer_to_writer(w, uv, opt) - case Float: return unparse_float_to_writer(w, uv, opt) - case Boolean: return unparse_boolean_to_writer(w, uv, opt) - case String: return unparse_string_to_writer(w, uv, opt) - case Array: return unparse_array_to_writer(w, uv, opt) - case Object: return unparse_object_to_writer(w, uv, opt) + case Null: + return unparse_null_to_writer(w, opt) + case Integer: + return unparse_integer_to_writer(w, uv, opt) + case Float: + return unparse_float_to_writer(w, uv, opt) + case Boolean: + return unparse_boolean_to_writer(w, uv, opt) + case String: + return unparse_string_to_writer(w, uv, opt) + case Array: + return unparse_array_to_writer(w, uv, opt) + case Object: + return unparse_object_to_writer(w, uv, opt) } return nil } @@ -73,9 +80,9 @@ unparse_string_to_writer :: proc(w: io.Writer, v: String, opt: ^Marshal_Options) unparse_array_to_writer :: proc(w: io.Writer, v: Array, opt: ^Marshal_Options) -> io.Error { opt_write_start(w, opt, '[') or_return - for i in 0..