Skip to content

Commit d2d2f03

Browse files
authored
Add support for draft 4 (#140)
Resolves #139
1 parent 68756be commit d2d2f03

File tree

11 files changed

+451
-5
lines changed

11 files changed

+451
-5
lines changed

api/json-schema-validator.api

+1
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ public final class io/github/optimumcode/json/schema/SchemaType : java/lang/Enum
195195
public static final field Companion Lio/github/optimumcode/json/schema/SchemaType$Companion;
196196
public static final field DRAFT_2019_09 Lio/github/optimumcode/json/schema/SchemaType;
197197
public static final field DRAFT_2020_12 Lio/github/optimumcode/json/schema/SchemaType;
198+
public static final field DRAFT_4 Lio/github/optimumcode/json/schema/SchemaType;
198199
public static final field DRAFT_6 Lio/github/optimumcode/json/schema/SchemaType;
199200
public static final field DRAFT_7 Lio/github/optimumcode/json/schema/SchemaType;
200201
public static final fun find (Ljava/lang/String;)Lio/github/optimumcode/json/schema/SchemaType;

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

+3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package io.github.optimumcode.json.schema
33
import com.eygraber.uri.Uri
44
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2019_09
55
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2020_12
6+
import io.github.optimumcode.json.schema.SchemaType.DRAFT_4
67
import io.github.optimumcode.json.schema.SchemaType.DRAFT_6
78
import io.github.optimumcode.json.schema.SchemaType.DRAFT_7
89
import io.github.optimumcode.json.schema.extension.ExternalAssertionFactory
910
import io.github.optimumcode.json.schema.internal.SchemaLoader
1011
import io.github.optimumcode.json.schema.internal.wellknown.Draft201909
1112
import io.github.optimumcode.json.schema.internal.wellknown.Draft202012
13+
import io.github.optimumcode.json.schema.internal.wellknown.Draft4
1214
import io.github.optimumcode.json.schema.internal.wellknown.Draft6
1315
import io.github.optimumcode.json.schema.internal.wellknown.Draft7
1416
import kotlinx.serialization.json.JsonElement
@@ -19,6 +21,7 @@ public interface JsonSchemaLoader {
1921
public fun registerWellKnown(draft: SchemaType): JsonSchemaLoader =
2022
apply {
2123
when (draft) {
24+
DRAFT_4 -> Draft4.entries.forEach { register(it.content) }
2225
DRAFT_6 -> Draft6.entries.forEach { register(it.content) }
2326
DRAFT_7 -> Draft7.entries.forEach { register(it.content) }
2427
DRAFT_2019_09 -> Draft201909.entries.forEach { register(it.content) }

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

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.eygraber.uri.Uri
44
import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig
55
import io.github.optimumcode.json.schema.internal.config.Draft201909SchemaLoaderConfig
66
import io.github.optimumcode.json.schema.internal.config.Draft202012SchemaLoaderConfig
7+
import io.github.optimumcode.json.schema.internal.config.Draft4SchemaLoaderConfig
78
import io.github.optimumcode.json.schema.internal.config.Draft6SchemaLoaderConfig
89
import io.github.optimumcode.json.schema.internal.config.Draft7SchemaLoaderConfig
910
import kotlin.jvm.JvmStatic
@@ -12,6 +13,7 @@ public enum class SchemaType(
1213
internal val schemaId: Uri,
1314
internal val config: SchemaLoaderConfig,
1415
) {
16+
DRAFT_4(Uri.parse("http://json-schema.org/draft-04/schema"), Draft4SchemaLoaderConfig),
1517
DRAFT_6(Uri.parse("http://json-schema.org/draft-06/schema"), Draft6SchemaLoaderConfig),
1618
DRAFT_7(Uri.parse("http://json-schema.org/draft-07/schema"), Draft7SchemaLoaderConfig),
1719
DRAFT_2019_09(Uri.parse("https://json-schema.org/draft/2019-09/schema"), Draft201909SchemaLoaderConfig),

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,25 @@ internal class SchemaLoader : JsonSchemaLoader {
157157
)
158158

159159
private fun addExtensionFactory(extensionFactory: ExternalAssertionFactory) {
160+
val matchedDrafts = mutableMapOf<String, MutableList<SchemaType>>()
160161
for (schemaType in SchemaType.entries) {
161162
val match =
162163
schemaType.config.allFactories.find { it.property.equals(extensionFactory.keywordName, ignoreCase = true) }
163164
if (match == null) {
164165
continue
165166
}
167+
matchedDrafts
168+
.getOrPut(
169+
match.property,
170+
::ArrayList,
171+
).add(schemaType)
172+
}
173+
if (matchedDrafts.isNotEmpty()) {
166174
error(
167175
"external factory with keyword '${extensionFactory.keywordName}' " +
168-
"overlaps with '${match.property}' keyword from $schemaType",
176+
"overlaps with ${matchedDrafts.entries.joinToString { (property, drafts) ->
177+
"'$property' keyword in $drafts draft(s)"
178+
}}",
169179
)
170180
}
171181
val duplicate = extensionFactories.keys.find { it.equals(extensionFactory.keywordName, ignoreCase = true) }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package io.github.optimumcode.json.schema.internal.config
2+
3+
import io.github.optimumcode.json.schema.FormatBehavior
4+
import io.github.optimumcode.json.schema.SchemaOption
5+
import io.github.optimumcode.json.schema.internal.AssertionFactory
6+
import io.github.optimumcode.json.schema.internal.KeyWord
7+
import io.github.optimumcode.json.schema.internal.KeyWord.ANCHOR
8+
import io.github.optimumcode.json.schema.internal.KeyWord.COMPATIBILITY_DEFINITIONS
9+
import io.github.optimumcode.json.schema.internal.KeyWord.DEFINITIONS
10+
import io.github.optimumcode.json.schema.internal.KeyWord.DYNAMIC_ANCHOR
11+
import io.github.optimumcode.json.schema.internal.KeyWord.ID
12+
import io.github.optimumcode.json.schema.internal.KeyWordResolver
13+
import io.github.optimumcode.json.schema.internal.ReferenceFactory
14+
import io.github.optimumcode.json.schema.internal.ReferenceFactory.RefHolder
15+
import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig
16+
import io.github.optimumcode.json.schema.internal.SchemaLoaderContext
17+
import io.github.optimumcode.json.schema.internal.config.Draft4KeyWordResolver.REF_PROPERTY
18+
import io.github.optimumcode.json.schema.internal.factories.array.AdditionalItemsAssertionFactory
19+
import io.github.optimumcode.json.schema.internal.factories.array.ContainsAssertionFactory
20+
import io.github.optimumcode.json.schema.internal.factories.array.ItemsAssertionFactory
21+
import io.github.optimumcode.json.schema.internal.factories.array.MaxItemsAssertionFactory
22+
import io.github.optimumcode.json.schema.internal.factories.array.MinItemsAssertionFactory
23+
import io.github.optimumcode.json.schema.internal.factories.array.UniqueItemsAssertionFactory
24+
import io.github.optimumcode.json.schema.internal.factories.condition.AllOfAssertionFactory
25+
import io.github.optimumcode.json.schema.internal.factories.condition.AnyOfAssertionFactory
26+
import io.github.optimumcode.json.schema.internal.factories.condition.NotAssertionFactory
27+
import io.github.optimumcode.json.schema.internal.factories.condition.OneOfAssertionFactory
28+
import io.github.optimumcode.json.schema.internal.factories.general.ConstAssertionFactory
29+
import io.github.optimumcode.json.schema.internal.factories.general.EnumAssertionFactory
30+
import io.github.optimumcode.json.schema.internal.factories.general.FormatAssertionFactory
31+
import io.github.optimumcode.json.schema.internal.factories.general.TypeAssertionFactory
32+
import io.github.optimumcode.json.schema.internal.factories.number.Draft4MaximumAssertionFactory
33+
import io.github.optimumcode.json.schema.internal.factories.number.Draft4MinimumAssertionFactory
34+
import io.github.optimumcode.json.schema.internal.factories.number.MinimumAssertionFactory
35+
import io.github.optimumcode.json.schema.internal.factories.number.MultipleOfAssertionFactory
36+
import io.github.optimumcode.json.schema.internal.factories.`object`.AdditionalPropertiesAssertionFactory
37+
import io.github.optimumcode.json.schema.internal.factories.`object`.DependenciesAssertionFactory
38+
import io.github.optimumcode.json.schema.internal.factories.`object`.MaxPropertiesAssertionFactory
39+
import io.github.optimumcode.json.schema.internal.factories.`object`.MinPropertiesAssertionFactory
40+
import io.github.optimumcode.json.schema.internal.factories.`object`.PatternPropertiesAssertionFactory
41+
import io.github.optimumcode.json.schema.internal.factories.`object`.PropertiesAssertionFactory
42+
import io.github.optimumcode.json.schema.internal.factories.`object`.PropertyNamesAssertionFactory
43+
import io.github.optimumcode.json.schema.internal.factories.`object`.RequiredAssertionFactory
44+
import io.github.optimumcode.json.schema.internal.factories.string.MaxLengthAssertionFactory
45+
import io.github.optimumcode.json.schema.internal.factories.string.MinLengthAssertionFactory
46+
import io.github.optimumcode.json.schema.internal.factories.string.PatternAssertionFactory
47+
import io.github.optimumcode.json.schema.internal.util.getStringRequired
48+
import kotlinx.serialization.json.JsonElement
49+
import kotlinx.serialization.json.JsonObject
50+
51+
internal object Draft4SchemaLoaderConfig : SchemaLoaderConfig {
52+
private val factories: List<AssertionFactory> =
53+
listOf(
54+
TypeAssertionFactory,
55+
EnumAssertionFactory,
56+
ConstAssertionFactory,
57+
MultipleOfAssertionFactory,
58+
Draft4MaximumAssertionFactory,
59+
Draft4MinimumAssertionFactory,
60+
MinimumAssertionFactory,
61+
MaxLengthAssertionFactory,
62+
MinLengthAssertionFactory,
63+
PatternAssertionFactory,
64+
ItemsAssertionFactory,
65+
AdditionalItemsAssertionFactory,
66+
MaxItemsAssertionFactory,
67+
MinItemsAssertionFactory,
68+
UniqueItemsAssertionFactory,
69+
ContainsAssertionFactory,
70+
MaxPropertiesAssertionFactory,
71+
MinPropertiesAssertionFactory,
72+
RequiredAssertionFactory,
73+
PropertiesAssertionFactory,
74+
PatternPropertiesAssertionFactory,
75+
AdditionalPropertiesAssertionFactory,
76+
PropertyNamesAssertionFactory,
77+
DependenciesAssertionFactory,
78+
AllOfAssertionFactory,
79+
AnyOfAssertionFactory,
80+
OneOfAssertionFactory,
81+
NotAssertionFactory,
82+
)
83+
84+
override val defaultVocabulary: SchemaLoaderConfig.Vocabulary = SchemaLoaderConfig.Vocabulary()
85+
override val allFactories: List<AssertionFactory>
86+
get() = factories
87+
88+
override fun createVocabulary(schemaDefinition: JsonElement): SchemaLoaderConfig.Vocabulary? = null
89+
90+
override fun factories(
91+
schemaDefinition: JsonElement,
92+
vocabulary: SchemaLoaderConfig.Vocabulary,
93+
options: SchemaLoaderConfig.Options,
94+
): List<AssertionFactory> =
95+
factories +
96+
when (options[SchemaOption.FORMAT_BEHAVIOR_OPTION]) {
97+
null, FormatBehavior.ANNOTATION_AND_ASSERTION -> FormatAssertionFactory.AnnotationAndAssertion
98+
FormatBehavior.ANNOTATION_ONLY -> FormatAssertionFactory.AnnotationOnly
99+
}
100+
101+
override val keywordResolver: KeyWordResolver
102+
get() = Draft4KeyWordResolver
103+
override val referenceFactory: ReferenceFactory
104+
get() = Draft4ReferenceFactory
105+
}
106+
107+
private object Draft4KeyWordResolver : KeyWordResolver {
108+
private const val DEFINITIONS_PROPERTY: String = "definitions"
109+
private const val ID_PROPERTY: String = "id"
110+
const val REF_PROPERTY: String = "\$ref"
111+
112+
override fun resolve(keyword: KeyWord): String? =
113+
when (keyword) {
114+
ID -> ID_PROPERTY
115+
DEFINITIONS -> DEFINITIONS_PROPERTY
116+
ANCHOR, COMPATIBILITY_DEFINITIONS, DYNAMIC_ANCHOR -> null
117+
}
118+
}
119+
120+
private object Draft4ReferenceFactory : ReferenceFactory {
121+
override fun extractRef(
122+
schemaDefinition: JsonObject,
123+
context: SchemaLoaderContext,
124+
): RefHolder? =
125+
if (REF_PROPERTY in schemaDefinition) {
126+
RefHolder.Simple(REF_PROPERTY, schemaDefinition.getStringRequired(REF_PROPERTY).let(context::ref))
127+
} else {
128+
null
129+
}
130+
131+
override val allowOverriding: Boolean
132+
get() = false
133+
override val resolveRefPriorId: Boolean
134+
get() = false
135+
136+
override fun recursiveResolutionEnabled(schemaDefinition: JsonObject): Boolean = true
137+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.github.optimumcode.json.schema.internal.factories.number
2+
3+
import io.github.optimumcode.json.schema.internal.AssertionFactory
4+
import io.github.optimumcode.json.schema.internal.JsonSchemaAssertion
5+
import io.github.optimumcode.json.schema.internal.LoadingContext
6+
import io.github.optimumcode.json.schema.internal.factories.number.util.NumberComparisonAssertion
7+
import io.github.optimumcode.json.schema.internal.factories.number.util.compareTo
8+
import io.github.optimumcode.json.schema.internal.factories.number.util.number
9+
import kotlinx.serialization.json.JsonElement
10+
import kotlinx.serialization.json.JsonObject
11+
import kotlinx.serialization.json.JsonPrimitive
12+
import kotlinx.serialization.json.booleanOrNull
13+
14+
@Suppress("unused")
15+
internal object Draft4MaximumAssertionFactory : AssertionFactory {
16+
private const val EXCLUSIVE_MAXIMUM_PROPERTY = "exclusiveMaximum"
17+
18+
override val property: String
19+
get() = "maximum"
20+
21+
override fun isApplicable(element: JsonElement): Boolean = element is JsonObject && element.contains(property)
22+
23+
override fun create(
24+
element: JsonElement,
25+
context: LoadingContext,
26+
): JsonSchemaAssertion {
27+
require(element is JsonObject) { "cannot extract $property property from ${element::class.simpleName}" }
28+
val typeElement = requireNotNull(element[property]) { "no property $property found in element $element" }
29+
val exclusive: Boolean =
30+
element[EXCLUSIVE_MAXIMUM_PROPERTY]?.let {
31+
require(it is JsonPrimitive) { "$EXCLUSIVE_MAXIMUM_PROPERTY must be a boolean" }
32+
requireNotNull(it.booleanOrNull) { "$EXCLUSIVE_MAXIMUM_PROPERTY must be a valid boolean" }
33+
} ?: false
34+
return createFromProperty(typeElement, context.at(property), exclusive)
35+
}
36+
37+
private fun createFromProperty(
38+
element: JsonElement,
39+
context: LoadingContext,
40+
exclusive: Boolean,
41+
): JsonSchemaAssertion {
42+
require(element is JsonPrimitive) { "$property must be a number" }
43+
val maximumValue: Number =
44+
requireNotNull(element.number) { "$property must be a valid number" }
45+
return NumberComparisonAssertion(
46+
context.schemaPath,
47+
maximumValue,
48+
element.content,
49+
errorMessage = if (exclusive) "must be less" else "must be less or equal to",
50+
if (exclusive) {
51+
{ a, b -> a < b }
52+
} else {
53+
{ a, b -> a <= b }
54+
},
55+
)
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.github.optimumcode.json.schema.internal.factories.number
2+
3+
import io.github.optimumcode.json.schema.internal.AssertionFactory
4+
import io.github.optimumcode.json.schema.internal.JsonSchemaAssertion
5+
import io.github.optimumcode.json.schema.internal.LoadingContext
6+
import io.github.optimumcode.json.schema.internal.factories.number.util.NumberComparisonAssertion
7+
import io.github.optimumcode.json.schema.internal.factories.number.util.compareTo
8+
import io.github.optimumcode.json.schema.internal.factories.number.util.number
9+
import kotlinx.serialization.json.JsonElement
10+
import kotlinx.serialization.json.JsonObject
11+
import kotlinx.serialization.json.JsonPrimitive
12+
import kotlinx.serialization.json.booleanOrNull
13+
14+
@Suppress("unused")
15+
internal object Draft4MinimumAssertionFactory : AssertionFactory {
16+
private const val EXCLUSIVE_MINIMUM_PROPERTY = "exclusiveMinimum"
17+
18+
override val property: String
19+
get() = "minimum"
20+
21+
override fun isApplicable(element: JsonElement): Boolean = element is JsonObject && element.contains(property)
22+
23+
override fun create(
24+
element: JsonElement,
25+
context: LoadingContext,
26+
): JsonSchemaAssertion {
27+
require(element is JsonObject) { "cannot extract $property property from ${element::class.simpleName}" }
28+
val typeElement = requireNotNull(element[property]) { "no property $property found in element $element" }
29+
val exclusive: Boolean =
30+
element[EXCLUSIVE_MINIMUM_PROPERTY]?.let {
31+
require(it is JsonPrimitive) { "$EXCLUSIVE_MINIMUM_PROPERTY must be a boolean" }
32+
requireNotNull(it.booleanOrNull) { "$EXCLUSIVE_MINIMUM_PROPERTY must be a valid boolean" }
33+
} ?: false
34+
return createFromProperty(typeElement, context.at(property), exclusive)
35+
}
36+
37+
private fun createFromProperty(
38+
element: JsonElement,
39+
context: LoadingContext,
40+
exclusive: Boolean,
41+
): JsonSchemaAssertion {
42+
require(element is JsonPrimitive) { "$property must be a number" }
43+
val maximumValue: Number =
44+
requireNotNull(element.number) { "$property must be a valid number" }
45+
return NumberComparisonAssertion(
46+
context.schemaPath,
47+
maximumValue,
48+
element.content,
49+
errorMessage = if (exclusive) "must be greater" else "must be greater or equal to",
50+
if (exclusive) {
51+
{ a, b -> a > b }
52+
} else {
53+
{ a, b -> a >= b }
54+
},
55+
)
56+
}
57+
}

0 commit comments

Comments
 (0)