Skip to content

Provide ability to register custom assertion #53

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 9 commits into from
Feb 10, 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
38 changes: 38 additions & 0 deletions api/json-schema-validator.api
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ public final class io/github/optimumcode/json/pointer/JsonPointerKt {
public static final fun JsonPointer (Ljava/lang/String;)Lio/github/optimumcode/json/pointer/JsonPointer;
}

public abstract class io/github/optimumcode/json/schema/AnnotationKey {
public static final field Companion Lio/github/optimumcode/json/schema/AnnotationKey$Companion;
public synthetic fun <init> (Ljava/lang/String;Lkotlin/reflect/KClass;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public static final fun simple (Ljava/lang/String;Lkotlin/reflect/KClass;)Lio/github/optimumcode/json/schema/AnnotationKey;
public fun toString ()Ljava/lang/String;
}

public final class io/github/optimumcode/json/schema/AnnotationKey$Companion {
public final fun simple (Ljava/lang/String;Lkotlin/reflect/KClass;)Lio/github/optimumcode/json/schema/AnnotationKey;
}

public abstract interface class io/github/optimumcode/json/schema/ErrorCollector {
public static final field Companion Lio/github/optimumcode/json/schema/ErrorCollector$Companion;
public static final field EMPTY Lio/github/optimumcode/json/schema/ErrorCollector;
Expand Down Expand Up @@ -70,6 +83,8 @@ public abstract interface class io/github/optimumcode/json/schema/JsonSchemaLoad
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun registerWellKnown (Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun withExtensions (Lio/github/optimumcode/json/schema/extension/ExternalAssertionFactory;[Lio/github/optimumcode/json/schema/extension/ExternalAssertionFactory;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun withExtensions (Ljava/lang/Iterable;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
}

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

public abstract interface class io/github/optimumcode/json/schema/extension/ExternalAnnotationCollector {
public abstract fun annotate (Lio/github/optimumcode/json/schema/AnnotationKey;Ljava/lang/Object;)V
public abstract fun annotated (Lio/github/optimumcode/json/schema/AnnotationKey;)Ljava/lang/Object;
}

public abstract interface class io/github/optimumcode/json/schema/extension/ExternalAssertion {
public abstract fun validate (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/extension/ExternalAssertionContext;Lio/github/optimumcode/json/schema/ErrorCollector;)Z
}

public abstract interface class io/github/optimumcode/json/schema/extension/ExternalAssertionContext {
public abstract fun getAnnotationCollector ()Lio/github/optimumcode/json/schema/extension/ExternalAnnotationCollector;
public abstract fun getObjectPath ()Lio/github/optimumcode/json/pointer/JsonPointer;
}

public abstract interface class io/github/optimumcode/json/schema/extension/ExternalAssertionFactory {
public abstract fun create (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/extension/ExternalLoadingContext;)Lio/github/optimumcode/json/schema/extension/ExternalAssertion;
public abstract fun getKeywordName ()Ljava/lang/String;
}

public abstract interface class io/github/optimumcode/json/schema/extension/ExternalLoadingContext {
public abstract fun getSchemaPath ()Lio/github/optimumcode/json/pointer/JsonPointer;
}

5 changes: 4 additions & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ coverage:
project:
default:
target: 90%
threshold: 1%
threshold: 1%
patch:
default:
informational: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.github.optimumcode.json.schema

import kotlin.jvm.JvmStatic
import kotlin.reflect.KClass

/**
* Class represents a key with certain type that can be used
* to annotate a [JSON element][kotlinx.serialization.json.JsonElement].
* Only **one instance** of a key should be created and used to annotate or retrieve annotations.
*
* ```kotlin
*object Factory : AbstractAssertionFactory(/* .. */) {
* val ANNOTATION: AnnotationKey<String> = AnnotationKey.simple(/*...*/)
*}
* ```
*/
public sealed class AnnotationKey<T : Any> private constructor(
private val name: String,
internal val type: KClass<T>,
) {
override fun equals(other: Any?): Boolean = this === other

override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + type.hashCode()
return result
}

override fun toString(): String = "${this::class.simpleName}($name(${type.simpleName}))"

internal class SimpleAnnotationKey<T : Any> private constructor(
name: String,
type: KClass<T>,
) : AnnotationKey<T>(name, type) {
internal companion object {
@JvmStatic
fun <T : Any> create(
name: String,
type: KClass<T>,
): AnnotationKey<T> = SimpleAnnotationKey(name, type)
}
}

internal class AggregatableAnnotationKey<T : Any> private constructor(
name: String,
type: KClass<T>,
internal val aggregator: Aggregator<T>,
) : AnnotationKey<T>(name, type) {
internal companion object {
@JvmStatic
fun <T : Any> create(
name: String,
type: KClass<T>,
aggregator: (T, T) -> T,
): AnnotationKey<T> = AggregatableAnnotationKey(name, type, aggregator)
}
}

public companion object {
@JvmStatic
public inline fun <reified T : Any> simple(name: String): AnnotationKey<T> = simple(name, T::class)

@JvmStatic
public fun <T : Any> simple(
name: String,
type: KClass<T>,
): AnnotationKey<T> = SimpleAnnotationKey.create(name, type)
}
}

internal fun interface Aggregator<T : Any> {
fun aggregate(
a: T,
b: T,
): T?
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.optimumcode.json.schema
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2019_09
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2020_12
import io.github.optimumcode.json.schema.SchemaType.DRAFT_7
import io.github.optimumcode.json.schema.extension.ExternalAssertionFactory
import io.github.optimumcode.json.schema.internal.SchemaLoader
import io.github.optimumcode.json.schema.internal.wellknown.Draft201909
import io.github.optimumcode.json.schema.internal.wellknown.Draft202012
Expand Down Expand Up @@ -45,6 +46,13 @@ public interface JsonSchemaLoader {
draft: SchemaType?,
): JsonSchemaLoader

public fun withExtensions(
externalFactory: ExternalAssertionFactory,
vararg otherExternalFactories: ExternalAssertionFactory,
): JsonSchemaLoader

public fun withExtensions(externalFactories: Iterable<ExternalAssertionFactory>): JsonSchemaLoader

public fun fromDefinition(schema: String): JsonSchema = fromDefinition(schema, null)

public fun fromDefinition(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.optimumcode.json.schema.extension

import io.github.optimumcode.json.schema.AnnotationKey

public interface ExternalAnnotationCollector {
/**
* Adds annotation with provided [key]
*/
public fun <T : Any> annotate(
key: AnnotationKey<T>,
value: T,
)

/**
* Checks if there is an annotation with provided [key] and returns it if exists
*/
public fun <T : Any> annotated(key: AnnotationKey<T>): T?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.github.optimumcode.json.schema.extension

import io.github.optimumcode.json.schema.ErrorCollector
import kotlinx.serialization.json.JsonElement

/**
* This interface allows you to implement your own schema assertion.
* This interface **does not** allow implementing custom applicators.
* Only simple assertions (like: _format_, _type_) can be implemented.
*/
public interface ExternalAssertion {
/**
* Validates passes [element].
* If [element] does not pass the assertion returns `false`
* and calls [ErrorCollector.onError] on passed [errorCollector].
* Otherwise, returns `true`
*
* You should follow the rules from JSON specification.
* E.g. element passes assertion if it has a different type from that the assertion expects.
* This would mean for 'format' assertion if the [element] is not a primitive the assertion must pass
*
* @param element JSON element to validate
* @param context [ExternalAssertionContext] associated with the [element]
* @param errorCollector handler for [io.github.optimumcode.json.schema.ValidationError] produced by assertion
* @return `true` if element is valid against assertion. Otherwise, returns `false`
*/
public fun validate(
element: JsonElement,
context: ExternalAssertionContext,
errorCollector: ErrorCollector,
): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.optimumcode.json.schema.extension

import io.github.optimumcode.json.pointer.JsonPointer

public interface ExternalAssertionContext {
/**
* A JSON pointer to the currently validating element in the object that is being validated
*/
public val objectPath: JsonPointer

/**
* The [ExternalAnnotationCollector] associated with currently validating element
*/
public val annotationCollector: ExternalAnnotationCollector
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.optimumcode.json.schema.extension

import kotlinx.serialization.json.JsonElement

public interface ExternalAssertionFactory {
/**
* A keyword that is associated with the [ExternalAssertion] created by this factory.
* This keyword **must not** overlap with any existing keywords for existing drafts.
* If keyword overlaps with any keyword for any existing draft and [IllegalStateException] will be thrown
* when this factory is registered in [io.github.optimumcode.json.schema.JsonSchemaLoader].
*
* NOTE: currently the library does not have **format** assertion implemented. But it will have.
* If you decide to implement it as an [ExternalAssertion] please be aware
* that one day this will cause an [IllegalStateException] as it was added to the library itself
*/
public val keywordName: String

/**
* Creates corresponding [ExternalAssertion] form the passed [element].
* The [element] matches the element specified in the schema under the [keywordName] provided by the factory
*
* @param context the [ExternalLoadingContext] associated with the [element].
*
* @return [ExternalAssertion] that correspond to the passed [element]
* @throws IllegalArgumentException if [element] does not match the requirements for this [ExternalAssertion]
*/
public fun create(
element: JsonElement,
context: ExternalLoadingContext,
): ExternalAssertion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.github.optimumcode.json.schema.extension

import io.github.optimumcode.json.pointer.JsonPointer

public interface ExternalLoadingContext {
/**
* A JSON pointer to the current position in schema associated with currently processing element
*/
public val schemaPath: JsonPointer
}
Loading