Skip to content

Split items and additionalItems assertions #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.github.optimumcode.json.pointer.relative
import io.github.optimumcode.json.schema.JsonSchema
import io.github.optimumcode.json.schema.internal.ReferenceValidator.ReferenceLocation
import io.github.optimumcode.json.schema.internal.factories.FactoryGroup
import io.github.optimumcode.json.schema.internal.factories.array.AdditionalItemsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.ContainsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.ItemsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.MaxItemsAssertionFactory
Expand Down Expand Up @@ -58,7 +59,10 @@ private val factories: List<AssertionFactory> = listOf(
MaxLengthAssertionFactory,
MinLengthAssertionFactory,
PatternAssertionFactory,
ItemsAssertionFactory,
FactoryGroup(
ItemsAssertionFactory,
AdditionalItemsAssertionFactory,
),
MaxItemsAssertionFactory,
MinItemsAssertionFactory,
UniqueItemsAssertionFactory,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.github.optimumcode.json.schema.internal.factories.array

import io.github.optimumcode.json.schema.ErrorCollector
import io.github.optimumcode.json.schema.internal.AssertionContext
import io.github.optimumcode.json.schema.internal.JsonSchemaAssertion
import io.github.optimumcode.json.schema.internal.LoadingContext
import io.github.optimumcode.json.schema.internal.factories.AbstractAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.ItemsAssertionFactory.Result
import io.github.optimumcode.json.schema.internal.factories.array.ItemsAssertionFactory.Result.All
import io.github.optimumcode.json.schema.internal.factories.array.ItemsAssertionFactory.Result.Index
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement

internal object AdditionalItemsAssertionFactory : AbstractAssertionFactory("additionalItems") {
override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion {
require(context.isJsonSchema(element)) { "$property must be a valid JSON schema" }
val assertion = context.schemaFrom(element)
return AdditionalItemsAssertion(assertion)
}
}

private class AdditionalItemsAssertion(
private val assertion: JsonSchemaAssertion,
) : JsonSchemaAssertion {
override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean {
if (element !is JsonArray) {
return true
}
val lastProcessedIndex: Int = when (val result: Result? = context.annotated(ItemsAssertionFactory.ANNOTATION)) {
All -> element.size
is Index -> result.value
null -> return true // items assertion is not used so this one should be ignored
}
if (lastProcessedIndex == element.size) {
// we have nothing to process here
return true
}
var valid = true
for ((index, el) in element.withIndex()) {
if (index <= lastProcessedIndex) {
continue
}
val res = assertion.validate(
el,
context.at(index),
errorCollector,
)
valid = valid && res
}

return valid
}
}
Original file line number Diff line number Diff line change
@@ -1,53 +1,42 @@
package io.github.optimumcode.json.schema.internal.factories.array

import io.github.optimumcode.json.schema.ErrorCollector
import io.github.optimumcode.json.schema.internal.AnnotationKey
import io.github.optimumcode.json.schema.internal.AssertionContext
import io.github.optimumcode.json.schema.internal.AssertionFactory
import io.github.optimumcode.json.schema.internal.JsonSchemaAssertion
import io.github.optimumcode.json.schema.internal.LoadingContext
import io.github.optimumcode.json.schema.internal.factories.AbstractAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.ItemsAssertionFactory.Result
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject

@Suppress("unused")
internal object ItemsAssertionFactory : AssertionFactory {
private const val itemsProperty: String = "items"
private const val additionalItemsProperty: String = "additionalItems"

override fun isApplicable(element: JsonElement): Boolean {
return element is JsonObject &&
element.run { contains(itemsProperty) }
internal object ItemsAssertionFactory : AbstractAssertionFactory("items") {
sealed class Result {
object All : Result()
class Index(val value: Int) : Result()
}

override fun create(element: JsonElement, context: LoadingContext): JsonSchemaAssertion {
require(element is JsonObject) { "cannot extract properties from ${element::class.simpleName}" }
val itemsElement: JsonElement = requireNotNull(element[itemsProperty]) {
"cannot extract $itemsProperty property from element"
}
val itemsContext = context.at(itemsProperty)
val itemsAssertions: List<JsonSchemaAssertion> = if (itemsElement is JsonArray) {
require(itemsElement.isNotEmpty()) { "$itemsProperty must have at least one element" }
require(itemsElement.all(context::isJsonSchema)) {
"all elements in $itemsProperty must be a valid JSON schema"
val ANNOTATION: AnnotationKey<Result> = AnnotationKey.create(property)

override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion {
val itemsAssertions: List<JsonSchemaAssertion> = if (element is JsonArray) {
require(element.isNotEmpty()) { "$property must have at least one element" }
require(element.all(context::isJsonSchema)) {
"all elements in $property must be a valid JSON schema"
}
itemsElement.mapIndexed { index, item -> itemsContext.at(index).schemaFrom(item) }
element.mapIndexed { index, item -> context.at(index).schemaFrom(item) }
} else {
require(context.isJsonSchema(itemsElement)) { "$itemsProperty must be a valid JSON schema" }
listOf(itemsContext.schemaFrom(itemsElement))
require(context.isJsonSchema(element)) { "$property must be a valid JSON schema" }
listOf(context.schemaFrom(element))
}

val additionalItemsAssertion: JsonSchemaAssertion? = element[additionalItemsProperty]?.let {
require(context.isJsonSchema(it)) { "$additionalItemsProperty must be a valid JSON schema" }
context.at(additionalItemsProperty).schemaFrom(it)
}
return ElementsAssertion(itemsAssertions, itemsElement !is JsonArray, additionalItemsAssertion)
return ItemsAssertion(itemsAssertions, element !is JsonArray)
}
}

private class ElementsAssertion(
private class ItemsAssertion(
private val items: List<JsonSchemaAssertion>,
private val allElements: Boolean,
private val additionalItems: JsonSchemaAssertion?,
) : JsonSchemaAssertion {
init {
if (allElements) {
Expand All @@ -61,36 +50,31 @@ private class ElementsAssertion(
return if (allElements) {
validateEachItem(element, context, errorCollector)
} else {
validateWithAdditionalItems(element, context, errorCollector)
validateElementsAtIndexes(element, context, errorCollector)
}
}

private fun validateWithAdditionalItems(
private fun validateElementsAtIndexes(
element: JsonArray,
context: AssertionContext,
errorCollector: ErrorCollector,
): Boolean {
var valid = true
element.forEachIndexed { index, item ->
val result: Boolean
var lastProcessedIndex = 0
for ((index, item) in element.withIndex()) {
if (index < items.size) {
result = items[index].validate(
val result: Boolean = items[index].validate(
item,
context.at(index),
errorCollector,
)
lastProcessedIndex = index
valid = valid && result
} else {
if (additionalItems == null) {
return valid
}
result = additionalItems.validate(
item,
context.at(index),
errorCollector,
)
break
}
valid = valid && result
}
context.annotate(ItemsAssertionFactory.ANNOTATION, Result.Index(lastProcessedIndex))
return valid
}

Expand All @@ -109,6 +93,7 @@ private class ElementsAssertion(
)
valid = valid && result
}
context.annotate(ItemsAssertionFactory.ANNOTATION, Result.All)
return valid
}
}