Skip to content

Commit e3aa56f

Browse files
committed
Full support for case variant objects.
1 parent 227f24d commit e3aa56f

File tree

5 files changed

+199
-88
lines changed

5 files changed

+199
-88
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,18 @@ thing.toStaticJson()
231231
```
232232

233233
Make sure `thing` is a `static` or a `const` value and you will get a compile time string with your JSON.
234+
235+
### Full support for case variant objects.
236+
237+
Case variant objects like this are fully supported:
238+
239+
```nim
240+
t=oe RefNode = ref object
241+
case kind: NodeNumKind # The ``kind`` field is the discriminator.
242+
of nkInt: intVal: int
243+
of nkFloat: floatVal: float
244+
```
245+
246+
The discriminator do no have to come first, if they do come in the middle this
247+
library will scan the object, find the discriminator field, then rewind and
248+
parse the object normally.

src/jsony.nim

+39-53
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import macros, strutils, tables, unicode
1+
import jsony/objvar, strutils, tables, unicode
22

33
type JsonError* = object of ValueError
44

@@ -220,10 +220,7 @@ proc skipValue(s: string, i: var int) =
220220
else:
221221
discard parseSymbol(s, i)
222222

223-
proc camelCase(s: string): string =
224-
return s
225-
226-
proc snakeCase(s: string): string =
223+
proc snakeCaseDynamic(s: string): string =
227224
if s.len == 0:
228225
return
229226
var prevCap = false
@@ -237,39 +234,9 @@ proc snakeCase(s: string): string =
237234
prevCap = false
238235
result.add c
239236

240-
macro fieldsMacro(v: typed, key: string) =
241-
## Crates a parser for object fields.
242-
result = nnkCaseStmt.newTree(ident"key")
243-
# Get implementation of v's type.
244-
var impl = getTypeImpl(v)
245-
# Walk refs and pointers to the real type.
246-
while impl.kind in {nnkRefTy, nnkPtrTy}:
247-
impl = getTypeImpl(impl[0])
248-
# For each field in the type:
249-
var used: seq[string]
250-
for f in impl[2]:
251-
# Get fields name and type information.
252-
let fieldName = f[0]
253-
let filedNameStr = fieldName.strVal()
254-
let filedType = f[1]
255-
# Output a name/type checker for it:
256-
for fn in [camelCase, snakeCase]:
257-
let caseName = fn(filedNameStr)
258-
if caseName in used:
259-
continue
260-
used.add(caseName)
261-
let ofClause = nnkOfBranch.newTree(newLit(caseName))
262-
let body = quote:
263-
var value: `filedType`
264-
parseHook(s, i, value)
265-
v.`fieldName` = value
266-
ofClause.add(body)
267-
result.add(ofClause)
268-
let ofElseClause = nnkElse.newTree()
269-
let body = quote:
270-
skipValue(s, i)
271-
ofElseClause.add(body)
272-
result.add(ofElseClause)
237+
template snakeCase(s: string): string =
238+
const k = snakeCaseDynamic(s)
239+
k
273240

274241
proc parseHook*[T: enum](s: string, i: var int, v: var T) =
275242
eatSpace(s, i)
@@ -291,16 +258,37 @@ proc parseHook*[T: enum](s: string, i: var int, v: var T) =
291258
error("Can't parse enum.", i)
292259

293260
proc parseHook*[T: object|ref object](s: string, i: var int, v: var T) =
294-
## Parse an object.
261+
## Parse an object or ref object.
295262
eatSpace(s, i)
296263
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':
297264
i += 4
298265
return
299266
eatChar(s, i, '{')
300-
when compiles(newHook(v)):
301-
newHook(v)
302-
elif compiles(new(v)):
303-
new(v)
267+
when not v.isObjectVariant:
268+
when compiles(newHook(v)):
269+
newHook(v)
270+
elif compiles(new(v)):
271+
new(v)
272+
else:
273+
# Look for the discriminatorFieldName
274+
eatSpace(s, i)
275+
var saveI = i
276+
while i < s.len:
277+
var key: string
278+
parseHook(s, i, key)
279+
eatChar(s, i, ':')
280+
if key == v.discriminatorFieldName:
281+
var discriminator: type(v.discriminatorField)
282+
parseHook(s, i, discriminator)
283+
new(v, discriminator)
284+
when compiles(newHook(v)):
285+
newHook(v)
286+
break
287+
skipValue(s, i)
288+
if i < s.len and s[i] == '}':
289+
error("No discriminator field.", i)
290+
eatChar(s, i, ',')
291+
i = saveI
304292
while i < s.len:
305293
eatSpace(s, i)
306294
if i < s.len and s[i] == '}':
@@ -310,7 +298,14 @@ proc parseHook*[T: object|ref object](s: string, i: var int, v: var T) =
310298
eatChar(s, i, ':')
311299
when compiles(renameHook(v, key)):
312300
renameHook(v, key)
313-
fieldsMacro(v, key)
301+
block all:
302+
for k, v in v.fieldPairs:
303+
if k == key or snakeCase(k) == key:
304+
var v2: type(v)
305+
parseHook(s, i, v2)
306+
v = v2
307+
break all
308+
skipValue(s, i)
314309
eatSpace(s, i)
315310
if i < s.len and s[i] == ',':
316311
inc i
@@ -339,15 +334,6 @@ proc parseHook*[T](s: string, i: var int, v: var Table[string, T]) =
339334
break
340335
eatChar(s, i, '}')
341336

342-
# proc fromJson*[T](s: string): T =
343-
# ## Takes json and outputs the object it represents.
344-
# ## * Extra json fields are ignored.
345-
# ## * Missing json fields keep their default values.
346-
# ## * `proc newHook(foo: var ...)` Can be used to populate default values.
347-
348-
# var i = 0
349-
# parseHook(s, i, result)
350-
351337
proc fromJson*[T](s: string, x: typedesc[T]): T =
352338
## Takes json and outputs the object it represents.
353339
## * Extra json fields are ignored.

src/jsony/objvar.nim

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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`)

