Skip to content

Commit 5a35a24

Browse files
committed
Migrate to output collectors
1 parent 5a2ae7a commit 5a35a24

File tree

54 files changed

+874
-414
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+874
-414
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ kotlin {
179179

180180
dependencies {
181181
api(libs.kotlin.serialization.json)
182-
implementation(libs.uri)
182+
api(libs.uri)
183183
// When using approach like above you won't be able to add because block
184184
implementation(libs.kotlin.codepoints.get().toString()) {
185185
because("simplifies work with unicode codepoints")

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.optimumcode.json.schema
22

33
import io.github.optimumcode.json.pointer.JsonPointer
4+
import io.github.optimumcode.json.schema.OutputCollector.DelegateOutputCollector
45
import io.github.optimumcode.json.schema.internal.DefaultAssertionContext
56
import io.github.optimumcode.json.schema.internal.DefaultReferenceResolver
67
import io.github.optimumcode.json.schema.internal.IsolatedLoader
@@ -29,7 +30,9 @@ public class JsonSchema internal constructor(
2930
errorCollector: ErrorCollector,
3031
): Boolean {
3132
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolver)
32-
return assertion.validate(value, context, errorCollector)
33+
return DelegateOutputCollector(errorCollector).use {
34+
assertion.validate(value, context, this)
35+
}
3336
}
3437

3538
public companion object {
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
package io.github.optimumcode.json.schema
2+
3+
import io.github.optimumcode.json.pointer.JsonPointer
4+
import io.github.optimumcode.json.schema.ValidationOutput.Detailed
5+
import io.github.optimumcode.json.schema.ValidationOutput.Verbose
6+
7+
internal typealias OutputErrorTransformer<T> = OutputCollector<T>.(ValidationError) -> ValidationError?
8+
9+
public sealed class OutputCollector<T> private constructor(
10+
protected open val parent: OutputCollector<T>? = null,
11+
protected val transformer: OutputErrorTransformer<T> = { it },
12+
) : ErrorCollector {
13+
public abstract val output: T
14+
15+
internal abstract fun updateLocation(path: JsonPointer): OutputCollector<T>
16+
17+
internal abstract fun updateKeywordLocation(path: JsonPointer): OutputCollector<T>
18+
19+
internal abstract fun withErrorTransformer(transformer: OutputErrorTransformer<T>): OutputCollector<T>
20+
21+
internal abstract fun childCollector(): OutputCollector<T>
22+
23+
internal open fun reportErrors() = Unit
24+
25+
internal inline fun <OUT> use(block: OutputCollector<T>.() -> OUT): OUT =
26+
try {
27+
block(this)
28+
} finally {
29+
reportErrors()
30+
}
31+
32+
internal fun transformError(error: ValidationError): ValidationError? {
33+
return transformer(error)?.let {
34+
parent.let { p ->
35+
if (p == null) {
36+
it
37+
} else {
38+
p.transformError(it)
39+
}
40+
}
41+
}
42+
}
43+
44+
internal data object Empty : OutputCollector<Nothing>() {
45+
override val output: Nothing
46+
get() = throw UnsupportedOperationException("no output in empty collector")
47+
48+
override fun updateLocation(path: JsonPointer): OutputCollector<Nothing> = this
49+
50+
override fun updateKeywordLocation(path: JsonPointer): OutputCollector<Nothing> = this
51+
52+
override fun withErrorTransformer(transformer: OutputErrorTransformer<Nothing>): OutputCollector<Nothing> = this
53+
54+
override fun childCollector(): OutputCollector<Nothing> = this
55+
56+
override fun onError(error: ValidationError) = Unit
57+
}
58+
59+
internal class DelegateOutputCollector(
60+
private val errorCollector: ErrorCollector,
61+
override val parent: DelegateOutputCollector? = null,
62+
transformer: OutputErrorTransformer<Nothing> = { it },
63+
) : OutputCollector<Nothing>(parent, transformer) {
64+
private val reportedErrors = mutableListOf<ValidationError>()
65+
66+
override fun onError(error: ValidationError) {
67+
transformError(error)?.also(reportedErrors::add)
68+
}
69+
70+
override val output: Nothing
71+
get() = throw UnsupportedOperationException("no output in delegate collector")
72+
73+
override fun updateLocation(path: JsonPointer): OutputCollector<Nothing> =
74+
DelegateOutputCollector(errorCollector, this)
75+
76+
override fun updateKeywordLocation(path: JsonPointer): OutputCollector<Nothing> =
77+
DelegateOutputCollector(errorCollector, this)
78+
79+
override fun withErrorTransformer(transformer: OutputErrorTransformer<Nothing>): OutputCollector<Nothing> {
80+
return DelegateOutputCollector(errorCollector, parent, transformer)
81+
}
82+
83+
override fun reportErrors() {
84+
parent?.also { it.reportedErrors.addAll(reportedErrors) }
85+
?: reportedErrors.forEach(errorCollector::onError)
86+
}
87+
88+
override fun childCollector(): OutputCollector<Nothing> = DelegateOutputCollector(errorCollector, this, transformer)
89+
}
90+
91+
public class Flag private constructor(
92+
override val parent: Flag? = null,
93+
transformer: OutputErrorTransformer<ValidationOutput.Flag> = { it },
94+
) : OutputCollector<ValidationOutput.Flag>(parent, transformer) {
95+
private var valid: Boolean = true
96+
override val output: ValidationOutput.Flag
97+
get() =
98+
if (valid) {
99+
ValidationOutput.Flag.VALID
100+
} else {
101+
ValidationOutput.Flag.INVALID
102+
}
103+
104+
override fun updateKeywordLocation(path: JsonPointer): Flag = childCollector()
105+
106+
override fun updateLocation(path: JsonPointer): Flag = childCollector()
107+
108+
override fun withErrorTransformer(transformer: OutputErrorTransformer<ValidationOutput.Flag>): Flag =
109+
Flag(parent, transformer)
110+
111+
override fun reportErrors() {
112+
parent?.also {
113+
it.valid = it.valid && valid
114+
}
115+
}
116+
117+
override fun onError(error: ValidationError) {
118+
transformError(error) ?: return
119+
if (!valid) {
120+
return
121+
}
122+
valid = false
123+
}
124+
125+
override fun childCollector(): Flag = Flag(this)
126+
}
127+
128+
public class Detailed private constructor(
129+
private val location: JsonPointer = JsonPointer.ROOT,
130+
private val keywordLocation: JsonPointer = JsonPointer.ROOT,
131+
override val parent: Detailed? = null,
132+
transformer: OutputErrorTransformer<ValidationOutput.Detailed> = { it },
133+
) : OutputCollector<ValidationOutput.Detailed>(parent, transformer) {
134+
private val errors: MutableList<ValidationOutput.Detailed> = mutableListOf()
135+
136+
override val output: ValidationOutput.Detailed
137+
get() =
138+
if (errors.size == 1) {
139+
errors.single()
140+
} else {
141+
Detailed(
142+
valid = errors.any { !it.valid },
143+
keywordLocation = keywordLocation,
144+
absoluteKeywordLocation = null,
145+
instanceLocation = location,
146+
errors = errors.toList(),
147+
)
148+
}
149+
150+
override fun updateLocation(path: JsonPointer): Detailed =
151+
Detailed(
152+
location = path,
153+
keywordLocation = keywordLocation,
154+
parent = this,
155+
)
156+
157+
override fun updateKeywordLocation(path: JsonPointer): Detailed =
158+
Detailed(
159+
location = location,
160+
keywordLocation = path,
161+
parent = this,
162+
)
163+
164+
override fun childCollector(): OutputCollector<ValidationOutput.Detailed> =
165+
Detailed(location, keywordLocation, this)
166+
167+
override fun withErrorTransformer(
168+
transformer: OutputErrorTransformer<ValidationOutput.Detailed>,
169+
): OutputCollector<ValidationOutput.Detailed> = Detailed(location, keywordLocation, parent, transformer)
170+
171+
override fun reportErrors() {
172+
parent?.errors?.add(output)
173+
}
174+
175+
override fun onError(error: ValidationError) {
176+
val err = transformError(error) ?: return
177+
errors.add(
178+
Detailed(
179+
valid = false,
180+
instanceLocation = err.objectPath,
181+
keywordLocation = err.schemaPath,
182+
absoluteKeywordLocation = err.absoluteLocation,
183+
error = err.message,
184+
),
185+
)
186+
}
187+
}
188+
189+
public class Verbose private constructor(
190+
private val location: JsonPointer = JsonPointer.ROOT,
191+
private val keywordLocation: JsonPointer = JsonPointer.ROOT,
192+
override val parent: Verbose? = null,
193+
transformer: OutputErrorTransformer<ValidationOutput.Verbose> = { it },
194+
) : OutputCollector<ValidationOutput.Verbose>(parent, transformer) {
195+
private val errors: MutableList<ValidationOutput.Verbose> = mutableListOf()
196+
197+
override val output: ValidationOutput.Verbose
198+
get() =
199+
Verbose(
200+
valid = errors.any { !it.valid },
201+
keywordLocation = keywordLocation,
202+
absoluteKeywordLocation = null,
203+
instanceLocation = location,
204+
errors = errors.toList(),
205+
)
206+
207+
override fun updateLocation(path: JsonPointer): Verbose =
208+
Verbose(
209+
location = path,
210+
keywordLocation = keywordLocation,
211+
parent = this,
212+
)
213+
214+
override fun updateKeywordLocation(path: JsonPointer): Verbose =
215+
Verbose(
216+
location = location,
217+
keywordLocation = path,
218+
parent = this,
219+
)
220+
221+
override fun childCollector(): OutputCollector<ValidationOutput.Verbose> {
222+
return Verbose(location, keywordLocation, this)
223+
}
224+
225+
override fun withErrorTransformer(
226+
transformer: OutputErrorTransformer<ValidationOutput.Verbose>,
227+
): OutputCollector<ValidationOutput.Verbose> = Verbose(location, keywordLocation, parent, transformer)
228+
229+
override fun reportErrors() {
230+
parent?.errors?.add(output)
231+
}
232+
233+
override fun onError(error: ValidationError) {
234+
val err = transformError(error) ?: return
235+
errors.add(
236+
Verbose(
237+
valid = false,
238+
instanceLocation = err.objectPath,
239+
keywordLocation = err.schemaPath,
240+
absoluteKeywordLocation = err.absoluteLocation,
241+
error = err.message,
242+
),
243+
)
244+
}
245+
}
246+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.optimumcode.json.schema
22

3+
import com.eygraber.uri.Uri
34
import io.github.optimumcode.json.pointer.JsonPointer
45

56
/**
@@ -25,5 +26,5 @@ public data class ValidationError(
2526
/**
2627
* The absolute path to triggered assertion if the $ref was used
2728
*/
28-
val absoluteLocation: JsonPointer? = null,
29+
val absoluteLocation: Uri? = null,
2930
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.github.optimumcode.json.schema
2+
3+
import com.eygraber.uri.Uri
4+
import io.github.optimumcode.json.pointer.JsonPointer
5+
import kotlin.jvm.JvmField
6+
7+
public sealed class ValidationOutput private constructor(
8+
public val valid: Boolean,
9+
) {
10+
public class Flag(valid: Boolean) : ValidationOutput(valid) {
11+
public companion object {
12+
@JvmField
13+
public val VALID: Flag = Flag(true)
14+
15+
@JvmField
16+
public val INVALID: Flag = Flag(false)
17+
}
18+
}
19+
20+
public class Detailed(
21+
valid: Boolean,
22+
public val keywordLocation: JsonPointer,
23+
public val instanceLocation: JsonPointer,
24+
public val absoluteKeywordLocation: Uri? = null,
25+
public val error: String? = null,
26+
public val errors: List<Detailed> = emptyList(),
27+
) : ValidationOutput(valid)
28+
29+
public class Verbose(
30+
valid: Boolean,
31+
public val keywordLocation: JsonPointer,
32+
public val instanceLocation: JsonPointer,
33+
public val absoluteKeywordLocation: Uri? = null,
34+
public val error: String? = null,
35+
public val errors: List<Verbose> = emptyList(),
36+
public val annotations: List<Verbose> = emptyList(),
37+
) : ValidationOutput(valid)
38+
}

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/BooleanSchemaAssertion.kt

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package io.github.optimumcode.json.schema.internal
22

33
import io.github.optimumcode.json.pointer.JsonPointer
4-
import io.github.optimumcode.json.schema.ErrorCollector
4+
import io.github.optimumcode.json.schema.OutputCollector
55
import io.github.optimumcode.json.schema.ValidationError
66
import kotlinx.serialization.json.JsonElement
77

@@ -11,15 +11,17 @@ internal class FalseSchemaAssertion(
1111
override fun validate(
1212
element: JsonElement,
1313
context: AssertionContext,
14-
errorCollector: ErrorCollector,
14+
errorCollector: OutputCollector<*>,
1515
): Boolean {
16-
errorCollector.onError(
17-
ValidationError(
18-
schemaPath = path,
19-
objectPath = context.objectPath,
20-
message = "all values fail against the false schema",
21-
),
22-
)
16+
errorCollector.updateKeywordLocation(path).use {
17+
onError(
18+
ValidationError(
19+
schemaPath = path,
20+
objectPath = context.objectPath,
21+
message = "all values fail against the false schema",
22+
),
23+
)
24+
}
2325
return false
2426
}
2527
}
@@ -28,7 +30,7 @@ internal object TrueSchemaAssertion : JsonSchemaAssertion {
2830
override fun validate(
2931
element: JsonElement,
3032
context: AssertionContext,
31-
errorCollector: ErrorCollector,
33+
errorCollector: OutputCollector<*>,
3234
): Boolean {
3335
return true
3436
}

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/JsonSchemaAssertion.kt

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

33
import io.github.optimumcode.json.schema.ErrorCollector
4+
import io.github.optimumcode.json.schema.OutputCollector
45
import kotlinx.serialization.json.JsonElement
56

67
internal interface JsonSchemaAssertion {
@@ -18,6 +19,6 @@ internal interface JsonSchemaAssertion {
1819
fun validate(
1920
element: JsonElement,
2021
context: AssertionContext,
21-
errorCollector: ErrorCollector,
22+
errorCollector: OutputCollector<*>,
2223
): Boolean
2324
}

0 commit comments

Comments
 (0)