Skip to content

Commit 71a83a7

Browse files
committed
Add support for parseHook.
1 parent 0181d2a commit 71a83a7

File tree

3 files changed

+151
-56
lines changed

3 files changed

+151
-56
lines changed

README.md

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# JSONy - A loose, direct to object json parser with hooks.
22

3-
Real world json is never what you want. It might have extra fields that you don't care about. It might have missing fields signifying default values. It might change or grow new fields at any moment. Json might use camelCase or snake_case. It might use inconsistent naming.
3+
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.
44

55
With this library you can parse json your way, from the mess you get to the objects you want.
66

7-
## No garbage.
7+
## Fast/No garbage.
88

9-
Current standard module first parses json into JsonNodes and then turns the JsonNodes into objects you want. This is slower and creates unnecessary work for the garbage collector. This library skips the JsonNodes and creates the objects you want directly.
9+
Current standard module first parses 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.
1010

1111
## Can parse most object types:
1212

@@ -16,10 +16,11 @@ Current standard module first parses json into JsonNodes and then turns the Json
1616
* tuples
1717
* seq and arrays
1818
* tables
19+
* and `parseHook()` enables you to parse any type!
1920

2021
## Not strict.
2122

22-
Extra json fields are ignored and missing json fields keep their default values. Json is never exactly what you want.
23+
Extra json fields are ignored and missing json fields keep their default values.
2324

2425
```nim
2526
type Entry1 = object
@@ -31,7 +32,7 @@ doAssert v.color == ""
3132

3233
## Snake_case or CamelCase
3334

34-
Nim usually uses camalCase for its variables, while a bunch of json in the wild uses snake_case. This library will convert snake_case to camalCase for you when reading json.
35+
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.
3536

3637
```nim
3738
type Entry4 = object
@@ -48,15 +49,15 @@ doAssert v.colorBlend == "red"
4849

4950
### `proc newHook()` Can be used to populate default values.
5051

51-
Some times absence of a field means it should have a default value. Normally hits would just be Nim's default value for the variable type. But with the newHook() you can setup the object with defaults before the main parsing happens.
52+
Some times 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 with the newHook() you can setup the object with defaults before the main parsing happens.
5253

5354
```nim
5455
type
5556
Foo5 = object
5657
visible: string
5758
id: string
5859
proc newHook(foo: var Foo5) =
59-
# Populates the object before its deserialized.
60+
# Populates the object before its fully deserialized.
6061
foo.visible = "yes"
6162
6263
var s = """{"id":"123"}"""
@@ -102,3 +103,50 @@ proc renameHook(v: var Node, fieldName: var string) =
102103
var node = fromJson[Node]("""{"type":"root"}""")
103104
doAssert node.kind == "root"
104105
```
106+
107+
### `proc parseHook()` Can be used to do anything.
108+
109+
Json can't store dates, so they are usually stored as strings. You can use
110+
`parseHook()` to override default parsing and parse date times as a string:
111+
112+
```nim
113+
proc parseHook(s:string, i:var int, v: var DateTime) =
114+
var str: string
115+
parseHook(s, i, str)
116+
v = parse(str, "yyyy-MM-dd hh:mm:ss")
117+
118+
var dt = fromJson[DateTime](""" "2020-01-01 00:00:00" """)
119+
```
120+
121+
Some times json gives you a object of entries with their id as keys, but you might want it as a sequence with ids inside the objects, again you can do anything with `parseHook()`:
122+
123+
```nim
124+
type Entry = object
125+
id: string
126+
count: int
127+
filled: int
128+
129+
let data = """{
130+
"1": {"count":12, "filled": 11},
131+
"2": {"count":66, "filled": 0},
132+
"3": {"count":99, "filled": 99}
133+
}"""
134+
135+
proc parseHook(s:string, i:var int, v: var seq[Entry]) =
136+
var table: Table[string, Entry]
137+
parseHook(s, i, table)
138+
for k, entry in table.mpairs:
139+
entry.id = k
140+
v.add(entry)
141+
142+
let s = fromJson[seq[Entry]](data)
143+
```
144+
145+
Gives us:
146+
```
147+
@[
148+
(id: "1", count: 12, filled: 11),
149+
(id: "2", count: 66, filled: 0),
150+
(id: "3", count: 99, filled: 99)
151+
]"""
152+
```

