Skip to content

Commit

Permalink
Merge branch 'main' into feat/OpenAPIAuth
Browse files Browse the repository at this point in the history
  • Loading branch information
gzoller authored Jan 16, 2025
2 parents 3a90dd6 + 79ad89d commit 1fed164
Show file tree
Hide file tree
Showing 47 changed files with 1,431 additions and 269 deletions.
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @jdegoes @vigoo @adamgfraser @987Nabil
* @jdegoes @vigoo @kyri-petrou @987Nabil
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ jobs:
- name: Build project
run: sbt '++ ${{ matrix.scala }}' test

- uses: coursier/setup-action@v1

- name: Test sbt plugin
if: ${{ github.event_name == 'pull_request' }} && matrix.scala == '2.12.19'
run: sbt ++2.12.19 zioHttpGenSbt/scripted

- uses: coursier/setup-action@v1
with:
apps: sbt
Expand All @@ -106,7 +112,7 @@ jobs:
run: sbt '++ ${{ matrix.scala }}' zioHttpShadedTests/test

- name: Compress target directories
run: tar cf targets.tar sbt-zio-http-grpc/target zio-http-cli/target target zio-http/jvm/target zio-http-docs/target sbt-zio-http-grpc-tests/target zio-http-gen/target zio-http-benchmarks/target zio-http-tools/target zio-http-example/target zio-http-testkit/target zio-http/js/target zio-http-htmx/target project/target
run: tar cf targets.tar sbt-zio-http-grpc/target zio-http-gen-sbt-plugin/target zio-http-cli/target target zio-http/jvm/target zio-http-docs/target sbt-zio-http-grpc-tests/target zio-http-gen/target zio-http-benchmarks/target zio-http-tools/target zio-http-example/target zio-http-testkit/target zio-http/js/target zio-http-htmx/target project/target

- name: Upload target directories
uses: actions/upload-artifact@v4
Expand Down
39 changes: 38 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ ThisBuild / githubWorkflowBuildPreamble := Seq(
),
)

ThisBuild / githubWorkflowBuild := {
(ThisBuild / githubWorkflowBuild).value ++ WorkflowJob(
"testSbtPlugin",
"Test sbt plugin",
List(
WorkflowStep.Use(UseRef.Public("coursier", "setup-action", "v1")),
WorkflowStep.Run(
name = Some(s"Test sbt plugin"),
commands = List(s"sbt ++${Scala212} zioHttpGenSbt/scripted"),
cond = Some(s"$${{ github.event_name == 'pull_request' }} && matrix.scala == '$Scala212'"),
),
),
scalas = List(Scala212),
).steps
}

ThisBuild / githubWorkflowBuildPostamble :=
WorkflowJob(
"checkDocGeneration",
Expand Down Expand Up @@ -334,7 +350,8 @@ lazy val zioHttpGen = (project in file("zio-http-gen"))
scalafmt.cross(CrossVersion.for3Use2_13),
scalametaParsers
.cross(CrossVersion.for3Use2_13)
.exclude("org.scala-lang.modules", "scala-collection-compat_2.13"),
.exclude("org.scala-lang.modules", "scala-collection-compat_2.13")
.exclude("com.lihaoyi", "sourcecode_2.13"),
`zio-json-yaml` % Test,
),
)
Expand All @@ -349,6 +366,26 @@ lazy val zioHttpGen = (project in file("zio-http-gen"))
)
.dependsOn(zioHttpJVM)

lazy val zioHttpGenSbt = (project in file("zio-http-gen-sbt-plugin"))
.enablePlugins(SbtPlugin)
.settings(publishSetting(true))
.settings(
name := "zio-http-sbt-codegen",
sbtPlugin := true,
scalaVersion := Scala212,
semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision,
scalacOptions ++= stdOptions ++ extraOptions(scalaVersion.value),
sbtTestDirectory := sourceDirectory.value / "sbt-test",
scriptedLaunchOpts += ("-Dplugin.version=" + version.value),
scriptedBufferLog := false,
libraryDependencies ++= Seq(
`zio-json-yaml`,
`zio-test`,
`zio-test-sbt`,
)
).dependsOn(LocalProject("zioHttpGen"))

