Skip to content

Add support for draft 4 #140

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 2 commits into from
Jun 19, 2024
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
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
Loading