src/jsony.nim

Lines changed: 48 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ type JsonError = object of ValueError
44

55
const whiteSpace = {' ', '\n', '\t', '\r'}
66

7-
proc parseJson[T](s: string, i: var int, v: var seq[T])
8-
proc parseJson[T:enum](s: string, i: var int, v: var T)
9-
proc parseJson[T:object|ref object](s: string, i: var int, v: var T)
10-
proc parseJson[T](s: string, i: var int, v: var Table[string, T])
11-
proc parseJson[T:tuple](s: string, i: var int, v: var T)
12-
proc parseJson[T:array](s: string, i: var int, v: var T)
7+
proc parseHook*[T](s: string, i: var int, v: var seq[T])
8+
proc parseHook*[T:enum](s: string, i: var int, v: var T)
9+
proc parseHook*[T:object|ref object](s: string, i: var int, v: var T)
10+
proc parseHook*[T](s: string, i: var int, v: var Table[string, T])
11+
proc parseHook*[T:tuple](s: string, i: var int, v: var T)
12+
proc parseHook*[T:array](s: string, i: var int, v: var T)
1313

1414
template error(msg: string, i: int) =
1515
## Short cut to raise an exception.
1616
raise newException(JsonError, msg)
1717

18-
proc eatSpace(s: string, i: var int) =
18+
proc eatSpace*(s: string, i: var int) =
1919
## Will consume white space.
2020
while i < s.len:
2121
let c = s[i]
@@ -25,7 +25,7 @@ proc eatSpace(s: string, i: var int) =
2525
return
2626
inc i
2727

28-
proc eat(s: string, i: var int, c: char) =
28+
proc eatChar*(s: string, i: var int, c: char) =
2929
## Will consume space before and then the character `c`.
3030
## Will raise an exception if `c` is not found.
3131
eatSpace(s, i)
@@ -36,7 +36,7 @@ proc eat(s: string, i: var int, c: char) =
3636
else:
3737
error("Expected " & c & " at offset.", i)
3838

39-
proc parseSymbol(s: string, i: var int): string =
39+
proc parseSymbol*(s: string, i: var int): string =
4040
## Will read a symbol and return it.
4141
## Used for numbers and booleans.
4242
eatSpace(s, i)
@@ -50,7 +50,7 @@ proc parseSymbol(s: string, i: var int): string =
5050
inc i
5151
return s[j ..< i]
5252

53-
proc parseJson(s: string, i: var int, v: var bool) =
53+
proc parseHook*(s: string, i: var int, v: var bool) =
5454
## Will parse boolean true or false.
5555
case parseSymbol(s, i)
5656
of "true":
@@ -60,16 +60,16 @@ proc parseJson(s: string, i: var int, v: var bool) =
6060
else:
6161
error("Boolean true or false expected.", i)
6262

63-
proc parseJson(s: string, i: var int, v: var SomeInteger) =
63+
proc parseHook*(s: string, i: var int, v: var SomeInteger) =
6464
## Will parse int8, uint8, int16, uint16, int32, uint32, int64, uint64 or
6565
## just int.
6666
v = type(v)(parseInt(parseSymbol(s, i)))
6767

68-
proc parseJson(s: string, i: var int, v: var SomeFloat) =
68+
proc parseHook*(s: string, i: var int, v: var SomeFloat) =
6969
## Will parse float32 and float64.
7070
v = type(v)(parseFloat(parseSymbol(s, i)))
7171

