Library Support

SCRAML allows generating supporting types for a number of libraries:

cats

sourcelazy val root = (project in file("."))
  .settings(
    scalaVersion := "2.13.16",
    crossScalaVersions ++= Seq("3.3.4"),
    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.10"

lazy val root = (project in file("."))
  .settings(
    name := "scraml-json-test",
    scalaVersion := "2.13.16",
    crossScalaVersions ++= Seq("3.3.4"),
    version := "0.1",
    ramlFile := Some(file("api/json.raml")),
    basePackageName := "scraml",
    librarySupport := Set(scraml.libs.CirceJsonSupport(
      // formats = Map("localDateTime" -> "io.circe.Decoder.decodeLocalDateTime"),
      imports = Seq("io.circe.Decoder.decodeLocalDateTime") // alternative to formats to provide custom codecs via import
    )),
    defaultEnumVariant := Some("Unknown"),
    Compile / sourceGenerators += runScraml,
    libraryDependencies ++= (CrossVersion.partialVersion(scalaVersion.value) match {
      case Some((2, 13)) => Seq("com.commercetools" %% "sphere-json" % "0.12.5")
      case _             => Seq()
    }),
    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.16",
    crossScalaVersions ++= Seq("3.3.4"),
    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.10"
val refinedVersion = "0.11.3"

lazy val root = (project in file("."))
  .settings(
    scalaVersion := "2.13.16",
    crossScalaVersions ++= Seq("3.3.4"),
    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(imports = Seq("io.circe.Decoder.decodeLocalDateTime")),
      scraml.libs.RefinedSupport
    ),
    Compile / sourceGenerators += runScraml,
    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"
    ).map(_ % circeVersion),
    libraryDependencies += "io.circe" %% "circe-refined" % "0.15.1",
  )

Sphere JSON

sourceval circeVersion = "0.14.10"

lazy val root = (project in file("."))
  .settings(
    scalaVersion := "2.13.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 := "3.3.4"

val circeVersion = "0.14.10"
val tapirVersion = "1.11.9"

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-pekko-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,
    Compile / scalacOptions ++= Seq("-Xmax-inlines", "128")
  )

Interpreting tapir endpoints

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

sourcepackage examples

import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.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.pekkohttp.PekkoHttpServerInterpreter
  import scala.concurrent.Future
  import org.apache.pekko.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 =
    PekkoHttpServerInterpreter().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
}

bean properties

Unlike the above library support components, the bean properties library support instructs scraml to emit JavaBeans method definitions and does not require use of an external library. Instead, it can enable scraml generated types to have methods Java-based libraries often expect.

sourceval refinedVersion = "0.11.3"

lazy val root = (project in file("."))
  .settings(
    scalaVersion := "2.13.16",
    crossScalaVersions ++= Seq("3.3.4"),
    name := "scraml-bean-java-types-test",
    version := "0.1",
    ramlFile := Some(file("api/bean.raml")),
    ramlFieldMatchPolicy := scraml.FieldMatchPolicy.Exact(),
    librarySupport := Set(
      scraml.libs.BeanPropertiesSupport,
      scraml.libs.RefinedSupport
      ),
    beanProperties := scraml.BeanProperties(
      anyVal = scraml.BeanProperties.UseJavaLangTypes,
      array = scraml.BeanProperties.UseJavaCollectionTypes,
      optional = scraml.BeanProperties.UseJavaOptionalType,
      scalaNumber = scraml.BeanProperties.UseJavaLangTypes
    ),
    Compile / sourceGenerators += runScraml,
    libraryDependencies ++= Seq(
      "eu.timepit" %% "refined",
      "eu.timepit" %% "refined-cats"
    ).map(_ % refinedVersion),
  )

Including scraml.libs.BeanPropertiesSupport in the librarySupport definition enables bean property generation. Further customization is specified in the optional scraml.BeanProperties value.

Each scraml.BeanProperties configuration option is detailed here.

anyVal

The anyVal configuration setting determines what, if any, transformations are applied to Scala AnyVal types (such as Int, Double, etc.).

BeanProperties.UseJavaLangTypes

When enabled, Scala AnyVal types are converted into their java.lang equivalent.

Scala Type Bean Definition Type
Byte java.lang.Byte
Char java.lang.Character
Double java.lang.Double
Float java.lang.Float
Int java.lang.Integer
Long java.lang.Long
Short java.lang.Short

BeanProperties.Unchanged

No transformations are applied to the Scala type.

array

The array configuration setting determines what, if any, transformation is applied to the configured DefaultTypes.array type.

BeanProperties.UseJavaCollectionTypes

When enabled, convert the DefaultTypes.array type to java.lang.List. Only Seq-based types are currently supported.

BeanProperties.Unchanged

No transformations are applied to the Scala type.

evaluate

The evaluate configuration setting determines whether each bean method will be evaluated every time it is invoked or only the first time.

BeanProperties.EveryInvocation

Evaluate each time the property is used (def).

BeanProperties.Once

Evaluate only the first time the property is used (lazy val).

optional

The optional configuration setting determines what, if any, transformation is applied to scala.Option properties.

BeanProperties.UseJavaOptionalType

When enabled, convert scala.Option to java.lang.Optional.

BeanProperties.UseNullableReturnType

When enabled, convert all scala.Option properties to return the equivalent AnyRef representation. When the original property isEmpty, null is returned.

NOTE: This option automatically enables BeanProperties.UseJavaLangTypes for anyVal.

BeanProperties.Unchanged

No transformations are applied to the Scala type.

scalaNumber

The scalaNumber configuration setting determines what, if any, transformation is applied to scala.math properties.

Scala Type Bean Definition Type
BigDecimal java.math.BigDecimal
BigInt java.math.BigInt
The source code for this page can be found here.