Skip to content

Commit d3cb7d1

Browse files
committed
First real commit.
1 parent 5c7d3ec commit d3cb7d1

File tree

6 files changed

+439
-0
lines changed

6 files changed

+439
-0
lines changed

jsony.nimble

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version = "0.0.0"
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.2.2"

src/jsony.nim

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import macros, strutils
2+
3+
type JsonError = object of ValueError
4+
5+
const whiteSpace = {' ', '\n', '\t', '\r'}
6+
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+
11+
12+
template error(msg: string) =
13+
## Short cut to raise an exception.
14+
raise newException(JsonError, msg)
15+
16+
proc eatSpace(s: string, i: var int) =
17+
## Will consume white space.
18+
while i < s.len:
19+
let c = s[i]
20+
if c in whiteSpace:
21+
discard
22+
else:
23+
return
24+
inc i
25+
26+
proc eat(s: string, i: var int, c: char) =
27+
## Will consume space before and then the character `c`.
28+
## Will raise an exception if `c` is not found.
29+
eatSpace(s, i)
30+
if i >= s.len:
31+
error("Expected " & c & " but end reached.")
32+
if s[i] == c:
33+
inc i
34+
else:
35+
error("Expected " & c & " at offset " & $i & ".")
36+
37+
proc parseSymbol(s: string, i: var int): string =
38+
## Will read a symbol and return it.
39+
## Used for numbers and booleans.
40+
eatSpace(s, i)
41+
var j = i
42+
while i < s.len:
43+
case s[i]
44+
of ',', '}', ']', whiteSpace:
45+
break
46+
else:
47+
discard
48+
inc i
49+
return s[j ..< i]
50+
51+
proc parseJson(s: string, i: var int, v: var bool) =
52+
## Will parse boolean true or false.
53+
case parseSymbol(s, i)
54+
of "true":
55+
v = true
56+
of "false":
57+
v = false
58+
else:
59+
error("Boolean true or false expected at offset " & $i & ".")
60+
61+
proc parseJson(s: string, i: var int, v: var SomeInteger) =
62+
## Will parse int8, uint8, int16, uint16, int32, uint32, int64, uint64 or
63+
## just int.
64+
v = type(v)(parseInt(parseSymbol(s, i)))
65+
66+
proc parseJson(s: string, i: var int, v: var SomeFloat) =
67+
## Will parse float32 and float64.
68+
v = type(v)(parseFloat(parseSymbol(s, i)))
69+
70+
proc parseJson(s: string, i: var int, v: var string) =
71+
## Parse string.
72+
eat(s, i, '"')
73+
var j = i
74+
while i < s.len:
75+
case s[i]
76+
of '"':
77+
v = s[j ..< i]
78+
break
79+
else:
80+
discard
81+
inc i
82+
eat(s, i, '"')
83+
84+
proc parseJson[T](s: string, i: var int, v: var seq[T]) =
85+
## Parse seq.
86+
eat(s, i, '[')
87+
while i < s.len:
88+
eatSpace(s, i)
89+
if s[i] == ']':
90+
break
91+
var element: T
92+
parseJson(s, i, element)
93+
v.add(element)
94+
eatSpace(s, i)
95+
if s[i] == ',':
96+
inc i
97+
else:
98+
break
99+
eat(s, i, ']')
100+
101+
proc skipValue(s: string, i: var int) =
102+
## Used to skip values of extra fields.
103+
eatSpace(s, i)
104+
if s[i] == '{':
105+
eat(s, i, '{')
106+
while i < s.len:
107+
eatSpace(s, i)
108+
if s[i] == '}':
109+
break
110+
skipValue(s, i)
111+
eat(s, i, ':')
112+
skipValue(s, i)
113+
eatSpace(s, i)
114+
if s[i] == ',':
115+
inc i
116+
eat(s, i, '}')
117+
elif s[i] == '[':
118+
eat(s, i, '[')
119+
while i < s.len:
120+
eatSpace(s, i)
121+
if s[i] == ']':
122+
break
123+
skipValue(s, i)
124+
eatSpace(s, i)
125+
if s[i] == ',':
126+
inc i
127+
eat(s, i, ']')
128+
elif s[i] == '"':
129+
eat(s, i, '"')
130+
while i < s.len:
131+
inc i
132+
if s[i] == '"':
133+
break
134+
eat(s, i, '"')
135+
else:
136+
discard parseSymbol(s, i)
137+
138+
proc camelCase(s: string): string =
139+
return s
140+
141+
proc snakeCase(s: string): string =
142+
var prevCap = false
143+
for i, c in s:
144+
if c in {'A'..'Z'}:
145+
if result.len > 0 and result[^1] != '_' and not prevCap:
146+
result.add '_'
147+
prevCap = true
148+
result.add c.toLowerAscii()
149+
else:
150+
prevCap = false
151+
result.add c
152+
153+
macro fieldsMacro(v: typed, key: string) =
154+
## Crates a parser for object fields.
155+
result = nnkCaseStmt.newTree(ident"key")
156+
# Get implementation of v's type.
157+
var impl = getTypeImpl(v)
158+
# Walk refs and pointers to the real type.
159+
while impl.kind in {nnkRefTy, nnkPtrTy}:
160+
impl = getTypeImpl(impl[0])
161+
# For each field in the type:
162+
var used: seq[string]
163+
for f in impl[2]:
164+
# Get fields name and type information.
165+
let fieldName = f[0]
166+
let filedNameStr = fieldName.strVal()
167+
let filedType = f[1]
168+
# Output a name/type checker for it:
169+
for fn in [camelCase, snakeCase]:
170+
let caseName = fn(filedNameStr)
171+
if caseName in used:
172+
continue
173+
used.add(caseName)
174+
let ofClause = nnkOfBranch.newTree(newLit(caseName))
175+
let body = quote:
176+
var value: `filedType`
177+
parseJson(s, i, value)
178+
v.`fieldName` = value
179+
ofClause.add(body)
180+
result.add(ofClause)
181+
let ofElseClause = nnkElse.newTree()
182+
let body = quote:
183+
skipValue(s, i)
184+
ofElseClause.add(body)
185+
result.add(ofElseClause)
186+
187+
proc parseJson[T:enum](s: string, i: var int, v: var T) =
188+
eatSpace(s, i)
189+
var strV: string
190+
if s[i] == '"':
191+
parseJson(s, i, strV)
192+
when compiles(enumHook[T](strV)):
193+
v = enumHook[T](strV)
194+
else:
195+
v = parseEnum[T](strV)
196+
else:
197+
strV = parseSymbol(s, i)
198+
v = T(parseInt(strV))
199+
200+
proc parseJson[T:object|ref object](s: string, i: var int, v: var T) =
201+
## Parse an object.
202+
eat(s, i, '{')
203+
when compiles(newHook(v)):
204+
newHook(v)
205+
elif compiles(new(v)):
206+
new(v)
207+
while i < s.len:
208+
eatSpace(s, i)
209+
if s[i] == '}':
210+
break
211+
var key: string
212+
parseJson(s, i, key)
213+
eat(s, i, ':')
214+
fieldsMacro(v, key)
215+
eatSpace(s, i)
216+
if s[i] == ',':
217+
inc i
218+
else:
219+
break
220+
eat(s, i, '}')
221+
222+
proc fromJson*[T](s: string): T =
223+
## Takes json and outputs the object it represents.
224+
## * Create little intermediate values.
225+
## * Extra json fields are ignored.
226+
## * Missing json fields keep their default values.
227+
## * `proc newHook(foo: var ...)` Can be used to populate default values.
228+
229+
var i = 0
230+
parseJson(s, i, result)

