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

endpoints4s.openapi.model.OpenApi.scala Maven / Gradle / Ivy

There is a newer version: 5.0.1
Show newest version
package endpoints4s.openapi.model

import java.io.Serializable

import endpoints4s.{Encoder, Hashing}
import endpoints4s.algebra.{ExternalDocumentationObject, Tag}

import scala.collection.mutable

/**
  * @see [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md]]
  * @note Throws an exception on creation if several tags have the same name but not the same other attributes.
  */
@throws(classOf[IllegalArgumentException])
final class OpenApi private (
    val info: Info,
    val paths: Map[String, PathItem],
    val components: Components
) extends Serializable {

  override def toString =
    s"OpenApi($info, $paths, $components)"

  override def equals(other: Any): Boolean =
    other match {
      case that: OpenApi =>
        info == that.info && paths == that.paths && components == that.components
      case _ => false
    }

  override def hashCode(): Int = Hashing.hash(info, paths, components)

  val tags: Set[Tag] = OpenApi.extractTags(paths)

  private[this] def copy(
      info: Info = info,
      paths: Map[String, PathItem] = paths,
      components: Components = components
  ): OpenApi =
    new OpenApi(info, paths, components)

  def withInfo(info: Info): OpenApi =
    copy(info = info)

  def withPaths(paths: Map[String, PathItem]): OpenApi =
    copy(paths = paths)

  def withComponents(components: Components): OpenApi =
    copy(components = components)
}

object OpenApi {

  val openApiVersion = "3.0.0"

  def apply(info: Info, paths: Map[String, PathItem], components: Components) =
    new OpenApi(info, paths, components)

  private def mapJson[A](map: Map[String, A])(f: A => ujson.Value): ujson.Obj =
    new ujson.Obj(mutable.LinkedHashMap(map.iterator.map {
      case (k, v) => (k, f(v))
    }.toSeq: _*))

  private[openapi] def schemaJson(schema: Schema): ujson.Obj = {
    val fields = mutable.LinkedHashMap.empty[String, ujson.Value]
    schema match {
      case primitive: Schema.Primitive =>
        fields += "type" -> ujson.Str(primitive.name)
        primitive.format.foreach(s => fields += "format" -> ujson.Str(s))
      case obj: Schema.Object =>
        fields ++= List(
          "type" -> "object",
          "properties" -> new ujson.Obj(
            mutable.LinkedHashMap(
              obj.properties.map(p =>
                p.name -> schemaJson(
                  p.schema.withDefinedDescription(p.description)
                )
              ): _*
            )
          )
        )
        val required = obj.properties.filter(_.isRequired).map(_.name)
        if (required.nonEmpty) {
          fields += "required" -> ujson.Arr(required.map(ujson.Str(_)): _*)
        }
        obj.additionalProperties.foreach(p => fields += "additionalProperties" -> schemaJson(p))
      case array: Schema.Array =>
        val itemsSchema = array.elementType match {
          case Left(value)  => schemaJson(value)
          case Right(value) => ujson.Arr(value.map(schemaJson): _*)
        }
        fields ++= List(
          "type" -> "array",
          "items" -> itemsSchema
        )
      case enm: Schema.Enum =>
        fields ++= schemaJson(
          enm.elementType.withDefinedDescription(enm.description)
        ).value
        fields += "enum" -> ujson.Arr(enm.values: _*)
      case oneOf: Schema.OneOf =>
        fields ++=
          (oneOf.alternatives match {
            case discAlternatives: Schema.DiscriminatedAlternatives =>
              val mappingFields: mutable.LinkedHashMap[String, ujson.Value] =
                mutable.LinkedHashMap(discAlternatives.alternatives.collect {
                  case (tag, ref: Schema.Reference) =>
                    tag -> ujson.Str(Schema.Reference.toRefPath(ref.name))
                }: _*)
              val discFields = mutable.LinkedHashMap.empty[String, ujson.Value]
              discFields += "propertyName" -> ujson.Str(
                discAlternatives.discriminatorFieldName
              )
              if (mappingFields.nonEmpty) {
                discFields += "mapping" -> new ujson.Obj(mappingFields)
              }
              List(
                "oneOf" -> ujson
                  .Arr(
                    discAlternatives.alternatives
                      .map(kv => schemaJson(kv._2)): _*
                  ),
                "discriminator" -> ujson.Obj(discFields)
              )
            case enumAlternatives: Schema.EnumeratedAlternatives =>
              List(
                "oneOf" -> ujson
                  .Arr(enumAlternatives.alternatives.map(schemaJson): _*)
              )
          })
      case allOf: Schema.AllOf =>
        fields += "allOf" -> ujson.Arr(allOf.schemas.map(schemaJson): _*)
      case reference: Schema.Reference =>
        fields += "$ref" -> ujson.Str(
          Schema.Reference.toRefPath(reference.name)
        )
    }
    for (description <- schema.description) {
      fields += "description" -> ujson.Str(description)
    }
    for (example <- schema.example) {
      fields += "example" -> example
    }
    for (title <- schema.title) {
      fields += "title" -> title
    }
    new ujson.Obj(fields)
  }

