Skip to content

Commit

Permalink
Add support for json-pointer and relative-json-pointer formats (#73)
Browse files Browse the repository at this point in the history
Related to #54
  • Loading branch information
OptimumCode authored Mar 4, 2024
1 parent 9a78967 commit b53d8fd
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
with:
gradle-version: wrapper
- name: Build
run: ./gradlew --no-daemon --info ${{ inputs.task }} detektAll ktlintCheck apiCheck koverXmlReport -x :benchmark:benchmark
run: ./gradlew --no-daemon --info ${{ inputs.task }} koverXmlReport -x :benchmark:benchmark
- name: Upload coverage reports to Codecov
if: inputs.upload-code-coverage && github.actor != 'dependabot[bot]'
uses: codecov/codecov-action@v4
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,30 @@ concurrency:
cancel-in-progress: true

jobs:
check-style:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version-file: .ci-java-version
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v2
- name: Cache konan
uses: actions/cache@v4
with:
path: ~/.konan
key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
gradle-version: wrapper
- name: Build
run: ./gradlew --no-daemon --continue detektAll ktlintCheck apiCheck
check-linux:
uses: ./.github/workflows/check.yml
with:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ The library supports `format` assertion. For now only a few formats are supporte
* time
* date-time
* duration
* json-pointer
* relative-json-pointer

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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import io.github.optimumcode.json.schema.internal.factories.AbstractAssertionFac
import io.github.optimumcode.json.schema.internal.formats.DateFormatValidator
import io.github.optimumcode.json.schema.internal.formats.DateTimeFormatValidator
import io.github.optimumcode.json.schema.internal.formats.DurationFormatValidator
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 kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
Expand Down Expand Up @@ -58,6 +60,8 @@ internal sealed class FormatAssertionFactory(
"time" to TimeFormatValidator,
"date-time" to DateTimeFormatValidator,
"duration" to DurationFormatValidator,
"json-pointer" to JsonPointerFormatValidator,
"relative-json-pointer" to RelativeJsonPointerFormatValidator,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.github.optimumcode.json.schema.internal.formats

import io.github.optimumcode.json.pointer.JsonPointer
import io.github.optimumcode.json.schema.FormatValidationResult
import io.github.optimumcode.json.schema.FormatValidator

internal object JsonPointerFormatValidator : AbstractStringFormatValidator() {
override fun validate(value: String): FormatValidationResult {
if (value.isEmpty()) {
return FormatValidator.Valid()
}
if (!value.startsWith(JsonPointer.SEPARATOR)) {
return FormatValidator.Invalid()
}
var escape = false
for (symbol in value) {
if (escape && symbol != JsonPointer.QUOTATION_ESCAPE && symbol != JsonPointer.SEPARATOR_ESCAPE) {
return FormatValidator.Invalid()
}
escape = symbol == JsonPointer.QUOTATION
}
return if (escape) {
// escape character '~' in the end of the segment
FormatValidator.Invalid()
} else {
FormatValidator.Valid()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.github.optimumcode.json.schema.internal.formats

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

internal object RelativeJsonPointerFormatValidator : AbstractStringFormatValidator() {
private const val ZERO_CODE: Int = '0'.code
private const val NINE_CODE: Int = '9'.code
private const val REF_SYMBOL = '#'

override fun validate(value: String): FormatValidationResult {
if (value.isEmpty()) {
return FormatValidator.Invalid()
}
val isFirstZero = value[0].code == ZERO_CODE
for ((index, symbol) in value.withIndex()) {
val code = symbol.code
val isDigit = code in ZERO_CODE..NINE_CODE
val isRef = symbol == REF_SYMBOL
if (!isDigit) {
return checkEnding(index, isRef, value)
}
if (code > ZERO_CODE && isFirstZero) {
// leading zeros are not allowed
return FormatValidator.Invalid()
}
}
return FormatValidator.Valid()
}

private fun checkEnding(
index: Int,
isRef: Boolean,
value: String,
): FormatValidationResult =
when {
// we must have a digit at the beginning
index == 0 -> FormatValidator.Invalid()
isRef ->
if (index == value.lastIndex) {
FormatValidator.Valid()
} else {
// # must be the last character
FormatValidator.Invalid()
}

else -> JsonPointerFormatValidator.validate(value.substring(index))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.github.optimumcode.json.schema.assertions.general.format

import io.github.optimumcode.json.schema.assertions.general.format.FormatValidationTestSuite.TestCase
import io.kotest.core.spec.style.FunSpec

class JsonSchemaJsonPointerFormatValidationTest : FunSpec() {
init {
FormatValidationTestSuite(
format = "json-pointer",
validTestCases =
listOf(
"",
"/",
"/test//a",
"/test/",
"/tes~0",
"/test~1",
),
invalidTestCases =
listOf(
TestCase("test", "does not start from separator"),
TestCase("/test~2", "invalid quotation"),
TestCase("/test~", "trailing quotation"),
),
).run { testFormat() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.github.optimumcode.json.schema.assertions.general.format

import io.github.optimumcode.json.schema.assertions.general.format.FormatValidationTestSuite.TestCase
import io.kotest.core.spec.style.FunSpec

class JsonSchemaRelativeJsonPointerFormatValidationTest : FunSpec() {
init {
FormatValidationTestSuite(
format = "relative-json-pointer",
validTestCases =
listOf(
"0",
"1",
"105",
"0#",
"105#",
"0/test",
"105/test",
"0/0",
),
invalidTestCases =
listOf(
TestCase("", "empty RJP is not valid"),
TestCase("01", "leading zeroes are not allowed"),
TestCase("0##", "ref is the last character"),
TestCase("0#/test", "ref and JSON pointer are not allowed"),
TestCase("/test", "JSON pointer is not a valid RJP"),
TestCase("test", "invalid Json Pointer"),
),
).run { testFormat() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class JsonSchemaTimeFormatValidationTest : FunSpec() {
"12:42:45.00000001Z",
"12:42:45.000000001Z",
"12:42:45+04:00",
"12:42:45+04:00",
"12:42:45+23:59",
"12:42:45.000000001+02:42",
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ internal val COMMON_FORMAT_FILTER =
"ipv6" to emptySet(),
"iri" to emptySet(),
"iri-reference" to emptySet(),
"json-pointer" to emptySet(),
"regex" to emptySet(),
"relative-json-pointer" to emptySet(),
"uri" to emptySet(),
"uri-reference" to emptySet(),
"uri-template" to emptySet(),
Expand Down

0 comments on commit b53d8fd

Please sign in to comment.