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

discovery.Codegen.scala Maven / Gradle / Ivy

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

import cats.Traverse
import cats.data.Writer
import org.typelevel.paiges.Doc
import org.typelevel.paiges.Document.ops._

import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Path}
import scala.collection.compat._

object Codegen {
  case class SourceFile(pkg: String, name: String, imports: List[String], body: String) {

    def render =
      s"""|package $pkg
          |
          |${imports.map("import " + _).mkString("\n")}
          |
          |$body
          |""".stripMargin

    def writeTo(basedir: Path) = {
      val packageDir = pkg.split("\\.").foldLeft(basedir)(_ resolve _)
      val file = packageDir.resolve(s"${name}.scala")
      Files.createDirectories(packageDir)
      Files.write(file, render.getBytes(StandardCharsets.UTF_8))
      file
    }
  }

  def generateFromDiscovery(packageName: String, discovery: Discovery) = {
    val static = Codegen.jsonInstances(packageName) :: Codegen.abstractClient(
      packageName) :: Codegen.googleError(packageName) :: Nil
    val clients = ClientCodegen
      .clientsFrom(discovery)
      .map(c =>
        Codegen.SourceFile(
          packageName,
          c.name,
          c.imports,
          c.toCode
        ))

    val models = discovery.schemas.view
      .filterKeys(!Set("JsonObject", "JsonValue").contains(_))
      .toList
      .flatMap { case (name, schema) =>
        Codegen.mkSchema(name, schema)
      }
      .map { generatedType =>
        Codegen.SourceFile(
          packageName,
          generatedType.name,
          generatedType.imports,
          generatedType.doc.render(80)
        )
      }
    static ::: models ::: clients
  }

  def mkSchema(name: String, schema: Schema): List[GeneratedType] = {
    type F[A] = Writer[List[GeneratedType], A]

    if (schema.properties.forall(_.isEmpty)) {
      List(
        CaseClass(
          name,
          List(
            Parameter("value", Type("JsonObject"), None, false, default = Some(Doc.text("None"))))))
    } else {
      Traverse[List]
        .traverse[F, (String, Schema), Parameter](schema.properties.toList.flatMap(_.toList)) {
          case (propertyName, property) =>
            mkSchemaProperty(name, propertyName, property)
        }
        .flatMap(parameters => Writer.tell(CaseClass(name, parameters) :: Nil))
        .written
    }
  }

  def mkSchemaProperty(
      parentName: String,
      name: String,
      property: Schema): Writer[List[GeneratedType], Parameter] =
    mkSchemaPropertyType(parentName, name, property).map { t =>
      Parameter(
        name,
        t,
        property.description,
        required = false,
        default = Some(Doc.text("None"))
      )
    }

  def mkSchemaPropertyType(
      parentName: String,
      name: String,
      property: Schema): Writer[List[GeneratedType], Type] = {

    val primitive = property.format
      .collect {
        case "int32" => Type("Int")
        case "int64" | "uint32" =>
          if (property.description.exists(_.contains("millis")))
            Type.importedType("scala.concurrent.duration.FiniteDuration")
          else
            Type("Long")
        case "uint64" => Type("BigInt")
        case "double" => Type("Double")
        case "byte" => Type.importedType("scodec.bits.ByteVector")
      }
      .orElse(property.`type`.filter(_ => property.`enum`.isEmpty).collect {
        case "string" => Type("String")
        case "boolean" => Type("Boolean")
        case "integer" => Type("Int")
        case "number" => Type("Double")
      })
      .map(Writer(List.empty[GeneratedType], _))

    val array = property.`type`.collect { case "array" =>
      property.items.map { p =>
        mkSchemaPropertyType(parentName, inflector.singularize(name).capitalize, p).map { t =>
          Type.list(t)
        }
      }
    }.flatten

    val ref = property.`$ref`.map(_.split("/").last)
      .map {
        case "JsonValue" => Type.apply("Json")
        case "JsonObject" => Type.apply("JsonObject")
        case x => Type.apply(x)
      }
      .map(Writer(List.empty[GeneratedType], _))

    val obj = property.`type`.collect { case "object" =>
      property.properties
        .filter(_.nonEmpty)
        .map { p =>
          val schemaName = s"$parentName${name.capitalize}"
          Writer(
            mkSchema(schemaName, Schema(properties = Some(p))),
            Type.apply(schemaName)
          )
        }
        .orElse(property.additionalProperties.map { p =>
          mkSchemaPropertyType(parentName, name.capitalize, p).map(t =>
            Type.map(Type.apply("String"), t))
        })
        .getOrElse(Writer(List.empty[GeneratedType], Type.apply("JsonObject")))
    }

    val enumType: Option[Writer[List[GeneratedType], Type]] = property.`enum`.map { enums =>
      val typeName = s"$parentName${name.capitalize}"
      Writer(
        EnumType(typeName, enums, property.enumDescriptions.getOrElse(Nil)) :: Nil,
        Type.apply(typeName))
    }

    primitive
      .orElse(enumType)
      .orElse(ref)
      .orElse(array)
      .orElse(obj)
      .getOrElse(Writer(List.empty[GeneratedType], Type.apply("Json")))
  }

