Skip to content

Commit

Permalink
Add partial support for format keyword (#72)
Browse files Browse the repository at this point in the history
This patch introduces:

- API for implementing user's format validator and registering it in the
schema
- Support for data/time formats

Related to #54
  • Loading branch information
OptimumCode authored Feb 29, 2024
1 parent b1bd666 commit 4f7cdbf
Show file tree
Hide file tree
Showing 32 changed files with 1,319 additions and 77 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,20 @@ val valid = schema.validate(elementToValidate, errors::add)
| | not | Supported |
</details>

## Format assertion

The library supports `format` assertion. For now only a few formats are supported:
* date
* time
* date-time
* duration

But there is an API to implement the user's defined format validation.
The [FormatValidator](src/commonMain/kotlin/io/github/optimumcode/json/schema/ValidationError.kt) interface can be user for that.
The custom format validators can be register in [JsonSchemaLoader](src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt).

_**Please note, that the format validation API is marked as experimental and will require `OptIn` declaration in your code.**_

## Custom assertions

You can implement custom assertions and use them. Read more [here](docs/custom_assertions.md).
Expand Down
43 changes: 43 additions & 0 deletions api/json-schema-validator.api
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ 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 final class io/github/optimumcode/json/schema/Annotations {
public static final field FORMAT_ANNOTATION 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 All @@ -51,6 +55,34 @@ public abstract interface class io/github/optimumcode/json/schema/ErrorCollector
public final class io/github/optimumcode/json/schema/ErrorCollector$Companion {
}

public abstract interface annotation class io/github/optimumcode/json/schema/ExperimentalApi : java/lang/annotation/Annotation {
}

public final class io/github/optimumcode/json/schema/FormatBehavior : java/lang/Enum {
public static final field ANNOTATION_AND_ASSERTION Lio/github/optimumcode/json/schema/FormatBehavior;
public static final field ANNOTATION_ONLY Lio/github/optimumcode/json/schema/FormatBehavior;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lio/github/optimumcode/json/schema/FormatBehavior;
public static fun values ()[Lio/github/optimumcode/json/schema/FormatBehavior;
}

public abstract class io/github/optimumcode/json/schema/FormatValidationResult {
public synthetic fun <init> (ZLkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun isValid ()Z
}

public abstract interface class io/github/optimumcode/json/schema/FormatValidator {
public static final field Companion Lio/github/optimumcode/json/schema/FormatValidator$Companion;
public static fun Invalid ()Lio/github/optimumcode/json/schema/FormatValidationResult;
public static fun Valid ()Lio/github/optimumcode/json/schema/FormatValidationResult;
public abstract fun validate (Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/FormatValidationResult;
}

public final class io/github/optimumcode/json/schema/FormatValidator$Companion {
public final fun Invalid ()Lio/github/optimumcode/json/schema/FormatValidationResult;
public final fun Valid ()Lio/github/optimumcode/json/schema/FormatValidationResult;
}

public final class io/github/optimumcode/json/schema/JsonSchema {
public static final field Companion Lio/github/optimumcode/json/schema/JsonSchema$Companion;
public static final fun fromDefinition (Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchema;
Expand Down Expand Up @@ -83,8 +115,11 @@ 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 withCustomFormat (Ljava/lang/String;Lio/github/optimumcode/json/schema/FormatValidator;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun withCustomFormats (Ljava/util/Map;)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 abstract fun withSchemaOption (Lio/github/optimumcode/json/schema/SchemaOption;Ljava/lang/Object;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
}

public final class io/github/optimumcode/json/schema/JsonSchemaLoader$Companion {
Expand All @@ -104,6 +139,14 @@ public final class io/github/optimumcode/json/schema/JsonSchemaStream {
public static final fun fromStream (Lio/github/optimumcode/json/schema/JsonSchema$Companion;Ljava/io/InputStream;)Lio/github/optimumcode/json/schema/JsonSchema;
}

public final class io/github/optimumcode/json/schema/SchemaOption {
public static final field Companion Lio/github/optimumcode/json/schema/SchemaOption$Companion;
public static final field FORMAT_BEHAVIOR_OPTION Lio/github/optimumcode/json/schema/SchemaOption;
}

public final class io/github/optimumcode/json/schema/SchemaOption$Companion {
}

public final class io/github/optimumcode/json/schema/SchemaType : java/lang/Enum {
public static final field Companion Lio/github/optimumcode/json/schema/SchemaType$Companion;
public static final field DRAFT_2019_09 Lio/github/optimumcode/json/schema/SchemaType;
Expand Down
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetWithTests
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
Expand Down Expand Up @@ -26,6 +27,11 @@ apiValidation {

kotlin {
explicitApi()

@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
freeCompilerArgs.add("-opt-in=io.github.optimumcode.json.schema.ExperimentalApi")
}
jvm {
jvmToolchain(11)
withJava()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@file:JvmName("Annotations")

package io.github.optimumcode.json.schema

import io.github.optimumcode.json.schema.internal.factories.general.FormatAssertionFactory
import kotlin.jvm.JvmField
import kotlin.jvm.JvmName

/**
* Key for getting annotation from `format` assertion
*/
@JvmField
public val FORMAT_ANNOTATION: AnnotationKey<String> = FormatAssertionFactory.ANNOTATION
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.github.optimumcode.json.schema

/**
* Marks declarations that are experimental or published as a 'preview' version.
* The API for those declarations can be changed in future releases
* based on library needs or user's feedback.
* Once the API is final the backward compatibility will be maintained within patch and minor updates.
*/
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS)
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
public annotation class ExperimentalApi
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.github.optimumcode.json.schema

import kotlinx.serialization.json.JsonElement
import kotlin.jvm.JvmStatic

/**
* The [FormatValidator] is used to check whether the [JsonElement] matches the expected format.
* If the [JsonElement] is not of the required type (e.g. validator expects string but the [JsonElement] is an object)
* the validator **MUST** return [FormatValidator.Valid] result
*/
@ExperimentalApi
public interface FormatValidator {
/**
* Validates [element] against the expected format
*
* @param element JSON element to validate against the expected format
* @return the result of the validation
*/
public fun validate(element: JsonElement): FormatValidationResult

public companion object {
@Suppress("ktlint:standard:function-naming", "FunctionName")
@JvmStatic
public fun Valid(): FormatValidationResult = FormatValidationResult.Valid

@Suppress("ktlint:standard:function-naming", "FunctionName")
@JvmStatic
public fun Invalid(): FormatValidationResult = FormatValidationResult.Invalid
}
}

@ExperimentalApi
public sealed class FormatValidationResult private constructor(private val valid: Boolean) {
public fun isValid(): Boolean = valid

internal data object Valid : FormatValidationResult(true)

internal data object Invalid : FormatValidationResult(false)
}

public enum class FormatBehavior {
/**
* Only annotation. If the value does not match format the validation will pass
*/
ANNOTATION_ONLY,

/**
* Annotation and assertion. If the value does not match format the validation will fail
*/
ANNOTATION_AND_ASSERTION,
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.github.optimumcode.json.schema.internal.wellknown.Draft7
import kotlinx.serialization.json.JsonElement
import kotlin.jvm.JvmStatic

@Suppress("detekt:TooManyFunctions")
public interface JsonSchemaLoader {
public fun registerWellKnown(draft: SchemaType): JsonSchemaLoader =
apply {
Expand Down Expand Up @@ -53,6 +54,20 @@ public interface JsonSchemaLoader {

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

@ExperimentalApi
public fun withCustomFormat(
format: String,
formatValidator: FormatValidator,
): JsonSchemaLoader

@ExperimentalApi
public fun withCustomFormats(formats: Map<String, FormatValidator>): JsonSchemaLoader

public fun <T : Any> withSchemaOption(
option: SchemaOption<T>,
value: T,
): 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,11 @@
package io.github.optimumcode.json.schema

import kotlin.jvm.JvmField
import kotlin.reflect.KClass

public class SchemaOption<T : Any> private constructor(internal val type: KClass<T>) {
public companion object {
@JvmField
public val FORMAT_BEHAVIOR_OPTION: SchemaOption<FormatBehavior> = SchemaOption(FormatBehavior::class)
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package io.github.optimumcode.json.schema.internal

import io.github.optimumcode.json.schema.FormatValidator
import io.github.optimumcode.json.schema.extension.ExternalLoadingContext
import kotlinx.serialization.json.JsonElement

internal interface LoadingContext : ExternalLoadingContext {
val customFormatValidators: Map<String, FormatValidator>

fun at(property: String): LoadingContext

fun at(index: Int): LoadingContext
Expand Down
Loading

0 comments on commit 4f7cdbf

Please sign in to comment.