Skip to content

Commit 3b57aef

Browse files
committed
Add methods to use output collectors. Make sure keyword location is updated even if no errors reported
1 parent 2b3ff59 commit 3b57aef

37 files changed

+595
-422
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ public class JsonSchema internal constructor(
3535
}
3636
}
3737

38+
public fun <T> validate(
39+
value: JsonElement,
40+
outputCollector: OutputCollector<T>,
41+
): T {
42+
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolver)
43+
outputCollector.use {
44+
assertion.validate(value, context, this)
45+
}
46+
return outputCollector.output
47+
}
48+
3849
public companion object {
3950
/**
4051
* Loads JSON schema from the [schema] definition

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

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

33
import io.github.optimumcode.json.pointer.JsonPointer
4+
import io.github.optimumcode.json.pointer.plus
5+
import io.github.optimumcode.json.pointer.relative
6+
import io.github.optimumcode.json.schema.ValidationOutput.BasicError
47
import io.github.optimumcode.json.schema.ValidationOutput.Detailed
58
import io.github.optimumcode.json.schema.ValidationOutput.Verbose
9+
import kotlin.jvm.JvmStatic
610

711
internal typealias OutputErrorTransformer<T> = OutputCollector<T>.(ValidationError) -> ValidationError?
812

@@ -12,6 +16,20 @@ public sealed class OutputCollector<T> private constructor(
1216
parent: OutputCollector<T>? = null,
1317
transformer: OutputErrorTransformer<T> = NO_TRANSFORMATION,
1418
) : ErrorCollector {
19+
public companion object {
20+
@JvmStatic
21+
public fun flag(): Flag = Flag()
22+
23+
@JvmStatic
24+
public fun basic(): Basic = Basic()
25+
26+
@JvmStatic
27+
public fun detailed(): Detailed = Detailed()
28+
29+
@JvmStatic
30+
public fun verbose(): Verbose = Verbose()
31+
}
32+
1533
public abstract val output: T
1634
private val transformerFunc: OutputErrorTransformer<T> =
1735
parent?.let { p ->
@@ -32,7 +50,11 @@ public sealed class OutputCollector<T> private constructor(
3250

3351
internal abstract fun updateLocation(path: JsonPointer): OutputCollector<T>
3452

35-
internal abstract fun updateKeywordLocation(path: JsonPointer): OutputCollector<T>
53+
internal abstract fun updateKeywordLocation(
54+
path: JsonPointer,
55+
absoluteLocation: AbsoluteLocation? = null,
56+
canCollapse: Boolean = true,
57+
): OutputCollector<T>
3658

3759
internal abstract fun withErrorTransformer(transformer: OutputErrorTransformer<T>): OutputCollector<T>
3860

@@ -55,7 +77,11 @@ public sealed class OutputCollector<T> private constructor(
5577

5678
override fun updateLocation(path: JsonPointer): OutputCollector<Nothing> = this
5779

58-
override fun updateKeywordLocation(path: JsonPointer): OutputCollector<Nothing> = this
80+
override fun updateKeywordLocation(
81+
path: JsonPointer,
82+
absoluteLocation: AbsoluteLocation?,
83+
canCollapse: Boolean,
84+
): OutputCollector<Nothing> = this
5985

6086
override fun withErrorTransformer(transformer: OutputErrorTransformer<Nothing>): OutputCollector<Nothing> = this
6187

@@ -81,26 +107,33 @@ public sealed class OutputCollector<T> private constructor(
81107
override fun updateLocation(path: JsonPointer): OutputCollector<Nothing> =
82108
DelegateOutputCollector(errorCollector, this)
83109

84-
override fun updateKeywordLocation(path: JsonPointer): OutputCollector<Nothing> =
85-
DelegateOutputCollector(errorCollector, this)
110+
override fun updateKeywordLocation(
111+
path: JsonPointer,
112+
absoluteLocation: AbsoluteLocation?,
113+
canCollapse: Boolean,
114+
): OutputCollector<Nothing> = DelegateOutputCollector(errorCollector, this)
86115

87116
override fun withErrorTransformer(transformer: OutputErrorTransformer<Nothing>): OutputCollector<Nothing> {
88117
return DelegateOutputCollector(errorCollector, parent, transformer)
89118
}
90119

91120
override fun reportErrors() {
121+
if (reportedErrors.isEmpty()) {
122+
return
123+
}
92124
parent?.also { it.reportedErrors.addAll(reportedErrors) }
93125
?: reportedErrors.forEach(errorCollector::onError)
94126
}
95127

96128
override fun childCollector(): OutputCollector<Nothing> = DelegateOutputCollector(errorCollector, this)
97129
}
98130

99-
public class Flag private constructor(
131+
public class Flag internal constructor(
100132
private val parent: Flag? = null,
101133
transformer: OutputErrorTransformer<ValidationOutput.Flag> = NO_TRANSFORMATION,
102134
) : OutputCollector<ValidationOutput.Flag>(parent, transformer) {
103135
private var valid: Boolean = true
136+
private var hasErrors: Boolean = false
104137
override val output: ValidationOutput.Flag
105138
get() =
106139
if (valid) {
@@ -109,72 +142,152 @@ public sealed class OutputCollector<T> private constructor(
109142
ValidationOutput.Flag.INVALID
110143
}
111144

112-
override fun updateKeywordLocation(path: JsonPointer): Flag = childCollector()
145+
override fun updateKeywordLocation(
146+
path: JsonPointer,
147+
absoluteLocation: AbsoluteLocation?,
148+
canCollapse: Boolean,
149+
): Flag = childCollector()
113150

114151
override fun updateLocation(path: JsonPointer): Flag = childCollector()
115152

116153
override fun withErrorTransformer(transformer: OutputErrorTransformer<ValidationOutput.Flag>): Flag =
117154
Flag(parent, transformer)
118155

119156
override fun reportErrors() {
157+
valid = valid && !hasErrors
120158
parent?.also {
121159
it.valid = it.valid && valid
122160
}
123161
}
124162

125163
override fun onError(error: ValidationError) {
126164
transformError(error) ?: return
127-
if (!valid) {
165+
if (hasErrors) {
128166
return
129167
}
130-
valid = false
168+
hasErrors = true
131169
}
132170

133171
override fun childCollector(): Flag = Flag(this)
134172
}
135173

136-
public class Detailed private constructor(
174+
public class Basic internal constructor(
175+
private val parent: Basic? = null,
176+
transformer: OutputErrorTransformer<ValidationOutput.Basic> = NO_TRANSFORMATION,
177+
) : OutputCollector<ValidationOutput.Basic>(parent, transformer) {
178+
private val errors = mutableListOf<BasicError>()
179+
180+
override fun onError(error: ValidationError) {
181+
val err = transformError(error) ?: return
182+
errors +=
183+
BasicError(
184+
keywordLocation = err.schemaPath,
185+
instanceLocation = err.objectPath,
186+
absoluteKeywordLocation = err.absoluteLocation,
187+
error = err.message,
188+
)
189+
}
190+
191+
override val output: ValidationOutput.Basic
192+
get() =
193+
ValidationOutput.Basic(
194+
valid = errors.isEmpty(),
195+
errors = errors.toSet(),
196+
)
197+
198+
override fun updateLocation(path: JsonPointer): OutputCollector<ValidationOutput.Basic> = childCollector()
199+
200+
override fun updateKeywordLocation(
201+
path: JsonPointer,
202+
absoluteLocation: AbsoluteLocation?,
203+
canCollapse: Boolean,
204+
): OutputCollector<ValidationOutput.Basic> = childCollector()
205+
206+
override fun withErrorTransformer(
207+
transformer: OutputErrorTransformer<ValidationOutput.Basic>,
208+
): OutputCollector<ValidationOutput.Basic> = Basic(parent, transformer)
209+
210+
override fun childCollector(): OutputCollector<ValidationOutput.Basic> = Basic(this)
211+
212+
override fun reportErrors() {
213+
parent?.errors?.addAll(errors)
214+
}
215+
}
216+
217+
public class Detailed internal constructor(
137218
private val location: JsonPointer = JsonPointer.ROOT,
138219
private val keywordLocation: JsonPointer = JsonPointer.ROOT,
139220
private val parent: Detailed? = null,
221+
private val absoluteLocation: AbsoluteLocation? = null,
222+
private val collapse: Boolean = true,
140223
transformer: OutputErrorTransformer<ValidationOutput.Detailed> = NO_TRANSFORMATION,
141224
) : OutputCollector<ValidationOutput.Detailed>(parent, transformer) {
142225
private val errors: MutableList<ValidationOutput.Detailed> = mutableListOf()
143226

144227
override val output: ValidationOutput.Detailed
145-
get() =
146-
if (errors.size == 1) {
147-
errors.single()
228+
get() {
229+
val valid = errors.none { !it.valid }
230+
if (valid) {
231+
return Detailed(
232+
valid = true,
233+
keywordLocation = keywordLocation,
234+
instanceLocation = location,
235+
absoluteKeywordLocation = absoluteLocation,
236+
errors = emptySet(),
237+
)
238+
}
239+
val failed = errors.filterTo(hashSetOf()) { it.error != null || it.errors.isNotEmpty() }
240+
return if (failed.size == 1 && collapse) {
241+
failed.single()
148242
} else {
149243
Detailed(
150-
valid = errors.any { !it.valid },
244+
valid = false,
151245
keywordLocation = keywordLocation,
152-
absoluteKeywordLocation = null,
246+
absoluteKeywordLocation = absoluteLocation,
153247
instanceLocation = location,
154-
errors = errors.toList(),
248+
errors = failed,
155249
)
156250
}
251+
}
157252

158253
override fun updateLocation(path: JsonPointer): Detailed =
159254
Detailed(
160255
location = path,
161256
keywordLocation = keywordLocation,
257+
absoluteLocation = absoluteLocation,
162258
parent = this,
163259
)
164260

165-
override fun updateKeywordLocation(path: JsonPointer): Detailed =
166-
Detailed(
261+
override fun updateKeywordLocation(
262+
path: JsonPointer,
263+
absoluteLocation: AbsoluteLocation?,
264+
canCollapse: Boolean,
265+
): Detailed {
266+
val newKeywordLocation =
267+
if (this.absoluteLocation == null) {
268+
path
269+
} else {
270+
this.keywordLocation + this.absoluteLocation.path.relative(path)
271+
}
272+
if (keywordLocation == newKeywordLocation) {
273+
return this
274+
}
275+
return Detailed(
167276
location = location,
168-
keywordLocation = path,
277+
keywordLocation = newKeywordLocation,
278+
absoluteLocation = absoluteLocation ?: this.absoluteLocation?.copy(path = path),
169279
parent = this,
280+
collapse = absoluteLocation == null && canCollapse,
170281
)
282+
}
171283

172284
override fun childCollector(): OutputCollector<ValidationOutput.Detailed> =
173-
Detailed(location, keywordLocation, this)
285+
Detailed(location, keywordLocation, this, absoluteLocation)
174286

175287
override fun withErrorTransformer(
176288
transformer: OutputErrorTransformer<ValidationOutput.Detailed>,
177-
): OutputCollector<ValidationOutput.Detailed> = Detailed(location, keywordLocation, parent, transformer)
289+
): OutputCollector<ValidationOutput.Detailed> =
290+
Detailed(location, keywordLocation, parent, absoluteLocation, collapse, transformer = transformer)
178291

179292
override fun reportErrors() {
180293
parent?.errors?.add(output)
@@ -194,45 +307,78 @@ public sealed class OutputCollector<T> private constructor(
194307
}
195308
}
196309

197-
public class Verbose private constructor(
310+
public class Verbose internal constructor(
198311
private val location: JsonPointer = JsonPointer.ROOT,
199312
private val keywordLocation: JsonPointer = JsonPointer.ROOT,
200313
private val parent: Verbose? = null,
314+
private val absoluteLocation: AbsoluteLocation? = null,
201315
transformer: OutputErrorTransformer<ValidationOutput.Verbose> = NO_TRANSFORMATION,
202316
) : OutputCollector<ValidationOutput.Verbose>(parent, transformer) {
203317
private val errors: MutableList<ValidationOutput.Verbose> = mutableListOf()
204318

205319
override val output: ValidationOutput.Verbose
206-
get() =
207-
Verbose(
208-
valid = errors.any { !it.valid },
320+
get() {
321+
if (errors.size == 1) {
322+
// when this is a leaf we should return the reported error
323+
// instead of creating a new node
324+
val childError = errors.single()
325+
if (
326+
childError.errors.isEmpty() &&
327+
childError.let {
328+
it.keywordLocation == keywordLocation && it.instanceLocation == it.instanceLocation
329+
}
330+
) {
331+
return childError
332+
}
333+
}
334+
return Verbose(
335+
valid = errors.none { !it.valid },
209336
keywordLocation = keywordLocation,
210-
absoluteKeywordLocation = null,
337+
absoluteKeywordLocation = absoluteLocation,
211338
instanceLocation = location,
212-
errors = errors.toList(),
339+
errors = errors.toSet(),
213340
)
341+
}
214342

215-
override fun updateLocation(path: JsonPointer): Verbose =
216-
Verbose(
343+
override fun updateLocation(path: JsonPointer): Verbose {
344+
return Verbose(
217345
location = path,
218346
keywordLocation = keywordLocation,
347+
absoluteLocation = absoluteLocation,
219348
parent = this,
220349
)
350+
}
221351

222-
override fun updateKeywordLocation(path: JsonPointer): Verbose =
223-
Verbose(
352+
override fun updateKeywordLocation(
353+
path: JsonPointer,
354+
absoluteLocation: AbsoluteLocation?,
355+
canCollapse: Boolean,
356+
): Verbose {
357+
val newKeywordLocation =
358+
if (this.absoluteLocation == null) {
359+
path
360+
} else {
361+
this.keywordLocation + this.absoluteLocation.path.relative(path)
362+
}
363+
if (keywordLocation == newKeywordLocation) {
364+
return this
365+
}
366+
return Verbose(
224367
location = location,
225-
keywordLocation = path,
368+
keywordLocation = newKeywordLocation,
369+
absoluteLocation = absoluteLocation ?: this.absoluteLocation?.copy(path = path),
226370
parent = this,
227371
)
372+
}
228373

229374
override fun childCollector(): OutputCollector<ValidationOutput.Verbose> {
230-
return Verbose(location, keywordLocation, this)
375+
return Verbose(location, keywordLocation, this, absoluteLocation)
231376
}
232377

233378
override fun withErrorTransformer(
234379
transformer: OutputErrorTransformer<ValidationOutput.Verbose>,
235-
): OutputCollector<ValidationOutput.Verbose> = Verbose(location, keywordLocation, parent, transformer)
380+
): OutputCollector<ValidationOutput.Verbose> =
381+
Verbose(location, keywordLocation, parent, absoluteLocation, transformer)
236382

237383
override fun reportErrors() {
238384
parent?.errors?.add(output)

0 commit comments

Comments
 (0)