Library Support

SCRAML allows to generate supporting types for a number of libraries:

cats

sourcelazy val root = (project in file("."))
  .settings(
    scalaVersion := "2.13.8",
    name := "scraml-cats-test",
    version := "0.1",
    ramlFile := Some(file("api/simple.raml")),
    librarySupport := Set(
      scraml.libs.CatsEqSupport,
      scraml.libs.CatsShowSupport,
      scraml.libs.MonocleOpticsSupport
    ),
    Compile / sourceGenerators += runScraml,
    libraryDependencies ++= Seq(
      "org.typelevel" %% "cats-core" % "2.6.1",
      "dev.optics" %% "monocle-core" % "3.0.0",
    )
  )

circe

sourceval circeVersion = "0.14.2"

lazy val root = (project in file("."))
  .settings(
    name := "scraml-json-test",
    scalaVersion := "2.12.16",
    version := "0.1",
    ramlFile := Some(file("api/json.raml")),
    basePackageName := "scraml",
    librarySupport := Set(scraml.libs.CirceJsonSupport(formats = Map("localDateTime" -> "io.circe.Decoder.decodeLocalDateTime"))),
    defaultEnumVariant := Some("Unknown"),
    Compile / sourceGenerators += runScraml,
    libraryDependencies += "com.commercetools" %% "sphere-json" % "0.12.5",
    libraryDependencies ++= Seq(
        "io.circe" %% "circe-core",
        "io.circe" %% "circe-generic",
        "io.circe" %% "circe-parser"
    ).map(_ % circeVersion)
  )

monocle

sourcelazy val root = (project in file("."))
  .settings(
    scalaVersion := "2.13.8",
    name := "scraml-cats-test",
    version := "0.1",
    ramlFile := Some(file("api/simple.raml")),
    librarySupport := Set(
      scraml.libs.CatsEqSupport,
      scraml.libs.CatsShowSupport,
      scraml.libs.MonocleOpticsSupport
    ),
    Compile / sourceGenerators += runScraml,
    libraryDependencies ++= Seq(
      "org.typelevel" %% "cats-core" % "2.6.1",
      "dev.optics" %% "monocle-core" % "3.0.0",
    )
  )

refined (with cats and circe)

sourceimport scraml.FieldMatchPolicy._

val circeVersion = "0.14.1"
val refinedVersion = "0.9.27"

lazy val root = (project in file("."))
  .settings(
    scalaVersion := "2.13.8",
    name := "scraml-refined-test",
    version := "0.1",
    ramlFile := Some(file("api/refined.raml")),
    defaultTypes := scraml.DefaultTypes(long = "scala.math.BigInt"),
    // Override the default field match policy.
    ramlFieldMatchPolicy := MatchInOrder(
      // Generate exact field matching code except for these types.
      // Note that the exclusion set includes types which the next
      // policy is configured to exclude as well.  This allows them
      // to "fall through" to the last policy.
      Exact(
        excluding = Set(
          "ChildInheritsAll",
          "ChildOverridesAll",
          "DataType",
          "NoProps"
        )
      ) ::
      // Generate field matching code which ignores properties not
      // explicitly defined in the RAML *and* not matched above,
      // unless "excluding" matches.
      IgnoreExtra(
        excluding = Set(
          "DataType",
          "NoProps"
        )
      ) ::
      // If the above policies don't match, then try this one (which
      // will always match as its "excluding" Set is empty.
      KeepExtra() ::
      Nil
    ),
    librarySupport := Set(
      scraml.libs.CatsEqSupport,
      scraml.libs.CatsShowSupport,
      scraml.libs.CirceJsonSupport(
        formats = Map(
          "localDateTime" -> "io.circe.Decoder.decodeLocalDateTime"
        )
      ),
      scraml.libs.RefinedSupport
    ),
    Compile / sourceGenerators += runScraml,
    libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.7",
    libraryDependencies ++= Seq(
      "eu.timepit" %% "refined",
      "eu.timepit" %% "refined-cats"
    ).map(_ % refinedVersion),
    libraryDependencies ++= Seq(
        "io.circe" %% "circe-core",
        "io.circe" %% "circe-generic",
        "io.circe" %% "circe-parser",
        "io.circe" %% "circe-refined"
    ).map(_ % circeVersion)
  )

Sphere JSON

sourceval circeVersion = "0.14.1"

lazy val root = (project in file("."))
  .settings(
    scalaVersion := "2.12.16",
    name := "scraml-ct-api-sphere-test",
    version := "0.1",
    ramlFile := Some(file("reference/api-specs/api/api.raml")),
    basePackageName := "de.commercetools.api",
    librarySupport := Set(scraml.libs.SphereJsonSupport),
    Compile / sourceGenerators += runScraml,
    libraryDependencies += "com.commercetools" %% "sphere-json" % "0.12.5"
  )

tapir

the tapir support generating endpoint values:

Example tapir build.sbt

sourcescalaVersion := "2.13.14"

