From 3e4ded131e994a5cca2d95c99c8389548a084877 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Wed, 12 Jun 2024 18:38:25 +0400 Subject: [PATCH 1/2] Add Uri to JsonSchemaLoader API --- .../json/schema/JsonSchemaLoader.kt | 30 +++++ .../json/schema/internal/SchemaLoader.kt | 103 +++++++++++------- .../schema/suite/AbstractSchemaTestSuite.kt | 38 ++++++- 3 files changed, 127 insertions(+), 44 deletions(-) diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt index 98be9d5b..a30ee63c 100644 --- a/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt @@ -1,5 +1,6 @@ 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_7 @@ -36,15 +37,44 @@ public interface JsonSchemaLoader { draft: SchemaType?, ): JsonSchemaLoader + @Deprecated( + message = "This method will be removed in a future release. Please use the alternative that accepts Uri type", + level = DeprecationLevel.WARNING, + replaceWith = + ReplaceWith( + imports = ["com.eygraber.uri.Uri"], + expression = "register(schema, Uri.parse(remoteUri))", + ), + ) public fun register( schema: JsonElement, remoteUri: String, + ): JsonSchemaLoader = register(schema, Uri.parse(remoteUri), null) + + public fun register( + schema: JsonElement, + remoteUri: Uri, ): JsonSchemaLoader = register(schema, remoteUri, null) + @Deprecated( + message = "This method will be removed in a future release. Please use the alternative that accepts Uri type", + level = DeprecationLevel.WARNING, + replaceWith = + ReplaceWith( + imports = ["com.eygraber.uri.Uri"], + expression = "register(schema, Uri.parse(remoteUri), draft)", + ), + ) public fun register( schema: JsonElement, remoteUri: String, draft: SchemaType?, + ): JsonSchemaLoader = register(schema, Uri.parse(remoteUri), draft) + + public fun register( + schema: JsonElement, + remoteUri: Uri, + draft: SchemaType?, ): JsonSchemaLoader public fun withExtensions( diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt index 97d8ebd6..11b7ccbc 100644 --- a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt @@ -58,14 +58,14 @@ internal class SchemaLoader : JsonSchemaLoader { override fun register( schema: JsonElement, - remoteUri: String, + remoteUri: Uri, draft: SchemaType?, ): JsonSchemaLoader = apply { loadSchemaData( schema, createParameters(draft), - Uri.parse(remoteUri), + remoteUri, ) } @@ -268,13 +268,15 @@ private fun validateReferences( private fun createSchema(result: LoadResult): JsonSchema { val dynamicRefs = - result.references.asSequence() + result.references + .asSequence() .filter { it.value.dynamic } .map { it.key } .toSet() // pre-filter references to get rid of unused references val usedReferencesWithPath: Map = - result.references.asSequence() + result.references + .asSequence() .filter { it.key in result.usedRefs || it.key in dynamicRefs } .associate { it.key to it.value } return JsonSchema(result.assertion, DefaultReferenceResolver(usedReferencesWithPath)) @@ -300,8 +302,8 @@ private fun resolveSchemaType( return schemaType ?: defaultType ?: SchemaType.entries.last() } -private fun extractSchema(schemaDefinition: JsonElement): String? { - return if (schemaDefinition is JsonObject) { +private fun extractSchema(schemaDefinition: JsonElement): String? = + if (schemaDefinition is JsonObject) { schemaDefinition[SCHEMA_PROPERTY]?.let { require(it is JsonPrimitive && it.isString) { "$SCHEMA_PROPERTY must be a string" } it.content @@ -309,7 +311,6 @@ private fun extractSchema(schemaDefinition: JsonElement): String? { } else { null } -} private fun loadDefinitions( schemaDefinition: JsonElement, @@ -436,7 +437,8 @@ private fun loadJsonSchemaRoot( refAssertion: JsonSchemaAssertion?, ): JsonSchemaRoot { val assertions = - context.assertionFactories.filter { it.isApplicable(schemaDefinition) } + context.assertionFactories + .filter { it.isApplicable(schemaDefinition) } .map { it.create( schemaDefinition, @@ -460,8 +462,8 @@ private fun loadJsonSchemaRoot( private fun loadRefAssertion( refHolder: RefHolder, context: DefaultLoadingContext, -): JsonSchemaAssertion { - return when (refHolder) { +): JsonSchemaAssertion = + when (refHolder) { is Simple -> RefSchemaAssertion(context.schemaPath / refHolder.property, refHolder.refId) is Recursive -> RecursiveRefSchemaAssertion( @@ -469,7 +471,6 @@ private fun loadRefAssertion( refHolder.refId, ) } -} /** * Used to identify the [location] where this [id] was defined @@ -499,14 +500,11 @@ private data class DefaultLoadingContext( val config: SchemaLoaderConfig, val assertionFactories: List, override val customFormatValidators: Map, -) : LoadingContext, SchemaLoaderContext { - override fun at(property: String): DefaultLoadingContext { - return copy(schemaPath = schemaPath / property) - } +) : LoadingContext, + SchemaLoaderContext { + override fun at(property: String): DefaultLoadingContext = copy(schemaPath = schemaPath / property) - override fun at(index: Int): DefaultLoadingContext { - return copy(schemaPath = schemaPath[index]) - } + override fun at(index: Int): DefaultLoadingContext = copy(schemaPath = schemaPath[index]) override fun schemaFrom(element: JsonElement): JsonSchemaAssertion = loadSchema(element, this) @@ -527,7 +525,8 @@ private data class DefaultLoadingContext( for ((baseId, location) in additionalIDs) { val relativePointer = location.relative(schemaPath) val referenceId: RefId = - baseId.buildUpon() + baseId + .buildUpon() .encodedFragment(relativePointer.toString()) .buildRefId() if (referenceId.uri == id) { @@ -549,12 +548,18 @@ private data class DefaultLoadingContext( dynamic: Boolean, ) { require(ANCHOR_REGEX.matches(anchor)) { "$anchor must match the format ${ANCHOR_REGEX.pattern}" } - val refId = additionalIDs.last().id.buildUpon().fragment(anchor).buildRefId() + val refId = + additionalIDs + .last() + .id + .buildUpon() + .fragment(anchor) + .buildRefId() register(refId, assertion, dynamic) } - fun addId(additionalId: Uri): DefaultLoadingContext { - return when { + fun addId(additionalId: Uri): DefaultLoadingContext = + when { additionalId.isAbsolute -> copy(additionalIDs = additionalIDs + IdWithLocation(additionalId, schemaPath)) additionalId.isRelative && !additionalId.path.isNullOrBlank() -> copy( @@ -570,7 +575,6 @@ private data class DefaultLoadingContext( else -> this } - } override fun ref(refId: String): RefId { // library parsed fragment as empty if # is in the URI @@ -581,18 +585,32 @@ private data class DefaultLoadingContext( return when { refUri.isAbsolute -> refUri.buildRefId() // the ref is absolute and should be resolved from current base URI host:port part - refId.startsWith('/') -> additionalIDs.last().id.buildUpon().encodedPath(refUri.path).buildRefId() + refId.startsWith('/') -> + additionalIDs + .last() + .id + .buildUpon() + .encodedPath(refUri.path) + .buildRefId() // in this case the ref must be resolved from the current base ID !refUri.path.isNullOrBlank() -> - additionalIDs.resolvePath(refUri.path).run { - if (refUri.fragment.isNullOrBlank()) { - this - } else { - buildUpon().encodedFragment(refUri.fragment).build() - } - }.buildRefId() - - refUri.fragment != null -> additionalIDs.last().id.buildUpon().encodedFragment(refUri.fragment).buildRefId() + additionalIDs + .resolvePath(refUri.path) + .run { + if (refUri.fragment.isNullOrBlank()) { + this + } else { + buildUpon().encodedFragment(refUri.fragment).build() + } + }.buildRefId() + + refUri.fragment != null -> + additionalIDs + .last() + .id + .buildUpon() + .encodedFragment(refUri.fragment) + .buildRefId() else -> throw IllegalArgumentException("invalid reference '$refId'") }.also { usedRef += ReferenceLocation(schemaPath, it) } } @@ -632,7 +650,12 @@ private data class DefaultLoadingContext( !id.fragment.isNullOrBlank() -> register( // register JSON schema by fragment - additionalIDs.last().id.buildUpon().encodedFragment(id.fragment).buildRefId(), + additionalIDs + .last() + .id + .buildUpon() + .encodedFragment(id.fragment) + .buildRefId(), assertion, dynamic, ) @@ -651,9 +674,12 @@ private data class DefaultLoadingContext( } } -private fun Set.resolvePath(path: String?): Uri { - return last().id.appendPathToParent(requireNotNull(path) { "path is null" }) -} +private fun Set.resolvePath(path: String?): Uri = + last().id.appendPathToParent( + requireNotNull(path) { + "path is null" + }, + ) private fun Uri.appendPathToParent(path: String): Uri { if (path.startsWith('/')) { @@ -669,7 +695,8 @@ private fun Uri.appendPathToParent(path: String): Uri { .path(null) // reset path in builder .apply { if (pathSegments.isEmpty()) return@apply - pathSegments.asSequence() + pathSegments + .asSequence() .take(pathSegments.size - 1) // drop last path segment .forEach(this::appendPath) } diff --git a/test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/AbstractSchemaTestSuite.kt b/test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/AbstractSchemaTestSuite.kt index 7ec899e7..20741e6d 100644 --- a/test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/AbstractSchemaTestSuite.kt +++ b/test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/AbstractSchemaTestSuite.kt @@ -1,5 +1,6 @@ package io.github.optimumcode.json.schema.suite +import com.eygraber.uri.Uri import io.github.optimumcode.json.schema.ErrorCollector import io.github.optimumcode.json.schema.FormatBehavior import io.github.optimumcode.json.schema.FormatBehavior.ANNOTATION_AND_ASSERTION @@ -13,10 +14,16 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.kotest.mpp.env import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement @@ -93,7 +100,7 @@ internal fun FunSpec.runTestSuites( require(fs.exists(remoteSchemasDefinitions)) { "file $remoteSchemasDefinitions with remote schemas does not exist" } - val remoteSchemas: Map = loadRemoteSchemas(fs, remoteSchemasDefinitions) + val remoteSchemas: Map = loadRemoteSchemas(fs, remoteSchemasDefinitions) require(fs.exists(testSuiteDir)) { "folder $testSuiteDir does not exist" } @@ -131,16 +138,34 @@ internal fun FunSpec.runTestSuites( private fun loadRemoteSchemas( fs: FileSystem, remoteSchemasDefinitions: Path, -): Map = +): Map = fs.openReadOnly(remoteSchemasDefinitions).use { fh -> fh.source().use { Json.decodeFromBufferedSource( - MapSerializer(String.serializer(), JsonElement.serializer()), + MapSerializer(UriSerializer, JsonElement.serializer()), it.buffer(), ) } } +private object UriSerializer : KSerializer { + override val descriptor: SerialDescriptor + get() = + PrimitiveSerialDescriptor( + "com.eygraber.uri.Uri", + kind = PrimitiveKind.STRING, + ) + + override fun deserialize(decoder: Decoder): Uri = Uri.parse(decoder.decodeString()) + + override fun serialize( + encoder: Encoder, + value: Uri, + ) { + encoder.encodeString(value.toString()) + } +} + @OptIn(ExperimentalSerializationApi::class) private fun FunSpec.executeFromDirectory( fs: FileSystem, @@ -148,7 +173,7 @@ private fun FunSpec.executeFromDirectory( excludeSuites: Map>, excludeTests: Map>, schemaType: SchemaType?, - remoteSchemas: Map = emptyMap(), + remoteSchemas: Map = emptyMap(), formatBehavior: FormatBehavior? = null, ) { fs.list(testSuiteDir).forEach { testSuiteFile -> @@ -170,14 +195,15 @@ private fun FunSpec.executeFromDirectory( } } val schemaLoader = - JsonSchemaLoader.create() + JsonSchemaLoader + .create() .apply { formatBehavior?.also { withSchemaOption(SchemaOption.FORMAT_BEHAVIOR_OPTION, it) } SchemaType.entries.forEach(::registerWellKnown) for ((uri, schema) in remoteSchemas) { - if (uri.contains("draft4", ignoreCase = true)) { + if (uri.toString().contains("draft4", ignoreCase = true)) { // skip draft4 schemas continue } From 623d80dfeac7cebc7a9ceb14f8cf9055233c9ebf Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Wed, 12 Jun 2024 18:39:17 +0400 Subject: [PATCH 2/2] Generate API dump --- api/json-schema-validator.api | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/json-schema-validator.api b/api/json-schema-validator.api index b83f9ee3..f3dc2b5e 100644 --- a/api/json-schema-validator.api +++ b/api/json-schema-validator.api @@ -131,6 +131,8 @@ public abstract interface class io/github/optimumcode/json/schema/JsonSchemaLoad public abstract fun register (Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; public abstract fun register (Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; public abstract fun register (Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; + public abstract fun register (Lkotlinx/serialization/json/JsonElement;Lcom/eygraber/uri/Uri;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; + public abstract fun register (Lkotlinx/serialization/json/JsonElement;Lcom/eygraber/uri/Uri;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; public abstract fun register (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; @@ -151,7 +153,9 @@ public final class io/github/optimumcode/json/schema/JsonSchemaLoader$DefaultImp public static fun fromJsonElement (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchema; public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; + public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;Lcom/eygraber/uri/Uri;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; + public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; public static fun registerWellKnown (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader; }