Skip to content

Commit

Permalink
Merge pull request #456 from disneystreaming/dfrancoeur/cors-example
Browse files Browse the repository at this point in the history
Add a guide module - show case CORS middleware
  • Loading branch information
Baccata authored Oct 19, 2022
2 parents 815426b + 18e1f75 commit 73e7aa0
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 0 deletions.
20 changes: 20 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sbt.internal.IvyConsole
import org.scalajs.jsenv.nodejs.NodeJSEnv

import java.io.File
Expand Down Expand Up @@ -67,6 +68,7 @@ lazy val allModules = Seq(
`codegen-cli`,
dynamic,
testUtils,
guides,
complianceTests
).flatMap(_.projectRefs)

Expand Down Expand Up @@ -761,6 +763,24 @@ lazy val example = projectMatrix
.jvmPlatform(List(Scala213), jvmDimSettings)
.settings(Smithy4sPlugin.doNotPublishArtifact)

lazy val guides = projectMatrix
.in(file("modules/guides"))
.dependsOn(http4s)
.settings(
Compile / allowedNamespaces := Seq("smithy4s.guides.hello"),
smithySpecs := Seq(
(ThisBuild / baseDirectory).value / "modules" / "guides" / "smithy" / "hello.smithy"
),
(Compile / sourceGenerators) := Seq(genSmithyScala(Compile).taskValue),
isCE3 := true,
libraryDependencies ++= Seq(
Dependencies.Http4s.emberServer.value,
Dependencies.Weaver.cats.value % Test
)
)
.jvmPlatform(Seq(Scala3), jvmDimSettings)
.settings(Smithy4sPlugin.doNotPublishArtifact)

/**
* Pretty primitive benchmarks to test that we're not doing anything drastically
* slow.
Expand Down
15 changes: 15 additions & 0 deletions modules/docs/src/03-protocols/02-simple-rest-json/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,18 @@ The `SimpleRestJson` protocol supports 3 different union encodings :
* discriminated

See the section about [unions](../../04-codegen/02-unions.md) for a detailed description.

## Supported traits

Here is the list of traits supported by `SimpleRestJson`

```scala mdoc:passthrough
smithy4s.api.SimpleRestJson.hints
.get[smithy.api.ProtocolDefinition]
.getOrElse(sys.error("Unable to grab protocol defition information."))
.traits.toList.flatten.map(_.value)
.map(id => s"- `$id`")
.foreach(println)
```

Currently, `@cors` is not supported. This is because the `@cors` annotation is too restrictive. You can still use it in your model and configure your API using the information found in the generated code. See the [`Cors.scala`](https://github.com/disneystreaming/smithy4s/tree/main/modules/guides/src/smithy4s/guides/Cors.scala) file in the `guides` module for an example.
24 changes: 24 additions & 0 deletions modules/guides/smithy/hello.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
$version: "2"

namespace smithy4s.guides.hello

use smithy4s.api#simpleRestJson

@simpleRestJson
@cors(origin: "http://mysite.com", additionalAllowedHeaders: ["Authorization"], additionalExposedHeaders: ["X-Smithy4s"])
service HelloWorldService {
version: "1.0.0",
operations: [SayWorld]
}


@readonly
@http(method: "GET", uri: "/hello", code: 200)
operation SayWorld {
output: World
}

structure World {
message: String = "World !"
}

108 changes: 108 additions & 0 deletions modules/guides/src/smithy4s/guides/Cors.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2021-2022 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package smithy4s.guides

import smithy4s.guides.hello._
import cats.effect.*
import cats.implicits.*
import org.http4s.implicits.*
import org.http4s.ember.server.*
import org.http4s.*
import com.comcast.ip4s.*
import smithy4s.http4s.SimpleRestJsonBuilder
import org.http4s.server.Middleware
import org.http4s.headers.Origin
import org.typelevel.ci.CIString.apply
import org.typelevel.ci.CIString
import scala.concurrent.duration.Duration

object HelloWorldImpl extends HelloWorldService[IO] {
def sayWorld(): IO[World] = World().pure[IO]
}

/**
* This is an example of how you can configure CORS on a http4s router. The
* source of truth for the configuration is the Smithy annotation on the
* service. There is currently no support for CORS in Smithy4s because
* the `@cors` annotation is a too restrictive.
*
* See: https://github.com/awslabs/smithy/issues/1396
*/
object Routes {
import org.http4s.server.middleware.*

val noMiddleware = (routes: HttpRoutes[IO]) => routes

/**
* We build a http4s CORS policy from the trait values.
* In this example, we set a few fields like the `origin`, the `maxAge`, etc
* but there is more information available in `smithy.api.Cors`.
*
* @param corsConfig instance of the trait with values from the specification
*/
def buildHttp4sCors(corsConfig: smithy.api.Cors): CORSPolicy = {
val configuredOrigin = Origin.parse(corsConfig.origin.value)
configuredOrigin.swap.foreach { err =>
// There are better approach for error handling than a println but
// this will do for an example.
println(
s"Could not configure cors origin: ${err.getMessage()}"
)
}
val configuredExposedHeaders =
corsConfig.additionalExposedHeaders.toList.flatten
.map(_.value)
.toSet
.map(CIString(_))
val configuredAge =
Duration(corsConfig.maxAge, scala.concurrent.duration.SECONDS)
CORS.policy
.withAllowOriginHeader {
case Origin.Null => false
case o =>
configuredOrigin.toOption.exists { co => co == o }
}
.withExposeHeadersIn(configuredExposedHeaders)
.withAllowCredentials(false)
.withMaxAge(configuredAge)
}

// We use Smithy4s api to retrieve the trait (called `Hint` in Smithy4s)
// We get an `Option` because service are not required to have this trait
val corsTrait = HelloWorldServiceGen.hints.get[smithy.api.Cors]
val corsMiddleWare = corsTrait
.map { corsConfig => buildHttp4sCors(corsConfig).apply(_: HttpRoutes[IO]) }
.getOrElse(noMiddleware)

private val helloRoutes: Resource[IO, HttpRoutes[IO]] =
SimpleRestJsonBuilder.routes(HelloWorldImpl).resource

val all: Resource[IO, HttpRoutes[IO]] =
helloRoutes.map(r => corsMiddleWare(r))
}

object Main extends IOApp.Simple {
val run = Routes.all.flatMap { routes =>
EmberServerBuilder
.default[IO]
.withPort(port"9000")
.withHost(host"localhost")
.withHttpApp(routes.orNotFound)
.build
}.useForever

}

0 comments on commit 73e7aa0

Please sign in to comment.