├── tests ├── config.nims ├── run_test_jsony.nim ├── test_errors.nim ├── test_char.nim ├── test_fast_numbers.nim ├── test_distinct.nim ├── all.nim ├── fuzz_strings.nim ├── run_extranl_suite.nim ├── test_arrays.nim ├── test_enums.nim ├── test_options.nim ├── test_refs.nim ├── test_json_in_json.nim ├── test_tables.nim ├── test_quirkydump.nim ├── bench_statics.nim ├── fuzz.nim ├── test_tuples.nim ├── test_strings.nim ├── test_numbers.nim ├── test_sets.nim ├── bench.nim ├── test_tojson.nim ├── test_parseHook.nim ├── bench_parts.nim └── test_objects.nim ├── .gitignore ├── jsony.nimble ├── .github └── workflows │ └── build.yml ├── LICENSE ├── src ├── jsony │ └── objvar.nim └── jsony.nim └── README.md /tests/config.nims: -------------------------------------------------------------------------------- 1 | --path:"../src" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore files with no extention: 2 | * 3 | !*/ 4 | !*.* 5 | 6 | # normal ignores: 7 | *.js 8 | *.exe 9 | nimcache 10 | *.pdb 11 | *.ilk 12 | .* 13 | -------------------------------------------------------------------------------- /jsony.nimble: -------------------------------------------------------------------------------- 1 | version = "1.1.3" 2 | author = "Andre von Houck" 3 | description = "A loose direct to object json parser with hooks." 4 | license = "MIT" 5 | 6 | srcDir = "src" 7 | 8 | requires "nim >= 1.4.0" 9 | -------------------------------------------------------------------------------- /tests/run_test_jsony.nim: -------------------------------------------------------------------------------- 1 | # Rebuild with: nim c -d:release 2 | 3 | import json 4 | import os 5 | 6 | let fn = os.paramStr(1) 7 | 8 | try: 9 | discard fn.readFile().parseJson() 10 | quit(2) 11 | except: 12 | quit(1) 13 | -------------------------------------------------------------------------------- /tests/test_errors.nim: -------------------------------------------------------------------------------- 1 | import jsony 2 | 3 | doAssertRaises(JsonError): 4 | discard "{invalid".fromJson() 5 | 6 | doAssertRaises(JsonError): 7 | discard "{a:}".fromJson() 8 | 9 | doAssertRaises(JsonError): 10 | discard "1.23.23".fromJson() 11 | -------------------------------------------------------------------------------- /tests/test_char.nim: -------------------------------------------------------------------------------- 1 | import jsony 2 | 3 | doAssert """ "a" """.fromJson(char) == 'a' 4 | doAssert """["a"]""".fromJson(seq[char]) == @['a'] 5 | doAssert """["a", "b", "c"]""".fromJson(seq[char]) == @['a', 'b', 'c'] 6 | doAssert 'a'.toJson() == """"a"""" 7 | doAssert 'b'.toJson() == """"b"""" 8 | -------------------------------------------------------------------------------- /tests/test_fast_numbers.nim: -------------------------------------------------------------------------------- 1 | import jsony 2 | 3 | doAssertRaises JsonError: 4 | var 5 | s = "" 6 | i = 0 7 | n: uint64 8 | parseHook(s, i, n) 9 | 10 | for i in 0 .. 10000: 11 | var s = "" 12 | dumpHook(s, i) 13 | doAssert $i == s 14 | 15 | for i in 0 .. 10000: 16 | var s = $i 17 | var idx = 0 18 | var v: int 19 | parseHook(s, idx, v) 20 | doAssert i == v 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Github Actions 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | os: [ubuntu-latest, windows-latest] 9 | 10 | runs-on: ${{ matrix.os }} 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: jiro4989/setup-nim-action@v1 15 | - run: nimble test -y 16 | - run: nimble test --gc:orc -y 17 | - run: nim js -r tests/all.nim 18 | - run: nim cpp -d:release --gc:arc -r tests/all.nim 19 | -------------------------------------------------------------------------------- /tests/test_distinct.nim: -------------------------------------------------------------------------------- 1 | import jsony 2 | 3 | type 4 | Timestamp* = distinct float64 ## Always seconds since 1970 UTC. 5 | 6 | proc `$`*(a: Timestamp): string = 7 | ## Display a timestamps as a float64. 8 | $float64(a) 9 | 10 | proc `==`*(a, b: Timestamp): bool = 11 | ## Compare timestamps. 12 | float64(a) == float64(b) 13 | 14 | var t = Timestamp(123.123) 15 | 16 | doAssert t.toJson() == "123.123" 17 | doAssert "1234.123".fromJson(Timestamp) == Timestamp(1234.123) 18 | doAssert t.toJson().fromJson(Timestamp) == t 19 | -------------------------------------------------------------------------------- /tests/all.nim: -------------------------------------------------------------------------------- 1 | # This is mainly used to test js 2 | # nim js -r .\tests\all.nim 3 | # nim cpp -r .\tests\all.nim 4 | 5 | import test_arrays 6 | import test_char 7 | import test_enums 8 | import test_errors 9 | import test_fast_numbers 10 | import test_json_in_json 11 | import test_numbers 12 | import test_objects 13 | import test_options 14 | import test_parseHook 15 | import test_sets 16 | import test_strings 17 | import test_tables 18 | import test_tojson 19 | import test_tuples 20 | import test_refs 21 | 22 | echo "all tests pass" 23 | -------------------------------------------------------------------------------- /tests/fuzz_strings.nim: -------------------------------------------------------------------------------- 1 | import random, jsony, unicode 2 | 3 | randomize() 4 | 5 | # ASCII 6 | for i in 0 ..< 100_000: 7 | var s = "" 8 | for i in 0 ..< rand(0 .. 100): 9 | s.add char(rand(0 .. 128)) 10 | 11 | if s.toJson().fromJson(string) != s: 12 | echo "some thing wrong!" 13 | echo repr(s) 14 | 15 | # UNICODE 16 | for i in 0 ..< 100_000: 17 | var s = "" 18 | for i in 0 ..< rand(0 .. 100): 19 | s.add $Rune(rand(0 .. 0x10FFFF)) 20 | 21 | if s.toJson().fromJson(string) != s: 22 | echo "some thing wrong!" 23 | echo repr(s) 24 | -------------------------------------------------------------------------------- /tests/run_extranl_suite.nim: -------------------------------------------------------------------------------- 1 | import osproc, os, strutils, sequtils 2 | 3 | # https://github.com/nst/JSONTestSuite 4 | # A comprehensive test suite for RFC 8259 compliant JSON parsers 5 | 6 | let p = absolutePath("../JSONTestSuite/test_parsing/*") 7 | 8 | for f in toSeq(os.walkFiles(p)): 9 | let 10 | what = f.split("/")[^1][0] 11 | cmd = "tests/run_test_jsony" & " " & f 12 | code = execCmd(cmd) 13 | var status = 14 | if code == 2: 15 | if what == 'y': "good " 16 | elif what == 'n': "fail " 17 | elif what == 'i': "good " 18 | else: "?" 19 | elif code == 1: 20 | if what == 'y': "fail " 21 | elif what == 'n': "good " 22 | elif what == 'i': "good " 23 | else: "?" 24 | else: "crash" 25 | echo "[", status, "] ", cmd 26 | -------------------------------------------------------------------------------- /tests/test_arrays.nim: -------------------------------------------------------------------------------- 1 | import jsony 2 | 3 | block: 4 | var s = "[1, 2, 3]" 5 | var v = s.fromJson(array[3, int]) 6 | doAssert v[0] == 1 7 | doAssert v[1] == 2 8 | doAssert v[2] == 3 9 | 10 | block: 11 | var s = "[1.5, 2.25, 3.0]" 12 | var v = s.fromJson(array[3, float32]) 13 | doAssert v[0] == 1.5 14 | doAssert v[1] == 2.25 15 | doAssert v[2] == 3.0 16 | 17 | block: 18 | var s = """["no", "yes"]""" 19 | var v = s.fromJson(array[2, string]) 20 | doAssert v[0] == "no" 21 | doAssert v[1] == "yes" 22 | 23 | block: 24 | var s = """["no", "yes"]""" 25 | var v = s.fromJson(ref array[2, string]) 26 | doAssert v[0] == "no" 27 | doAssert v[1] == "yes" 28 | 29 | block: 30 | var s = "null" 31 | var v = s.fromJson(ref array[2, string]) 32 | doAssert v == nil 33 | -------------------------------------------------------------------------------- /tests/test_enums.nim: -------------------------------------------------------------------------------- 1 | import jsony 2 | 3 | type Color = enum 4 | cRed 5 | cBlue 6 | cGreen 7 | 8 | doAssert "0".fromJson(Color) == cRed 9 | doAssert "1".fromJson(Color) == cBlue 10 | doAssert "2".fromJson(Color) == cGreen 11 | 12 | doAssert """ "cRed" """.fromJson(Color) == cRed 13 | doAssert """ "cBlue" """.fromJson(Color) == cBlue 14 | doAssert """ "cGreen" """.fromJson(Color) == cGreen 15 | 16 | type Color2 = enum 17 | c2Red 18 | c2Blue 19 | c2Green 20 | 21 | proc enumHook(s: string, v: var Color2) = 22 | v = case s: 23 | of "RED": c2Red 24 | of "BLUE": c2Blue 25 | of "GREEN": c2Green 26 | else: c2Red 27 | 28 | doAssert """ "RED" """.fromJson(Color2) == c2Red 29 | doAssert """ "BLUE" """.fromJson(Color2) == c2Blue 30 | doAssert """ "GREEN" """.fromJson(Color2) == c2Green 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT 2 | 3 | Copyright 2020 Andre von Houck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /tests/test_options.nim: -------------------------------------------------------------------------------- 1 | import options, jsony 2 | 3 | var 4 | a: Option[int] = some(123) 5 | b: Option[int] 6 | 7 | doAssert a.toJson() == """123""" 8 | doAssert b.toJson() == """null""" 9 | 10 | doAssert $("""1""".fromJson(Option[int])) == "some(1)" 11 | doAssert $("""null""".fromJson(Option[int])) == "none(int)" 12 | 13 | proc check[T](v: T) = 14 | var v2 = some(v) 15 | var v3 = none(type(v)) 16 | doAssert v2.toJson.fromJson(Option[T]) == v2 17 | doAssert v3.toJson.fromJson(Option[T]) == v3 18 | 19 | check(1.int) 20 | check(1.int8) 21 | check(1.int16) 22 | check(1.int32) 23 | check(1.int64) 24 | check(1.uint8) 25 | check(1.uint16) 26 | check(1.uint32) 27 | check(1.uint64) 28 | 29 | check("hello") 30 | check([1,2,3]) 31 | check(@[1,2,3]) 32 | 33 | type Entry = object 34 | color: string 35 | 36 | check(Entry()) 37 | 38 | type 39 | Test = object 40 | key: Option[int] 41 | var test = """{ "key": null }""".fromJson(Test) 42 | doAssert test.key.isNone == true 43 | var test2 = """{ "key": 2 }""".fromJson(Test) 44 | doAssert test2.key.isNone == false 45 | doAssert test2.key.get == 2 46 | -------------------------------------------------------------------------------- /tests/test_refs.nim: -------------------------------------------------------------------------------- 1 | import jsony 2 | 3 | proc newRef[T](v: T): ref T = 4 | new(result) 5 | result[] = v 6 | 7 | var 8 | a: ref int = newRef(123) 9 | b: ref int 10 | 11 | doAssert a.toJson() == """123""" 12 | doAssert b.toJson() == """null""" 13 | 14 | doAssert $(fromJson("""1""", ref int)[]) == "1" 15 | doAssert fromJson("""null""", ref int) == nil 16 | 17 | proc check[T](v: T) = 18 | var v2: ref T = newRef(v) 19 | var v3: ref T = nil 20 | doAssert v2.toJson.fromJson(ref T)[] == v2[] 21 | doAssert v3.toJson.fromJson(ref T) == nil 22 | 23 | check(1.int) 24 | check(1.int8) 25 | check(1.int16) 26 | check(1.int32) 27 | check(1.int64) 28 | check(1.uint8) 29 | check(1.uint16) 30 | check(1.uint32) 31 | check(1.uint64) 32 | 33 | check("hello") 34 | check([1,2,3]) 35 | check(@[1,2,3]) 36 | 37 | type Entry = object 38 | color: string 39 | 40 | check(Entry()) 41 | 42 | type 43 | Test = object 44 | key: ref int 45 | var test = """{ "key": null }""".fromJson(Test) 46 | doAssert test.key == nil 47 | var test2 = """{ "key": 2 }""".fromJson(Test) 48 | doAssert test2.key != nil 49 | doAssert test2.key[] == 2 50 | -------------------------------------------------------------------------------- /tests/test_json_in_json.nim: -------------------------------------------------------------------------------- 1 | import jsony, json 2 | 3 | block: 4 | type Entry = object 5 | name: string 6 | data: JsonNode 7 | 8 | var entry = Entry() 9 | entry.name = "json-in-json" 10 | entry.data = %*{ 11 | "random-data": "here", 12 | "number": 123, 13 | "number2": 123.456, 14 | "array": @[1, 2, 3], 15 | "active": true, 16 | "null": nil 17 | } 18 | 19 | doAssert entry.toJson() == """{"name":"json-in-json","data":{"random-data":"here","number":123,"number2":123.456,"array":[1,2,3],"active":true,"null":null}}""" 20 | doAssert $entry.toJson.fromJson(Entry) == """(name: "json-in-json", data: {"random-data":"here","number":123,"number2":123.456,"array":[1,2,3],"active":true,"null":null})""" 21 | 22 | let s = """{"name":"json-in-json","data":{"random-data":"here","number":123,"number2":123.456,"array":[1,2,3],"active":true,"null":null}}""" 23 | doAssert $s.fromJson() == """{"name":"json-in-json","data":{"random-data":"here","number":123,"number2":123.456,"array":[1,2,3],"active":true,"null":null}}""" 24 | 25 | let ns = """[123, +123, -123, 123.456, +123.456, -123.456, 123.456E9, +123.456E9, -123.456E9]""" 26 | doAssert $ns.fromJson() == """[123,123,-123,123.456,123.456,-123.456,123456000000.0,123456000000.0,-123456000000.0]""" 27 | 28 | var foo = Entry() 29 | doAssert toJson(foo) == """{"name":"","data":null}""" 30 | 31 | block: 32 | # https://github.com/treeform/jsony/issues/30 33 | let s = r"""[9e-8]""" 34 | doAssert fromJson(s)[0].getFloat() == 9e-8 35 | -------------------------------------------------------------------------------- /tests/test_tables.nim: -------------------------------------------------------------------------------- 1 | import jsony, tables 2 | 3 | block: 4 | 5 | var s = "{}" 6 | var v = s.fromJson(Table[string, int]) 7 | doAssert v.len == 0 8 | 9 | block: 10 | var s = """{"a":2}""" 11 | var v = s.fromJson(Table[string, int]) 12 | doAssert v.len == 1 13 | doAssert v["a"] == 2 14 | 15 | block: 16 | var s = """{"a":2, "b":3, "c" : 4}""" 17 | var v = s.fromJson(Table[string, uint8]) 18 | doAssert v.len == 3 19 | doAssert v["a"] == 2 20 | doAssert v["b"] == 3 21 | doAssert v["c"] == 4 22 | 23 | block: 24 | type Entry = object 25 | color: string 26 | var s = """{ 27 | "a": {"color":"red"}, 28 | "b": {"color":"green"}, 29 | "c": {"color":"blue"} 30 | }""" 31 | var v = s.fromJson(Table[string, Entry]) 32 | doAssert v.len == 3 33 | doAssert v["a"].color == "red" 34 | doAssert v["b"].color == "green" 35 | doAssert v["c"].color == "blue" 36 | 37 | block: 38 | type Entry = object 39 | color: string 40 | var s = """{ 41 | "a": {"color":"red"}, 42 | "b": {"color":"green"}, 43 | "c": {"color":"blue"} 44 | }""" 45 | var v = s.fromJson(OrderedTableRef[string, Entry]) 46 | doAssert v.len == 3 47 | doAssert v["a"].color == "red" 48 | doAssert v["b"].color == "green" 49 | doAssert v["c"].color == "blue" 50 | 51 | block: 52 | doAssert {"j":10,"s":20,"o":100,"n":5000}.toJson() == 53 | """{"j":10,"s":20,"o":100,"n":5000}""" 54 | 55 | doAssert {"j":"a","s":"b","o":"c","n":"d"}.toJson() == 56 | """{"j":"a","s":"b","o":"c","n":"d"}""" 57 | 58 | doAssert [{"j":"a"}].toJson() == 59 | """[{"j":"a"}]""" 60 | -------------------------------------------------------------------------------- /src/jsony/objvar.nim: -------------------------------------------------------------------------------- 1 | import macros 2 | 3 | proc hasKind(node: NimNode, kind: NimNodeKind): bool = 4 | for c in node.children: 5 | if c.kind == kind: 6 | return true 7 | return false 8 | 9 | proc `[]`(node: NimNode, kind: NimNodeKind): NimNode = 10 | for c in node.children: 11 | if c.kind == kind: 12 | return c 13 | return nil 14 | 15 | template fieldPairs*[T: ref object](x: T): untyped = 16 | x[].fieldPairs 17 | 18 | macro isObjectVariant*(v: typed): bool = 19 | ## Is this an object variant? 20 | var typ = v.getTypeImpl() 21 | if typ.kind == nnkSym: 22 | return ident("false") 23 | while typ.kind != nnkObjectTy: 24 | typ = typ[0].getTypeImpl() 25 | if typ[2].hasKind(nnkRecCase): 26 | ident("true") 27 | else: 28 | ident("false") 29 | 30 | proc discriminator*(v: NimNode): NimNode = 31 | var typ = v.getTypeImpl() 32 | while typ.kind != nnkObjectTy: 33 | typ = typ[0].getTypeImpl() 34 | return typ[nnkRecList][nnkRecCase][nnkIdentDefs][nnkSym] 35 | 36 | macro discriminatorFieldName*(v: typed): untyped = 37 | ## Turns into the discriminator field. 38 | return newLit($discriminator(v)) 39 | 40 | macro discriminatorField*(v: typed): untyped = 41 | ## Turns into the discriminator field. 42 | let 43 | fieldName = discriminator(v) 44 | return quote do: 45 | `v`.`fieldName` 46 | 47 | macro new*(v: typed, d: typed): untyped = 48 | ## Creates a new object variant with the discriminator field. 49 | let 50 | typ = v.getTypeInst() 51 | fieldName = discriminator(v) 52 | return quote do: 53 | `v` = `typ`(`fieldName`: `d`) 54 | -------------------------------------------------------------------------------- /tests/test_quirkydump.nim: -------------------------------------------------------------------------------- 1 | import jsony, options 2 | 3 | type 4 | Bar = ref object 5 | nameOfThing: string 6 | arr: seq[int] 7 | 8 | Foo = ref object 9 | idRef: string 10 | bar1Object: Option[Bar] 11 | bar2Object: Option[Bar] 12 | 13 | proc camel2snake*(s: string): string = 14 | ## CanBeFun => can_be_fun 15 | ## https://forum.nim-lang.org/t/1701 16 | result = newStringOfCap(s.len) 17 | for i in 0.. 0: 20 | result.add('_') 21 | result.add(chr(ord(s[i]) + (ord('a') - ord('A')))) 22 | else: 23 | result.add(s[i]) 24 | 25 | template dumpKey(s: var string, v: string) = 26 | const v2 = v.camel2snake().toJson() & ":" 27 | s.add v2 28 | 29 | proc dumpHook*(s: var string, v: object) = 30 | s.add '{' 31 | var i = 0 32 | when compiles(for k, e in v.pairs: discard): 33 | # Tables and table like objects. 34 | for k, e in v.pairs: 35 | if i > 0: 36 | s.add ',' 37 | s.dumpHook(k) 38 | s.add ':' 39 | s.dumpHook(e) 40 | inc i 41 | else: 42 | # Normal objects. 43 | for k, e in v.fieldPairs: 44 | when compiles(e.isSome): 45 | if e.isSome: 46 | if i > 0: 47 | s.add ',' 48 | s.dumpKey(k) 49 | s.dumpHook(e) 50 | inc i 51 | else: 52 | if i > 0: 53 | s.add ',' 54 | s.dumpKey(k) 55 | s.dumpHook(e) 56 | inc i 57 | s.add '}' 58 | 59 | var foo = Foo( 60 | idRef: "0000-1234", 61 | bar1Object: some(Bar(nameOfThing:"Bar", arr: @[1, 2, 3])) 62 | ) 63 | 64 | echo foo.toJson() 65 | -------------------------------------------------------------------------------- /tests/bench_statics.nim: -------------------------------------------------------------------------------- 1 | import jsony, jason, benchy 2 | 3 | const 4 | number11 = 11 5 | stringHello = "Hello" 6 | seqOfInts = @[1, 2, 3, 4, 5, 6, 7] 7 | 8 | timeIt "treeform/jsony number", 100: 9 | for i in 0 .. 1000: 10 | discard number11.toStaticJson() 11 | 12 | timeIt "disruptek/jason number", 100: 13 | for i in 0 .. 1000: 14 | discard number11.jason.string 15 | 16 | timeIt "treeform/jsony string", 100: 17 | discard stringHello.toStaticJson() 18 | 19 | timeIt "disruptek/jason string", 100: 20 | for i in 0 .. 1000: 21 | discard stringHello.jason.string 22 | 23 | timeIt "treeform/jsony seq", 100: 24 | for i in 0 .. 1000: 25 | discard seqOfInts.toStaticJson() 26 | 27 | timeIt "disruptek/jason seq", 100: 28 | for i in 0 .. 1000: 29 | discard seqOfInts.jason.string 30 | 31 | type 32 | Some = object 33 | goats: array[4, string] 34 | sheep: int 35 | ducks: float 36 | dogs: string 37 | cats: bool 38 | fish: seq[uint64] 39 | llama: (int, bool, string, float) 40 | frogs: tuple[toads: bool, rats: string] 41 | geese: (int, int, int, int, int) 42 | 43 | const 44 | thing = Some( 45 | goats: ["black", "pigs", "pink", "horses"], 46 | sheep: 11, ducks: 12.0, 47 | fish: @[8'u64, 6, 7, 5, 3, 0, 9], 48 | dogs: "woof", cats: false, 49 | llama: (1, true, "secret", 42.0), 50 | geese: (9, 0, 2, 1, 0), 51 | frogs: (toads: true, rats: "yep") 52 | ) 53 | 54 | timeIt "treeform/jsony object", 100: 55 | for i in 0 .. 1000: 56 | discard thing.toStaticJson() 57 | 58 | timeIt "disruptek/jason object", 100: 59 | for i in 0 .. 1000: 60 | discard thing.jason.string -------------------------------------------------------------------------------- /tests/fuzz.nim: -------------------------------------------------------------------------------- 1 | import os, strutils, random, strformat, tables, jsony 2 | 3 | 4 | type 5 | NodeKind = enum 6 | nkRed, nkBlue, nkGree, nkBlack 7 | Node = ref object 8 | active: bool 9 | kind: NodeKind 10 | name: string 11 | tup: (int8, uint16, int32, uint64) 12 | id: int 13 | kids: seq[Node] 14 | table: Table[string, uint8] 15 | 16 | var r = initRand(2020) 17 | var genId: int 18 | proc genTree(depth: int): Node = 19 | result = Node() 20 | result.id = genId 21 | result.tup[0] = r.rand(0 .. int8.high.int).int8 22 | result.tup[1] = r.rand(0 .. uint16.high.int).uint16 23 | result.tup[2] = r.rand(0 .. int32.high.int).int32 24 | result.tup[3] = r.rand(0 .. int.high).uint64 25 | inc genId 26 | if r.rand(0 .. 1) == 0: 27 | result.active = true 28 | result.name = "node" & $result.id 29 | result.kind = [nkRed, nkBlue, nkGree, nkBlack][r.rand(0 .. 3)] 30 | result.table["cat"] = 4 31 | result.table["dog"] = 4 32 | if depth > 0: 33 | for i in 0 .. r.rand(0..3): 34 | result.kids.add genTree(depth - 1) 35 | for i in 0 .. r.rand(0..3): 36 | result.kids.add nil 37 | 38 | let 39 | treeStr = genTree(5).toJson() 40 | 41 | randomize() 42 | 43 | for i in 0 ..< 10000: 44 | var 45 | data = treeStr 46 | pos = rand(data.len) 47 | value = rand(255).char 48 | #pos = 18716 49 | #value = 125.char 50 | 51 | data[pos] = value 52 | echo &"{i} {pos} {value.uint8}" 53 | try: 54 | let node = data.fromJson(Node) 55 | doAssert node != nil 56 | except JsonError: 57 | discard 58 | 59 | var data2 = data[0 ..< pos] 60 | try: 61 | let node = data2.fromJson(Node) 62 | doAssert node != nil 63 | except JsonError: 64 | discard 65 | 66 | # JsonNode 67 | try: 68 | let node = data.fromJson() 69 | doAssert node != nil 70 | except JsonError: 71 | discard 72 | 73 | try: 74 | let node = data2.fromJson() 75 | doAssert node != nil 76 | except JsonError: 77 | discard 78 | -------------------------------------------------------------------------------- /tests/test_tuples.nim: -------------------------------------------------------------------------------- 1 | import jsony 2 | 3 | block: 4 | var s = "[1, 2, 3]" 5 | var v = s.fromJson((int, int, int)) 6 | doAssert v[0] == 1 7 | doAssert v[1] == 2 8 | doAssert v[2] == 3 9 | 10 | block: 11 | var s = """[1, "hi", 3.5]""" 12 | var v = s.fromJson((int, string, float32)) 13 | doAssert v[0] == 1 14 | doAssert v[1] == "hi" 15 | doAssert v[2] == 3.5 16 | 17 | block: 18 | type Vector3i = tuple[x: int, y: int, z: int] 19 | var s = """[0, 1, 2]""" 20 | var v = s.fromJson(Vector3i) 21 | doAssert v[0] == 0 22 | doAssert v[1] == 1 23 | doAssert v[2] == 2 24 | doAssert v.x == 0 25 | doAssert v.y == 1 26 | doAssert v.z == 2 27 | 28 | 29 | block: 30 | type Entry = tuple[id: int, name: string, dist: float32] 31 | var s = """[134, "red", 13.5]""" 32 | var v = s.fromJson(Entry) 33 | doAssert v[0] == 134 34 | doAssert v[1] == "red" 35 | doAssert v[2] == 13.5 36 | doAssert v.id == 134 37 | doAssert v.name == "red" 38 | doAssert v.dist == 13.5 39 | 40 | block: 41 | type Entry = tuple[id: int, name: string, dist: float32] 42 | var s = """{"id": 134, "name": "red", "dist": 13.5}""" 43 | var v = s.fromJson(Entry) 44 | doAssert v[0] == 134 45 | doAssert v[1] == "red" 46 | doAssert v[2] == 13.5 47 | doAssert v.id == 134 48 | doAssert v.name == "red" 49 | 50 | block: 51 | type Entry = tuple[id: int, name: string, dist: float32] 52 | var s = """[{"id": 134, "name": "red", "dist": 13.5}]""" 53 | var entries = s.fromJson(seq[Entry]) 54 | doAssert entries.len == 1 55 | var v = entries[0] 56 | doAssert v.dist == 13.5 57 | doAssert v[0] == 134 58 | doAssert v[1] == "red" 59 | doAssert v[2] == 13.5 60 | doAssert v.id == 134 61 | doAssert v.name == "red" 62 | doAssert v.dist == 13.5 63 | 64 | type EntryForHook = tuple[id: int, name: string] 65 | proc postHook(entry: var EntryForHook) = 66 | entry.id = 42 67 | 68 | block: 69 | var s = """{"id": 6, "name": "red"}""" 70 | var v = s.fromJson(EntryForHook) 71 | doAssert v.id == 42 72 | doAssert v.name == "red" 73 | -------------------------------------------------------------------------------- /tests/test_strings.nim: -------------------------------------------------------------------------------- 1 | import json, jsony 2 | 3 | block: 4 | var s = """ "hello" """ 5 | var v = s.fromJson(string) 6 | doAssert v == "hello" 7 | doAssert v.toJson().fromJson(string) == v 8 | doAssert v.toJson().fromJson(string) == v 9 | 10 | block: 11 | var s = """ "new\nline" """ 12 | var v = s.fromJson(string) 13 | doAssert v == "new\nline" 14 | doAssert v.toJson().fromJson(string) == v 15 | doAssert v.toJson().fromJson().toJson().fromJson() == newJString("new\nline") 16 | 17 | block: 18 | var s = """ "quote\"inside" """ 19 | var v = s.fromJson(string) 20 | doAssert v == "quote\"inside" 21 | doAssert v.toJson().fromJson(string) == v 22 | 23 | block: 24 | var s = """ "special: \"\\\/\b\f\n\r\t chars" """ 25 | var v = s.fromJson(string) 26 | doAssert v == "special: \"\\/\b\f\n\r\t chars" 27 | doAssert v.toJson().fromJson(string) == v 28 | 29 | block: 30 | var s = """ "unicode: \u0020 \u0F88 \u1F21" """ 31 | var v = s.fromJson(string) 32 | doAssert v == "unicode: \u0020 \u0F88 \u1F21" 33 | doAssert v.toJson().fromJson(string) == v 34 | 35 | block: 36 | # https://github.com/treeform/jsony/issues/45 37 | # A string with 🔒 emoji encoded both as normal UTF-8 and as a surrogate pair 38 | type 39 | TestObj = object 40 | content: string 41 | let 42 | raw = """{"content":"\uD83D\uDD12🔒"}""" 43 | parsed = raw.fromJson(TestObj) 44 | parsedStd = parseJson(raw).to(TestObj) 45 | echo "jsony - ", parsed.content 46 | echo "std/json - ", parsedStd.content 47 | doAssert parsed.content == parsedStd.content 48 | 49 | let 50 | raw2 = """{"content":"\u00A1\uD835\uDC7D\uD835\uDC96\uD835\uDC86\uD835\uDC8D\uD835\uDC97\uD835\uDC86\uD835\uDC8F \uD835\uDC8F\uD835\uDC96\uD835\uDC86\uD835\uDC94\uD835\uDC95\uD835\uDC93\uD835\uDC90\uD835\uDC94 \uD835\uDC89\uD835\uDC8A\uD835\uDC8F\uD835\uDC84\uD835\uDC89\uD835\uDC82\uD835\uDC94!"}""" 51 | parsed2 = raw2.fromJson(TestObj) 52 | parsedStd2 = parseJson(raw2).to(TestObj) 53 | echo "jsony - ", parsed2.content 54 | echo "std/json - ", parsedStd2.content 55 | doAssert parsed2.content == parsedStd2.content 56 | -------------------------------------------------------------------------------- /tests/test_numbers.nim: -------------------------------------------------------------------------------- 1 | import jsony 2 | 3 | block: 4 | doAssert "true".fromJson(bool) == true 5 | doAssert "false".fromJson(bool) == false 6 | doAssert " true ".fromJson(bool) == true 7 | doAssert " false ".fromJson(bool) == false 8 | 9 | doAssert "1".fromJson(int) == 1 10 | doAssert "12".fromJson(int) == 12 11 | doAssert " 123 ".fromJson(int) == 123 12 | 13 | doAssert " 123 ".fromJson(int8) == 123 14 | doAssert " 123 ".fromJson(uint8) == 123 15 | doAssert " 123 ".fromJson(int16) == 123 16 | doAssert " 123 ".fromJson(uint16) == 123 17 | doAssert " 123 ".fromJson(int32) == 123 18 | doAssert " 123 ".fromJson(uint32) == 123 19 | doAssert " 123 ".fromJson(int64) == 123 20 | doAssert " 123 ".fromJson(uint64) == 123 21 | 22 | doAssert " -99 ".fromJson(int8) == -99 23 | doAssert " -99 ".fromJson(int16) == -99 24 | doAssert " -99 ".fromJson(int32) == -99 25 | doAssert " -99 ".fromJson(int64) == -99 26 | 27 | doAssert " +99 ".fromJson(int8) == 99 28 | doAssert " +99 ".fromJson(int16) == 99 29 | doAssert " +99 ".fromJson(int32) == 99 30 | doAssert " +99 ".fromJson(int64) == 99 31 | 32 | doAssert " 1.25 ".fromJson(float32) == 1.25 33 | doAssert " 1.25 ".fromJson(float32) == 1.25 34 | doAssert " +1.25 ".fromJson(float64) == 1.25 35 | doAssert " +1.25 ".fromJson(float64) == 1.25 36 | doAssert " -1.25 ".fromJson(float64) == -1.25 37 | doAssert " -1.25 ".fromJson(float64) == -1.25 38 | 39 | doAssert " 1.34E3 ".fromJson(float32) == 1.34E3 40 | doAssert " 1.34E3 ".fromJson(float32) == 1.34E3 41 | doAssert " +1.34E3 ".fromJson(float64) == 1.34E3 42 | doAssert " +1.34E3 ".fromJson(float64) == 1.34E3 43 | doAssert " -1.34E3 ".fromJson(float64) == -1.34E3 44 | doAssert " -1.34E3 ".fromJson(float64) == -1.34E3 45 | 46 | doAssert "9e-8".fromJson(float64) == 9e-8 47 | 48 | block: 49 | doAssert "[1, 2, 3]".fromJson(seq[int]) == @[1, 2, 3] 50 | doAssert """["hi", "bye", "maybe"]""".fromJson(seq[string]) == 51 | @["hi", "bye", "maybe"] 52 | doAssert """[["hi", "bye"], ["maybe"], []]""".fromJson(seq[seq[string]]) == 53 | @[@["hi", "bye"], @["maybe"], @[]] 54 | -------------------------------------------------------------------------------- /tests/test_sets.nim: -------------------------------------------------------------------------------- 1 | import jsony, sets 2 | 3 | block: 4 | let 5 | s1 = toHashSet([9, 5, 1]) 6 | s2 = toOrderedSet([3, 5, 7]) 7 | 8 | doAssert s1.toJson() == "[9,1,5]" 9 | doAssert s2.toJson() == "[3,5,7]" 10 | 11 | doAssert s1.toJson.fromJson(type(s1)) == s1 12 | doAssert s2.toJson.fromJson(type(s2)) == s2 13 | 14 | block: 15 | let 16 | s1: set[int8] = {1'i8, 2, 3} 17 | s2: set[int16] = {1'i16, 2, 3} 18 | 19 | doAssert s1.toJson() == "[1,2,3]" 20 | doAssert s2.toJson() == "[1,2,3]" 21 | 22 | doAssert s1.toJson.fromJson(set[int8]) == s1 23 | doAssert s2.toJson.fromJson(set[int16]) == s2 24 | 25 | block: 26 | let 27 | s1: set[uint8] = {1'u8, 2, 3} 28 | s2: set[uint16] = {1'u16, 2, 3} 29 | 30 | doAssert s1.toJson() == "[1,2,3]" 31 | doAssert s2.toJson() == "[1,2,3]" 32 | 33 | doAssert s1.toJson.fromJson(set[uint8]) == s1 34 | doAssert s2.toJson.fromJson(set[uint16]) == s2 35 | 36 | block: 37 | let 38 | s1: set[char] = {'0'..'9'} 39 | 40 | doAssert s1.toJson() == """["0","1","2","3","4","5","6","7","8","9"]""" 41 | 42 | doAssert s1.toJson.fromJson(set[char]) == s1 43 | 44 | block: 45 | type 46 | E1 = enum 47 | e1Elem1, e1Elem2, e1Elem3 48 | E2 = enum 49 | e2Elem1 = "custString1", e2Elem2 = "custString2", e2Elem3 = "custString3" 50 | E3 = enum 51 | e3Elem1 = 10, e3Elem2 = 20, e3Elem3 = 30 52 | 53 | let 54 | s1: set[E1] = {e1Elem1, e1Elem2, e1Elem3} 55 | s2: set[E2] = {e2Elem1, e2Elem2, e2Elem3} 56 | s3: set[E3] = {e3Elem1, e3Elem2, e3Elem3} 57 | 58 | doAssert s1.toJson() == """["e1Elem1","e1Elem2","e1Elem3"]""" 59 | doAssert s2.toJson() == """["custString1","custString2","custString3"]""" 60 | doAssert s3.toJson() == """["e3Elem1","e3Elem2","e3Elem3"]""" 61 | 62 | doAssert s1.toJson.fromJson(set[E1]) == s1 63 | doAssert s2.toJson.fromJson(set[E2]) == s2 64 | doAssert s3.toJson.fromJson(set[E3]) == s3 65 | 66 | block: 67 | type 68 | E1 = enum 69 | e1Elem1, e1Elem2, e1Elem3 70 | S1 = set[E1] 71 | 72 | let 73 | s1: S1 = {e1Elem1, e1Elem2, e1Elem3} 74 | 75 | doAssert s1.toJson() == """["e1Elem1","e1Elem2","e1Elem3"]""" 76 | doAssert s1.toJson.fromJson(set[E1]) == s1 77 | -------------------------------------------------------------------------------- /tests/bench.nim: -------------------------------------------------------------------------------- 1 | import benchy, random, streams, macros 2 | import jsony, jason 3 | import eminim 4 | when defined(packedjson): 5 | import packedjson, packedjson/deserialiser 6 | else: 7 | import json 8 | when not defined(gcArc): 9 | import serialization 10 | import json_serialization except Json, toJson 11 | #from deser_json import parse, to 12 | 13 | type Node = ref object 14 | active: bool 15 | kind: string 16 | name: string 17 | id: int 18 | kids: seq[Node] 19 | 20 | var r = initRand(2020) 21 | var genId: int 22 | proc genTree(depth: int): Node = 23 | result = Node() 24 | result.id = genId 25 | inc genId 26 | if r.rand(0 .. 1) == 0: 27 | result.active = true 28 | result.name = "node" & $result.id 29 | result.kind = "NODE" 30 | if depth > 0: 31 | for i in 0 .. r.rand(0..3): 32 | result.kids.add genTree(depth - 1) 33 | for i in 0 .. r.rand(0..3): 34 | result.kids.add nil 35 | 36 | var tree = genTree(10) 37 | 38 | var treeStr = tree.toJson() 39 | echo treeStr[0 ..< 100] 40 | echo genId, " node tree:" 41 | 42 | timeIt "treeform/jsony", 100: 43 | keep treeStr.fromJson(Node) 44 | 45 | when not defined(gcArc): 46 | timeIt "status-im/nim-json-serialization", 100: 47 | keep json_serialization.Json.decode(treeStr, Node) 48 | 49 | timeIt "planetis-m/eminim", 100: 50 | keep newStringStream(treeStr).jsonTo(Node) 51 | 52 | when defined(packedjson): 53 | timeIt "araq/packedjson", 100: 54 | keep deserialiser.to(packedjson.parseJson(treeStr), Node) 55 | else: 56 | timeIt "nim std/json", 100: 57 | keep json.to(json.parseJson(treeStr), Node) 58 | 59 | # timeIt "gabbhack/deser_json", 100: 60 | # TODO: fix m.tokens[pos].kind == JSMN_OBJECT ... 61 | # keep treeStr.parse().to(Node) 62 | 63 | echo "serialize:" 64 | 65 | timeIt "treeform/jsony", 100: 66 | keep tree.toJson() 67 | 68 | when not defined(gcArc): 69 | timeIt "status-im/nim-json-serialization", 100: 70 | keep json_serialization.Json.encode(tree) 71 | doAssert json_serialization.Json.encode(tree) == treeStr 72 | 73 | timeIt "planetis-m/eminim", 100: 74 | var s = newStringStream() 75 | s.storeJson(tree) 76 | s.setPosition(0) 77 | keep s.data 78 | 79 | timeIt "disruptek/jason", 100: 80 | keep tree.jason.string 81 | 82 | when defined(packedjson): 83 | timeIt "araq/packedjson", 100: 84 | keep packedjson.`$`(packedjson.`%`(tree)) 85 | else: 86 | timeIt "nim std/json", 100: 87 | keep json.`$`(json.`%`(tree)) 88 | -------------------------------------------------------------------------------- /tests/test_tojson.nim: -------------------------------------------------------------------------------- 1 | import jsony, strutils, json, tables 2 | 3 | proc match[T](what: T) = 4 | doAssert what.toJson() == $(%what) 5 | 6 | doAssert 1.uint8.toJson() == "1" 7 | doAssert 1.uint16.toJson() == "1" 8 | doAssert 1.uint32.toJson() == "1" 9 | doAssert 1.int8.toJson() == "1" 10 | doAssert 1.int16.toJson() == "1" 11 | doAssert 1.int32.toJson() == "1" 12 | doAssert 3.14.float64.toJson() == "3.14" 13 | 14 | when not defined(js): 15 | doAssert 1.int64.toJson() == "1" 16 | doAssert 1.uint64.toJson() == "1" 17 | doAssert 3.14.float32.toJson() == "3.140000104904175" 18 | 19 | match 1 20 | match 3.14.float32 21 | match 3.14.float64 22 | 23 | doAssert [1, 2, 3].toJson() == "[1,2,3]" 24 | doAssert @[1, 2, 3].toJson() == "[1,2,3]" 25 | 26 | match [1, 2, 3] 27 | match @[1, 2, 3] 28 | 29 | doAssert true.toJson == "true" 30 | doAssert false.toJson == "false" 31 | 32 | doAssert 'a'.toJson == "\"a\"" 33 | match "hi there" 34 | match "hi\nthere\b\f\n\r\t" 35 | match "как дела" 36 | match """ "quote\"inside" """ 37 | 38 | block: 39 | type 40 | Obj = object 41 | a: int 42 | b: float 43 | c: string 44 | var obj = Obj() 45 | doAssert obj.toJson() == """{"a":0,"b":0.0,"c":""}""" 46 | match obj 47 | 48 | block: 49 | type 50 | Obj = ref object 51 | a: int 52 | b: float 53 | c: string 54 | var obj = Obj() 55 | doAssert obj.toJson() == """{"a":0,"b":0.0,"c":""}""" 56 | match obj 57 | 58 | var obj2: Obj 59 | doAssert obj2.toJson() == "null" 60 | match obj 61 | 62 | var t = (1, 2.2, "hi") 63 | doAssert t.toJson() == """[1,2.2,"hi"]""" 64 | 65 | var tb: Table[string, int] 66 | tb["hi"] = 1 67 | tb["bye"] = 2 68 | doAssert tb.toJson() == """{"hi":1,"bye":2}""" 69 | 70 | type Fraction = object 71 | numerator: int 72 | denominator: int 73 | 74 | proc dumpHook(s: var string, v: Fraction) = 75 | ## Output fraction type as a string "x/y". 76 | s.add '"' 77 | s.add $v.numerator 78 | s.add '/' 79 | s.add $v.denominator 80 | s.add '"' 81 | 82 | var f = Fraction(numerator: 10, denominator: 13) 83 | doAssert f.toJson() == "\"10/13\"" 84 | 85 | block: 86 | type 87 | NodeNumKind = enum # the different node types 88 | nkInt, # a leaf with an integer value 89 | nkFloat, # a leaf with a float value 90 | RefNode = ref object 91 | active: bool 92 | case kind: NodeNumKind # the ``kind`` field is the discriminator 93 | of nkInt: intVal: int 94 | of nkFloat: floatVal: float 95 | ValueNode = object 96 | active: bool 97 | case kind: NodeNumKind # the ``kind`` field is the discriminator 98 | of nkInt: intVal: int 99 | of nkFloat: floatVal: float 100 | 101 | var 102 | refNode1 = RefNode(kind: nkFloat, active: true, floatVal: 3.14) 103 | refNode2 = RefNode(kind: nkInt, active: false, intVal: 42) 104 | 105 | valueNode1 = ValueNode(kind: nkFloat, active: true, floatVal: 3.14) 106 | valueNode2 = ValueNode(kind: nkInt, active: false, intVal: 42) 107 | 108 | doAssert refNode1.toJson() == """{"active":true,"kind":"nkFloat","floatVal":3.14}""" 109 | doAssert refNode2.toJson() == """{"active":false,"kind":"nkInt","intVal":42}""" 110 | doAssert valueNode1.toJson() == """{"active":true,"kind":"nkFloat","floatVal":3.14}""" 111 | doAssert valueNode2.toJson() == """{"active":false,"kind":"nkInt","intVal":42}""" 112 | -------------------------------------------------------------------------------- /tests/test_parseHook.nim: -------------------------------------------------------------------------------- 1 | import jsony, strutils, tables, times, json 2 | 3 | type Fraction = object 4 | numerator: int 5 | denominator: int 6 | 7 | proc parseHook(s: string, i: var int, v: var Fraction) = 8 | ## Instead of looking for fraction object look for a string. 9 | var str: string 10 | parseHook(s, i, str) 11 | let arr = str.split("/") 12 | v = Fraction() 13 | v.numerator = parseInt(arr[0]) 14 | v.denominator = parseInt(arr[1]) 15 | 16 | var frac = """ "1/3" """.fromJson(Fraction) 17 | doAssert frac.numerator == 1 18 | doAssert frac.denominator == 3 19 | 20 | proc parseHook(s: string, i: var int, v: var DateTime) = 21 | var str: string 22 | parseHook(s, i, str) 23 | v = parse(str, "yyyy-MM-dd hh:mm:ss") 24 | 25 | var dt = """ "2020-01-01 00:00:00" """.fromJson(DateTime) 26 | doAssert dt.year == 2020 27 | 28 | type Entry = object 29 | id: string 30 | count: int 31 | filled: int 32 | 33 | let data = """{ 34 | "1": {"count":12, "filled": 11}, 35 | "2": {"count":66, "filled": 0}, 36 | "3": {"count":99, "filled": 99} 37 | }""" 38 | 39 | proc parseHook(s: string, i: var int, v: var seq[Entry]) = 40 | var table: Table[string, Entry] 41 | parseHook(s, i, table) 42 | for k, entry in table.mpairs: 43 | entry.id = k 44 | v.add(entry) 45 | 46 | let s = data.fromJson(seq[Entry]) 47 | doAssert type(s) is seq[Entry] 48 | doAssert $s == """@[(id: "1", count: 12, filled: 11), (id: "2", count: 66, filled: 0), (id: "3", count: 99, filled: 99)]""" 49 | 50 | type Entry2 = object 51 | id: int 52 | pre: int 53 | post: int 54 | kind: string 55 | 56 | let data2 = """{ 57 | "id": 3444, 58 | "changes": [1, 2, "hi"] 59 | }""" 60 | 61 | proc parseHook(s: string, i: var int, v: var Entry2) = 62 | var entry: JsonNode 63 | parseHook(s, i, entry) 64 | v = Entry2() 65 | v.id = entry["id"].getInt() 66 | v.pre = entry["changes"][0].getInt() 67 | v.post = entry["changes"][1].getInt() 68 | v.kind = entry["changes"][2].getStr() 69 | 70 | let s2 = data2.fromJson(Entry2) 71 | doAssert type(s2) is Entry2 72 | doAssert $s2 == """(id: 3444, pre: 1, post: 2, kind: "hi")""" 73 | 74 | # Non unique / double keys in json 75 | # https://forum.nim-lang.org/t/8787 76 | type Header = object 77 | key: string 78 | value: string 79 | proc parseHook(s: string, i: var int, v: var seq[Header]) = 80 | eatChar(s, i, '{') 81 | while i < s.len: 82 | eatSpace(s, i) 83 | if i < s.len and s[i] == '}': 84 | break 85 | var key, value: string 86 | parseHook(s, i, key) 87 | eatChar(s, i, ':') 88 | parseHook(s, i, value) 89 | v.add(Header(key: key, value: value)) 90 | eatSpace(s, i) 91 | if i < s.len and s[i] == ',': 92 | inc i 93 | else: 94 | break 95 | eatChar(s, i, '}') 96 | 97 | let data3 = """{ 98 | "Cache-Control": "private, max-age=0d", 99 | "Content-Encoding": "brd", 100 | "Set-Cookie": "name=valued", 101 | "Set-Cookie": "name=value; name2=value2; name3=value3d" 102 | }""" 103 | 104 | let headers = data3.fromJson(seq[Header]) 105 | doAssert headers[0].key == "Cache-Control" 106 | doAssert headers[0].value == "private, max-age=0d" 107 | doAssert headers[1].key == "Content-Encoding" 108 | doAssert headers[1].value == "brd" 109 | doAssert headers[2].key == "Set-Cookie" 110 | doAssert headers[2].value == "name=valued" 111 | doAssert headers[3].key == "Set-Cookie" 112 | doAssert headers[3].value == "name=value; name2=value2; name3=value3d" 113 | -------------------------------------------------------------------------------- /tests/bench_parts.nim: -------------------------------------------------------------------------------- 1 | import benchy, random, streams 2 | import jsony, jason 3 | import eminim 4 | when defined(packedjson): 5 | import packedjson, packedjson/deserialiser 6 | else: 7 | import json 8 | when not defined(gcArc): 9 | import serialization 10 | import json_serialization except Json, toJson 11 | 12 | 13 | block: 14 | echo "deserialize string:" 15 | var jsonStr = "\"hello there how are you?\"" 16 | timeIt "treeform/jsony", 100: 17 | for i in 0 ..< 1000: 18 | keep jsonStr.fromJson(string) 19 | 20 | when not defined(gcArc): 21 | timeIt "status-im/nim-json-serialization", 100: 22 | for i in 0 ..< 1000: 23 | keep json_serialization.Json.decode(jsonStr, string) 24 | 25 | block: 26 | echo "deserialize obj:" 27 | type Node = ref object 28 | active: bool 29 | kind: string 30 | name: string 31 | id: int 32 | kids: seq[Node] 33 | var node = Node() 34 | var jsonStr = node.toJson() 35 | timeIt "treeform/jsony", 100: 36 | for i in 0 ..< 1000: 37 | keep jsonStr.fromJson(Node) 38 | 39 | when not defined(gcArc): 40 | timeIt "status-im/nim-json-serialization", 100: 41 | for i in 0 ..< 1000: 42 | keep json_serialization.Json.decode(jsonStr, Node) 43 | 44 | block: 45 | echo "deserialize seq[obj]:" 46 | type Node = object 47 | active: bool 48 | kind: string 49 | name: string 50 | id: int 51 | kids: seq[Node] 52 | var seqObj: seq[Node] 53 | for i in 0 ..< 100000: 54 | seqObj.add(Node()) 55 | var jsonStr = seqObj.toJson() 56 | timeIt "treeform/jsony", 100: 57 | keep jsonStr.fromJson(seq[Node]) 58 | 59 | when not defined(gcArc): 60 | timeIt "status-im/nim-json-serialization", 100: 61 | keep json_serialization.Json.decode(jsonStr, seq[Node]) 62 | 63 | block: 64 | echo "serialize int:" 65 | var number42: uint64 = 42 66 | 67 | timeIt "$", 100: 68 | for i in 0 ..< 1000: 69 | keep $number42 70 | 71 | timeIt "treeform/jsony", 100: 72 | for i in 0 ..< 1000: 73 | keep number42.toJson() 74 | 75 | timeIt "disruptek/jason", 100: 76 | for i in 0 ..< 1000: 77 | keep number42.jason.string 78 | 79 | when not defined(gcArc): 80 | timeIt "status-im/nim-json-serialization", 100: 81 | for i in 0 ..< 1000: 82 | keep json_serialization.Json.encode(number42) 83 | 84 | block: 85 | echo "serialize string:" 86 | var hello = "Hello" 87 | 88 | timeIt "'$'", 100: 89 | for i in 0 ..< 1000: 90 | keep '"' & hello & '"' 91 | 92 | timeIt "treeform/jsony", 100: 93 | for i in 0 ..< 1000: 94 | keep hello.toJson() 95 | 96 | timeIt "disruptek/jason", 100: 97 | for i in 0 ..< 1000: 98 | keep hello.jason.string 99 | 100 | when not defined(gcArc): 101 | timeIt "status-im/nim-json-serialization", 100: 102 | for i in 0 ..< 1000: 103 | keep json_serialization.Json.encode(hello) 104 | 105 | block: 106 | echo "serialize seq:" 107 | var numArray = @[1, 2, 3, 4, 5, 6, 7, 8, 9] 108 | 109 | timeIt "treeform/jsony", 100: 110 | for i in 0 ..< 1000: 111 | keep numArray.toJson() 112 | 113 | timeIt "disruptek/jason", 100: 114 | for i in 0 ..< 1000: 115 | keep numArray.jason.string 116 | 117 | when not defined(gcArc): 118 | timeIt "status-im/nim-json-serialization", 100: 119 | for i in 0 ..< 1000: 120 | keep json_serialization.Json.encode(numArray) 121 | 122 | block: 123 | echo "serialize obj:" 124 | type Node = ref object 125 | active: bool 126 | kind: string 127 | name: string 128 | id: int 129 | kids: seq[Node] 130 | var node = Node() 131 | 132 | timeIt "treeform/jsony", 100: 133 | for i in 0 ..< 1000: 134 | keep node.toJson() 135 | 136 | timeIt "disruptek/jason", 100: 137 | for i in 0 ..< 1000: 138 | keep node.jason.string 139 | 140 | when not defined(gcArc): 141 | timeIt "status-im/nim-json-serialization", 100: 142 | for i in 0 ..< 1000: 143 | keep json_serialization.Json.encode(node) 144 | -------------------------------------------------------------------------------- /tests/test_objects.nim: -------------------------------------------------------------------------------- 1 | import jsony, strutils 2 | 3 | block: 4 | type Entry1 = object 5 | color: string 6 | var s = "{}" 7 | var v = s.fromJson(Entry1) 8 | doAssert v.color == "" 9 | 10 | block: 11 | type Foo2 = ref object 12 | field: string 13 | a: string 14 | ratio: float32 15 | var s = """{"field":"is here", "a":"b", "ratio":22.5}""" 16 | var v = s.fromJson(Foo2) 17 | doAssert v.field == "is here" 18 | doAssert v.a == "b" 19 | doAssert v.ratio == 22.5 20 | 21 | type 22 | Bar3 = ref object 23 | name: string 24 | Foo3 = ref object 25 | id: string 26 | bar: Bar3 27 | var s = """{"id":"123", "bar":{"name":"abc"}}""" 28 | var v = s.fromJson(Foo3) 29 | doAssert v.id == "123" 30 | doAssert v.bar.name == "abc" 31 | 32 | # hooks can't be inside blocks 33 | 34 | type 35 | Bar4 = ref object 36 | visible: string 37 | name: string 38 | Foo4 = ref object 39 | visible: string 40 | id: string 41 | bar: Bar4 42 | 43 | proc newHook(foo: var Foo4) = 44 | foo = Foo4() 45 | foo.visible = "yes" 46 | 47 | block: 48 | var s = """{"id":"123", "bar":{"name":"abc", "visible": "yes"}}""" 49 | var v = s.fromJson(Foo4) 50 | doAssert v.id == "123" 51 | doAssert v.visible == "yes" 52 | doAssert v.bar.name == "abc" 53 | doAssert v.bar.visible == "yes" 54 | 55 | # Hooks can't be inside blocks. 56 | type 57 | Foo5 = object 58 | visible: string 59 | id: string 60 | proc newHook(foo: var Foo5) = 61 | foo.visible = "yes" 62 | 63 | block: 64 | var s = """{"id":"123", "visible": "yes"}""" 65 | var v = s.fromJson(Foo5) 66 | doAssert v.id == "123" 67 | doAssert v.visible == "yes" 68 | 69 | block: 70 | var s = """{"id":"123"}""" 71 | var v = s.fromJson(Foo5) 72 | doAssert v.id == "123" 73 | doAssert v.visible == "yes" 74 | 75 | block: 76 | var s = """{"id":"123", "visible": "no"}""" 77 | var v = s.fromJson(Foo5) 78 | doAssert v.id == "123" 79 | doAssert v.visible == "no" 80 | 81 | block: 82 | type Entry2 = object 83 | color: string 84 | var s = """[{}, {"color":"red"}]""" 85 | var v = s.fromJson(seq[Entry2]) 86 | doAssert v.len == 2 87 | doAssert v[0].color == "" 88 | doAssert v[1].color == "red" 89 | 90 | block: 91 | ## Skip extra fields 92 | type Entry3 = object 93 | color: string 94 | var s = """[{"id":123}, {"color":"red", "id":123}, {"ex":[{"color":"red"}]}]""" 95 | var v = s.fromJson(seq[Entry3]) 96 | doAssert v.len == 3 97 | doAssert v[0].color == "" 98 | doAssert v[1].color == "red" 99 | doAssert v[2].color == "" 100 | 101 | block: 102 | ## Skip extra fields 103 | type Entry4 = object 104 | colorBlend: string 105 | 106 | var v = """{"colorBlend":"red"}""".fromJson(Entry4) 107 | doAssert v.colorBlend == "red" 108 | 109 | v = """{"color_blend":"red"}""".fromJson(Entry4) 110 | doAssert v.colorBlend == "red" 111 | 112 | proc snakeCase(s: string): string = 113 | var prevCap = false 114 | for i, c in s: 115 | if c in {'A'..'Z'}: 116 | if result.len > 0 and result[^1] != '_' and not prevCap: 117 | result.add '_' 118 | prevCap = true 119 | result.add c.toLowerAscii() 120 | else: 121 | prevCap = false 122 | result.add c 123 | 124 | doAssert snakeCase("colorRule") == "color_rule" 125 | doAssert snakeCase("ColorRule") == "color_rule" 126 | doAssert snakeCase("Color_Rule") == "color_rule" 127 | doAssert snakeCase("color_Rule") == "color_rule" 128 | doAssert snakeCase("color_rule") == "color_rule" 129 | doAssert snakeCase("httpGet") == "http_get" 130 | doAssert snakeCase("restAPI") == "rest_api" 131 | 132 | block: 133 | type Entry5 = object 134 | color: string 135 | var s = "null" 136 | var v = s.fromJson(Entry5) 137 | doAssert v.color == "" 138 | 139 | block: 140 | type Entry6 = ref object 141 | color: string 142 | var s = "null" 143 | var v = s.fromJson(Entry6) 144 | doAssert v == nil 145 | 146 | type Node = ref object 147 | kind: string 148 | 149 | proc renameHook(v: var Node, fieldName: var string) = 150 | if fieldName == "type": 151 | fieldName = "kind" 152 | 153 | var node = """{"type":"root"}""".fromJson(Node) 154 | doAssert node.kind == "root" 155 | 156 | type Sizer = object 157 | size: int 158 | originalSize: int 159 | 160 | proc postHook(v: var Sizer) = 161 | v.originalSize = v.size 162 | 163 | var sizer = """{"size":10}""".fromJson(Sizer) 164 | doAssert sizer.size == 10 165 | doAssert sizer.originalSize == 10 166 | 167 | block: 168 | 169 | type 170 | NodeNumKind = enum # the different node types 171 | nkInt, # a leaf with an integer value 172 | nkFloat, # a leaf with a float value 173 | RefNode = ref object 174 | active: bool 175 | case kind: NodeNumKind # the ``kind`` field is the discriminator 176 | of nkInt: intVal: int 177 | of nkFloat: floatVal: float 178 | ValueNode = object 179 | active: bool 180 | case kind: NodeNumKind # the ``kind`` field is the discriminator 181 | of nkInt: intVal: int 182 | of nkFloat: floatVal: float 183 | 184 | block: 185 | var nodeNum = RefNode(kind: nkFloat, active: true, floatVal: 3.14) 186 | var nodeNum2 = RefNode(kind: nkInt, active: false, intVal: 42) 187 | doAssert nodeNum.toJson.fromJson(type(nodeNum)).floatVal == nodeNum.floatVal 188 | doAssert nodeNum2.toJson.fromJson(type(nodeNum2)).intVal == nodeNum2.intVal 189 | doAssert nodeNum.toJson.fromJson(type(nodeNum)).active == nodeNum.active 190 | doAssert nodeNum2.toJson.fromJson(type(nodeNum2)).active == nodeNum2.active 191 | 192 | block: 193 | # Test discriminator Field Name not being first. 194 | let 195 | a = """{"active":true,"kind":"nkFloat","floatVal":3.14}""".fromJson(RefNode) 196 | b = """{"floatVal":3.14,"active":true,"kind":"nkFloat"}""".fromJson(RefNode) 197 | c = """{"kind":"nkFloat","floatVal":3.14,"active":true}""".fromJson(RefNode) 198 | doAssert a.kind == nkFloat 199 | doAssert b.kind == nkFloat 200 | doAssert c.kind == nkFloat 201 | 202 | block: 203 | # Test discriminator field name not being there. 204 | let 205 | a = """{"active":true,"intVal":42}""".fromJson(RefNode) 206 | doAssert a.kind == nkInt 207 | 208 | block: 209 | var nodeNum = ValueNode(kind: nkFloat, active: true, floatVal: 3.14) 210 | var nodeNum2 = ValueNode(kind: nkInt, active: false, intVal: 42) 211 | doAssert nodeNum.toJson.fromJson(type(nodeNum)).floatVal == nodeNum.floatVal 212 | doAssert nodeNum2.toJson.fromJson(type(nodeNum2)).intVal == nodeNum2.intVal 213 | doAssert nodeNum.toJson.fromJson(type(nodeNum)).active == nodeNum.active 214 | doAssert nodeNum2.toJson.fromJson(type(nodeNum2)).active == nodeNum2.active 215 | 216 | block: 217 | # Test discriminator Field Name not being first. 218 | let 219 | a = """{"active":true,"kind":"nkFloat","floatVal":3.14}""".fromJson(ValueNode) 220 | b = """{"floatVal":3.14,"active":true,"kind":"nkFloat"}""".fromJson(ValueNode) 221 | c = """{"kind":"nkFloat","floatVal":3.14,"active":true}""".fromJson(ValueNode) 222 | doAssert a.kind == nkFloat 223 | doAssert b.kind == nkFloat 224 | doAssert c.kind == nkFloat 225 | 226 | block: 227 | # Test discriminator field name not being there. 228 | let 229 | a = """{"active":true,"intVal":42}""".fromJson(ValueNode) 230 | doAssert a.kind == nkInt 231 | 232 | type 233 | NodeNumKind = enum # the different node types 234 | nkInt, # a leaf with an integer value 235 | nkFloat, # a leaf with a float value 236 | RefNode = ref object 237 | active: bool 238 | case kind: NodeNumKind # the ``kind`` field is the discriminator 239 | of nkInt: intVal: int 240 | of nkFloat: floatVal: float 241 | ValueNode = object 242 | active: bool 243 | case kind: NodeNumKind # the ``kind`` field is the discriminator 244 | of nkInt: intVal: int 245 | of nkFloat: floatVal: float 246 | 247 | proc renameHook*(v: var RefNode|ValueNode, fieldName: var string) = 248 | # rename``type`` field name to ``kind`` 249 | if fieldName == "type": 250 | fieldName = "kind" 251 | 252 | # Test renameHook and discriminator Field Name not being first/missing. 253 | block: 254 | let 255 | a = """{"active":true,"type":"nkFloat","floatVal":3.14}""".fromJson(RefNode) 256 | b = """{"floatVal":3.14,"active":true,"type":"nkFloat"}""".fromJson(RefNode) 257 | c = """{"type":"nkFloat","floatVal":3.14,"active":true}""".fromJson(RefNode) 258 | d = """{"active":true,"intVal":42}""".fromJson(RefNode) 259 | doAssert a.kind == nkFloat 260 | doAssert b.kind == nkFloat 261 | doAssert c.kind == nkFloat 262 | doAssert d.kind == nkInt 263 | 264 | block: 265 | let 266 | a = """{"active":true,"type":"nkFloat","floatVal":3.14}""".fromJson(ValueNode) 267 | b = """{"floatVal":3.14,"active":true,"type":"nkFloat"}""".fromJson(ValueNode) 268 | c = """{"type":"nkFloat","floatVal":3.14,"active":true}""".fromJson(ValueNode) 269 | d = """{"active":true,"intVal":42}""".fromJson(ValueNode) 270 | doAssert a.kind == nkFloat 271 | doAssert b.kind == nkFloat 272 | doAssert c.kind == nkFloat 273 | doAssert d.kind == nkInt 274 | 275 | 276 | # test https://forum.nim-lang.org/t/7619 277 | 278 | import jsony 279 | 280 | type 281 | FooBar = object 282 | `Foo Bar`: string 283 | 284 | const jsonString = "{\"Foo Bar\": \"Hello World\"}" 285 | 286 | proc renameHook*(v: var FooBar, fieldName: var string) = 287 | if fieldName == "Foo Bar": 288 | fieldName = "FooBar" 289 | 290 | echo jsonString.fromJson(FooBar) 291 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSONy - A loose, direct to object json parser and serializer with hooks. 2 | 3 | `nimble install jsony` 4 | 5 | ![Github Actions](https://github.com/treeform/jsony/workflows/Github%20Actions/badge.svg) 6 | 7 | [API reference](https://nimdocs.com/treeform/jsony) 8 | 9 | This library has no dependencies other than the Nim standard library. 10 | 11 | ## About 12 | 13 | Real world json is _never what you want_. It might have extra fields that you don't care about. It might have missing fields requiring default values. It might change or grow new fields at any moment. Json might use `camelCase` or `snake_case`. It might use inconsistent naming. 14 | 15 | With this library you can use json your way, from the mess you get to the objects you want. 16 | 17 | ```nim 18 | @[1, 2, 3].toJson() -> "[1,2,3]" 19 | "[1,2,3]".fromJson(seq[int]) -> @[1, 2, 3] 20 | ``` 21 | 22 | ## Fast. 23 | 24 | Currently, the Nim standard module first parses or serializes json into JsonNodes and then turns the JsonNodes into your objects with the `to()` macro. This is slower and creates unnecessary work for the garbage collector. This library skips the JsonNodes and creates the objects you want directly. 25 | 26 | Another speed up comes from not using `StringStream`. Stream has a function dispatch overhead because it has to be able to switch between `StringStream` or `FileStream` at runtime. Jsony skips the overhead and just directly reads or writes to memory buffers. 27 | 28 | Another speed up comes from parsing and readings its own numbers directly from memory buffer. This allows it to bypass `string` allocations that `parseInt` or `$` create. 29 | 30 | ### Serialize speed 31 | 32 | ``` 33 | name ............................... min time avg time std dv times 34 | treeform/jsony ..................... 1.317 ms 1.365 ms ±0.054 x100 35 | status-im/nim-json-serialization ... 2.043 ms 3.448 ms ±0.746 x100 36 | planetis-m/eminim .................. 5.951 ms 9.305 ms ±3.210 x100 37 | disruptek/jason .................... 6.858 ms 7.043 ms ±0.125 x100 38 | nim std/json ....................... 8.222 ms 8.510 ms ±0.123 x100 39 | ``` 40 | 41 | ### Deserialize speed 42 | 43 | ``` 44 | name ............................... min time avg time std dv times 45 | treeform/jsony ..................... 4.134 ms 4.196 ms ±0.052 x100 46 | status-im/nim-json-serialization ... 7.119 ms 14.276 ms ±2.033 x100 47 | planetis-m/eminim .................. 7.761 ms 8.001 ms ±0.277 x100 48 | nim std/json ...................... 14.326 ms 14.473 ms ±0.113 x100 49 | ``` 50 | 51 | Note: If you find a faster nim json parser or serializer let me know! 52 | 53 | ## Can parse or serialize most types: 54 | 55 | - numbers and strings 56 | - seq and arrays 57 | - objects and ref objects 58 | - options 59 | - enums 60 | - tuples 61 | - characters 62 | - `HashTable`s and `OrderedTable`s 63 | - `HashSet`s and `OrderedSet`s 64 | - json nodes 65 | - and `parseHook()` enables you to parse any type! 66 | 67 | ## Not strict. 68 | 69 | Extra json fields are ignored and missing json fields keep their default values. 70 | 71 | ```nim 72 | type Entry1 = object 73 | color: string 74 | var s = """{"extra":"foo"}""" 75 | var v = s.fromJson(Entry1) 76 | doAssert v.color == "" 77 | ``` 78 | 79 | ## Converts snake_case to camelCase. 80 | 81 | Nim usually uses `camelCase` for its variables, while a bunch of json in the wild uses `snake_case`. This library will convert `snake_case` to `camelCase` for you when reading json. 82 | 83 | ```nim 84 | type Entry4 = object 85 | colorBlend: string 86 | 87 | var v = """{"colorBlend":"red"}""".fromJson(Entry4) 88 | doAssert v.colorBlend == "red" 89 | 90 | v = """{"color_blend":"red"}""".fromJson(Entry4) 91 | doAssert v.colorBlend == "red" 92 | ``` 93 | 94 | ## Has hooks. 95 | 96 | Hooks are a powerful concept that allows you to parse json "your way" and is the main idea behind `jsony`! 97 | 98 | - Note: that hooks need to be exported to where you are parsing the json so that the parsing system can pick them up. 99 | 100 | ### `proc newHook*()` Can be used to populate default values. 101 | 102 | Sometimes the absence of a field means it should have a default value. Normally this would just be Nim's default value for the variable type but that isn't always what you want. With the newHook() you can initialize the object's defaults before the main parsing happens. 103 | 104 | ```nim 105 | type 106 | Foo5 = object 107 | visible: string 108 | id: string 109 | proc newHook*(foo: var Foo5) = 110 | # Populates the object before its fully deserialized. 111 | foo.visible = "yes" 112 | 113 | var s = """{"id":"123"}""" 114 | var v = s.fromJson(Foo5) 115 | doAssert v.id == "123" 116 | doAssert v.visible == "yes" 117 | ``` 118 | 119 | ### `proc postHook*()` Can be used to run code after the object is fully parsed. 120 | 121 | Sometimes we need run some code after the object is created. For example to set other values based on values that were set but are not part of the json data. Maybe to sanitize the object or convert older versions to new versions. Here I need to retain the original size as I will be messing with the object's regular size: 122 | 123 | ```nim 124 | type Sizer = object 125 | size: int 126 | originalSize: int 127 | 128 | proc postHook*(v: var Sizer) = 129 | v.originalSize = v.size 130 | 131 | var sizer = """{"size":10}""".fromJson(Sizer) 132 | doAssert sizer.size == 10 133 | doAssert sizer.originalSize == 10 134 | ``` 135 | 136 | ### `proc enumHook*()` Can be used to parse enums. 137 | 138 | In the wild json enum names almost never match to Nim enum names which usually have a prefix. The `enumHook*()` allows you to rename the enums to your internal names. 139 | 140 | ```nim 141 | type Color2 = enum 142 | c2Red 143 | c2Blue 144 | c2Green 145 | 146 | proc enumHook*(v: string): Color2 = 147 | case v: 148 | of "RED": c2Red 149 | of "BLUE": c2Blue 150 | of "GREEN": c2Green 151 | else: c2Red 152 | 153 | doAssert """ "RED" """.fromJson(Color2) == c2Red 154 | doAssert """ "BLUE" """.fromJson(Color2) == c2Blue 155 | doAssert """ "GREEN" """.fromJson(Color2) == c2Green 156 | ``` 157 | 158 | ### `proc renameHook*()` Can be used to rename fields at run time. 159 | 160 | In the wild json field names can be reserved words such as type, class, or array. With the `renameHook*()` you can rename fields to what you want. 161 | 162 | ```nim 163 | type Node = ref object 164 | kind: string 165 | 166 | proc renameHook*(v: var Node, fieldName: var string) = 167 | if fieldName == "type": 168 | fieldName = "kind" 169 | 170 | var node = """{"type":"root"}""".fromJson(Node) 171 | doAssert node.kind == "root" 172 | ``` 173 | 174 | ### `proc parseHook*()` Can be used to do anything. 175 | 176 | Json can't store dates, so they are usually stored as strings. You can use 177 | `parseHook*()` to override default parsing and parse `DateTime` as a `string`: 178 | 179 | ```nim 180 | import jsony, times 181 | 182 | proc parseHook*(s: string, i: var int, v: var DateTime) = 183 | var str: string 184 | parseHook(s, i, str) 185 | v = parse(str, "yyyy-MM-dd hh:mm:ss") 186 | 187 | var dt = """ "2020-01-01 00:00:00" """.fromJson(DateTime) 188 | ``` 189 | 190 | Sometimes json gives you an object of entries with their id as keys, but you might want it as a sequence with ids inside the objects. You can handle this and many other scenarios with `parseHook*()`: 191 | 192 | ```nim 193 | type Entry = object 194 | id: string 195 | count: int 196 | filled: int 197 | 198 | let data = """{ 199 | "1": {"count":12, "filled": 11}, 200 | "2": {"count":66, "filled": 0}, 201 | "3": {"count":99, "filled": 99} 202 | }""" 203 | 204 | proc parseHook*(s: string, i: var int, v: var seq[Entry]) = 205 | var table: Table[string, Entry] 206 | parseHook(s, i, table) 207 | for k, entry in table.mpairs: 208 | entry.id = k 209 | v.add(entry) 210 | 211 | let s = data.fromJson(seq[Entry]) 212 | ``` 213 | 214 | Gives us: 215 | 216 | ``` 217 | @[ 218 | (id: "1", count: 12, filled: 11), 219 | (id: "2", count: 66, filled: 0), 220 | (id: "3", count: 99, filled: 99) 221 | ]""" 222 | ``` 223 | 224 | ### `proc dumpHook*()` Can be used to serialize into custom representation. 225 | 226 | Just like reading custom data types you can also write data types with `dumpHook*()`. 227 | The `dumpHook()` will receive the incomplete string representation of a given serialization (here `s`). 228 | You will need to add the serialization of your data type (here `v`) to that string. 229 | 230 | ```nim 231 | type Fraction = object 232 | numerator: int 233 | denominator: int 234 | 235 | proc dumpHook*(s: var string, v: Fraction) = 236 | ## Output fraction type as a string "x/y". 237 | s.add '"' 238 | s.add $v.numerator 239 | s.add '/' 240 | s.add $v.denominator 241 | s.add '"' 242 | 243 | var f = Fraction(numerator: 10, denominator: 13) 244 | let s = f.toJson() 245 | ``` 246 | 247 | Gives us: 248 | 249 | ``` 250 | "10/13" 251 | ``` 252 | 253 | ## Static writing with `toStaticJson`. 254 | 255 | Sometimes you have some json, and you want to write it in a static way. There is a special function for that: 256 | 257 | ```nim 258 | thing.toStaticJson() 259 | ``` 260 | 261 | Make sure `thing` is a `static` or a `const` value, and you will get a compile time string with your JSON. 262 | 263 | ## Full support for case variant objects. 264 | 265 | Case variant objects like this are fully supported: 266 | 267 | ```nim 268 | type RefNode = ref object 269 | case kind: NodeNumKind # The ``kind`` field is the discriminator. 270 | of nkInt: intVal: int 271 | of nkFloat: floatVal: float 272 | ``` 273 | 274 | The discriminator does not have to come first, if they do come in the middle this library will scan the object, find the discriminator field, then rewind and parse the object normally. 275 | 276 | ## Full support for json-in-json. 277 | 278 | Sometimes your json objects could contain arbitrary json structures, 279 | maybe event user defined, that could only be walked as json nodes. This library allows you to parse json-in-json were you parse some of the structure as real nim objects but leave some parts of it as Json Nodes to be walked later with code: 280 | 281 | ```nim 282 | import jsony, json 283 | 284 | type Entry = object 285 | name: string 286 | data: JsonNode 287 | 288 | """ 289 | { 290 | "name":"json-in-json", 291 | "data":{ 292 | "random-data":"here", 293 | "number":123, 294 | "number2":123.456, 295 | "array":[1,2,3], 296 | "active":true, 297 | "null":null 298 | } 299 | }""".fromJson(Entry) 300 | ``` 301 | -------------------------------------------------------------------------------- /src/jsony.nim: -------------------------------------------------------------------------------- 1 | import jsony/objvar, strutils, tables, sets, unicode, json, options, parseutils, typetraits 2 | 3 | type JsonError* = object of ValueError 4 | 5 | const whiteSpace = {' ', '\n', '\t', '\r'} 6 | 7 | when defined(release): 8 | {.push checks: off, inline.} 9 | 10 | type 11 | SomeTable*[K, V] = Table[K, V] | OrderedTable[K, V] | 12 | TableRef[K, V] | OrderedTableRef[K, V] 13 | 14 | proc parseHook*[T](s: string, i: var int, v: var seq[T]) 15 | proc parseHook*[T: enum](s: string, i: var int, v: var T) 16 | proc parseHook*[T: object|ref object](s: string, i: var int, v: var T) 17 | proc parseHook*[T](s: string, i: var int, v: var SomeTable[string, T]) 18 | proc parseHook*[T](s: string, i: var int, v: var (SomeSet[T]|set[T])) 19 | proc parseHook*[T: tuple](s: string, i: var int, v: var T) 20 | proc parseHook*[T: array](s: string, i: var int, v: var T) 21 | proc parseHook*[T: not object](s: string, i: var int, v: var ref T) 22 | proc parseHook*(s: string, i: var int, v: var JsonNode) 23 | proc parseHook*(s: string, i: var int, v: var char) 24 | proc parseHook*[T: distinct](s: string, i: var int, v: var T) 25 | 26 | template error(msg: string, i: int) = 27 | ## Shortcut to raise an exception. 28 | raise newException(JsonError, msg & " At offset: " & $i) 29 | 30 | template eatSpace*(s: string, i: var int) = 31 | ## Will consume whitespace. 32 | while i < s.len: 33 | let c = s[i] 34 | if c notin whiteSpace: 35 | break 36 | inc i 37 | 38 | template eatChar*(s: string, i: var int, c: char) = 39 | ## Will consume space before and then the character `c`. 40 | ## Will raise an exception if `c` is not found. 41 | eatSpace(s, i) 42 | if i >= s.len: 43 | error("Expected " & c & " but end reached.", i) 44 | if s[i] == c: 45 | inc i 46 | else: 47 | error("Expected " & c & " but got " & s[i] & " instead.", i) 48 | 49 | proc parseSymbol*(s: string, i: var int): string = 50 | ## Will read a symbol and return it. 51 | ## Used for numbers and booleans. 52 | eatSpace(s, i) 53 | var j = i 54 | while i < s.len: 55 | case s[i] 56 | of ',', '}', ']', whiteSpace: 57 | break 58 | else: 59 | discard 60 | inc i 61 | return s[j ..< i] 62 | 63 | proc parseHook*(s: string, i: var int, v: var bool) = 64 | ## Will parse boolean true or false. 65 | when nimvm: 66 | case parseSymbol(s, i) 67 | of "true": 68 | v = true 69 | of "false": 70 | v = false 71 | else: 72 | error("Boolean true or false expected.", i) 73 | else: 74 | # Its faster to do char by char scan: 75 | eatSpace(s, i) 76 | if i + 3 < s.len and s[i+0] == 't' and s[i+1] == 'r' and s[i+2] == 'u' and s[i+3] == 'e': 77 | i += 4 78 | v = true 79 | elif i + 4 < s.len and s[i+0] == 'f' and s[i+1] == 'a' and s[i+2] == 'l' and s[i+3] == 's' and s[i+4] == 'e': 80 | i += 5 81 | v = false 82 | else: 83 | error("Boolean true or false expected.", i) 84 | 85 | proc parseHook*(s: string, i: var int, v: var SomeUnsignedInt) = 86 | ## Will parse unsigned integers. 87 | when nimvm: 88 | v = type(v)(parseInt(parseSymbol(s, i))) 89 | else: 90 | eatSpace(s, i) 91 | var 92 | v2: uint64 = 0 93 | startI = i 94 | while i < s.len and s[i] in {'0'..'9'}: 95 | v2 = v2 * 10 + (s[i].ord - '0'.ord).uint64 96 | inc i 97 | if startI == i: 98 | error("Number expected.", i) 99 | v = type(v)(v2) 100 | 101 | proc parseHook*(s: string, i: var int, v: var SomeSignedInt) = 102 | ## Will parse signed integers. 103 | when nimvm: 104 | v = type(v)(parseInt(parseSymbol(s, i))) 105 | else: 106 | eatSpace(s, i) 107 | if i < s.len and s[i] == '+': 108 | inc i 109 | if i < s.len and s[i] == '-': 110 | var v2: uint64 111 | inc i 112 | parseHook(s, i, v2) 113 | v = -type(v)(v2) 114 | else: 115 | var v2: uint64 116 | parseHook(s, i, v2) 117 | try: 118 | v = type(v)(v2) 119 | except: 120 | error("Number type to small to contain the number.", i) 121 | 122 | proc parseHook*(s: string, i: var int, v: var SomeFloat) = 123 | ## Will parse float32 and float64. 124 | var f: float 125 | eatSpace(s, i) 126 | let chars = parseutils.parseFloat(s, f, i) 127 | if chars == 0: 128 | error("Failed to parse a float.", i) 129 | i += chars 130 | v = f 131 | 132 | proc parseUnicodeEscape(s: string, i: var int): int = 133 | inc i 134 | result = parseHexInt(s[i ..< i + 4]) 135 | i += 3 136 | # Deal with UTF-16 surrogates. Most of the time strings are encoded as utf8 137 | # but some APIs will reply with UTF-16 surrogate pairs which needs to be dealt 138 | # with. 139 | if (result and 0xfc00) == 0xd800: 140 | inc i 141 | if s[i] != '\\': 142 | error("Found an Orphan Surrogate.", i) 143 | inc i 144 | if s[i] != 'u': 145 | error("Found an Orphan Surrogate.", i) 146 | inc i 147 | let nextRune = parseHexInt(s[i ..< i + 4]) 148 | i += 3 149 | if (nextRune and 0xfc00) == 0xdc00: 150 | result = 0x10000 + (((result - 0xd800) shl 10) or (nextRune - 0xdc00)) 151 | 152 | proc parseStringSlow(s: string, i: var int, v: var string) = 153 | while i < s.len: 154 | let c = s[i] 155 | case c 156 | of '"': 157 | break 158 | of '\\': 159 | inc i 160 | let c = s[i] 161 | case c 162 | of '"', '\\', '/': v.add(c) 163 | of 'b': v.add '\b' 164 | of 'f': v.add '\f' 165 | of 'n': v.add '\n' 166 | of 'r': v.add '\r' 167 | of 't': v.add '\t' 168 | of 'u': 169 | v.add(Rune(parseUnicodeEscape(s, i)).toUTF8()) 170 | else: 171 | v.add(c) 172 | else: 173 | v.add(c) 174 | inc i 175 | eatChar(s, i, '"') 176 | 177 | proc parseStringFast(s: string, i: var int, v: var string) = 178 | # It appears to be faster to scan the string once, then allocate exact chars, 179 | # and then scan the string again populating it. 180 | var 181 | j = i 182 | ll = 0 183 | while j < s.len: 184 | let c = s[j] 185 | case c 186 | of '"': 187 | break 188 | of '\\': 189 | inc j 190 | let c = s[j] 191 | case c 192 | of 'u': 193 | ll += Rune(parseUnicodeEscape(s, j)).toUTF8().len 194 | else: 195 | inc ll 196 | else: 197 | inc ll 198 | inc j 199 | 200 | if ll > 0: 201 | v = newString(ll) 202 | var 203 | at = 0 204 | ss = cast[ptr UncheckedArray[char]](v[0].addr) 205 | template add(ss: ptr UncheckedArray[char], c: char) = 206 | ss[at] = c 207 | inc at 208 | while i < s.len: 209 | let c = s[i] 210 | case c 211 | of '"': 212 | break 213 | of '\\': 214 | inc i 215 | let c = s[i] 216 | case c 217 | of '"', '\\', '/': ss.add(c) 218 | of 'b': ss.add '\b' 219 | of 'f': ss.add '\f' 220 | of 'n': ss.add '\n' 221 | of 'r': ss.add '\r' 222 | of 't': ss.add '\t' 223 | of 'u': 224 | for c in Rune(parseUnicodeEscape(s, i)).toUTF8(): 225 | ss.add(c) 226 | else: 227 | ss.add(c) 228 | else: 229 | ss.add(c) 230 | inc i 231 | 232 | eatChar(s, i, '"') 233 | 234 | proc parseHook*(s: string, i: var int, v: var string) = 235 | ## Parse string. 236 | eatSpace(s, i) 237 | if i + 3 < s.len and s[i+0] == 'n' and s[i+1] == 'u' and s[i+2] == 'l' and s[i+3] == 'l': 238 | i += 4 239 | return 240 | eatChar(s, i, '"') 241 | 242 | when nimvm: 243 | parseStringSlow(s, i, v) 244 | else: 245 | when defined(js): 246 | parseStringSlow(s, i, v) 247 | else: 248 | parseStringFast(s, i, v) 249 | 250 | proc parseHook*(s: string, i: var int, v: var char) = 251 | var str: string 252 | s.parseHook(i, str) 253 | if str.len != 1: 254 | error("String can't fit into a char.", i) 255 | v = str[0] 256 | 257 | proc parseHook*[T](s: string, i: var int, v: var seq[T]) = 258 | ## Parse seq. 259 | eatChar(s, i, '[') 260 | while i < s.len: 261 | eatSpace(s, i) 262 | if i < s.len and s[i] == ']': 263 | break 264 | var element: T 265 | parseHook(s, i, element) 266 | v.add(element) 267 | eatSpace(s, i) 268 | if i < s.len and s[i] == ',': 269 | inc i 270 | else: 271 | break 272 | eatChar(s, i, ']') 273 | 274 | proc parseHook*[T: array](s: string, i: var int, v: var T) = 275 | eatSpace(s, i) 276 | eatChar(s, i, '[') 277 | for value in v.mitems: 278 | eatSpace(s, i) 279 | parseHook(s, i, value) 280 | eatSpace(s, i) 281 | if i < s.len and s[i] == ',': 282 | inc i 283 | eatChar(s, i, ']') 284 | 285 | proc parseHook*[T: not object](s: string, i: var int, v: var ref T) = 286 | eatSpace(s, i) 287 | if i + 3 < s.len and s[i+0] == 'n' and s[i+1] == 'u' and s[i+2] == 'l' and s[i+3] == 'l': 288 | i += 4 289 | return 290 | new(v) 291 | parseHook(s, i, v[]) 292 | 293 | proc skipValue*(s: string, i: var int) = 294 | ## Used to skip values of extra fields. 295 | eatSpace(s, i) 296 | if i < s.len and s[i] == '{': 297 | eatChar(s, i, '{') 298 | while i < s.len: 299 | eatSpace(s, i) 300 | if i < s.len and s[i] == '}': 301 | break 302 | skipValue(s, i) 303 | eatChar(s, i, ':') 304 | skipValue(s, i) 305 | eatSpace(s, i) 306 | if i < s.len and s[i] == ',': 307 | inc i 308 | eatChar(s, i, '}') 309 | elif i < s.len and s[i] == '[': 310 | eatChar(s, i, '[') 311 | while i < s.len: 312 | eatSpace(s, i) 313 | if i < s.len and s[i] == ']': 314 | break 315 | skipValue(s, i) 316 | eatSpace(s, i) 317 | if i < s.len and s[i] == ',': 318 | inc i 319 | eatChar(s, i, ']') 320 | elif i < s.len and s[i] == '"': 321 | var str: string 322 | parseHook(s, i, str) 323 | else: 324 | discard parseSymbol(s, i) 325 | 326 | proc snakeCaseDynamic(s: string): string = 327 | if s.len == 0: 328 | return 329 | var prevCap = false 330 | for i, c in s: 331 | if c in {'A'..'Z'}: 332 | if result.len > 0 and result[result.len-1] != '_' and not prevCap: 333 | result.add '_' 334 | prevCap = true 335 | result.add c.toLowerAscii() 336 | else: 337 | prevCap = false 338 | result.add c 339 | 340 | template snakeCase(s: string): string = 341 | const k = snakeCaseDynamic(s) 342 | k 343 | 344 | proc parseObjectInner[T](s: string, i: var int, v: var T) = 345 | while i < s.len: 346 | eatSpace(s, i) 347 | if i < s.len and s[i] == '}': 348 | break 349 | var key: string 350 | parseHook(s, i, key) 351 | eatChar(s, i, ':') 352 | when compiles(renameHook(v, key)): 353 | renameHook(v, key) 354 | block all: 355 | for k, v in v.fieldPairs: 356 | if k == key or snakeCase(k) == key: 357 | var v2: type(v) 358 | parseHook(s, i, v2) 359 | v = v2 360 | break all 361 | skipValue(s, i) 362 | eatSpace(s, i) 363 | if i < s.len and s[i] == ',': 364 | inc i 365 | else: 366 | break 367 | when compiles(postHook(v)): 368 | postHook(v) 369 | 370 | proc parseHook*[T: tuple](s: string, i: var int, v: var T) = 371 | eatSpace(s, i) 372 | when T.isNamedTuple(): 373 | if i < s.len and s[i] == '{': 374 | eatChar(s, i, '{') 375 | parseObjectInner(s, i, v) 376 | eatChar(s, i, '}') 377 | return 378 | eatChar(s, i, '[') 379 | for name, value in v.fieldPairs: 380 | eatSpace(s, i) 381 | parseHook(s, i, value) 382 | eatSpace(s, i) 383 | if i < s.len and s[i] == ',': 384 | inc i 385 | eatChar(s, i, ']') 386 | 387 | proc parseHook*[T: enum](s: string, i: var int, v: var T) = 388 | eatSpace(s, i) 389 | var strV: string 390 | if i < s.len and s[i] == '"': 391 | parseHook(s, i, strV) 392 | when compiles(enumHook(strV, v)): 393 | enumHook(strV, v) 394 | else: 395 | try: 396 | v = parseEnum[T](strV) 397 | except: 398 | error("Can't parse enum.", i) 399 | else: 400 | try: 401 | strV = parseSymbol(s, i) 402 | v = T(parseInt(strV)) 403 | except: 404 | error("Can't parse enum.", i) 405 | 406 | proc parseHook*[T: object|ref object](s: string, i: var int, v: var T) = 407 | ## Parse an object or ref object. 408 | eatSpace(s, i) 409 | if i + 3 < s.len and s[i+0] == 'n' and s[i+1] == 'u' and s[i+2] == 'l' and s[i+3] == 'l': 410 | i += 4 411 | return 412 | eatChar(s, i, '{') 413 | when not v.isObjectVariant: 414 | when compiles(newHook(v)): 415 | newHook(v) 416 | elif compiles(new(v)): 417 | new(v) 418 | else: 419 | # Try looking for the discriminatorFieldName, then parse as normal object. 420 | eatSpace(s, i) 421 | var saveI = i 422 | while i < s.len: 423 | var key: string 424 | parseHook(s, i, key) 425 | eatChar(s, i, ':') 426 | when compiles(renameHook(v, key)): 427 | renameHook(v, key) 428 | if key == v.discriminatorFieldName: 429 | var discriminator: type(v.discriminatorField) 430 | parseHook(s, i, discriminator) 431 | new(v, discriminator) 432 | when compiles(newHook(v)): 433 | newHook(v) 434 | break 435 | skipValue(s, i) 436 | if i < s.len and s[i] != '}': 437 | eatChar(s, i, ',') 438 | else: 439 | when compiles(newHook(v)): 440 | newHook(v) 441 | elif compiles(new(v)): 442 | new(v) 443 | break 444 | i = saveI 445 | parseObjectInner(s, i, v) 446 | eatChar(s, i, '}') 447 | 448 | proc parseHook*[T](s: string, i: var int, v: var Option[T]) = 449 | ## Parse an Option. 450 | eatSpace(s, i) 451 | if i + 3 < s.len and s[i+0] == 'n' and s[i+1] == 'u' and s[i+2] == 'l' and s[i+3] == 'l': 452 | i += 4 453 | return 454 | var e: T 455 | parseHook(s, i, e) 456 | v = some(e) 457 | 458 | proc parseHook*[T](s: string, i: var int, v: var SomeTable[string, T]) = 459 | ## Parse an object. 460 | when compiles(new(v)): 461 | new(v) 462 | eatChar(s, i, '{') 463 | while i < s.len: 464 | eatSpace(s, i) 465 | if i < s.len and s[i] == '}': 466 | break 467 | var key: string 468 | parseHook(s, i, key) 469 | eatChar(s, i, ':') 470 | var element: T 471 | parseHook(s, i, element) 472 | v[key] = element 473 | if i < s.len and s[i] == ',': 474 | inc i 475 | else: 476 | break 477 | eatChar(s, i, '}') 478 | 479 | proc parseHook*[T](s: string, i: var int, v: var (SomeSet[T]|set[T])) = 480 | ## Parses `HashSet`, `OrderedSet`, or a built-in `set` type. 481 | eatSpace(s, i) 482 | eatChar(s, i, '[') 483 | while true: 484 | eatSpace(s, i) 485 | if i < s.len and s[i] == ']': 486 | break 487 | var e: T 488 | parseHook(s, i, e) 489 | v.incl(e) 490 | eatSpace(s, i) 491 | if i < s.len and s[i] == ',': 492 | inc i 493 | eatChar(s, i, ']') 494 | 495 | proc parseHook*(s: string, i: var int, v: var JsonNode) = 496 | ## Parses a regular json node. 497 | eatSpace(s, i) 498 | if i < s.len and s[i] == '{': 499 | v = newJObject() 500 | eatChar(s, i, '{') 501 | while i < s.len: 502 | eatSpace(s, i) 503 | if i < s.len and s[i] == '}': 504 | break 505 | var k: string 506 | parseHook(s, i, k) 507 | eatChar(s, i, ':') 508 | var e: JsonNode 509 | parseHook(s, i, e) 510 | v[k] = e 511 | eatSpace(s, i) 512 | if i < s.len and s[i] == ',': 513 | inc i 514 | eatChar(s, i, '}') 515 | elif i < s.len and s[i] == '[': 516 | v = newJArray() 517 | eatChar(s, i, '[') 518 | while i < s.len: 519 | eatSpace(s, i) 520 | if i < s.len and s[i] == ']': 521 | break 522 | var e: JsonNode 523 | parseHook(s, i, e) 524 | v.add(e) 525 | eatSpace(s, i) 526 | if i < s.len and s[i] == ',': 527 | inc i 528 | eatChar(s, i, ']') 529 | elif i < s.len and s[i] == '"': 530 | var str: string 531 | parseHook(s, i, str) 532 | v = newJString(str) 533 | else: 534 | var data = parseSymbol(s, i) 535 | if data == "null": 536 | v = newJNull() 537 | elif data == "true": 538 | v = newJBool(true) 539 | elif data == "false": 540 | v = newJBool(false) 541 | elif data.len > 0 and data[0] in {'0'..'9', '-', '+'}: 542 | try: 543 | v = newJInt(parseInt(data)) 544 | except ValueError: 545 | try: 546 | v = newJFloat(parseFloat(data)) 547 | except ValueError: 548 | error("Invalid number.", i) 549 | else: 550 | error("Unexpected.", i) 551 | 552 | proc parseHook*[T: distinct](s: string, i: var int, v: var T) = 553 | var x: T.distinctBase 554 | parseHook(s, i, x) 555 | v = cast[T](x) 556 | 557 | proc fromJson*[T](s: string, x: typedesc[T]): T = 558 | ## Takes json and outputs the object it represents. 559 | ## * Extra json fields are ignored. 560 | ## * Missing json fields keep their default values. 561 | ## * `proc newHook(foo: var ...)` Can be used to populate default values. 562 | var i = 0 563 | s.parseHook(i, result) 564 | 565 | proc fromJson*(s: string): JsonNode = 566 | ## Takes json parses it into `JsonNode`s. 567 | var i = 0 568 | s.parseHook(i, result) 569 | 570 | proc dumpHook*(s: var string, v: bool) 571 | proc dumpHook*(s: var string, v: uint|uint8|uint16|uint32|uint64) 572 | proc dumpHook*(s: var string, v: int|int8|int16|int32|int64) 573 | proc dumpHook*(s: var string, v: SomeFloat) 574 | proc dumpHook*(s: var string, v: string) 575 | proc dumpHook*(s: var string, v: char) 576 | proc dumpHook*(s: var string, v: tuple) 577 | proc dumpHook*(s: var string, v: enum) 578 | type t[T] = tuple[a:string, b:T] 579 | proc dumpHook*[N, T](s: var string, v: array[N, t[T]]) 580 | proc dumpHook*[N, T](s: var string, v: array[N, T]) 581 | proc dumpHook*[T](s: var string, v: seq[T]) 582 | proc dumpHook*(s: var string, v: object) 583 | proc dumpHook*(s: var string, v: ref) 584 | proc dumpHook*[T: distinct](s: var string, v: T) 585 | 586 | proc dumpHook*[T: distinct](s: var string, v: T) = 587 | var x = cast[T.distinctBase](v) 588 | s.dumpHook(x) 589 | 590 | proc dumpHook*(s: var string, v: bool) = 591 | if v: 592 | s.add "true" 593 | else: 594 | s.add "false" 595 | 596 | const lookup = block: 597 | ## Generate 00, 01, 02 ... 99 pairs. 598 | var s = "" 599 | for i in 0 ..< 100: 600 | if ($i).len == 1: 601 | s.add("0") 602 | s.add($i) 603 | s 604 | 605 | proc dumpNumberSlow(s: var string, v: uint|uint8|uint16|uint32|uint64) = 606 | s.add $v.uint64 607 | 608 | proc dumpNumberFast(s: var string, v: uint|uint8|uint16|uint32|uint64) = 609 | # Its faster to not allocate a string for a number, 610 | # but to write it out the digits directly. 611 | if v == 0: 612 | s.add '0' 613 | return 614 | # Max size of a uin64 number is 20 digits. 615 | var digits: array[20, char] 616 | var v = v 617 | var p = 0 618 | while v != 0: 619 | # Its faster to look up 2 digits at a time, less int divisions. 620 | let idx = v mod 100 621 | digits[p] = lookup[idx*2+1] 622 | inc p 623 | digits[p] = lookup[idx*2] 624 | inc p 625 | v = v div 100 626 | var at = s.len 627 | if digits[p-1] == '0': 628 | dec p 629 | s.setLen(s.len + p) 630 | dec p 631 | while p >= 0: 632 | s[at] = digits[p] 633 | dec p 634 | inc at 635 | 636 | proc dumpHook*(s: var string, v: uint|uint8|uint16|uint32|uint64) = 637 | when nimvm: 638 | s.dumpNumberSlow(v) 639 | else: 640 | when defined(js): 641 | s.dumpNumberSlow(v) 642 | else: 643 | s.dumpNumberFast(v) 644 | 645 | proc dumpHook*(s: var string, v: int|int8|int16|int32|int64) = 646 | if v < 0: 647 | s.add '-' 648 | dumpHook(s, 0.uint64 - v.uint64) 649 | else: 650 | dumpHook(s, v.uint64) 651 | 652 | proc dumpHook*(s: var string, v: SomeFloat) = 653 | s.add $v 654 | 655 | proc dumpStrSlow(s: var string, v: string) = 656 | s.add '"' 657 | for c in v: 658 | case c: 659 | of '\\': s.add r"\\" 660 | of '\b': s.add r"\b" 661 | of '\f': s.add r"\f" 662 | of '\n': s.add r"\n" 663 | of '\r': s.add r"\r" 664 | of '\t': s.add r"\t" 665 | of '"': s.add r"\""" 666 | else: 667 | s.add c 668 | s.add '"' 669 | 670 | proc dumpStrFast(s: var string, v: string) = 671 | # Its faster to grow the string only once. 672 | # Then fill the string with pointers. 673 | # Then cap it off to right length. 674 | var at = s.len 675 | s.setLen(s.len + v.len*2+2) 676 | 677 | var ss = cast[ptr UncheckedArray[char]](s[0].addr) 678 | template add(ss: ptr UncheckedArray[char], c: char) = 679 | ss[at] = c 680 | inc at 681 | template add(ss: ptr UncheckedArray[char], c1, c2: char) = 682 | ss[at] = c1 683 | inc at 684 | ss[at] = c2 685 | inc at 686 | 687 | ss.add '"' 688 | for c in v: 689 | case c: 690 | of '\\': ss.add '\\', '\\' 691 | of '\b': ss.add '\\', 'b' 692 | of '\f': ss.add '\\', 'f' 693 | of '\n': ss.add '\\', 'n' 694 | of '\r': ss.add '\\', 'r' 695 | of '\t': ss.add '\\', 't' 696 | of '"': ss.add '\\', '"' 697 | else: 698 | ss.add c 699 | ss.add '"' 700 | s.setLen(at) 701 | 702 | proc dumpHook*(s: var string, v: string) = 703 | when nimvm: 704 | s.dumpStrSlow(v) 705 | else: 706 | when defined(js): 707 | s.dumpStrSlow(v) 708 | else: 709 | s.dumpStrFast(v) 710 | 711 | template dumpKey(s: var string, v: string) = 712 | const v2 = v.toJson() & ":" 713 | s.add v2 714 | 715 | proc dumpHook*(s: var string, v: char) = 716 | s.add '"' 717 | s.add v 718 | s.add '"' 719 | 720 | proc dumpHook*(s: var string, v: tuple) = 721 | s.add '[' 722 | var i = 0 723 | for _, e in v.fieldPairs: 724 | if i > 0: 725 | s.add ',' 726 | s.dumpHook(e) 727 | inc i 728 | s.add ']' 729 | 730 | proc dumpHook*(s: var string, v: enum) = 731 | s.dumpHook($v) 732 | 733 | proc dumpHook*[N, T](s: var string, v: array[N, T]) = 734 | s.add '[' 735 | var i = 0 736 | for e in v: 737 | if i != 0: 738 | s.add ',' 739 | s.dumpHook(e) 740 | inc i 741 | s.add ']' 742 | 743 | proc dumpHook*[T](s: var string, v: seq[T]) = 744 | s.add '[' 745 | for i, e in v: 746 | if i != 0: 747 | s.add ',' 748 | s.dumpHook(e) 749 | s.add ']' 750 | 751 | proc dumpHook*[T](s: var string, v: Option[T]) = 752 | if v.isNone: 753 | s.add "null" 754 | else: 755 | s.dumpHook(v.get()) 756 | 757 | proc dumpHook*(s: var string, v: object) = 758 | s.add '{' 759 | var i = 0 760 | when compiles(for k, e in v.pairs: discard): 761 | # Tables and table like objects. 762 | for k, e in v.pairs: 763 | if i > 0: 764 | s.add ',' 765 | s.dumpHook(k) 766 | s.add ':' 767 | s.dumpHook(e) 768 | inc i 769 | else: 770 | # Normal objects. 771 | for k, e in v.fieldPairs: 772 | if i > 0: 773 | s.add ',' 774 | s.dumpKey(k) 775 | s.dumpHook(e) 776 | inc i 777 | s.add '}' 778 | 779 | proc dumpHook*[N, T](s: var string, v: array[N, t[T]]) = 780 | s.add '{' 781 | var i = 0 782 | # Normal objects. 783 | for (k, e) in v: 784 | if i > 0: 785 | s.add ',' 786 | s.dumpHook(k) 787 | s.add ':' 788 | s.dumpHook(e) 789 | inc i 790 | s.add '}' 791 | 792 | proc dumpHook*(s: var string, v: ref) = 793 | if v == nil: 794 | s.add "null" 795 | else: 796 | s.dumpHook(v[]) 797 | 798 | proc dumpHook*[T](s: var string, v: SomeSet[T]|set[T]) = 799 | s.add '[' 800 | var i = 0 801 | for e in v: 802 | if i != 0: 803 | s.add ',' 804 | s.dumpHook(e) 805 | inc i 806 | s.add ']' 807 | 808 | proc dumpHook*(s: var string, v: JsonNode) = 809 | ## Dumps a regular json node. 810 | if v == nil: 811 | s.add "null" 812 | else: 813 | case v.kind: 814 | of JObject: 815 | s.add '{' 816 | var i = 0 817 | for k, e in v.pairs: 818 | if i != 0: 819 | s.add "," 820 | s.dumpHook(k) 821 | s.add ':' 822 | s.dumpHook(e) 823 | inc i 824 | s.add '}' 825 | of JArray: 826 | s.add '[' 827 | var i = 0 828 | for e in v: 829 | if i != 0: 830 | s.add "," 831 | s.dumpHook(e) 832 | inc i 833 | s.add ']' 834 | of JNull: 835 | s.add "null" 836 | of JInt: 837 | s.dumpHook(v.getInt) 838 | of JFloat: 839 | s.dumpHook(v.getFloat) 840 | of JString: 841 | s.dumpHook(v.getStr) 842 | of JBool: 843 | s.dumpHook(v.getBool) 844 | 845 | proc toJson*[T](v: T): string = 846 | dumpHook(result, v) 847 | 848 | template toStaticJson*(v: untyped): static[string] = 849 | ## This will turn v into json at compile time and return the json string. 850 | const s = v.toJson() 851 | s 852 | 853 | # A compiler bug prevents this from working. Otherwise toStaticJson and toJson 854 | # can be same thing. 855 | # TODO: Figure out the compiler bug. 856 | # proc toJsonDynamic*[T](v: T): string = 857 | # dumpHook(result, v) 858 | # template toJson*[T](v: static[T]): string = 859 | # ## This will turn v into json at compile time and return the json string. 860 | # const s = v.toJsonDynamic() 861 | # s 862 | 863 | 864 | when defined(release): 865 | {.pop.} 866 | --------------------------------------------------------------------------------