Skip to content

Support vocabulary customization in custom meta-schemes #66

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 3 commits into from
Feb 20, 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ val valid = schema.validate(elementToValidate, errors::add)
| $ref | Supported |
| $recursiveRef | Supported |
| $defs/definitions | Supported. Definitions are loaded and can be referenced |
| $vocabulary | Supported. You can disable and enable vocabularies through custom meta-schemes |

- Assertions

Expand Down Expand Up @@ -237,6 +238,7 @@ val valid = schema.validate(elementToValidate, errors::add)
| $ref | Supported |
| $dynamicRef/$dynamicAnchor | Supported |
| $defs/definitions | Supported. Definitions are loaded and can be referenced |
| $vocabulary | Supported. You can disable and enable vocabularies through custom meta-schemes |

- Assertions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import io.github.optimumcode.json.schema.internal.config.Draft7SchemaLoaderConfi
import kotlin.jvm.JvmStatic

public enum class SchemaType(
private val schemaId: Uri,
internal val schemaId: Uri,
internal val config: SchemaLoaderConfig,
) {
DRAFT_7(Uri.parse("http://json-schema.org/draft-07/schema"), Draft7SchemaLoaderConfig),
Expand All @@ -17,25 +17,29 @@ public enum class SchemaType(
;

public companion object {
private const val HTTP_SCHEMA: String = "http"
private const val HTTPS_SCHEMA: String = "https"

@JvmStatic
public fun find(schemaId: String): SchemaType? {
val uri = Uri.parse(schemaId)
if (uri.scheme.let { it != HTTP_SCHEMA && it != HTTPS_SCHEMA }) {
// the schema in URI is unknown
// so, it definitely is not a supported schema ID
return null
}
return entries.find {
it.schemaId.run {
host == uri.host &&
port == uri.port &&
path == uri.path &&
fragment == uri.fragment?.takeUnless(String::isEmpty)
}
}
return findSchemaType(uri)
}
}
}

private const val HTTP_SCHEMA: String = "http"
private const val HTTPS_SCHEMA: String = "https"

internal fun findSchemaType(uri: Uri): SchemaType? {
if (uri.scheme.let { it != HTTP_SCHEMA && it != HTTPS_SCHEMA }) {
// the schema in URI is unknown
// so, it definitely is not a supported schema ID
return null
}
return SchemaType.entries.find {
it.schemaId.run {
host == uri.host &&
port == uri.port &&
path == uri.path &&
fragment == uri.fragment?.takeUnless(String::isEmpty)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import io.github.optimumcode.json.schema.JsonSchema
import io.github.optimumcode.json.schema.JsonSchemaLoader
import io.github.optimumcode.json.schema.SchemaType
import io.github.optimumcode.json.schema.extension.ExternalAssertionFactory
import io.github.optimumcode.json.schema.findSchemaType
import io.github.optimumcode.json.schema.internal.ReferenceFactory.RefHolder
import io.github.optimumcode.json.schema.internal.ReferenceFactory.RefHolder.Recursive
import io.github.optimumcode.json.schema.internal.ReferenceFactory.RefHolder.Simple
import io.github.optimumcode.json.schema.internal.ReferenceValidator.PointerWithBaseId
import io.github.optimumcode.json.schema.internal.ReferenceValidator.ReferenceLocation
import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig.Vocabulary
import io.github.optimumcode.json.schema.internal.factories.ExternalAssertionFactoryAdapter
import io.github.optimumcode.json.schema.internal.util.getString
import kotlinx.serialization.json.Json
Expand All @@ -30,13 +32,14 @@ internal class SchemaLoader : JsonSchemaLoader {
private val references: MutableMap<RefId, AssertionWithPath> = linkedMapOf()
private val usedRefs: MutableSet<ReferenceLocation> = linkedSetOf()
private val extensionFactories: MutableMap<String, AssertionFactory> = linkedMapOf()
private val customMetaSchemas: MutableMap<Uri, Pair<SchemaType, Vocabulary>> = linkedMapOf()

override fun register(
schema: JsonElement,
draft: SchemaType?,
): JsonSchemaLoader =
apply {
loadSchemaData(schema, LoadingParameters(draft, references, usedRefs, extensionFactories.values))
loadSchemaData(schema, createParameters(draft))
}

override fun register(
Expand All @@ -56,12 +59,7 @@ internal class SchemaLoader : JsonSchemaLoader {
apply {
loadSchemaData(
schema,
LoadingParameters(
draft,
references,
usedRefs,
extensionFactories = extensionFactories.values,
),
createParameters(draft),
Uri.parse(remoteUri),
)
}
Expand Down Expand Up @@ -99,7 +97,7 @@ internal class SchemaLoader : JsonSchemaLoader {
val assertion: JsonSchemaAssertion =
loadSchemaData(
schemaElement,
LoadingParameters(draft, references, usedRefs, extensionFactories.values),
createParameters(draft),
)
validateReferences(references, usedRefs)
return createSchema(
Expand All @@ -111,6 +109,20 @@ internal class SchemaLoader : JsonSchemaLoader {
)
}

private fun createParameters(draft: SchemaType?): LoadingParameters =
LoadingParameters(
defaultType = draft,
references = references,
usedRefs = usedRefs,
extensionFactories = extensionFactories.values,
registerMetaSchema = { uri, type, vocab ->
val prev = customMetaSchemas.put(uri, type to vocab)
require(prev == null) { "duplicated meta-schema with uri '$uri'" }
},
resolveCustomVocabulary = { customMetaSchemas[it]?.second },
resolveCustomMetaSchemaType = { customMetaSchemas[it]?.first },
)

private fun addExtensionFactory(extensionFactory: ExternalAssertionFactory) {
for (schemaType in SchemaType.entries) {
val match =
Expand Down Expand Up @@ -174,22 +186,35 @@ internal object IsolatedLoader : JsonSchemaLoader {
}
}

@Suppress("detekt:LongParameterList")
private class LoadingParameters(
val defaultType: SchemaType?,
val references: MutableMap<RefId, AssertionWithPath>,
val usedRefs: MutableSet<ReferenceLocation>,
val extensionFactories: Collection<AssertionFactory> = emptySet(),
val resolveCustomMetaSchemaType: (Uri) -> SchemaType? = { null },
val resolveCustomVocabulary: (Uri) -> Vocabulary? = { null },
val registerMetaSchema: (Uri, SchemaType, Vocabulary) -> Unit = { _, _, _ -> },
)

private fun loadSchemaData(
schemaDefinition: JsonElement,
parameters: LoadingParameters,
externalUri: Uri? = null,
): JsonSchemaAssertion {
val schemaType = extractSchemaType(schemaDefinition, parameters.defaultType)
val schema: Uri? = extractSchema(schemaDefinition)?.let(Uri::parse)
val schemaType: SchemaType = resolveSchemaType(schema, parameters.defaultType, parameters.resolveCustomMetaSchemaType)
val baseId: Uri = extractID(schemaDefinition, schemaType.config) ?: externalUri ?: Uri.EMPTY
val schemaVocabulary: Vocabulary? =
schemaType.config.createVocabulary(schemaDefinition)?.also {
parameters.registerMetaSchema(baseId, schemaType, it)
}
val vocabulary: Vocabulary =
schemaVocabulary
?: schema?.let(parameters.resolveCustomVocabulary)
?: schemaType.config.defaultVocabulary
val assertionFactories =
schemaType.config.factories(schemaDefinition).let {
schemaType.config.factories(schemaDefinition, vocabulary).let {
if (parameters.extensionFactories.isEmpty()) {
it
} else {
Expand Down Expand Up @@ -245,22 +270,31 @@ private class LoadResult(
val usedRefs: Set<RefId>,
)

private fun extractSchemaType(
schemaDefinition: JsonElement,
private fun resolveSchemaType(
schema: Uri?,
defaultType: SchemaType?,
resolveCustomMetaSchemaType: (Uri) -> SchemaType?,
): SchemaType {
val schemaType: SchemaType? =
if (schemaDefinition is JsonObject) {
schemaDefinition[SCHEMA_PROPERTY]?.let {
require(it is JsonPrimitive && it.isString) { "$SCHEMA_PROPERTY must be a string" }
SchemaType.find(it.content) ?: throw IllegalArgumentException("unsupported schema type ${it.content}")
}
} else {
null
schema?.let {
findSchemaType(it)
?: resolveCustomMetaSchemaType(it)
?: throw IllegalArgumentException("unsupported schema type $it")
}
return schemaType ?: defaultType ?: SchemaType.entries.last()
}

private fun extractSchema(schemaDefinition: JsonElement): String? {
return if (schemaDefinition is JsonObject) {
schemaDefinition[SCHEMA_PROPERTY]?.let {
require(it is JsonPrimitive && it.isString) { "$SCHEMA_PROPERTY must be a string" }
it.content
}
} else {
null
}
}

private fun loadDefinitions(
schemaDefinition: JsonElement,
context: DefaultLoadingContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,23 @@ import kotlinx.serialization.json.JsonObject
internal interface SchemaLoaderConfig {
val allFactories: List<AssertionFactory>

fun factories(schemaDefinition: JsonElement): List<AssertionFactory>
val defaultVocabulary: Vocabulary

fun createVocabulary(schemaDefinition: JsonElement): Vocabulary?

fun factories(
schemaDefinition: JsonElement,
vocabulary: Vocabulary,
): List<AssertionFactory>

val keywordResolver: KeyWordResolver
val referenceFactory: ReferenceFactory

class Vocabulary(
private val vocabularies: Map<String, Boolean> = emptyMap(),
) {
fun enabled(vocabulary: String): Boolean = vocabularies[vocabulary] ?: false
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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.SchemaLoaderConfig.Vocabulary
import io.github.optimumcode.json.schema.internal.SchemaLoaderContext
import io.github.optimumcode.json.schema.internal.config.Draft201909KeyWordResolver.REC_ANCHOR_PROPERTY
import io.github.optimumcode.json.schema.internal.config.Draft201909KeyWordResolver.REC_REF_PROPERTY
Expand Down Expand Up @@ -53,6 +54,8 @@ import io.github.optimumcode.json.schema.internal.factories.string.MaxLengthAsse
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 io.github.optimumcode.json.schema.internal.wellknown.Draft201909
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
Expand Down Expand Up @@ -109,18 +112,40 @@ internal object Draft201909SchemaLoaderConfig : SchemaLoaderConfig {
TypeAssertionFactory,
)

override val defaultVocabulary: Vocabulary =
requireNotNull(createVocabulary(Json.parseToJsonElement(Draft201909.DRAFT201909_SCHEMA.content))) {
"draft schema must have a vocabulary"
}

override val allFactories: List<AssertionFactory> =
applicatorFactories + validationFactories

override fun factories(schemaDefinition: JsonElement): List<AssertionFactory> {
override fun createVocabulary(schemaDefinition: JsonElement): Vocabulary? {
if (schemaDefinition !is JsonObject || VOCABULARY_PROPERTY !in schemaDefinition) {
return null
}
val vocabulary = schemaDefinition.getValue(VOCABULARY_PROPERTY)
require(vocabulary is JsonObject) { "$VOCABULARY_PROPERTY must be a JSON object" }
if (vocabulary.isEmpty()) {
return null
}
return Vocabulary(
vocabularies =
vocabulary.mapValues { (_, state) -> state.jsonPrimitive.boolean },
)
}

override fun factories(
schemaDefinition: JsonElement,
vocabulary: Vocabulary,
): List<AssertionFactory> {
if (schemaDefinition !is JsonObject) {
// no point to return any factories here
return emptyList()
}
val vocabularyElement = schemaDefinition[VOCABULARY_PROPERTY] ?: return allFactories()
require(vocabularyElement is JsonObject) { "$VOCABULARY_PROPERTY must be a JSON object" }
val applicators = vocabularyElement[APPLICATOR_VOCABULARY_URI]?.jsonPrimitive?.boolean ?: true
val validations = vocabularyElement[VALIDATION_VOCABULARY_URI]?.jsonPrimitive?.boolean ?: true

val applicators = vocabulary.enabled(APPLICATOR_VOCABULARY_URI)
val validations = vocabulary.enabled(VALIDATION_VOCABULARY_URI)
return when {
applicators && validations -> allFactories()
applicators -> applicatorFactories
Expand Down
Loading