<|start_filename|>src/commonMain/kotlin/com/github/ricky12awesome/jss/dsl/JsonSchemaDSL.kt<|end_filename|>
package com.github.ricky12awesome.jss.dsl

import com.github.ricky12awesome.jss.JsonType
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.*
import kotlinx.serialization.serializer
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

@DslMarker
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalJsonSchemaDSL

@ExperimentalJsonSchemaDSL
sealed class PropertyType<T, B : PropertyBuilder<T>>(val builder: () -> B) {
  class Number<T : kotlin.Number>(
    serializer: KSerializer<T>
  ) : PropertyType<T, NumberPropertyBuilder<T>>({ NumberPropertyBuilder(serializer) }) {
    companion object {
      inline operator fun <reified T : kotlin.Number> invoke(): Number<T> {
        return Number(serializer())
      }
    }
  }

  class Enum<E : kotlin.Enum<E>>(
    serializer: KSerializer<E>
  ) : PropertyType<E, EnumPropertyBuilder<E>>({ EnumPropertyBuilder(serializer) }) {
    companion object {
      inline operator fun <reified E : kotlin.Enum<E>> invoke(): Enum<E> = Enum(serializer())
    }
  }

  object String : PropertyType<kotlin.String, StringPropertyBuilder>(::StringPropertyBuilder)
  object Char : PropertyType<kotlin.Char, CharPropertyBuilder>(::CharPropertyBuilder)
  object Any : PropertyType<JsonElement, PropertyBuilder<JsonElement>>({ PropertyBuilder(JsonElement.serializer()) })

  class Array<T, B : PropertyBuilder<T>>(
    serializer: KSerializer<T>,
    type: PropertyType<T, B>
  ) : PropertyType<List<T>, ArrayPropertyBuilder<T, B>>({ ArrayPropertyBuilder(serializer, type) }) {
    companion object {
      inline operator fun <reified T, B : PropertyBuilder<T>> invoke(type: PropertyType<T, B>): Array<T, B> {
        return Array(serializer(), type)
      }
    }
  }

  object Object : PropertyType<JsonObject, ObjectBuilder>(::ObjectBuilder)
  object ObjectSealed : PropertyType<JsonObject, ObjectSealedBuilder>(::ObjectSealedBuilder)

  class ObjectMap<T, B : PropertyBuilder<T>>(
    serializer: KSerializer<T>,
    type: PropertyType<T, B>
  ) : PropertyType<Map<kotlin.String, T>, ObjectMapBuilder<T, B>>({ ObjectMapBuilder(serializer, type) }) {
    companion object {
      inline operator fun <reified T, B : PropertyBuilder<T>> invoke(type: PropertyType<T, B>): ObjectMap<T, B> {
        return ObjectMap(serializer(), type)
      }
    }
  }

  inline fun build(body: B.() -> Unit): JsonObject {
    return build(builder(), body)
  }

  inline fun build(builder: B, body: B.() -> Unit): JsonObject {
    return builder.run {
      body()
      build()
    }
  }

  inline fun build(definitions: ObjectBuilder.Definitions?, body: B.() -> Unit): JsonObject {
    return when (this) {
      Object -> build(ObjectBuilder(definitions, false) as B, body)
      else -> build(body)
    }
  }
}

inline class DefinitionReference(val id: String) {
  val url get() = JsonPrimitive("#/definitions/$id")
}

open class PropertyBuilder<T>(
  val serializer: KSerializer<T>,
  val type: JsonType? = null
) {
  val data: MutableMap<String, JsonElement> = mutableMapOf()

  init {
    type?.let {
      data["type"] = it.json
    }
  }

  fun reference(id: DefinitionReference) {
    data["\$ref"] = id.url
  }

  fun referenceUnsafe(id: String) {
    reference(DefinitionReference(id))
  }

  @ExperimentalJsonSchemaDSL
  open fun PropertyType<T, *>.build(): JsonObject {
    return JsonObject(data)
  }
}

class StringPropertyBuilder : PropertyBuilder<String>(String.serializer(), JsonType.STRING)
class CharPropertyBuilder : PropertyBuilder<Char>(Char.serializer(), JsonType.STRING)

class EnumPropertyBuilder<E : Enum<E>>(
  serializer: KSerializer<E>
) : PropertyBuilder<E>(serializer, JsonType.STRING)

class NumberPropertyBuilder<T : Number>(
  serializer: KSerializer<T>
) : PropertyBuilder<T>(serializer, JsonType.NUMBER)