72-
proc parseJson(s: string, i: var int, v: var string) =
72+
proc parseHook*(s: string, i: var int, v: var string) =
7373
## Parse string.
7474
#echo "S:", s[i .. min(i + 80, s.len-1)]
7575
eatSpace(s, i)
@@ -79,8 +79,7 @@ proc parseJson(s: string, i: var int, v: var string) =
7979
return
8080
else:
8181
error("Expected \" or null at offset.", i)
82-
eat(s, i, '"')
83-
var j = i
82+
eatChar(s, i, '"')
8483
while i < s.len:
8584
let c = s[i]
8685
case c
@@ -106,70 +105,70 @@ proc parseJson(s: string, i: var int, v: var string) =
106105
else:
107106
v.add(c)
108107
inc i
109-
eat(s, i, '"')
108+
eatChar(s, i, '"')
110109

111-
proc parseJson[T](s: string, i: var int, v: var seq[T]) =
110+
proc parseHook*[T](s: string, i: var int, v: var seq[T]) =
112111
## Parse seq.
113-
eat(s, i, '[')
112+
eatChar(s, i, '[')
114113
while i < s.len:
115114
eatSpace(s, i)
116115
if s[i] == ']':
117116
break
118117
var element: T
119-
parseJson(s, i, element)
118+
parseHook(s, i, element)
120119
v.add(element)
121120
eatSpace(s, i)
122121
if s[i] == ',':
123122
inc i
124123
else:
125124
break
126-
eat(s, i, ']')
125+
eatChar(s, i, ']')
127126

128-
proc parseJson[T:tuple](s: string, i: var int, v: var T) =
127+
proc parseHook*[T:tuple](s: string, i: var int, v: var T) =
129128
eatSpace(s, i)
130129
var strV: string
131-
eat(s, i, '[')
130+
eatChar(s, i, '[')
132131
for name, value in v.fieldPairs:
133132
eatSpace(s, i)
134-
parseJson(s, i, value)
133+
parseHook(s, i, value)
135134
eatSpace(s, i)
136135
if s[i] == ',':
137136
inc i
138-
eat(s, i, ']')
137+
eatChar(s, i, ']')
139138

140-
proc parseJson[T:array](s: string, i: var int, v: var T) =
139+
proc parseHook*[T:array](s: string, i: var int, v: var T) =
141140
eatSpace(s, i)
142141
var strV: string
143-
eat(s, i, '[')
142+
eatChar(s, i, '[')
144143
for value in v.mitems:
145144
eatSpace(s, i)
146-
parseJson(s, i, value)
145+
parseHook(s, i, value)
147146
eatSpace(s, i)
148147
if s[i] == ',':
149148
inc i
150-
eat(s, i, ']')
149+
eatChar(s, i, ']')
151150

152151
proc skipValue(s: string, i: var int) =
153152
## Used to skip values of extra fields.
154153
#echo "Skip:", s[i .. min(i + 80, s.len-1)]
155154
eatSpace(s, i)
156155
if s[i] == '{':
157156
#echo "skip obj"
158-
eat(s, i, '{')
157+
eatChar(s, i, '{')
159158
while i < s.len:
160159
eatSpace(s, i)
161160
if s[i] == '}':
162161
break
163162
skipValue(s, i)
164-
eat(s, i, ':')
163+
eatChar(s, i, ':')
165164
skipValue(s, i)
166165
eatSpace(s, i)
167166
if s[i] == ',':
168167
inc i
169-
eat(s, i, '}')
168+
eatChar(s, i, '}')
170169
elif s[i] == '[':
171170
#echo "skip arr"
172-
eat(s, i, '[')
171+
eatChar(s, i, '[')
173172
while i < s.len:
174173
eatSpace(s, i)
175174
if s[i] == ']':
@@ -178,11 +177,11 @@ proc skipValue(s: string, i: var int) =
178177
eatSpace(s, i)
179178
if s[i] == ',':
180179
inc i
181-
eat(s, i, ']')
180+
eatChar(s, i, ']')
182181
elif s[i] == '"':
183182
#echo "skip str"
184183
var str: string
185-
parseJson(s, i, str)
184+
parseHook(s, i, str)
186185
else:
187186
#echo "skip sym"
188187
discard parseSymbol(s, i)
@@ -228,7 +227,7 @@ macro fieldsMacro(v: typed, key: string) =
228227
let ofClause = nnkOfBranch.newTree(newLit(caseName))
229228
let body = quote:
230229
var value: `filedType`
231-
parseJson(s, i, value)
230+
parseHook(s, i, value)
232231
v.`fieldName` = value
233232
ofClause.add(body)
234233
result.add(ofClause)
@@ -238,11 +237,11 @@ macro fieldsMacro(v: typed, key: string) =
238237
ofElseClause.add(body)
239238
result.add(ofElseClause)
240239

