Skip to content

Add support for uri and iri formats #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading