Skip to content

Commit e8bc497

Browse files
author
Oleg
committed
Add oneOf assertion
1 parent e4b9343 commit e8bc497

File tree

6 files changed

+158
-4
lines changed

6 files changed

+158
-4
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
@@ -19,6 +19,7 @@ import smirnov.oleg.json.schema.internal.factories.condition.AllOfAssertionFacto
1919
import smirnov.oleg.json.schema.internal.factories.condition.AnyOfAssertionFactory
2020
import smirnov.oleg.json.schema.internal.factories.condition.IfThenElseAssertionFactory
2121
import smirnov.oleg.json.schema.internal.factories.condition.NotAssertionFactory
22+
import smirnov.oleg.json.schema.internal.factories.condition.OneOfAssertionFactory
2223
import smirnov.oleg.json.schema.internal.factories.general.ConstAssertionFactory
2324
import smirnov.oleg.json.schema.internal.factories.general.EnumAssertionFactory
2425
import smirnov.oleg.json.schema.internal.factories.general.TypeAssertionFactory
@@ -63,6 +64,7 @@ private val factories: List<AssertionFactory> = listOf(
6364
IfThenElseAssertionFactory,
6465
AllOfAssertionFactory,
6566
AnyOfAssertionFactory,
67+
OneOfAssertionFactory,
6668
NotAssertionFactory,
6769
)
6870

src/commonMain/kotlin/smirnov/oleg/json/schema/internal/factories/condition/AbstractAssertionsCollectionFactory.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ internal abstract class AbstractAssertionsCollectionFactory(property: String) :
1616
context.at(index).schemaFrom(item)
1717
}
1818

19-
return createAssertion(assertions)
19+
return createAssertion(context, assertions)
2020
}
2121

22-
protected abstract fun createAssertion(assertions: List<JsonSchemaAssertion>): JsonSchemaAssertion
22+
protected abstract fun createAssertion(context: LoadingContext, assertions: List<JsonSchemaAssertion>): JsonSchemaAssertion
2323
}

src/commonMain/kotlin/smirnov/oleg/json/schema/internal/factories/condition/AllOfAssertion.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import kotlinx.serialization.json.JsonElement
44
import smirnov.oleg.json.schema.ErrorCollector
55
import smirnov.oleg.json.schema.internal.AssertionContext
66
import smirnov.oleg.json.schema.internal.JsonSchemaAssertion
7+
import smirnov.oleg.json.schema.internal.LoadingContext
78

89
@Suppress("unused")
910
internal object AllOfAssertionFactory : AbstractAssertionsCollectionFactory("allOf") {
10-
override fun createAssertion(assertions: List<JsonSchemaAssertion>): JsonSchemaAssertion = AllOfAssertion(assertions)
11+
override fun createAssertion(context: LoadingContext, assertions: List<JsonSchemaAssertion>): JsonSchemaAssertion =
12+
AllOfAssertion(assertions)
1113
}
1214

