aboutsummaryrefslogtreecommitdiff
path: root/core/encoding/json
diff options
context:
space:
mode:
authorKarl Zylinski <karl@zylinski.se>2023-11-01 00:23:17 +0100
committerKarl Zylinski <karl@zylinski.se>2023-11-01 00:23:17 +0100
commita58a08c0c3aaf91bec0b02e53bbca64af4ded256 (patch)
tree93b379ba72b16185b6c0998243272f31fd61ff2c /core/encoding/json
parent03ab6add5c3d12bb5c3b36bd3dfb39b81b8c362b (diff)
JSON: Option to sort marshaled maps before outputting. Also added a json.clone_value proc
Diffstat (limited to 'core/encoding/json')
-rw-r--r--core/encoding/json/marshal.odin132
-rw-r--r--core/encoding/json/types.odin27
2 files changed, 121 insertions, 38 deletions
diff --git a/core/encoding/json/marshal.odin b/core/encoding/json/marshal.odin
index 85eca50b6..6922f9b77 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,
- spaces: int,
- // state
- indentation: int,
+ // Given use_spaces true, use this many spaces per indent level. 0 means 4 spaces.
+ spaces: 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,
}
@@ -263,36 +275,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..<map_cap {
- runtime.map_hash_is_valid(hs[bucket_index]) or_continue
-
- opt_write_iteration(w, opt, i) or_return
- i += 1
+ if opt.sort_maps_by_key {
+ Entry :: struct {
+ key: string,
+ value: any,
+ }
- key := rawptr(runtime.map_cell_index_dynamic(ks, info.map_info.ks, bucket_index))
- value := rawptr(runtime.map_cell_index_dynamic(vs, info.map_info.vs, bucket_index))
+ // If we are sorting the map by key, then we temp alloc an array
+ // and sort it, then output the result.
+ sorted := make([dynamic]Entry, 0, map_cap, context.temp_allocator)
+ for bucket_index in 0..<map_cap {
+ runtime.map_hash_is_valid(hs[bucket_index]) or_continue
- // check for string type
- {
- v := any{key, info.key.id}
- ti := runtime.type_info_base(type_info_of(v.id))
- a := any{v.data, ti.id}
+ key := rawptr(runtime.map_cell_index_dynamic(ks, info.map_info.ks, bucket_index))
+ value := rawptr(runtime.map_cell_index_dynamic(vs, info.map_info.vs, bucket_index))
name: string
- #partial switch info in ti.variant {
- case runtime.Type_Info_String:
- switch s in a {
- case string: name = s
- case cstring: name = string(s)
- }
- opt_write_key(w, opt, name) or_return
+ // check for string type
+ {
+ v := any{key, info.key.id}
+ ti := runtime.type_info_base(type_info_of(v.id))
+ a := any{v.data, ti.id}
+
+ #partial switch info in ti.variant {
+ case runtime.Type_Info_String:
+ switch s in a {
+ case string: name = s
+ case cstring: name = string(s)
+ }
- case: return .Unsupported_Type
+ case: return .Unsupported_Type
+ }
}
+
+ append(&sorted, Entry { key = name, value = any{value, info.value.id}})
}
- marshal_to_writer(w, any{value, info.value.id}, opt) or_return
+ slice.sort_by(sorted[:], proc(i, j: Entry) -> 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
+ }
+ } else {
+ i := 0
+ for bucket_index in 0..<map_cap {
+ runtime.map_hash_is_valid(hs[bucket_index]) or_continue
+
+ opt_write_iteration(w, opt, i) or_return
+ i += 1
+
+ key := rawptr(runtime.map_cell_index_dynamic(ks, info.map_info.ks, bucket_index))
+ value := rawptr(runtime.map_cell_index_dynamic(vs, info.map_info.vs, bucket_index))
+
+ // check for string type
+ {
+ v := any{key, info.key.id}
+ ti := runtime.type_info_base(type_info_of(v.id))
+ a := any{v.data, ti.id}
+ name: string
+
+ #partial switch info in ti.variant {
+ case runtime.Type_Info_String:
+ switch s in a {
+ case string: name = s
+ case cstring: name = string(s)
+ }
+ opt_write_key(w, opt, name) or_return
+
+ case: return .Unsupported_Type
+ }
+ }
+
+ marshal_to_writer(w, any{value, info.value.id}, opt) or_return
+ }
}
}
@@ -424,8 +481,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 +531,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..60b3defa1 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,29 @@ 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:
+ len := len(v)
+ new_a := make(Array, len)
+ vv := v
+ for elem, idx in vv {
+ new_a[idx] = clone_value(elem)
+ }
+ return new_a
+ case String:
+ return strings.clone(v)
+ }
+
+ return value
} \ No newline at end of file