Skip to content

Commit

Permalink
Use Uri class from kmp-uri library in JsonSchemaLoader's public API (#…
Browse files Browse the repository at this point in the history
…133)

Resolves #132
  • Loading branch information
OptimumCode authored Jun 12, 2024
1 parent fff36e1 commit fcf9f70
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 44 deletions.
4 changes: 4 additions & 0 deletions api/json-schema-validator.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}

Expand Down Expand Up @@ -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<RefId, AssertionWithPath> =
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))
Expand All @@ -300,16 +302,15 @@ 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
}
} else {
null
}
}

private fun loadDefinitions(
schemaDefinition: JsonElement,
Expand Down Expand Up @@ -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,
Expand All @@ -460,16 +462,15 @@ 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(
context.schemaPath / refHolder.property,
refHolder.refId,
)
}
}

/**
* Used to identify the [location] where this [id] was defined
Expand Down Expand Up @@ -499,14 +500,11 @@ private data class DefaultLoadingContext(
val config: SchemaLoaderConfig,
val assertionFactories: List<AssertionFactory>,
override val customFormatValidators: Map<String, FormatValidator>,
) : 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)

Expand All @@ -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) {
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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) }
}
Expand Down Expand Up @@ -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,
)
Expand All @@ -651,9 +674,12 @@ private data class DefaultLoadingContext(
}
}

private fun Set<IdWithLocation>.resolvePath(path: String?): Uri {
return last().id.appendPathToParent(requireNotNull(path) { "path is null" })
}
private fun Set<IdWithLocation>.resolvePath(path: String?): Uri =
last().id.appendPathToParent(
requireNotNull(path) {
"path is null"
},
)

private fun Uri.appendPathToParent(path: String): Uri {
if (path.startsWith('/')) {
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -93,7 +100,7 @@ internal fun FunSpec.runTestSuites(

require(fs.exists(remoteSchemasDefinitions)) { "file $remoteSchemasDefinitions with remote schemas does not exist" }

val remoteSchemas: Map<String, JsonElement> = loadRemoteSchemas(fs, remoteSchemasDefinitions)
val remoteSchemas: Map<Uri, JsonElement> = loadRemoteSchemas(fs, remoteSchemasDefinitions)

require(fs.exists(testSuiteDir)) { "folder $testSuiteDir does not exist" }

Expand Down Expand Up @@ -131,24 +138,42 @@ internal fun FunSpec.runTestSuites(
private fun loadRemoteSchemas(
fs: FileSystem,
remoteSchemasDefinitions: Path,
): Map<String, JsonElement> =
): Map<Uri, JsonElement> =
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<Uri> {
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,
testSuiteDir: Path,
excludeSuites: Map<String, Set<String>>,
excludeTests: Map<String, Set<String>>,
schemaType: SchemaType?,
remoteSchemas: Map<String, JsonElement> = emptyMap(),
remoteSchemas: Map<Uri, JsonElement> = emptyMap(),
formatBehavior: FormatBehavior? = null,
) {
fs.list(testSuiteDir).forEach { testSuiteFile ->
Expand All @@ -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
}
Expand Down

0 comments on commit fcf9f70

Please sign in to comment.