@ExperimentalJsonSchemaDSL
class ArrayPropertyBuilder<T, B : PropertyBuilder<T>>(
  serializer: KSerializer<T>,
  val itemType: PropertyType<T, B>
) : PropertyBuilder<List<T>>(ListSerializer(serializer), JsonType.ARRAY)

open class CommonObjectBuilder<T>(serializer: KSerializer<T>, type: JsonType) : PropertyBuilder<T>(serializer, type)

@ExperimentalJsonSchemaDSL
class ObjectMapBuilder<T, B : PropertyBuilder<T>>(
  serializer: KSerializer<T>,
  val valueType: PropertyType<T, B>
) : CommonObjectBuilder<Map<String, T>>(MapSerializer(String.serializer(), serializer), JsonType.OBJECT_MAP)

class ObjectSealedBuilder : CommonObjectBuilder<JsonObject>(JsonObject.serializer(), JsonType.OBJECT_SEALED)

class ObjectBuilder(
  definitions: Definitions? = null,
  private val isRoot: Boolean = true
) : CommonObjectBuilder<JsonObject>(JsonObject.serializer(), JsonType.OBJECT) {
  val definitions = definitions ?: Definitions()
  val properties = Properties(this.definitions)

  class Properties(@PublishedApi internal val definitions: Definitions) {
    val data = mutableMapOf<String, JsonElement>()
    val required = mutableListOf<String>()

    @ExperimentalJsonSchemaDSL
    inline fun <B : PropertyBuilder<*>> property(
      name: String,
      type: PropertyType<*, B>,
      isRequired: Boolean = true,
      builder: B.() -> Unit
    ) = propertyUnsafe(name, buildProperty(type, builder), isRequired)

    @ExperimentalJsonSchemaDSL
    inline fun <B : PropertyBuilder<*>> requiredProperty(
      name: String,
      type: PropertyType<*, B>,
      builder: B.() -> Unit
    ) = property(name, type, true, builder)

    @ExperimentalJsonSchemaDSL
    inline fun <B : PropertyBuilder<*>> optionalProperty(
      name: String,
      type: PropertyType<*, B>,
      builder: B.() -> Unit
    ) = property(name, type, false, builder)

    fun propertyUnsafe(name: String, element: JsonObject, isRequired: Boolean = true) {
      data[name] = element

      if (isRequired) {
        required += name
      }
    }

    fun requiredPropertyUnsafe(name: String, element: JsonObject) = propertyUnsafe(name, element, true)
    fun optionalPropertyUnsafe(name: String, element: JsonObject) = propertyUnsafe(name, element, false)

    @ExperimentalJsonSchemaDSL
    fun propertyReference(name: String, id: DefinitionReference, isRequired: Boolean = true) {
      if (isRequired) {
        required += name
      }

      return property(name, PropertyType.Any) {
        reference(id)
      }
    }

    @ExperimentalJsonSchemaDSL
    fun propertyReferenceUnsafe(name: String, id: String, isRequired: Boolean = true) {
      if (isRequired) {
        required += name
      }

      return property(name, PropertyType.Any) {
        referenceUnsafe(id)
      }
    }

    @ExperimentalJsonSchemaDSL
    inline operator fun invoke(builder: Properties.() -> Unit): Properties {
      return apply(builder)
    }
  }

  class Definitions {
    val data = mutableMapOf<String, JsonElement>()

    @ExperimentalJsonSchemaDSL
    inline fun <T, B : PropertyBuilder<T>> definition(
      id: String,
      type: PropertyType<T, B>,
      builder: B.() -> Unit
    ): DefinitionReference {
      data[id] = type.build(this, builder)

      return DefinitionReference(id)
    }

    @ExperimentalJsonSchemaDSL
    inline operator fun invoke(builder: Definitions.() -> Unit): Definitions {
      return apply(builder)
    }

    @ExperimentalJsonSchemaDSL
    operator fun <T, B : PropertyBuilder<T>> invoke(
      type: PropertyType<T, B>,
      builder: B.() -> Unit
    ): DefinitionReferenceDelegate<T, B> {
      return DefinitionReferenceDelegate(type, builder)
    }

    @ExperimentalJsonSchemaDSL
    inner class DefinitionReferenceDelegate<T, B : PropertyBuilder<T>>(
      private val type: PropertyType<T, B>,
      private val builder: B.() -> Unit
    ) : ReadOnlyProperty<Any?, DefinitionReference> {
      override fun getValue(thisRef: Any?, property: KProperty<*>): DefinitionReference {
        if (property.name !in data) {
          definition(property.name, type, builder)
        }

        return DefinitionReference(property.name)
      }
    }
  }

  @ExperimentalJsonSchemaDSL
  override fun PropertyType<JsonObject, *>.build(): JsonObject {
    data["type"] = JsonType.OBJECT.json

    if (properties.data.isNotEmpty()) {
      data["properties"] = JsonObject(properties.data)
    }

    if (properties.required.isNotEmpty()) {
      data["required"] = JsonArray(properties.required.map(::JsonPrimitive))
    }

    if (isRoot) {
      if (definitions.data.isNotEmpty()) {
        data["definitions"] = JsonObject(definitions.data)
      }
    }

    return JsonObject(data)
  }
}

