Skip to content

Commit 95b1dbb

Browse files
committed
Add support for custom metaschemes and vocabylary
1 parent 38cca63 commit 95b1dbb

File tree

8 files changed

+173
-58
lines changed

8 files changed

+173
-58
lines changed

src/commonMain/kotlin/io/github/optimumcode/json/schema/SchemaType.kt

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import io.github.optimumcode.json.schema.internal.config.Draft7SchemaLoaderConfi
88
import kotlin.jvm.JvmStatic
99

1010
public enum class SchemaType(
11-
private val schemaId: Uri,
11+
internal val schemaId: Uri,
1212
internal val config: SchemaLoaderConfig,
1313
) {
1414
DRAFT_7(Uri.parse("http://json-schema.org/draft-07/schema"), Draft7SchemaLoaderConfig),
@@ -17,25 +17,29 @@ public enum class SchemaType(
1717
;
1818

1919
public companion object {
20-
private const val HTTP_SCHEMA: String = "http"
21-
private const val HTTPS_SCHEMA: String = "https"
22-
2320
@JvmStatic
2421
public fun find(schemaId: String): SchemaType? {
2522
val uri = Uri.parse(schemaId)
26-
if (uri.scheme.let { it != HTTP_SCHEMA && it != HTTPS_SCHEMA }) {
27-
// the schema in URI is unknown
28-
// so, it definitely is not a supported schema ID
29-
return null
30-
}
31-
return entries.find {
32-
it.schemaId.run {
33-
host == uri.host &&
34-
port == uri.port &&
35-
path == uri.path &&
36-
fragment == uri.fragment?.takeUnless(String::isEmpty)
37-
}
38-
}
23+
return findSchemaType(uri)
24+
}
25+
}
26+
}
27+
28+
private const val HTTP_SCHEMA: String = "http"
29+
private const val HTTPS_SCHEMA: String = "https"
30+
31+
internal fun findSchemaType(uri: Uri): SchemaType? {
32+
if (uri.scheme.let { it != HTTP_SCHEMA && it != HTTPS_SCHEMA }) {
33+
// the schema in URI is unknown
34+
// so, it definitely is not a supported schema ID
35+
return null
36+
}
37+
return SchemaType.entries.find {
38+
it.schemaId.run {
39+
host == uri.host &&
40+
port == uri.port &&
41+
path == uri.path &&
42+
fragment == uri.fragment?.takeUnless(String::isEmpty)
3943
}
4044
}
4145
}

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import io.github.optimumcode.json.schema.JsonSchema
1010
import io.github.optimumcode.json.schema.JsonSchemaLoader
1111
import io.github.optimumcode.json.schema.SchemaType
1212
import io.github.optimumcode.json.schema.extension.ExternalAssertionFactory
13+
import io.github.optimumcode.json.schema.findSchemaType
1314
import io.github.optimumcode.json.schema.internal.ReferenceFactory.RefHolder
1415
import io.github.optimumcode.json.schema.internal.ReferenceFactory.RefHolder.Recursive
1516
import io.github.optimumcode.json.schema.internal.ReferenceFactory.RefHolder.Simple
1617
import io.github.optimumcode.json.schema.internal.ReferenceValidator.PointerWithBaseId
1718
import io.github.optimumcode.json.schema.internal.ReferenceValidator.ReferenceLocation
19+
import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig.Vocabulary
1820
import io.github.optimumcode.json.schema.internal.factories.ExternalAssertionFactoryAdapter
1921
import io.github.optimumcode.json.schema.internal.util.getString
2022
import kotlinx.serialization.json.Json
@@ -30,13 +32,14 @@ internal class SchemaLoader : JsonSchemaLoader {
3032
private val references: MutableMap<RefId, AssertionWithPath> = linkedMapOf()
3133
private val usedRefs: MutableSet<ReferenceLocation> = linkedSetOf()
3234
private val extensionFactories: MutableMap<String, AssertionFactory> = linkedMapOf()
35+
private val customMetaSchemas: MutableMap<Uri, Pair<SchemaType, Vocabulary>> = linkedMapOf()
3336

3437
override fun register(
3538
schema: JsonElement,
3639
draft: SchemaType?,
3740
): JsonSchemaLoader =
3841
apply {
39-
loadSchemaData(schema, LoadingParameters(draft, references, usedRefs, extensionFactories.values))
42+
loadSchemaData(schema, createParameters(draft))
4043
}
4144

4245
override fun register(
@@ -56,12 +59,7 @@ internal class SchemaLoader : JsonSchemaLoader {
5659
apply {
5760
loadSchemaData(
5861
schema,
59-
LoadingParameters(
60-
draft,
61-
references,
62-
usedRefs,
63-
extensionFactories = extensionFactories.values,
64-
),
62+
createParameters(draft),
6563
Uri.parse(remoteUri),
6664
)
6765
}
@@ -99,7 +97,7 @@ internal class SchemaLoader : JsonSchemaLoader {
9997
val assertion: JsonSchemaAssertion =
10098
loadSchemaData(
10199
schemaElement,
102-
LoadingParameters(draft, references, usedRefs, extensionFactories.values),
100+
createParameters(draft),
103101
)
104102
validateReferences(references, usedRefs)
105103
return createSchema(
@@ -111,6 +109,20 @@ internal class SchemaLoader : JsonSchemaLoader {
111109
)
112110
}
113111

112+
private fun createParameters(draft: SchemaType?): LoadingParameters =
113+
LoadingParameters(
114+
defaultType = draft,
115+
references = references,
116+
usedRefs = usedRefs,
117+
extensionFactories = extensionFactories.values,
118+
registerMetaSchema = { uri, type, vocab ->
119+
val prev = customMetaSchemas.put(uri, type to vocab)
120+
require(prev == null) { "duplicated meta-schema with uri '$uri'" }
121+
},
122+
resolveCustomVocabulary = { customMetaSchemas[it]?.second },
123+
resolveCustomMetaSchemaType = { customMetaSchemas[it]?.first },
124+
)
125+
114126
private fun addExtensionFactory(extensionFactory: ExternalAssertionFactory) {
115127
for (schemaType in SchemaType.entries) {
116128
val match =
@@ -174,22 +186,35 @@ internal object IsolatedLoader : JsonSchemaLoader {
174186
}
175187
}
176188

189+
@Suppress("detekt:LongParameterList")
177190
private class LoadingParameters(
178191
val defaultType: SchemaType?,
179192
val references: MutableMap<RefId, AssertionWithPath>,
180193
val usedRefs: MutableSet<ReferenceLocation>,
181194
val extensionFactories: Collection<AssertionFactory> = emptySet(),
195+
val resolveCustomMetaSchemaType: (Uri) -> SchemaType? = { null },
196+
val resolveCustomVocabulary: (Uri) -> Vocabulary? = { null },
197+
val registerMetaSchema: (Uri, SchemaType, Vocabulary) -> Unit = { _, _, _ -> },
182198
)
183199

184200
private fun loadSchemaData(
185201
schemaDefinition: JsonElement,
186202
parameters: LoadingParameters,
187203
externalUri: Uri? = null,
188204
): JsonSchemaAssertion {
189-
val schemaType = extractSchemaType(schemaDefinition, parameters.defaultType)
205+
val schema: Uri? = extractSchema(schemaDefinition)?.let(Uri::parse)
206+
val schemaType: SchemaType = resolveSchemaType(schema, parameters.defaultType, parameters.resolveCustomMetaSchemaType)
190207
val baseId: Uri = extractID(schemaDefinition, schemaType.config) ?: externalUri ?: Uri.EMPTY
208+
val schemaVocabulary: Vocabulary? =
209+
schemaType.config.createVocabulary(schemaDefinition)?.also {
210+
parameters.registerMetaSchema(baseId, schemaType, it)
211+
}
212+
val vocabulary: Vocabulary =
213+
schemaVocabulary
214+
?: schema?.let(parameters.resolveCustomVocabulary)
215+
?: schemaType.config.defaultVocabulary
191216
val assertionFactories =
192-
schemaType.config.factories(schemaDefinition).let {
217+
schemaType.config.factories(schemaDefinition, vocabulary).let {
193218
if (parameters.extensionFactories.isEmpty()) {
194219
it
195220
} else {
@@ -245,22 +270,31 @@ private class LoadResult(
245270
val usedRefs: Set<RefId>,
246271
)
247272

248-
private fun extractSchemaType(
249-
schemaDefinition: JsonElement,
273+
private fun resolveSchemaType(
274+
schema: Uri?,
250275
defaultType: SchemaType?,
276+
resolveCustomMetaSchemaType: (Uri) -> SchemaType?,
251277
): SchemaType {
252278
val schemaType: SchemaType? =
253-
if (schemaDefinition is JsonObject) {
254-
schemaDefinition[SCHEMA_PROPERTY]?.let {
255-
require(it is JsonPrimitive && it.isString) { "$SCHEMA_PROPERTY must be a string" }
256-
SchemaType.find(it.content) ?: throw IllegalArgumentException("unsupported schema type ${it.content}")
257-
}
258-
} else {
259-
null
279+
schema?.let {
280+
findSchemaType(it)
281+
?: resolveCustomMetaSchemaType(it)
282+
?: throw IllegalArgumentException("unsupported schema type $it")
260283
}
261284
return schemaType ?: defaultType ?: SchemaType.entries.last()
262285
}
263286

287+
private fun extractSchema(schemaDefinition: JsonElement): String? {
288+
return if (schemaDefinition is JsonObject) {
289+
schemaDefinition[SCHEMA_PROPERTY]?.let {
290+
require(it is JsonPrimitive && it.isString) { "$SCHEMA_PROPERTY must be a string" }
291+
it.content
292+
}
293+
} else {
294+
null
295+
}
296+
}
297+
264298
private fun loadDefinitions(
265299
schemaDefinition: JsonElement,
266300
context: DefaultLoadingContext,

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoaderConfig.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,23 @@ import kotlinx.serialization.json.JsonObject
66
internal interface SchemaLoaderConfig {
77
val allFactories: List<AssertionFactory>
88

9-
fun factories(schemaDefinition: JsonElement): List<AssertionFactory>
9+
val defaultVocabulary: Vocabulary
10+
11+
fun createVocabulary(schemaDefinition: JsonElement): Vocabulary?
12+
13+
fun factories(
14+
schemaDefinition: JsonElement,
15+
vocabulary: Vocabulary,
16+
): List<AssertionFactory>
1017

1118
val keywordResolver: KeyWordResolver
1219
val referenceFactory: ReferenceFactory
20+
21+
class Vocabulary(
22+
private val vocabularies: Map<String, Boolean> = emptyMap(),
23+
) {
24+
fun enabled(vocabulary: String): Boolean = vocabularies[vocabulary] ?: false
25+
}
1326
}
1427

1528
/**

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/config/Draft201909SchemaLoaderConfig.kt

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import io.github.optimumcode.json.schema.internal.KeyWordResolver
1111
import io.github.optimumcode.json.schema.internal.ReferenceFactory
1212
import io.github.optimumcode.json.schema.internal.ReferenceFactory.RefHolder
1313
import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig
14+
import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig.Vocabulary
1415
import io.github.optimumcode.json.schema.internal.SchemaLoaderContext
1516
import io.github.optimumcode.json.schema.internal.config.Draft201909KeyWordResolver.REC_ANCHOR_PROPERTY
1617
import io.github.optimumcode.json.schema.internal.config.Draft201909KeyWordResolver.REC_REF_PROPERTY
@@ -53,6 +54,8 @@ import io.github.optimumcode.json.schema.internal.factories.string.MaxLengthAsse
5354
import io.github.optimumcode.json.schema.internal.factories.string.MinLengthAssertionFactory
5455
import io.github.optimumcode.json.schema.internal.factories.string.PatternAssertionFactory
5556
import io.github.optimumcode.json.schema.internal.util.getStringRequired
57+
import io.github.optimumcode.json.schema.internal.wellknown.Draft201909
58+
import kotlinx.serialization.json.Json
5659
import kotlinx.serialization.json.JsonElement
5760
import kotlinx.serialization.json.JsonObject
5861
import kotlinx.serialization.json.boolean
@@ -109,18 +112,40 @@ internal object Draft201909SchemaLoaderConfig : SchemaLoaderConfig {
109112
TypeAssertionFactory,
110113
)
111114

115+
override val defaultVocabulary: Vocabulary =
116+
requireNotNull(createVocabulary(Json.parseToJsonElement(Draft201909.DRAFT201909_SCHEMA.content))) {
117+
"draft schema must have a vocabulary"
118+
}
119+
112120
override val allFactories: List<AssertionFactory> =
113121
applicatorFactories + validationFactories
114122

115-
override fun factories(schemaDefinition: JsonElement): List<AssertionFactory> {
123+
override fun createVocabulary(schemaDefinition: JsonElement): Vocabulary? {
124+
if (schemaDefinition !is JsonObject || VOCABULARY_PROPERTY !in schemaDefinition) {
125+
return null
126+
}
127+
val vocabulary = schemaDefinition.getValue(VOCABULARY_PROPERTY)
128+
require(vocabulary is JsonObject) { "$VOCABULARY_PROPERTY must be a JSON object" }
129+
if (vocabulary.isEmpty()) {
130+
return null
131+
}
132+
return Vocabulary(
133+
vocabularies =
134+
vocabulary.mapValues { (_, state) -> state.jsonPrimitive.boolean },
135+
)
136+
}
137+
138+
override fun factories(
139+
schemaDefinition: JsonElement,
140+
vocabulary: Vocabulary,
141+
): List<AssertionFactory> {
116142
if (schemaDefinition !is JsonObject) {
117143
// no point to return any factories here
118144
return emptyList()
119145
}
120-
val vocabularyElement = schemaDefinition[VOCABULARY_PROPERTY] ?: return allFactories()
121-
require(vocabularyElement is JsonObject) { "$VOCABULARY_PROPERTY must be a JSON object" }
122-
val applicators = vocabularyElement[APPLICATOR_VOCABULARY_URI]?.jsonPrimitive?.boolean ?: true
123-
val validations = vocabularyElement[VALIDATION_VOCABULARY_URI]?.jsonPrimitive?.boolean ?: true
146+
147+
val applicators = vocabulary.enabled(APPLICATOR_VOCABULARY_URI)
148+
val validations = vocabulary.enabled(VALIDATION_VOCABULARY_URI)
124149
return when {
125150
applicators && validations -> allFactories()
126151
applicators -> applicatorFactories

0 commit comments

Comments
 (0)