Skip to content

Commit 2bfb59d

Browse files
committed
add JsonAssertions
1 parent e84fa35 commit 2bfb59d

File tree

2 files changed

+377
-0
lines changed

2 files changed

+377
-0
lines changed

.github/workflows/test.yaml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Test
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
7+
permissions:
8+
id-token: write # This is required for requesting the JWT
9+
contents: write # This is required for actions/checkout
10+
11+
jobs:
12+
runTests:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
- uses: coursier/cache-action@v6
19+
- uses: VirtusLab/scala-cli-setup@main
20+
with:
21+
jvm: adoptium:1.21
22+
apps: scala
23+
power: true
24+
- name: Run tests
25+
run: scala test .
26+
27+

JsonAssertions.scala

+350
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
package org.encalmo.lambda
2+
3+
import ujson.*
4+
import munit.Location
5+
import scala.collection.immutable.ArraySeq
6+
7+
object JsonAssertions extends munit.Assertions {
8+
9+
extension (value: Value) {
10+
11+
final def getStringAt(
12+
path: String
13+
)(using Location): String =
14+
lookupJsonPath(value, path) match {
15+
case Str(value) => value
16+
case other =>
17+
fail(
18+
s"Expected JSON string at path [$path] but found $other"
19+
)
20+
}
21+
22+
final def getIntAt(
23+
path: String
24+
)(using Location): Int =
25+
lookupJsonPath(value, path) match {
26+
case Num(value) =>
27+
try { value.toInt }
28+
catch {
29+
case e: Exception =>
30+
fail(
31+
s"Expected integer at path [$path] but found $value"
32+
)
33+
}
34+
case other =>
35+
fail(
36+
s"Expected JSON number at path [$path] but found $other"
37+
)
38+
}
39+
40+
final def getStringOrIntAt(
41+
path: String
42+
)(using Location): Int =
43+
lookupJsonPath(value, path) match {
44+
case Str(value) =>
45+
try { value.toInt }
46+
catch {
47+
case e: Exception =>
48+
fail(
49+
s"Expected integer string at path [$path] but found $value"
50+
)
51+
}
52+
case Num(value) =>
53+
try { value.toInt }
54+
catch {
55+
case e: Exception =>
56+
fail(
57+
s"Expected integer number at path [$path] but found $value"
58+
)
59+
}
60+
case other =>
61+
fail(
62+
s"Expected JSON integer (string or number) at path [$path] but found $other"
63+
)
64+
}
65+
66+
final def getBooleanAt(
67+
path: String
68+
)(using Location): Boolean =
69+
lookupJsonPath(value, path) match {
70+
case Bool(value) => value
71+
case other =>
72+
fail(
73+
s"Expected JSON boolean at path [$path] but found $other"
74+
)
75+
}
76+
77+
final def getDoubleAt(
78+
path: String
79+
)(using Location): Double =
80+
lookupJsonPath(value, path) match {
81+
case Num(value) => value
82+
case other =>
83+
fail(
84+
s"Expected JSON number at path [$path] but found $other"
85+
)
86+
}
87+
88+
final def getBigDecimalAt(
89+
path: String
90+
)(using Location): BigDecimal =
91+
lookupJsonPath(value, path) match {
92+
case Num(value) => BigDecimal(value)
93+
case Str(value) => BigDecimal(value)
94+
case other =>
95+
fail(
96+
s"Expected JSON decimal at path [$path] but found $other"
97+
)
98+
}
99+
100+
final def assertObjectExistsAt(
101+
path: String
102+
)(using Location): Value =
103+
lookupJsonPath(value, path) match {
104+
case obj: Obj => value
105+
case other =>
106+
fail(
107+
s"Expected JSON object at path [$path] but found $other"
108+
)
109+
}
110+
111+
final def assertObjectAt(
112+
path: String
113+
)(check: Obj => Unit)(using Location): Value =
114+
lookupJsonPath(value, path) match {
115+
case obj: Obj => check(obj); value
116+
case other =>
117+
fail(
118+
s"Expected JSON object at path [$path] but found $other"
119+
)
120+
}
121+
122+
final def assertObjectIfExistsAt(
123+
path: String
124+
)(check: Obj => Unit)(using Location): Value =
125+
lookupJsonPath(value, path) match {
126+
case obj: Obj => check(obj); value
127+
case other => value
128+
}
129+
130+
final def assertStringExistsAt(path: String)(using
131+
Location
132+
): Value =
133+
value.getStringAt(path)
134+
value
135+
136+
final def assertStringAt(path: String)(expected: String)(using
137+
Location
138+
): Value =
139+
val arg = value.getStringAt(path)
140+
assertEquals(
141+
arg,
142+
expected,
143+
s"string `$expected` expected at path [$path] but got '$arg'"
144+
)
145+
value
146+
147+
final def assertStringAt(path: String)(check: String => Boolean)(using
148+
Location
149+
): Value =
150+
val arg = value.getStringAt(path)
151+
assert(
152+
check(arg),
153+
s"valid string expected at path [$path] but got '$arg'"
154+
)
155+
value
156+
157+
final def assertBooleanExistsAt(path: String)(using
158+
Location
159+
): Value =
160+
value.getBooleanAt(path)
161+
value
162+
163+
final def assertBooleanAt(path: String)(expected: Boolean)(using
164+
Location
165+
): Value =
166+
val arg = value.getBooleanAt(path)
167+
assertEquals(
168+
arg,
169+
expected,
170+
s"boolean `$expected` expected at at path [$path] but got '$arg'"
171+
)
172+
value
173+
174+
final def assertTrueAt(path: String)(using
175+
Location
176+
): Value =
177+
val arg = value.getBooleanAt(path)
178+
assertEquals(
179+
arg,
180+
true,
181+
s"boolean `true` expected at at path [$path] but got '$arg'"
182+
)
183+
value
184+
185+
final def assertFalseAt(path: String)(using
186+
Location
187+
): Value =
188+
val arg = value.getBooleanAt(path)
189+
assertEquals(
190+
arg,
191+
false,
192+
s"boolean `false` expected at at path [$path] but got '$arg'"
193+
)
194+
value
195+
196+
final def assertIntExistsAt(path: String)(using
197+
Location
198+
): Value =
199+
value.getIntAt(path)
200+
value
201+
202+
final def assertIntAt(path: String)(expected: Int)(using
203+
Location
204+
): Value =
205+
val arg = value.getIntAt(path)
206+
assertEquals(
207+
arg,
208+
expected,
209+
s"integer `$expected` expected at path [$path] but got '$arg'"
210+
)
211+
value
212+
213+
final def assertIntStringAt(path: String)(expected: Int)(using
214+
Location
215+
): Value =
216+
val arg = value.getStringOrIntAt(path)
217+
assertEquals(
218+
arg,
219+
expected,
220+
s"integer `$expected` expected at path [$path] but got '$arg'"
221+
)
222+
value
223+
224+
final def assertDoubleExistsAt(path: String)(using
225+
Location
226+
): Value =
227+
value.getDoubleAt(path)
228+
value
229+
230+
final def assertDoubleAt(path: String)(expected: Double)(using
231+
Location
232+
): Value =
233+
val arg = value.getDoubleAt(path)
234+
assertEquals(
235+
arg,
236+
expected,
237+
s"number `$expected` expected at path [$path] but got '$arg'"
238+
)
239+
value
240+
241+
final def assertBigDecimalExistsAt(path: String)(using
242+
Location
243+
): Value =
244+
value.getBigDecimalAt(path)
245+
value
246+
247+
final def assertBigDecimalAt(path: String)(expected: BigDecimal)(using
248+
Location
249+
): Value =
250+
val arg = value.getBigDecimalAt(path)
251+
assertEquals(
252+
arg,
253+
expected,
254+
s"decimal `$expected` expected at path [$path] but got '$arg'"
255+
)
256+
value
257+
258+
final def assertBigDecimalAt2(path: String)(check: BigDecimal => Boolean)(using
259+
Location
260+
): Value =
261+
val arg = value.getBigDecimalAt(path)
262+
assert(
263+
check(arg),
264+
s"valid decimal expected at path [$path] but got '$arg'"
265+
)
266+
value
267+
268+
private def lookupJsonPath(
269+
current: Value,
270+
path: String
271+
)(using Location): Value =
272+
lookupJsonPath(
273+
current,
274+
ArraySeq.unsafeWrapArray(path.split("\\.")),
275+
Seq.empty
276+
)
277+
278+
private def lookupJsonPath(
279+
current: Value,
280+
path: Seq[String],
281+
breadcrumbs: Seq[String]
282+
)(using Location): Value = {
283+
current match {
284+
case Obj(map) =>
285+
path.headOption match {
286+
case Some(name) =>
287+
if (map.contains(name)) {
288+
val nestedValue = map(name)
289+
if (path.tail.isEmpty)
290+
nestedValue
291+
else
292+
lookupJsonPath(nestedValue, path.tail, breadcrumbs :+ name)
293+
} else
294+
fail(
295+
s"JSON object${
296+
if (breadcrumbs.isEmpty) ""
297+
else s" at ${breadcrumbs.mkString(".")}"
298+
} does NOT contain property [$name], see:\n${ujson
299+
.write(current, indent = 2)}"
300+
)
301+
302+
case None =>
303+
current
304+
}
305+
306+
case other =>
307+
fail(
308+
s"Expected JSON object at path ${breadcrumbs.mkString(".")} but found $other"
309+
)
310+
}
311+
}
312+
313+
private def lookupMaybeJsonPath(
314+
current: Value,
315+
path: String
316+
)(using Location): Option[Value] =
317+
lookupMaybeJsonPath(
318+
current,
319+
ArraySeq.unsafeWrapArray(path.split("\\.")),
320+
Seq.empty
321+
)
322+
323+
private def lookupMaybeJsonPath(
324+
current: Value,
325+
path: Seq[String],
326+
breadcrumbs: Seq[String]
327+
)(using Location): Option[Value] = {
328+
current match {
329+
case Obj(map) =>
330+
path.headOption match {
331+
case Some(name) =>
332+
if (map.contains(name)) {
333+
val nestedValue = map(name)
334+
if (path.tail.isEmpty)
335+
Some(nestedValue)
336+
else
337+
lookupMaybeJsonPath(nestedValue, path.tail, breadcrumbs :+ name)
338+
} else
339+
None
340+
341+
case None =>
342+
Some(current)
343+
}
344+
345+
case other =>
346+
None
347+
}
348+
}
349+
}
350+
}

0 commit comments

Comments
 (0)