Skip to content

Commit 2547e3f

Browse files
committed
Fix reporting behavior for anyOf and oneOf
1 parent 1b02d70 commit 2547e3f

File tree

5 files changed

+188
-45
lines changed

5 files changed

+188
-45
lines changed

src/commonMain/kotlin/io/github/optimumcode/json/schema/OutputCollector.kt

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public sealed class OutputCollector<T> private constructor(
7575
internal abstract fun withErrorTransformer(transformer: OutputErrorTransformer<T>): OutputCollector<T>
7676

7777
/**
78-
* Creates a child [OutputCollector] that has exactly same instance, keyword and absolute locations information.
78+
* Creates a child [OutputCollector] that has exactly same instance, keyword and absolute locations' information.
7979
*/
8080
internal abstract fun childCollector(): OutputCollector<T>
8181

@@ -293,6 +293,7 @@ public sealed class OutputCollector<T> private constructor(
293293
private val parent: Detailed? = null,
294294
private val absoluteLocation: AbsoluteLocation? = null,
295295
private val collapse: Boolean = true,
296+
private val child: Boolean = false,
296297
transformer: OutputErrorTransformer<OutputUnit> = NO_TRANSFORMATION,
297298
) : OutputCollector<OutputUnit>(parent, transformer) {
298299
private lateinit var results: MutableSet<OutputUnit>
@@ -308,6 +309,17 @@ public sealed class OutputCollector<T> private constructor(
308309
results.add(result)
309310
}
310311

312+
private fun addResults(results: MutableSet<OutputUnit>) {
313+
if (results.all { it.valid }) {
314+
return
315+
}
316+
if (::results.isInitialized) {
317+
results.addAll(results)
318+
} else {
319+
this.results = results
320+
}
321+
}
322+
311323
override val output: OutputUnit
312324
get() {
313325
if (!::results.isInitialized) {
@@ -366,13 +378,22 @@ public sealed class OutputCollector<T> private constructor(
366378
}
367379

368380
override fun childCollector(): OutputCollector<OutputUnit> =
369-
Detailed(location, keywordLocation, this, absoluteLocation)
381+
Detailed(location, keywordLocation, this, absoluteLocation, child = true)
370382

371383
override fun withErrorTransformer(transformer: OutputErrorTransformer<OutputUnit>): OutputCollector<OutputUnit> =
372384
Detailed(location, keywordLocation, parent, absoluteLocation, collapse, transformer = transformer)
373385

374386
override fun reportErrors() {
375-
parent?.addResult(output)
387+
if (parent == null) {
388+
return
389+
}
390+
if (child) {
391+
if (::results.isInitialized) {
392+
parent.addResults(results)
393+
}
394+
} else {
395+
parent.addResult(output)
396+
}
376397
}
377398

378399
override fun onError(error: ValidationError) {
@@ -394,6 +415,7 @@ public sealed class OutputCollector<T> private constructor(
394415
private val keywordLocation: JsonPointer = JsonPointer.ROOT,
395416
private val parent: Verbose? = null,
396417
private val absoluteLocation: AbsoluteLocation? = null,
418+
private val child: Boolean = false,
397419
transformer: OutputErrorTransformer<OutputUnit> = NO_TRANSFORMATION,
398420
) : OutputCollector<OutputUnit>(parent, transformer) {
399421
private val errors: MutableList<OutputUnit> = ArrayList(1)
@@ -404,6 +426,10 @@ public sealed class OutputCollector<T> private constructor(
404426
errors.add(result)
405427
}
406428

429+
private fun addResults(results: MutableList<OutputUnit>) {
430+
errors.addAll(results)
431+
}
432+
407433
override val output: OutputUnit
408434
get() {
409435
if (errors.size == 1) {
@@ -460,14 +486,21 @@ public sealed class OutputCollector<T> private constructor(
460486
}
461487

462488
override fun childCollector(): OutputCollector<OutputUnit> {
463-
return Verbose(location, keywordLocation, this, absoluteLocation)
489+
return Verbose(location, keywordLocation, this, absoluteLocation, child = true)
464490
}
465491

466492
override fun withErrorTransformer(transformer: OutputErrorTransformer<OutputUnit>): OutputCollector<OutputUnit> =
467-
Verbose(location, keywordLocation, parent, absoluteLocation, transformer)
493+
Verbose(location, keywordLocation, parent, absoluteLocation, transformer = transformer)
468494

469495
override fun reportErrors() {
470-
parent?.addResult(output)
496+
if (parent == null) {
497+
return
498+
}
499+
if (child) {
500+
parent.addResults(errors)
501+
} else {
502+
parent.addResult(output)
503+
}
471504
}
472505

473506
override fun onError(error: ValidationError) {

src/commonMain/kotlin/io/github/optimumcode/json/schema/ValidationOutput.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.github.optimumcode.json.schema
22

33
import io.github.optimumcode.json.pointer.JsonPointer
44
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.Transient
56
import kotlin.jvm.JvmField
67

78
public sealed class ValidationOutput private constructor() {
@@ -35,6 +36,7 @@ public sealed class ValidationOutput private constructor() {
3536
public val annotations: Set<OutputUnit> = emptySet(),
3637
) : ValidationOutput() {
3738
// hashcode is stored to avoid recursive recalculation for each error in `errors` property
39+
@Transient
3840
private var hash = 0
3941

4042
override fun equals(other: Any?): Boolean {

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/AnyOfAssertionFactory.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ private class AnyOfAssertion(
3535
val res = it.validate(element, childContext, collector)
3636
if (res) {
3737
childContext.propagateToParent()
38+
collector.reportErrors()
3839
}
3940
valid = valid or res
4041
}

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/condition/OneOfAssertionFactory.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ private class OneOfAssertion(
3434
val res = assertion.validate(element, childContext, childCollector)
3535
if (res) {
3636
childContext.propagateToParent()
37+
childCollector.reportErrors()
3738
matched += index
3839
}
3940
}

src/commonTest/kotlin/io/github/optimumcode/json/schema/base/OutputCollectorsTest.kt

Lines changed: 145 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -620,49 +620,155 @@ class OutputCollectorsTest : FunSpec() {
620620
),
621621
)
622622
//endregion
623-
compare(expected, res)
624623
res shouldBe expected
625624
}
626625
}
627-
}
628626

629-
private fun compare(
630-
expected: OutputUnit,
631-
actual: OutputUnit,
632-
): Boolean {
633-
if (expected.valid != actual.valid) {
634-
println("diff valid flag: $expected and $actual")
635-
return false
636-
}
637-
if (expected.keywordLocation != actual.keywordLocation) {
638-
println("diff keyword location: $expected and $actual")
639-
return false
640-
}
641-
if (expected.instanceLocation != actual.instanceLocation) {
642-
println("diff instance location: $expected and $actual")
643-
return false
644-
}
645-
if (expected.absoluteKeywordLocation != actual.absoluteKeywordLocation) {
646-
println("diff absolute keyword location: $expected and $actual")
647-
return false
648-
}
649-
if (expected.errors.isEmpty() xor actual.errors.isEmpty()) {
650-
println("diff number of errors: $expected and $actual")
651-
return false
652-
}
653-
var match = true
654-
expected.errors.forEach { expectedErr ->
655-
val actualErr =
656-
actual.errors.find {
657-
it.keywordLocation == expectedErr.keywordLocation &&
658-
it.instanceLocation == expectedErr.instanceLocation
659-
} ?: run {
660-
println("no match for $expectedErr")
661-
match = false
662-
return@forEach
663-
}
664-
match = compare(expectedErr, actualErr)
627+
JsonSchema.fromDefinition(
628+
"""
629+
{
630+
"oneOf": [
631+
{
632+
"type": "string"
633+
},
634+
{
635+
"type": "object"
636+
}
637+
]
638+
}
639+
""".trimIndent(),
640+
).also { schema ->
641+
test("verbose output with oneOf for string") {
642+
val res =
643+
schema.validate(
644+
JsonPrimitive("test"),
645+
OutputCollector.verbose(),
646+
)
647+
648+
res shouldBe
649+
OutputUnit(
650+
valid = true,
651+
keywordLocation = JsonPointer.ROOT,
652+
instanceLocation = JsonPointer.ROOT,
653+
errors =
654+
setOf(
655+
OutputUnit(
656+
valid = true,
657+
keywordLocation = JsonPointer("/oneOf"),
658+
instanceLocation = JsonPointer.ROOT,
659+
errors =
660+
setOf(
661+
OutputUnit(
662+
valid = true,
663+
keywordLocation = JsonPointer("/oneOf/0"),
664+
instanceLocation = JsonPointer.ROOT,
665+
errors =
666+
setOf(
667+
OutputUnit(
668+
valid = true,
669+
keywordLocation = JsonPointer("/oneOf/0/type"),
670+
instanceLocation = JsonPointer.ROOT,
671+
),
672+
),
673+
),
674+
),
675+
),
676+
),
677+
)
678+
}
679+
680+
test("verbose output with oneOf for object") {
681+
val res =
682+
schema.validate(
683+
buildJsonObject { },
684+
OutputCollector.verbose(),
685+
)
686+
687+
res shouldBe
688+
OutputUnit(
689+
valid = true,
690+
keywordLocation = JsonPointer.ROOT,
691+
instanceLocation = JsonPointer.ROOT,
692+
errors =
693+
setOf(
694+
OutputUnit(
695+
valid = true,
696+
keywordLocation = JsonPointer("/oneOf"),
697+
instanceLocation = JsonPointer.ROOT,
698+
errors =
699+
setOf(
700+
OutputUnit(
701+
valid = true,
702+
keywordLocation = JsonPointer("/oneOf/1"),
703+
instanceLocation = JsonPointer.ROOT,
704+
errors =
705+
setOf(
706+
OutputUnit(
707+
valid = true,
708+
keywordLocation = JsonPointer("/oneOf/1/type"),
709+
instanceLocation = JsonPointer.ROOT,
710+
),
711+
),
712+
),
713+
),
714+
),
715+
),
716+
)
717+
}
718+
719+
test("verbose output with oneOf for invalid") {
720+
val res =
721+
schema.validate(
722+
JsonPrimitive(42),
723+
OutputCollector.verbose(),
724+
)
725+
726+
res shouldBe
727+
OutputUnit(
728+
valid = false,
729+
keywordLocation = JsonPointer.ROOT,
730+
instanceLocation = JsonPointer.ROOT,
731+
errors =
732+
setOf(
733+
OutputUnit(
734+
valid = false,
735+
keywordLocation = JsonPointer("/oneOf"),
736+
instanceLocation = JsonPointer.ROOT,
737+
errors =
738+
setOf(
739+
OutputUnit(
740+
valid = false,
741+
keywordLocation = JsonPointer("/oneOf/0"),
742+
instanceLocation = JsonPointer.ROOT,
743+
errors =
744+
setOf(
745+
OutputUnit(
746+
valid = false,
747+
keywordLocation = JsonPointer("/oneOf/0/type"),
748+
instanceLocation = JsonPointer.ROOT,
749+
error = "element is not a string",
750+
),
751+
),
752+
),
753+
OutputUnit(
754+
valid = false,
755+
keywordLocation = JsonPointer("/oneOf/1"),
756+
instanceLocation = JsonPointer.ROOT,
757+
errors =
758+
setOf(
759+
OutputUnit(
760+
valid = false,
761+
keywordLocation = JsonPointer("/oneOf/1/type"),
762+
instanceLocation = JsonPointer.ROOT,
763+
error = "element is not a object",
764+
),
765+
),
766+
),
767+
),
768+
),
769+
),
770+
)
771+
}
665772
}
666-
return match
667773
}
668774
}

0 commit comments

Comments
 (0)