Skip to content

Commit fcf9f70

Browse files
authored
Use Uri class from kmp-uri library in JsonSchemaLoader's public API (#133)
Resolves #132
1 parent fff36e1 commit fcf9f70

File tree

4 files changed

+131
-44
lines changed

4 files changed

+131
-44
lines changed

api/json-schema-validator.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ public abstract interface class io/github/optimumcode/json/schema/JsonSchemaLoad
131131
public abstract fun register (Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
132132
public abstract fun register (Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
133133
public abstract fun register (Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
134+
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Lcom/eygraber/uri/Uri;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
135+
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Lcom/eygraber/uri/Uri;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
134136
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
135137
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
136138
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
@@ -151,7 +153,9 @@ public final class io/github/optimumcode/json/schema/JsonSchemaLoader$DefaultImp
151153
public static fun fromJsonElement (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchema;
152154
public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
153155
public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
156+
public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;Lcom/eygraber/uri/Uri;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
154157
public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
158+
public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
155159
public static fun registerWellKnown (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
156160
}
157161

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.optimumcode.json.schema
22

3+
import com.eygraber.uri.Uri
34
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2019_09
45
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2020_12
56
import io.github.optimumcode.json.schema.SchemaType.DRAFT_7
@@ -36,15 +37,44 @@ public interface JsonSchemaLoader {
3637
draft: SchemaType?,
3738
): JsonSchemaLoader
3839

40+
@Deprecated(
41+
message = "This method will be removed in a future release. Please use the alternative that accepts Uri type",
42+
level = DeprecationLevel.WARNING,
43+
replaceWith =
44+
ReplaceWith(
45+
imports = ["com.eygraber.uri.Uri"],
46+
expression = "register(schema, Uri.parse(remoteUri))",
47+
),
48+
)
3949
public fun register(
4050
schema: JsonElement,
4151
remoteUri: String,
52+
): JsonSchemaLoader = register(schema, Uri.parse(remoteUri), null)
53+
54+
public fun register(
55+
schema: JsonElement,
56+
remoteUri: Uri,
4257
): JsonSchemaLoader = register(schema, remoteUri, null)
4358

59+
@Deprecated(
60+
message = "This method will be removed in a future release. Please use the alternative that accepts Uri type",
61+
level = DeprecationLevel.WARNING,
62+
replaceWith =
63+
ReplaceWith(
64+
imports = ["com.eygraber.uri.Uri"],
65+
expression = "register(schema, Uri.parse(remoteUri), draft)",
66+
),
67+
)
4468
public fun register(
4569
schema: JsonElement,
4670
remoteUri: String,
4771
draft: SchemaType?,
72+
): JsonSchemaLoader = register(schema, Uri.parse(remoteUri), draft)
73+
74+
public fun register(
75+
schema: JsonElement,
76+
remoteUri: Uri,
77+
draft: SchemaType?,
4878
): JsonSchemaLoader
4979

5080
public fun withExtensions(

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

Lines changed: 65 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ internal class SchemaLoader : JsonSchemaLoader {
5858

5959
override fun register(
6060
schema: JsonElement,
61-
remoteUri: String,
61+
remoteUri: Uri,
6262
draft: SchemaType?,
6363
): JsonSchemaLoader =
6464
apply {
6565
loadSchemaData(
6666
schema,
6767
createParameters(draft),
68-
Uri.parse(remoteUri),
68+
remoteUri,
6969
)
7070
}
7171

@@ -268,13 +268,15 @@ private fun validateReferences(
268268

269269
private fun createSchema(result: LoadResult): JsonSchema {
270270
val dynamicRefs =
271-
result.references.asSequence()
271+
result.references
272+
.asSequence()
272273
.filter { it.value.dynamic }
273274
.map { it.key }
274275
.toSet()
275276
// pre-filter references to get rid of unused references
276277
val usedReferencesWithPath: Map<RefId, AssertionWithPath> =
277-
result.references.asSequence()
278+
result.references
279+
.asSequence()
278280
.filter { it.key in result.usedRefs || it.key in dynamicRefs }
279281
.associate { it.key to it.value }
280282
return JsonSchema(result.assertion, DefaultReferenceResolver(usedReferencesWithPath))
@@ -300,16 +302,15 @@ private fun resolveSchemaType(
300302
return schemaType ?: defaultType ?: SchemaType.entries.last()
301303
}
302304

303-
private fun extractSchema(schemaDefinition: JsonElement): String? {
304-
return if (schemaDefinition is JsonObject) {
305+
private fun extractSchema(schemaDefinition: JsonElement): String? =
306+
if (schemaDefinition is JsonObject) {
305307
schemaDefinition[SCHEMA_PROPERTY]?.let {
306308
require(it is JsonPrimitive && it.isString) { "$SCHEMA_PROPERTY must be a string" }
307309
it.content
308310
}
309311
} else {
310312
null
311313
}
312-
}
313314

314315
private fun loadDefinitions(
315316
schemaDefinition: JsonElement,
@@ -436,7 +437,8 @@ private fun loadJsonSchemaRoot(
436437
refAssertion: JsonSchemaAssertion?,
437438
): JsonSchemaRoot {
438439
val assertions =
439-
context.assertionFactories.filter { it.isApplicable(schemaDefinition) }
440+
context.assertionFactories
441+
.filter { it.isApplicable(schemaDefinition) }
440442
.map {
441443
it.create(
442444
schemaDefinition,
@@ -460,16 +462,15 @@ private fun loadJsonSchemaRoot(
460462
private fun loadRefAssertion(
461463
refHolder: RefHolder,
462464
context: DefaultLoadingContext,
463-
): JsonSchemaAssertion {
464-
return when (refHolder) {
465+
): JsonSchemaAssertion =
466+
when (refHolder) {
465467
is Simple -> RefSchemaAssertion(context.schemaPath / refHolder.property, refHolder.refId)
466468
is Recursive ->
467469
RecursiveRefSchemaAssertion(
468470
context.schemaPath / refHolder.property,
469471
refHolder.refId,
470472
)
471473
}
472-
}
473474

474475
/**
475476
* Used to identify the [location] where this [id] was defined
@@ -499,14 +500,11 @@ private data class DefaultLoadingContext(
499500
val config: SchemaLoaderConfig,
500501
val assertionFactories: List<AssertionFactory>,
501502
override val customFormatValidators: Map<String, FormatValidator>,
502-
) : LoadingContext, SchemaLoaderContext {
503-
override fun at(property: String): DefaultLoadingContext {
504-
return copy(schemaPath = schemaPath / property)
505-
}
503+
) : LoadingContext,
504+
SchemaLoaderContext {
505+
override fun at(property: String): DefaultLoadingContext = copy(schemaPath = schemaPath / property)
506506

507-
override fun at(index: Int): DefaultLoadingContext {
508-
return copy(schemaPath = schemaPath[index])
509-
}
507+
override fun at(index: Int): DefaultLoadingContext = copy(schemaPath = schemaPath[index])
510508

511509
override fun schemaFrom(element: JsonElement): JsonSchemaAssertion = loadSchema(element, this)
512510

@@ -527,7 +525,8 @@ private data class DefaultLoadingContext(
527525
for ((baseId, location) in additionalIDs) {
528526
val relativePointer = location.relative(schemaPath)
529527
val referenceId: RefId =
530-
baseId.buildUpon()
528+
baseId
529+
.buildUpon()
531530
.encodedFragment(relativePointer.toString())
532531
.buildRefId()
533532
if (referenceId.uri == id) {
@@ -549,12 +548,18 @@ private data class DefaultLoadingContext(
549548
dynamic: Boolean,
550549
) {
551550
require(ANCHOR_REGEX.matches(anchor)) { "$anchor must match the format ${ANCHOR_REGEX.pattern}" }
552-
val refId = additionalIDs.last().id.buildUpon().fragment(anchor).buildRefId()
551+
val refId =
552+
additionalIDs
553+
.last()
554+
.id
555+
.buildUpon()
556+
.fragment(anchor)
557+
.buildRefId()
553558
register(refId, assertion, dynamic)
554559
}
555560

556-
fun addId(additionalId: Uri): DefaultLoadingContext {
557-
return when {
561+
fun addId(additionalId: Uri): DefaultLoadingContext =
562+
when {
558563
additionalId.isAbsolute -> copy(additionalIDs = additionalIDs + IdWithLocation(additionalId, schemaPath))
559564
additionalId.isRelative && !additionalId.path.isNullOrBlank() ->
560565
copy(
@@ -570,7 +575,6 @@ private data class DefaultLoadingContext(
570575

571576
else -> this
572577
}
573-
}
574578

575579
override fun ref(refId: String): RefId {
576580
// library parsed fragment as empty if # is in the URI
@@ -581,18 +585,32 @@ private data class DefaultLoadingContext(
581585
return when {
582586
refUri.isAbsolute -> refUri.buildRefId()
583587
// the ref is absolute and should be resolved from current base URI host:port part
584-
refId.startsWith('/') -> additionalIDs.last().id.buildUpon().encodedPath(refUri.path).buildRefId()
588+
refId.startsWith('/') ->
589+
additionalIDs
590+
.last()
591+
.id
592+
.buildUpon()
593+
.encodedPath(refUri.path)
594+
.buildRefId()
585595
// in this case the ref must be resolved from the current base ID
586596
!refUri.path.isNullOrBlank() ->
587-
additionalIDs.resolvePath(refUri.path).run {
588-
if (refUri.fragment.isNullOrBlank()) {
589-
this
590-
} else {
591-
buildUpon().encodedFragment(refUri.fragment).build()
592-
}
593-
}.buildRefId()
594-
595-
refUri.fragment != null -> additionalIDs.last().id.buildUpon().encodedFragment(refUri.fragment).buildRefId()
597+
additionalIDs
598+
.resolvePath(refUri.path)
599+
.run {
600+
if (refUri.fragment.isNullOrBlank()) {
601+
this
602+
} else {
603+
buildUpon().encodedFragment(refUri.fragment).build()
604+
}
605+
}.buildRefId()
606+
607+
refUri.fragment != null ->
608+
additionalIDs
609+
.last()
610+
.id
611+
.buildUpon()
612+
.encodedFragment(refUri.fragment)
613+
.buildRefId()
596614
else -> throw IllegalArgumentException("invalid reference '$refId'")
597615
}.also { usedRef += ReferenceLocation(schemaPath, it) }
598616
}
@@ -632,7 +650,12 @@ private data class DefaultLoadingContext(
632650
!id.fragment.isNullOrBlank() ->
633651
register(
634652
// register JSON schema by fragment
635-
additionalIDs.last().id.buildUpon().encodedFragment(id.fragment).buildRefId(),
653+
additionalIDs
654+
.last()
655+
.id
656+
.buildUpon()
657+
.encodedFragment(id.fragment)
658+
.buildRefId(),
636659
assertion,
637660
dynamic,
638661
)
@@ -651,9 +674,12 @@ private data class DefaultLoadingContext(
651674
}
652675
}
653676

654-
private fun Set<IdWithLocation>.resolvePath(path: String?): Uri {
655-
return last().id.appendPathToParent(requireNotNull(path) { "path is null" })
656-
}
677+
private fun Set<IdWithLocation>.resolvePath(path: String?): Uri =
678+
last().id.appendPathToParent(
679+
requireNotNull(path) {
680+
"path is null"
681+
},
682+
)
657683

658684
private fun Uri.appendPathToParent(path: String): Uri {
659685
if (path.startsWith('/')) {
@@ -669,7 +695,8 @@ private fun Uri.appendPathToParent(path: String): Uri {
669695
.path(null) // reset path in builder
670696
.apply {
671697
if (pathSegments.isEmpty()) return@apply
672-
pathSegments.asSequence()
698+
pathSegments
699+
.asSequence()
673700
.take(pathSegments.size - 1) // drop last path segment
674701
.forEach(this::appendPath)
675702
}

test-suites/src/commonTest/kotlin/io/github/optimumcode/json/schema/suite/AbstractSchemaTestSuite.kt

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.optimumcode.json.schema.suite
22

3+
import com.eygraber.uri.Uri
34
import io.github.optimumcode.json.schema.ErrorCollector
45
import io.github.optimumcode.json.schema.FormatBehavior
56
import io.github.optimumcode.json.schema.FormatBehavior.ANNOTATION_AND_ASSERTION
@@ -13,10 +14,16 @@ import io.kotest.core.spec.style.FunSpec
1314
import io.kotest.matchers.shouldBe
1415
import io.kotest.mpp.env
1516
import kotlinx.serialization.ExperimentalSerializationApi
17+
import kotlinx.serialization.KSerializer
1618
import kotlinx.serialization.Serializable
1719
import kotlinx.serialization.builtins.ListSerializer
1820
import kotlinx.serialization.builtins.MapSerializer
1921
import kotlinx.serialization.builtins.serializer
22+
import kotlinx.serialization.descriptors.PrimitiveKind
23+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
24+
import kotlinx.serialization.descriptors.SerialDescriptor
25+
import kotlinx.serialization.encoding.Decoder
26+
import kotlinx.serialization.encoding.Encoder
2027
import kotlinx.serialization.json.Json
2128
import kotlinx.serialization.json.JsonArray
2229
import kotlinx.serialization.json.JsonElement
@@ -93,7 +100,7 @@ internal fun FunSpec.runTestSuites(
93100

94101
require(fs.exists(remoteSchemasDefinitions)) { "file $remoteSchemasDefinitions with remote schemas does not exist" }
95102

96-
val remoteSchemas: Map<String, JsonElement> = loadRemoteSchemas(fs, remoteSchemasDefinitions)
103+
val remoteSchemas: Map<Uri, JsonElement> = loadRemoteSchemas(fs, remoteSchemasDefinitions)
97104

98105
require(fs.exists(testSuiteDir)) { "folder $testSuiteDir does not exist" }
99106

@@ -131,24 +138,42 @@ internal fun FunSpec.runTestSuites(
131138
private fun loadRemoteSchemas(
132139
fs: FileSystem,
133140
remoteSchemasDefinitions: Path,
134-
): Map<String, JsonElement> =
141+
): Map<Uri, JsonElement> =
135142
fs.openReadOnly(remoteSchemasDefinitions).use { fh ->
136143
fh.source().use {
137144
Json.decodeFromBufferedSource(
138-
MapSerializer(String.serializer(), JsonElement.serializer()),
145+
MapSerializer(UriSerializer, JsonElement.serializer()),
139146
it.buffer(),
140147
)
141148
}
142149
}
143150

151+
private object UriSerializer : KSerializer<Uri> {
152+
override val descriptor: SerialDescriptor
153+
get() =
154+
PrimitiveSerialDescriptor(
155+
"com.eygraber.uri.Uri",
156+
kind = PrimitiveKind.STRING,
157+
)
158+
159+
override fun deserialize(decoder: Decoder): Uri = Uri.parse(decoder.decodeString())
160+
161+
override fun serialize(
162+
encoder: Encoder,
163+
value: Uri,
164+
) {
165+
encoder.encodeString(value.toString())
166+
}
167+
}
168+
144169
@OptIn(ExperimentalSerializationApi::class)
145170
private fun FunSpec.executeFromDirectory(
146171
fs: FileSystem,
147172
testSuiteDir: Path,
148173
excludeSuites: Map<String, Set<String>>,
149174
excludeTests: Map<String, Set<String>>,
150175
schemaType: SchemaType?,
151-
remoteSchemas: Map<String, JsonElement> = emptyMap(),
176+
remoteSchemas: Map<Uri, JsonElement> = emptyMap(),
152177
formatBehavior: FormatBehavior? = null,
153178
) {
154179
fs.list(testSuiteDir).forEach { testSuiteFile ->
@@ -170,14 +195,15 @@ private fun FunSpec.executeFromDirectory(
170195
}
171196
}
172197
val schemaLoader =
173-
JsonSchemaLoader.create()
198+
JsonSchemaLoader
199+
.create()
174200
.apply {
175201
formatBehavior?.also {
176202
withSchemaOption(SchemaOption.FORMAT_BEHAVIOR_OPTION, it)
177203
}
178204
SchemaType.entries.forEach(::registerWellKnown)
179205
for ((uri, schema) in remoteSchemas) {
180-
if (uri.contains("draft4", ignoreCase = true)) {
206+
if (uri.toString().contains("draft4", ignoreCase = true)) {
181207
// skip draft4 schemas
182208
continue
183209
}

0 commit comments

Comments
 (0)