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

json.Schema.scala Maven / Gradle / Ivy

package json

import com.github.andyglow.json.Value.ValueAdapter
import com.github.andyglow.json._
import com.github.andyglow.jsonschema.AsTree
import json.Schema.`object`.Field.RWMode
import json.schema.{validation => V}

import scala.collection.immutable.ListMap

sealed trait Schema[+T] {
  type Self <: Schema[T]

  private var _title: Option[String]                    = None
  private var _description: Option[String]              = None
  private var _discriminationKey: Option[String]        = None
  private var _validations: collection.Seq[V.Def[_, _]] = Seq.empty
  def jsonType: String
  def withValidation[TT >: T, B](v: V.Def[B, _], vs: V.Def[B, _]*)(implicit
    bound: V.Magnet[TT, B]
  ): Self = {
    val copy = this.duplicate()
    copy._validations = (v +: vs).foldLeft(_validations) { case (agg, v) =>
      bound.append(agg, v)
    }
    copy
  }
  def description: Option[String]       = _description
  def title: Option[String]             = _title
  def discriminationKey: Option[String] = _discriminationKey

  // NOTE: `.toSeq` is required for scala 2.13
  // otherwise we'll see
  // type mismatch;
  //  [error]  found   : Seq[json.ValidationDef[_, _]] (in scala.collection)
  //  [error]  required: Seq[json.ValidationDef[_, _]] (in scala.collection.immutable)
  def validations: Seq[V.Def[_, _]] = _validations.toSeq
  def toDebugString: String         = AsTree(this).rendered
  protected object ToString {
    def apply(fn: StringBuilder => Any): String = {
      val sb = new StringBuilder
      fn(sb)
      writeValidations(sb)
      writeExtra(sb)
      sb.toString
    }
    def writeExtra(sb: StringBuilder): Unit = {
      _description foreach { x => sb.append(" description=`").append(x).append('`') }
      _title foreach { x => sb.append(" title=`").append(x).append('`') }
      _discriminationKey foreach { x => sb.append(" discriminationKey=`").append(x).append('`') }
    }
    def writeValidations(sb: StringBuilder): Unit = {
      if (validations.nonEmpty) {
        sb.append(" {")
        var f = true
        validations foreach { v =>
          if (!f) sb.append(", ")
          sb.append(v.validation)
          sb.append(":=")
          sb.append(v.json)
          f = false
        }
        sb.append("}")
      }

      ()
    }
  }
  protected def mkCopy(): Self
  def duplicate(
    description: Option[String] = this._description,
    title: Option[String] = this._title,
    discriminationKey: Option[String] = this._discriminationKey
  ): Self = {

    val copy = mkCopy()
    copy._validations = this._validations
    copy._discriminationKey = discriminationKey
    copy._description = description
    copy._title = title

    copy
  }
  def withExtraFrom(x: Schema[_]): Self = {
    val copy = mkCopy()
    copy._validations = x._validations
    copy._description = x._description
    copy._title = x._title
    copy._discriminationKey = x._discriminationKey

    copy
  }
  def withValidationsAddedFrom(x: Schema[_]): Self = {
    val copy = mkCopy()
    copy._validations = copy._validations ++ x._validations

    copy
  }
  def canEqual(that: Any): Boolean = that.isInstanceOf[Schema[_]] // && getClass == that.getClass
  override def equals(obj: Any): Boolean = obj match {
    case s: Schema[_] =>
      s.canEqual(this) &&
      this.title == s.title &&
      this.description == s.description &&
      this.discriminationKey == s.discriminationKey &&
      // compare collections disregarding order
      this.validations.forall(s.validations.contains) &&
      s.validations.forall(this.validations.contains)

    case _ => false
  }
  def withDescription(x: String): Self                     = duplicate(description = Some(x))
  def withTitle(x: String): Self                           = duplicate(title = Some(x))
  def withDiscriminationKey(x: String): Self               = duplicate(discriminationKey = Some(x))
  def toDefinition[TT >: T](sig: String): Schema.`def`[TT] = Schema.`def`(sig, this)
  @deprecated("please use `toDefinition` instead", "1.0.0") def apply(refName: String): Schema[T] =
    toDefinition(refName)
}

object Schema {

  def apply[T: Schema]: Schema[T] = implicitly

