Skip to content

Commit

Permalink
Split object properties assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
OptimumCode committed Jul 31, 2023
1 parent c7d3fa2 commit 277b928
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -64,7 +66,11 @@ private val factories: List<AssertionFactory> = listOf(
MaxPropertiesAssertionFactory,
MinPropertiesAssertionFactory,
RequiredAssertionFactory,
PropertiesAssertionFactory,
FactoryGroup(
PropertiesAssertionFactory,
PatternPropertiesAssertionFactory,
AdditionalPropertiesAssertionFactory,
),
PropertyNamesAssertionFactory,
DependenciesAssertionFactory,
FactoryGroup(
Expand Down Expand Up @@ -307,6 +313,7 @@ private data class DefaultLoadingContext(
private fun Set<IdWithLocation>.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) {
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>? = context.annotated(PropertiesAssertionFactory.ANNOTATION)
val patternAnnotation: Set<String>? = 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
}
}
Original file line number Diff line number Diff line change
@@ -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<Set<String>> = AnnotationKey.create(property)

override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion {
require(element is JsonObject) { "$property must be an object" }
val propAssertions: Map<Regex, JsonSchemaAssertion> = 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<Regex, JsonSchemaAssertion>,
) : 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<String>? = 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
}
}
Original file line number Diff line number Diff line change
@@ -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<Set<String>> = 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<String, JsonSchemaAssertion> = extractPropertiesAssertions(element, context)
val patternAssertions: Map<Regex, JsonSchemaAssertion> = 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<Regex, JsonSchemaAssertion> {
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<String, JsonSchemaAssertion> {
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<String, JsonSchemaAssertion> = 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<String, JsonSchemaAssertion>,
private val patternAssertions: Map<Regex, JsonSchemaAssertion>,
private val additionalProperties: JsonSchemaAssertion?,
private val assertionsByProperty: Map<String, JsonSchemaAssertion>,
) : 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
}
}

0 comments on commit 277b928

Please sign in to comment.