Skip to content

Commit

Permalink
Add support for uri and iri formats (#99)
Browse files Browse the repository at this point in the history
Related to #54
  • Loading branch information
OptimumCode authored Apr 26, 2024
1 parent e458073 commit cb90f5e
Show file tree
Hide file tree
Showing 15 changed files with 839 additions and 7 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ val valid = schema.validate(elementToValidate, errors::add)

## Format assertion

The library supports `format` assertion. For now only a few formats are supported:
The library supports `format` assertion. Not all formats are supported yet. The supported formats are:
* date
* time
* date-time
Expand All @@ -344,6 +344,11 @@ The library supports `format` assertion. For now only a few formats are supporte
* uuid
* hostname
* idn-hostname
* uri
* uri-reference
* uri-template
* iri
* iri-reference

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


**NOTE:** _Python 3.* is required to run test-suites._
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ import io.github.optimumcode.json.schema.internal.formats.HostnameFormatValidato
import io.github.optimumcode.json.schema.internal.formats.IdnHostnameFormatValidator
import io.github.optimumcode.json.schema.internal.formats.IpV4FormatValidator
import io.github.optimumcode.json.schema.internal.formats.IpV6FormatValidator
import io.github.optimumcode.json.schema.internal.formats.IriFormatValidator
import io.github.optimumcode.json.schema.internal.formats.IriReferenceFormatValidator
import io.github.optimumcode.json.schema.internal.formats.JsonPointerFormatValidator
import io.github.optimumcode.json.schema.internal.formats.RelativeJsonPointerFormatValidator
import io.github.optimumcode.json.schema.internal.formats.TimeFormatValidator
import io.github.optimumcode.json.schema.internal.formats.UriFormatValidator
import io.github.optimumcode.json.schema.internal.formats.UriReferenceFormatValidator
import io.github.optimumcode.json.schema.internal.formats.UriTemplateFormatValidator
import io.github.optimumcode.json.schema.internal.formats.UuidFormatValidator
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
Expand Down Expand Up @@ -72,6 +77,11 @@ internal sealed class FormatAssertionFactory(
"uuid" to UuidFormatValidator,
"hostname" to HostnameFormatValidator,
"idn-hostname" to IdnHostnameFormatValidator,
"uri" to UriFormatValidator,
"uri-reference" to UriReferenceFormatValidator,
"iri" to IriFormatValidator,
"iri-reference" to IriReferenceFormatValidator,
"uri-template" to UriTemplateFormatValidator,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.optimumcode.json.schema.internal.formats

import io.github.optimumcode.json.schema.FormatValidationResult

internal object IriFormatValidator : AbstractStringFormatValidator() {
override fun validate(value: String): FormatValidationResult {
if (value.isEmpty()) {
return UriFormatValidator.validate(value)
}
val uri = IriSpec.covertToUri(value)
return UriFormatValidator.validate(uri)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.optimumcode.json.schema.internal.formats

import io.github.optimumcode.json.schema.FormatValidationResult

internal object IriReferenceFormatValidator : AbstractStringFormatValidator() {
override fun validate(value: String): FormatValidationResult {
if (value.isEmpty()) {
return UriReferenceFormatValidator.validate(value)
}
val uri = IriSpec.covertToUri(value)
return UriReferenceFormatValidator.validate(uri)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.optimumcode.json.schema.internal.formats

internal object IriSpec {
private const val BITS_SHIFT = 4
private const val LOWER_BITS = 0x0F
private const val HEX_DECIMAL = "0123456789ABCDEF"

fun covertToUri(iri: String): String {
return buildString {
for (byte in iri.encodeToByteArray()) {
if (byte >= 0) {
append(byte.toInt().toChar())
} else {
val unsignedInt = byte.toUByte().toInt()
append('%')
append(HEX_DECIMAL[unsignedInt shr BITS_SHIFT])
append(HEX_DECIMAL[unsignedInt and LOWER_BITS])
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.github.optimumcode.json.schema.internal.formats

import io.github.optimumcode.json.schema.FormatValidationResult
import io.github.optimumcode.json.schema.FormatValidator
import io.github.optimumcode.json.schema.internal.formats.UriSpec.FRAGMENT_DELIMITER
import io.github.optimumcode.json.schema.internal.formats.UriSpec.QUERY_DELIMITER
import io.github.optimumcode.json.schema.internal.formats.UriSpec.SCHEMA_DELIMITER

internal object UriFormatValidator : AbstractStringFormatValidator() {
@Suppress("detekt:ReturnCount")
override fun validate(value: String): FormatValidationResult {
if (value.isEmpty()) {
return FormatValidator.Invalid()
}

val schemaEndIndex = value.indexOf(SCHEMA_DELIMITER)
if (schemaEndIndex < 0 || schemaEndIndex == value.lastIndex) {
return FormatValidator.Invalid()
}

val schema = value.substring(0, schemaEndIndex)
if (!UriSpec.isValidSchema(schema)) {
return FormatValidator.Invalid()
}

val fragmentDelimiterIndex = value.indexOf(FRAGMENT_DELIMITER)
val queryDelimiterIndex =
value.indexOf(QUERY_DELIMITER)
.takeUnless { fragmentDelimiterIndex in 0..<it }
?: -1
val hierPart =
when {
queryDelimiterIndex > 0 ->
value.substring(schemaEndIndex + 1, queryDelimiterIndex)
fragmentDelimiterIndex > 0 ->
value.substring(schemaEndIndex + 1, fragmentDelimiterIndex)
else ->
value.substring(schemaEndIndex + 1)
}
if (!UriSpec.isValidHierPart(hierPart)) {
return FormatValidator.Invalid()
}

if (queryDelimiterIndex > 0 && queryDelimiterIndex < value.lastIndex) {
val query =
if (fragmentDelimiterIndex > 0) {
value.substring(queryDelimiterIndex + 1, fragmentDelimiterIndex)
} else {
value.substring(queryDelimiterIndex + 1)
}
if (!UriSpec.isValidQuery(query)) {
return FormatValidator.Invalid()
}
}

if (fragmentDelimiterIndex > 0 && fragmentDelimiterIndex < value.lastIndex) {
val fragment = value.substring(fragmentDelimiterIndex + 1)
if (!UriSpec.isValidFragment(fragment)) {
return FormatValidator.Invalid()
}
}

return FormatValidator.Valid()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.github.optimumcode.json.schema.internal.formats

import io.github.optimumcode.json.schema.FormatValidationResult
import io.github.optimumcode.json.schema.FormatValidator
import io.github.optimumcode.json.schema.internal.formats.UriSpec.FRAGMENT_DELIMITER
import io.github.optimumcode.json.schema.internal.formats.UriSpec.QUERY_DELIMITER

internal object UriReferenceFormatValidator : AbstractStringFormatValidator() {
@Suppress("detekt:ReturnCount")
override fun validate(value: String): FormatValidationResult {
if (UriFormatValidator.validate(value).isValid()) {
return FormatValidator.Valid()
}

val fragmentDelimiterIndex = value.indexOf(FRAGMENT_DELIMITER)
val queryDelimiterIndex =
value.indexOf(QUERY_DELIMITER)
.takeUnless { fragmentDelimiterIndex in 0..<it }
?: -1
val relativePart =
when {
queryDelimiterIndex >= 0 ->
value.substring(0, queryDelimiterIndex)
fragmentDelimiterIndex >= 0 ->
value.substring(0, fragmentDelimiterIndex)
else -> value
}
if (!UriSpec.isValidRelativePart(relativePart)) {
return FormatValidator.Invalid()
}

if (queryDelimiterIndex >= 0 && queryDelimiterIndex < value.lastIndex) {
val query =
if (fragmentDelimiterIndex > 0) {
value.substring(queryDelimiterIndex + 1, fragmentDelimiterIndex)
} else {
value.substring(queryDelimiterIndex + 1)
}
if (!UriSpec.isValidQuery(query)) {
return FormatValidator.Invalid()
}
}

if (fragmentDelimiterIndex >= 0 && fragmentDelimiterIndex < value.lastIndex) {
val fragment = value.substring(fragmentDelimiterIndex + 1)
if (!UriSpec.isValidFragment(fragment)) {
return FormatValidator.Invalid()
}
}

return FormatValidator.Valid()
}
}
Loading

0 comments on commit cb90f5e

Please sign in to comment.