245 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			V
		
	
	
			
		
		
	
	
			245 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			V
		
	
	
| // Copyright (c) 2019-2022 Alexander Medvednikov. All rights reserved.
 | |
| // Use of this source code is governed by an MIT license
 | |
| // that can be found in the LICENSE file.
 | |
| module json2
 | |
| 
 | |
| import io
 | |
| import strings
 | |
| 
 | |
| // Encoder encodes the an `Any` type into JSON representation.
 | |
| // It provides parameters in order to change the end result.
 | |
| pub struct Encoder {
 | |
| 	newline              u8
 | |
| 	newline_spaces_count int
 | |
| 	escape_unicode       bool = true
 | |
| }
 | |
| 
 | |
| // byte array versions of the most common tokens/chars
 | |
| // to avoid reallocations
 | |
| const null_in_bytes = 'null'.bytes()
 | |
| 
 | |
| const true_in_bytes = 'true'.bytes()
 | |
| 
 | |
| const false_in_bytes = 'false'.bytes()
 | |
| 
 | |
| const zero_in_bytes = [u8(`0`)]
 | |
| 
 | |
| const comma_bytes = [u8(`,`)]
 | |
| 
 | |
| const colon_bytes = [u8(`:`)]
 | |
| 
 | |
| const space_bytes = [u8(` `)]
 | |
| 
 | |
| const unicode_escape_chars = [u8(`\\`), `u`]
 | |
| 
 | |
| const quote_bytes = [u8(`"`)]
 | |
| 
 | |
| const escaped_chars = [(r'\b').bytes(), (r'\f').bytes(), (r'\n').bytes(),
 | |
| 	(r'\r').bytes(), (r'\t').bytes()]
 | |
| 
 | |
| // encode_value encodes an `Any` value to the specific writer.
 | |
| pub fn (e &Encoder) encode_value(f Any, mut wr io.Writer) ? {
 | |
| 	e.encode_value_with_level(f, 1, mut wr)?
 | |
| }
 | |
| 
 | |
| fn (e &Encoder) encode_newline(level int, mut wr io.Writer) ? {
 | |
| 	if e.newline != 0 {
 | |
| 		wr.write([e.newline])?
 | |
| 		for j := 0; j < level * e.newline_spaces_count; j++ {
 | |
| 			wr.write(json2.space_bytes)?
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| fn (e &Encoder) encode_value_with_level(f Any, level int, mut wr io.Writer) ? {
 | |
| 	match f {
 | |
| 		string {
 | |
| 			e.encode_string(f, mut wr)?
 | |
| 		}
 | |
| 		bool {
 | |
| 			if f == true {
 | |
| 				wr.write(json2.true_in_bytes)?
 | |
| 			} else {
 | |
| 				wr.write(json2.false_in_bytes)?
 | |
| 			}
 | |
| 		}
 | |
| 		int, u64, i64 {
 | |
| 			wr.write(f.str().bytes())?
 | |
| 		}
 | |
| 		f32, f64 {
 | |
| 			$if !nofloat ? {
 | |
| 				str_float := f.str().bytes()
 | |
| 				wr.write(str_float)?
 | |
| 				if str_float[str_float.len - 1] == `.` {
 | |
| 					wr.write(json2.zero_in_bytes)?
 | |
| 				}
 | |
| 				return
 | |
| 			}
 | |
| 			wr.write(json2.zero_in_bytes)?
 | |
| 		}
 | |
| 		map[string]Any {
 | |
| 			wr.write([u8(`{`)])?
 | |
| 			mut i := 0
 | |
| 			for k, v in f {
 | |
| 				e.encode_newline(level, mut wr)?
 | |
| 				e.encode_string(k, mut wr)?
 | |
| 				wr.write(json2.colon_bytes)?
 | |
| 				if e.newline != 0 {
 | |
| 					wr.write(json2.space_bytes)?
 | |
| 				}
 | |
| 				e.encode_value_with_level(v, level + 1, mut wr)?
 | |
| 				if i < f.len - 1 {
 | |
| 					wr.write(json2.comma_bytes)?
 | |
| 				}
 | |
| 				i++
 | |
| 			}
 | |
| 			e.encode_newline(level - 1, mut wr)?
 | |
| 			wr.write([u8(`}`)])?
 | |
| 		}
 | |
| 		[]Any {
 | |
| 			wr.write([u8(`[`)])?
 | |
| 			for i, v in f {
 | |
| 				e.encode_newline(level, mut wr)?
 | |
| 				e.encode_value_with_level(v, level + 1, mut wr)?
 | |
| 				if i < f.len - 1 {
 | |
| 					wr.write(json2.comma_bytes)?
 | |
| 				}
 | |
| 			}
 | |
| 			e.encode_newline(level - 1, mut wr)?
 | |
| 			wr.write([u8(`]`)])?
 | |
| 		}
 | |
| 		Null {
 | |
| 			wr.write(json2.null_in_bytes)?
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // str returns the JSON string representation of the `map[string]Any` type.
 | |
| pub fn (f map[string]Any) str() string {
 | |
| 	return Any(f).json_str()
 | |
| }
 | |
| 
 | |
| // str returns the JSON string representation of the `[]Any` type.
 | |
| pub fn (f []Any) str() string {
 | |
| 	return Any(f).json_str()
 | |
| }
 | |
| 
 | |
| // str returns the string representation of the `Any` type. Use the `json_str` method
 | |
| // if you want to use the escaped str() version of the `Any` type.
 | |
| pub fn (f Any) str() string {
 | |
| 	if f is string {
 | |
| 		return f
 | |
| 	} else {
 | |
| 		return f.json_str()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // json_str returns the JSON string representation of the `Any` type.
 | |
| [manualfree]
 | |
| pub fn (f Any) json_str() string {
 | |
| 	mut sb := strings.new_builder(4096)
 | |
| 	defer {
 | |
| 		unsafe { sb.free() }
 | |
| 	}
 | |
| 	mut enc := Encoder{}
 | |
| 	enc.encode_value(f, mut sb) or { return '' }
 | |
| 	return sb.str()
 | |
| }
 | |
| 
 | |
| // prettify_json_str returns the pretty-formatted JSON string representation of the `Any` type.
 | |
| [manualfree]
 | |
| pub fn (f Any) prettify_json_str() string {
 | |
| 	mut sb := strings.new_builder(4096)
 | |
| 	defer {
 | |
| 		unsafe { sb.free() }
 | |
| 	}
 | |
| 	mut enc := Encoder{
 | |
| 		newline: `\n`
 | |
| 		newline_spaces_count: 4
 | |
| 	}
 | |
| 	enc.encode_value(f, mut sb) or { return '' }
 | |
| 	return sb.str()
 | |
| }
 | |
| 
 | |
| // CharLengthIterator is an iterator that generates a char
 | |
| // length value of every iteration based on the given text.
 | |
| // (e.g.: "t✔" => [t => 1, ✔ => 2])
 | |
| struct CharLengthIterator {
 | |
| 	text string
 | |
| mut:
 | |
| 	idx int
 | |
| }
 | |
| 
 | |
| fn (mut iter CharLengthIterator) next() ?int {
 | |
| 	if iter.idx >= iter.text.len {
 | |
| 		return none
 | |
| 	}
 | |
| 	defer {
 | |
| 		iter.idx++
 | |
| 	}
 | |
| 	mut len := 1
 | |
| 	c := iter.text[iter.idx]
 | |
| 	if (c & (1 << 7)) != 0 {
 | |
| 		for t := u8(1 << 6); (c & t) != 0; t >>= 1 {
 | |
| 			len++
 | |
| 			iter.idx++
 | |
| 		}
 | |
| 	}
 | |
| 	return len
 | |
| }
 | |
| 
 | |
| // encode_string returns the JSON spec-compliant version of the string.
 | |
| [manualfree]
 | |
| fn (e &Encoder) encode_string(s string, mut wr io.Writer) ? {
 | |
| 	mut char_lens := CharLengthIterator{
 | |
| 		text: s
 | |
| 	}
 | |
| 	mut i := 0
 | |
| 	wr.write(json2.quote_bytes)?
 | |
| 	for char_len in char_lens {
 | |
| 		if char_len == 1 {
 | |
| 			chr := s[i]
 | |
| 			if chr in important_escapable_chars {
 | |
| 				for j := 0; j < important_escapable_chars.len; j++ {
 | |
| 					if chr == important_escapable_chars[j] {
 | |
| 						wr.write(json2.escaped_chars[j])?
 | |
| 						break
 | |
| 					}
 | |
| 				}
 | |
| 			} else if chr == `"` || chr == `/` || chr == `\\` {
 | |
| 				wr.write([u8(`\\`), chr])?
 | |
| 			} else if int(chr) < 0x20 {
 | |
| 				hex_code := chr.hex().bytes()
 | |
| 				wr.write(json2.unicode_escape_chars)? // \u
 | |
| 				wr.write(json2.zero_in_bytes)? // \u0
 | |
| 				wr.write(json2.zero_in_bytes)? // \u00
 | |
| 				wr.write(hex_code)? // \u00xxxx
 | |
| 			} else {
 | |
| 				wr.write([u8(chr)])?
 | |
| 			}
 | |
| 		} else {
 | |
| 			slice := s[i..i + char_len]
 | |
| 			hex_code := slice.utf32_code().hex().bytes()
 | |
| 			if !e.escape_unicode || hex_code.len < 4 {
 | |
| 				// unescaped non-ASCII char
 | |
| 				wr.write(slice.bytes())?
 | |
| 			} else if hex_code.len == 4 {
 | |
| 				// a unicode endpoint
 | |
| 				wr.write(json2.unicode_escape_chars)?
 | |
| 				wr.write(hex_code)?
 | |
| 			} else {
 | |
| 				// TODO: still figuring out what
 | |
| 				// to do with more than 4 chars
 | |
| 				wr.write(json2.space_bytes)?
 | |
| 			}
 | |
| 			unsafe {
 | |
| 				slice.free()
 | |
| 				hex_code.free()
 | |
| 			}
 | |
| 		}
 | |
| 		i += char_len
 | |
| 	}
 | |
| 
 | |
| 	wr.write(json2.quote_bytes)?
 | |
| }
 |