All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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