@ExperimentalJsonSchemaDSL
inline fun <T, B : PropertyBuilder<T>> buildProperty(type: PropertyType<T, B>, builder: B.() -> Unit): JsonObject {
  return type.build(builder)
}

@ExperimentalJsonSchemaDSL
inline fun buildSchema(builder: ObjectBuilder.() -> Unit): JsonObject {
  return buildProperty(PropertyType.Object, builder)
}

<|start_filename|>src/commonTest/kotlin/Tests.kt<|end_filename|>
import com.github.ricky12awesome.jss.JsonSchema
import com.github.ricky12awesome.jss.JsonSchema.*
import com.github.ricky12awesome.jss.dsl.*
import com.github.ricky12awesome.jss.encodeToSchema
import com.github.ricky12awesome.jss.globalJson
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
import kotlin.test.Test

@Serializable
data class Config(
  @Description(arrayOf("Name for this project."))
  val name: String = "",
  @Description(arrayOf("Theme for this project."))
  val theme: Theme = Theme()
)

@Serializable
sealed class ThemeColor {
  @Serializable @SerialName("HEX")
  data class HEX(
    @Pattern("#[0-9a-fA-F]{2,6}") val hex: String
  ) : ThemeColor()

  @Serializable @SerialName("RGB")
  data class RGB(
    @JsonSchema.IntRange(0, 255) val r: Int,
    @JsonSchema.IntRange(0, 255) val g: Int,
    @JsonSchema.IntRange(0, 255) val b: Int
  ) : ThemeColor()

  @Serializable @SerialName("HSV")
  data class HSV(
    @JsonSchema.IntRange(1, 360) val h: Int,
    @FloatRange(0.0, 1.0) val s: Double,
    @FloatRange(0.0, 1.0) val v: Double
  ) : ThemeColor()

  @Serializable @SerialName("HSL")
  data class HSL(
    @JsonSchema.IntRange(1, 360) val h: Int,
    @FloatRange(0.0, 1.0) val s: Double,
    @FloatRange(0.0, 1.0) val l: Double
  ) : ThemeColor()
}

@Serializable
data class Theme(
  @Description(arrayOf("Primary color for this theme."))
  @Definition("ThemeColor") val primary: ThemeColor = ThemeColor.RGB(128, 128, 128),
  @Description(arrayOf("Secondary color for this theme."))
  @Definition("ThemeColor") val secondary: ThemeColor = ThemeColor.HSV(0, 0.0, 0.3),
  @Description(arrayOf("Accent color for this theme."))
  @Definition("ThemeColor") val accent: ThemeColor = ThemeColor.HSL(0, 0.0, 0.8),
  @Description(arrayOf("Background color for this theme."))
  @Definition("ThemeColor") val background: ThemeColor = ThemeColor.HEX("#242424")
)

class Tests {
  val json = globalJson

  @Test
  fun annotated_schema() {
    println(json.encodeToSchema(Config.serializer(), false))
  }

  @Test
  @ExperimentalJsonSchemaDSL
  fun schema_dsl() {
    val schema = buildSchema {
      val data by definitions(PropertyType.Object) {
        properties {
          property("name", PropertyType.String) {
            description = "Name of something"
          }

          property("type", PropertyType.String) {
            enum = listOf("A", "B", "C")
          }
        }
      }

      properties {
        property("array", PropertyType.Array(PropertyType.Object)) {
          description = "Some array with random data"
          minItems = 1
          maxItems = 64

          items {
            reference(data)
          }
        }

        property("map", PropertyType.ObjectMap(PropertyType.Object)) {
          description = "Some map with random data"

          propertyNames {
            pattern = Regex("^(\\d*)\$")
          }

          additionalProperties {
            reference(data)
          }
        }
      }
    }

    println(json.encodeToString(JsonObject.serializer(), schema))
  }
}


