smithy4play generates Play routes and controllers from your Smithy models using smithy4s. The example project smithy4playTest shows the current setup with Play 3, Scala 3.3.7, and smithy4s 0.18.46.
- Add sbt plugins
// project/plugins.sbt
addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.10")
addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % "0.18.46")- Enable plugins and dependencies
// build.sbt
lazy val myService = project
.in(file("my-service"))
.enablePlugins(Smithy4sCodegenPlugin, PlayScala)
.settings(
scalaVersion := "3.x.x",
libraryDependencies ++= Seq(
"de.innfactory" %% "smithy4play" % "0.5.0",
"de.innfactory" %% "smithy4play-mcp" % "0.5.0"
)
)- Define your Smithy models (see
smithy4playTest/testSpecs) and runsbt compileto generate service code underapp/specs.
Auto-routing with MCP (recommended)
- Configure packages in
conf/application.conf:smithy4play.autoRoutePackage = "controller"smithy4play.servicePackage = "your.namespace"
- Bind the router in
conf/routes:
-> / controller.TestRouter
- Implement the router by extending
AutoRouterWithMcpand optionally add middlewares:
@Singleton
class TestRouter @Inject() (implicit
mcpController: McpController,
cc: ControllerComponents,
app: Application,
ec: ExecutionContext,
config: Config
) extends AutoRouterWithMcp {
override def smithy4PlayMiddleware: Seq[Smithy4PlayMiddleware] =
super.smithy4PlayMiddleware ++ Seq(ValidateAuthMiddleware(), AddHeaderMiddleware())
}Manual binding
You can still wire controllers manually by extending BaseRouter and returning your controllers as Play Routes instances if you do not want auto-routing.
Extend the smithy4s-generated service trait with ContextRoute and mix in Controller to gain helpers:
@Singleton
class TestController @Inject() (implicit
cc: ControllerComponents,
ec: ExecutionContext,
ws: WSClient
) extends TestControllerService[ContextRoute]
with Controller {
override def test(): ContextRoute[SimpleTestResponse] = Kleisli { _ =>
EitherT.rightT[Future, Throwable](SimpleTestResponse(Some("ok")))
}
override def testWithOutput(
pathParam: String,
testQuery: String,
testHeader: String,
body: TestRequestBody
): ContextRoute[TestWithOutputResponse] = Kleisli { _ =>
EitherT.rightT[Future, Throwable](TestWithOutputResponse(TestResponseBody(testHeader, pathParam, testQuery, body.message)))
}
}Register cross-cutting logic by extending Smithy4PlayMiddleware and appending it in your router:
@Singleton
class ValidateAuthMiddleware @Inject() (implicit ec: ExecutionContext) extends Smithy4PlayMiddleware {
override def skipMiddleware(r: RoutingContext): Boolean = false
override def logic(r: RoutingContext, next: RoutingContext => RoutingResult[Result]): RoutingResult[Result] =
EitherT.leftT[Future, Result](UnauthorizedError("Unauthorized"))
}The MCP module exposes smithy-defined tools. Implement them like any controller:
@Singleton
class McpTestController @Inject() (implicit cc: ControllerComponents, ec: ExecutionContext, ws: WSClient)
extends McpControllerService[ContextRoute]
with Controller {
def reverseString(text: String, tagged: Option[TaggedTestUnion], untagged: Option[UntaggedTestUnion], discriminated: Option[DiscriminatedTestUnion]): ContextRoute[ReverseStringOutput] = Kleisli { _ =>
val reversed = text.reverse
EitherT.rightT[Future, Throwable](ReverseStringOutput(reversed, reversed.replaceAll("\\s", "").length * 2))
}
}Key settings used in smithy4playTest/conf/application.conf:
smithy4play.autoRoutePackage = "controller"
smithy4play.servicePackage = "testDefinitions.test"
play.http.parser.maxMemoryBuffer = 100MB
play.filters.enabled = []- Full sample project:
smithy4playTest - Generated Smithy specs:
smithy4playTest/testSpecs - Controllers:
smithy4playTest/app/controller - Middlewares:
smithy4playTest/app/controller/middlewares
For more, browse the sample code and run sbt test to execute the compliance tests.
