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

discovery.CaseClass.scala Maven / Gradle / Ivy

There is a newer version: 0.6.2
Show newest version
package discovery

import org.typelevel.paiges.{Doc, Document}
import org.typelevel.paiges.Document.ops._

sealed trait Type extends Product with Serializable {
  def asDoc: Doc
  def asString = asDoc.render(80)
}

object Type {
  def apply(in: String): Type = Simple(in)
  def importedType(in: String): Type = Imported(in, in.substring(in.lastIndexOf('.') + 1))
  def constructor(outer: Type, inner: Type*): Type = Constructor(outer, inner.toList)

  def list(typ: Type): Type = constructor(apply("List"), typ)
  def option(typ: Type): Type = constructor(apply("Option"), typ)
  def map(left: Type, right: Type): Type = constructor(apply("Map"), left, right)
  def encoder(typ: Type): Type = constructor(apply("Encoder"), typ)
  def decoder(typ: Type): Type = constructor(apply("Decoder"), typ)

  case class Simple(name: String) extends Type {
    val asDoc = Doc.text(name)
  }

  case class Constructor(outer: Type, elemTypes: List[Type]) extends Type {

    override def asDoc: Doc = Doc
      .intercalate(Doc.comma + Doc.lineOrSpace, elemTypes.map(_.asDoc))
      .tightBracketBy(outer.asDoc + Code.lbracket, Code.rbracket)
  }

  case class Imported(qualified: String, name: String) extends Type {
    def asDoc = Doc.text(name)
  }

  def findImports(param: Type, imports: List[String]): List[String] = param match {
    case Type.Constructor(outer, elemTypes) =>
      findImports(outer, elemTypes.flatMap(findImports(_, imports)))
    case Type.Imported(fqcn, _) => fqcn :: imports
    case _ => imports
  }
}

sealed trait GeneratedType {
  def name: String
  def imports: List[String]
}

object GeneratedType {
  implicit val renderer: Document[GeneratedType] = Document.instance {
    case cc: CaseClass => CaseClass.renderer.document(cc)
    case e: EnumType => EnumType.renderer.document(e)
    case w: JsonObjectWrapper => JsonObjectWrapper.renderer.document(w)
  }

}

case class TypeClassInstance(name: String, `type`: Type, body: Doc)
object TypeClassInstance {
  implicit val renderer: Document[TypeClassInstance] = Document.instance(tci =>
    Code.assigment(
      Code.ascribed(Doc.text("implicit val ") + Code.term(tci.name), tci.`type`.asDoc),
      tci.body))
}

case class EnumType(name: String, cases: List[String], descriptions: List[String])
    extends GeneratedType {

  override def imports: List[String] = List("io.circe._")
}

object EnumType {
  implicit val renderer: Document[EnumType] =
    Document.instance { enumType =>
      def toObjectName(_case: String) = _case.toUpperCase.replaceAll("\\W", "_")

      val cls = Doc.text(
        s"sealed abstract class ${enumType.name}(val value: String) extends Product with Serializable")

      val objects =
        Doc.intercalate(
          Doc.hardLine,
          enumType.cases.zip(enumType.descriptions).map { case (k, v) =>
            val obj =
              Doc.text(s"case object ${toObjectName(k)} extends ${enumType.name}") + Doc
                .text(k)
                .tightBracketBy(Code.lparens + Code.quote, Code.quote + Code.rparens, 0)
            val comment = Code.blockComment(v)
            comment + obj
          }
        )

      val companionPrefix = Doc.text(s"object ${enumType.name} {")

      val values = Doc
        .intercalate(Doc.comma + Doc.line, enumType.cases.map(toObjectName).map(Doc.text))
        .tightBracketBy(
          Doc.text("val values = List") + Code.lparens,
          Code.rparens
        )

      val enumAsType = Type(enumType.name)
      val fromString =
        Code.Def(
          "fromString",
          Nil,
          List(Parameter("input", Type("String"), None, required = true)),
          Some(Type.constructor(Type("Either"), Type("String"), enumAsType)), {
            val msg = Code.interpolate(
              "s",
              Doc.text(s"'$$input' was not a valid value for ${enumType.name}"))
            Code.termSelect(
              Code.termSelect(Code.term("values"), Doc.text("find(_.value == input)")),
              Code.termApply(Doc.text("toRight"), Nil, msg)
            )
          }
        )

      val decoderInstance =
        TypeClassInstance(
          "decoder",
          Type.decoder(enumAsType),
          Doc.text("Decoder[String].emap(s => fromString(s))"))
      val encoderInstance =
        TypeClassInstance(
          "encoder",
          Type.encoder(enumAsType),
          Doc.text("Encoder[String].contramap(_.value)"))

      val body =
        Doc.intercalate(
          Doc.hardLine,
          List(objects, values, fromString, decoderInstance.doc, encoderInstance.doc))

      val companion = body.tightBracketBy(companionPrefix, Doc.hardLine + Code.rbrace)

      Doc.intercalate(Doc.hardLine, List(cls, companion)) + Doc.hardLine
    }

}