  private def securitySchemeJson(securityScheme: SecurityScheme): ujson.Obj = {
    val fields = mutable.LinkedHashMap[String, ujson.Value](
      "type" -> ujson.Str(securityScheme.`type`)
    )
    for (description <- securityScheme.description) {
      fields += "description" -> ujson.Str(description)
    }
    for (name <- securityScheme.name) {
      fields += "name" -> ujson.Str(name)
    }
    for (in <- securityScheme.in) {
      fields += "in" -> ujson.Str(in)
    }
    for (scheme <- securityScheme.scheme) {
      fields += "scheme" -> ujson.Str(scheme)
    }
    for (bearerFormat <- securityScheme.bearerFormat) {
      fields += "bearerFormat" -> ujson.Str(bearerFormat)
    }
    new ujson.Obj(fields)
  }

  private def infoJson(info: Info): ujson.Obj = {
    val fields: mutable.LinkedHashMap[String, ujson.Value] =
      mutable.LinkedHashMap(
        "title" -> ujson.Str(info.title),
        "version" -> ujson.Str(info.version)
      )
    info.description.foreach(description => fields += "description" -> ujson.Str(description))
    ujson.Obj(fields)
  }

  private def componentsJson(components: Components): ujson.Obj =
    ujson.Obj(
      "schemas" -> mapJson(components.schemas)(schemaJson),
      "securitySchemes" -> mapJson(components.securitySchemes)(
        securitySchemeJson
      )
    )

  private def responseJson(response: Response): ujson.Obj = {
    val fields = mutable.LinkedHashMap[String, ujson.Value](
      "description" -> ujson.Str(response.description)
    )
    if (response.headers.nonEmpty) {
      fields += "headers" -> mapJson(response.headers)(responseHeaderJson)
    }
    if (response.content.nonEmpty) {
      fields += "content" -> mapJson(response.content)(mediaTypeJson)
    }
    new ujson.Obj(fields)
  }

  def responseHeaderJson(responseHeader: ResponseHeader): ujson.Value = {
    val fields = mutable.LinkedHashMap[String, ujson.Value](
      "schema" -> schemaJson(responseHeader.schema)
    )
    if (responseHeader.required) {
      fields += "required" -> ujson.True
    }
    responseHeader.description.foreach { description =>
      fields += "description" -> ujson.Str(description)
    }
    new ujson.Obj(fields)
  }

  def mediaTypeJson(mediaType: MediaType): ujson.Value =
    mediaType.schema match {
      case Some(schema) => ujson.Obj("schema" -> schemaJson(schema))
      case None         => ujson.Obj()
    }

  private def operationJson(operation: Operation): ujson.Obj = {
    val fields = mutable.LinkedHashMap[String, ujson.Value](
      "responses" -> mapJson(operation.responses)(responseJson)
    )
    operation.operationId.foreach { id =>
      fields += "operationId" -> ujson.Str(id)
    }
    operation.summary.foreach { summary =>
      fields += "summary" -> ujson.Str(summary)
    }
    operation.description.foreach { description =>
      fields += "description" -> ujson.Str(description)
    }
    if (operation.parameters.nonEmpty) {
      fields += "parameters" -> ujson.Arr(
        operation.parameters.map(parameterJson): _*
      )
    }
    operation.requestBody.foreach { requestBody =>
      fields += "requestBody" -> requestBodyJson(requestBody)
    }
    if (operation.tags.nonEmpty) {
      fields += "tags" -> ujson.Arr(
        operation.tags.map(tag => ujson.Str(tag.name)): _*
      )
    }
    if (operation.security.nonEmpty) {
      fields += "security" -> ujson.Arr(
        operation.security.map(securityRequirementJson): _*
      )
    }
    if (operation.callbacks.nonEmpty) {
      fields += "callbacks" -> mapJson(operation.callbacks)(pathsJson)
    }
    if (operation.deprecated) {
      fields += "deprecated" -> ujson.True
    }
    new ujson.Obj(fields)
  }

  private def parameterJson(parameter: Parameter): ujson.Value = {
    val fields = mutable.LinkedHashMap[String, ujson.Value](
      "name" -> ujson.Str(parameter.name),
      "in" -> inJson(parameter.in),
      "schema" -> schemaJson(parameter.schema)
    )
    parameter.description.foreach { description =>
      fields += "description" -> ujson.Str(description)
    }
    if (parameter.required) {
      fields += "required" -> ujson.True
    }
    new ujson.Obj(fields)
  }

  private def inJson(in: In): ujson.Value =
    in match {
      case In.Query  => ujson.Str("query")
      case In.Path   => ujson.Str("path")
      case In.Header => ujson.Str("header")
      case In.Cookie => ujson.Str("cookie")
    }

  private def requestBodyJson(body: RequestBody): ujson.Value = {
    val fields = mutable.LinkedHashMap[String, ujson.Value](
      "content" -> mapJson(body.content)(mediaTypeJson)
    )
    body.description.foreach { description =>
      fields += "description" -> ujson.Str(description)
    }
    new ujson.Obj(fields)
  }

  private def tagJson(tag: Tag): ujson.Value = {
    val fields: mutable.LinkedHashMap[String, ujson.Value] =
      mutable.LinkedHashMap(
        "name" -> ujson.Str(tag.name)
      )

    if (tag.description.nonEmpty) {
      fields += "description" -> tag.description.get
    }
    if (tag.externalDocs.nonEmpty) {
      fields += "externalDocs" -> externalDocumentationObjectJson(
        tag.externalDocs.get
      )
    }
    new ujson.Obj(fields)
  }

