Skip to content

Commit 119f2a3

Browse files
authored
Make library comply to official JSON schema repository test-suites (#19)
Resolves #15
1 parent 9bfbddf commit 119f2a3

File tree

45 files changed

+894
-68
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+894
-68
lines changed

.github/workflows/check.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ jobs:
1818
steps:
1919
- name: 'Checkout Repository'
2020
uses: actions/checkout@v3
21+
with:
22+
submodules: true
2123
- uses: actions/setup-java@v3
2224
with:
2325
distribution: temurin

.github/workflows/release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ jobs:
3131
steps:
3232
- name: 'Checkout Repository'
3333
uses: actions/checkout@v3
34+
with:
35+
submodules: true
3436
- uses: actions/setup-java@v3
3537
with:
3638
distribution: temurin

.github/workflows/snapshot_release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ jobs:
1111
steps:
1212
- name: 'Checkout Repository'
1313
uses: actions/checkout@v3
14+
with:
15+
submodules: true
1416
- uses: actions/setup-java@v3
1517
with:
1618
distribution: temurin

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "schema-test-suite"]
2+
path = test-suites/schema-test-suite
3+
url = [email protected]:json-schema-org/JSON-Schema-Test-Suite.git

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,13 @@ val valid = schema.validate(elementToValidate, errors::add)
167167
| | oneOf | Supported |
168168
| | not | Supported |
169169

