Skip to content

Implemented parsing of recursive $ref #20

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions src/main/kotlin/net/pwall/json/schema/JSONSchema.kt
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ sealed class JSONSchema(

override fun hashCode(): Int = uri.hashCode() xor location.hashCode()

override fun toString() =
"${this::class.java.simpleName}(uri=$uri, location=$location, description=$description, title=$title)"

@Suppress("EqualsOrHashCode")
class True(uri: URI?, location: JSONPointer) : JSONSchema(uri, location) {

Expand Down Expand Up @@ -267,6 +270,9 @@ sealed class JSONSchema(
override fun hashCode(): Int = super.hashCode() xor schemaVersion.hashCode() xor title.hashCode() xor
description.hashCode() xor children.hashCode()

override fun toString() = "General(uri=$uri, location=$location, " +
"schemaVersion='$schemaVersion', title=$title, description=$description, children=$children)}"

}

companion object {
Expand Down
27 changes: 21 additions & 6 deletions src/main/kotlin/net/pwall/json/schema/parser/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ class Parser(var options: Options = Options(), uriResolver: (URI) -> InputStream

private val schemaCache = mutableMapOf<URI, JSONSchema>()

private val pendingRefs = mutableMapOf<URI, MutableList<RefSchema>>()

fun preLoad(filename: String) {
jsonReader.preLoad(filename)
}
Expand Down Expand Up @@ -181,9 +183,9 @@ class Parser(var options: Options = Options(), uriResolver: (URI) -> InputStream
}
uri?.let {
val fragmentURI = uri.withFragment(pointer)
schemaCache[fragmentURI]?.let {
return if (it !is JSONSchema.False) it else fatal("Recursive \$ref", uri, pointer)
}
// Always return an existing schema, recursion is handled in parseRef() and below.
schemaCache[fragmentURI]?.let { return it }
// Create a "dummy entry" to detect recursions before the actual parsing.
schemaCache[fragmentURI] = JSONSchema.False(uri, pointer)
}
val title = schemaJSON.getStringOrNull(uri, "title")
Expand Down Expand Up @@ -268,7 +270,12 @@ class Parser(var options: Options = Options(), uriResolver: (URI) -> InputStream
}
if (options.validateDefault && schemaJSON.containsKey("default"))
validateExample(result, pointer, json, pointer.child("default"), defaultValidationErrors)
uri?.let { schemaCache[uri.withFragment(pointer)] = result }
uri?.let {
val fragmentUri = uri.withFragment(pointer)
schemaCache[fragmentUri] = result
// Complete pending (i.e. recursive) RefSchema instances.
pendingRefs.remove(fragmentUri)?.forEach { it.target = result }
}
return result
}

Expand Down Expand Up @@ -374,12 +381,20 @@ class Parser(var options: Options = Options(), uriResolver: (URI) -> InputStream
}
if (!refPointer.exists(refJSON))
fatal("\$ref not found $refString", uri, pointer)
return RefSchema(
val target = parseSchema(refJSON, refPointer, refURI)
val refSchema = RefSchema(
uri = uri,
location = pointer,
target = parseSchema(refJSON, refPointer, refURI),
target = target,
fragment = refURIFragment,
)
if (target is JSONSchema.False) {
val fragmentUri = target.uri?.withFragment(target.location)
?: fatal("Cannot build fragmentUri for recursive \$ref", uri, pointer)
// Register refSchema for future completion when schema becomes available.
pendingRefs.computeIfAbsent(fragmentUri) { mutableListOf() } += refSchema
}
return refSchema
}

private fun parseItems(json: JSONValue, pointer: JSONPointer, uri: URI?, value: JSONValue?): JSONSchema.SubSchema {
Expand Down
31 changes: 30 additions & 1 deletion src/main/kotlin/net/pwall/json/schema/subschema/RefSchema.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,21 @@ import java.net.URI
import net.pwall.json.JSONValue
import net.pwall.json.pointer.JSONPointer
import net.pwall.json.schema.JSONSchema
import net.pwall.json.schema.JSONSchemaException
import net.pwall.json.schema.output.BasicOutput
import net.pwall.json.schema.output.DetailedOutput

class RefSchema(uri: URI?, location: JSONPointer, val target: JSONSchema, val fragment: String?) :
class RefSchema(uri: URI?, location: JSONPointer, target: JSONSchema, val fragment: String?) :
JSONSchema.SubSchema(uri, location) {

var target: JSONSchema = target
set(value) {
if (field !is False) {
throw JSONSchemaException("Modification of resolved RefSchema target is prohibited")
}
field = value
}

override fun childLocation(pointer: JSONPointer): JSONPointer = pointer.child("\$ref")

override fun validate(json: JSONValue?, instanceLocation: JSONPointer): Boolean =
Expand All @@ -58,4 +67,24 @@ class RefSchema(uri: URI?, location: JSONPointer, val target: JSONSchema, val fr

override fun hashCode(): Int = super.hashCode() xor target.hashCode()

private var toStringVisiting = false

/**
* toString() implementation with loop protection.
* The var "toStringVisiting" is set to true before construction of the resulting String.
* When it is found to be already set, the contents will not be evaluated recursively.
*/
override fun toString(): String {
return if (toStringVisiting) {
"RefSchema(<recursive access>)"
} else {
try {
toStringVisiting = true
"RefSchema(uri=$uri, location=$location, fragment=$fragment, target=$target)"
} finally {
toStringVisiting = false
}
}
}

}