Skip to content

Commit 7156fb4

Browse files
author
Oleg
committed
Add required assertion
1 parent 536c687 commit 7156fb4

File tree

3 files changed

+148
-0
lines changed

3 files changed

+148
-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
@@ -23,6 +23,7 @@ import smirnov.oleg.json.schema.internal.factories.number.MinimumAssertionFactor
2323
import smirnov.oleg.json.schema.internal.factories.number.MultipleOfAssertionFactory
2424
import smirnov.oleg.json.schema.internal.factories.`object`.MaxPropertiesAssertionFactory
2525
import smirnov.oleg.json.schema.internal.factories.`object`.MinPropertiesAssertionFactory
26+
import smirnov.oleg.json.schema.internal.factories.`object`.RequiredAssertionFactory
2627
import smirnov.oleg.json.schema.internal.factories.string.MaxLengthAssertionFactory
2728
import smirnov.oleg.json.schema.internal.factories.string.MinLengthAssertionFactory
2829
import smirnov.oleg.json.schema.internal.factories.string.PatternAssertionFactory
@@ -46,6 +47,7 @@ private val factories: List<AssertionFactory> = listOf(
4647
ContainsAssertionFactory,
4748
MaxPropertiesAssertionFactory,
4849
MinPropertiesAssertionFactory,
50+
RequiredAssertionFactory,
4951
)
5052

5153
class SchemaLoader {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package smirnov.oleg.json.schema.internal.factories.`object`
2+
3+
import kotlinx.serialization.json.JsonArray
4+
import kotlinx.serialization.json.JsonElement
5+
import kotlinx.serialization.json.JsonObject
6+
import kotlinx.serialization.json.JsonPrimitive
7+
import smirnov.oleg.json.pointer.JsonPointer
8+
import smirnov.oleg.json.schema.ErrorCollector
9+
import smirnov.oleg.json.schema.ValidationError
10+
import smirnov.oleg.json.schema.internal.AssertionContext
11+
import smirnov.oleg.json.schema.internal.JsonSchemaAssertion
12+
import smirnov.oleg.json.schema.internal.LoadingContext
13+
import smirnov.oleg.json.schema.internal.TrueSchemaAssertion
14+
import smirnov.oleg.json.schema.internal.factories.AbstractAssertionFactory
15+
16+
@Suppress("unused")
17+
internal object RequiredAssertionFactory : AbstractAssertionFactory("required") {
18+
override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion {
19+
require(element is JsonArray) { "$property must be an array" }
20+
require(element.all { it is JsonPrimitive && it.isString }) { "$property must contain only strings" }
21+
val uniqueRequiredProperties = element.mapTo(linkedSetOf()) { (it as JsonPrimitive).content }
22+
require(uniqueRequiredProperties.size == element.size) { "$property must consist of unique elements" }
23+
return if (uniqueRequiredProperties.isEmpty()) {
24+
TrueSchemaAssertion
25+
} else {
26+
RequiredAssertion(context.schemaPath, uniqueRequiredProperties)
27+
}
28+
}
29+
}
30+
31+
private class RequiredAssertion(
32+
private val path: JsonPointer,
33+
private val requiredProperties: Set<String>,
34+
) : JsonSchemaAssertion {
35+
override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean {
36+
if (element !is JsonObject) {
37+
return true
38+
}
39+
val missingProperties = requiredProperties.asSequence()
40+
.filter { !element.containsKey(it) }
41+
.toSet()
42+
if (missingProperties.isEmpty()) {
43+
return true
44+
}
45+
errorCollector.onError(
46+
ValidationError(
47+
schemaPath = path,
48+
objectPath = context.objectPath,
49+
message = "missing required properties: $missingProperties",
50+
)
51+
)
52+
return false
53+
}
54+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package smirnov.oleg.json.schema.assertions.`object`
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.JsonNull
9+
import kotlinx.serialization.json.JsonPrimitive
10+
import kotlinx.serialization.json.buildJsonArray
11+
import kotlinx.serialization.json.buildJsonObject
12+
import smirnov.oleg.json.pointer.JsonPointer
13+
import smirnov.oleg.json.schema.JsonSchema
14+
import smirnov.oleg.json.schema.KEY
15+
import smirnov.oleg.json.schema.ValidationError
16+
17+
@Suppress("unused")
18+
class JsonSchemaRequiredValidationTest : FunSpec() {
19+
init {
20+
val schema = JsonSchema.fromDescription(
21+
"""
22+
{
23+
"${KEY}schema": "http://json-schema.org/draft-07/schema#",
24+
"required": ["prop1", "prop2"]
25+
}
26+
""".trimIndent()
27+
)
28+
29+
listOf(
30+
buildJsonObject {
31+
put("prop1", JsonPrimitive("a"))
32+
put("prop2", JsonPrimitive("b"))
33+
},
34+
buildJsonObject {
35+
put("prop1", JsonPrimitive("a"))
36+
put("prop2", JsonPrimitive("b"))
37+
put("prop3", JsonPrimitive("c"))
38+
},
39+
).forEach {
40+
test("object with props ${it.keys} passes validation") {
41+
val errors = mutableListOf<ValidationError>()
42+
val valid = schema.validate(it, errors::add)
43+
it.asClue {
44+
valid shouldBe true
45+
errors shouldHaveSize 0
46+
}
47+
}
48+
}
49+
50+
listOf(
51+
buildJsonObject {
52+
put("prop1", JsonPrimitive("a"))
53+
} to listOf("prop2"),
54+
buildJsonObject {
55+
put("prop2", JsonPrimitive("a"))
56+
} to listOf("prop1"),
57+
buildJsonObject {
58+
} to listOf("prop1", "prop2")
59+
).forEach { (jsonObject, missingProps) ->
60+
test("object with props ${jsonObject.keys} fails validation") {
61+
val errors = mutableListOf<ValidationError>()
62+
val valid = schema.validate(jsonObject, errors::add)
63+
jsonObject.asClue {
64+
valid shouldBe false
65+
errors.shouldContainExactly(
66+
ValidationError(
67+
schemaPath = JsonPointer("/required"),
68+
objectPath = JsonPointer.ROOT,
69+
message = "missing required properties: $missingProps",
70+
)
71+
)
72+
}
73+
}
74+
}
75+
76+
listOf(
77+
JsonPrimitive("a"),
78+
JsonPrimitive(42),
79+
JsonPrimitive(42.5),
80+
JsonPrimitive(true),
81+
JsonNull,
82+
buildJsonArray { },
83+
).forEach {
84+
test("not an object $it passes validation") {
85+
val errors = mutableListOf<ValidationError>()
86+
val valid = schema.validate(it, errors::add)
87+
valid shouldBe true
88+
errors shouldHaveSize 0
89+
}
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)