Skip to content

Commit 937a5b8

Browse files
authored
Add support for Draft 2019-09 (#30)
Resolves #25
1 parent 98a7ba7 commit 937a5b8

File tree

46 files changed

+1803
-272
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1803
-272
lines changed

api/json-schema-validator.api

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,33 @@ public final class io/github/optimumcode/json/schema/ErrorCollector$Companion {
4141
public final class io/github/optimumcode/json/schema/JsonSchema {
4242
public static final field Companion Lio/github/optimumcode/json/schema/JsonSchema$Companion;
4343
public static final fun fromDefinition (Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchema;
44+
public static final fun fromDefinition (Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchema;
4445
public static final fun fromJsonElement (Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchema;
46+
public static final fun fromJsonElement (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchema;
4547
public final fun validate (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/ErrorCollector;)Z
4648
}
4749

4850
public final class io/github/optimumcode/json/schema/JsonSchema$Companion {
4951
public final fun fromDefinition (Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchema;
52+
public final fun fromDefinition (Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchema;
53+
public static synthetic fun fromDefinition$default (Lio/github/optimumcode/json/schema/JsonSchema$Companion;Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;ILjava/lang/Object;)Lio/github/optimumcode/json/schema/JsonSchema;
5054
public final fun fromJsonElement (Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchema;
55+
public final fun fromJsonElement (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchema;
56+
public static synthetic fun fromJsonElement$default (Lio/github/optimumcode/json/schema/JsonSchema$Companion;Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/SchemaType;ILjava/lang/Object;)Lio/github/optimumcode/json/schema/JsonSchema;
5157
}
5258

5359
public final class io/github/optimumcode/json/schema/JsonSchemaStream {
5460
public static final fun fromStream (Lio/github/optimumcode/json/schema/JsonSchema$Companion;Ljava/io/InputStream;)Lio/github/optimumcode/json/schema/JsonSchema;
5561
}
5662

63+
public final class io/github/optimumcode/json/schema/SchemaType : java/lang/Enum {
64+
public static final field DRAFT_2019_09 Lio/github/optimumcode/json/schema/SchemaType;
65+
public static final field DRAFT_7 Lio/github/optimumcode/json/schema/SchemaType;
66+
public static final fun find (Ljava/lang/String;)Lio/github/optimumcode/json/schema/SchemaType;
67+
public static fun valueOf (Ljava/lang/String;)Lio/github/optimumcode/json/schema/SchemaType;
68+
public static fun values ()[Lio/github/optimumcode/json/schema/SchemaType;
69+
}
70+
5771
public final class io/github/optimumcode/json/schema/ValidationError {
5872
public fun <init> (Lio/github/optimumcode/json/pointer/JsonPointer;Lio/github/optimumcode/json/pointer/JsonPointer;Ljava/lang/String;Ljava/util/Map;Lio/github/optimumcode/json/pointer/JsonPointer;)V
5973
public synthetic fun <init> (Lio/github/optimumcode/json/pointer/JsonPointer;Lio/github/optimumcode/json/pointer/JsonPointer;Ljava/lang/String;Ljava/util/Map;Lio/github/optimumcode/json/pointer/JsonPointer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V

config/detekt/detekt.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ complexity:
9494
ignoreOverloaded: false
9595
CyclomaticComplexMethod:
9696
active: true
97-
threshold: 15
97+
threshold: 20
9898
ignoreSingleWhenExpression: false
9999
ignoreSimpleWhenEntries: false
100100
ignoreNestingFunctions: false
@@ -156,12 +156,12 @@ complexity:
156156
active: true
157157
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
158158
thresholdInFiles: 11
159-
thresholdInClasses: 11
160-
thresholdInInterfaces: 11
159+
thresholdInClasses: 15
160+
thresholdInInterfaces: 15
161161
thresholdInObjects: 11
162162
thresholdInEnums: 11
163163
ignoreDeprecated: false
164-
ignorePrivate: false
164+
ignorePrivate: true
165165
ignoreOverridden: false
166166

167167
coroutines:

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ kotlin.code.style=official
22
kotlin.js.compiler=ir
33
org.gradle.jvmargs=-Xmx1G
44
org.gradle.java.installations.auto-download=false
5+
org.gradle.daemon=false
56

67
version=0.0.3-SNAPSHOT
78
group=io.github.optimumcode

src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchema.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.github.optimumcode.json.schema.internal.RefId
88
import io.github.optimumcode.json.schema.internal.SchemaLoader
99
import kotlinx.serialization.json.Json
1010
import kotlinx.serialization.json.JsonElement
11+
import kotlin.jvm.JvmOverloads
1112
import kotlin.jvm.JvmStatic
1213

1314
/**
@@ -33,19 +34,27 @@ public class JsonSchema internal constructor(
3334
public companion object {
3435
/**
3536
* Loads JSON schema from the [schema] definition
37+
* @param defaultType expected schema draft to use when loading schema.
38+
* If `null` draft will be defined by schema definition
39+
* or the latest supported draft will be used
3640
*/
3741
@JvmStatic
38-
public fun fromDefinition(schema: String): JsonSchema {
42+
@JvmOverloads
43+
public fun fromDefinition(schema: String, defaultType: SchemaType? = null): JsonSchema {
3944
val schemaElement: JsonElement = Json.parseToJsonElement(schema)
40-
return fromJsonElement(schemaElement)
45+
return fromJsonElement(schemaElement, defaultType)
4146
}
4247

4348
/**
4449
* Loads JSON schema from the [schemaElement] JSON element
50+
* @param defaultType expected schema draft to use when loading schema.
51+
* If `null` draft will be defined by schema definition
52+
* or the latest supported draft will be used
4553
*/
4654
@JvmStatic
47-
public fun fromJsonElement(schemaElement: JsonElement): JsonSchema {
48-
return SchemaLoader().load(schemaElement)
55+
@JvmOverloads
56+
public fun fromJsonElement(schemaElement: JsonElement, defaultType: SchemaType? = null): JsonSchema {
57+
return SchemaLoader().load(schemaElement, defaultType)
4958
}
5059
}
5160
}

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/SchemaType.kt renamed to src/commonMain/kotlin/io/github/optimumcode/json/schema/SchemaType.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
package io.github.optimumcode.json.schema.internal
1+
package io.github.optimumcode.json.schema
22

33
import com.eygraber.uri.Uri
4+
import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig
5+
import io.github.optimumcode.json.schema.internal.config.Draft201909SchemaLoaderConfig
6+
import io.github.optimumcode.json.schema.internal.config.Draft7SchemaLoaderConfig
47
import kotlin.jvm.JvmStatic
58

6-
internal enum class SchemaType(
9+
public enum class SchemaType(
710
private val schemaId: Uri,
11+
internal val config: SchemaLoaderConfig,
812
) {
9-
DRAFT_7(Uri.parse("http://json-schema.org/draft-07/schema")),
13+
DRAFT_7(Uri.parse("http://json-schema.org/draft-07/schema"), Draft7SchemaLoaderConfig),
14+
DRAFT_2019_09(Uri.parse("https://json-schema.org/draft/2019-09/schema"), Draft201909SchemaLoaderConfig),
1015
;
1116

12-
companion object {
17+
internal companion object {
1318
private const val HTTP_SCHEMA: String = "http"
1419
private const val HTTPS_SCHEMA: String = "https"
1520

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/AssertionContext.kt

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,58 @@ internal interface AssertionContext {
1111
val objectPath: JsonPointer
1212
fun <T : Any> annotate(key: AnnotationKey<T>, value: T)
1313
fun <T : Any> annotated(key: AnnotationKey<T>): T?
14+
fun <T : Any> aggregatedAnnotation(key: AnnotationKey<T>): T?
1415
fun at(index: Int): AssertionContext
1516
fun at(property: String): AssertionContext
1617
fun resolveRef(refId: RefId): Pair<JsonPointer, JsonSchemaAssertion>
1718

19+
/**
20+
* Discards collected annotations
21+
*/
1822
fun resetAnnotations()
23+
24+
/**
25+
* Applies collected annotations
26+
*/
27+
fun applyAnnotations()
28+
29+
/**
30+
* Propagates aggregated annotations to parent context if it has one.
31+
* Otherwise, does nothing
32+
*/
33+
fun propagateToParent()
34+
35+
/**
36+
* Creates a child context with a new annotation scope.
37+
* Current context will get the collected annotations only
38+
* if [propagateToParent] method is called on the child context
39+
*/
40+
fun childContext(): AssertionContext
41+
42+
/**
43+
* Sets the recursive root to the [schema] if no recursive root was set before
44+
*/
45+
fun setRecursiveRootIfAbsent(schema: JsonSchemaAssertion)
46+
47+
/**
48+
* Resets recursive root
49+
*/
50+
fun resetRecursiveRoot()
51+
52+
/**
53+
* Returns recursive root for current state of the validation
54+
*/
55+
fun getRecursiveRoot(): JsonSchemaAssertion?
56+
}
57+
58+
internal fun interface Aggregator<T : Any> {
59+
fun aggregate(a: T, b: T): T?
1960
}
2061

2162
internal class AnnotationKey<T : Any> private constructor(
2263
private val name: String,
2364
internal val type: KClass<T>,
65+
internal val aggregator: Aggregator<T>,
2466
) {
2567
override fun equals(other: Any?): Boolean {
2668
if (this === other) return true
@@ -43,19 +85,37 @@ internal class AnnotationKey<T : Any> private constructor(
4385
override fun toString(): String = "$name(${type.simpleName})"
4486

4587
companion object {
88+
internal val NOT_AGGREGATABLE: (Any, Any) -> Nothing? = { _, _ -> null }
89+
90+
private fun <T : Any> notAggragatable(): (T, T) -> T? = NOT_AGGREGATABLE
91+
4692
@JvmStatic
4793
inline fun <reified T : Any> create(name: String): AnnotationKey<T> = create(name, T::class)
4894

4995
@JvmStatic
50-
fun <T : Any> create(name: String, type: KClass<T>): AnnotationKey<T> = AnnotationKey(name, type)
96+
inline fun <reified T : Any> createAggregatable(name: String, noinline aggregator: (T, T) -> T): AnnotationKey<T> =
97+
createAggregatable(name, T::class, aggregator)
98+
99+
@JvmStatic
100+
fun <T : Any> create(name: String, type: KClass<T>): AnnotationKey<T> = AnnotationKey(name, type, notAggragatable())
101+
102+
@JvmStatic
103+
fun <T : Any> createAggregatable(
104+
name: String,
105+
type: KClass<T>,
106+
aggregator: (T, T) -> T,
107+
): AnnotationKey<T> = AnnotationKey(name, type, aggregator)
51108
}
52109
}
53110

54111
internal data class DefaultAssertionContext(
55112
override val objectPath: JsonPointer,
56113
private val references: Map<RefId, AssertionWithPath>,
114+
private val parent: DefaultAssertionContext? = null,
115+
private var recursiveRoot: JsonSchemaAssertion? = null,
57116
) : AssertionContext {
58117
private lateinit var _annotations: MutableMap<AnnotationKey<*>, Any>
118+
private lateinit var _aggregatedAnnotations: MutableMap<AnnotationKey<*>, Any>
59119
override fun <T : Any> annotate(key: AnnotationKey<T>, value: T) {
60120
annotations()[key] = value
61121
}
@@ -67,6 +127,24 @@ internal data class DefaultAssertionContext(
67127
return _annotations[key]?.let { key.type.cast(it) }
68128
}
69129

130+
override fun <T : Any> aggregatedAnnotation(key: AnnotationKey<T>): T? {
131+
if (!::_aggregatedAnnotations.isInitialized && !::_annotations.isInitialized) {
132+
return null
133+
}
134+
val currentLevelAnnotation: T? = annotated(key)
135+
if (!::_aggregatedAnnotations.isInitialized) {
136+
return currentLevelAnnotation
137+
}
138+
return _aggregatedAnnotations[key]?.let {
139+
val aggregatedAnnotation: T = key.type.cast(it)
140+
if (currentLevelAnnotation == null) {
141+
aggregatedAnnotation
142+
} else {
143+
key.aggregator.aggregate(currentLevelAnnotation, aggregatedAnnotation)
144+
}
145+
} ?: currentLevelAnnotation
146+
}
147+
70148
override fun at(index: Int): AssertionContext = copy(objectPath = objectPath[index])
71149

72150
override fun at(property: String): AssertionContext {
@@ -79,7 +157,70 @@ internal data class DefaultAssertionContext(
79157
}
80158

81159
override fun resetAnnotations() {
82-
annotations().clear()
160+
if (::_annotations.isInitialized && _annotations.isNotEmpty()) {
161+
_annotations.clear()
162+
}
163+
}
164+
165+
override fun applyAnnotations() {
166+
if (::_annotations.isInitialized && _annotations.isNotEmpty()) {
167+
aggregateAnnotations(_annotations) { aggregatedAnnotations() }
168+
_annotations.clear()
169+
}
170+
}
171+
172+
override fun propagateToParent() {
173+
if (parent == null) {
174+
return
175+
}
176+
if (!::_aggregatedAnnotations.isInitialized) {
177+
return
178+
}
179+
aggregateAnnotations(_aggregatedAnnotations) { parent.aggregatedAnnotations() }
180+
}
181+
182+
override fun childContext(): AssertionContext {
183+
return copy(parent = this)
184+
}
185+
186+
override fun setRecursiveRootIfAbsent(schema: JsonSchemaAssertion) {
187+
if (this.recursiveRoot != null) {
188+
return
189+
}
190+
this.recursiveRoot = schema
191+
}
192+
193+
override fun resetRecursiveRoot() {
194+
this.recursiveRoot = null
195+
}
196+
197+
override fun getRecursiveRoot(): JsonSchemaAssertion? {
198+
return recursiveRoot
199+
}
200+
201+
private inline fun aggregateAnnotations(
202+
source: MutableMap<AnnotationKey<*>, Any>,
203+
destination: () -> MutableMap<AnnotationKey<*>, Any>,
204+
) {
205+
source.forEach { (key, value) ->
206+
if (key.aggregator === AnnotationKey.NOT_AGGREGATABLE) {
207+
return@forEach
208+
}
209+
val aggregatedAnnotations = destination()
210+
val oldValue: Any? = aggregatedAnnotations[key]
211+
if (oldValue != null) {
212+
// Probably there is a mistake in the architecture
213+
// Need to think on how to change that to avoid unchecked cast
214+
@Suppress("UNCHECKED_CAST")
215+
val aggregator: Aggregator<Any> = key.aggregator as Aggregator<Any>
216+
val aggregated = aggregator.aggregate(key.type.cast(oldValue), key.type.cast(value))
217+
if (aggregated != null) {
218+
aggregatedAnnotations[key] = aggregated
219+
}
220+
} else {
221+
aggregatedAnnotations[key] = value
222+
}
223+
}
83224
}
84225

85226
private fun annotations(): MutableMap<AnnotationKey<*>, Any> {
@@ -88,4 +229,11 @@ internal data class DefaultAssertionContext(
88229
}
89230
return _annotations
90231
}
232+
233+
private fun aggregatedAnnotations(): MutableMap<AnnotationKey<*>, Any> {
234+
if (!::_aggregatedAnnotations.isInitialized) {
235+
_aggregatedAnnotations = hashMapOf()
236+
}
237+
return _aggregatedAnnotations
238+
}
91239
}

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/AssertionsCollection.kt renamed to src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/JsonSchemaRoot.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,28 @@ package io.github.optimumcode.json.schema.internal
33
import io.github.optimumcode.json.schema.ErrorCollector
44
import kotlinx.serialization.json.JsonElement
55

6-
internal class AssertionsCollection(
6+
internal class JsonSchemaRoot(
77
private val assertions: Collection<JsonSchemaAssertion>,
8+
private val canBeReferencedRecursively: Boolean,
89
) : JsonSchemaAssertion {
910

1011
override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean {
12+
if (canBeReferencedRecursively) {
13+
context.setRecursiveRootIfAbsent(this)
14+
} else {
15+
context.resetRecursiveRoot()
16+
}
1117
var result = true
1218
assertions.forEach {
1319
val valid = it.validate(element, context, errorCollector)
1420
result = result and valid
1521
}
16-
context.resetAnnotations()
22+
// According to spec the annotations should not be applied if element does not match the schema
23+
if (result) {
24+
context.applyAnnotations()
25+
} else {
26+
context.resetAnnotations()
27+
}
1728
return result
1829
}
1930
}

0 commit comments

Comments
 (0)