diff --git a/api/json-schema-validator.api b/api/json-schema-validator.api index 470ef031..ab770f8a 100644 --- a/api/json-schema-validator.api +++ b/api/json-schema-validator.api @@ -195,6 +195,7 @@ 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; public static final field DRAFT_2020_12 Lio/github/optimumcode/json/schema/SchemaType; + public static final field DRAFT_6 Lio/github/optimumcode/json/schema/SchemaType; public static final field DRAFT_7 Lio/github/optimumcode/json/schema/SchemaType; public static final fun find (Ljava/lang/String;)Lio/github/optimumcode/json/schema/SchemaType; public static fun getEntries ()Lkotlin/enums/EnumEntries; diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt index a30ee63c..d2751fcb 100644 --- a/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchemaLoader.kt @@ -3,11 +3,13 @@ package io.github.optimumcode.json.schema import com.eygraber.uri.Uri 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_6 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 +import io.github.optimumcode.json.schema.internal.wellknown.Draft6 import io.github.optimumcode.json.schema.internal.wellknown.Draft7 import kotlinx.serialization.json.JsonElement import kotlin.jvm.JvmStatic @@ -17,6 +19,7 @@ public interface JsonSchemaLoader { public fun registerWellKnown(draft: SchemaType): JsonSchemaLoader = apply { when (draft) { + DRAFT_6 -> Draft6.entries.forEach { register(it.content) } DRAFT_7 -> Draft7.entries.forEach { register(it.content) } DRAFT_2019_09 -> Draft201909.entries.forEach { register(it.content) } DRAFT_2020_12 -> Draft202012.entries.forEach { register(it.content) } diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/SchemaType.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/SchemaType.kt index b9f42cff..67b6cc88 100644 --- a/src/commonMain/kotlin/io/github/optimumcode/json/schema/SchemaType.kt +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/SchemaType.kt @@ -4,6 +4,7 @@ import com.eygraber.uri.Uri import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig import io.github.optimumcode.json.schema.internal.config.Draft201909SchemaLoaderConfig import io.github.optimumcode.json.schema.internal.config.Draft202012SchemaLoaderConfig +import io.github.optimumcode.json.schema.internal.config.Draft6SchemaLoaderConfig import io.github.optimumcode.json.schema.internal.config.Draft7SchemaLoaderConfig import kotlin.jvm.JvmStatic @@ -11,6 +12,7 @@ public enum class SchemaType( internal val schemaId: Uri, internal val config: SchemaLoaderConfig, ) { + DRAFT_6(Uri.parse("http://json-schema.org/draft-06/schema"), Draft6SchemaLoaderConfig), DRAFT_7(Uri.parse("http://json-schema.org/draft-07/schema"), Draft7SchemaLoaderConfig), DRAFT_2019_09(Uri.parse("https://json-schema.org/draft/2019-09/schema"), Draft201909SchemaLoaderConfig), DRAFT_2020_12(Uri.parse("https://json-schema.org/draft/2020-12/schema"), Draft202012SchemaLoaderConfig), diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/config/Draft6SchemaLoaderConfig.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/config/Draft6SchemaLoaderConfig.kt new file mode 100644 index 00000000..3a761e7e --- /dev/null +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/config/Draft6SchemaLoaderConfig.kt @@ -0,0 +1,139 @@ +package io.github.optimumcode.json.schema.internal.config + +import io.github.optimumcode.json.schema.FormatBehavior +import io.github.optimumcode.json.schema.SchemaOption +import io.github.optimumcode.json.schema.internal.AssertionFactory +import io.github.optimumcode.json.schema.internal.KeyWord +import io.github.optimumcode.json.schema.internal.KeyWord.ANCHOR +import io.github.optimumcode.json.schema.internal.KeyWord.COMPATIBILITY_DEFINITIONS +import io.github.optimumcode.json.schema.internal.KeyWord.DEFINITIONS +import io.github.optimumcode.json.schema.internal.KeyWord.DYNAMIC_ANCHOR +import io.github.optimumcode.json.schema.internal.KeyWord.ID +import io.github.optimumcode.json.schema.internal.KeyWordResolver +import io.github.optimumcode.json.schema.internal.ReferenceFactory +import io.github.optimumcode.json.schema.internal.ReferenceFactory.RefHolder +import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig +import io.github.optimumcode.json.schema.internal.SchemaLoaderContext +import io.github.optimumcode.json.schema.internal.config.Draft6KeyWordResolver.REF_PROPERTY +import io.github.optimumcode.json.schema.internal.factories.array.AdditionalItemsAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.array.ContainsAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.array.ItemsAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.array.MaxItemsAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.array.MinItemsAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.array.UniqueItemsAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.condition.AllOfAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.condition.AnyOfAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.condition.NotAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.condition.OneOfAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.general.ConstAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.general.EnumAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.general.FormatAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.general.TypeAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.number.ExclusiveMaximumAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.number.ExclusiveMinimumAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.number.MaximumAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.number.MinimumAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.number.MultipleOfAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.`object`.AdditionalPropertiesAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.`object`.DependenciesAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.`object`.MaxPropertiesAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.`object`.MinPropertiesAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.`object`.PatternPropertiesAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.`object`.PropertiesAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.`object`.PropertyNamesAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.`object`.RequiredAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.string.MaxLengthAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.string.MinLengthAssertionFactory +import io.github.optimumcode.json.schema.internal.factories.string.PatternAssertionFactory +import io.github.optimumcode.json.schema.internal.util.getStringRequired +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +internal object Draft6SchemaLoaderConfig : SchemaLoaderConfig { + private val factories: List = + listOf( + TypeAssertionFactory, + EnumAssertionFactory, + ConstAssertionFactory, + MultipleOfAssertionFactory, + MaximumAssertionFactory, + ExclusiveMaximumAssertionFactory, + MinimumAssertionFactory, + ExclusiveMinimumAssertionFactory, + MaxLengthAssertionFactory, + MinLengthAssertionFactory, + PatternAssertionFactory, + ItemsAssertionFactory, + AdditionalItemsAssertionFactory, + MaxItemsAssertionFactory, + MinItemsAssertionFactory, + UniqueItemsAssertionFactory, + ContainsAssertionFactory, + MaxPropertiesAssertionFactory, + MinPropertiesAssertionFactory, + RequiredAssertionFactory, + PropertiesAssertionFactory, + PatternPropertiesAssertionFactory, + AdditionalPropertiesAssertionFactory, + PropertyNamesAssertionFactory, + DependenciesAssertionFactory, + AllOfAssertionFactory, + AnyOfAssertionFactory, + OneOfAssertionFactory, + NotAssertionFactory, + ) + + override val defaultVocabulary: SchemaLoaderConfig.Vocabulary = SchemaLoaderConfig.Vocabulary() + override val allFactories: List + get() = factories + + override fun createVocabulary(schemaDefinition: JsonElement): SchemaLoaderConfig.Vocabulary? = null + + override fun factories( + schemaDefinition: JsonElement, + vocabulary: SchemaLoaderConfig.Vocabulary, + options: SchemaLoaderConfig.Options, + ): List = + factories + + when (options[SchemaOption.FORMAT_BEHAVIOR_OPTION]) { + null, FormatBehavior.ANNOTATION_AND_ASSERTION -> FormatAssertionFactory.AnnotationAndAssertion + FormatBehavior.ANNOTATION_ONLY -> FormatAssertionFactory.AnnotationOnly + } + + override val keywordResolver: KeyWordResolver + get() = Draft6KeyWordResolver + override val referenceFactory: ReferenceFactory + get() = Draft6ReferenceFactory +} + +private object Draft6KeyWordResolver : KeyWordResolver { + private const val DEFINITIONS_PROPERTY: String = "definitions" + private const val ID_PROPERTY: String = "\$id" + const val REF_PROPERTY: String = "\$ref" + + override fun resolve(keyword: KeyWord): String? = + when (keyword) { + ID -> ID_PROPERTY + DEFINITIONS -> DEFINITIONS_PROPERTY + ANCHOR, COMPATIBILITY_DEFINITIONS, DYNAMIC_ANCHOR -> null + } +} + +private object Draft6ReferenceFactory : ReferenceFactory { + override fun extractRef( + schemaDefinition: JsonObject, + context: SchemaLoaderContext, + ): RefHolder? = + if (REF_PROPERTY in schemaDefinition) { + RefHolder.Simple(REF_PROPERTY, schemaDefinition.getStringRequired(REF_PROPERTY).let(context::ref)) + } else { + null + } + + override val allowOverriding: Boolean + get() = false + override val resolveRefPriorId: Boolean + get() = false + + override fun recursiveResolutionEnabled(schemaDefinition: JsonObject): Boolean = true +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/wellknown/Draft6.kt b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/wellknown/Draft6.kt new file mode 100644 index 00000000..80044ed8 --- /dev/null +++ b/src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/wellknown/Draft6.kt @@ -0,0 +1,165 @@ +package io.github.optimumcode.json.schema.internal.wellknown + +internal enum class Draft6( + val content: String, +) { + SCHEMA( +""" +{ + "${"$"}schema": "http://json-schema.org/draft-06/schema#", + "${"$"}id": "http://json-schema.org/draft-06/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "${"$"}ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "${"$"}ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "${"$"}id": { + "type": "string", + "format": "uri-reference" + }, + "${"$"}schema": { + "type": "string", + "format": "uri" + }, + "${"$"}ref": { + "type": "string", + "format": "uri-reference" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "examples": { + "type": "array", + "items": {} + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "${"$"}ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "${"$"}ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "${"$"}ref": "#" }, + "items": { + "anyOf": [ + { "${"$"}ref": "#" }, + { "${"$"}ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "${"$"}ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "${"$"}ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "${"$"}ref": "#" }, + "maxProperties": { "${"$"}ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "${"$"}ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "${"$"}ref": "#/definitions/stringArray" }, + "additionalProperties": { "${"$"}ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "${"$"}ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "${"$"}ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "${"$"}ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "${"$"}ref": "#" }, + { "${"$"}ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "${"$"}ref": "#" }, + "const": {}, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "${"$"}ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "${"$"}ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "${"$"}ref": "#/definitions/schemaArray" }, + "anyOf": { "${"$"}ref": "#/definitions/schemaArray" }, + "oneOf": { "${"$"}ref": "#/definitions/schemaArray" }, + "not": { "${"$"}ref": "#" } + }, + "default": {} +} +""", + ), +} \ No newline at end of file diff --git a/src/commonTest/kotlin/io/github/optimumcode/json/schema/extension/JsonSchemaExtensionTest.kt b/src/commonTest/kotlin/io/github/optimumcode/json/schema/extension/JsonSchemaExtensionTest.kt index 0da2de0d..e4d8e686 100644 --- a/src/commonTest/kotlin/io/github/optimumcode/json/schema/extension/JsonSchemaExtensionTest.kt +++ b/src/commonTest/kotlin/io/github/optimumcode/json/schema/extension/JsonSchemaExtensionTest.kt @@ -22,14 +22,16 @@ class JsonSchemaExtensionTest : FunSpec() { init { test("reports keyword that matches one of the existing keywords") { shouldThrow { - JsonSchemaLoader.create() + JsonSchemaLoader + .create() .withExtensions(DuplicatedAssertionFactory) - }.message shouldBe "external factory with keyword 'type' overlaps with 'type' keyword from DRAFT_7" + }.message shouldBe "external factory with keyword 'type' overlaps with 'type' keyword from DRAFT_6" } test("reports duplicated extension keywords") { shouldThrow { - JsonSchemaLoader.create() + JsonSchemaLoader + .create() .withExtensions(SimpleDateFormatAssertionFactory, SimpleDateFormatAssertionFactory) }.message shouldBe "duplicated extension factory with keyword 'dateFormat'" } @@ -93,7 +95,8 @@ class JsonSchemaExtensionTest : FunSpec() { test("registers all extensions with varargs") { val schema = shouldNotThrowAny { - JsonSchemaLoader.create() + JsonSchemaLoader + .create() .withExtensions(SimpleTimeFormatAssertionFactory, SimpleDateFormatAssertionFactory) .fromDefinition(schemaDef) } @@ -103,7 +106,8 @@ class JsonSchemaExtensionTest : FunSpec() { test("registers all extensions with iterable") { val schema = shouldNotThrowAny { - JsonSchemaLoader.create() + JsonSchemaLoader + .create() .withExtensions(listOf(SimpleTimeFormatAssertionFactory, SimpleDateFormatAssertionFactory)) .fromDefinition(schemaDef) } diff --git a/test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/AbstractSchemaTestSuite.kt b/test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/AbstractSchemaTestSuite.kt index 20741e6d..8031b85e 100644 --- a/test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/AbstractSchemaTestSuite.kt +++ b/test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/AbstractSchemaTestSuite.kt @@ -18,7 +18,6 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.builtins.serializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor diff --git a/test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/draft6/TestSuite.kt b/test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/draft6/TestSuite.kt new file mode 100644 index 00000000..25bd7760 --- /dev/null +++ b/test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/draft6/TestSuite.kt @@ -0,0 +1,17 @@ +package io.github.optimumcode.json.schema.suite.draft6 + +import io.github.optimumcode.json.schema.SchemaType.DRAFT_6 +import io.github.optimumcode.json.schema.suite.COMMON_FORMAT_FILTER +import io.github.optimumcode.json.schema.suite.runTestSuites +import io.kotest.core.spec.style.FunSpec + +@Suppress("unused") +internal class TestSuite : FunSpec() { + init { + runTestSuites( + draftName = "draft6", + schemaType = DRAFT_6, + formatFilter = COMMON_FORMAT_FILTER, + ) + } +} \ No newline at end of file