  def jsonInstances(packageName: String) = SourceFile(
    packageName,
    "JsonInstances",
    List("io.circe._", "scala.concurrent.duration._", "scodec.bits._"), {
      val last = packageName.lastIndexOf('.')
      val simpleName = if (last != -1) packageName.substring(last + 1) else packageName

      s"""|private[$simpleName] object JsonInstances {
        |  implicit val durationEncoder: Encoder[FiniteDuration] = Encoder[Long].contramap(_.toMillis)
        |  implicit val durationDecoder: Decoder[FiniteDuration] = Decoder[Long].map(_.millis)
        |
        |  implicit val byteVectorEncoder: Encoder[ByteVector] = Encoder[String].contramap(_.toBase64)
        |  implicit val byteVectorDecoder: Decoder[ByteVector] = Decoder[String].emap(bv => ByteVector.fromBase64Descriptive(bv))
        |}
        |""".stripMargin
    }
  )

  def abstractClient(packageName: String): SourceFile = SourceFile(
    packageName,
    "AbstractClient",
    List(
      "cats.syntax.all._",
      "cats.effect.Concurrent",
      "io.circe._",
      "org.http4s._",
      "org.http4s.client.Client"),
    """abstract class AbstractClient[F[_]](client: Client[F])(implicit
      |    F: Concurrent[F]) {
      |  private implicit def entityDecoder[A: Decoder]: EntityDecoder[F, A] =
      |    org.http4s.circe.jsonOf[F, A]
      |  private implicit def entityEncoder[A: Encoder]: EntityEncoder[F, A] =
      |    org.http4s.circe.jsonEncoderOf[F, A]
      |
      |  protected def request(uri: Uri, method: Method) =
      |    Request[F](uri = uri, method = method)
      |
      |  protected def requestWithBody[A: Encoder](uri: Uri, method: Method)(input: A) =
      |    Request[F](uri = uri, method = method).withEntity(input)
      |
      |  def expectJson[A: Decoder](req: Request[F]) =
      |    client.expectOr[A](req) { res =>
      |      res
      |        .as[GoogleError]
      |        .attempt
      |        .flatMap(err =>
      |          F.raiseError(
      |            err
      |              .fold(
      |                err => GoogleError(Some(res.status.code), Option(err.getMessage), Nil, Nil),
      |                identity)
      |          ))
      |    }
      |}
      |
      |""".stripMargin
  )

  def googleError(packageName: String): SourceFile = SourceFile(
    packageName,
    "GoogleError",
    List("io.circe.Decoder"),
    """case class GoogleError(
      |    code: Option[Int],
      |    message: Option[String],
      |    errors: List[GoogleError.ErrorInfo],
      |    details: List[GoogleError.Details]
      |) extends Exception(message.getOrElse(""))
      |
      |object GoogleError {
      |  final case class ErrorInfo(
      |      domain: Option[String],
      |      reason: Option[String],
      |      message: Option[String],
      |      location: Option[String],
      |      locationType: Option[String]
      |  )
      |
      |  object ErrorInfo {
      |    implicit val decoder: Decoder[ErrorInfo] =
      |      Decoder.forProduct5("domain", "reason", "message", "location", "locationType")(apply)
      |  }
      |
      |  final case class Details(
      |      `type`: Option[String],
      |      reason: Option[String],
      |      parameterViolations: List[ParameterViolation])
      |  object Details {
      |    implicit val decoder: Decoder[Details] = Decoder.instance(c =>
      |      for {
      |        t <- c.get[Option[String]]("@type")
      |        r <- c.get[Option[String]]("reason")
      |        v <- c.get[Option[List[ParameterViolation]]]("parameterViolations")
      |      } yield Details(t, r, v.getOrElse(Nil)))
      |  }
      |
      |  final case class ParameterViolation(parameter: Option[String], description: Option[String])
      |
      |  object ParameterViolation {
      |    implicit val decoder: Decoder[ParameterViolation] =
      |      Decoder.forProduct2("parameter", "description")(apply)
      |  }
      |
      |  implicit val decoder: Decoder[GoogleError] = Decoder
      |    .instance(c =>
      |      for {
      |        code <- c.get[Option[Int]]("code")
      |        message <- c.get[Option[String]]("message")
      |        errors <- c.get[Option[List[ErrorInfo]]]("errors")
      |        details <- c.get[Option[List[Details]]]("details")
      |      } yield GoogleError(code, message, errors.getOrElse(Nil), details.getOrElse(Nil)))
      |    .at("error")
      |}
      |""".stripMargin
  )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy