Skip to content

Commit f4167e8

Browse files
authored
Provide ability to register custom assertion (#53)
Provide interfaces and methods to allow users registering custom assertions and use them in their schemes. Resolves #29
1 parent 47a3f49 commit f4167e8

File tree

46 files changed

+838
-140
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

+838
-140
lines changed

api/json-schema-validator.api

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ public final class io/github/optimumcode/json/pointer/JsonPointerKt {
2929
public static final fun JsonPointer (Ljava/lang/String;)Lio/github/optimumcode/json/pointer/JsonPointer;
3030
}
3131

32+
public abstract class io/github/optimumcode/json/schema/AnnotationKey {
33+
public static final field Companion Lio/github/optimumcode/json/schema/AnnotationKey$Companion;
34+
public synthetic fun <init> (Ljava/lang/String;Lkotlin/reflect/KClass;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
35+
public fun equals (Ljava/lang/Object;)Z
36+
public fun hashCode ()I
37+
public static final fun simple (Ljava/lang/String;Lkotlin/reflect/KClass;)Lio/github/optimumcode/json/schema/AnnotationKey;
38+
public fun toString ()Ljava/lang/String;
39+
}
40+
41+
public final class io/github/optimumcode/json/schema/AnnotationKey$Companion {
42+
public final fun simple (Ljava/lang/String;Lkotlin/reflect/KClass;)Lio/github/optimumcode/json/schema/AnnotationKey;
43+
}
44+
3245
public abstract interface class io/github/optimumcode/json/schema/ErrorCollector {
3346
public static final field Companion Lio/github/optimumcode/json/schema/ErrorCollector$Companion;
3447
public static final field EMPTY Lio/github/optimumcode/json/schema/ErrorCollector;
@@ -70,6 +83,8 @@ public abstract interface class io/github/optimumcode/json/schema/JsonSchemaLoad
7083
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
7184
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
7285
public abstract fun registerWellKnown (Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
86+
public abstract fun withExtensions (Lio/github/optimumcode/json/schema/extension/ExternalAssertionFactory;[Lio/github/optimumcode/json/schema/extension/ExternalAssertionFactory;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
87+
public abstract fun withExtensions (Ljava/lang/Iterable;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
7388
}
7489

7590
public final class io/github/optimumcode/json/schema/JsonSchemaLoader$Companion {
@@ -124,3 +139,26 @@ public final class io/github/optimumcode/json/schema/ValidationError {
124139
public fun toString ()Ljava/lang/String;
125140
}
126141

142+
public abstract interface class io/github/optimumcode/json/schema/extension/ExternalAnnotationCollector {
143+
public abstract fun annotate (Lio/github/optimumcode/json/schema/AnnotationKey;Ljava/lang/Object;)V
144+
public abstract fun annotated (Lio/github/optimumcode/json/schema/AnnotationKey;)Ljava/lang/Object;
145+
}
146+
147+
public abstract interface class io/github/optimumcode/json/schema/extension/ExternalAssertion {
148+
public abstract fun validate (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/extension/ExternalAssertionContext;Lio/github/optimumcode/json/schema/ErrorCollector;)Z
149+
}
150+
151+
public abstract interface class io/github/optimumcode/json/schema/extension/ExternalAssertionContext {
152+
public abstract fun getAnnotationCollector ()Lio/github/optimumcode/json/schema/extension/ExternalAnnotationCollector;
153+
public abstract fun getObjectPath ()Lio/github/optimumcode/json/pointer/JsonPointer;
154+
}
155+
156+
public abstract interface class io/github/optimumcode/json/schema/extension/ExternalAssertionFactory {
157+
public abstract fun create (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/extension/ExternalLoadingContext;)Lio/github/optimumcode/json/schema/extension/ExternalAssertion;
158+
public abstract fun getKeywordName ()Ljava/lang/String;
159+
}
160+
161+
public abstract interface class io/github/optimumcode/json/schema/extension/ExternalLoadingContext {
162+
public abstract fun getSchemaPath ()Lio/github/optimumcode/json/pointer/JsonPointer;
163+
}
164+

codecov.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ coverage:
33
project:
44
default:
55
target: 90%
6-
threshold: 1%
6+
threshold: 1%
7+
patch:
8+
default:
9+
informational: true
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package io.github.optimumcode.json.schema
2+
3+
import kotlin.jvm.JvmStatic
4+
import kotlin.reflect.KClass
5+
6+
/**
7+
* Class represents a key with certain type that can be used
8+
* to annotate a [JSON element][kotlinx.serialization.json.JsonElement].
9+
* Only **one instance** of a key should be created and used to annotate or retrieve annotations.
10+
*
11+
* ```kotlin
12+
*object Factory : AbstractAssertionFactory(/* .. */) {
13+
* val ANNOTATION: AnnotationKey<String> = AnnotationKey.simple(/*...*/)
14+
*}
15+
* ```
16+
*/
17+
public sealed class AnnotationKey<T : Any> private constructor(
18+
private val name: String,
19+
internal val type: KClass<T>,
20+
) {
21+
override fun equals(other: Any?): Boolean = this === other
22+
23+
override fun hashCode(): Int {
24+
var result = name.hashCode()
25+
result = 31 * result + type.hashCode()
26+
return result
27+
}
28+
29+
override fun toString(): String = "${this::class.simpleName}($name(${type.simpleName}))"
30+
31+
internal class SimpleAnnotationKey<T : Any> private constructor(
32+
name: String,
33+
type: KClass<T>,
34+
) : AnnotationKey<T>(name, type) {
35+
internal companion object {
36+
@JvmStatic
37+
fun <T : Any> create(
38+
name: String,
39+
type: KClass<T>,
40+
): AnnotationKey<T> = SimpleAnnotationKey(name, type)
41+
}
42+
}
43+
44+
internal class AggregatableAnnotationKey<T : Any> private constructor(
45+
name: String,
46+
type: KClass<T>,
47+
internal val aggregator: Aggregator<T>,
48+
) : AnnotationKey<T>(name, type) {
49+
internal companion object {
50+
@JvmStatic
51+
fun <T : Any> create(
52+
name: String,
53+
type: KClass<T>,
54+
aggregator: (T, T) -> T,
55+
): AnnotationKey<T> = AggregatableAnnotationKey(name, type, aggregator)
56+
}
57+
}
58+
59+
public companion object {
60+
@JvmStatic
61+
public inline fun <reified T : Any> simple(name: String): AnnotationKey<T> = simple(name, T::class)
62+
63+
@JvmStatic
64+
public fun <T : Any> simple(
65+
name: String,
66+
type: KClass<T>,
67+
): AnnotationKey<T> = SimpleAnnotationKey.create(name, type)
68+
}
69+
}
70+
71+
internal fun interface Aggregator<T : Any> {
72+
fun aggregate(
73+
a: T,
74+
b: T,
75+
): T?
76+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.github.optimumcode.json.schema
33
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2019_09
44
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2020_12
55
import io.github.optimumcode.json.schema.SchemaType.DRAFT_7
6+
import io.github.optimumcode.json.schema.extension.ExternalAssertionFactory
67
import io.github.optimumcode.json.schema.internal.SchemaLoader
78
import io.github.optimumcode.json.schema.internal.wellknown.Draft201909
89
import io.github.optimumcode.json.schema.internal.wellknown.Draft202012
@@ -45,6 +46,13 @@ public interface JsonSchemaLoader {
4546
draft: SchemaType?,
4647
): JsonSchemaLoader
4748

49+
public fun withExtensions(
50+
externalFactory: ExternalAssertionFactory,
51+
vararg otherExternalFactories: ExternalAssertionFactory,
52+
): JsonSchemaLoader
53+
54+
public fun withExtensions(externalFactories: Iterable<ExternalAssertionFactory>): JsonSchemaLoader
55+
4856
public fun fromDefinition(schema: String): JsonSchema = fromDefinition(schema, null)
4957

5058
public fun fromDefinition(
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.github.optimumcode.json.schema.extension
2+
3+
import io.github.optimumcode.json.schema.AnnotationKey
4+
5+
public interface ExternalAnnotationCollector {
6+
/**
7+
* Adds annotation with provided [key]
8+
*/
9+
public fun <T : Any> annotate(
10+
key: AnnotationKey<T>,
11+
value: T,
12+
)
13+
14+
/**
15+
* Checks if there is an annotation with provided [key] and returns it if exists
16+
*/
17+
public fun <T : Any> annotated(key: AnnotationKey<T>): T?
18+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.github.optimumcode.json.schema.extension
2+
3+
import io.github.optimumcode.json.schema.ErrorCollector
4+
import kotlinx.serialization.json.JsonElement
5+
6+
/**
7+
* This interface allows you to implement your own schema assertion.
8+
* This interface **does not** allow implementing custom applicators.
9+
* Only simple assertions (like: _format_, _type_) can be implemented.
10+
*/
11+
public interface ExternalAssertion {
12+
/**
13+
* Validates passes [element].
14+
* If [element] does not pass the assertion returns `false`
15+
* and calls [ErrorCollector.onError] on passed [errorCollector].
16+
* Otherwise, returns `true`
17+
*
18+
* You should follow the rules from JSON specification.
19+
* E.g. element passes assertion if it has a different type from that the assertion expects.
20+
* This would mean for 'format' assertion if the [element] is not a primitive the assertion must pass
21+
*
22+
* @param element JSON element to validate
23+
* @param context [ExternalAssertionContext] associated with the [element]
24+
* @param errorCollector handler for [io.github.optimumcode.json.schema.ValidationError] produced by assertion
25+
* @return `true` if element is valid against assertion. Otherwise, returns `false`
26+
*/
27+
public fun validate(
28+
element: JsonElement,
29+
context: ExternalAssertionContext,
30+
errorCollector: ErrorCollector,
31+
): Boolean
32+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.github.optimumcode.json.schema.extension
2+
3+
import io.github.optimumcode.json.pointer.JsonPointer
4+
5+
public interface ExternalAssertionContext {
6+
/**
7+
* A JSON pointer to the currently validating element in the object that is being validated
8+
*/
9+
public val objectPath: JsonPointer
10+
11+
/**
12+
* The [ExternalAnnotationCollector] associated with currently validating element
13+
*/
14+
public val annotationCollector: ExternalAnnotationCollector
15+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.github.optimumcode.json.schema.extension
2+
3+
import kotlinx.serialization.json.JsonElement
4+
5+
public interface ExternalAssertionFactory {
6+
/**
7+
* A keyword that is associated with the [ExternalAssertion] created by this factory.
8+
* This keyword **must not** overlap with any existing keywords for existing drafts.
9+
* If keyword overlaps with any keyword for any existing draft and [IllegalStateException] will be thrown
10+
* when this factory is registered in [io.github.optimumcode.json.schema.JsonSchemaLoader].
11+
*
12+
* NOTE: currently the library does not have **format** assertion implemented. But it will have.
13+
* If you decide to implement it as an [ExternalAssertion] please be aware
14+
* that one day this will cause an [IllegalStateException] as it was added to the library itself
15+
*/
16+
public val keywordName: String
17+
18+
/**
19+
* Creates corresponding [ExternalAssertion] form the passed [element].
20+
* The [element] matches the element specified in the schema under the [keywordName] provided by the factory
21+
*
22+
* @param context the [ExternalLoadingContext] associated with the [element].
23+
*
24+
* @return [ExternalAssertion] that correspond to the passed [element]
25+
* @throws IllegalArgumentException if [element] does not match the requirements for this [ExternalAssertion]
26+
*/
27+
public fun create(
28+
element: JsonElement,
29+
context: ExternalLoadingContext,
30+
): ExternalAssertion
31+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.github.optimumcode.json.schema.extension
2+
3+
import io.github.optimumcode.json.pointer.JsonPointer
4+
5+
public interface ExternalLoadingContext {
6+
/**
7+
* A JSON pointer to the current position in schema associated with currently processing element
8+
*/
9+
public val schemaPath: JsonPointer
10+
}

0 commit comments

Comments
 (0)