Skip to content
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

Each assertion should be loaded independently instead of grouping in one factory #22

Merged
merged 5 commits into from
Jul 31, 2023
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ repositories {
mavenCentral()
}

implementation("io.github.optimumcode:json-schema-validator:0.0.1")
implementation("io.github.optimumcode:json-schema-validator:0.0.2")
```

##### Groovy
Expand All @@ -45,7 +45,7 @@ repositories {
mavenCentral()
}

implementation 'io.github.optimumcode:json-schema-validator:0.0.1'
implementation 'io.github.optimumcode:json-schema-validator:0.0.2'
```

_Release are published to Sonatype repository. The synchronization with Maven Central takes time._
Expand Down Expand Up @@ -78,7 +78,7 @@ repositories {
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots")
}

implementation("io.github.optimumcode:json-schema-validator:0.0.1-SNAPSHOT")
implementation("io.github.optimumcode:json-schema-validator:0.0.3-SNAPSHOT")
```

##### Groovy
Expand All @@ -88,7 +88,7 @@ repositories {
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' }
}

implementation 'io.github.optimumcode:json-schema-validator:0.0.1-SNAPSHOT'
implementation 'io.github.optimumcode:json-schema-validator:0.0.3-SNAPSHOT'
```

### Example
Expand Down
1 change: 1 addition & 0 deletions changelog_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
{
"title": "## 🚀 Features",
"labels": ["enhancement"],
"exclude_labels": ["internal"],
"empty_content": "No new features today 😢"
},
{
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ kotlin.js.compiler=ir
org.gradle.jvmargs=-Xmx1G
org.gradle.java.installations.auto-download=false

version=0.0.2-SNAPSHOT
version=0.0.3-SNAPSHOT
group=io.github.optimumcode
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,70 @@ package io.github.optimumcode.json.schema.internal
import io.github.optimumcode.json.pointer.JsonPointer
import io.github.optimumcode.json.pointer.div
import io.github.optimumcode.json.pointer.get
import kotlin.jvm.JvmStatic
import kotlin.reflect.KClass
import kotlin.reflect.cast

internal interface AssertionContext {
val objectPath: JsonPointer

fun <T : Any> annotate(key: AnnotationKey<T>, value: T)
fun <T : Any> annotated(key: AnnotationKey<T>): T?
fun at(index: Int): AssertionContext
fun at(property: String): AssertionContext
fun resolveRef(refId: RefId): Pair<JsonPointer, JsonSchemaAssertion>

fun resetAnnotations()
}

internal class AnnotationKey<T : Any> private constructor(
private val name: String,
internal val type: KClass<T>,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as AnnotationKey<*>

if (name != other.name) return false
if (type != other.type) return false

return true
}

override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + type.hashCode()
return result
}

override fun toString(): String = "$name(${type.simpleName})"

companion object {
@JvmStatic
inline fun <reified T : Any> create(name: String): AnnotationKey<T> = create(name, T::class)

@JvmStatic
fun <T : Any> create(name: String, type: KClass<T>): AnnotationKey<T> = AnnotationKey(name, type)
}
}

