Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add json.unparse: RTTI-less conversion from json.Value to []u8 #4628

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/encoding/json/marshal.odin
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
120 changes: 120 additions & 0 deletions core/encoding/json/unparse.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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 i in 0..<len(v) {
opt_write_iteration(w, opt, i == 0) or_return
unparse_to_writer(w, v[i], 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
}
64 changes: 63 additions & 1 deletion tests/core/encoding/json/test_core_json.odin
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}

@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))
}