1315
private class AllOfAssertion(

src/commonMain/kotlin/smirnov/oleg/json/schema/internal/factories/condition/AnyOfAssertion.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import smirnov.oleg.json.schema.ErrorCollector
55
import smirnov.oleg.json.schema.ValidationError
66
import smirnov.oleg.json.schema.internal.AssertionContext
77
import smirnov.oleg.json.schema.internal.JsonSchemaAssertion
8+
import smirnov.oleg.json.schema.internal.LoadingContext
89

910
@Suppress("unused")
1011
internal object AnyOfAssertionFactory : AbstractAssertionsCollectionFactory("anyOf") {
11-
override fun createAssertion(assertions: List<JsonSchemaAssertion>): JsonSchemaAssertion = AnyOfAssertion(assertions)
12+
override fun createAssertion(context: LoadingContext, assertions: List<JsonSchemaAssertion>): JsonSchemaAssertion =
13+
AnyOfAssertion(assertions)
1214
}
1315

1416
private class AnyOfAssertion(
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package smirnov.oleg.json.schema.internal.factories.condition
2+
3+
import kotlinx.serialization.json.JsonElement
4+
import smirnov.oleg.json.pointer.JsonPointer
5+
import smirnov.oleg.json.schema.ErrorCollector
6+
import smirnov.oleg.json.schema.ValidationError
7+
import smirnov.oleg.json.schema.internal.AssertionContext
8+
import smirnov.oleg.json.schema.internal.JsonSchemaAssertion
9+
import smirnov.oleg.json.schema.internal.LoadingContext
10+
11+
internal object OneOfAssertionFactory : AbstractAssertionsCollectionFactory("oneOf") {
12+
override fun createAssertion(context: LoadingContext, assertions: List<JsonSchemaAssertion>): JsonSchemaAssertion =
13+
OneOfAssertion(context.schemaPath, assertions)
14+
}
15+
16+
private class OneOfAssertion(
17+
private val path: JsonPointer,
18+
private val assertions: List<JsonSchemaAssertion>,
19+
) : JsonSchemaAssertion {
20+
override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean {
21+
val suppressedErrors = mutableListOf<ValidationError>()
22+
val matched: MutableList<Int> = ArrayList(1)
23+
for ((index, assertion) in assertions.withIndex()) {
24+
val res = assertion.validate(element, context, suppressedErrors::add)
25+
if (res) {
26+
matched += index
27+
}
28+
}
29+
30+
when {
31+
matched.size > 1 -> errorCollector.onError(
32+
ValidationError(
33+
schemaPath = path,
34+
objectPath = context.objectPath,
35+
message = "element matches more than one JSON schema at indexes: $matched"
36+
)
37+
)
38+
matched.size == 0 -> suppressedErrors.forEach(errorCollector::onError)
39+
}
40+
41+
return matched.size == 1
42+
}
43+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package smirnov.oleg.json.schema.assertions.condition
2+
3+
import io.kotest.core.spec.style.FunSpec
4+
import io.kotest.matchers.collections.shouldContainExactly
5+
import io.kotest.matchers.collections.shouldHaveSize
6+
import io.kotest.matchers.shouldBe
7+
import kotlinx.serialization.json.JsonPrimitive
8+
import kotlinx.serialization.json.buildJsonObject
9+
import smirnov.oleg.json.pointer.JsonPointer
10+
import smirnov.oleg.json.schema.JsonSchema
11+
import smirnov.oleg.json.schema.ValidationError
12+
import smirnov.oleg.json.schema.base.KEY
13+
14+
@Suppress("unused")
15+
class JsonSchemaOneOfValidationTest : FunSpec() {
16+
init {
17+
testInvalidSchemaInArray("oneOf")
18+
JsonSchema.fromDescription(
19+
"""
20+
{
21+
"${KEY}schema": "http://json-schema.org/draft-07/schema#",
22+
"oneOf": [
23+
{
24+
"type": "string"
25+
},
26+
{
27+
"type": "number"
28+
},
29+
{
30+
"type": "boolean"
31+
}
32+
]
33+
}
34+
""".trimIndent()
35+
).apply {
36+
listOf(
37+
JsonPrimitive("test"),
38+
JsonPrimitive(42),
39+
JsonPrimitive(true),
40+
).forEach {
41+
test("element $it matches one of the schemas and passes validation") {
42+
val errors = mutableListOf<ValidationError>()
43+
val valid = validate(it, errors::add)
44+
45+
valid shouldBe true
46+
errors shouldHaveSize 0
47+
}
48+
}
49+
50+
test("element does not match any of the schemas and fails validation") {
51+
val errors = mutableListOf<ValidationError>()
52+
val valid = validate(buildJsonObject { }, errors::add)
53+
54+
valid shouldBe false
55+
errors.shouldContainExactly(
56+
ValidationError(
57+
schemaPath = JsonPointer("/oneOf/0/type"),
58+
objectPath = JsonPointer.ROOT,
59+
message = "element is not a string",
60+
),
61+
ValidationError(
62+
schemaPath = JsonPointer("/oneOf/1/type"),
63+
objectPath = JsonPointer.ROOT,
64+
message = "element is not a number",
65+
),
66+
ValidationError(
67+
schemaPath = JsonPointer("/oneOf/2/type"),
68+
objectPath = JsonPointer.ROOT,
69+
message = "element is not a boolean",
70+
),
71+
)
72+
}
73+
}
74+
75+
JsonSchema.fromDescription(
76+
"""
77+
{
78+
"${KEY}schema": "http://json-schema.org/draft-07/schema#",
79+
"oneOf": [
80+
{
81+
"type": "integer"
82+
},
83+
{
84+
"type": "number"
85+
}
86+
]
87+
}
88+
""".trimIndent()
89+
).apply {
90+
test("element matches more than one JSON schema and fails validation") {
91+
val errors = mutableListOf<ValidationError>()
92+
val valid = validate(JsonPrimitive(42), errors::add)
93+
94+
valid shouldBe false
95+
errors.shouldContainExactly(
96+
ValidationError(
97+
schemaPath = JsonPointer("/oneOf"),
98+
objectPath = JsonPointer.ROOT,
99+
message = "element matches more than one JSON schema at indexes: [0, 1]",
100+
),
101+
)
102+
}
103+
}
104+
}
105+
}

0 commit comments

Comments
 (0)