<|start_filename|>settings.gradle.kts<|end_filename|>
enableFeaturePreview("GRADLE_METADATA")

rootProject.name = "json-schema-serialization"

pluginManagement {
  repositories {
    gradlePluginPortal()
    jcenter()
  }
}


<|start_filename|>src/commonMain/kotlin/com/github/ricky12awesome/jss/dsl/JsonSchemaProperties.kt<|end_filename|>
package com.github.ricky12awesome.jss.dsl

import com.github.ricky12awesome.jss.globalJson
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.*
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class PropertyDelegate<T, B : PropertyBuilder<T>, V>(
  val id: String? = null,
  val default: JsonElement = JsonNull,
  val get: B.(JsonElement) -> V,
  val set: B.(V) -> JsonElement
) : ReadWriteProperty<B, V> {
  override fun getValue(thisRef: B, property: KProperty<*>): V {
    return thisRef.get(thisRef.data[id ?: property.name] ?: default)
  }

  override fun setValue(thisRef: B, property: KProperty<*>, value: V) {
    thisRef.data[id ?: property.name] = thisRef.set(value)
  }
}

var <T> PropertyBuilder<T>.enum by PropertyDelegate(
  default = JsonArray(listOf()),
  get = { globalJson.decodeFromJsonElement(ListSerializer(serializer), it) },
  set = { globalJson.encodeToJsonElement(ListSerializer(serializer), it) }
)

var <T> PropertyBuilder<T>.const by PropertyDelegate(
  default = JsonNull,
  get = { globalJson.decodeFromJsonElement(serializer, it) },
  set = { globalJson.encodeToJsonElement(serializer, it) }
)

var <T> PropertyBuilder<T>.description by PropertyDelegate(
  default = JsonPrimitive(""),
  get = { globalJson.decodeFromJsonElement(String.serializer(), it) },
  set = { globalJson.encodeToJsonElement(String.serializer(), it) }
)

var StringPropertyBuilder.pattern by PropertyDelegate(
  default = JsonPrimitive(""),
  get = { globalJson.decodeFromJsonElement(serializer, it).toRegex() },
  set = { globalJson.encodeToJsonElement(serializer, it.toString()) }
)

var <T> PropertyBuilder<T>.default by PropertyDelegate(
  default = JsonNull,
  get = { globalJson.decodeFromJsonElement(serializer, it) },
  set = { globalJson.encodeToJsonElement(serializer, it) }
)

var <T : Number> NumberPropertyBuilder<T>.minimum by PropertyDelegate(
  default = JsonPrimitive(0),
  get = { globalJson.decodeFromJsonElement(serializer, it) },
  set = { globalJson.encodeToJsonElement(serializer, it) }
)

var <T : Number> NumberPropertyBuilder<T>.maximum by PropertyDelegate(
  default = JsonPrimitive(0),
  get = { globalJson.decodeFromJsonElement(serializer, it) },
  set = { globalJson.encodeToJsonElement(serializer, it) }
)

@ExperimentalJsonSchemaDSL
var <T, B : PropertyBuilder<T>> ArrayPropertyBuilder<T, B>.minItems by PropertyDelegate(
  default = JsonPrimitive(0),
  get = { globalJson.decodeFromJsonElement(Int.serializer(), it) },
  set = { globalJson.encodeToJsonElement(Int.serializer(), it) }
)

@ExperimentalJsonSchemaDSL
var <T, B : PropertyBuilder<T>> ArrayPropertyBuilder<T, B>.maxItems by PropertyDelegate(
  default = JsonPrimitive(0),
  get = { globalJson.decodeFromJsonElement(Int.serializer(), it) },
  set = { globalJson.encodeToJsonElement(Int.serializer(), it) }
)

@ExperimentalJsonSchemaDSL
inline fun <T, B : PropertyBuilder<T>> ArrayPropertyBuilder<T, B>.items(builder: B.() -> Unit) {
  data["items"] = buildProperty(itemType, builder)
}

@ExperimentalJsonSchemaDSL
inline fun CommonObjectBuilder<*>.additionalProperties(value: Boolean) {
  data["additionalProperties"] = JsonPrimitive(value)
}

@ExperimentalJsonSchemaDSL
inline fun <T, B : PropertyBuilder<T>> CommonObjectBuilder<*>.additionalProperties(
  type: PropertyType<T, B>,
  builder: B.() -> Unit
) {
  data["additionalProperties"] = buildProperty(type, builder)
}

