Skip to content

Commit 0dbc3c1

Browse files
author
Oleg
committed
Add if-then-else assertion
1 parent fec7851 commit 0dbc3c1

File tree

3 files changed

+219
-0
lines changed

3 files changed

+219
-0
lines changed

src/commonMain/kotlin/smirnov/oleg/json/schema/internal/SchemaLoader.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import smirnov.oleg.json.schema.internal.factories.array.ItemsAssertionFactory
1313
import smirnov.oleg.json.schema.internal.factories.array.MaxItemsAssertionFactory
1414
import smirnov.oleg.json.schema.internal.factories.array.MinItemsAssertionFactory
1515
import smirnov.oleg.json.schema.internal.factories.array.UniqueItemsAssertionFactory
16+
import smirnov.oleg.json.schema.internal.factories.condition.IfThenElseAssertionFactory
1617
import smirnov.oleg.json.schema.internal.factories.general.ConstAssertionFactory
1718
import smirnov.oleg.json.schema.internal.factories.general.EnumAssertionFactory
1819
import smirnov.oleg.json.schema.internal.factories.general.TypeAssertionFactory
@@ -54,6 +55,7 @@ private val factories: List<AssertionFactory> = listOf(
5455
PropertiesAssertionFactory,
5556
PropertyNamesAssertionFactory,
5657
DependenciesAssertionFactory,
58+
IfThenElseAssertionFactory,
5759
)
5860

