Merge pull request #2912 from karl-zylinski/marshal-sort-maps-by-key-and-clone-value

JSON: Option to sort marshaled maps before outputting + clone_value proc + small improvements
This commit is contained in:
gingerBill
2024-01-22 14:05:15 +00:00
committed by GitHub
2 changed files with 122 additions and 38 deletions
+97 -38
View File
@@ -7,6 +7,7 @@ import "core:strconv"
import "core:strings" import "core:strings"
import "core:reflect" import "core:reflect"
import "core:io" import "core:io"
import "core:slice"
Marshal_Data_Error :: enum { Marshal_Data_Error :: enum {
None, None,
@@ -18,29 +19,40 @@ Marshal_Error :: union #shared_nil {
io.Error, 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 { Marshal_Options :: struct {
// output based on spec // output based on spec
spec: Specification, spec: Specification,
// use line breaks & tab|spaces // Use line breaks & tabs/spaces
pretty: bool, pretty: bool,
// spacing // Use spaces for indentation instead of tabs
use_spaces: bool, use_spaces: bool,
// Given use_spaces true, use this many spaces per indent level. 0 means 4 spaces.
spaces: int, spaces: int,
// state // Output uint as hex in JSON5 & MJSON
indentation: int,
// option to output uint in JSON5 & MJSON
write_uint_as_hex: bool, 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, 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_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_start: bool,
mjson_skipped_first_braces_end: bool, mjson_skipped_first_braces_end: bool,
} }
@@ -51,6 +63,9 @@ marshal :: proc(v: any, opt: Marshal_Options = {}, allocator := context.allocato
strings.builder_destroy(&b) 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 opt := opt
marshal_to_builder(&b, v, &opt) or_return 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^)) map_cap := uintptr(runtime.map_cap(m^))
ks, vs, hs, _, _ := runtime.map_kvh_data_dynamic(m^, info.map_info) ks, vs, hs, _, _ := runtime.map_kvh_data_dynamic(m^, info.map_info)
i := 0 if !opt.sort_maps_by_key {
for bucket_index in 0..<map_cap { i := 0
runtime.map_hash_is_valid(hs[bucket_index]) or_continue 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 opt_write_iteration(w, opt, i) or_return
i += 1 i += 1
key := rawptr(runtime.map_cell_index_dynamic(ks, info.map_info.ks, bucket_index)) 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)) value := rawptr(runtime.map_cell_index_dynamic(vs, info.map_info.vs, bucket_index))
// check for string type // check for string type
{ {
v := any{key, info.key.id} v := any{key, info.key.id}
ti := runtime.type_info_base(type_info_of(v.id)) ti := runtime.type_info_base(type_info_of(v.id))
a := any{v.data, ti.id} a := any{v.data, ti.id}
name: string name: string
#partial switch info in ti.variant { #partial switch info in ti.variant {
case runtime.Type_Info_String: case runtime.Type_Info_String:
switch s in a { switch s in a {
case string: name = s case string: name = s
case cstring: name = string(s) case cstring: name = string(s)
}
opt_write_key(w, opt, name) or_return
case: return .Unsupported_Type
} }
opt_write_key(w, opt, name) or_return
case: return .Unsupported_Type
} }
marshal_to_writer(w, any{value, info.value.id}, opt) or_return
}
} else {
Entry :: struct {
key: string,
value: any,
} }
marshal_to_writer(w, any{value, info.value.id}, opt) or_return // 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
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
// 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
}
}
append(&sorted, Entry { key = name, value = any{value, info.value.id}})
}
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
}
} }
} }
@@ -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 // insert start byte and increase indentation on pretty
opt_write_start :: proc(w: io.Writer, opt: ^Marshal_Options, c: byte) -> (err: io.Error) { opt_write_start :: proc(w: io.Writer, opt: ^Marshal_Options, c: byte) -> (err: io.Error) {
// skip mjson starting braces // Skip MJSON starting braces. We make sure to only do this for c == '{',
if opt.spec == .MJSON && !opt.mjson_skipped_first_braces_start { // 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 opt.mjson_skipped_first_braces_start = true
return 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 // decrease indent, write spacing and insert end byte
opt_write_end :: proc(w: io.Writer, opt: ^Marshal_Options, c: byte) -> (err: io.Error) { 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.spec == .MJSON && opt.mjson_skipped_first_braces_start && !opt.mjson_skipped_first_braces_end && opt.indentation == 0 && c == '}' {
if opt.indentation == 0 { opt.mjson_skipped_first_braces_end = true
opt.mjson_skipped_first_braces_end = true return
return
}
} }
opt.indentation -= 1 opt.indentation -= 1
+25
View File
@@ -1,5 +1,7 @@
package json package json
import "core:strings"
/* /*
JSON JSON
strict JSON strict JSON
@@ -105,3 +107,26 @@ destroy_value :: proc(value: Value, allocator := context.allocator) {
delete(v) 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
}