diff --git a/core/encoding/json/marshal.odin b/core/encoding/json/marshal.odin index 85eca50b6..ab2af9561 100644 --- a/core/encoding/json/marshal.odin +++ b/core/encoding/json/marshal.odin @@ -7,6 +7,7 @@ import "core:strconv" import "core:strings" import "core:reflect" import "core:io" +import "core:slice" Marshal_Data_Error :: enum { None, @@ -18,29 +19,40 @@ Marshal_Error :: union #shared_nil { io.Error, } -// careful with MJSON maps & non quotes usage as keys without whitespace will lead to bad results +// careful with MJSON maps & non quotes usage as keys with whitespace will lead to bad results Marshal_Options :: struct { // output based on spec spec: Specification, - // use line breaks & tab|spaces + // Use line breaks & tabs/spaces pretty: bool, - // spacing + // Use spaces for indentation instead of tabs use_spaces: bool, + + // Given use_spaces true, use this many spaces per indent level. 0 means 4 spaces. spaces: int, - // state - indentation: int, - - // option to output uint in JSON5 & MJSON + // Output uint as hex in JSON5 & MJSON write_uint_as_hex: bool, - // mjson output options + // If spec is MJSON and this is true, then keys will be quoted. + // + // WARNING: If your keys contain whitespace and this is false, then the + // output will be bad. mjson_keys_use_quotes: bool, + + // If spec is MJSON and this is true, then use '=' as delimiter between + // keys and values, otherwise ':' is used. mjson_keys_use_equal_sign: bool, - // mjson state + // When outputting a map, sort the output by key. + // + // NOTE: This will temp allocate and sort a list for each map. + sort_maps_by_key: bool, + + // Internal state + indentation: int, mjson_skipped_first_braces_start: bool, mjson_skipped_first_braces_end: bool, } @@ -50,6 +62,9 @@ marshal :: proc(v: any, opt: Marshal_Options = {}, allocator := context.allocato 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 marshal_to_builder(&b, v, &opt) or_return @@ -263,36 +278,81 @@ marshal_to_writer :: proc(w: io.Writer, v: any, opt: ^Marshal_Options) -> (err: map_cap := uintptr(runtime.map_cap(m^)) ks, vs, hs, _, _ := runtime.map_kvh_data_dynamic(m^, info.map_info) - i := 0 - for bucket_index in 0.. bool { return i.key < j.key }) + + for s, i in sorted { + opt_write_iteration(w, opt, i) or_return + opt_write_key(w, opt, s.key) or_return + marshal_to_writer(w, s.value, opt) or_return + } } } @@ -424,8 +484,9 @@ opt_write_key :: proc(w: io.Writer, opt: ^Marshal_Options, name: string) -> (err // insert start byte and increase indentation on pretty opt_write_start :: proc(w: io.Writer, opt: ^Marshal_Options, c: byte) -> (err: io.Error) { - // skip mjson starting braces - if opt.spec == .MJSON && !opt.mjson_skipped_first_braces_start { + // Skip MJSON starting braces. We make sure to only do this for c == '{', + // skipping a starting '[' is not allowed. + if opt.spec == .MJSON && !opt.mjson_skipped_first_braces_start && opt.indentation == 0 && c == '{' { opt.mjson_skipped_first_braces_start = true return } @@ -473,11 +534,9 @@ opt_write_iteration :: proc(w: io.Writer, opt: ^Marshal_Options, iteration: int) // decrease indent, write spacing and insert end byte opt_write_end :: proc(w: io.Writer, opt: ^Marshal_Options, c: byte) -> (err: io.Error) { - if opt.spec == .MJSON && opt.mjson_skipped_first_braces_start && !opt.mjson_skipped_first_braces_end { - if opt.indentation == 0 { - opt.mjson_skipped_first_braces_end = true - return - } + if opt.spec == .MJSON && opt.mjson_skipped_first_braces_start && !opt.mjson_skipped_first_braces_end && opt.indentation == 0 && c == '}' { + opt.mjson_skipped_first_braces_end = true + return } opt.indentation -= 1 diff --git a/core/encoding/json/types.odin b/core/encoding/json/types.odin index 089fd9c9b..20c806236 100644 --- a/core/encoding/json/types.odin +++ b/core/encoding/json/types.odin @@ -1,5 +1,7 @@ package json +import "core:strings" + /* JSON strict JSON @@ -104,4 +106,27 @@ destroy_value :: proc(value: Value, allocator := context.allocator) { case String: delete(v) } +} + +clone_value :: proc(value: Value, allocator := context.allocator) -> Value { + context.allocator = allocator + + #partial switch &v in value { + case Object: + new_o := make(Object, len(v)) + for key, elem in v { + new_o[strings.clone(key)] = clone_value(elem) + } + return new_o + case Array: + new_a := make(Array, len(v)) + for elem, idx in v { + new_a[idx] = clone_value(elem) + } + return new_a + case String: + return strings.clone(v) + } + + return value } \ No newline at end of file