241-
proc parseJson[T:enum](s: string, i: var int, v: var T) =
240+
proc parseHook*[T:enum](s: string, i: var int, v: var T) =
242241
eatSpace(s, i)
243242
var strV: string
244243
if s[i] == '"':
245-
parseJson(s, i, strV)
244+
parseHook(s, i, strV)
246245
when compiles(enumHook(strV, v)):
247246
enumHook(strV, v)
248247
else:
@@ -251,7 +250,7 @@ proc parseJson[T:enum](s: string, i: var int, v: var T) =
251250
strV = parseSymbol(s, i)
252251
v = T(parseInt(strV))
253252

254-
proc parseJson[T:object|ref object](s: string, i: var int, v: var T) =
253+
proc parseHook*[T:object|ref object](s: string, i: var int, v: var T) =
255254
## Parse an object.
256255
eatSpace(s, i)
257256
if s[i] == 'n':
@@ -260,7 +259,7 @@ proc parseJson[T:object|ref object](s: string, i: var int, v: var T) =
260259
return
261260
else:
262261
error("Expected {} or null at offset.", i)
263-
eat(s, i, '{')
262+
eatChar(s, i, '{')
264263
when compiles(newHook(v)):
265264
newHook(v)
266265
elif compiles(new(v)):
@@ -270,8 +269,8 @@ proc parseJson[T:object|ref object](s: string, i: var int, v: var T) =
270269
if s[i] == '}':
271270
break
272271
var key: string
273-
parseJson(s, i, key)
274-
eat(s, i, ':')
272+
parseHook(s, i, key)
273+
eatChar(s, i, ':')
275274
when compiles(renameHook(v, key)):
276275
renameHook(v, key)
277276
fieldsMacro(v, key)
@@ -280,26 +279,26 @@ proc parseJson[T:object|ref object](s: string, i: var int, v: var T) =
280279
inc i
281280
else:
282281
break
283-
eat(s, i, '}')
282+
eatChar(s, i, '}')
284283

285-
proc parseJson[T](s: string, i: var int, v: var Table[string, T]) =
284+
proc parseHook*[T](s: string, i: var int, v: var Table[string, T]) =
286285
## Parse an object.
287-
eat(s, i, '{')
286+
eatChar(s, i, '{')
288287
while i < s.len:
289288
eatSpace(s, i)
290289
if s[i] == '}':
291290
break
292291
var key: string
293-
parseJson(s, i, key)
294-
eat(s, i, ':')
292+
parseHook(s, i, key)
293+
eatChar(s, i, ':')
295294
var element: T
296-
parseJson(s, i, element)
295+
parseHook(s, i, element)
297296
v[key] = element
298297
if s[i] == ',':
299298
inc i
300299
else:
301300
break
302-
eat(s, i, '}')
301+
eatChar(s, i, '}')
303302

304303
proc fromJson*[T](s: string): T =
305304
## Takes json and outputs the object it represents.
@@ -309,4 +308,4 @@ proc fromJson*[T](s: string): T =
309308
## * `proc newHook(foo: var ...)` Can be used to populate default values.
310309

311310
var i = 0
312-
parseJson(s, i, result)
311+
parseHook(s, i, result)

0 commit comments

Comments
 (0)