  private def externalDocumentationObjectJson(
      externalDoc: ExternalDocumentationObject
  ): ujson.Value = {
    val fields: mutable.LinkedHashMap[String, ujson.Value] =
      mutable.LinkedHashMap(
        "url" -> ujson.Str(externalDoc.url)
      )

    if (externalDoc.description.nonEmpty)
      fields += "description" -> externalDoc.description.get
    new ujson.Obj(fields)
  }

  private def securityRequirementJson(
      securityRequirement: SecurityRequirement
  ): ujson.Value =
    ujson.Obj(
      securityRequirement.name -> ujson.Arr(
        securityRequirement.scopes.map(ujson.Str): _*
      )
    )

  private def pathsJson(paths: Map[String, PathItem]): ujson.Obj =
    mapJson(paths)(pathItem => mapJson(pathItem.operations)(operationJson))

  private val jsonEncoder: Encoder[OpenApi, ujson.Value] =
    openApi => {
      val fields: mutable.LinkedHashMap[String, ujson.Value] =
        mutable.LinkedHashMap(
          "openapi" -> ujson.Str(openApiVersion),
          "info" -> infoJson(openApi.info),
          "paths" -> pathsJson(openApi.paths)
        )
      if (openApi.tags.nonEmpty) {
        val tagsAsJson = openApi.tags.map(tag => tagJson(tag)).toList
        fields += "tags" -> ujson.Arr(tagsAsJson: _*)
      }
      if (openApi.components.schemas.nonEmpty || openApi.components.securitySchemes.nonEmpty) {
        fields += "components" -> componentsJson(openApi.components)
      }
      new ujson.Obj(fields)
    }

  private def extractTags(paths: Map[String, PathItem]): Set[Tag] = {
    val allTags = paths.flatMap {
      case (_, pathItem) =>
        pathItem.operations.map {
          case (_, operation) =>
            operation.tags
        }
    }.flatten

    val tagsByName = allTags.groupBy(_.name)
    tagsByName.foreach {
      case (_, listOfTags) =>
        val set = listOfTags.toSet
        if (set.size > 1) {
          throw new IllegalArgumentException(
            s"Found tags with the same name but different values: $set"
          )
        }
    }

    // Note that tags without any additional information will still be shown. However there is no
    // reason to add these tags to the root since tags with only names can and will be defined at
    // the moment they will be used in the endpoint descriptions themselves.
    allTags
      .filter(tag => tag.description.nonEmpty || tag.externalDocs.nonEmpty)
      .toSet
  }

  implicit val stringEncoder: Encoder[OpenApi, String] =
    openApi => jsonEncoder.encode(openApi).transform(ujson.StringRenderer()).toString

}

final class Info private (
    val title: String,
    val version: String,
    val description: Option[String]
) extends Serializable {

  override def toString: String =
    s"Info($title, $version, $description)"

  override def equals(other: Any): Boolean =
    other match {
      case that: Info =>
        title == that.title && version == that.version && description == that.description
      case _ => false
    }

  override def hashCode(): Int =
    Hashing.hash(title, version, description)

  private[this] def copy(
      title: String = title,
      version: String = version,
      description: Option[String] = description
  ): Info =
    new Info(title, version, description)

  def withTitle(title: String): Info =
    copy(title = title)

  def withVersion(version: String): Info =
    copy(version = version)

  def withDescription(description: Option[String]): Info =
    copy(description = description)

}

object Info {

  def apply(title: String, version: String): Info =
    new Info(title, version, None)

}

final class PathItem private (
    val operations: Map[String, Operation]
) extends Serializable {

  override def toString =
    s"PathItem($operations)"

  override def equals(other: Any): Boolean =
    other match {
      case that: PathItem => operations == that.operations
      case _              => false
    }

  override def hashCode(): Int =
    Hashing.hash(operations)

  def withOperations(operations: Map[String, Operation]): PathItem =
    PathItem(operations)

}

object PathItem {

  def apply(operations: Map[String, Operation]): PathItem =
    new PathItem(operations)

}

final class Components private (
    val schemas: Map[String, Schema],
    val securitySchemes: Map[String, SecurityScheme]
) extends Serializable {

  override def toString: String =
    s"Components($schemas, $securitySchemes)"

  override def equals(other: Any): Boolean =
    other match {
      case that: Components =>
        schemas == that.schemas && securitySchemes == that.securitySchemes
      case _ => false
    }

  override def hashCode(): Int =
    Hashing.hash(schemas, securitySchemes)

  private[this] def copy(
      schemas: Map[String, Schema] = schemas,
      securitySchemes: Map[String, SecurityScheme] = securitySchemes
  ) = new Components(schemas, securitySchemes)

  def withSchemas(schemas: Map[String, Schema]): Components =
    copy(schemas = schemas)

  def withSecuritySchemas(
      securitySchemes: Map[String, SecurityScheme]
  ): Components =
    copy(securitySchemes = securitySchemes)

}

object Components {

  def apply(
      schemas: Map[String, Schema],
      securitySchemes: Map[String, SecurityScheme]
  ): Components =
    new Components(schemas, securitySchemes)

}

