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 |