
commonMain.io.kotest.assertions.json.schema.builder.kt Maven / Gradle / Ivy
@file:Suppress("unused") // We use unused receivers to scope DSL functions
package io.kotest.assertions.json.schema
import io.kotest.assertions.json.ContainsSpec
import io.kotest.assertions.json.JsonNode
import io.kotest.assertions.json.JsonNode.*
import io.kotest.common.ExperimentalKotest
import io.kotest.matchers.Matcher
import io.kotest.matchers.sequences.beUnique
import kotlinx.serialization.Serializable
@DslMarker
annotation class JsonSchemaMarker
@JsonSchemaMarker
@ExperimentalKotest
@Serializable(with = SchemaDeserializer::class)
sealed interface JsonSchemaElement {
fun typeName(): String
}
internal interface ValueNode {
val matcher: Matcher?
get() = null
}
@ExperimentalKotest
data class JsonSchema(
val root: JsonSchemaElement
) {
/**
* Provides a way to access pre-defined schemas in a conventional way (e.g. `type()`). Example:
* ```kotlin
* val addressSchema = jsonSchema {
* obj {
* withProperty("street") { string() }
* withProperty("zipCode") { integer { beEven() } }
* }
* }
*
* val personSchema = jsonSchema {
* obj {
* withProperty("name") { string() }
* withProperty("address") { addressSchema() }
* }
* }
* ```
*/
@ExperimentalKotest
operator fun invoke() = root
object Builder
internal interface JsonNumber
data class JsonArray(
val minItems: Int = 0,
val maxItems: Int = Int.MAX_VALUE,
val matcher: Matcher>? = null,
val contains: ContainsSpec? = null,
val elementType: JsonSchemaElement? = null,
) : JsonSchemaElement {
override fun typeName() = "array"
}
class JsonObjectBuilder {
var additionalProperties: Boolean = true
var minProperties: Int = 0
var maxProperties: Int? = null
var properties: MutableMap = mutableMapOf()
/**
* https://json-schema.org/understanding-json-schema/reference/object.html#required-properties
*/
var requiredProperties: MutableList = mutableListOf()
/**
* By default properties are optional.
* Using [required], you can specify that it must be included.
*/
fun withProperty(
name: String,
optional: Boolean = false,
elementBuilder: Builder.() -> JsonSchemaElement
) {
properties[name] = Builder.elementBuilder()
if (!optional) requiredProperties.add(name)
}
fun string(
name: String,
optional: Boolean = false,
matcherBuilder: () -> Matcher? = { null }
) = withProperty(name, optional) { string(matcherBuilder) }
fun integer(
name: String,
optional: Boolean = false,
matcherBuilder: () -> Matcher? = { null }
) = withProperty(name, optional) { integer(matcherBuilder) }
fun decimal(
name: String,
optional: Boolean = false,
matcherBuilder: () -> Matcher? = { null }
) = withProperty(name, optional) { decimal(matcherBuilder) }
fun number(
name: String,
optional: Boolean = false,
matcherBuilder: () -> Matcher? = { null }
) = withProperty(name, optional) { number(matcherBuilder) }
fun array(
name: String,
optional: Boolean = false,
typeBuilder: () -> JsonSchemaElement
) = withProperty(name, optional) { array(typeBuilder = typeBuilder) }
fun obj(
name: String,
optional: Boolean = false,
dsl: JsonSchema.JsonObjectBuilder.() -> Unit = {}
) = withProperty(name, optional) { obj(dsl) }
fun boolean(
name: String,
optional: Boolean = false,
) = withProperty(name, optional) { boolean() }
fun `null`(
name: String,
optional: Boolean = false,
) = withProperty(name, optional) { `null`() }
fun build() = JsonObject(
additionalProperties = additionalProperties,
minProperties = minProperties,
maxProperties = maxProperties,
properties = properties,
requiredProperties = requiredProperties
)
}
@Serializable
data class JsonObject(
/**
* Controls whether this node allows additional properties to be defined or not.
* By default, additional properties are _allowed_
*
* https://json-schema.org/understanding-json-schema/reference/object.html#additional-properties
*/
val additionalProperties: Boolean = true,
val minProperties: Int = 0,
val maxProperties: Int? = null,
val properties: Map,
/**
* https://json-schema.org/understanding-json-schema/reference/object.html#required-properties
*/
val requiredProperties: List = emptyList(),
) : JsonSchemaElement {
operator fun get(name: String) = properties.get(name)
override fun typeName() = "object"
}
data class JsonString(override val matcher: Matcher? = null) : JsonSchemaElement, ValueNode {
override fun typeName() = "string"
}
data class JsonInteger(
override val matcher: Matcher? = null
) : JsonSchemaElement, JsonNumber, ValueNode {
override fun typeName() = "integer"
}
data class JsonDecimal(
override val matcher: Matcher? = null
) : JsonSchemaElement, JsonNumber, ValueNode {
override fun typeName() = "number"
}
@Serializable
object JsonBoolean : JsonSchemaElement, ValueNode {
override fun typeName() = "boolean"
}
@Serializable
object Null : JsonSchemaElement {
override fun typeName() = "null"
}
}
/**
* Creates a [JsonSchema.JsonString] node, which is a leaf node that will hold a [String] value.
* Optionally, you can provide a [matcherBuilder] that constructs a [Matcher] which the node will be tested with.
*/
@ExperimentalKotest
fun JsonSchema.Builder.string(matcherBuilder: () -> Matcher? = { null }) =
JsonSchema.JsonString(matcherBuilder())
/**
* Creates a [JsonSchema.JsonInteger] node, which is a leaf node that will hold a [Long] value.
* Optionally, you can provide a [matcherBuilder] that constructs a [Matcher] which the node will be tested with.
*/
@ExperimentalKotest
fun JsonSchema.Builder.integer(matcherBuilder: () -> Matcher? = { null }) =
JsonSchema.JsonInteger(matcherBuilder())
/**
* Creates a [JsonSchema.JsonDecimal] node, which is a leaf node that will hold a [Double] value.
* Optionally, you can provide a [matcherBuilder] that constructs a [Matcher] which the node will be tested with.
*/
@ExperimentalKotest
fun JsonSchema.Builder.number(matcherBuilder: () -> Matcher? = { null }) =
JsonSchema.JsonDecimal(matcherBuilder())
/**
* Alias for [number]
*
* Creates a [JsonSchema.JsonDecimal] node, which is a leaf node that will hold a [Double] value.
* Optionally, you can provide a [matcherBuilder] that constructs a [Matcher] which the node will be tested with.
*/
@ExperimentalKotest
fun JsonSchema.Builder.decimal(matcherBuilder: () -> Matcher? = { null }) =
JsonSchema.JsonDecimal(matcherBuilder())
/**
* Creates a [JsonSchema.JsonBoolean] node, which is a leaf node that will hold a [Boolean] value.
* It supports no further configuration. The actual value must always be either true or false.
*/
@ExperimentalKotest
fun JsonSchema.Builder.boolean() = JsonSchema.JsonBoolean
/**
* Creates a [JsonSchema.Null] node, which is a leaf node that must always be null, if present.
*/
@ExperimentalKotest
fun JsonSchema.Builder.`null`() = JsonSchema.Null
/**
* Creates a [JsonSchema.JsonObject] node. Expand on the object configuration using the [dsl] which lets you specify
* properties using [withProperty]
*
* Example:
* ```kotlin
* val personSchema = jsonSchema {
* obj {
* withProperty("name") { string() }
* withProperty("age") { number() }
* }
* }
* ```
*/
@ExperimentalKotest
fun JsonSchema.Builder.obj(dsl: JsonSchema.JsonObjectBuilder.() -> Unit = {}) =
JsonSchema.JsonObjectBuilder().apply(dsl).build()
/**
* Defines a [JsonSchema.JsonArray] node where the elements are of the type provided by [typeBuilder].
* The length of the array can be specified using the [minItems] and [maxItems] keywords. Schema can ensure
* that each of item in an array is unique specified by [uniqueItems] keyword.
*
* @param minItems - minimum array length, default value is 0
* @param maxItems - maximum array length, default value is [Int.MAX_VALUE]
* @param uniqueItems - item uniqueness, default value is false
*/
@ExperimentalKotest
fun JsonSchema.Builder.array(
minItems: Int = 0,
maxItems: Int = Int.MAX_VALUE,
uniqueItems: Boolean = false,
contains: ContainsSpec? = null,
typeBuilder: (() -> JsonSchemaElement?)? = null
): JsonSchema.JsonArray {
val matcher: Matcher>? = if (uniqueItems) beUnique() else null
return JsonSchema.JsonArray(minItems, maxItems, matcher, contains, typeBuilder?.invoke())
}
@ExperimentalKotest
fun jsonSchema(
rootBuilder: JsonSchema.Builder.() -> JsonSchemaElement
): JsonSchema = JsonSchema(
JsonSchema.Builder.rootBuilder()
)
fun JsonSchema.Builder.containsSpec(
minContains: Int = 0,
maxContains: Int = Int.MAX_VALUE,
schema: JsonSchema.Builder.() -> JsonSchemaElement
) = ContainsSpec(schema(), minContains = minContains, maxContains = maxContains)
© 2015 - 2025 Weber Informatics LLC | Privacy Policy