Browse Source

Merge pull request #1938 from Skytrias/pretty-json-2

add pretty json output in marshal based on spec + options
gingerBill 3 years ago
parent
commit
d73a4aa34b
1 changed files with 211 additions and 40 deletions
  1. 211 40
      core/encoding/json/marshal.odin

+ 211 - 40
core/encoding/json/marshal.odin

@@ -17,25 +17,54 @@ Marshal_Error :: union #shared_nil {
 	io.Error,
 }
 
-marshal :: proc(v: any, allocator := context.allocator) -> (data: []byte, err: Marshal_Error) {
+// careful with MJSON maps & non quotes usage as keys without whitespace will lead to bad results
+Marshal_Options :: struct {
+	// output based on spec
+	spec: Specification,
+
+	// use line breaks & tab|spaces
+	pretty: bool, 
+
+	// spacing
+	use_spaces: bool,
+	spaces: int,
+
+	// state
+	indentation: int,
+
+	// option to output uint in JSON5 & MJSON
+	write_uint_as_hex: bool, 
+
+	// mjson output options
+	mjson_keys_use_quotes: bool,
+	mjson_keys_use_equal_sign: bool,
+
+	// mjson state
+	mjson_skipped_first_braces_start: bool,
+	mjson_skipped_first_braces_end: bool,
+}
+
+marshal :: proc(v: any, opt: Marshal_Options = {}, allocator := context.allocator) -> (data: []byte, err: Marshal_Error) {
 	b := strings.builder_make(allocator)
 	defer if err != nil {
 		strings.builder_destroy(&b)
 	}
 
-	marshal_to_builder(&b, v) or_return
+	opt := opt
+	marshal_to_builder(&b, v, &opt) or_return
 	
 	if len(b.buf) != 0 {
 		data = b.buf[:]
 	}
+
 	return data, nil
 }
 
-marshal_to_builder :: proc(b: ^strings.Builder, v: any) -> Marshal_Error {
-	return marshal_to_writer(strings.to_writer(b), v)
+marshal_to_builder :: proc(b: ^strings.Builder, v: any, opt: ^Marshal_Options) -> Marshal_Error {
+	return marshal_to_writer(strings.to_writer(b), v, opt)
 }
 
-marshal_to_writer :: proc(w: io.Writer, v: any) -> (err: Marshal_Error) {
+marshal_to_writer :: proc(w: io.Writer, v: any, opt: ^Marshal_Options) -> (err: Marshal_Error) {
 	if v == nil {
 		io.write_string(w, "null") or_return
 		return
@@ -82,7 +111,21 @@ marshal_to_writer :: proc(w: io.Writer, v: any) -> (err: Marshal_Error) {
 		case u128be: u = u128(i)
 		}
 
-		s := strconv.append_bits_128(buf[:], u, 10, info.signed, 8*ti.size, "0123456789", nil)
+		s: string
+
+		// allow uints to be printed as hex
+		if opt.write_uint_as_hex && (opt.spec == .JSON5 || opt.spec == .MJSON) {
+			switch i in a {
+			case u8, u16, u32, u64, u128: 
+				s = strconv.append_bits_128(buf[:], u, 16, info.signed, 8*ti.size, "0123456789abcdef", { .Prefix })
+
+			case:
+				s = strconv.append_bits_128(buf[:], u, 10, info.signed, 8*ti.size, "0123456789", nil)
+			}
+		} else {
+			s = strconv.append_bits_128(buf[:], u, 10, info.signed, 8*ti.size, "0123456789", nil)
+		}
+
 		io.write_string(w, s) or_return
 
 
@@ -169,52 +212,48 @@ marshal_to_writer :: proc(w: io.Writer, v: any) -> (err: Marshal_Error) {
 		return .Unsupported_Type
 
 	case runtime.Type_Info_Array:
-		io.write_byte(w, '[') or_return
+		opt_write_start(w, opt, '[') or_return
 		for i in 0..<info.count {
-			if i > 0 { io.write_string(w, ", ") or_return }
-
+			opt_write_iteration(w, opt, i) or_return
 			data := uintptr(v.data) + uintptr(i*info.elem_size)
-			marshal_to_writer(w, any{rawptr(data), info.elem.id}) or_return
+			marshal_to_writer(w, any{rawptr(data), info.elem.id}, opt) or_return
 		}
-		io.write_byte(w, ']') or_return
+		opt_write_end(w, opt, ']') or_return
 		
 	case runtime.Type_Info_Enumerated_Array:
 		index := runtime.type_info_base(info.index).variant.(runtime.Type_Info_Enum)
-		io.write_byte(w, '[') or_return
+		opt_write_start(w, opt, '[') or_return
 		for i in 0..<info.count {
-			if i > 0 { io.write_string(w, ", ") or_return }
-
+			opt_write_iteration(w, opt, i) or_return
 			data := uintptr(v.data) + uintptr(i*info.elem_size)
-			marshal_to_writer(w, any{rawptr(data), info.elem.id}) or_return
+			marshal_to_writer(w, any{rawptr(data), info.elem.id}, opt) or_return
 		}
-		io.write_byte(w, ']') or_return
+		opt_write_end(w, opt, ']') or_return
 		
 	case runtime.Type_Info_Dynamic_Array:
-		io.write_byte(w, '[') or_return
+		opt_write_start(w, opt, '[') or_return
 		array := cast(^mem.Raw_Dynamic_Array)v.data
 		for i in 0..<array.len {
-			if i > 0 { io.write_string(w, ", ") or_return }
-
+			opt_write_iteration(w, opt, i) or_return
 			data := uintptr(array.data) + uintptr(i*info.elem_size)
-			marshal_to_writer(w, any{rawptr(data), info.elem.id}) or_return
+			marshal_to_writer(w, any{rawptr(data), info.elem.id}, opt) or_return
 		}
-		io.write_byte(w, ']') or_return
+		opt_write_end(w, opt, ']') or_return
 
 	case runtime.Type_Info_Slice:
-		io.write_byte(w, '[') or_return
+		opt_write_start(w, opt, '[') or_return
 		slice := cast(^mem.Raw_Slice)v.data
 		for i in 0..<slice.len {
-			if i > 0 { io.write_string(w, ", ") or_return }
-
+			opt_write_iteration(w, opt, i) or_return
 			data := uintptr(slice.data) + uintptr(i*info.elem_size)
-			marshal_to_writer(w, any{rawptr(data), info.elem.id}) or_return
+			marshal_to_writer(w, any{rawptr(data), info.elem.id}, opt) or_return
 		}
-		io.write_byte(w, ']') or_return
+		opt_write_end(w, opt, ']') or_return
 
 	case runtime.Type_Info_Map:
 		m := (^mem.Raw_Map)(v.data)
+		opt_write_start(w, opt, '{') or_return
 
-		io.write_byte(w, '{') or_return
 		if m != nil {
 			if info.generated_struct == nil {
 				return .Unsupported_Type
@@ -226,31 +265,50 @@ marshal_to_writer :: proc(w: io.Writer, v: any) -> (err: Marshal_Error) {
 			entry_size := ed.elem_size
 
 			for i in 0..<entries.len {
-				if i > 0 { io.write_string(w, ", ") or_return }
+				opt_write_iteration(w, opt, i) or_return
 
 				data := uintptr(entries.data) + uintptr(i*entry_size)
 				key   := rawptr(data + entry_type.offsets[2])
 				value := rawptr(data + entry_type.offsets[3])
 
-				marshal_to_writer(w, any{key, info.key.id})     or_return
-				io.write_string(w, ": ")                        or_return
-				marshal_to_writer(w, any{value, info.value.id}) 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}
+					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
 			}
 		}
-		io.write_byte(w, '}') or_return
+
+		opt_write_end(w, opt, '}') or_return
 
 	case runtime.Type_Info_Struct:
-		io.write_byte(w, '{') or_return
+		opt_write_start(w, opt, '{') or_return
+		
 		for name, i in info.names {
-			if i > 0 { io.write_string(w, ", ") or_return }
-			io.write_quoted_string(w, name) or_return
-			io.write_string(w, ": ") or_return
+			opt_write_iteration(w, opt, i) or_return
+			opt_write_key(w, opt, name) or_return
 
 			id := info.types[i].id
 			data := rawptr(uintptr(v.data) + info.offsets[i])
-			marshal_to_writer(w, any{data, id}) or_return
+			marshal_to_writer(w, any{data, id}, opt) or_return
 		}
-		io.write_byte(w, '}') or_return
+
+		opt_write_end(w, opt, '}') or_return
 
 	case runtime.Type_Info_Union:
 		tag_ptr := uintptr(v.data) + info.tag_offset
@@ -273,11 +331,11 @@ marshal_to_writer :: proc(w: io.Writer, v: any) -> (err: Marshal_Error) {
 			io.write_string(w, "null") or_return
 		} else {
 			id := info.variants[tag-1].id
-			return marshal_to_writer(w, any{v.data, id})
+			return marshal_to_writer(w, any{v.data, id}, opt)
 		}
 
 	case runtime.Type_Info_Enum:
-		return marshal_to_writer(w, any{v.data, info.base.id})
+		return marshal_to_writer(w, any{v.data, info.base.id}, opt)
 
 	case runtime.Type_Info_Bit_Set:
 		is_bit_set_different_endian_to_platform :: proc(ti: ^runtime.Type_Info) -> bool {
@@ -333,3 +391,116 @@ marshal_to_writer :: proc(w: io.Writer, v: any) -> (err: Marshal_Error) {
 
 	return
 }
+
+// write key as quoted string or with optional quotes in mjson
+opt_write_key :: proc(w: io.Writer, opt: ^Marshal_Options, name: string) -> (err: io.Error)  {
+	switch opt.spec {
+	case .JSON, .JSON5:
+		io.write_quoted_string(w, name) or_return
+		io.write_string(w, ": ") or_return
+
+	case .MJSON:
+		if opt.mjson_keys_use_quotes {
+			io.write_quoted_string(w, name) or_return
+		} else {
+			io.write_string(w, name) or_return
+		}
+		
+		if opt.mjson_keys_use_equal_sign {
+			io.write_string(w, " = ") or_return
+		} else {
+			io.write_string(w, ": ") or_return
+		}
+	}	
+
+	return
+}
+
+// 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 {
+		opt.mjson_skipped_first_braces_start = true
+		return
+	}
+
+	io.write_byte(w, c) or_return
+	opt.indentation += 1
+
+	if opt.pretty {
+		io.write_byte(w, '\n') or_return
+	}
+
+	return
+}
+
+// insert comma seperation and write indentations
+opt_write_iteration :: proc(w: io.Writer, opt: ^Marshal_Options, iteration: int) -> (err: io.Error) {
+	switch opt.spec {
+	case .JSON, .JSON5: 
+		if iteration > 0 {
+			io.write_string(w, ", ") or_return
+
+			if opt.pretty {
+				io.write_byte(w, '\n') or_return
+			}
+		}
+
+		opt_write_indentation(w, opt) or_return
+
+	case .MJSON: 
+		if iteration > 0 {
+			// on pretty no commas necessary
+			if opt.pretty {
+				io.write_byte(w, '\n') or_return
+			} else {
+				// comma seperation necessary for non pretty output!
+				io.write_string(w, ", ") or_return
+			}
+		}
+
+		opt_write_indentation(w, opt) or_return
+	}
+
+	return
+}
+
+// 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
+		}
+	}
+
+	opt.indentation -= 1
+
+	if opt.pretty {
+		io.write_byte(w, '\n') or_return
+		opt_write_indentation(w, opt) or_return
+	}
+
+	io.write_byte(w, c) or_return
+	return
+}
+
+// writes current indentation level based on options
+opt_write_indentation :: proc(w: io.Writer, opt: ^Marshal_Options) -> (err: io.Error) {
+	if !opt.pretty {
+		return
+	}
+
+	if opt.use_spaces {
+		spaces := opt.spaces == 0 ? 4 : opt.spaces
+		for _ in 0..<opt.indentation * spaces {
+			io.write_byte(w, ' ') or_return
+		}
+	} else {
+		for _ in 0..<opt.indentation {
+			io.write_byte(w, '\t') or_return
+		}
+	}
+
+	return
+}