final class Operation private (
    val operationId: Option[String],
    val summary: Option[String],
    val description: Option[String],
    val parameters: List[Parameter],
    val requestBody: Option[RequestBody],
    val responses: Map[String, Response],
    val tags: List[Tag],
    val security: List[SecurityRequirement],
    val callbacks: Map[String, Map[String, PathItem]],
    val deprecated: Boolean
) extends Serializable {

  override def toString: String =
    s"Operation($operationId, $summary, $description, $parameters, $requestBody, $responses, $tags, $security, $callbacks, $deprecated)"

  override def equals(other: Any): Boolean =
    other match {
      case that: Operation =>
        operationId == that.operationId && summary == that.summary && description == that.description && parameters == that.parameters &&
          requestBody == that.requestBody && responses == that.responses && tags == that.tags &&
          security == that.security && callbacks == that.callbacks && deprecated == that.deprecated
    }

  override def hashCode(): Int =
    Hashing.hash(
      operationId,
      summary,
      description,
      parameters,
      requestBody,
      responses,
      tags,
      security,
      callbacks,
      deprecated
    )

  private[this] def copy(
      id: Option[String] = operationId,
      summary: Option[String] = summary,
      description: Option[String] = description,
      parameters: List[Parameter] = parameters,
      requestBody: Option[RequestBody] = requestBody,
      responses: Map[String, Response] = responses,
      tags: List[Tag] = tags,
      security: List[SecurityRequirement] = security,
      callbacks: Map[String, Map[String, PathItem]] = callbacks,
      deprecated: Boolean = deprecated
  ): Operation =
    Operation(
      id,
      summary,
      description,
      parameters,
      requestBody,
      responses,
      tags,
      security,
      callbacks,
      deprecated
    )

  def withOperationId(operationId: Option[String]): Operation =
    copy(id = operationId)

  def withSummary(summary: Option[String]): Operation =
    copy(summary = summary)

  def withDescription(description: Option[String]): Operation =
    copy(description = description)

  def withParameters(parameters: List[Parameter]): Operation =
    copy(parameters = parameters)

  def withRequestBody(requestBody: Option[RequestBody]): Operation =
    copy(requestBody = requestBody)

  def withResponses(responses: Map[String, Response]): Operation =
    copy(responses = responses)

  def withTags(tags: List[Tag]): Operation =
    copy(tags = tags)

  def withSecurity(security: List[SecurityRequirement]): Operation =
    copy(security = security)

  def withCallbacks(callbacks: Map[String, Map[String, PathItem]]): Operation =
    copy(callbacks = callbacks)

  def withDeprecated(deprecated: Boolean): Operation =
    copy(deprecated = deprecated)
}

object Operation {

  def apply(
      id: Option[String],
      summary: Option[String],
      description: Option[String],
      parameters: List[Parameter],
      requestBody: Option[RequestBody],
      responses: Map[String, Response],
      tags: List[Tag],
      security: List[SecurityRequirement],
      callbacks: Map[String, Map[String, PathItem]],
      deprecated: Boolean
  ): Operation =
    new Operation(
      id,
      summary,
      description,
      parameters,
      requestBody,
      responses,
      tags,
      security,
      callbacks,
      deprecated
    )

}

final class SecurityRequirement private (
    val name: String,
    val scheme: SecurityScheme,
    val scopes: List[String]
) extends Serializable {

  override def toString: String =
    s"SecurityRequirement($name, $scheme, $scopes)"

  override def equals(other: Any): Boolean =
    other match {
      case that: SecurityRequirement =>
        name == that.name && scheme == that.scheme && scopes == that.scopes
      case _ => false
    }

  override def hashCode(): Int =
    Hashing.hash(name, scheme, scopes)

  private[this] def copy(
      name: String = name,
      scheme: SecurityScheme = scheme,
      scopes: List[String] = scopes
  ): SecurityRequirement =
    new SecurityRequirement(name, scheme, scopes)

  def withName(name: String): SecurityRequirement =
    copy(name = name)

  def withScheme(scheme: SecurityScheme): SecurityRequirement =
    copy(scheme = scheme)

  def withScopes(scopes: List[String]): SecurityRequirement =
    copy(scopes = scopes)
}

object SecurityRequirement {

  def apply(
      name: String,
      scheme: SecurityScheme
  ): SecurityRequirement =
    new SecurityRequirement(name, scheme, Nil)

  @deprecated(
    "Use `SecurityRequirement().withScopes(...)` instead of `SecurityRequirement(scopes = ...)`",
    "1.0.0"
  )
  def apply(
      name: String,
      scheme: SecurityScheme,
      scopes: List[String] = Nil
  ): SecurityRequirement =
    new SecurityRequirement(name, scheme, scopes)
}

final class RequestBody private (
    val description: Option[String],
    val content: Map[String, MediaType]
) extends Serializable {
  assert(content.nonEmpty)

  override def toString: String =
    s"RequestBody($description, $content)"

  override def equals(other: Any): Boolean =
    other match {
      case that: RequestBody =>
        description == that.description && content == that.content
      case _ => false
    }

  override def hashCode(): Int =
    Hashing.hash(description, content)

  private[this] def copy(
      description: Option[String] = description,
      content: Map[String, MediaType] = content
  ): RequestBody =
    new RequestBody(description, content)

  def withDescription(description: Option[String]): RequestBody =
    copy(description = description)

  def withContent(content: Map[String, MediaType]): RequestBody =
    copy(content = content)
}