lazy val sbtZioHttpGrpc = (project in file("sbt-zio-http-grpc"))
.settings(stdSettings("sbt-zio-http-grpc"))
.settings(publishSetting(true))
Expand Down
36 changes: 36 additions & 0 deletions docs/examples/endpoint-scala3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
id: endpoint-scala3
title: "Endpoint Scala 3 Syntax"
sidebar_label: "Endpoint Scala 3 Syntax"
---

```scala
import zio.http.*
import zio.http.codec.*
import zio.http.endpoint.*

import java.util.UUID

type NotFound[EntityId] = EntityId
type EntityId = UUID

val union: ContentCodec[String | UUID | Boolean] =
HttpCodec.content[String] || HttpCodec.content[UUID] || HttpCodec.content[Boolean]

val unionEndpoint =
Endpoint(Method.GET / "api" / "complex-union")
.outCodec(union)

val unionWithErrorEndpoint
: Endpoint[Unit, Unit, NotFound[EntityId] | String, UUID | Unit, AuthType.None] =
Endpoint(Method.GET / "api" / "union-with-error")
.out[UUID]
.orOut[Unit](Status.NoContent)
.outError[NotFound[EntityId]](Status.NotFound)
.orOutError[String](Status.BadRequest)

val impl = unionWithErrorEndpoint.implementEither { _ =>
val result: Either[NotFound[EntityId] | String, UUID | Unit] = Left("error")
result
}
```
8 changes: 4 additions & 4 deletions docs/examples/websocket.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ Our WebSocketApp will handle the following events send by the client:
* If the client sends "end", we will close the connection.
* If the client sends any other message, we will send the same message back to the client 10 times.

For the client to establish a connection with the server, we offer the `/subscriptions` endpoint.
For the client to establish a connection with the server, we offer the `/subscriptions` endpoint:

```scala mdoc:passthrough
import utils._

printSource("zio-http-example/src/main/scala/example/WebSocketAdvanced.scala", lines=Seq((3, 7), (9, 60)), showLineNumbers=false)
printSource("zio-http-example/src/main/scala/example/websocket/WebSocketServerAdvanced.scala")
```

A few things worth noting:
Expand All @@ -47,7 +47,7 @@ All we need for that, is the URL of the server. In our case it's `"ws://localhos
```scala mdoc:passthrough
import utils._

printSource("zio-http-example/src/main/scala/example/WebSocketAdvanced.scala", lines=Seq((3, 7), (62, 99)), showLineNumbers=false)
printSource("zio-http-example/src/main/scala/example/websocket/WebSocketClientAdvanced.scala")
```