@ExperimentalJsonSchemaDSL
inline fun <T, B : PropertyBuilder<T>> ObjectMapBuilder<T, B>.additionalProperties(
  builder: B.() -> Unit
) {
  data["additionalProperties"] = buildProperty(valueType, builder)
}

@ExperimentalJsonSchemaDSL
inline fun CommonObjectBuilder<*>.propertyNames(
  builder: StringPropertyBuilder.() -> Unit
) {
  data["propertyNames"] = buildProperty(PropertyType.String, builder)
}



<|start_filename|>src/commonMain/kotlin/com/github/ricky12awesome/jss/Util.kt<|end_filename|>
package com.github.ricky12awesome.jss

import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject

private fun <K, V> MutableMap<K, V>.remapMerge(
  key: K,
  value: V,
  remap: (V, V) -> V?
): V? {
  val oldValue: V? = this[key]
  val newValue: V? = if (oldValue == null) value else remap(oldValue, value)

  if (newValue == null) {
    remove(key)
  } else {
    put(key, newValue)
  }

  return newValue
}

fun MutableMap<String, JsonElement>.merge(key: String, element: JsonElement): JsonElement? {
  return remapMerge(key, element, ::merge)
}

fun merge(previous: JsonElement, value: JsonElement): JsonElement? {
  return when {
    previous == value -> value
    previous is JsonObject && value is JsonObject -> merge(previous, value)
    previous is JsonArray && value is JsonArray -> JsonArray(previous + value)
    value is JsonNull -> previous
    else -> value
  }
}

fun merge(previous: JsonObject, value: JsonObject): JsonObject? {
  val elements = mutableMapOf<String, JsonElement>()
  val values = (previous.keys + value.keys).toSet().associateWith {
    (previous[it] ?: JsonNull) to (value[it] ?: JsonNull)
  }

  values.forEach { (name, oldNewPair) ->
    val (old, new) = oldNewPair
    val merged = merge(old, new) ?: return null

    elements[name] = merged
  }

  return JsonObject(elements)
}

<|start_filename|>src/commonMain/kotlin/com/github/ricky12awesome/jss/JsonSchema.kt<|end_filename|>
package com.github.ricky12awesome.jss

import com.github.ricky12awesome.jss.internal.JsonSchemaDefinitions
import com.github.ricky12awesome.jss.internal.createJsonSchema
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.json.*
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty

/**
 * Global Json object for basic serialization. uses Stable Configuration.
 */
val globalJson by lazy {
  Json {
    prettyPrintIndent = "  "
    prettyPrint = true
    ignoreUnknownKeys = true
    isLenient = true
    coerceInputValues = true
    encodeDefaults = true
  }
}

/**
 * Represents the type of a json type
 */
enum class JsonType(jsonType: String) {
  /**
   * Represents the json array type
   */
  ARRAY("array"),

  /**
   * Represents the json number type
   */
  NUMBER("number"),

  /**
   * Represents the string type
   */
  STRING("string"),

  /**
   * Represents the boolean type
   */
  BOOLEAN("boolean"),

  /**
   * Represents the object type, this is used for serializing normal classes
   */
  OBJECT("object"),

  /**
   * Represents the object type, this is used for serializing sealed classes
   */
  OBJECT_SEALED("object"),

  /**
   * Represents the object type, this is used for serializing maps
   */
  OBJECT_MAP("object");

  val json = JsonPrimitive(jsonType)

  override fun toString(): String = json.content
}

@Target()
annotation class JsonSchema {
  /**
   * Description of this property
   */
  @SerialInfo
  @Repeatable
  @Retention(AnnotationRetention.BINARY)
  @Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
  annotation class Description(val lines: Array<out String>)

  /**
   * Enum-like values for non-enum string
   */
  @SerialInfo
  @Retention(AnnotationRetention.BINARY)
  @Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
  annotation class StringEnum(val values: Array<out String>)

  /**
   * Minimum and Maximum values using whole numbers
   *
   * Only works when [SerialKind] is any of
   * [PrimitiveKind.BYTE], [PrimitiveKind.SHORT], [PrimitiveKind.INT], [PrimitiveKind.LONG]
   */
  @SerialInfo
  @Retention(AnnotationRetention.BINARY)
  @Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
  annotation class IntRange(val min: Long, val max: Long)

  /**
   * Minimum and Maximum values using floating point numbers
   *
   * Only works when [SerialKind] is [PrimitiveKind.FLOAT] or [PrimitiveKind.DOUBLE]
   */
  @SerialInfo
  @Retention(AnnotationRetention.BINARY)
  @Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
  annotation class FloatRange(val min: Double, val max: Double)

  /**
   * [pattern] to use on this property
   *
   * Only works when [SerialKind] is [PrimitiveKind.STRING]
   */
  @SerialInfo
  @Retention(AnnotationRetention.BINARY)
  @Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
  annotation class Pattern(val pattern: String)

  /**
   * Should this property be a definition and be referenced using [id]?
   *
   * @param id The id for this definition, this will be referenced by '#/definitions/$[id]'
   */
  @SerialInfo
  @Retention(AnnotationRetention.BINARY)
  @Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
  annotation class Definition(val id: String)

  /**
   * This property will not create definitions
   */
  @SerialInfo
  @Retention(AnnotationRetention.BINARY)
  @Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
  annotation class NoDefinition
}