tests/test_objects.nim

+64-34
Original file line numberDiff line numberDiff line change
@@ -164,37 +164,67 @@ var sizer = """{"size":10}""".fromJson(Sizer)
164164
doAssert sizer.size == 10
165165
doAssert sizer.originalSize == 10
166166

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-
188-
# doAssert nodeNum.toJson.fromJson(type(nodeNum)).floatVal == nodeNum.floatVal
189-
# doAssert nodeNum2.toJson.fromJson(type(nodeNum2)).intVal == nodeNum2.intVal
190-
# doAssert nodeNum.toJson.fromJson(type(nodeNum)).active == nodeNum.active
191-
# doAssert nodeNum2.toJson.fromJson(type(nodeNum2)).active == nodeNum2.active
192-
193-
# block:
194-
# var nodeNum = ValueNode(kind: nkFloat, active: true, floatVal: 3.14)
195-
# var nodeNum2 = ValueNode(kind: nkInt, active: false, intVal: 42)
196-
197-
# doAssert nodeNum.toJson.fromJson(type(nodeNum)).floatVal == nodeNum.floatVal
198-
# doAssert nodeNum2.toJson.fromJson(type(nodeNum2)).intVal == nodeNum2.intVal
199-
# doAssert nodeNum.toJson.fromJson(type(nodeNum)).active == nodeNum.active
200-
# doAssert nodeNum2.toJson.fromJson(type(nodeNum2)).active == nodeNum2.active
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+
doAssertRaises JsonError:
205+
let
206+
a = """{"active":true,"floatVal":3.14}""".fromJson(RefNode)
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+
doAssertRaises JsonError:
229+
let
230+
a = """{"active":true,"floatVal":3.14}""".fromJson(ValueNode)

tests/test_tojson.nim

+28-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ block:
3939
b: float
4040
c: string
4141
var obj = Obj()
42-
echo obj.toJson()
4342
doAssert obj.toJson() == """{"a":0,"b":0.0,"c":""}"""
4443
match obj
4544

@@ -79,3 +78,31 @@ proc dumpHook(s: var string, v: Fraction) =
7978

8079
var f = Fraction(numerator: 10, denominator: 13)
8180
doAssert f.toJson() == "\"10/13\""
81+
82+
block:
83+
type
84+
NodeNumKind = enum # the different node types
85+
nkInt, # a leaf with an integer value
86+
nkFloat, # a leaf with a float value
87+
RefNode = ref object
88+
active: bool
89+
case kind: NodeNumKind # the ``kind`` field is the discriminator
90+
of nkInt: intVal: int
91+
of nkFloat: floatVal: float
92+
ValueNode = object
93+
active: bool
94+
case kind: NodeNumKind # the ``kind`` field is the discriminator
95+
of nkInt: intVal: int
96+
of nkFloat: floatVal: float
97+
98+
var
99+
refNode1 = RefNode(kind: nkFloat, active: true, floatVal: 3.14)
100+
refNode2 = RefNode(kind: nkInt, active: false, intVal: 42)
101+
102+
valueNode1 = ValueNode(kind: nkFloat, active: true, floatVal: 3.14)
103+
valueNode2 = ValueNode(kind: nkInt, active: false, intVal: 42)
104+
105+
doAssert refNode1.toJson() == """{"active":true,"kind":"nkFloat","floatVal":3.14}"""
106+
doAssert refNode2.toJson() == """{"active":false,"kind":"nkInt","intVal":42}"""
107+
doAssert valueNode1.toJson() == """{"active":true,"kind":"nkFloat","floatVal":3.14}"""
108+
doAssert valueNode2.toJson() == """{"active":false,"kind":"nkInt","intVal":42}"""

0 commit comments

Comments
 (0)