From 28b27e205f215f05c0d7d008f4d79403611a9f08 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Fri, 28 Jul 2023 18:31:26 +0400 Subject: [PATCH 1/5] Introduce factories group. Redo if-then-else assertions --- .../json/schema/internal/AssertionContext.kt | 64 ++++++++++++++++++- .../schema/internal/AssertionsCollection.kt | 1 + .../json/schema/internal/SchemaLoader.kt | 11 +++- .../factories/AssertionsGroupFactory.kt | 50 +++++++++++++++ .../condition/ElseAssertionFactory.kt | 28 ++++++++ .../factories/condition/IfAssertionFactory.kt | 31 +++++++++ .../condition/IfThenElseAssertionFactory.kt | 64 ------------------- .../condition/ThenAssertionFactory.kt | 28 ++++++++ 8 files changed, 210 insertions(+), 67 deletions(-) create mode 100644 src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/AssertionsGroupFactory.kt create mode 100644 src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/ElseAssertionFactory.kt create mode 100644 src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfAssertionFactory.kt delete mode 100644 src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfThenElseAssertionFactory.kt create mode 100644 src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/ThenAssertionFactory.kt diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/AssertionContext.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/AssertionContext.kt index b6954a47..ca9bf8ab 100644 --- a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/AssertionContext.kt +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/AssertionContext.kt @@ -3,19 +3,70 @@ package io.github.optimumcode.json.schema.internal import io.github.optimumcode.json.pointer.JsonPointer import io.github.optimumcode.json.pointer.div import io.github.optimumcode.json.pointer.get +import kotlin.jvm.JvmStatic +import kotlin.reflect.KClass +import kotlin.reflect.cast internal interface AssertionContext { val objectPath: JsonPointer - + fun annotate(key: AnnotationKey, value: T) + fun annotated(key: AnnotationKey): T? fun at(index: Int): AssertionContext fun at(property: String): AssertionContext fun resolveRef(refId: RefId): Pair + + fun resetAnnotations() +} + +internal class AnnotationKey private constructor( + private val name: String, + internal val type: KClass, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as AnnotationKey<*> + + if (name != other.name) return false + if (type != other.type) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + type.hashCode() + return result + } + + override fun toString(): String = "$name(${type.simpleName})" + + companion object { + @JvmStatic + inline fun create(name: String): AnnotationKey = create(name, T::class) + + @JvmStatic + fun create(name: String, type: KClass): AnnotationKey = AnnotationKey(name, type) + } } internal data class DefaultAssertionContext( override val objectPath: JsonPointer, private val references: Map, ) : AssertionContext { + private lateinit var _annotations: MutableMap, Any> + override fun annotate(key: AnnotationKey, value: T) { + annotations()[key] = value + } + + override fun annotated(key: AnnotationKey): T? { + if (!::_annotations.isInitialized) { + return null + } + return _annotations[key]?.let { key.type.cast(it) } + } + override fun at(index: Int): AssertionContext = copy(objectPath = objectPath[index]) override fun at(property: String): AssertionContext { @@ -26,4 +77,15 @@ internal data class DefaultAssertionContext( val resolvedRef = requireNotNull(references[refId]) { "$refId is not found" } return resolvedRef.schemaPath to resolvedRef.assertion } + + override fun resetAnnotations() { + annotations().clear() + } + + private fun annotations(): MutableMap, Any> { + if (!::_annotations.isInitialized) { + _annotations = hashMapOf() + } + return _annotations + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/AssertionsCollection.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/AssertionsCollection.kt index 49d322ce..e9722fa1 100644 --- a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/AssertionsCollection.kt +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/AssertionsCollection.kt @@ -13,6 +13,7 @@ internal class AssertionsCollection( val valid = it.validate(element, context, errorCollector) result = result and valid } + context.resetAnnotations() return result } } \ No newline at end of file diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt index ea3c8af0..bc19bb2c 100644 --- a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt @@ -8,6 +8,7 @@ import io.github.optimumcode.json.pointer.get 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.ContainsAssertionFactory import io.github.optimumcode.json.schema.internal.factories.array.ItemsAssertionFactory import io.github.optimumcode.json.schema.internal.factories.array.MaxItemsAssertionFactory @@ -15,9 +16,11 @@ import io.github.optimumcode.json.schema.internal.factories.array.MinItemsAssert import io.github.optimumcode.json.schema.internal.factories.array.UniqueItemsAssertionFactory import io.github.optimumcode.json.schema.internal.factories.condition.AllOfAssertionFactory import io.github.optimumcode.json.schema.internal.factories.condition.AnyOfAssertionFactory -import io.github.optimumcode.json.schema.internal.factories.condition.IfThenElseAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.condition.ElseAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.condition.IfAssertionFactory import io.github.optimumcode.json.schema.internal.factories.condition.NotAssertionFactory import io.github.optimumcode.json.schema.internal.factories.condition.OneOfAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.condition.ThenAssertionFactory import io.github.optimumcode.json.schema.internal.factories.general.ConstAssertionFactory import io.github.optimumcode.json.schema.internal.factories.general.EnumAssertionFactory import io.github.optimumcode.json.schema.internal.factories.general.TypeAssertionFactory @@ -64,7 +67,11 @@ private val factories: List = listOf( PropertiesAssertionFactory, PropertyNamesAssertionFactory, DependenciesAssertionFactory, - IfThenElseAssertionFactory, + FactoryGroup( + IfAssertionFactory, + ThenAssertionFactory, + ElseAssertionFactory, + ), AllOfAssertionFactory, AnyOfAssertionFactory, OneOfAssertionFactory, diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/AssertionsGroupFactory.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/AssertionsGroupFactory.kt new file mode 100644 index 00000000..80794459 --- /dev/null +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/AssertionsGroupFactory.kt @@ -0,0 +1,50 @@ +package io.github.optimumcode.json.schema.internal.factories + +import io.github.optimumcode.json.schema.ErrorCollector +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 kotlinx.serialization.json.JsonElement + +/** + * This class allows to create a group assertion that guarantees the order of assertion execution + * (the same order as [group]) + */ +internal class AssertionsGroupFactory( + private val group: List, +) : AssertionFactory { + init { + require(group.isNotEmpty()) { "at least one assertion must be in group" } + } + + override fun isApplicable(element: JsonElement): Boolean { + return group.any { it.isApplicable(element) } + } + + override fun create(element: JsonElement, context: LoadingContext): JsonSchemaAssertion { + return GroupAssertion( + assertions = group.asSequence() + .filter { it.isApplicable(element) } + .map { it.create(element, context) } + .toList(), + ) + } +} + +@Suppress("FunctionName") +internal fun FactoryGroup(vararg factories: AssertionFactory): AssertionFactory = + AssertionsGroupFactory(factories.toList()) + +private class GroupAssertion( + private val assertions: List, +) : JsonSchemaAssertion { + override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean { + var result = true + assertions.forEach { + val valid = it.validate(element, context, errorCollector) + result = result && valid + } + return result + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/ElseAssertionFactory.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/ElseAssertionFactory.kt new file mode 100644 index 00000000..acb4c993 --- /dev/null +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/ElseAssertionFactory.kt @@ -0,0 +1,28 @@ +package io.github.optimumcode.json.schema.internal.factories.condition + +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 kotlinx.serialization.json.JsonElement + +internal object ElseAssertionFactory : AbstractAssertionFactory("else") { + override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion { + require(context.isJsonSchema(element)) { "$property must be a valid JSON schema" } + val elseAssertion = context.schemaFrom(element) + return ElseAssertion(elseAssertion) + } +} + +private class ElseAssertion( + private val assertion: JsonSchemaAssertion, +) : JsonSchemaAssertion { + override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean { + return if (context.annotated(IfAssertionFactory.ANNOTATION) == false) { + assertion.validate(element, context, errorCollector) + } else { + true + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfAssertionFactory.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfAssertionFactory.kt new file mode 100644 index 00000000..96160533 --- /dev/null +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfAssertionFactory.kt @@ -0,0 +1,31 @@ +package io.github.optimumcode.json.schema.internal.factories.condition + +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.JsonSchemaAssertion +import io.github.optimumcode.json.schema.internal.LoadingContext +import io.github.optimumcode.json.schema.internal.factories.AbstractAssertionFactory +import kotlinx.serialization.json.JsonElement + +internal object IfAssertionFactory : AbstractAssertionFactory("if") { + val ANNOTATION: AnnotationKey = AnnotationKey.create("if") + + override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion { + require(context.isJsonSchema(element)) { "$property must be a valid JSON schema" } + val ifAssertion = context.schemaFrom(element) + return IfAssertion(ifAssertion) + } +} + +private class IfAssertion( + private val condition: JsonSchemaAssertion, +) : JsonSchemaAssertion { + override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean { + context.annotate( + IfAssertionFactory.ANNOTATION, + condition.validate(element, context, ErrorCollector.EMPTY), + ) + return true + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfThenElseAssertionFactory.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfThenElseAssertionFactory.kt deleted file mode 100644 index 938f200e..00000000 --- a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfThenElseAssertionFactory.kt +++ /dev/null @@ -1,64 +0,0 @@ -package io.github.optimumcode.json.schema.internal.factories.condition - -import io.github.optimumcode.json.schema.ErrorCollector -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.TrueSchemaAssertion -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject - -internal object IfThenElseAssertionFactory : AssertionFactory { - private const val IF_PROPERTY: String = "if" - private const val THEN_PROPERTY: String = "then" - private const val ELSE_PROPERTY: String = "else" - - override fun isApplicable(element: JsonElement): Boolean { - return element is JsonObject && element.run { - // we need to load all definitions because they can be referenced - containsKey(IF_PROPERTY) || containsKey(THEN_PROPERTY) || containsKey(ELSE_PROPERTY) - } - } - - override fun create(element: JsonElement, context: LoadingContext): JsonSchemaAssertion { - require(element is JsonObject) { "cannot extract properties from ${element::class.simpleName}" } - val ifElement: JsonElement? = element[IF_PROPERTY]?.apply { - require(context.isJsonSchema(this)) { "$IF_PROPERTY must be a valid JSON schema" } - } - val ifAssertion: JsonSchemaAssertion? = ifElement?.let(context.at(IF_PROPERTY)::schemaFrom) - - val thenAssertion: JsonSchemaAssertion? = loadOptionalAssertion(element, THEN_PROPERTY, context) - val elseAssertion: JsonSchemaAssertion? = loadOptionalAssertion(element, ELSE_PROPERTY, context) - - return when { - ifAssertion == null -> TrueSchemaAssertion // no if -> no effect - thenAssertion == null && elseAssertion == null -> TrueSchemaAssertion // only if - no effect - else -> IfThenElseAssertion(ifAssertion, thenAssertion, elseAssertion) - } - } - - private fun loadOptionalAssertion( - jsonObject: JsonObject, - property: String, - context: LoadingContext, - ): JsonSchemaAssertion? { - val element = jsonObject[property] ?: return null - require(context.isJsonSchema(element)) { "$property must be a valid JSON schema" } - return context.at(property).schemaFrom(element) - } -} - -private class IfThenElseAssertion( - private val ifAssertion: JsonSchemaAssertion, - private val thenAssertion: JsonSchemaAssertion?, - private val elseAssertion: JsonSchemaAssertion?, -) : JsonSchemaAssertion { - override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean { - return if (ifAssertion.validate(element, context, ErrorCollector.EMPTY)) { - thenAssertion?.validate(element, context, errorCollector) ?: true - } else { - elseAssertion?.validate(element, context, errorCollector) ?: true - } - } -} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/ThenAssertionFactory.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/ThenAssertionFactory.kt new file mode 100644 index 00000000..0770b174 --- /dev/null +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/ThenAssertionFactory.kt @@ -0,0 +1,28 @@ +package io.github.optimumcode.json.schema.internal.factories.condition + +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 kotlinx.serialization.json.JsonElement + +internal object ThenAssertionFactory : AbstractAssertionFactory("then") { + override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion { + require(context.isJsonSchema(element)) { "$property must be a valid JSON schema" } + val thenAssertion = context.schemaFrom(element) + return ThenAssertion(thenAssertion) + } +} + +private class ThenAssertion( + private val assertion: JsonSchemaAssertion, +) : JsonSchemaAssertion { + override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean { + return if (context.annotated(IfAssertionFactory.ANNOTATION) == true) { + assertion.validate(element, context, errorCollector) + } else { + true + } + } +} \ No newline at end of file From c7d3fa2642f728fc19a0cca387d3491a5b6d3594 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Fri, 28 Jul 2023 18:40:43 +0400 Subject: [PATCH 2/5] Exclude internal label from features category --- changelog_config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog_config.json b/changelog_config.json index fb5efb84..56ca45e1 100644 --- a/changelog_config.json +++ b/changelog_config.json @@ -3,6 +3,7 @@ { "title": "## 🚀 Features", "labels": ["enhancement"], + "exclude_labels": ["internal"], "empty_content": "No new features today 😢" }, { From 277b9285f516591ed0b0ba1b974211d2caef5501 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Mon, 31 Jul 2023 13:58:36 +0400 Subject: [PATCH 3/5] Split object properties assertions --- .../json/schema/internal/SchemaLoader.kt | 10 +- .../AdditionalPropertiesAssertionFactory.kt | 45 ++++++ .../PatternPropertiesAssertionFactory.kt | 73 ++++++++++ .../object/PropertiesAssertionFactory.kt | 136 ++++-------------- 4 files changed, 155 insertions(+), 109 deletions(-) create mode 100644 src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/AdditionalPropertiesAssertionFactory.kt create mode 100644 src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/PatternPropertiesAssertionFactory.kt diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt index bc19bb2c..90aa02f6 100644 --- a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt @@ -29,9 +29,11 @@ import io.github.optimumcode.json.schema.internal.factories.number.ExclusiveMini import io.github.optimumcode.json.schema.internal.factories.number.MaximumAssertionFactory import io.github.optimumcode.json.schema.internal.factories.number.MinimumAssertionFactory import io.github.optimumcode.json.schema.internal.factories.number.MultipleOfAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.`object`.AdditionalPropertiesAssertionFactory import io.github.optimumcode.json.schema.internal.factories.`object`.DependenciesAssertionFactory import io.github.optimumcode.json.schema.internal.factories.`object`.MaxPropertiesAssertionFactory import io.github.optimumcode.json.schema.internal.factories.`object`.MinPropertiesAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.`object`.PatternPropertiesAssertionFactory import io.github.optimumcode.json.schema.internal.factories.`object`.PropertiesAssertionFactory import io.github.optimumcode.json.schema.internal.factories.`object`.PropertyNamesAssertionFactory import io.github.optimumcode.json.schema.internal.factories.`object`.RequiredAssertionFactory @@ -64,7 +66,11 @@ private val factories: List = listOf( MaxPropertiesAssertionFactory, MinPropertiesAssertionFactory, RequiredAssertionFactory, - PropertiesAssertionFactory, + FactoryGroup( + PropertiesAssertionFactory, + PatternPropertiesAssertionFactory, + AdditionalPropertiesAssertionFactory, + ), PropertyNamesAssertionFactory, DependenciesAssertionFactory, FactoryGroup( @@ -307,6 +313,7 @@ private data class DefaultLoadingContext( private fun Set.resolvePath(path: String?): Uri { return last().id.appendPathToParent(requireNotNull(path) { "path is null" }) } + private fun Uri.appendPathToParent(path: String): Uri { val hasLastEmptySegment = toString().endsWith('/') return if (hasLastEmptySegment) { @@ -322,6 +329,7 @@ private fun Uri.appendPathToParent(path: String): Uri { }.appendEncodedPath(path) .build() } + private fun Uri.buildRefId(): RefId = RefId(this) private fun Builder.buildRefId(): RefId = build().buildRefId() diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/AdditionalPropertiesAssertionFactory.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/AdditionalPropertiesAssertionFactory.kt new file mode 100644 index 00000000..c7fb5aef --- /dev/null +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/AdditionalPropertiesAssertionFactory.kt @@ -0,0 +1,45 @@ +package io.github.optimumcode.json.schema.internal.factories.`object` + +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 kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +internal object AdditionalPropertiesAssertionFactory : AbstractAssertionFactory("additionalProperties") { + override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion { + require(context.isJsonSchema(element)) { "$property must be a valid JSON schema" } + val schemaAssertion = context.schemaFrom(element) + return AdditionalPropertiesAssertion(schemaAssertion) + } +} + +private class AdditionalPropertiesAssertion( + private val assertion: JsonSchemaAssertion, +) : JsonSchemaAssertion { + override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean { + if (element !is JsonObject) { + return true + } + val propertiesAnnotation: Set? = context.annotated(PropertiesAssertionFactory.ANNOTATION) + val patternAnnotation: Set? = context.annotated(PatternPropertiesAssertionFactory.ANNOTATION) + var result = true + for ((prop, value) in element) { + if (propertiesAnnotation?.contains(prop) == true) { + continue + } + if (patternAnnotation?.contains(prop) == true) { + continue + } + val valid = assertion.validate( + value, + context.at(prop), + errorCollector, + ) + result = result && valid + } + return result + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/PatternPropertiesAssertionFactory.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/PatternPropertiesAssertionFactory.kt new file mode 100644 index 00000000..e8f11fa9 --- /dev/null +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/PatternPropertiesAssertionFactory.kt @@ -0,0 +1,73 @@ +package io.github.optimumcode.json.schema.internal.factories.`object` + +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.JsonSchemaAssertion +import io.github.optimumcode.json.schema.internal.LoadingContext +import io.github.optimumcode.json.schema.internal.factories.AbstractAssertionFactory +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +internal object PatternPropertiesAssertionFactory : AbstractAssertionFactory("patternProperties") { + val ANNOTATION: AnnotationKey> = AnnotationKey.create(property) + + override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion { + require(element is JsonObject) { "$property must be an object" } + val propAssertions: Map = element.asSequence().associate { (prop, element) -> + require(context.isJsonSchema(element)) { "$prop must be a valid JSON schema" } + val regex = try { + prop.toRegex() + } catch (exOrJsError: Throwable) { // because of JsError + throw IllegalArgumentException("$prop must be a valid regular expression", exOrJsError) + } + regex to context.at(prop).schemaFrom(element) + } + return PatternAssertion(propAssertions) + } +} + +private class PatternAssertion( + private val assertionsByRegex: Map, +) : JsonSchemaAssertion { + override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean { + if (element !is JsonObject) { + return true + } + + if (assertionsByRegex.isEmpty()) { + return true + } + + var result = true + var checkedProps: MutableSet? = null + for ((prop, value) in element) { + val matchedRegex = assertionsByRegex.filter { (regex) -> + regex.find(prop) != null + } + if (matchedRegex.isEmpty()) { + continue + } + if (checkedProps == null) { + // initialize props + checkedProps = hashSetOf() + } + checkedProps.add(prop) + val propContext = context.at(prop) + for ((_, assertion) in matchedRegex) { + val valid = assertion.validate( + value, + propContext, + errorCollector, + ) + result = result && valid + } + } + + if (checkedProps != null) { + context.annotate(PatternPropertiesAssertionFactory.ANNOTATION, checkedProps) + } + + return result + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/PropertiesAssertionFactory.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/PropertiesAssertionFactory.kt index 48b7c2a4..a1ce9278 100644 --- a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/PropertiesAssertionFactory.kt +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/object/PropertiesAssertionFactory.kt @@ -1,132 +1,52 @@ package io.github.optimumcode.json.schema.internal.factories.`object` import io.github.optimumcode.json.schema.ErrorCollector +import io.github.optimumcode.json.schema.internal.AnnotationKey +import io.github.optimumcode.json.schema.internal.AnnotationKey.Companion 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 kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject -@Suppress("unused") -internal object PropertiesAssertionFactory : AssertionFactory { - private const val propertiesProperty: String = "properties" - private const val patternPropertiesProperty: String = "patternProperties" - private const val additionalPropertiesProperty: String = "additionalProperties" +internal object PropertiesAssertionFactory : AbstractAssertionFactory("properties") { + val ANNOTATION: AnnotationKey> = Companion.create(property) - override fun isApplicable(element: JsonElement): Boolean { - return element is JsonObject && element.run { - containsKey(propertiesProperty) || - containsKey(patternPropertiesProperty) || - containsKey(additionalPropertiesProperty) - } - } - - override fun create(element: JsonElement, context: LoadingContext): JsonSchemaAssertion { - require(element is JsonObject) { "cannot extract properties from ${element::class.simpleName}" } - val propertiesAssertions: Map = extractPropertiesAssertions(element, context) - val patternAssertions: Map = extractPatternAssertions(element, context) - val additionalProperties: JsonSchemaAssertion? = extractAdditionalProperties(element, context) - return PropertiesAssertion( - propertiesAssertions, - patternAssertions, - additionalProperties, - ) - } - - private fun extractAdditionalProperties(jsonObject: JsonObject, context: LoadingContext): JsonSchemaAssertion? { - if (jsonObject.isEmpty()) { - return null - } - val additionalElement = jsonObject[additionalPropertiesProperty] ?: return null - require(context.isJsonSchema(additionalElement)) { "$additionalPropertiesProperty must be a valid JSON schema" } - return context.at(additionalPropertiesProperty).schemaFrom(additionalElement) - } - - private fun extractPatternAssertions( - jsonObject: JsonObject, - context: LoadingContext, - ): Map { - if (jsonObject.isEmpty()) { - return emptyMap() - } - val propertiesElement = jsonObject[patternPropertiesProperty] ?: return emptyMap() - require(propertiesElement is JsonObject) { "$patternPropertiesProperty must be an object" } - if (propertiesElement.isEmpty()) { - return emptyMap() - } - val propContext = context.at(patternPropertiesProperty) - return propertiesElement.map { (pattern, element) -> - require(propContext.isJsonSchema(element)) { "$pattern must be a valid JSON schema" } - val regex = try { - pattern.toRegex() - } catch (exOrJsError: Throwable) { // because of JsError - throw IllegalArgumentException("$pattern must be a valid regular expression", exOrJsError) - } - regex to propContext.at(pattern).schemaFrom(element) - }.toMap() - } - - private fun extractPropertiesAssertions( - jsonObject: JsonObject, - context: LoadingContext, - ): Map { - if (jsonObject.isEmpty()) { - return emptyMap() - } - val propertiesElement = jsonObject[propertiesProperty] ?: return emptyMap() - require(propertiesElement is JsonObject) { "$propertiesProperty must be an object" } - if (propertiesElement.isEmpty()) { - return emptyMap() - } - val propertiesContext = context.at(propertiesProperty) - return propertiesElement.mapValues { (prop, element) -> - require(propertiesContext.isJsonSchema(element)) { "$prop must be a valid JSON schema" } - propertiesContext.at(prop).schemaFrom(element) + override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion { + require(element is JsonObject) { "$property must be an object" } + val propAssertions: Map = element.mapValues { (prop, element) -> + require(context.isJsonSchema(element)) { "$prop must be a valid JSON schema" } + context.at(prop).schemaFrom(element) } + return PropertiesAssertion(propAssertions) } } private class PropertiesAssertion( - private val propertiesAssertions: Map, - private val patternAssertions: Map, - private val additionalProperties: JsonSchemaAssertion?, + private val assertionsByProperty: Map, ) : JsonSchemaAssertion { override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean { + if (assertionsByProperty.isEmpty()) { + return true + } if (element !is JsonObject) { return true } - var valid = true - for ((prop, value) in element) { - val propContext = context.at(prop) - var triggered = false - var res = propertiesAssertions[prop]?.run { - triggered = true - validate( - value, - propContext, - errorCollector, - ) - } ?: true - valid = valid and res - for ((pattern, assertion) in patternAssertions) { - if (pattern.find(prop) != null) { - triggered = true - res = assertion.validate( - value, - propContext, - errorCollector, - ) - valid = valid and res - } - } - if (triggered) { - continue - } - res = additionalProperties?.validate(value, propContext, errorCollector) ?: true - valid = valid and res + var result = true + for ((prop, value) in element) { + val propAssertion = assertionsByProperty[prop] ?: continue + val valid = propAssertion.validate( + value, + context.at(prop), + errorCollector, + ) + result = result && valid } - return valid + + context.annotate(PropertiesAssertionFactory.ANNOTATION, assertionsByProperty.keys) + + return result } } \ No newline at end of file From 09cc76ef895c3d7be75e6b24f8c47df85b0fc644 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Mon, 31 Jul 2023 16:30:47 +0400 Subject: [PATCH 4/5] Use property value for key --- .../schema/internal/factories/condition/IfAssertionFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfAssertionFactory.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfAssertionFactory.kt index 96160533..e5723612 100644 --- a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfAssertionFactory.kt +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/IfAssertionFactory.kt @@ -9,7 +9,7 @@ import io.github.optimumcode.json.schema.internal.factories.AbstractAssertionFac import kotlinx.serialization.json.JsonElement internal object IfAssertionFactory : AbstractAssertionFactory("if") { - val ANNOTATION: AnnotationKey = AnnotationKey.create("if") + val ANNOTATION: AnnotationKey = AnnotationKey.create(property) override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion { require(context.isJsonSchema(element)) { "$property must be a valid JSON schema" } From 4939f8f5a52ce22a412ad371a6068b27dca959da Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Mon, 31 Jul 2023 17:12:55 +0400 Subject: [PATCH 5/5] Increment version --- README.md | 8 ++++---- gradle.properties | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 72f8787e..1799da60 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ repositories { mavenCentral() } -implementation("io.github.optimumcode:json-schema-validator:0.0.1") +implementation("io.github.optimumcode:json-schema-validator:0.0.2") ``` ##### Groovy @@ -45,7 +45,7 @@ repositories { mavenCentral() } -implementation 'io.github.optimumcode:json-schema-validator:0.0.1' +implementation 'io.github.optimumcode:json-schema-validator:0.0.2' ``` _Release are published to Sonatype repository. The synchronization with Maven Central takes time._ @@ -78,7 +78,7 @@ repositories { maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots") } -implementation("io.github.optimumcode:json-schema-validator:0.0.1-SNAPSHOT") +implementation("io.github.optimumcode:json-schema-validator:0.0.3-SNAPSHOT") ``` ##### Groovy @@ -88,7 +88,7 @@ repositories { maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' } } -implementation 'io.github.optimumcode:json-schema-validator:0.0.1-SNAPSHOT' +implementation 'io.github.optimumcode:json-schema-validator:0.0.3-SNAPSHOT' ``` ### Example diff --git a/gradle.properties b/gradle.properties index 12dc4868..c6fab6c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,5 +3,5 @@ kotlin.js.compiler=ir org.gradle.jvmargs=-Xmx1G org.gradle.java.installations.auto-download=false -version=0.0.2-SNAPSHOT +version=0.0.3-SNAPSHOT group=io.github.optimumcode \ No newline at end of file