170+
## Compliance to JSON schema test suites
171+
172+
This library uses official [JSON schema test suites](https://github.com/json-schema-org/JSON-Schema-Test-Suite)
173+
as a part of the CI to make sure the validation meet the expected behavior.
174+
Not everything is supported right now but the missing functionality might be added in the future.
175+
The test are located [here](test-suites).
176+
170177
## Future plans
171178

172179
- [x] Add `$schema` property validation (if not set the latest supported will be used)

gradle/libs.versions.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ kotlin = "1.8.22"
33
kotest = "5.5.4"
44
detekt = "1.23.0"
55
ktlint = "0.50.0"
6+
okio = "3.4.0"
7+
serialization = "1.5.1"
68

79
[plugins]
810
kotlin-mutliplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
@@ -16,8 +18,11 @@ kotlin-binaryCompatibility = { id = "org.jetbrains.kotlinx.binary-compatibility-
1618
nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version = "1.3.0" }
1719

1820
[libraries]
19-
kotlin-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.5.1" }
21+
kotlin-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
22+
kotlin-serialization-json-okio = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json-okio", version.ref = "serialization" }
2023
kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
2124
kotest-framework-engine = { module = "io.kotest:kotest-framework-engine", version.ref = "kotest" }
2225
kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
23-
uri = { group = "com.eygraber", name = "uri-kmp", version = "0.0.12" }
26+
uri = { group = "com.eygraber", name = "uri-kmp", version = "0.0.12" }
27+
okio-common = { group = "com.squareup.okio", name = "okio", version.ref = "okio" }
28+
okio-nodefilesystem = { group = "com.squareup.okio", name = "okio-nodefilesystem", version.ref = "okio" }

settings.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
rootProject.name = "json-schema-validator"
1+
rootProject.name = "json-schema-validator"
2+
3+
include(":test-suites")

src/commonMain/kotlin/io/github/optimumcode/json/pointer/JsonPointer.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ public sealed class JsonPointer(
3333
buildString {
3434
val pointer = this@JsonPointer.toString()
3535
append(pointer)
36-
if (!pointer.endsWith(SEPARATOR)) {
37-
append(SEPARATOR)
38-
}
36+
append(SEPARATOR)
3937
append(index)
4038
},
4139
)
@@ -53,10 +51,14 @@ public sealed class JsonPointer(
5351
buildString {
5452
val pointer = this@JsonPointer.toString()
5553
append(pointer)
56-
if (!pointer.endsWith(SEPARATOR)) {
57-
append(SEPARATOR)
54+
append(SEPARATOR)
55+
for (ch in property) {
56+
when (ch) {
57+
QUOTATION -> append(QUOTATION).append(QUOTATION_ESCAPE)
58+
SEPARATOR -> append(QUOTATION).append(SEPARATOR_ESCAPE)
59+
else -> append(ch)
60+
}
5861
}
59-
append(property)
6062
},
6163
)
6264

@@ -87,6 +89,8 @@ public sealed class JsonPointer(
8789
public companion object {
8890
internal const val SEPARATOR: Char = '/'
8991
internal const val QUOTATION: Char = '~'
92+
internal const val QUOTATION_ESCAPE: Char = '0'
93+
internal const val SEPARATOR_ESCAPE: Char = '1'
9094

9195
/**
9296
* An empty [JsonPointer]. The empty JSON pointer corresponds to the current JSON element.s
@@ -205,8 +209,8 @@ private fun StringBuilder.appendEscapedSegment(expr: String, start: Int, offset:
205209

206210
private fun StringBuilder.appendEscaped(ch: Char) {
207211
val result = when (ch) {
208-
'0' -> JsonPointer.QUOTATION
209-
'1' -> JsonPointer.SEPARATOR
212+
JsonPointer.QUOTATION_ESCAPE -> JsonPointer.QUOTATION
213+
JsonPointer.SEPARATOR_ESCAPE -> JsonPointer.SEPARATOR
210214
else -> {
211215
append(JsonPointer.QUOTATION)
212216
ch

src/commonMain/kotlin/io/github/optimumcode/json/pointer/extensions.kt

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,8 @@ public operator fun JsonPointer.plus(otherPointer: JsonPointer): JsonPointer {
5757
}
5858
return JsonPointer(
5959
buildString {
60-
val pointer = this@plus.toString()
61-
append(pointer)
62-
if (pointer.endsWith(JsonPointer.SEPARATOR)) {
63-
setLength(length - 1)
64-
}
65-
val other = otherPointer.toString()
66-
append(other)
60+
append(this@plus.toString())
61+
append(otherPointer.toString())
6762
},
6863
)
6964
}

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,8 @@ private data class DefaultLoadingContext(
195195
private val baseId: Uri,
196196
override val schemaPath: JsonPointer = JsonPointer.ROOT,
197197
val additionalIDs: Set<IdWithLocation> = linkedSetOf(IdWithLocation(baseId, schemaPath)),
198-
val references: MutableMap<RefId, AssertionWithPath> = hashMapOf(),
199-
val usedRef: MutableSet<ReferenceLocation> = hashSetOf(),
198+
val references: MutableMap<RefId, AssertionWithPath> = linkedMapOf(),
199+
val usedRef: MutableSet<ReferenceLocation> = linkedSetOf(),
200200
) : LoadingContext {
201201
override fun at(property: String): DefaultLoadingContext {
202202
return copy(schemaPath = schemaPath / property)
@@ -237,7 +237,7 @@ private data class DefaultLoadingContext(
237237
copy(
238238
additionalIDs = additionalIDs.run {
239239
this + IdWithLocation(
240-
baseId.buildUpon().encodedPath(additionalId.path).build(),
240+
additionalIDs.resolvePath(additionalId.path),
241241
schemaPath,
242242
)
243243
},
@@ -255,7 +255,10 @@ private data class DefaultLoadingContext(
255255
val refUri = Uri.parse(refId).buildUpon().build()
256256
return when {
257257
refUri.isAbsolute -> refUri.buildRefId()
258-
!refUri.path.isNullOrBlank() -> baseId.buildUpon().encodedPath(refUri.path).buildRefId()
258+
// the ref is absolute and should be resolved from current base URI host:port part
259+
refId.startsWith('/') -> additionalIDs.last().id.buildUpon().encodedPath(refUri.path).buildRefId()
260+
// in this case the ref must be resolved from the current base ID
261+
!refUri.path.isNullOrBlank() -> additionalIDs.resolvePath(refUri.path).buildRefId()
259262
refUri.fragment != null -> additionalIDs.last().id.buildUpon().encodedFragment(refUri.fragment).buildRefId()
260263
else -> throw IllegalArgumentException("invalid reference $refId")
261264
}.also { usedRef += ReferenceLocation(schemaPath, it) }
@@ -271,7 +274,7 @@ private data class DefaultLoadingContext(
271274
when {
272275
!id.path.isNullOrBlank() -> register(
273276
// register JSON schema by related path
274-
baseId.buildUpon().encodedPath(id.path).buildRefId(),
277+
additionalIDs.resolvePath(id.path).buildRefId(),
275278
assertion,
276279
)
277280

@@ -294,6 +297,24 @@ private data class DefaultLoadingContext(
294297
}
295298
}
296299

300+
private fun Set<IdWithLocation>.resolvePath(path: String?): Uri {
301+
return last().id.appendPathToParent(requireNotNull(path) { "path is null" })
302+
}
303+
private fun Uri.appendPathToParent(path: String): Uri {
304+
val hasLastEmptySegment = toString().endsWith('/')
305+
return if (hasLastEmptySegment) {
306+
buildUpon() // don't need to drop anything. just add the path because / in the end means empty segment
307+
} else {
308+
buildUpon()
309+
.path(null) // reset path in builder
310+
.apply {
311+
pathSegments.asSequence()
312+
.take(pathSegments.size - 1) // drop last path segment
313+
.forEach(this::appendPath)
314+
}
315+
}.appendEncodedPath(path)
316+
.build()
317+
}
297318
private fun Uri.buildRefId(): RefId = RefId(this)
298319

299320
private fun Builder.buildRefId(): RefId = build().buildRefId()

0 commit comments

Comments
 (0)