While we access here `Queue[String]` via the ZIO environment, you should use a service in a real world application, that requires a queue as one of its constructor dependencies.
Expand All @@ -59,5 +59,5 @@ See [ZIO Services](https://zio.dev/reference/service-pattern/) for more informat
```scala mdoc:passthrough
import utils._

printSource("zio-http-example/src/main/scala/example/WebSocketEcho.scala")
printSource("zio-http-example/src/main/scala/example/websocket/WebSocketEcho.scala")
```
2 changes: 1 addition & 1 deletion docs/reference/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,5 +511,5 @@ This example represents a WebSocket client application that automatically attemp
```scala mdoc:passthrough
import utils._

printSource("zio-http-example/src/main/scala/example/WebSocketReconnectingClient.scala")
printSource("zio-http-example/src/main/scala/example/websocket/WebSocketReconnectingClient.scala")
```
33 changes: 33 additions & 0 deletions docs/reference/endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,15 @@ object EndpointWithMultipleOutputTypes extends ZIOAppDefault {
}
```

For Scala 3, we can use a union type instead of an `Either` by calling `Endpoint#orOut` for more than one output:

```scala
val endpoint: Endpoint[Unit, Unit, ZNothing, Course | Quiz, AuthType.None] =
Endpoint(RoutePattern.GET / "resources")
.out[Course]
.orOut[Quiz]
```

In the above example, we defined an endpoint that describes a path parameter `id` as input and returns either a `Book` or an `Article` as output.

With multiple outputs, we can define if all of them or just some should add an output header, by the order of calling `out` and `outHeader` methods:
Expand Down Expand Up @@ -472,6 +481,30 @@ utils.printSource("zio-http-example/src/main/scala/example/endpoint/EndpointWith
```
</details>

### Multiple Failure Outputs Using Union Types

The `Endpoint#orOutError` method can be used to describe multiple failure outputs using union types:

```scala
import zio.schema.DeriveSchema

case class Book(title: String, authors: List[String])
implicit val bookSchema = DeriveSchema.gen[Book]

case class BookNotFound(message: String, bookId: Int)
case class AuthenticationError(message: String, userId: Int)

implicit val notFoundSchema = DeriveSchema.gen[BookNotFound]
implicit val authSchema = DeriveSchema.gen[AuthenticationError]

val endpoint: Endpoint[Int, (Int, Header.Authorization), BookNotFound | AuthenticationError, Book, AuthType.None] =
Endpoint(RoutePattern.GET / "books" / PathCodec.int("id"))
.header(HeaderCodec.authorization)
.out[Book]
.outError[BookNotFound](Status.NotFound)
.orOutError[AuthenticationError](Status.Unauthorized)
```

## Transforming Endpoint Input/Output and Error Types

To transform the input, output, and error types of an endpoint, we can use the `Endpoint#transformIn`, `Endpoint#transformOut`, and `Endpoint#transformError` methods, respectively. Let's see an example:
Expand Down
13 changes: 11 additions & 2 deletions docs/reference/http-codec.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ There is also a `|` operator that allows us to create a codec that can decode ei
```scala mdoc:silent
import zio.http.codec._

val eitherQueryCodec: QueryCodec[String] = HttpCodec.query[String]("q") | HttpCodec.query[String]("query")
val eitherQueryCodec: QueryCodec[Either[Boolean, String]] = HttpCodec.query[Boolean]("q") | HttpCodec.query[String]("query")
```

Assume we have a request
Expand All @@ -229,7 +229,16 @@ We can decode the query parameter using the `decodeRequest` method:
```scala mdoc:silent
import zio._

val result: Task[String] = eitherQueryCodec.decodeRequest(request)
val result: Task[Either[Boolean, String]] = eitherQueryCodec.decodeRequest(request)
```

#### Scala 3 Union Type Syntax
For Scala 3 the `||` operator is available will return a union type instead of an `Either`.

```scala
import zio.http.codec._

val unionQueryCodec: QueryCodec[Boolean | String] = HttpCodec.query[Boolean]("q") || HttpCodec.query[String]("query")
```

```scala mdoc:invisible:reset
Expand Down
75 changes: 75 additions & 0 deletions docs/reference/openapi-gen-sbt-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
id: openapi-gen-sbt-plugin
title: OpenAPI codegen sbt plugin
---

This plugin allows to easily generate scala source code with zio-http Endpoints from OpenAPI spec files.

## How to use

The plugin offers 2 modes of operation that can be mixed and used together:
- Generating from unmanaged static OpenAPI spec files
- Generating from managed dynamic OpenAPI spec files

in `project/plugins.sbt` add the following line:
```scala
addSbtPlugin("dev.zio" % "zio-http-sbt-codegen" % "@VERSION@") // make sure the version of the sbt plugin
// matches the version of zio-http you are using
```

in `build.sbt` enable the plugin by adding:
```scala
enablePlugins(ZioHttpCodegen)
```

### 1. Generating from unmanaged static OpenAPI spec files
Place your manually curated OpenAPI spec files (`.yml`, `.yaml`, or `.json`) in `src/main/oapi/<path as package>/<openapi spec file>`.\
That's it. No other configuration is needed for basic usage. \
Once you `compile` your project, the `zioHttpCodegenMake` task is automatically invoked, and the generated code will be placed under `target/scala-<scala_binary_version>/src_managed/main/scala`.

### 2. Generating from managed dynamic OpenAPI spec files
In this mode, you can hook into `ZIOpenApi / sourceGenerators` a task to generate OpenAPI spec file, exactly like you would do with regular `Compile / sourceGenerators` for scala source files.
You might have some OpenAPI spec files hosted on [swaggerhub](https://app.swaggerhub.com/) or a similar service,
or maybe you use services that expose OpenAPI specs via REST API, or perhaps you have a local project that can build its own spec and you want to run the spec generate command.
Whatever the scenario you're dealing with, it can be very handy to dynamically fetch/generate the latest most updated spec file, so the generated code stays up to date with any changes introduced.

Here's how you can do it:
```scala
import gigahorse.support.apachehttp.Gigahorse
import scala.concurrent.Await
import scala.concurrent.duration.DurationInt

ZIOpenApi / sourceGenerators += Def.task[Seq[File]] {
// we'll fetch a spec from https://www.petstore.dev/
// gigahorse comes builtin with sbt, but any other http client can be used
val http = Gigahorse.http(Gigahorse.config)
val request = Gigahorse.url("https://raw.githubusercontent.com/readmeio/oas-examples/main/3.0/yaml/response-http-behavior.yaml")
val response = http.run(request, Gigahorse.asString)
val content = Await.result(response, 1.minute)

// path under target/scala-<scala_bin_version>/src_managed/oapi/
// corresponds to the package where scala sources will be generated
val outFile = (ZIOpenApi / sourceManaged).value / "dev" / "petstore" / "http" / "test" / "api.yaml"
IO.write(outFile, content)

// as long the task yields a Seq[File] of valid OpenAPI spec files,
// and those files follow the path structure `src_managed/oapi/<path as package>/<openapi spec file>`,
// the plugin will pick it up, and generate the corresponding scala sources.
Seq(outFile)
}
```

## Configuration
The plugin offers a setting key which you can set to control how code is generated:
```scala
zioHttpCodegenConf := zio.http.gen.openapi.Config.default
```

## Caveats
The plugin allows you to provide multiple files.
Note that if you place multiple files in the same directory,
which means same package for the generated code - you must make sure there are no "collisions" between generated classes.
If the same class is going to be generated differently in different files, you probably want to have a different package for it.

Also, please note that the plugin relies on the file extension to determine how to parse it.
So files must have the correct extension (`.yml`, `.yaml`, or `.json`), and the content must be formatted accordingly.
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const sidebars = {
"examples/websocket",
"examples/streaming",
"examples/endpoint",
"examples/endpoint-scala3",
"examples/middleware-cors-handling",
"examples/authentication",
"examples/graceful-shutdown",
Expand Down
2 changes: 1 addition & 1 deletion project/BuildHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ object BuildHelper extends ScalaSettings {
val ScoverageVersion = "2.0.12"
val JmhVersion = "0.4.7"

private val stdOptions = Seq(
val stdOptions = Seq(
"-deprecation",
"-encoding",
"UTF-8",
Expand Down
2 changes: 1 addition & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import sbt.*

object Dependencies {
val JwtCoreVersion = "10.0.1"
val NettyVersion = "4.1.112.Final"
val NettyVersion = "4.1.116.Final"
val NettyIncubatorVersion = "0.0.25.Final"
val ScalaCompactCollectionVersion = "2.12.0"
val ZioVersion = "2.1.11"
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.10.0
sbt.version=1.10.6
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package example.websocket
import scala.annotation.nowarn

import zio._

import zio.http.ChannelEvent.Read
import zio.http._

object WebSocketSimpleClientAdvanced extends ZIOAppDefault {

def sendChatMessage(message: String): ZIO[Queue[String], Throwable, Unit] =
ZIO.serviceWithZIO[Queue[String]](_.offer(message).unit)

def processQueue(channel: WebSocketChannel): ZIO[Queue[String], Throwable, Unit] = {
for {
queue <- ZIO.service[Queue[String]]
msg <- queue.take
_ <- channel.send(Read(WebSocketFrame.Text(msg)))
} yield ()
}.forever.forkDaemon.unit

private def webSocketHandler: ZIO[Queue[String] with Client with Scope, Throwable, Response] =
Handler.webSocket { channel =>
for {
_ <- processQueue(channel)
_ <- channel.receiveAll {
case Read(WebSocketFrame.Text(text)) =>
Console.printLine(s"Server: $text")
case _ =>
ZIO.unit
}
} yield ()
}.connect("ws://localhost:8080/subscriptions")

@nowarn("msg=dead code")
override val run =
ZIO
.scoped(for {
_ <- webSocketHandler
_ <- Console.readLine.flatMap(sendChatMessage).forever.forkDaemon
_ <- ZIO.never
} yield ())
.provide(
Client.default,
ZLayer(Queue.bounded[String](100)),
)

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package example
package example.websocket

import zio._

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package example
package example.websocket

import zio._

Expand Down
Loading

0 comments on commit 1fed164

Please sign in to comment.