5961
class SchemaLoader {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package smirnov.oleg.json.schema.internal.factories.condition
2+
3+
import kotlinx.serialization.json.JsonElement
4+
import kotlinx.serialization.json.JsonObject
5+
import smirnov.oleg.json.schema.ErrorCollector
6+
import smirnov.oleg.json.schema.ErrorCollector.Companion
7+
import smirnov.oleg.json.schema.internal.AssertionContext
8+
import smirnov.oleg.json.schema.internal.AssertionFactory
9+
import smirnov.oleg.json.schema.internal.JsonSchemaAssertion
10+
import smirnov.oleg.json.schema.internal.LoadingContext
11+
12+
internal object IfThenElseAssertionFactory : AssertionFactory {
13+
private const val ifProperty: String = "if"
14+
private const val thenProperty: String = "then"
15+
private const val elseProperty: String = "else"
16+
17+
override fun isApplicable(element: JsonElement): Boolean {
18+
return element is JsonObject && element.run {
19+
// there is not point to extract the assertion when only `if` is present
20+
containsKey(ifProperty) && (containsKey(thenProperty) || containsKey(elseProperty))
21+
}
22+
}
23+
24+
override fun create(element: JsonElement, context: LoadingContext): JsonSchemaAssertion {
25+
require(element is JsonObject) { "cannot extract properties from ${element::class.simpleName}" }
26+
val ifElement = requireNotNull(element[ifProperty]) { "no property $ifProperty found in element $element" }
27+
require(context.isJsonSchema(ifElement)) { "$ifProperty must be a valid JSON schema" }
28+
val ifAssertion: JsonSchemaAssertion = context.at(ifProperty).schemaFrom(ifElement)
29+
30+
val thenAssertion: JsonSchemaAssertion? = loadOptionalAssertion(element, thenProperty, context)
31+
val elseAssertion: JsonSchemaAssertion? = loadOptionalAssertion(element, elseProperty, context)
32+
33+
require(thenAssertion != null || elseAssertion != null) {
34+
"either $thenProperty or $elseProperty must be specified"
35+
}
36+
return IfThenElseAssertion(ifAssertion, thenAssertion, elseAssertion)
37+
}
38+
39+
private fun loadOptionalAssertion(
40+
jsonObject: JsonObject,
41+
property: String,
42+
context: LoadingContext
43+
): JsonSchemaAssertion? {
44+
val element = jsonObject[property] ?: return null
45+
require(context.isJsonSchema(element)) { "$property must be a valid JSON schema" }
46+
return context.at(property).schemaFrom(element)
47+
}
48+
}
49+
50+
private class IfThenElseAssertion(
51+
private val ifAssertion: JsonSchemaAssertion,
52+
private val thenAssertion: JsonSchemaAssertion?,
53+
private val elseAssertion: JsonSchemaAssertion?,
54+
) : JsonSchemaAssertion {
55+
override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean {
56+
return if (ifAssertion.validate(element, context, ErrorCollector.EMPTY)) {
57+
thenAssertion?.validate(element, context, errorCollector) ?: true
58+
} else {
59+
elseAssertion?.validate(element, context, errorCollector) ?: true
60+
}
61+
}
62+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package smirnov.oleg.json.schema.assertions.condition
2+
3+
import io.kotest.assertions.asClue
4+
import io.kotest.core.spec.style.FunSpec
5+
import io.kotest.matchers.collections.shouldContainExactly
6+
import io.kotest.matchers.collections.shouldHaveSize
7+
import io.kotest.matchers.shouldBe
8+
import kotlinx.serialization.json.JsonPrimitive
9+
import kotlinx.serialization.json.buildJsonObject
10+
import smirnov.oleg.json.pointer.JsonPointer
11+
import smirnov.oleg.json.schema.JsonSchema
12+
import smirnov.oleg.json.schema.KEY
13+
import smirnov.oleg.json.schema.ValidationError
14+
15+
@Suppress("unused")
16+
class JsonSchemaIfThenElseValidationTest : FunSpec() {
17+
init {
18+
JsonSchema.fromDescription(
19+
"""
20+
{
21+
"${KEY}schema": "http://json-schema.org/draft-07/schema#",
22+
"if": {
23+
"type": "object"
24+
},
25+
"then": {
26+
"required": ["f1","f2"]
27+
},
28+
"else": {
29+
"type": "string"
30+
}
31+
}
32+
""".trimIndent()
33+
).also { schema ->
34+
test("when matches `if` passes `then` validation") {
35+
val jsonObject = buildJsonObject {
36+
put("f1", JsonPrimitive(42))
37+
put("f2", JsonPrimitive(43))
38+
}
39+
40+
val errors = mutableListOf<ValidationError>()
41+
val valid = schema.validate(jsonObject, errors::add)
42+
43+
jsonObject.asClue {
44+
valid shouldBe true
45+
errors shouldHaveSize 0
46+
}
47+
}
48+
49+
test("when matches `if` fails `then` validation") {
50+
val jsonObject = buildJsonObject {
51+
put("f1", JsonPrimitive(42))
52+
put("f3", JsonPrimitive(43))
53+
}
54+
55+
val errors = mutableListOf<ValidationError>()
56+
val valid = schema.validate(jsonObject, errors::add)
57+
58+
jsonObject.asClue {
59+
valid shouldBe false
60+
errors.shouldContainExactly(
61+
ValidationError(
62+
schemaPath = JsonPointer("/then/required"),
63+
objectPath = JsonPointer.ROOT,
64+
message = "missing required properties: [f2]",
65+
)
66+
)
67+
}
68+
}
69+
70+
test("when does not matches `if` passes `else` validation") {
71+
val element = JsonPrimitive("test")
72+
73+
val errors = mutableListOf<ValidationError>()
74+
val valid = schema.validate(element, errors::add)
75+
76+
element.asClue {
77+
valid shouldBe true
78+
errors shouldHaveSize 0
79+
}
80+
}
81+
82+
test("when does not matches `if` fails `else` validation") {
83+
val element = JsonPrimitive(42)
84+
85+
val errors = mutableListOf<ValidationError>()
86+
val valid = schema.validate(element, errors::add)
87+
88+
element.asClue {
89+
valid shouldBe false
90+
errors.shouldContainExactly(
91+
ValidationError(
92+
schemaPath = JsonPointer("/else/type"),
93+
objectPath = JsonPointer.ROOT,
94+
message = "element is not a string",
95+
)
96+
)
97+
}
98+
}
99+
}
100+
101+
JsonSchema.fromDescription(
102+
"""
103+
{
104+
"${KEY}schema": "http://json-schema.org/draft-07/schema#",
105+
"if": {
106+
"type": "object"
107+
},
108+
"then": {
109+
"required": ["f1","f2"]
110+
}
111+
}
112+
""".trimIndent()
113+
).also { schema ->
114+
test("when does not matches `if` and `else` is missing nothing is checked") {
115+
val element = JsonPrimitive("test")
116+
117+
val errors = mutableListOf<ValidationError>()
118+
val valid = schema.validate(element, errors::add)
119+
120+
element.asClue {
121+
valid shouldBe true
122+
errors shouldHaveSize 0
123+
}
124+
}
125+
}
126+
127+
JsonSchema.fromDescription(
128+
"""
129+
{
130+
"${KEY}schema": "http://json-schema.org/draft-07/schema#",
131+
"if": {
132+
"type": "object"
133+
},
134+
"else": {
135+
"type": "string"
136+
}
137+
}
138+
""".trimIndent()
139+
).also { schema ->
140+
test("when matches `if` and `then` is missing nothing is checked") {
141+
val element = buildJsonObject {
142+
put("f1", JsonPrimitive(42))
143+
}
144+
145+
val errors = mutableListOf<ValidationError>()
146+
val valid = schema.validate(element, errors::add)
147+
148+
element.asClue {
149+
valid shouldBe true
150+
errors shouldHaveSize 0
151+
}
152+
}
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)