tests/config.nims

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--path:"../src"

tests/test_basic.nim

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import jsony
2+
3+
block:
4+
doAssert fromJson[bool]("true") == true
5+
doAssert fromJson[bool]("false") == false
6+
doAssert fromJson[bool](" true ") == true
7+
doAssert fromJson[bool](" false ") == false
8+
9+
doAssert fromJson[int]("1") == 1
10+
doAssert fromJson[int]("12") == 12
11+
doAssert fromJson[int](" 123 ") == 123
12+
13+
doAssert fromJson[int8](" 123 ") == 123
14+
doAssert fromJson[uint8](" 123 ") == 123
15+
doAssert fromJson[int16](" 123 ") == 123
16+
doAssert fromJson[uint16](" 123 ") == 123
17+
doAssert fromJson[int32](" 123 ") == 123
18+
doAssert fromJson[uint32](" 123 ") == 123
19+
doAssert fromJson[int64](" 123 ") == 123
20+
doAssert fromJson[uint64](" 123 ") == 123
21+
22+
doAssert fromJson[int8](" -99 ") == -99
23+
doAssert fromJson[int16](" -99 ") == -99
24+
doAssert fromJson[int32](" -99 ") == -99
25+
doAssert fromJson[int64](" -99 ") == -99
26+
27+
doAssert fromJson[float32](" 1.34E3 ") == 1.34E3
28+
doAssert fromJson[float32](" 1.34E3 ") == 1.34E3
29+
doAssert fromJson[float64](" -1.34E3 ") == -1.34E3
30+
doAssert fromJson[float64](" -1.34E3 ") == -1.34E3
31+
32+
block:
33+
doAssert fromJson[seq[int]]("[1, 2, 3]") == @[1, 2, 3]
34+
doAssert fromJson[seq[string]]("""["hi", "bye", "maybe"]""") ==
35+
@["hi", "bye", "maybe"]
36+
doAssert fromJson[seq[seq[string]]]("""[["hi", "bye"], ["maybe"], []]""") ==
37+
@[@["hi", "bye"], @["maybe"], @[]]

tests/test_enums.nim

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import jsony
2+
3+
type Color = enum
4+
cRed
5+
cBlue
6+
cGreen
7+
8+
doAssert fromJson[Color]("0") == cRed
9+
doAssert fromJson[Color]("1") == cBlue
10+
doAssert fromJson[Color]("2") == cGreen
11+
12+
doAssert fromJson[Color](""" "cRed" """) == cRed
13+
doAssert fromJson[Color](""" "cBlue" """) == cBlue
14+
doAssert fromJson[Color](""" "cGreen" """) == cGreen
15+
16+
type Color2 = enum
17+
c2Red
18+
c2Blue
19+
c2Green
20+
21+
proc enumHook[Color2](v: string): Color2 =
22+
case v:
23+
of "RED": c2Red
24+
of "BLUE": c2Blue
25+
of "GREEN": c2Green
26+
else: c2Red
27+
28+
doAssert fromJson[Color2](""" "RED" """) == c2Red
29+
doAssert fromJson[Color2](""" "BLUE" """) == c2Blue
30+
doAssert fromJson[Color2](""" "GREEN" """) == c2Green

0 commit comments

Comments
 (0)