object RequestBody {

  def apply(description: Option[String], content: Map[String, MediaType]) =
    new RequestBody(description, content)

}

final class Response private (
    val description: String,
    val headers: Map[String, ResponseHeader],
    val content: Map[String, MediaType]
) extends Serializable {

  override def toString: String =
    s"Response($description, $headers, $content)"

  override def equals(other: Any): Boolean =
    other match {
      case that: Response =>
        description == that.description && headers == that.headers && content == that.content
      case _ => false
    }

  override def hashCode(): Int =
    Hashing.hash(description, headers, content)

  private[this] def copy(
      description: String = description,
      headers: Map[String, ResponseHeader] = headers,
      content: Map[String, MediaType] = content
  ): Response =
    new Response(description, headers, content)

  def withDescription(description: String): Response =
    copy(description = description)

  def withHeaders(headers: Map[String, ResponseHeader]): Response =
    copy(headers = headers)

  def withContent(content: Map[String, MediaType]): Response =
    copy(content = content)
}

object Response {

  def apply(
      description: String,
      headers: Map[String, ResponseHeader],
      content: Map[String, MediaType]
  ): Response =
    new Response(description, headers, content)

}

// Note: request headers don’t need a dedicated class because they are modeled as `Parameter`s
final class ResponseHeader private (
    val required: Boolean,
    val description: Option[String],
    val schema: Schema
) extends Serializable {

  override def toString: String =
    s"ResponseHeader($required, $description, $schema)"

  override def equals(other: Any): Boolean =
    other match {
      case that: ResponseHeader =>
        required == that.required && description == that.description && schema == that.schema
      case _ => false
    }

  override def hashCode(): Int =
    Hashing.hash(required, description, schema)

  private def copy(
      required: Boolean = required,
      description: Option[String] = description,
      schema: Schema = schema
  ): ResponseHeader =
    new ResponseHeader(required, description, schema)

  def withRequired(required: Boolean): ResponseHeader =
    copy(required = required)

  def withDescription(description: Option[String]): ResponseHeader =
    copy(description = description)

  def withSchema(schema: Schema): ResponseHeader =
    copy(schema = schema)

}

object ResponseHeader {

  def apply(
      required: Boolean,
      description: Option[String],
      schema: Schema
  ): ResponseHeader =
    new ResponseHeader(required, description, schema)

}

final class Parameter private (
    val name: String,
    val in: In,
    val required: Boolean,
    val description: Option[String],
    val schema: Schema // not specified in openapi spec but swagger-editor breaks without it for path parameters
) extends Serializable {

  override def toString: String =
    s"Parameter($name, $in, $required, $description, $schema)"

  override def equals(other: Any): Boolean =
    other match {
      case that: Parameter =>
        name == that.name && in == that.in && required == that.required &&
          description == that.description && schema == that.schema
      case _ => false
    }

  override def hashCode(): Int =
    Hashing.hash(name, in, required, description, schema)

  private[this] def copy(
      name: String = name,
      in: In = in,
      required: Boolean = required,
      description: Option[String] = description,
      schema: Schema = schema
  ): Parameter =
    new Parameter(name, in, required, description, schema)

  def withName(name: String): Parameter =
    copy(name = name)

  def withIn(in: In): Parameter =
    copy(in = in)

  def withDescription(description: Option[String]): Parameter =
    copy(description = description)

  def withSchema(schema: Schema): Parameter =
    copy(schema = schema)

}

object Parameter {

  def apply(
      name: String,
      in: In,
      required: Boolean,
      description: Option[String],
      schema: Schema
  ): Parameter =
    new Parameter(name, in, required, description, schema)

}

sealed trait In

object In {
  case object Query extends In
  case object Path extends In
  case object Header extends In
  case object Cookie extends In

  // All the possible values.
  val values: Seq[In] = Query :: Path :: Header :: Cookie :: Nil
}

final class MediaType private (val schema: Option[Schema]) extends Serializable {

  override def toString: String =
    s"Mediatype($schema)"

  override def equals(other: Any): Boolean =
    other match {
      case that: MediaType => schema == that.schema
      case _               => false
    }

  override def hashCode(): Int =
    Hashing.hash(schema)

  def withSchema(schema: Option[Schema]): MediaType = new MediaType(schema)
}

object MediaType {

  def apply(schema: Option[Schema]): MediaType = new MediaType(schema)

}

sealed trait Schema {
  def description: Option[String]
  def example: Option[ujson.Value]
  def title: Option[String]

  /**
    * @return The same schema with its description overridden by the given `description`,
    *         or stay unchanged if this one is empty.
    */
  def withDefinedDescription(description: Option[String]): Schema =
    this match {
      case s: Schema.Object =>
        s.withDescription(description.orElse(s.description))
      case s: Schema.Array =>
        s.withDescription(description.orElse(s.description))
      case s: Schema.Enum =>
        s.withDescription(description.orElse(s.description))
      case s: Schema.Primitive =>
        s.withDescription(description.orElse(s.description))
      case s: Schema.OneOf =>
        s.withDescription(description.orElse(s.description))
      case s: Schema.AllOf =>
        s.withDescription(description.orElse(s.description))
      case s: Schema.Reference =>
        s.withDescription(description.orElse(s.description))
    }
}

object Schema {

  final class Object private (
      val properties: List[Property],
      val additionalProperties: Option[Schema],
      val description: Option[String],
      val example: Option[ujson.Value],
      val title: Option[String]
  ) extends Schema
      with Serializable {

    override def toString: String =
      s"Object($properties, $additionalProperties, $description, $example, $title)"

    override def equals(other: Any): Boolean =
      other match {
        case that: Object =>
          properties == that.properties && additionalProperties == that.additionalProperties &&
            description == that.description && example == that.example &&
            title == that.title
        case _ => false
      }

    override def hashCode(): Int =
      Hashing.hash(
        properties,
        additionalProperties,
        description,
        example,
        title
      )

    private[this] def copy(
        properties: List[Property] = properties,
        additionalProperties: Option[Schema] = additionalProperties,
        description: Option[String] = description,
        example: Option[ujson.Value] = example,
        title: Option[String] = title
    ): Object =
      new Object(properties, additionalProperties, description, example, title)

    def withProperty(properties: List[Property]): Object =
      copy(properties = properties)

    def withAdditionalProperties(additionalProperties: Option[Schema]): Object =
      copy(additionalProperties = additionalProperties)

    def withDescription(description: Option[String]): Object =
      copy(description = description)

    def withExample(example: Option[ujson.Value]): Object =
      copy(example = example)

    def withTitle(title: Option[String]): Object =
      copy(title = title)
  }

  object Object {

    def apply(
        properties: List[Property],
        additionalProperties: Option[Schema],
        description: Option[String],
        example: Option[ujson.Value],
        title: Option[String]
    ): Object =
      new Object(properties, additionalProperties, description, example, title)

  }

  final class Array private (
      val elementType: Either[Schema, List[Schema]],
      val description: Option[String],
      val example: Option[ujson.Value],
      val title: Option[String]
  ) extends Schema
      with Serializable {

    override def toString: String =
      s"Array($elementType, $description, $example, $title)"

    override def equals(other: Any): Boolean =
      other match {
        case that: Array =>
          elementType == that.elementType && description == that.description && example == that.example && title == that.title
        case _ => false
      }

    override def hashCode(): Int =
      Hashing.hash(elementType, description, example, title)

    private[this] def copy(
        elementType: Either[Schema, List[Schema]] = elementType,
        description: Option[String] = description,
        example: Option[ujson.Value] = example,
        title: Option[String] = title
    ): Array =
      new Array(elementType, description, example, title)

    def withElementType(elementType: Either[Schema, List[Schema]]): Array =
      copy(elementType = elementType)

    def withDescription(description: Option[String]): Array =
      copy(description = description)

    def withExample(example: Option[ujson.Value]): Array =
      copy(example = example)

    def withTitle(title: Option[String]): Array =
      copy(title = title)

  }

  object Array {

    def apply(
        elementType: Either[Schema, List[Schema]],
        description: Option[String],
        example: Option[ujson.Value],
        title: Option[String]
    ): Array =
      new Array(elementType, description, example, title)

  }

  final class Enum private (
      val elementType: Schema,
      val values: List[ujson.Value],
      val description: Option[String],
      val example: Option[ujson.Value],
      val title: Option[String]
  ) extends Schema
      with Serializable {

    override def toString: String =
      s"Enum($elementType, $values, $description, $example, $title)"

    override def equals(other: Any): Boolean =
      other match {
        case that: Enum =>
          elementType == that.elementType && values == that.values && description == that.description &&
            example == that.example && title == that.title
        case _ => false
      }

    override def hashCode(): Int =
      Hashing.hash(elementType, values, description, example, title)

    private[this] def copy(
        elementType: Schema = elementType,
        values: List[ujson.Value] = values,
        description: Option[String] = description,
        example: Option[ujson.Value] = example,
        title: Option[String] = title
    ): Enum =
      new Enum(elementType, values, description, example, title)

    def withElementType(elementType: Schema): Enum =
      copy(elementType = elementType)

    def withValues(values: List[ujson.Value]): Enum =
      copy(values = values)

    def withDescription(description: Option[String]): Enum =
      copy(description = description)

    def withExample(example: Option[ujson.Value]): Enum =
      copy(example = example)

    def withTitle(title: Option[String]): Enum =
      copy(title = title)
  }

  object Enum {

    def apply(
        elementType: Schema,
        values: List[ujson.Value],
        description: Option[String],
        example: Option[ujson.Value],
        title: Option[String]
    ): Enum = new Enum(elementType, values, description, example, title)

  }

  final class Property private (
      val name: String,
      val schema: Schema,
      val isRequired: Boolean,
      val description: Option[String]
  ) extends Serializable {

    override def toString: String =
      s"Property($name, $schema, $isRequired, $description)"

    override def equals(other: Any): Boolean =
      other match {
        case that: Property =>
          name == that.name && schema == that.schema && isRequired == that.isRequired &&
            description == that.description
        case _ => false
      }

    override def hashCode(): Int =
      Hashing.hash(name, schema, isRequired, description)

    private[this] def copy(
        name: String = name,
        schema: Schema = schema,
        isRequired: Boolean = isRequired,
        description: Option[String] = description
    ): Property =
      new Property(name, schema, isRequired, description)

    def withName(name: String): Property =
      copy(name = name)

    def withSchema(schema: Schema): Property =
      copy(schema = schema)

    def withIsRequired(isRequired: Boolean): Property =
      copy(isRequired = isRequired)

    def withDescription(description: Option[String]): Property =
      copy(description = description)
  }

  object Property {

    def apply(
        name: String,
        schema: Schema,
        isRequired: Boolean,
        description: Option[String]
    ): Property = new Property(name, schema, isRequired, description)

  }

  final class Primitive private (
      val name: String,
      val format: Option[String],
      val description: Option[String],
      val example: Option[ujson.Value],
      val title: Option[String]
  ) extends Schema
      with Serializable {

    override def toString: String =
      s"Primitive($name, $format, $description, $example, $title)"

    override def equals(other: Any): Boolean =
      other match {
        case that: Primitive =>
          name == that.name && format == that.format && description == that.description &&
            example == that.example && title == that.title
        case _ => false
      }

    override def hashCode(): Int =
      Hashing.hash(name, format, description, example, title)

    private[this] def copy(
        name: String = name,
        format: Option[String] = format,
        description: Option[String] = description,
        example: Option[ujson.Value] = example,
        title: Option[String] = title
    ): Primitive =
      new Primitive(name, format, description, example, title)

    def withName(name: String): Primitive =
      copy(name = name)

    def withFormat(format: Option[String]): Primitive =
      copy(format = format)

    def withDescription(description: Option[String]): Primitive =
      copy(description = description)

    def withExample(example: Option[ujson.Value]): Primitive =
      copy(example = example)

    def withTitle(title: Option[String]): Primitive =
      copy(title = title)

  }

  object Primitive {

    def apply(
        name: String,
        format: Option[String],
        description: Option[String],
        example: Option[ujson.Value],
        title: Option[String]
    ): Primitive =
      new Primitive(name, format, description, example, title)

  }

  final class OneOf private (
      val alternatives: Alternatives,
      val description: Option[String],
      val example: Option[ujson.Value],
      val title: Option[String]
  ) extends Schema
      with Serializable {

    override def toString: String =
      s"OneOf($alternatives, $description, $example, $title)"

    override def equals(other: Any): Boolean =
      other match {
        case that: OneOf =>
          alternatives == that.alternatives && description == that.description &&
            example == that.example && title == that.title
        case _ => false
      }

    override def hashCode(): Int =
      Hashing.hash(alternatives, description, example, title)

    private[this] def copy(
        alternatives: Alternatives = alternatives,
        description: Option[String] = description,
        example: Option[ujson.Value] = example,
        title: Option[String] = title
    ): OneOf =
      new OneOf(alternatives, description, example, title)

    def withAlternatives(alternatives: Alternatives): OneOf =
      copy(alternatives = alternatives)

    def withDescription(description: Option[String]): OneOf =
      copy(description = description)

    def withExample(example: Option[ujson.Value]): OneOf =
      copy(example = example)

    def withTitle(title: Option[String]): OneOf =
      copy(title = title)

  }

  object OneOf {

    def apply(
        alternatives: Alternatives,
        description: Option[String],
        example: Option[ujson.Value],
        title: Option[String]
    ): OneOf = new OneOf(alternatives, description, example, title)

  }

  sealed trait Alternatives

  final class DiscriminatedAlternatives private (
      val discriminatorFieldName: String,
      val alternatives: List[(String, Schema)]
  ) extends Alternatives
      with Serializable {

    override def toString: String =
      s"DiscriminatedAlternatives($discriminatorFieldName, $alternatives)"

    override def equals(other: Any): Boolean =
      other match {
        case that: DiscriminatedAlternatives =>
          discriminatorFieldName == that.discriminatorFieldName && alternatives == that.alternatives
        case _ =>
          false
      }

    override def hashCode(): Int =
      Hashing.hash(discriminatorFieldName, alternatives)

    private[this] def copy(
        discriminatorFieldName: String = discriminatorFieldName,
        alternatives: List[(String, Schema)] = alternatives
    ): DiscriminatedAlternatives =
      new DiscriminatedAlternatives(discriminatorFieldName, alternatives)

    def withDiscriminatorFieldName(
        discriminiatorFieldName: String
    ): DiscriminatedAlternatives =
      copy(discriminatorFieldName = discriminiatorFieldName)

    def withAlternatives(
        alternatives: List[(String, Schema)]
    ): DiscriminatedAlternatives =
      copy(alternatives = alternatives)
  }

  object DiscriminatedAlternatives {

    def apply(
        discriminatorFieldName: String,
        alternatives: List[(String, Schema)]
    ): DiscriminatedAlternatives =
      new DiscriminatedAlternatives(discriminatorFieldName, alternatives)

  }

  final class EnumeratedAlternatives private (
      val alternatives: List[Schema]
  ) extends Alternatives
      with Serializable {

    override def toString: String =
      s"EnumeratedAlternatives($alternatives)"

    override def equals(other: Any): Boolean =
      other match {
        case that: EnumeratedAlternatives => alternatives == that.alternatives
        case _                            => false
      }

    override def hashCode(): Int =
      Hashing.hash(alternatives)

    def withAlternatives(alternatives: List[Schema]): EnumeratedAlternatives =
      new EnumeratedAlternatives(alternatives)
  }

  object EnumeratedAlternatives {

    def apply(alternatives: List[Schema]): EnumeratedAlternatives =
      new EnumeratedAlternatives(alternatives)

  }

  final class AllOf private (
      val schemas: List[Schema],
      val description: Option[String],
      val example: Option[ujson.Value],
      val title: Option[String]
  ) extends Schema
      with Serializable {

    override def toString: String =
      s"AllOf($schemas, $description, $example, $title)"

    override def equals(other: Any): Boolean =
      other match {
        case that: AllOf =>
          schemas == that.schemas && description == that.description &&
            example == that.example && title == that.title
        case _ => false
      }

    override def hashCode(): Int =
      Hashing.hash(schemas, description, example, title)

    private[this] def copy(
        schemas: List[Schema] = schemas,
        description: Option[String] = description,
        example: Option[ujson.Value] = example,
        title: Option[String] = title
    ): AllOf =
      new AllOf(schemas, description, example, title)

    def withSchemas(schemas: List[Schema]): AllOf =
      copy(schemas = schemas)

    def withDescription(description: Option[String]): AllOf =
      copy(description = description)

    def withExample(example: Option[ujson.Value]): AllOf =
      copy(example = example)

    def withTitle(title: Option[String]): AllOf =
      copy(title = title)

  }

  object AllOf {

    def apply(
        schemas: List[Schema],
        description: Option[String],
        example: Option[ujson.Value],
        title: Option[String]
    ): AllOf = new AllOf(schemas, description, example, title)

  }

  final class Reference private (
      val name: String,
      val original: Option[Schema],
      val description: Option[String]
  ) extends Schema
      with Serializable {

    override val example: None.type = None // Reference objects can’t have examples
    override val title: None.type = None // Reference objects can’t have a title

    override def toString: String =
      s"Reference($name, $original, $description)"

    override def equals(other: Any): Boolean =
      other match {
        case that: Reference =>
          name == that.name && original == that.original && description == that.description
        case _ => false
      }

    override def hashCode(): Int =
      Hashing.hash(name, original, description, example, title)

    private[this] def copy(
        name: String = name,
        original: Option[Schema] = original,
        description: Option[String] = description
    ) = new Reference(name, original, description)

    def withName(name: String): Reference = copy(name = name)

    def withOriginal(original: Option[Schema]): Reference =
      copy(original = original)

    def withDescription(description: Option[String]): Reference =
      copy(description = description)

  }

  object Reference {

    def apply(
        name: String,
        original: Option[Schema],
        description: Option[String]
    ): Reference = new Reference(name, original, description)

    def toRefPath(name: String): String =
      s"#/components/schemas/$name"
  }

  val simpleUUID = Primitive("string", format = Some("uuid"), None, None, None)
  val simpleString = Primitive("string", None, None, None, None)
  val simpleInteger = Primitive("integer", None, None, None, None)
  val simpleBoolean = Primitive("boolean", None, None, None, None)
  val simpleNumber = Primitive("number", None, None, None, None)

}

final class SecurityScheme private (
    val `type`: String, // TODO This should be a sealed trait, the `type` field should only exist in the JSON representation
    val description: Option[String],
    val name: Option[String],
    val in: Option[String], // TODO Create a typed enumeration
    val scheme: Option[String],
    val bearerFormat: Option[String]
) extends Serializable {

  override def toString: String =
    s"SecurityScheme(${`type`}, $description, $name, $in, $scheme, $bearerFormat)"

  override def equals(other: Any): Boolean =
    other match {
      case that: SecurityScheme =>
        `type` == that.`type` && description == that.description && name == that.name &&
          in == that.in && scheme == that.scheme && bearerFormat == that.bearerFormat
      case _ =>
        false
    }

  override def hashCode(): Int =
    Hashing.hash(`type`, description, name, in, scheme, bearerFormat)

  private[this] def copy(
      `type`: String = `type`,
      description: Option[String] = description,
      name: Option[String] = name,
      in: Option[String] = in,
      scheme: Option[String] = scheme,
      bearerFormat: Option[String] = bearerFormat
  ) = new SecurityScheme(`type`, description, name, in, scheme, bearerFormat)

  def withType(tpe: String): SecurityScheme =
    copy(`type` = tpe)

  def withDescription(description: Option[String]): SecurityScheme =
    copy(description = description)

  def withName(name: Option[String]): SecurityScheme =
    copy(name = name)

  def withIn(in: Option[String]): SecurityScheme =
    copy(in = in)

  def withScheme(scheme: Option[String]): SecurityScheme =
    copy(scheme = scheme)

  def withBearerFormat(bearerFormat: Option[String]): SecurityScheme =
    copy(bearerFormat = bearerFormat)

}

object SecurityScheme {

  def apply(
      `type`: String,
      description: Option[String],
      name: Option[String],
      in: Option[String],
      scheme: Option[String],
      bearerFormat: Option[String]
  ): SecurityScheme =
    new SecurityScheme(`type`, description, name, in, scheme, bearerFormat)

  def httpBasic: SecurityScheme =
    SecurityScheme(
      `type` = "http",
      description = Some("Http Basic Authentication"),
      name = None,
      in = None,
      scheme = Some("basic"),
      bearerFormat = None
    )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy