tjsonutils.nim 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. discard """
  2. targets: "c cpp js"
  3. """
  4. import std/jsonutils
  5. import std/json
  6. proc testRoundtrip[T](t: T, expected: string) =
  7. let j = t.toJson
  8. doAssert $j == expected, $j
  9. doAssert j.jsonTo(T).toJson == j
  10. var t2: T
  11. t2.fromJson(j)
  12. doAssert t2.toJson == j
  13. import tables, sets, algorithm, sequtils, options, strtabs
  14. type Foo = ref object
  15. id: int
  16. proc `==`(a, b: Foo): bool =
  17. a.id == b.id
  18. template fn() =
  19. block: # toJson, jsonTo
  20. type Foo = distinct float
  21. testRoundtrip('x', """120""")
  22. when not defined(js):
  23. testRoundtrip(cast[pointer](12345)): """12345"""
  24. when nimvm:
  25. discard
  26. # bugs:
  27. # Error: unhandled exception: 'intVal' is not accessible using discriminant 'kind' of type 'TNode' [
  28. # Error: VM does not support 'cast' from tyNil to tyPointer
  29. else:
  30. testRoundtrip(pointer(nil)): """0"""
  31. testRoundtrip(cast[pointer](nil)): """0"""
  32. # causes workaround in `fromJson` potentially related to
  33. # https://github.com/nim-lang/Nim/issues/12282
  34. testRoundtrip(Foo(1.5)): """1.5"""
  35. block:
  36. testRoundtrip({"z": "Z", "y": "Y"}.toOrderedTable): """{"z":"Z","y":"Y"}"""
  37. when not defined(js): # pending https://github.com/nim-lang/Nim/issues/14574
  38. testRoundtrip({"z": (f1: 'f'), }.toTable): """{"z":{"f1":102}}"""
  39. block:
  40. testRoundtrip({"name": "John", "city": "Monaco"}.newStringTable): """{"mode":"modeCaseSensitive","table":{"city":"Monaco","name":"John"}}"""
  41. block: # complex example
  42. let t = {"z": "Z", "y": "Y"}.newStringTable
  43. type A = ref object
  44. a1: string
  45. 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))
  46. testRoundtrip(a):
  47. """[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}]"""
  48. block:
  49. # edge case when user defined `==` doesn't handle `nil` well, e.g.:
  50. # https://github.com/nim-lang/nimble/blob/63695f490728e3935692c29f3d71944d83bb1e83/src/nimblepkg/version.nim#L105
  51. testRoundtrip(@[Foo(id: 10), nil]): """[{"id":10},null]"""
  52. block:
  53. type Foo = enum f1, f2, f3, f4, f5
  54. type Bar = enum b1, b2, b3, b4
  55. let a = [f2: b2, f3: b3, f4: b4]
  56. doAssert b2.ord == 1 # explains the `1`
  57. testRoundtrip(a): """[1,2,3]"""
  58. block: # case object
  59. type Foo = object
  60. x0: float
  61. case t1: bool
  62. of true: z1: int8
  63. of false: z2: uint16
  64. x1: string
  65. testRoundtrip(Foo(t1: true, z1: 5, x1: "bar")): """{"x0":0.0,"t1":true,"z1":5,"x1":"bar"}"""
  66. testRoundtrip(Foo(x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
  67. type PFoo = ref Foo
  68. testRoundtrip(PFoo(x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
  69. block: # ref case object
  70. type Foo = ref object
  71. x0: float
  72. case t1: bool
  73. of true: z1: int8
  74. of false: z2: uint16
  75. x1: string
  76. testRoundtrip(Foo(t1: true, z1: 5, x1: "bar")): """{"x0":0.0,"t1":true,"z1":5,"x1":"bar"}"""
  77. testRoundtrip(Foo(x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
  78. block: # generic case object
  79. type Foo[T] = ref object
  80. x0: float
  81. case t1: bool
  82. of true: z1: int8
  83. of false: z2: uint16
  84. x1: string
  85. testRoundtrip(Foo[float](t1: true, z1: 5, x1: "bar")): """{"x0":0.0,"t1":true,"z1":5,"x1":"bar"}"""
  86. testRoundtrip(Foo[int](x0: 1.5, t1: false, z2: 6)): """{"x0":1.5,"t1":false,"z2":6,"x1":""}"""
  87. # sanity check: nesting inside a tuple
  88. testRoundtrip((Foo[int](x0: 1.5, t1: false, z2: 6), "foo")): """[{"x0":1.5,"t1":false,"z2":6,"x1":""},"foo"]"""
  89. block: # case object: 2 discriminants, `when` branch, range discriminant
  90. type Foo[T] = object
  91. case t1: bool
  92. of true:
  93. z1: int8
  94. of false:
  95. z2: uint16
  96. when T is float:
  97. case t2: range[0..3]
  98. of 0: z3: int8
  99. of 2,3: z4: uint16
  100. else: discard
  101. testRoundtrip(Foo[float](t1: true, z1: 5, t2: 3, z4: 12)): """{"t1":true,"z1":5,"t2":3,"z4":12}"""
  102. testRoundtrip(Foo[int](t1: false, z2: 7)): """{"t1":false,"z2":7}"""
  103. # pending https://github.com/nim-lang/Nim/issues/14698, test with `type Foo[T] = ref object`
  104. block testHashSet:
  105. testRoundtrip(HashSet[string]()): "[]"
  106. testRoundtrip([""].toHashSet): """[""]"""
  107. testRoundtrip(["one"].toHashSet): """["one"]"""
  108. var s: HashSet[string]
  109. fromJson(s, parseJson("""["one","two"]"""))
  110. doAssert s == ["one", "two"].toHashSet
  111. let jsonNode = toJson(s)
  112. doAssert jsonNode.elems.mapIt(it.str).sorted == @["one", "two"]
  113. block testOrderedSet:
  114. testRoundtrip(["one", "two", "three"].toOrderedSet):
  115. """["one","two","three"]"""
  116. block testOption:
  117. testRoundtrip(some("test")): "\"test\""
  118. testRoundtrip(none[string]()): "null"
  119. testRoundtrip(some(42)): "42"
  120. testRoundtrip(none[int]()): "null"
  121. block testStrtabs:
  122. testRoundtrip(newStringTable(modeStyleInsensitive)):
  123. """{"mode":"modeStyleInsensitive","table":{}}"""
  124. testRoundtrip(
  125. newStringTable("name", "John", "surname", "Doe", modeCaseSensitive)):
  126. """{"mode":"modeCaseSensitive","table":{"name":"John","surname":"Doe"}}"""
  127. block testJoptions:
  128. type
  129. AboutLifeUniverseAndEverythingElse = object
  130. question: string
  131. answer: int
  132. block testExceptionOnExtraKeys:
  133. var guide: AboutLifeUniverseAndEverythingElse
  134. let json = parseJson(
  135. """{"question":"6*9=?","answer":42,"author":"Douglas Adams"}""")
  136. doAssertRaises ValueError, fromJson(guide, json)
  137. doAssertRaises ValueError,
  138. fromJson(guide, json, Joptions(allowMissingKeys: true))
  139. type
  140. A = object
  141. a1,a2,a3: int
  142. var a: A
  143. let j = parseJson("""{"a3": 1, "a4": 2}""")
  144. doAssertRaises ValueError,
  145. fromJson(a, j, Joptions(allowMissingKeys: true))
  146. block testExceptionOnMissingKeys:
  147. var guide: AboutLifeUniverseAndEverythingElse
  148. let json = parseJson("""{"answer":42}""")
  149. doAssertRaises ValueError, fromJson(guide, json)
  150. doAssertRaises ValueError,
  151. fromJson(guide, json, Joptions(allowExtraKeys: true))
  152. block testAllowExtraKeys:
  153. var guide: AboutLifeUniverseAndEverythingElse
  154. let json = parseJson(
  155. """{"question":"6*9=?","answer":42,"author":"Douglas Adams"}""")
  156. fromJson(guide, json, Joptions(allowExtraKeys: true))
  157. doAssert guide == AboutLifeUniverseAndEverythingElse(
  158. question: "6*9=?", answer: 42)
  159. block testAllowMissingKeys:
  160. var guide = AboutLifeUniverseAndEverythingElse(
  161. question: "6*9=?", answer: 54)
  162. let json = parseJson("""{"answer":42}""")
  163. fromJson(guide, json, Joptions(allowMissingKeys: true))
  164. doAssert guide == AboutLifeUniverseAndEverythingElse(
  165. question: "6*9=?", answer: 42)
  166. block testAllowExtraAndMissingKeys:
  167. var guide = AboutLifeUniverseAndEverythingElse(
  168. question: "6*9=?", answer: 54)
  169. let json = parseJson(
  170. """{"answer":42,"author":"Douglas Adams"}""")
  171. fromJson(guide, json, Joptions(
  172. allowExtraKeys: true, allowMissingKeys: true))
  173. doAssert guide == AboutLifeUniverseAndEverythingElse(
  174. question: "6*9=?", answer: 42)
  175. type
  176. Foo = object
  177. a: array[2, string]
  178. case b: bool
  179. of false: f: float
  180. of true: t: tuple[i: int, s: string]
  181. case c: range[0 .. 2]
  182. of 0: c0: int
  183. of 1: c1: float
  184. of 2: c2: string
  185. block testExceptionOnMissingDiscriminantKey:
  186. var foo: Foo
  187. let json = parseJson("""{"a":["one","two"]}""")
  188. doAssertRaises ValueError, fromJson(foo, json)
  189. block testDoNotResetMissingFieldsWhenHaveDiscriminantKey:
  190. var foo = Foo(a: ["one", "two"], b: true, t: (i: 42, s: "s"),
  191. c: 0, c0: 1)
  192. let json = parseJson("""{"b":true,"c":2}""")
  193. fromJson(foo, json, Joptions(allowMissingKeys: true))
  194. doAssert foo.a == ["one", "two"]
  195. doAssert foo.b
  196. doAssert foo.t == (i: 42, s: "s")
  197. doAssert foo.c == 2
  198. doAssert foo.c2 == ""
  199. block testAllowMissingDiscriminantKeys:
  200. var foo: Foo
  201. let json = parseJson("""{"a":["one","two"],"c":1,"c1":3.14159}""")
  202. fromJson(foo, json, Joptions(allowMissingKeys: true))
  203. doAssert foo.a == ["one", "two"]
  204. doAssert not foo.b
  205. doAssert foo.f == 0.0
  206. doAssert foo.c == 1
  207. doAssert foo.c1 == 3.14159
  208. block testExceptionOnWrongDiscirminatBranchInJson:
  209. var foo = Foo(b: false, f: 3.14159, c: 0, c0: 42)
  210. let json = parseJson("""{"c2": "hello"}""")
  211. doAssertRaises ValueError,
  212. fromJson(foo, json, Joptions(allowMissingKeys: true))
  213. # Test that the original fields are not reset.
  214. doAssert not foo.b
  215. doAssert foo.f == 3.14159
  216. doAssert foo.c == 0
  217. doAssert foo.c0 == 42
  218. block testNoExceptionOnRightDiscriminantBranchInJson:
  219. var foo = Foo(b: false, f: 0, c:1, c1: 0)
  220. let json = parseJson("""{"f":2.71828,"c1": 3.14159}""")
  221. fromJson(foo, json, Joptions(allowMissingKeys: true))
  222. doAssert not foo.b
  223. doAssert foo.f == 2.71828
  224. doAssert foo.c == 1
  225. doAssert foo.c1 == 3.14159
  226. block testAllowExtraKeysInJsonOnWrongDisciriminatBranch:
  227. var foo = Foo(b: false, f: 3.14159, c: 0, c0: 42)
  228. let json = parseJson("""{"c2": "hello"}""")
  229. fromJson(foo, json, Joptions(allowMissingKeys: true,
  230. allowExtraKeys: true))
  231. # Test that the original fields are not reset.
  232. doAssert not foo.b
  233. doAssert foo.f == 3.14159
  234. doAssert foo.c == 0
  235. doAssert foo.c0 == 42
  236. when false:
  237. ## TODO: Implement support for nested variant objects allowing the tests
  238. ## bellow to pass.
  239. block testNestedVariantObjects:
  240. type
  241. Variant = object
  242. case b: bool
  243. of false:
  244. case bf: bool
  245. of false: bff: int
  246. of true: bft: float
  247. of true:
  248. case bt: bool
  249. of false: btf: string
  250. of true: btt: char
  251. testRoundtrip(Variant(b: false, bf: false, bff: 42)):
  252. """{"b": false, "bf": false, "bff": 42}"""
  253. testRoundtrip(Variant(b: false, bf: true, bft: 3.14159)):
  254. """{"b": false, "bf": true, "bft": 3.14159}"""
  255. testRoundtrip(Variant(b: true, bt: false, btf: "test")):
  256. """{"b": true, "bt": false, "btf": "test"}"""
  257. testRoundtrip(Variant(b: true, bt: true, btt: 'c')):
  258. """{"b": true, "bt": true, "btt": "c"}"""
  259. # TODO: Add additional tests with missing and extra JSON keys, both when
  260. # allowed and forbidden analogous to the tests for the not nested
  261. # variant objects.
  262. static: fn()
  263. fn()