internal data class DefaultAssertionContext(
override val objectPath: JsonPointer,
private val references: Map<RefId, AssertionWithPath>,
) : AssertionContext {
private lateinit var _annotations: MutableMap<AnnotationKey<*>, Any>
override fun <T : Any> annotate(key: AnnotationKey<T>, value: T) {
annotations()[key] = value
}

override fun <T : Any> annotated(key: AnnotationKey<T>): T? {
if (!::_annotations.isInitialized) {
return null
}
return _annotations[key]?.let { key.type.cast(it) }
}

override fun at(index: Int): AssertionContext = copy(objectPath = objectPath[index])

override fun at(property: String): AssertionContext {
Expand All @@ -26,4 +77,15 @@ internal data class DefaultAssertionContext(
val resolvedRef = requireNotNull(references[refId]) { "$refId is not found" }
return resolvedRef.schemaPath to resolvedRef.assertion
}

override fun resetAnnotations() {
annotations().clear()
}

private fun annotations(): MutableMap<AnnotationKey<*>, Any> {
if (!::_annotations.isInitialized) {
_annotations = hashMapOf()
}
return _annotations
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal class AssertionsCollection(
val valid = it.validate(element, context, errorCollector)
result = result and valid
}
context.resetAnnotations()
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ import io.github.optimumcode.json.pointer.get
import io.github.optimumcode.json.pointer.relative
import io.github.optimumcode.json.schema.JsonSchema
import io.github.optimumcode.json.schema.internal.ReferenceValidator.ReferenceLocation
import io.github.optimumcode.json.schema.internal.factories.FactoryGroup
import io.github.optimumcode.json.schema.internal.factories.array.ContainsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.ItemsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.MaxItemsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.MinItemsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.UniqueItemsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.AllOfAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.AnyOfAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.IfThenElseAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.ElseAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.IfAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.NotAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.OneOfAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.ThenAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.general.ConstAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.general.EnumAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.general.TypeAssertionFactory
Expand All @@ -26,9 +29,11 @@ import io.github.optimumcode.json.schema.internal.factories.number.ExclusiveMini
import io.github.optimumcode.json.schema.internal.factories.number.MaximumAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.number.MinimumAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.number.MultipleOfAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.AdditionalPropertiesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.DependenciesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.MaxPropertiesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.MinPropertiesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.PatternPropertiesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.PropertiesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.PropertyNamesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.RequiredAssertionFactory
Expand Down Expand Up @@ -61,10 +66,18 @@ private val factories: List<AssertionFactory> = listOf(
MaxPropertiesAssertionFactory,
MinPropertiesAssertionFactory,
RequiredAssertionFactory,
PropertiesAssertionFactory,
FactoryGroup(
PropertiesAssertionFactory,
PatternPropertiesAssertionFactory,
AdditionalPropertiesAssertionFactory,
),
PropertyNamesAssertionFactory,
DependenciesAssertionFactory,
IfThenElseAssertionFactory,
FactoryGroup(
IfAssertionFactory,
ThenAssertionFactory,
ElseAssertionFactory,
),
AllOfAssertionFactory,
AnyOfAssertionFactory,
OneOfAssertionFactory,
Expand Down Expand Up @@ -300,6 +313,7 @@ private data class DefaultLoadingContext(
private fun Set<IdWithLocation>.resolvePath(path: String?): Uri {
return last().id.appendPathToParent(requireNotNull(path) { "path is null" })
}

private fun Uri.appendPathToParent(path: String): Uri {
val hasLastEmptySegment = toString().endsWith('/')
return if (hasLastEmptySegment) {
Expand All @@ -315,6 +329,7 @@ private fun Uri.appendPathToParent(path: String): Uri {
}.appendEncodedPath(path)
.build()
}

private fun Uri.buildRefId(): RefId = RefId(this)

private fun Builder.buildRefId(): RefId = build().buildRefId()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.github.optimumcode.json.schema.internal.factories

import io.github.optimumcode.json.schema.ErrorCollector
import io.github.optimumcode.json.schema.internal.AssertionContext
import io.github.optimumcode.json.schema.internal.AssertionFactory
import io.github.optimumcode.json.schema.internal.JsonSchemaAssertion
import io.github.optimumcode.json.schema.internal.LoadingContext
import kotlinx.serialization.json.JsonElement

/**
* This class allows to create a group assertion that guarantees the order of assertion execution
* (the same order as [group])
*/
internal class AssertionsGroupFactory(
private val group: List<AssertionFactory>,
) : AssertionFactory {
init {
require(group.isNotEmpty()) { "at least one assertion must be in group" }
}

override fun isApplicable(element: JsonElement): Boolean {
return group.any { it.isApplicable(element) }
}

override fun create(element: JsonElement, context: LoadingContext): JsonSchemaAssertion {
return GroupAssertion(
assertions = group.asSequence()
.filter { it.isApplicable(element) }
.map { it.create(element, context) }
.toList(),
)
}
}

@Suppress("FunctionName")
internal fun FactoryGroup(vararg factories: AssertionFactory): AssertionFactory =
AssertionsGroupFactory(factories.toList())

private class GroupAssertion(
private val assertions: List<JsonSchemaAssertion>,
) : JsonSchemaAssertion {
override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean {
var result = true
assertions.forEach {
val valid = it.validate(element, context, errorCollector)
result = result && valid
}
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.github.optimumcode.json.schema.internal.factories.condition

import io.github.optimumcode.json.schema.ErrorCollector
import io.github.optimumcode.json.schema.internal.AssertionContext
import io.github.optimumcode.json.schema.internal.JsonSchemaAssertion
import io.github.optimumcode.json.schema.internal.LoadingContext
import io.github.optimumcode.json.schema.internal.factories.AbstractAssertionFactory
import kotlinx.serialization.json.JsonElement

internal object ElseAssertionFactory : AbstractAssertionFactory("else") {
override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion {
require(context.isJsonSchema(element)) { "$property must be a valid JSON schema" }
val elseAssertion = context.schemaFrom(element)
return ElseAssertion(elseAssertion)
}
}

private class ElseAssertion(
private val assertion: JsonSchemaAssertion,
) : JsonSchemaAssertion {
override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean {
return if (context.annotated(IfAssertionFactory.ANNOTATION) == false) {
assertion.validate(element, context, errorCollector)
} else {
true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.optimumcode.json.schema.internal.factories.condition

import io.github.optimumcode.json.schema.ErrorCollector
import io.github.optimumcode.json.schema.internal.AnnotationKey
import io.github.optimumcode.json.schema.internal.AssertionContext
import io.github.optimumcode.json.schema.internal.JsonSchemaAssertion
import io.github.optimumcode.json.schema.internal.LoadingContext
import io.github.optimumcode.json.schema.internal.factories.AbstractAssertionFactory
import kotlinx.serialization.json.JsonElement

internal object IfAssertionFactory : AbstractAssertionFactory("if") {
val ANNOTATION: AnnotationKey<Boolean> = AnnotationKey.create(property)

override fun createFromProperty(element: JsonElement, context: LoadingContext): JsonSchemaAssertion {
require(context.isJsonSchema(element)) { "$property must be a valid JSON schema" }
val ifAssertion = context.schemaFrom(element)
return IfAssertion(ifAssertion)
}
}

private class IfAssertion(
private val condition: JsonSchemaAssertion,
) : JsonSchemaAssertion {
override fun validate(element: JsonElement, context: AssertionContext, errorCollector: ErrorCollector): Boolean {
context.annotate(
IfAssertionFactory.ANNOTATION,
condition.validate(element, context, ErrorCollector.EMPTY),
)
return true
}
}
Loading