Skip to content
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

Use Uri class from kmp-uri library in JsonSchemaLoader's public API #133

Merged
merged 2 commits into from
Jun 12, 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
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 @@
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)

Check warning on line 52 in src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt

View check run for this annotation

Codecov / codecov/patch

src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt#L52

Added line #L52 was not covered by tests

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)

Check warning on line 72 in src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt

View check run for this annotation

Codecov / codecov/patch

src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt#L72

Added line #L72 was not covered by tests

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 @@

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 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 @@
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 @@
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 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 @@
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 @@
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 @@
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 @@

else -> this
}
}

override fun ref(refId: String): RefId {
// library parsed fragment as empty if # is in the URI
Expand All @@ -581,18 +585,32 @@
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 @@
!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 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"

Check warning on line 680 in src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaLoader.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L680 was not covered by tests
},
)

private fun Uri.appendPathToParent(path: String): Uri {
if (path.startsWith('/')) {
Expand All @@ -669,7 +695,8 @@
.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
Loading