diff --git a/src/main/g8/build.sbt b/src/main/g8/build.sbt index 83621f60..fbb4ccf0 100644 --- a/src/main/g8/build.sbt +++ b/src/main/g8/build.sbt @@ -2,6 +2,9 @@ val akkaHttpVersion = "10.2.2" val akkaVersion = "2.6.10" val slickVersion = "3.3.3" val zioVersion = "1.0.3" +$if(add_cors.truthy)$ +val akkaHttpCorsVersion = "1.1.0" +$endif$ val zioLoggingVersion = "0.5.4" val zioConfigVersion = "1.0.0-RC31-1" val flywayVersion = "7.3.2" @@ -31,6 +34,9 @@ val root = (project in file(".")) name := "$name$", addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"), libraryDependencies ++= Seq( + $if(add_cors.truthy)$ + "ch.megard" %% "akka-http-cors" % akkaHttpCorsVersion, + $endif$ "com.typesafe.akka" %% "akka-http" % akkaHttpVersion, "de.heikoseeberger" %% "akka-http-play-json" % "1.35.2", "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion, diff --git a/src/main/g8/default.properties b/src/main/g8/default.properties index 8a0fcab2..4ef07d94 100644 --- a/src/main/g8/default.properties +++ b/src/main/g8/default.properties @@ -4,5 +4,6 @@ scala_version=2.13.4 add_caliban_endpoint=yes add_server_sent_events_endpoint=yes add_websocket_endpoint=yes +add_cors=yes organization=com.example package=$organization$ diff --git a/src/main/g8/src/main/scala/$package$/api/Api.scala b/src/main/g8/src/main/scala/$package$/api/Api.scala index 8c372ab5..bf25586a 100644 --- a/src/main/g8/src/main/scala/$package$/api/Api.scala +++ b/src/main/g8/src/main/scala/$package$/api/Api.scala @@ -1,9 +1,9 @@ package $package$.api import akka.event.Logging._ -import akka.http.scaladsl.model.{HttpResponse, StatusCodes} +import akka.http.scaladsl.model.{ HttpResponse, StatusCodes } import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.{Directives, Route} +import akka.http.scaladsl.server.{ Directives, ExceptionHandler, RejectionHandler, Route } import akka.http.interop._ import akka.http.scaladsl.model.StatusCodes.NoContent import play.api.libs.json.JsObject @@ -44,128 +44,146 @@ object Api { case ValidationError(_) => HttpResponse(StatusCodes.BadRequest) } - val itemRoute: Route = - path("healthcheck") { - get { - complete(HealthCheckService.healthCheck.provide(env)) - } ~ head(complete(NoContent)) - } ~ pathPrefix("items") { - logRequestResult(("items", InfoLevel)) { - pathEnd { - get { - complete(ApplicationService.getItems.provide(env)) - } ~ - post { - entity(Directives.as[CreateItemRequest]) { req => - ApplicationService - .addItem(req.name, req.price) - .provide(env) - .map { id => - complete { - Item(id, req.name, req.price) + import ch.megard.akka.http.cors.scaladsl.CorsDirectives._ + + // Your CORS settings are loaded from `application.conf` + + // Your rejection handler + val rejectionHandler = corsRejectionHandler.withFallback(RejectionHandler.default) + + // Your exception handler + val exceptionHandler = ExceptionHandler { case e: NoSuchElementException => + complete(StatusCodes.NotFound -> e.getMessage) + } + // Combining the two handlers only for convenience + val handleErrors = handleRejections(rejectionHandler) & handleExceptions(exceptionHandler) + + val itemRoute: Route = + handleErrors { + cors() { + handleErrors { + path("healthcheck") { + get { + complete(HealthCheckService.healthCheck.provide(env)) + } ~ head(complete(NoContent)) + } ~ pathPrefix("items") { + logRequestResult(("items", InfoLevel)) { + pathEnd { + get { + complete(ApplicationService.getItems.provide(env)) + } ~ + post { + entity(Directives.as[CreateItemRequest]) { req => + ApplicationService + .addItem(req.name, req.price) + .provide(env) + .map { id => + complete { + Item(id, req.name, req.price) + } + } + } + } + } ~ + path(LongNumber) { + itemId => + delete { + complete( + ApplicationService + .deleteItem(ItemId(itemId)) + .provide(env) + .as(JsObject.empty) + ) + } ~ + get { + complete(ApplicationService.getItem(ItemId(itemId)).provide(env)) + } ~ + patch { + entity(Directives.as[PartialUpdateItemRequest]) { req => + complete( + ApplicationService + .partialUpdateItem(ItemId(itemId), req.name, req.price) + .provide(env) + .as(JsObject.empty) + ) + } + } ~ + put { + entity(Directives.as[UpdateItemRequest]) { req => + complete( + ApplicationService + .updateItem(ItemId(itemId), req.name, req.price) + .provide(env) + .as(JsObject.empty) + ) + } + } } - } - } - } - } ~ - path(LongNumber) { - itemId => - delete { - complete( - ApplicationService - .deleteItem(ItemId(itemId)) - .provide(env) - .as(JsObject.empty) - ) - } ~ - get { - complete(ApplicationService.getItem(ItemId(itemId)).provide(env)) - } ~ - patch { - entity(Directives.as[PartialUpdateItemRequest]) { req => - complete( - ApplicationService - .partialUpdateItem(ItemId(itemId), req.name, req.price) - .provide(env) - .as(JsObject.empty) - ) } - } ~ - put { - entity(Directives.as[UpdateItemRequest]) { req => - complete( - ApplicationService - .updateItem(ItemId(itemId), req.name, req.price) - .provide(env) - .as(JsObject.empty) - ) - } - } - } - } - } $if(add_server_sent_events_endpoint.truthy)$ ~ - pathPrefix("sse" / "items") { - import akka.http.scaladsl.marshalling.sse.EventStreamMarshalling._ + } $if (add_server_sent_events_endpoint.truthy) $ ~pathPrefix("sse" / "items") { + import akka.http.scaladsl.marshalling.sse.EventStreamMarshalling._ - logRequestResult(("sse/items", InfoLevel)) { - pathPrefix("deleted") { - get { - complete { - ApplicationService.deletedEvents.toPublisher - .map(p => - Source - .fromPublisher(p) - .map(itemId => ServerSentEvent(itemId.value.toString)) - .keepAlive(1.second, () => ServerSentEvent.heartbeat) - ) - .provide(env) + logRequestResult(("sse/items", InfoLevel)) { + pathPrefix("deleted") { + get { + complete { + ApplicationService.deletedEvents.toPublisher + .map(p => + Source + .fromPublisher(p) + .map(itemId => ServerSentEvent(itemId.value.toString)) + .keepAlive(1.second, () => ServerSentEvent.heartbeat) + ) + .provide(env) + } + } + } } - } - } - } - } $endif$ $if(add_websocket_endpoint.truthy)$ ~ - pathPrefix("ws" / "items") { - logRequestResult(("ws/items", InfoLevel)) { - val greeterWebSocketService = - Flow[Message].flatMapConcat { - case tm: TextMessage if tm.getStrictText == "deleted" => - Source.futureSource( - unsafeRunToFuture( - ApplicationService.deletedEvents.toPublisher - .map(p => - Source - .fromPublisher(p) - .map(itemId => TextMessage(s"deleted: \${itemId.value}")) + } $endif$ $if(add_websocket_endpoint.truthy) $ ~pathPrefix("ws" / "items") { + logRequestResult(("ws/items", InfoLevel)) { + val greeterWebSocketService = + Flow[Message].flatMapConcat { + case tm: TextMessage if tm.getStrictText == "deleted" => + Source.futureSource( + unsafeRunToFuture( + ApplicationService.deletedEvents.toPublisher + .map(p => + Source + .fromPublisher(p) + .map(itemId => TextMessage(s"deleted: \${itemId.value}")) + ) + .provide(env) + ) ) - .provide(env) - ) - ) - case tm: TextMessage => - Try(tm.getStrictText.toLong) match { - case Success(value) => - Source.futureSource( - unsafeRunToFuture( - ApplicationService - .getItem(ItemId(value)) - .bimap( - _.asThrowable, - o => Source(o.toList.map(i => TextMessage(i.toString))) + case tm: TextMessage => + Try(tm.getStrictText.toLong) match { + case Success(value) => + Source.futureSource( + unsafeRunToFuture( + ApplicationService + .getItem(ItemId(value)) + .bimap( + _.asThrowable, + o => Source(o.toList.map(i => TextMessage(i.toString))) + ) + .provide(env) + ) ) - .provide(env) - ) - ) - case Failure(_) => Source.empty - } - case bm: BinaryMessage => - bm.getStreamedData.runWith(Sink.ignore, env.get[ActorSystem]) - Source.empty - } + case Failure(_) => Source.empty + } + case bm: BinaryMessage => + bm.getStreamedData.runWith(Sink.ignore, env.get[ActorSystem]) + Source.empty + } - handleWebSocketMessages(greeterWebSocketService) + handleWebSocketMessages(greeterWebSocketService) + } + } + $endif$ + } } } - $endif$ - } + } ) // accessors