Browse Source

JSON: Option to sort marshaled maps before outputting. Also added a json.clone_value proc

Karl Zylinski 1 year ago
parent
commit
a58a08c0c3
2 changed files with 121 additions and 38 deletions
  1. 94 38
      core/encoding/json/marshal.odin
  2. 27 0
      core/encoding/json/types.odin

+ 94 - 38
core/encoding/json/marshal.odin

@@ -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,
-	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, 
 	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,
 }
 }
@@ -263,36 +275,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
-			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
 					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
 // 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
-	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
 		opt.mjson_skipped_first_braces_start = true
 		return
 		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
 // 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.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
 	opt.indentation -= 1

+ 27 - 0
core/encoding/json/types.odin

@@ -1,5 +1,7 @@
 package json
 package json
 
 
+import "core:strings"
+
 /*
 /*
 	JSON 
 	JSON 
 		strict JSON
 		strict JSON
@@ -104,4 +106,29 @@ destroy_value :: proc(value: Value, allocator := context.allocator) {
 	case String:
 	case String:
 		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:
+		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
 }
 }