Skip to content

Commit

Permalink
Add support for draft 4 (#140)
Browse files Browse the repository at this point in the history
Resolves #139
  • Loading branch information
OptimumCode authored Jun 19, 2024
1 parent 68756be commit d2d2f03
Show file tree
Hide file tree
Showing 11 changed files with 451 additions and 5 deletions.
1 change: 1 addition & 0 deletions api/json-schema-validator.api
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ public final class io/github/optimumcode/json/schema/SchemaType : java/lang/Enum
public static final field Companion Lio/github/optimumcode/json/schema/SchemaType$Companion;
public static final field DRAFT_2019_09 Lio/github/optimumcode/json/schema/SchemaType;
public static final field DRAFT_2020_12 Lio/github/optimumcode/json/schema/SchemaType;
public static final field DRAFT_4 Lio/github/optimumcode/json/schema/SchemaType;
public static final field DRAFT_6 Lio/github/optimumcode/json/schema/SchemaType;
public static final field DRAFT_7 Lio/github/optimumcode/json/schema/SchemaType;
public static final fun find (Ljava/lang/String;)Lio/github/optimumcode/json/schema/SchemaType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package io.github.optimumcode.json.schema
import com.eygraber.uri.Uri
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2019_09
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2020_12
import io.github.optimumcode.json.schema.SchemaType.DRAFT_4
import io.github.optimumcode.json.schema.SchemaType.DRAFT_6
import io.github.optimumcode.json.schema.SchemaType.DRAFT_7
import io.github.optimumcode.json.schema.extension.ExternalAssertionFactory
import io.github.optimumcode.json.schema.internal.SchemaLoader
import io.github.optimumcode.json.schema.internal.wellknown.Draft201909
import io.github.optimumcode.json.schema.internal.wellknown.Draft202012
import io.github.optimumcode.json.schema.internal.wellknown.Draft4
import io.github.optimumcode.json.schema.internal.wellknown.Draft6
import io.github.optimumcode.json.schema.internal.wellknown.Draft7
import kotlinx.serialization.json.JsonElement
Expand All @@ -19,6 +21,7 @@ public interface JsonSchemaLoader {
public fun registerWellKnown(draft: SchemaType): JsonSchemaLoader =
apply {
when (draft) {
DRAFT_4 -> Draft4.entries.forEach { register(it.content) }
DRAFT_6 -> Draft6.entries.forEach { register(it.content) }
DRAFT_7 -> Draft7.entries.forEach { register(it.content) }
DRAFT_2019_09 -> Draft201909.entries.forEach { register(it.content) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.eygraber.uri.Uri
import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig
import io.github.optimumcode.json.schema.internal.config.Draft201909SchemaLoaderConfig
import io.github.optimumcode.json.schema.internal.config.Draft202012SchemaLoaderConfig
import io.github.optimumcode.json.schema.internal.config.Draft4SchemaLoaderConfig
import io.github.optimumcode.json.schema.internal.config.Draft6SchemaLoaderConfig
import io.github.optimumcode.json.schema.internal.config.Draft7SchemaLoaderConfig
import kotlin.jvm.JvmStatic
Expand All @@ -12,6 +13,7 @@ public enum class SchemaType(
internal val schemaId: Uri,
internal val config: SchemaLoaderConfig,
) {
DRAFT_4(Uri.parse("http://json-schema.org/draft-04/schema"), Draft4SchemaLoaderConfig),
DRAFT_6(Uri.parse("http://json-schema.org/draft-06/schema"), Draft6SchemaLoaderConfig),
DRAFT_7(Uri.parse("http://json-schema.org/draft-07/schema"), Draft7SchemaLoaderConfig),
DRAFT_2019_09(Uri.parse("https://json-schema.org/draft/2019-09/schema"), Draft201909SchemaLoaderConfig),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,25 @@ internal class SchemaLoader : JsonSchemaLoader {
)

private fun addExtensionFactory(extensionFactory: ExternalAssertionFactory) {
val matchedDrafts = mutableMapOf<String, MutableList<SchemaType>>()
for (schemaType in SchemaType.entries) {
val match =
schemaType.config.allFactories.find { it.property.equals(extensionFactory.keywordName, ignoreCase = true) }
if (match == null) {
continue
}
matchedDrafts
.getOrPut(
match.property,
::ArrayList,
).add(schemaType)
}
if (matchedDrafts.isNotEmpty()) {
error(
"external factory with keyword '${extensionFactory.keywordName}' " +
"overlaps with '${match.property}' keyword from $schemaType",
"overlaps with ${matchedDrafts.entries.joinToString { (property, drafts) ->
"'$property' keyword in $drafts draft(s)"
}}",
)
}
val duplicate = extensionFactories.keys.find { it.equals(extensionFactory.keywordName, ignoreCase = true) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package io.github.optimumcode.json.schema.internal.config

import io.github.optimumcode.json.schema.FormatBehavior
import io.github.optimumcode.json.schema.SchemaOption
import io.github.optimumcode.json.schema.internal.AssertionFactory
import io.github.optimumcode.json.schema.internal.KeyWord
import io.github.optimumcode.json.schema.internal.KeyWord.ANCHOR
import io.github.optimumcode.json.schema.internal.KeyWord.COMPATIBILITY_DEFINITIONS
import io.github.optimumcode.json.schema.internal.KeyWord.DEFINITIONS
import io.github.optimumcode.json.schema.internal.KeyWord.DYNAMIC_ANCHOR
import io.github.optimumcode.json.schema.internal.KeyWord.ID
import io.github.optimumcode.json.schema.internal.KeyWordResolver
import io.github.optimumcode.json.schema.internal.ReferenceFactory
import io.github.optimumcode.json.schema.internal.ReferenceFactory.RefHolder
import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig
import io.github.optimumcode.json.schema.internal.SchemaLoaderContext
import io.github.optimumcode.json.schema.internal.config.Draft4KeyWordResolver.REF_PROPERTY
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
import io.github.optimumcode.json.schema.internal.factories.array.MinItemsAssertionFactory
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.NotAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.OneOfAssertionFactory
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.FormatAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.general.TypeAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.number.Draft4MaximumAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.number.Draft4MinimumAssertionFactory
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
import io.github.optimumcode.json.schema.internal.factories.string.MaxLengthAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.string.MinLengthAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.string.PatternAssertionFactory
import io.github.optimumcode.json.schema.internal.util.getStringRequired
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject

internal object Draft4SchemaLoaderConfig : SchemaLoaderConfig {
private val factories: List<AssertionFactory> =
listOf(
TypeAssertionFactory,
EnumAssertionFactory,
ConstAssertionFactory,
MultipleOfAssertionFactory,
Draft4MaximumAssertionFactory,
Draft4MinimumAssertionFactory,
MinimumAssertionFactory,
MaxLengthAssertionFactory,
MinLengthAssertionFactory,
PatternAssertionFactory,
ItemsAssertionFactory,
AdditionalItemsAssertionFactory,
MaxItemsAssertionFactory,
MinItemsAssertionFactory,
UniqueItemsAssertionFactory,
ContainsAssertionFactory,
MaxPropertiesAssertionFactory,
MinPropertiesAssertionFactory,
RequiredAssertionFactory,
PropertiesAssertionFactory,
PatternPropertiesAssertionFactory,
AdditionalPropertiesAssertionFactory,
PropertyNamesAssertionFactory,
DependenciesAssertionFactory,
AllOfAssertionFactory,
AnyOfAssertionFactory,
OneOfAssertionFactory,
NotAssertionFactory,
)

override val defaultVocabulary: SchemaLoaderConfig.Vocabulary = SchemaLoaderConfig.Vocabulary()
override val allFactories: List<AssertionFactory>
get() = factories

override fun createVocabulary(schemaDefinition: JsonElement): SchemaLoaderConfig.Vocabulary? = null

override fun factories(
schemaDefinition: JsonElement,
vocabulary: SchemaLoaderConfig.Vocabulary,
options: SchemaLoaderConfig.Options,
): List<AssertionFactory> =
factories +
when (options[SchemaOption.FORMAT_BEHAVIOR_OPTION]) {
null, FormatBehavior.ANNOTATION_AND_ASSERTION -> FormatAssertionFactory.AnnotationAndAssertion
FormatBehavior.ANNOTATION_ONLY -> FormatAssertionFactory.AnnotationOnly
}

override val keywordResolver: KeyWordResolver
get() = Draft4KeyWordResolver
override val referenceFactory: ReferenceFactory
get() = Draft4ReferenceFactory
}

private object Draft4KeyWordResolver : KeyWordResolver {
private const val DEFINITIONS_PROPERTY: String = "definitions"
private const val ID_PROPERTY: String = "id"
const val REF_PROPERTY: String = "\$ref"

override fun resolve(keyword: KeyWord): String? =
when (keyword) {
ID -> ID_PROPERTY
DEFINITIONS -> DEFINITIONS_PROPERTY
ANCHOR, COMPATIBILITY_DEFINITIONS, DYNAMIC_ANCHOR -> null
}
}

private object Draft4ReferenceFactory : ReferenceFactory {
override fun extractRef(
schemaDefinition: JsonObject,
context: SchemaLoaderContext,
): RefHolder? =
if (REF_PROPERTY in schemaDefinition) {
RefHolder.Simple(REF_PROPERTY, schemaDefinition.getStringRequired(REF_PROPERTY).let(context::ref))
} else {
null
}

override val allowOverriding: Boolean
get() = false
override val resolveRefPriorId: Boolean
get() = false

override fun recursiveResolutionEnabled(schemaDefinition: JsonObject): Boolean = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.github.optimumcode.json.schema.internal.factories.number

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.number.util.NumberComparisonAssertion
import io.github.optimumcode.json.schema.internal.factories.number.util.compareTo
import io.github.optimumcode.json.schema.internal.factories.number.util.number
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull

@Suppress("unused")
internal object Draft4MaximumAssertionFactory : AssertionFactory {
private const val EXCLUSIVE_MAXIMUM_PROPERTY = "exclusiveMaximum"

override val property: String
get() = "maximum"

override fun isApplicable(element: JsonElement): Boolean = element is JsonObject && element.contains(property)

override fun create(
element: JsonElement,
context: LoadingContext,
): JsonSchemaAssertion {
require(element is JsonObject) { "cannot extract $property property from ${element::class.simpleName}" }
val typeElement = requireNotNull(element[property]) { "no property $property found in element $element" }
val exclusive: Boolean =
element[EXCLUSIVE_MAXIMUM_PROPERTY]?.let {
require(it is JsonPrimitive) { "$EXCLUSIVE_MAXIMUM_PROPERTY must be a boolean" }
requireNotNull(it.booleanOrNull) { "$EXCLUSIVE_MAXIMUM_PROPERTY must be a valid boolean" }
} ?: false
return createFromProperty(typeElement, context.at(property), exclusive)
}

private fun createFromProperty(
element: JsonElement,
context: LoadingContext,
exclusive: Boolean,
): JsonSchemaAssertion {
require(element is JsonPrimitive) { "$property must be a number" }
val maximumValue: Number =
requireNotNull(element.number) { "$property must be a valid number" }
return NumberComparisonAssertion(
context.schemaPath,
maximumValue,
element.content,
errorMessage = if (exclusive) "must be less" else "must be less or equal to",
if (exclusive) {
{ a, b -> a < b }
} else {
{ a, b -> a <= b }
},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.github.optimumcode.json.schema.internal.factories.number

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.number.util.NumberComparisonAssertion
import io.github.optimumcode.json.schema.internal.factories.number.util.compareTo
import io.github.optimumcode.json.schema.internal.factories.number.util.number
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull

@Suppress("unused")
internal object Draft4MinimumAssertionFactory : AssertionFactory {
private const val EXCLUSIVE_MINIMUM_PROPERTY = "exclusiveMinimum"

override val property: String
get() = "minimum"

override fun isApplicable(element: JsonElement): Boolean = element is JsonObject && element.contains(property)

override fun create(
element: JsonElement,
context: LoadingContext,
): JsonSchemaAssertion {
require(element is JsonObject) { "cannot extract $property property from ${element::class.simpleName}" }
val typeElement = requireNotNull(element[property]) { "no property $property found in element $element" }
val exclusive: Boolean =
element[EXCLUSIVE_MINIMUM_PROPERTY]?.let {
require(it is JsonPrimitive) { "$EXCLUSIVE_MINIMUM_PROPERTY must be a boolean" }
requireNotNull(it.booleanOrNull) { "$EXCLUSIVE_MINIMUM_PROPERTY must be a valid boolean" }
} ?: false
return createFromProperty(typeElement, context.at(property), exclusive)
}

private fun createFromProperty(
element: JsonElement,
context: LoadingContext,
exclusive: Boolean,
): JsonSchemaAssertion {
require(element is JsonPrimitive) { "$property must be a number" }
val maximumValue: Number =
requireNotNull(element.number) { "$property must be a valid number" }
return NumberComparisonAssertion(
context.schemaPath,
maximumValue,
element.content,
errorMessage = if (exclusive) "must be greater" else "must be greater or equal to",
if (exclusive) {
{ a, b -> a > b }
} else {
{ a, b -> a >= b }
},
)
}
}
Loading

0 comments on commit d2d2f03

Please sign in to comment.