tjsonutils.nim 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. discard """
  2. targets: "c cpp js"
  3. """
  4. import std/jsonutils
  5. import std/json
  6. from std/math import isNaN, signbit
  7. from std/fenv import epsilon
  8. from stdtest/testutils import whenRuntimeJs
  9. proc testRoundtrip[T](t: T, expected: string) =
  10. # checks that `T => json => T2 => json2` is such that json2 = json
  11. let j = t.toJson
  12. doAssert $j == expected, "\n" & $j & "\n" & expected
  13. doAssert j.jsonTo(T).toJson == j
  14. var t2: T
  15. t2.fromJson(j)
  16. doAssert t2.toJson == j
  17. proc testRoundtripVal[T](t: T, expected: string) =
  18. # similar to testRoundtrip, but also checks that the `T => json => T2` is such that `T2 == T`
  19. # note that this isn't always possible, e.g. for pointer-like types.
  20. let j = t.toJson
  21. let j2 = $j
  22. doAssert j2 == expected, j2
  23. let j3 = j2.parseJson
  24. let t2 = j3.jsonTo(T)
  25. doAssert t2 == t
  26. doAssert $t2.toJson == j2 # still needed, because -0.0 = 0.0 but their json representation differs
  27. import tables, sets, algorithm, sequtils, options, strtabs
  28. from strutils import contains
  29. type Foo = ref object
  30. id: int
  31. proc `==`(a, b: Foo): bool =
  32. a.id == b.id
  33. type MyEnum = enum me0, me1 = "me1Alt", me2, me3, me4
  34. proc `$`(a: MyEnum): string =
  35. # putting this here pending https://github.com/nim-lang/Nim/issues/13747
  36. if a == me2: "me2Modif"
  37. else: system.`$`(a)
  38. template fn() =
  39. block: # toJson, jsonTo
  40. type Foo = distinct float
  41. testRoundtrip('x', """120""")
  42. when not defined(js):
  43. testRoundtrip(cast[pointer](12345)): """12345"""
  44. when nimvm:
  45. discard
  46. # bugs:
  47. # Error: unhandled exception: 'intVal' is not accessible using discriminant 'kind' of type 'TNode' [
  48. # Error: VM does not support 'cast' from tyNil to tyPointer
  49. else:
  50. testRoundtrip(pointer(nil)): """0"""
  51. testRoundtrip(cast[pointer](nil)): """0"""
  52. # causes workaround in `fromJson` potentially related to
  53. # https://github.com/nim-lang/Nim/issues/12282
  54. testRoundtrip(Foo(1.5)): """1.5"""
  55. block: # OrderedTable
  56. testRoundtrip({"z": "Z", "y": "Y"}.toOrderedTable): """{"z":"Z","y":"Y"}"""
  57. doAssert toJson({"z": 10, "": 11}.newTable).`$`.contains """"":11""" # allows hash to change
  58. testRoundtrip({"z".cstring: 1, "".cstring: 2}.toOrderedTable): """{"z":1,"":2}"""
  59. testRoundtrip({"z": (f1: 'f'), }.toTable): """{"z":{"f1":102}}"""
  60. block: # StringTable
  61. testRoundtrip({"name": "John", "city": "Monaco"}.newStringTable): """{"mode":"modeCaseSensitive","table":{"city":"Monaco","name":"John"}}"""
  62. block: # complex example
  63. let t = {"z": "Z", "y": "Y"}.newStringTable
  64. type A = ref object
  65. a1: string
  66. let a = (1.1, "fo", 'x', @[10,11], [true, false], [t,newStringTable()], [0'i8,3'i8], -4'i16, (foo: 0.5'f32, bar: A(a1: "abc"), bar2: A.default, cstring1: "foo", cstring2: "", cstring3: cstring(nil)))
  67. testRoundtrip(a):
  68. """[1.1,"fo",120,[10,11],[true,false],[{"mode":"modeCaseSensitive","table":{"y":"Y","z":"Z"}},{"mode":"modeCaseSensitive","table":{}}],[0,3],-4,{"foo":0.5,"bar":{"a1":"abc"},"bar2":null,"cstring1":"foo","cstring2":"","cstring3":null}]"""
  69. block:
  70. # edge case when user defined `==` doesn't handle `nil` well, e.g.:
  71. # https://github.com/nim-lang/nimble/blob/63695f490728e3935692c29f3d71944d83bb1e83/src/nimblepkg/version.nim#L105
  72. testRoundtrip(@[Foo(id: 10), nil]): """[{"id":10},null]"""
  73. block: # enum
  74. type Foo = enum f1, f2, f3, f4, f5
  75. type Bar = enum b1, b2, b3, b4
  76. let a = [f2: b2, f3: b3, f4: b4]
  77. doAssert b2.ord == 1 # explains the `1`
  78. testRoundtrip(a): """[1,2,3]"""
  79. block: # JsonNode
  80. let a = ((1, 2.5, "abc").toJson, (3, 4.5, "foo"))
  81. testRoundtripVal(a): """[[1,2.5,"abc"],[3,4.5,"foo"]]"""
  82. block:
  83. template toInt(a): untyped = cast[int](a)
  84. let a = 3.toJson
  85. let b = (a, a)
  86. let c1 = b.toJson
  87. doAssert c1[0].toInt == a.toInt
  88. doAssert c1[1].toInt == a.toInt
  89. let c2 = b.toJson(ToJsonOptions(jsonNodeMode: joptJsonNodeAsCopy))
  90. doAssert c2[0].toInt != a.toInt
  91. doAssert c2[1].toInt != c2[0].toInt
  92. doAssert c2[1] == c2[0]
  93. let c3 = b.toJson(ToJsonOptions(jsonNodeMode: joptJsonNodeAsObject))
  94. doAssert $c3 == """[{"isUnquoted":false,"kind":2,"num":3},{"isUnquoted":false,"kind":2,"num":3}]"""
  95. block: # ToJsonOptions
  96. let a = (me1, me2)
  97. doAssert $a.toJson() == "[1,2]"
  98. doAssert $a.toJson(ToJsonOptions(enumMode: joptEnumSymbol)) == """["me1","me2"]"""
  99. doAssert $a.toJson(ToJsonOptions(enumMode: joptEnumString)) == """["me1Alt","me2Modif"]"""
  100. block: # set
  101. type Foo = enum f1, f2, f3, f4, f5
  102. type Goo = enum g1 = 10, g2 = 15, g3 = 17, g4
  103. let a = ({f1, f3}, {1'u8, 7'u8}, {'0'..'9'}, {123'u16, 456, 789, 1121, 1122, 1542}, {g2, g3})
  104. testRoundtrip(a): """[[0,2],[1,7],[48,49,50,51,52,53,54,55,56,57],[123,456,789,1121,1122,1542],[15,17]]"""
  105. block: # bug #17383
  106. block:
  107. let a = (int32.high, uint32.high)
  108. testRoundtrip(a): "[2147483647,4294967295]"
  109. when int.sizeof > 4:
  110. block:
  111. let a = (int64.high, uint64.high)
  112. testRoundtrip(a): "[9223372036854775807,18446744073709551615]"
  113. block:
  114. let a = (int.high, uint.high)
  115. when int.sizeof == 4:
  116. testRoundtrip(a): "[2147483647,4294967295]"
  117. else:
  118. testRoundtrip(a): "[9223372036854775807,18446744073709551615]"
  119. block: # bug #18007
  120. testRoundtrip((NaN, Inf, -Inf, 0.0, -0.0, 1.0)): """["nan","inf","-inf",0.0,-0.0,1.0]"""
  121. testRoundtrip((float32(NaN), Inf, -Inf, 0.0, -0.0, 1.0)): """["nan","inf","-inf",0.0,-0.0,1.0]"""
  122. testRoundtripVal((Inf, -Inf, 0.0, -0.0, 1.0)): """["inf","-inf",0.0,-0.0,1.0]"""
  123. doAssert ($NaN.toJson).parseJson.jsonTo(float).isNaN
  124. block: # bug #18009; unfixable unless we change parseJson (which would have overhead),
  125. # but at least we can guarantee that the distinction between 0.0 and -0.0 is preserved.
  126. let a = (0, 0.0, -0.0, 0.5, 1, 1.0)
  127. testRoundtripVal(a): "[0,0.0,-0.0,0.5,1,1.0]"
  128. let a2 = $($a.toJson).parseJson
  129. whenRuntimeJs:
  130. doAssert a2 == "[0,0,-0.0,0.5,1,1]"
  131. do:
  132. doAssert a2 == "[0,0.0,-0.0,0.5,1,1.0]"
  133. let b = a2.parseJson.jsonTo(type(a))
  134. doAssert not b[1].signbit
  135. doAssert b[2].signbit
  136. doAssert not b[3].signbit
  137. block: # bug #15397, bug #13196
  138. let a = 0.1
  139. let x = 0.12345678901234567890123456789
  140. let b = (a + 0.2, 0.3, x)
  141. testRoundtripVal(b): "[0.30000000000000004,0.3,0.12345678901234568]"
  142. testRoundtripVal(0.12345678901234567890123456789): "0.12345678901234568"
  143. testRoundtripVal(epsilon(float64)): "2.220446049250313e-16"
  144. testRoundtripVal(1.0 + epsilon(float64)): "1.0000000000000002"
  145. block: # case object
  146. type Foo = object
  147. x0: float
  148. case t1: bool
  149. of true: z1: int8
  150. of false: z2: uint16
  151. x1: string
  152. testRoundtrip(Foo(t1: true, z1: 5, x1: "bar")): """{"x0":0.0,"t1":true,"z1":5,"x1":"bar"}"""
  153. testRoundtrip(Foo(x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
  154. type PFoo = ref Foo
  155. testRoundtrip(PFoo(x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
  156. block: # ref case object
  157. type Foo = ref object
  158. x0: float
  159. case t1: bool
  160. of true: z1: int8
  161. of false: z2: uint16
  162. x1: string
  163. testRoundtrip(Foo(t1: true, z1: 5, x1: "bar")): """{"x0":0.0,"t1":true,"z1":5,"x1":"bar"}"""
  164. testRoundtrip(Foo(x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
  165. block: # generic case object
  166. type Foo[T] = ref object
  167. x0: float
  168. case t1: bool
  169. of true: z1: int8
  170. of false: z2: uint16
  171. x1: string
  172. testRoundtrip(Foo[float](t1: true, z1: 5, x1: "bar")): """{"x0":0.0,"t1":true,"z1":5,"x1":"bar"}"""
  173. testRoundtrip(Foo[int](x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
  174. # sanity check: nesting inside a tuple
  175. testRoundtrip((Foo[int](x0: 1.5, t1: false, z2: 6), "foo")): """[{"x0":1.5,"t1":false,"z2":6,"x1":""},"foo"]"""
  176. block: # case object: 2 discriminants, `when` branch, range discriminant
  177. type Foo[T] = object
  178. case t1: bool
  179. of true:
  180. z1: int8
  181. of false:
  182. z2: uint16
  183. when T is float:
  184. case t2: range[0..3]
  185. of 0: z3: int8
  186. of 2,3: z4: uint16
  187. else: discard
  188. testRoundtrip(Foo[float](t1: true, z1: 5, t2: 3, z4: 12)): """{"t1":true,"z1":5,"t2":3,"z4":12}"""
  189. testRoundtrip(Foo[int](t1: false, z2: 7)): """{"t1":false,"z2":7}"""
  190. # pending https://github.com/nim-lang/Nim/issues/14698, test with `type Foo[T] = ref object`
  191. block: # bug: pass opt params in fromJson
  192. type Foo = object
  193. a: int
  194. b: string
  195. c: float
  196. type Bar = object
  197. foo: Foo
  198. boo: string
  199. var f: seq[Foo]
  200. try:
  201. fromJson(f, parseJson """[{"b": "bbb"}]""")
  202. doAssert false
  203. except ValueError:
  204. doAssert true
  205. fromJson(f, parseJson """[{"b": "bbb"}]""", Joptions(allowExtraKeys: true, allowMissingKeys: true))
  206. doAssert f == @[Foo(a: 0, b: "bbb", c: 0.0)]
  207. var b: Bar
  208. fromJson(b, parseJson """{"foo": {"b": "bbb"}}""", Joptions(allowExtraKeys: true, allowMissingKeys: true))
  209. doAssert b == Bar(foo: Foo(a: 0, b: "bbb", c: 0.0))
  210. block: # jsonTo with `opt`
  211. let b2 = """{"foo": {"b": "bbb"}}""".parseJson.jsonTo(Bar, Joptions(allowExtraKeys: true, allowMissingKeys: true))
  212. doAssert b2 == Bar(foo: Foo(a: 0, b: "bbb", c: 0.0))
  213. block testHashSet:
  214. testRoundtrip(HashSet[string]()): "[]"
  215. testRoundtrip([""].toHashSet): """[""]"""
  216. testRoundtrip(["one"].toHashSet): """["one"]"""
  217. var s: HashSet[string]
  218. fromJson(s, parseJson("""["one","two"]"""))
  219. doAssert s == ["one", "two"].toHashSet
  220. let jsonNode = toJson(s)
  221. doAssert jsonNode.elems.mapIt(it.str).sorted == @["one", "two"]
  222. block testOrderedSet:
  223. testRoundtrip(["one", "two", "three"].toOrderedSet):
  224. """["one","two","three"]"""
  225. block testOption:
  226. testRoundtrip(some("test")): "\"test\""
  227. testRoundtrip(none[string]()): "null"
  228. testRoundtrip(some(42)): "42"
  229. testRoundtrip(none[int]()): "null"
  230. block testStrtabs:
  231. testRoundtrip(newStringTable(modeStyleInsensitive)):
  232. """{"mode":"modeStyleInsensitive","table":{}}"""
  233. testRoundtrip(
  234. newStringTable("name", "John", "surname", "Doe", modeCaseSensitive)):
  235. """{"mode":"modeCaseSensitive","table":{"name":"John","surname":"Doe"}}"""
  236. block testJoptions:
  237. type
  238. AboutLifeUniverseAndEverythingElse = object
  239. question: string
  240. answer: int
  241. block testExceptionOnExtraKeys:
  242. var guide: AboutLifeUniverseAndEverythingElse
  243. let json = parseJson(
  244. """{"question":"6*9=?","answer":42,"author":"Douglas Adams"}""")
  245. doAssertRaises ValueError, fromJson(guide, json)
  246. doAssertRaises ValueError,
  247. fromJson(guide, json, Joptions(allowMissingKeys: true))
  248. type
  249. A = object
  250. a1,a2,a3: int
  251. var a: A
  252. let j = parseJson("""{"a3": 1, "a4": 2}""")
  253. doAssertRaises ValueError,
  254. fromJson(a, j, Joptions(allowMissingKeys: true))
  255. block testExceptionOnMissingKeys:
  256. var guide: AboutLifeUniverseAndEverythingElse
  257. let json = parseJson("""{"answer":42}""")
  258. doAssertRaises ValueError, fromJson(guide, json)
  259. doAssertRaises ValueError,
  260. fromJson(guide, json, Joptions(allowExtraKeys: true))
  261. block testAllowExtraKeys:
  262. var guide: AboutLifeUniverseAndEverythingElse
  263. let json = parseJson(
  264. """{"question":"6*9=?","answer":42,"author":"Douglas Adams"}""")
  265. fromJson(guide, json, Joptions(allowExtraKeys: true))
  266. doAssert guide == AboutLifeUniverseAndEverythingElse(
  267. question: "6*9=?", answer: 42)
  268. block refObject: #bug 17986
  269. type A = ref object
  270. case is_a: bool
  271. of true:
  272. a: int
  273. else:
  274. b: int
  275. var a = A()
  276. fromJson(a, """{"is_a": true, "a":1, "extra_key": 1}""".parse_json, Joptions(allowExtraKeys: true))
  277. doAssert $a[] == "(is_a: true, a: 1)"
  278. block testAllowMissingKeys:
  279. var guide = AboutLifeUniverseAndEverythingElse(
  280. question: "6*9=?", answer: 54)
  281. let json = parseJson("""{"answer":42}""")
  282. fromJson(guide, json, Joptions(allowMissingKeys: true))
  283. doAssert guide == AboutLifeUniverseAndEverythingElse(
  284. question: "6*9=?", answer: 42)
  285. block testAllowExtraAndMissingKeys:
  286. var guide = AboutLifeUniverseAndEverythingElse(
  287. question: "6*9=?", answer: 54)
  288. let json = parseJson(
  289. """{"answer":42,"author":"Douglas Adams"}""")
  290. fromJson(guide, json, Joptions(
  291. allowExtraKeys: true, allowMissingKeys: true))
  292. doAssert guide == AboutLifeUniverseAndEverythingElse(
  293. question: "6*9=?", answer: 42)
  294. type
  295. Foo = object
  296. a: array[2, string]
  297. case b: bool
  298. of false: f: float
  299. of true: t: tuple[i: int, s: string]
  300. case c: range[0 .. 2]
  301. of 0: c0: int
  302. of 1: c1: float
  303. of 2: c2: string
  304. block testExceptionOnMissingDiscriminantKey:
  305. var foo: Foo
  306. let json = parseJson("""{"a":["one","two"]}""")
  307. doAssertRaises ValueError, fromJson(foo, json)
  308. block testDoNotResetMissingFieldsWhenHaveDiscriminantKey:
  309. var foo = Foo(a: ["one", "two"], b: true, t: (i: 42, s: "s"),
  310. c: 0, c0: 1)
  311. let json = parseJson("""{"b":true,"c":2}""")
  312. fromJson(foo, json, Joptions(allowMissingKeys: true))
  313. doAssert foo.a == ["one", "two"]
  314. doAssert foo.b
  315. doAssert foo.t == (i: 42, s: "s")
  316. doAssert foo.c == 2
  317. doAssert foo.c2 == ""
  318. block testAllowMissingDiscriminantKeys:
  319. var foo: Foo
  320. let json = parseJson("""{"a":["one","two"],"c":1,"c1":3.14159}""")
  321. fromJson(foo, json, Joptions(allowMissingKeys: true))
  322. doAssert foo.a == ["one", "two"]
  323. doAssert not foo.b
  324. doAssert foo.f == 0.0
  325. doAssert foo.c == 1
  326. doAssert foo.c1 == 3.14159
  327. block testExceptionOnWrongDiscirminatBranchInJson:
  328. var foo = Foo(b: false, f: 3.14159, c: 0, c0: 42)
  329. let json = parseJson("""{"c2": "hello"}""")
  330. doAssertRaises ValueError,
  331. fromJson(foo, json, Joptions(allowMissingKeys: true))
  332. # Test that the original fields are not reset.
  333. doAssert not foo.b
  334. doAssert foo.f == 3.14159
  335. doAssert foo.c == 0
  336. doAssert foo.c0 == 42
  337. block testNoExceptionOnRightDiscriminantBranchInJson:
  338. var foo = Foo(b: false, f: 0, c:1, c1: 0)
  339. let json = parseJson("""{"f":2.71828,"c1": 3.14159}""")
  340. fromJson(foo, json, Joptions(allowMissingKeys: true))
  341. doAssert not foo.b
  342. doAssert foo.f == 2.71828
  343. doAssert foo.c == 1
  344. doAssert foo.c1 == 3.14159
  345. block testAllowExtraKeysInJsonOnWrongDisciriminatBranch:
  346. var foo = Foo(b: false, f: 3.14159, c: 0, c0: 42)
  347. let json = parseJson("""{"c2": "hello"}""")
  348. fromJson(foo, json, Joptions(allowMissingKeys: true,
  349. allowExtraKeys: true))
  350. # Test that the original fields are not reset.
  351. doAssert not foo.b
  352. doAssert foo.f == 3.14159
  353. doAssert foo.c == 0
  354. doAssert foo.c0 == 42
  355. when false:
  356. ## TODO: Implement support for nested variant objects allowing the tests
  357. ## bellow to pass.
  358. block testNestedVariantObjects:
  359. type
  360. Variant = object
  361. case b: bool
  362. of false:
  363. case bf: bool
  364. of false: bff: int
  365. of true: bft: float
  366. of true:
  367. case bt: bool
  368. of false: btf: string
  369. of true: btt: char
  370. testRoundtrip(Variant(b: false, bf: false, bff: 42)):
  371. """{"b": false, "bf": false, "bff": 42}"""
  372. testRoundtrip(Variant(b: false, bf: true, bft: 3.14159)):
  373. """{"b": false, "bf": true, "bft": 3.14159}"""
  374. testRoundtrip(Variant(b: true, bt: false, btf: "test")):
  375. """{"b": true, "bt": false, "btf": "test"}"""
  376. testRoundtrip(Variant(b: true, bt: true, btt: 'c')):
  377. """{"b": true, "bt": true, "btt": "c"}"""
  378. # TODO: Add additional tests with missing and extra JSON keys, both when
  379. # allowed and forbidden analogous to the tests for the not nested
  380. # variant objects.
  381. static: fn()
  382. fn()