Skip to content

Commit cb90f5e

Browse files
authored
Add support for uri and iri formats (#99)
Related to #54
1 parent e458073 commit cb90f5e

File tree

15 files changed

+839
-7
lines changed

15 files changed

+839
-7
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ val valid = schema.validate(elementToValidate, errors::add)
332332

333333
## Format assertion
334334

335-
The library supports `format` assertion. For now only a few formats are supported:
335+
The library supports `format` assertion. Not all formats are supported yet. The supported formats are:
336336
* date
337337
* time
338338
* date-time
@@ -344,6 +344,11 @@ The library supports `format` assertion. For now only a few formats are supporte
344344
* uuid
345345
* hostname
346346
* idn-hostname
347+
* uri
348+
* uri-reference
349+
* uri-template
350+
* iri
351+
* iri-reference
347352

348353
But there is an API to implement the user's defined format validation.
349354
The [FormatValidator](src/commonMain/kotlin/io/github/optimumcode/json/schema/ValidationError.kt) interface can be user for that.
@@ -360,7 +365,7 @@ You can implement custom assertions and use them. Read more [here](docs/custom_a
360365
This library uses official [JSON schema test suites](https://github.com/json-schema-org/JSON-Schema-Test-Suite)
361366
as a part of the CI to make sure the validation meet the expected behavior.
362367
Not everything is supported right now but the missing functionality might be added in the future.
363-
The test are located [here](test-suites).
368+
The tests are located [here](test-suites).
364369

365370

366371
**NOTE:** _Python 3.* is required to run test-suites._

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/general/FormatAssertionFactory.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ import io.github.optimumcode.json.schema.internal.formats.HostnameFormatValidato
2020
import io.github.optimumcode.json.schema.internal.formats.IdnHostnameFormatValidator
2121
import io.github.optimumcode.json.schema.internal.formats.IpV4FormatValidator
2222
import io.github.optimumcode.json.schema.internal.formats.IpV6FormatValidator
23+
import io.github.optimumcode.json.schema.internal.formats.IriFormatValidator
24+
import io.github.optimumcode.json.schema.internal.formats.IriReferenceFormatValidator
2325
import io.github.optimumcode.json.schema.internal.formats.JsonPointerFormatValidator
2426
import io.github.optimumcode.json.schema.internal.formats.RelativeJsonPointerFormatValidator
2527
import io.github.optimumcode.json.schema.internal.formats.TimeFormatValidator
28+
import io.github.optimumcode.json.schema.internal.formats.UriFormatValidator
29+
import io.github.optimumcode.json.schema.internal.formats.UriReferenceFormatValidator
30+
import io.github.optimumcode.json.schema.internal.formats.UriTemplateFormatValidator
2631
import io.github.optimumcode.json.schema.internal.formats.UuidFormatValidator
2732
import kotlinx.serialization.json.JsonElement
2833
import kotlinx.serialization.json.JsonPrimitive
@@ -72,6 +77,11 @@ internal sealed class FormatAssertionFactory(
7277
"uuid" to UuidFormatValidator,
7378
"hostname" to HostnameFormatValidator,
7479
"idn-hostname" to IdnHostnameFormatValidator,
80+
"uri" to UriFormatValidator,
81+
"uri-reference" to UriReferenceFormatValidator,
82+
"iri" to IriFormatValidator,
83+
"iri-reference" to IriReferenceFormatValidator,
84+
"uri-template" to UriTemplateFormatValidator,
7585
)
7686
}
7787
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.github.optimumcode.json.schema.internal.formats
2+
3+
import io.github.optimumcode.json.schema.FormatValidationResult
4+
5+
internal object IriFormatValidator : AbstractStringFormatValidator() {
6+
override fun validate(value: String): FormatValidationResult {
7+
if (value.isEmpty()) {
8+
return UriFormatValidator.validate(value)
9+
}
10+
val uri = IriSpec.covertToUri(value)
11+
return UriFormatValidator.validate(uri)
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.github.optimumcode.json.schema.internal.formats
2+
3+
import io.github.optimumcode.json.schema.FormatValidationResult
4+
5+
internal object IriReferenceFormatValidator : AbstractStringFormatValidator() {
6+
override fun validate(value: String): FormatValidationResult {
7+
if (value.isEmpty()) {
8+
return UriReferenceFormatValidator.validate(value)
9+
}
10+
val uri = IriSpec.covertToUri(value)
11+
return UriReferenceFormatValidator.validate(uri)
12+
}
13+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.github.optimumcode.json.schema.internal.formats
2+
3+
internal object IriSpec {
4+
private const val BITS_SHIFT = 4
5+
private const val LOWER_BITS = 0x0F
6+
private const val HEX_DECIMAL = "0123456789ABCDEF"
7+
8+
fun covertToUri(iri: String): String {
9+
return buildString {
10+
for (byte in iri.encodeToByteArray()) {
11+
if (byte >= 0) {
12+
append(byte.toInt().toChar())
13+
} else {
14+
val unsignedInt = byte.toUByte().toInt()
15+
append('%')
16+
append(HEX_DECIMAL[unsignedInt shr BITS_SHIFT])
17+
append(HEX_DECIMAL[unsignedInt and LOWER_BITS])
18+
}
19+
}
20+
}
21+
}
22+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.github.optimumcode.json.schema.internal.formats
2+
3+
import io.github.optimumcode.json.schema.FormatValidationResult
4+
import io.github.optimumcode.json.schema.FormatValidator
5+
import io.github.optimumcode.json.schema.internal.formats.UriSpec.FRAGMENT_DELIMITER
6+
import io.github.optimumcode.json.schema.internal.formats.UriSpec.QUERY_DELIMITER
7+
import io.github.optimumcode.json.schema.internal.formats.UriSpec.SCHEMA_DELIMITER
8+
9+
internal object UriFormatValidator : AbstractStringFormatValidator() {
10+
@Suppress("detekt:ReturnCount")
11+
override fun validate(value: String): FormatValidationResult {
12+
if (value.isEmpty()) {
13+
return FormatValidator.Invalid()
14+
}
15+
16+
val schemaEndIndex = value.indexOf(SCHEMA_DELIMITER)
17+
if (schemaEndIndex < 0 || schemaEndIndex == value.lastIndex) {
18+
return FormatValidator.Invalid()
19+
}
20+
21+
val schema = value.substring(0, schemaEndIndex)
22+
if (!UriSpec.isValidSchema(schema)) {
23+
return FormatValidator.Invalid()
24+
}
25+
26+
val fragmentDelimiterIndex = value.indexOf(FRAGMENT_DELIMITER)
27+
val queryDelimiterIndex =
28+
value.indexOf(QUERY_DELIMITER)
29+
.takeUnless { fragmentDelimiterIndex in 0..<it }
30+
?: -1
31+
val hierPart =
32+
when {
33+
queryDelimiterIndex > 0 ->
34+
value.substring(schemaEndIndex + 1, queryDelimiterIndex)
35+
fragmentDelimiterIndex > 0 ->
36+
value.substring(schemaEndIndex + 1, fragmentDelimiterIndex)
37+
else ->
38+
value.substring(schemaEndIndex + 1)
39+
}
40+
if (!UriSpec.isValidHierPart(hierPart)) {
41+
return FormatValidator.Invalid()
42+
}
43+
44+
if (queryDelimiterIndex > 0 && queryDelimiterIndex < value.lastIndex) {
45+
val query =
46+
if (fragmentDelimiterIndex > 0) {
47+
value.substring(queryDelimiterIndex + 1, fragmentDelimiterIndex)
48+
} else {
49+
value.substring(queryDelimiterIndex + 1)
50+
}
51+
if (!UriSpec.isValidQuery(query)) {
52+
return FormatValidator.Invalid()
53+
}
54+
}
55+
56+
if (fragmentDelimiterIndex > 0 && fragmentDelimiterIndex < value.lastIndex) {
57+
val fragment = value.substring(fragmentDelimiterIndex + 1)
58+
if (!UriSpec.isValidFragment(fragment)) {
59+
return FormatValidator.Invalid()
60+
}
61+
}
62+
63+
return FormatValidator.Valid()
64+
}
65+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.github.optimumcode.json.schema.internal.formats
2+
3+
import io.github.optimumcode.json.schema.FormatValidationResult
4+
import io.github.optimumcode.json.schema.FormatValidator
5+
import io.github.optimumcode.json.schema.internal.formats.UriSpec.FRAGMENT_DELIMITER
6+
import io.github.optimumcode.json.schema.internal.formats.UriSpec.QUERY_DELIMITER
7+
8+
internal object UriReferenceFormatValidator : AbstractStringFormatValidator() {
9+
@Suppress("detekt:ReturnCount")
10+
override fun validate(value: String): FormatValidationResult {
11+
if (UriFormatValidator.validate(value).isValid()) {
12+
return FormatValidator.Valid()
13+
}
14+
15+
val fragmentDelimiterIndex = value.indexOf(FRAGMENT_DELIMITER)
16+
val queryDelimiterIndex =
17+
value.indexOf(QUERY_DELIMITER)
18+
.takeUnless { fragmentDelimiterIndex in 0..<it }
19+
?: -1
20+
val relativePart =
21+
when {
22+
queryDelimiterIndex >= 0 ->
23+
value.substring(0, queryDelimiterIndex)
24+
fragmentDelimiterIndex >= 0 ->
25+
value.substring(0, fragmentDelimiterIndex)
26+
else -> value
27+
}
28+
if (!UriSpec.isValidRelativePart(relativePart)) {
29+
return FormatValidator.Invalid()
30+
}
31+
32+
if (queryDelimiterIndex >= 0 && queryDelimiterIndex < value.lastIndex) {
33+
val query =
34+
if (fragmentDelimiterIndex > 0) {
35+
value.substring(queryDelimiterIndex + 1, fragmentDelimiterIndex)
36+
} else {
37+
value.substring(queryDelimiterIndex + 1)
38+
}
39+
if (!UriSpec.isValidQuery(query)) {
40+
return FormatValidator.Invalid()
41+
}
42+
}
43+
44+
if (fragmentDelimiterIndex >= 0 && fragmentDelimiterIndex < value.lastIndex) {
45+
val fragment = value.substring(fragmentDelimiterIndex + 1)
46+
if (!UriSpec.isValidFragment(fragment)) {
47+
return FormatValidator.Invalid()
48+
}
49+
}
50+
51+
return FormatValidator.Valid()
52+
}
53+
}

0 commit comments

Comments
 (0)