/**
 * Will be removed in 0.8.0
 */
@Deprecated(
  message = "Use encodeWithSchema instead",
  replaceWith = ReplaceWith("this.encodeWithSchema(serializer, value, url)"),
  level = DeprecationLevel.ERROR
)
fun <T> Json.stringifyWithSchema(serializer: SerializationStrategy<T>, value: T, url: String): String {
  return encodeWithSchema(serializer,value, url)
}

/**
 * Adds a `$schema` property with the provided [url] that points to the Json Schema,
 * this can be a File location or a HTTP URL
 *
 * This is so when you serialize your [value] it will use [url] as it's Json Schema for code completion.
 */
fun <T> Json.encodeWithSchema(serializer: SerializationStrategy<T>, value: T, url: String): String {
  val json = encodeToJsonElement(serializer, value) as JsonObject
  val append = mapOf("\$schema" to JsonPrimitive(url))

  return encodeToString(JsonObject.serializer(), JsonObject(append + json))
}

/**
 * Will be removed in 0.8.0
 */
@Deprecated(
  message = "Use encodeToSchema instead",
  replaceWith = ReplaceWith("this.encodeToSchema(descriptor)"),
  level = DeprecationLevel.ERROR
)
fun Json.stringifyToSchema(descriptor: SerialDescriptor): String = encodeToSchema(descriptor)

/**
 * Stringifies the provided [descriptor] with [buildJsonSchema]
 *
 * @param generateDefinitions Should this generate definitions by default
 */
fun Json.encodeToSchema(descriptor: SerialDescriptor, generateDefinitions: Boolean = true): String {
  return encodeToString(JsonObject.serializer(), buildJsonSchema(descriptor, generateDefinitions))
}

/**
 * Will be removed in 0.8.0
 */
@Deprecated(
  message = "Use encodeToSchema instead",
  replaceWith = ReplaceWith("this.encodeToSchema(serializer)"),
  level = DeprecationLevel.ERROR
)
fun Json.stringifyToSchema(serializer: SerializationStrategy<*>): String = encodeToSchema(serializer)

/**
 * Stringifies the provided [serializer] with [buildJsonSchema], same as doing
 * ```kotlin
 * json.encodeToSchema(serializer.descriptor)
 * ```
 * @param generateDefinitions Should this generate definitions by default
 */
fun Json.encodeToSchema(serializer: SerializationStrategy<*>, generateDefinitions: Boolean = true): String {
  return encodeToSchema(serializer.descriptor, generateDefinitions)
}

/**
 * Creates a Json Schema using the provided [descriptor]
 *
 * @param autoDefinitions automatically generate definitions by default
 */
fun buildJsonSchema(descriptor: SerialDescriptor, autoDefinitions: Boolean = false): JsonObject {
  val prepend = mapOf("\$schema" to JsonPrimitive("http://json-schema.org/draft-07/schema"))
  val definitions = JsonSchemaDefinitions(autoDefinitions)
  val root = descriptor.createJsonSchema(descriptor.annotations, definitions)
  val append = mapOf("definitions" to definitions.getDefinitionsAsJsonObject())

  return JsonObject(prepend + root + append)
}

/**
 * Creates a Json Schema using the provided [serializer],
 * same as doing `jsonSchema(serializer.descriptor)`
 *
 * @param generateDefinitions Should this generate definitions by default
 */
fun buildJsonSchema(serializer: SerializationStrategy<*>, generateDefinitions: Boolean = true): JsonObject {
  return buildJsonSchema(serializer.descriptor, generateDefinitions)
}