val circeVersion = "0.14.7"
val tapirVersion = "1.10.7"

lazy val examples = (project in file("."))
  .settings(
    name := "sbt-scraml-examples",

    libraryDependencies ++= Seq(
      "io.circe" %% "circe-core",
      "io.circe" %% "circe-generic",
      "io.circe" %% "circe-parser"
    ).map(_ % circeVersion),

    libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-core" % tapirVersion,
    libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion,
    libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % tapirVersion,
    libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % tapirVersion,
    libraryDependencies += "com.softwaremill.sttp.client3" %% "async-http-client-backend-cats" % "3.9.6",

    libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.4",

    ramlFile := Some(file("../src/sbt-test/sbt-scraml/simple/api/simple.raml")),
    ramlFieldMatchPolicy := scraml.FieldMatchPolicy.Exact(),
    basePackageName := "scraml.examples",
    librarySupport := Set(scraml.libs.CirceJsonSupport(), scraml.libs.TapirSupport("Endpoints")),
    Compile / sourceGenerators += runScraml
  )

Interpreting tapir endpoints

usage of the generated Endpoints.Greeting.getGreeting type from the previous api example:

sourcepackage examples

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import cats.effect.{ExitCode, IO, IOApp}
import scraml.examples.{DataType, Endpoints, SomeEnum}
import scraml.examples.Endpoints.Greeting.GetGreetingParams
import sttp.client3.SttpBackend
import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend
import sttp.model.{Header, StatusCode}
import sttp.tapir.DecodeResult.{Failure, Value}
import sttp.tapir.client.sttp.WebSocketToPipe

import scala.concurrent.Future

class GreetingClient(apiUrl: String)(backend: SttpBackend[IO, Any])(implicit wsToPipe: WebSocketToPipe[Any]) {
  import sttp.client3._
  import sttp.tapir._
  import sttp.tapir.client.sttp.SttpClientInterpreter

  private lazy val client = SttpClientInterpreter()
  private def authenticate: IO[String] = IO.pure("sometoken")

  def getGreeting(params: GetGreetingParams): IO[DataType] = for {
    accessToken <- authenticate
    // adding an input and output to the endpoint to access headers and to provide an access token (checking not implemented)
    response <- client.toClient(Endpoints.Greeting.getGreeting.in(auth.bearer[String]()).out(headers), Some(uri"$apiUrl"), backend)(wsToPipe)(params, accessToken)
    result <- response match {
      case Value(value) => IO.fromEither(value.left.map(error => new RuntimeException(s"error in $response: $error")))
      case error: Failure => IO.raiseError(new RuntimeException(s"error while getting greeting: $error"))
    }
    (data, headers) = result
    _ <- IO(println(s"got headers: $headers"))
  } yield data
}

object GreetingClient {
  def apply(apiUrl: String): IO[(GreetingClient, SttpBackend[IO, Any])] =
    AsyncHttpClientCatsBackend[IO]().flatMap(backend => IO((new GreetingClient(apiUrl)(backend), backend)))
}

object GreetingServer {
  import sttp.tapir._
  import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter
  import scala.concurrent.Future
  import akka.http.scaladsl.server.Route

  def getGreeting(params: GetGreetingParams): Future[Either[Unit, (DataType, StatusCode, List[Header])]] =
    Future.successful(Right((DataType(params.name.getOrElse("no input"), customTypeProp = BigDecimal(42)), StatusCode.Ok, List(Header("custom-header", "value")))))

  implicit val httpSystem: ActorSystem = ActorSystem("http")

  import httpSystem.dispatcher

  // adding outputs to provide statusCode and headers in the implementation
  val greetingWithStatusAndHeaders = Endpoints.Greeting.getGreeting.out(statusCode and sttp.tapir.headers)

  val greetingRoute: Route =
    AkkaHttpServerInterpreter().toRoute(greetingWithStatusAndHeaders.serverLogic(getGreeting))

  def startServer: IO[Http.ServerBinding] =
    IO.fromFuture(IO(Http().newServerAt("localhost", 8080).bind(greetingRoute)))
}

object GreetingApp extends IOApp {
  implicit class FutureOps[T](future: => Future[T]) {
    def toIO: IO[T] = IO.fromFuture(IO(future))
  }

  override def run(args: List[String]): IO[ExitCode] = for {
    binding <- GreetingServer.startServer
    (clientWithBackend) <- GreetingClient(
      apiUrl = s"http://${binding.localAddress.getHostName}:${binding.localAddress.getPort}"
    )
    (client, clientBackend) = clientWithBackend
    result <- client.getGreeting(GetGreetingParams(enum_type = SomeEnum.A, name = Some("world"))).attempt
    _ <- IO(println(result))
    _ <-
      clientBackend
        .close()
        .guarantee(binding.unbind().toIO.void)
        .guarantee(GreetingServer.httpSystem.terminate().toIO.void)
  } yield ExitCode.Success
}
The source code for this page can be found here.