case class JsonObjectWrapper(name: String) extends GeneratedType {
  override def imports: List[String] = List(
    "io.circe._"
  )
}

object JsonObjectWrapper {
  implicit val renderer: Document[JsonObjectWrapper] = (a: JsonObjectWrapper) => {
    val cc = Doc.text(s"final case class ${a.name}(value: JsonObject = JsonObject.empty)")
    val companion = {
      val start = Doc.text(s"object ${a.name} ")
      val encoder = TypeClassInstance(
        "encoder",
        Type.encoder(Type(a.name)),
        Doc.text("Encoder[JsonObject].contramap(_.value)"))
      val decoder = TypeClassInstance(
        "decoder",
        Type.decoder(Type(a.name)),
        Doc.text("Decoder[JsonObject].map(apply)"))
      start + Code.blocks(List(encoder.doc, decoder.doc))
    }

    cc + Doc.hardLine + companion
  }
}

case class CaseClass(
    name: String,
    parameters: List[Parameter]
) extends GeneratedType {
  val asType: Type = Type(name)

  def jsonInstancesImport =
    if (parameters.exists { p =>
        val string = p.`type`.asString
        string.contains("FiniteDuration") || string.contains("ByteVector")
      }) List("JsonInstances._")
    else Nil

  def imports =
    jsonInstancesImport ::: List("io.circe._", "io.circe.syntax._") ::: parameters
      .flatMap(p => Type.findImports(p.`type`, Nil))
      .distinct
      .reverse
}

object CaseClass {

  def renderClass(cc: CaseClass) = {
    val prefix = Doc.text(s"final case class ${cc.name}")
    val params = if (cc.parameters.isEmpty) Doc.text("()") else Code.paramsToDoc(cc.parameters)
    prefix + params
  }

  implicit val renderer: Document[CaseClass] =
    Document.instance { cc =>
      val render = renderClass(cc)

      def renderCompanion: Doc = {
        val prefix = Doc.text(s"object ${cc.name} ")
        prefix + Code.blocks(List(encoderInstance.doc, decoderInstance.doc))
      }

      def encoderInstance: TypeClassInstance = {
        val obj = Doc
          .intercalate(
            Doc.comma + Doc.lineOrSpace,
            cc.parameters.map(p =>
              p.literal + Doc.lineOrSpace + Doc.text(":=") + Doc.lineOrSpace + Code
                .termSelect(Doc.text("x"), p.term))
          )
          .tightBracketBy(Doc.text("Json.obj("), Code.rparens)

        TypeClassInstance(
          "encoder",
          Type.encoder(cc.asType),
          obj.tightBracketBy(Doc.text("Encoder.instance{ x =>"), Code.rbrace)
        )
      }

      def decoderInstance = {
        val withIndex = cc.parameters.zipWithIndex

        val forcomp =
          Code.forComprehension(
            withIndex.map { case (p, idx) =>
              Doc.text(s"v$idx") -> Code.termApply(
                Doc.text("c.get"),
                List(p.actualType),
                Code
                  .literal(Doc.text(p.name)))
            },
            Code.termApply(
              Doc.text(cc.name),
              Nil,
              Doc
                .intercalate(
                  Doc.comma + Doc.lineOrSpace,
                  withIndex.map { case (_, idx) => Doc.text(s"v$idx") }))
          )

        TypeClassInstance(
          "decoder",
          Type.decoder(cc.asType),
          forcomp.tightBracketBy(
            Doc.text("Decoder.instance{ c => "),
            Doc.hardLine + Code.rbrace
          )
        )
      }

      Doc.intercalate(Doc.hardLine, List(render, renderCompanion)) + Doc.hardLine
    }
}

case class Parameter(
    name: String,
    `type`: Type,
    description: Option[String],
    required: Boolean,
    default: Option[Doc] = None
) {
  def literal = Code.literal(Doc.text(name))
  def term = Code.term(name)
  def actualType = if (required) `type` else Type.option(`type`)
  def asDoc = actualType.asDoc
}

object Parameter {
  implicit val renderer: Document[Parameter] = Document.instance { p =>
    val comment = p.description.fold(Doc.empty)(Code.blockComment)
    val defaultOrEmpty = p.default
      .map(d => Doc.char('=').tightBracketBy(Doc.lineOrSpace, Doc.lineOrSpace) + d)
      .getOrElse(Doc.empty)
    comment + Code.ascribed(p.term, p.asDoc) + defaultOrEmpty
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy