Skip to content

Commit 62a3f63

Browse files
author
Oleg
committed
Add defenitions loading. Store paths to the loaded schemas
1 parent 9fb5ac4 commit 62a3f63

File tree

7 files changed

+66
-22
lines changed

7 files changed

+66
-22
lines changed

src/commonMain/kotlin/smirnov/oleg/json/schema/JsonSchema.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import smirnov.oleg.json.schema.internal.SchemaLoader
1010
import kotlin.jvm.JvmStatic
1111

1212
class JsonSchema internal constructor(
13+
private val baseId: String,
1314
private val assertion: JsonSchemaAssertion,
15+
private val references: Map<String, JsonSchemaAssertion>,
1416
) {
1517
fun validate(value: JsonElement, errorCollector: ErrorCollector): Boolean {
1618
return assertion.validate(value, DefaultAssertionContext(JsonPointer.ROOT), errorCollector)
@@ -20,8 +22,7 @@ class JsonSchema internal constructor(
2022
@JvmStatic
2123
fun fromDescription(schema: String): JsonSchema {
2224
val schemaElement: JsonElement = Json.parseToJsonElement(schema)
23-
val schemaAssertion = SchemaLoader().load(schemaElement)
24-
return JsonSchema(schemaAssertion)
25+
return SchemaLoader().load(schemaElement)
2526
}
2627
}
2728
}

src/commonMain/kotlin/smirnov/oleg/json/schema/internal/AssertionContext.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import smirnov.oleg.json.pointer.JsonPointer
44
import smirnov.oleg.json.pointer.div
55
import smirnov.oleg.json.pointer.get
66

7-
interface AssertionContext {
7+
internal interface AssertionContext {
88
val objectPath: JsonPointer
99

1010
fun at(index: Int): AssertionContext
1111
fun at(property: String): AssertionContext
1212
}
1313

14-
data class DefaultAssertionContext(
14+
internal data class DefaultAssertionContext(
1515
override val objectPath: JsonPointer,
1616
) : AssertionContext {
1717
override fun at(index: Int): AssertionContext = copy(objectPath = objectPath[index])

src/commonMain/kotlin/smirnov/oleg/json/schema/internal/AssertionFactory.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package smirnov.oleg.json.schema.internal
22

33
import kotlinx.serialization.json.JsonElement
44

5-
interface AssertionFactory {
5+
internal interface AssertionFactory {
66
/**
77
* Checks whether the factory can create an assertion from the [element].
88
*

src/commonMain/kotlin/smirnov/oleg/json/schema/internal/AssertionsCollection.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package smirnov.oleg.json.schema.internal
33
import kotlinx.serialization.json.JsonElement
44
import smirnov.oleg.json.schema.ErrorCollector
55

6-
class AssertionsCollection(
6+
internal class AssertionsCollection(
77
private val assertions: Collection<JsonSchemaAssertion>,
88
) : JsonSchemaAssertion {
99

src/commonMain/kotlin/smirnov/oleg/json/schema/internal/JsonSchemaAssertion.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package smirnov.oleg.json.schema.internal
33
import kotlinx.serialization.json.JsonElement
44
import smirnov.oleg.json.schema.ErrorCollector
55

6-
interface JsonSchemaAssertion {
6+
internal interface JsonSchemaAssertion {
77
/**
88
* Validates passes [element].
99
* If [element] does not pass the assertion returns `false` and calls [ErrorCollector.onError] on passed [errorCollector].

src/commonMain/kotlin/smirnov/oleg/json/schema/internal/LoadingContext.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import smirnov.oleg.json.pointer.JsonPointer
55
import smirnov.oleg.json.pointer.div
66
import smirnov.oleg.json.pointer.get
77

8-
interface LoadingContext {
8+
internal interface LoadingContext {
99
val schemaPath: JsonPointer
1010

1111
fun at(property: String): LoadingContext

src/commonMain/kotlin/smirnov/oleg/json/schema/internal/SchemaLoader.kt

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import kotlinx.serialization.json.booleanOrNull
88
import smirnov.oleg.json.pointer.JsonPointer
99
import smirnov.oleg.json.pointer.div
1010
import smirnov.oleg.json.pointer.get
11+
import smirnov.oleg.json.schema.JsonSchema
1112
import smirnov.oleg.json.schema.internal.factories.array.ContainsAssertionFactory
1213
import smirnov.oleg.json.schema.internal.factories.array.ItemsAssertionFactory
1314
import smirnov.oleg.json.schema.internal.factories.array.MaxItemsAssertionFactory
@@ -64,47 +65,89 @@ private val factories: List<AssertionFactory> = listOf(
6465
NotAssertionFactory,
6566
)
6667

68+
private const val DEFINITIONS_PROPERTY: String = "definitions"
69+
private const val ID_PROPERTY: String = "\$id"
70+
71+
private const val rootReference = '#'
72+
6773
class SchemaLoader {
68-
fun load(schemaDefinition: JsonElement): JsonSchemaAssertion {
69-
return loadSchema(schemaDefinition)
74+
fun load(schemaDefinition: JsonElement): JsonSchema {
75+
val baseId = extractBaseID(schemaDefinition)
76+
val context = defaultLoadingContext(baseId)
77+
loadDefinitions(schemaDefinition, context)
78+
val schemaAssertion = loadSchema(schemaDefinition, context)
79+
return JsonSchema(baseId, schemaAssertion, context.references)
7080
}
81+
82+
private fun loadDefinitions(schemaDefinition: JsonElement, context: DefaultLoadingContext) {
83+
if (schemaDefinition !is JsonObject) {
84+
return
85+
}
86+
val definitionsElement = schemaDefinition[DEFINITIONS_PROPERTY] ?: return
87+
require(definitionsElement is JsonObject) { "$DEFINITIONS_PROPERTY must be an object" }
88+
val definitionsContext = context.at(DEFINITIONS_PROPERTY)
89+
for ((name, element) in definitionsElement) {
90+
loadSchema(element, definitionsContext.at(name))
91+
}
92+
}
93+
94+
private fun extractBaseID(schemaDefinition: JsonElement): String =
95+
when (schemaDefinition) {
96+
is JsonObject -> {
97+
schemaDefinition[ID_PROPERTY]?.let {
98+
require(it is JsonPrimitive && it.isString) { "$ID_PROPERTY must be a string" }
99+
it.content
100+
} ?: ""
101+
}
102+
103+
else -> ""
104+
}.trimEnd(rootReference)
71105
}
72106

73107
private fun loadSchema(
74108
schemaDefinition: JsonElement,
75-
context: LoadingContext = defaultLoadingContext()
109+
context: DefaultLoadingContext,
76110
): JsonSchemaAssertion {
77111
require(context.isJsonSchema(schemaDefinition)) {
78112
"schema must be either a valid JSON object or boolean"
79113
}
80-
if (schemaDefinition is JsonPrimitive) {
81-
return if (schemaDefinition.boolean) {
114+
return when (schemaDefinition) {
115+
is JsonPrimitive -> if (schemaDefinition.boolean) {
82116
TrueSchemaAssertion
83117
} else {
84118
FalseSchemaAssertion(path = context.schemaPath)
85119
}
86-
}
87-
val assertions = factories.filter { it.isApplicable(schemaDefinition) }
88-
.map {
89-
it.create(schemaDefinition, context)
90-
}
91-
return AssertionsCollection(assertions)
120+
121+
else -> factories.filter { it.isApplicable(schemaDefinition) }
122+
.map {
123+
it.create(schemaDefinition, context)
124+
}.let(::AssertionsCollection)
125+
}.apply(context::register)
92126
}
93127

94128
private data class DefaultLoadingContext(
129+
private val baseId: String,
95130
override val schemaPath: JsonPointer = JsonPointer.ROOT,
131+
val references: MutableMap<String, JsonSchemaAssertion> = hashMapOf(),
96132
) : LoadingContext {
97-
override fun at(property: String): LoadingContext {
133+
override fun at(property: String): DefaultLoadingContext {
98134
return copy(schemaPath = schemaPath / property)
99135
}
100136

101-
override fun at(index: Int): LoadingContext {
137+
override fun at(index: Int): DefaultLoadingContext {
102138
return copy(schemaPath = schemaPath[index])
103139
}
104140

105141
override fun schemaFrom(element: JsonElement): JsonSchemaAssertion = loadSchema(element, this)
106142
override fun isJsonSchema(element: JsonElement): Boolean = (element is JsonObject
107143
|| (element is JsonPrimitive && element.booleanOrNull != null))
144+
145+
fun register(assertion: JsonSchemaAssertion) {
146+
val referenceId = "$baseId$rootReference$schemaPath"
147+
references.put(referenceId, assertion)?.apply {
148+
throw IllegalStateException("duplicated definition $referenceId")
149+
}
150+
}
108151
}
109152

110-
private fun defaultLoadingContext(): DefaultLoadingContext = DefaultLoadingContext()
153+
private fun defaultLoadingContext(baseId: String): DefaultLoadingContext = DefaultLoadingContext(baseId)

0 commit comments

Comments
 (0)