  // +------------
  // | Boolean
  // +---------------
  //
  sealed class `boolean` extends Schema[Boolean] {
    type Self = `boolean`
    def mkCopy()                              = new `boolean`
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`boolean`]
    override def jsonType: String             = "boolean"
    override def toString: String             = ToString(_ append "boolean")
  }
  object `boolean` extends `boolean`

  // +------------
  // | Integer
  // +---------------
  //
  sealed class `integer` extends Schema[Int] {
    type Self = `integer`
    def mkCopy()                              = new `integer`()
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`integer`]
    override def jsonType: String             = "integer"
    override def toString: String             = ToString(_ append "integer")
  }
  object `integer` extends `integer`

  // +------------
  // | Number
  // +---------------
  //
  final class `number`[T: Numeric] extends Schema[T] {
    type Self = `number`[T]
    def mkCopy()                              = new `number`[T]
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`number`[_]]
    override def jsonType: String             = "number"
    override def toString: String             = ToString(_ append "number")
  }
  object `number` {
    def apply[T: Numeric]: `number`[T] = new `number`[T]
  }

  // +------------
  // | String
  // +---------------
  //
  sealed case class `string`[T](format: Option[`string`.Format]) extends Schema[T] {
    type Self = `string`[T]
    def mkCopy()                              = new `string`[T](format)
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`string`[_]]
    override def equals(obj: Any): Boolean = obj match {
      case `string`(f) => format == f && super.equals(obj)
      case _           => false
    }
    override def jsonType: String = "string"
    override def toString: String = ToString { sb =>
      sb append "string"
      format foreach { format =>
        sb append "(format = "
        sb append format
        sb append ")"
      }
    }
  }
  object `string` extends `string`[String](None) {
    def apply[T]: `string`[T]                 = new `string`[T](None)
    def apply[T](format: Format): `string`[T] = new `string`[T](Some(format))
    trait Format extends Product
    object Format {
      case object `date`      extends Format
      case object `time`      extends Format
      case object `date-time` extends Format // Date representation, as defined by RFC 3339, section 5.6.
      case object `email`     extends Format // Internet email address, see RFC 5322, section 3.4.1.
      case object `hostname`  extends Format // Internet host name, see RFC 1034, section 3.1.
      case object `ipv4`      extends Format // Internet host name, see RFC 1034, section 3.1.
      case object `ipv6`      extends Format // IPv6 address, as defined in RFC 2373, section 2.2.
      case object `uri`       extends Format // A universal resource identifier (URI), according to RFC3986.

      // added in 2019-09
      case object `duration`     extends Format // The duration format is from the ISO 8601 ABNF as given in Appendix A of RFC 3339
      case object `idn-hostname` extends Format // Use RFC 1123 instead of RFC 1034; this allows for a leading digit,
      // `hostname` is also RFC 1123 since 2019-09
      case object `uuid` extends Format // A string instance is valid against this attribute if it is a valid string representation of a UUID, according to RFC4122
    }
  }

  // +------------
  // | Array
  // +---------------
  //
  final case class `array`[T, C[_]](componentType: Schema[T], unique: Boolean = false) extends Schema[C[T]] {
    type Self = `array`[T, C]
    def mkCopy() = new `array`[T, C](componentType, unique)
    override def canEqual(that: Any): Boolean = that match {
      case `array`(_, _) => true
      case _             => false
    }
    override def equals(obj: Any): Boolean = obj match {
      case `array`(c, u) => u == unique && componentType == c && super.equals(obj)
      case _             => false
    }
    override def jsonType: String = "array"
    override def toString: String = ToString { sb =>
      sb append "array(component ="
      sb append componentType
      sb append ", unique ="
      sb append unique
      sb append ")"
    }
  }

  // +------------
  // | Dictionary
  // +---------------
  //
  final case class `dictionary`[K, V, C[_, _]](valueType: Schema[V]) extends Schema[C[K, V]] {
    type Self = `dictionary`[K, V, C]
    override def jsonType = "object"
    def mkCopy()          = new `dictionary`[K, V, C](valueType)
    override def canEqual(that: Any): Boolean = that match {
      case `dictionary`(_) => true
      case _               => false
    }
    override def equals(obj: Any): Boolean = obj match {
      case `dictionary`(c) => valueType == c && super.equals(obj)
      case _               => false
    }
    override def toString: String = ToString { sb =>
      sb append "dictionary(value ="
      sb append valueType
      sb append ")"
    }
  }
  object `dictionary` {
    abstract class KeyPattern[T](val pattern: String)
    object KeyPattern {
      def mk[T](pattern: String): KeyPattern[T] = new KeyPattern[T](pattern) {}
      def forEnum[T](vals: Iterable[String]): KeyPattern[T] = {
        require(vals.nonEmpty)
        mk[T](vals.toList.distinct.sorted.mkString("^(?:", "|", ")$"))
      }
      implicit object StringKeyPattern extends KeyPattern[String]("^.*$")
      implicit object CharKeyPattern   extends KeyPattern[Char]("^.{1}$")
      implicit object ByteKeyPattern   extends KeyPattern[Byte]("^[0-9]+$")
      implicit object ShortKeyPattern  extends KeyPattern[Short]("^[0-9]+$")
      implicit object IntKeyPattern    extends KeyPattern[Int]("^[0-9]+$")
      implicit object LongKeyPattern   extends KeyPattern[Long]("^[0-9]+$")
    }
  }

  // +------------
  // | Object
  // +---------------
  //
  sealed case class `object`[T] private (fields: List[`object`.Field[_]]) extends Schema[T] {
    import `object`._
    type Self = `object`[T]
    def mkCopy()                              = copy()
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`object`[_]]
    override def equals(obj: Any): Boolean = obj match {
      case `object`(f) => fields.toSet == f.toSet && super.equals(obj)
      case _           => false
    }

    def dropField(pred: Field[_] => Boolean): `object`[T] =
      copy(fields = this.fields.filterNot(pred)).withExtraFrom(this)
    def withField(f: Field[_]): `object`[T]                                            = copy(fields = fields :+ f).withExtraFrom(this)
    def withField(name: String, tpe: Schema[_], required: Boolean = true): `object`[T] = withField(Field(name, tpe, required))
    def withFieldsUpdated(pf: PartialFunction[Field[_], Field[_]]): `object`[T] = copy(
      fields = fields collect {
        case f if pf isDefinedAt f => pf(f)
        case x                          => x
      }
    ).withExtraFrom(this)
    override def jsonType: String = "object"
    override def toString: String = ToString { sb =>
      sb append "object("
      fields foreach { field =>
        sb append field
        sb append ", "
      }
      sb.setLength(sb.length - 1) // drop last comma
      sb append ")"

      if (this.isInstanceOf[Free]) { sb append " additionalProperties=true" }
    }

    def free: `object`[T] with Free = {
      val self = this
      new `object`[T](fields) with Free {
        type Type = T
        def strict: `object`[T] = self
      }
    }
  }
  object `object` {
    sealed trait Free { this: `object`[_] =>
      type Type
      def strict: `object`[Type]
    }
    object Free {
      def apply[T](): `object`[T] with Free = {
        new `object`[T](List.empty) with Free {
          type Type = T
          override def strict: `object`[T] = new `object`[T](List.empty)
        }
      }
    }
    final case class Field[T](
      name: String,
      tpe: Schema[T],
      required: Boolean,
      default: Option[Value],
      description: Option[String],
      rwMode: Field.RWMode
    ) {
      def canEqual(that: Any): Boolean = that.isInstanceOf[Field[T]]
      override def equals(that: Any): Boolean = canEqual(that) && {
        val other = that.asInstanceOf[Field[T]]
        this.name == other.name &&
        this.required == other.required &&
        this.tpe == other.tpe &&
        this.default == other.default &&
        this.rwMode == other.rwMode
      }
      override def hashCode: Int = name.hashCode
      override def toString: String = {
        var extra = (required, default) match {
          case (true, None)     => " /R"
          case (false, None)    => ""
          case (true, Some(v))  => s" /R /$v"
          case (false, Some(v)) => s" /$v"
        }
        description foreach { x => extra = extra + s" description=`$x`" }
        s"$name: $tpe: $rwMode$extra"
      }
      def withDescription(x: Option[String]): Field[T] =
        new Field(name, tpe, required, default, x, rwMode)
      def withRWMode(x: RWMode): Field[T] = new Field(name, tpe, required, default, description, x)
      def setReadOnly: Field[T]           = withRWMode(RWMode.ReadOnly)
      def setWriteOnly: Field[T]          = withRWMode(RWMode.WriteOnly)
    }
    object Field {
      sealed trait RWMode
      object RWMode {
        case object ReadOnly  extends RWMode
        case object WriteOnly extends RWMode
        case object ReadWrite extends RWMode
      }

      def apply[T](name: String, tpe: Schema[T]): Field[T] =
        new Field(name, tpe, required = true, default = None, description = None, RWMode.ReadWrite)

      def apply[T](name: String, tpe: Schema[T], required: Boolean): Field[T] =
        new Field(name, tpe, required, default = None, description = None, RWMode.ReadWrite)

      def apply[T: ToValue](
        name: String,
        tpe: Schema[T],
        required: Boolean,
        default: T,
        rwMode: RWMode = RWMode.ReadWrite
      ): Field[T] =
        new Field(name, tpe, required, Some(ToValue(default)), description = None, rwMode = rwMode)

      def fromJson[T](
        name: String,
        tpe: Schema[T],
        required: Boolean,
        default: Option[Value],
        rwMode: RWMode = RWMode.ReadWrite
      ): Field[T] = new Field(name, tpe, required, default, description = None, rwMode = rwMode)
    }

    def apply[T](field: Field[_], xs: Field[_]*): `object`[T] = {
      fromList(field +: xs.toList)
    }

    def fromList[T](fields: List[Field[_]]): `object`[T] = {
      `object`(fields.distinct)
    }
  }

  // +------------
  // | Enum
  // +---------------
  //
  final case class `enum`[T](tpe: Schema[_], values: Set[Value]) extends Schema[T] {
    type Self = `enum`[T]
    def mkCopy()                              = new `enum`[T](tpe, values)
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`enum`[_]]
    override def equals(obj: Any): Boolean = obj match {
      case `enum`(t, v) => t == tpe && values == v && super.equals(obj)
      case _            => false
    }
    override def jsonType: String = "enum"
    override def toString: String = ToString { sb =>
      sb append "enum["
      sb append tpe.toString
      sb append "]("
      values foreach { value =>
        sb append value
        sb append ","
      }
      sb.setLength(sb.length - 1) // drop last comma
      sb append ")"
    }
  }
  object `enum` {
    def of[T](tpe: Schema[_], x: Value, xs: Value*): `enum`[T] = new `enum`[T](tpe, (x +: xs).toSet)
    def of[T](x: T, xs: T*)(implicit va: ValueAdapter[T], vs: ValueSchema[T]): `enum`[vs.S] = {
      new `enum`[vs.S](vs.schema, (x +: xs).toSet.map { (x: T) => va.adapt(x) })
    }
  }

  // +------------
  // | OneOf
  // +---------------
  //
  final case class `oneof`[T](subTypes: Set[Schema[_]], discriminationField: Option[String] = None) extends Schema[T] {
    type Self = `oneof`[T]
    def mkCopy()                              = new `oneof`[T](subTypes, discriminationField)
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`oneof`[_]]
    override def equals(obj: Any): Boolean = obj match {
      case `oneof`(s, d) => subTypes == s && discriminationField == d && super.equals(obj)
      case _             => false
    }
    override def jsonType: String = "oneof"
    override def toString: String = ToString { sb =>
      sb append "oneof("
      subTypes foreach { tpe =>
        sb append tpe
        sb append ","
      }
      sb.setLength(sb.length - 1) // drop last comma
      discriminationField foreach { f =>
        sb append "| discriminationField="
        sb append f
      }
      sb append ")"
    }
    def discriminatedBy(x: String): Self = new `oneof`[T](subTypes, Some(x))
  }
  object `oneof` {
    def of[T](x: Schema[_], xs: Schema[_]*): `oneof`[T] = new `oneof`[T]((x +: xs).toSet, None)
  }

  // +------------
  // | AllOf
  // +---------------
  //
  final case class `allof`[T](subTypes: Set[Schema[_]]) extends Schema[T] {
    type Self = `allof`[T]
    override def jsonType: String             = "allof"
    def mkCopy()                              = new `allof`[T](subTypes)
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`allof`[_]]
    override def equals(obj: Any): Boolean = obj match {
      case `allof`(s) => subTypes == s && super.equals(obj)
      case _          => false
    }
    override def toString: String = ToString { sb =>
      sb append "allof("
      subTypes foreach { tpe =>
        sb append tpe
        sb append ","
      }
      sb.setLength(sb.length - 1) // drop last comma
      sb append ")"
    }
  }
  object `allof` {
    def of[T](x: Schema[_], xs: Schema[_]*): `allof`[T] = new `allof`[T]((x +: xs).toSet)
  }

  // +------------
  // | Not
  // +---------------
  //
  final case class `not`[T](tpe: Schema[T]) extends Schema[T] {
    type Self = `not`[T]
    override def jsonType: String             = "not"
    def mkCopy()                              = new `not`[T](tpe)
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`not`[_]]
    override def equals(obj: Any): Boolean = obj match {
      case `not`(t) => tpe == t && super.equals(obj)
      case _        => false
    }
    override def toString: String = ToString { sb =>
      sb append "not("
      sb append tpe
      sb append ")"
    }
  }

  // +------------
  // | Def
  // +---------------
  //
  final case class `def`[T](sig: String, tpe: Schema[_]) extends Schema[T] {
    type Self = `def`[T]
    override def jsonType: String             = ??? // s"$$ref"
    def mkCopy()                              = new `def`[T](sig, tpe)
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`def`[_]]
    override def equals(obj: Any): Boolean = obj match {
      case `def`(s, t) => sig == s && tpe == t && super.equals(obj)
      case _           => false
    }
    override def toString: String = ToString { sb =>
      sb append "def(signature = "
      sb append sig
      sb append ", schema = "
      sb append tpe
      sb append ")"
    }
    override def withValidation[TT >: T, B](v: V.Def[B, _], vs: V.Def[B, _]*)(implicit
      bound: V.Magnet[TT, B]
    ): `def`[T] = copy(tpe = tpe.asInstanceOf[Schema[TT]].withValidation(v, vs: _*)).withExtraFrom(this)
    override def toDefinition[TT >: T](sig: String): `def`[TT] = {
      def deepCopy(x: Schema[_]): Schema[_] = {
        val y = x match {
          case `object`(fields)     => `object`(fields.map { f => f.copy(tpe = deepCopy(f.tpe)) })
          case `array`(y, u)        => `array`(deepCopy(y), u)
          case `dictionary`(y)      => `dictionary`(deepCopy(y))
          case `oneof`(ys, df)      => `oneof`(ys map deepCopy, df)
          case `allof`(ys)          => `allof`(ys map deepCopy)
          case `not`(y)             => `not`(deepCopy(y))
          case `def`(s, y)          => `def`(sig, deepCopy(y))
          case `ref`(s) if s == sig => `ref`(sig)
          case y                    => y
        }
        y withExtraFrom x
      }

      copy(sig = sig, tpe = deepCopy(tpe)).withExtraFrom(this)
    }
  }

  object `def` {
    def adapt[T](tpe: Schema[_], sig: => String): `def`[T] = {
      tpe match {
        case `def`(originalSig, innerTpe) => `def`(originalSig, innerTpe)
        case `value-class`(innerTpe)      => `def`(sig, innerTpe)
        case _                            => `def`(sig, tpe)
      }
    }
  }

  // +------------
  // | Value-Class
  // +---------------
  // Pseudo member, doesn't have it's own representation in resulted schema
  //
  final case class `value-class`[O, I](tpe: Schema[I]) extends Schema[O] {
    type Self = `value-class`[O, I]
    override def jsonType: String             = tpe.jsonType
    def mkCopy()                              = new `value-class`[O, I](tpe)
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`value-class`[_, _]]
    override def equals(obj: Any): Boolean = obj match {
      case `value-class`(t) => tpe == t && super.equals(obj)
      case _                => false
    }
    override def toString: String = ToString { sb =>
      sb append "value-class("
      sb append tpe
      sb append ")"
    }
    override def toDefinition[TT >: O](sig: String): `def`[TT] =
      `def`[TT](sig, tpe.withValidationsAddedFrom(this))
  }

  // +------------
  // | LazyRef
  // +---------------
  // Pseudo member, doesn't have it's own representation in resulted schema
  //
  final case class `ref`[T](sig: String) extends Schema[T] {
    override type Self = `ref`[T]
    override protected def mkCopy(): `ref`[T] = `ref`(sig)
    override def jsonType: String =
      ??? // should never call this. instead calling code should interpret it as `ref`
    override def toString: String = ToString { sb =>
      sb append "ref("
      sb append sig
      sb append ")"
    }
  }

  // +------------
  // | Const
  // +---------------
  // It is used in conjunction with OneOf as Alternative to Enum
  // INTERNAL API
  final case class `const`[T](value: Value) extends Schema[T] {
    override def jsonType: String = "const"
    override type Self = `const`[T]
    override protected def mkCopy(): `const`[T] = `const`(value)
    override def toString: String = ToString { sb =>
      sb append "const("
      sb append JsonFormatter.format(value)
      sb append ": "
      sb append value.tpe
      sb append ")"
    }
    override def canEqual(that: Any): Boolean = that.isInstanceOf[`const`[_]]
    override def equals(obj: Any): Boolean = obj match {
      case `const`(v) => value == v && super.equals(obj)
      case _          => false
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy