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 new file mode 100644 index 00000000000..3e6bc4cf6d9 --- /dev/null +++ b/core/encoding/json/unparse.odin @@ -0,0 +1,126 @@ +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 { + if v == nil { + return unparse_null_to_writer(w, opt) + } + + 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) + } + return nil +} + +unparse_null_to_writer :: proc(w: io.Writer, 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 e, i in v { + opt_write_iteration(w, opt, i == 0) or_return + unparse_to_writer(w, e, opt) or_return + } + opt_write_end(w, opt, ']') or_return + return nil +} + +unparse_object_to_writer :: proc(w: io.Writer, m: Object, opt: ^Marshal_Options) -> 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..4eba47a7bab 100644 --- a/tests/core/encoding/json/test_core_json.odin +++ b/tests/core/encoding/json/test_core_json.odin @@ -482,4 +482,66 @@ 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"}, + }, + "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)) + } + 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: %v", 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)) +}