Skip to content

Fix algorithm for dynamic reference resolution #101

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
Apr 22, 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
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal interface AssertionContext : ExternalAssertionContext {

fun pushSchemaPath(
path: JsonPointer,
baseId: Uri,
scopeId: Uri,
)

fun popSchemaPath()
Expand Down Expand Up @@ -110,9 +110,9 @@ internal data class DefaultAssertionContext(

override fun pushSchemaPath(
path: JsonPointer,
baseId: Uri,
scopeId: Uri,
) {
referenceResolver.pushSchemaPath(path, baseId)
referenceResolver.pushSchemaPath(path, scopeId)
}

override fun popSchemaPath() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import io.github.optimumcode.json.schema.ErrorCollector
import kotlinx.serialization.json.JsonElement

internal class JsonSchemaRoot(
private val baseId: Uri,
private val scopeId: Uri,
private val schemaPath: JsonPointer,
private val assertions: Collection<JsonSchemaAssertion>,
private val canBeReferencedRecursively: Boolean,
Expand All @@ -22,7 +22,7 @@ internal class JsonSchemaRoot(
context.resetRecursiveRoot()
}
var result = true
context.pushSchemaPath(schemaPath, baseId)
context.pushSchemaPath(schemaPath, scopeId)
assertions.forEach {
val valid = it.validate(element, context, errorCollector)
result = result and valid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package io.github.optimumcode.json.schema.internal

import com.eygraber.uri.Uri
import io.github.optimumcode.json.pointer.JsonPointer
import io.github.optimumcode.json.pointer.internal.dropLast
import io.github.optimumcode.json.pointer.internal.length
import io.github.optimumcode.json.pointer.startsWith

internal interface ReferenceResolver {
fun ref(refId: RefId): Pair<JsonPointer, JsonSchemaAssertion>
Expand All @@ -26,6 +24,7 @@ internal class DefaultReferenceResolver(
if (!originalRef.dynamic) {
return originalRef.schemaPath to originalRef.assertion
}

val fragment = refId.fragment
val possibleDynamicRefs: MutableList<AssertionWithPath> =
references.asSequence()
Expand All @@ -35,43 +34,22 @@ internal class DefaultReferenceResolver(
possibleDynamicRefs.sortBy { it.schemaPath.length }

val resolvedDynamicRef =
findMostOuterRef(possibleDynamicRefs)
// Try to select by base id starting from the most outer uri in path to the current location
?: schemaPathsStack.firstNotNullOfOrNull { (_, uri) ->
possibleDynamicRefs.firstOrNull { it.baseId == uri }
}
schemaPathsStack.firstNotNullOfOrNull { (_, scopeId) ->
possibleDynamicRefs.firstOrNull { it.scopeId == scopeId }
}
// If no outer anchor found use the original ref
?: originalRef
return resolvedDynamicRef.schemaPath to resolvedDynamicRef.assertion
}

fun pushSchemaPath(
path: JsonPointer,
baseId: Uri,
scopeId: Uri,
) {
schemaPathsStack.addLast(path to baseId)
schemaPathsStack.addLast(path to scopeId)
}

fun popSchemaPath() {
schemaPathsStack.removeLast()
}

@Suppress("detekt:NestedBlockDepth")
private fun findMostOuterRef(possibleRefs: List<AssertionWithPath>): AssertionWithPath? {
// Try to find the most outer anchor to use
// Check every schema in the current chain
// If not matches - take the most outer by location
for ((schemaPath, baseId) in schemaPathsStack) {
var currPath: JsonPointer = schemaPath
while (currPath != JsonPointer.ROOT) {
for (dynamicRef in possibleRefs) {
if (dynamicRef.schemaPath.startsWith(currPath) && dynamicRef.baseId == baseId) {
return dynamicRef
}
}
currPath = currPath.dropLast() ?: break
}
}
return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ private fun validateReferences(
usedRefs: Set<ReferenceLocation>,
) {
ReferenceValidator.validateReferences(
references.mapValues { it.value.run { PointerWithBaseId(this.baseId, schemaPath) } },
references.mapValues { it.value.run { PointerWithBaseId(this.scopeId, schemaPath) } },
usedRefs,
)
}
Expand Down Expand Up @@ -389,7 +389,7 @@ private fun loadSchema(
}
if (refAssertion != null && !referenceFactory.allowOverriding) {
JsonSchemaRoot(
contextWithAdditionalID.baseId,
contextWithAdditionalID.additionalIDs.last().id,
contextWithAdditionalID.schemaPath,
listOf(refAssertion),
contextWithAdditionalID.recursiveResolution,
Expand Down Expand Up @@ -450,7 +450,7 @@ private fun loadJsonSchemaRoot(
addAll(assertions)
}
return JsonSchemaRoot(
context.baseId,
context.additionalIDs.last().id,
context.schemaPath,
result,
context.recursiveResolution,
Expand Down Expand Up @@ -486,7 +486,7 @@ internal data class AssertionWithPath(
val assertion: JsonSchemaAssertion,
val schemaPath: JsonPointer,
val dynamic: Boolean,
val baseId: Uri,
val scopeId: Uri,
)

private data class DefaultLoadingContext(
Expand Down Expand Up @@ -639,7 +639,7 @@ private data class DefaultLoadingContext(
assertion: JsonSchemaAssertion,
dynamic: Boolean,
) {
references.put(referenceId, AssertionWithPath(assertion, schemaPath, dynamic, baseId))?.apply {
references.put(referenceId, AssertionWithPath(assertion, schemaPath, dynamic, additionalIDs.last().id))?.apply {
throw IllegalStateException("duplicated definition $referenceId")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
Expand Down Expand Up @@ -240,6 +241,7 @@ private class TestSuite(
val schema: JsonElement,
val tests: List<SchemaTest>,
val comment: String? = null,
val specification: JsonArray = EMPTY_JSON_ARRAY,
)

@Serializable
Expand All @@ -250,6 +252,7 @@ private class SchemaTest(
val comment: String? = null,
)

private val EMPTY_JSON_ARRAY: JsonArray = JsonArray(emptyList())
private val TEST_SUITES_DIR: Path = "schema-test-suite/tests".toPath()
private val TEST_SUITES_DIR_FROM_ROOT: Path = "test-suites".toPath() / TEST_SUITES_DIR
private const val TEST_SUITES_DIR_ENV_VAR: String = "TEST_SUITES_DIR"
Expand Down