diff --git a/build.sbt b/build.sbt index 8397f08db..d89920ca8 100644 --- a/build.sbt +++ b/build.sbt @@ -303,6 +303,7 @@ lazy val zioHttpTestkit = (project in file("zio-http-testkit")) lazy val docs = project .in(file("zio-http-docs")) + .settings(stdSettings("zio-http-docs")) .settings( moduleName := "zio-http-docs", scalacOptions -= "-Yno-imports", diff --git a/docs/examples/advanced/authentication.md b/docs/examples/advanced/authentication.md deleted file mode 100644 index 8411c8378..000000000 --- a/docs/examples/advanced/authentication.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -id: authentication-server -title: "Authentication Server Example" -sidebar_label: "Authentication Server" ---- - -```scala mdoc:silent -import java.time.Clock - -import zio._ - -import zio.http._ -import zio.http.Middleware.bearerAuth -import zio.http.codec.PathCodec.string - -import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim} - -object AuthenticationServer extends ZIOAppDefault { - - /** - * This is an example to demonstrate barer Authentication middleware. The - * Server has 2 routes. The first one is for login,Upon a successful login, it - * will return a jwt token for accessing protected routes. The second route is - * a protected route that is accessible only if the request has a valid jwt - * token. AuthenticationClient example can be used to makes requests to this - * server. - */ - - // Secret Authentication key - val SECRET_KEY = "secretKey" - - implicit val clock: Clock = Clock.systemUTC - - // Helper to encode the JWT token - def jwtEncode(username: String): String = { - val json = s"""{"user": "${username}"}""" - val claim = JwtClaim { - json - }.issuedNow.expiresIn(300) - Jwt.encode(claim, SECRET_KEY, JwtAlgorithm.HS512) - } - - // Helper to decode the JWT token - def jwtDecode(token: String): Option[JwtClaim] = { - Jwt.decode(token, SECRET_KEY, Seq(JwtAlgorithm.HS512)).toOption - } - - // Http app that is accessible only via a jwt token - def user: HttpApp[Any] = Routes( - Method.GET / "user" / string("name") / "greet" -> handler { (name: String, req: Request) => - Response.text(s"Welcome to the ZIO party! ${name}") - }, - ).toHttpApp @@ bearerAuth(jwtDecode(_).isDefined) - - // App that let's the user login - // Login is successful only if the password is the reverse of the username - def login: HttpApp[Any] = - Routes( - Method.GET / "login" / string("username") / string("password") -> - handler { (username: String, password: String, req: Request) => - if (password.reverse.hashCode == username.hashCode) Response.text(jwtEncode(username)) - else Response.text("Invalid username or password.").status(Status.Unauthorized) - }, - ).toHttpApp - - // Composing all the HttpApps together - val app: HttpApp[Any] = login ++ user - - // Run it like any simple app - override val run = Server.serve(app).provide(Server.default) -} -``` \ No newline at end of file diff --git a/docs/examples/advanced/concrete-entity.md b/docs/examples/advanced/concrete-entity.md deleted file mode 100644 index e97b35dac..000000000 --- a/docs/examples/advanced/concrete-entity.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: concrete-entity -title: "Concrete Entity Example" -sidebar_label: "Concrete Entity" ---- - -```scala mdoc:silent - -import zio._ - -import zio.http._ - -/** - * Example to build app on concrete entity - */ -object ConcreteEntity extends ZIOAppDefault { - // Request - case class CreateUser(name: String) - - // Response - case class UserCreated(id: Long) - - val user: Handler[Any, Nothing, CreateUser, UserCreated] = - Handler.fromFunction[CreateUser] { case CreateUser(_) => - UserCreated(2) - } - - val app: HttpApp[Any] = - user - .contramap[Request](req => CreateUser(req.path.encode)) // Http[Any, Nothing, Request, UserCreated] - .map(userCreated => Response.text(userCreated.id.toString)) // Http[Any, Nothing, Request, Response] - .toHttpApp - - // Run it like any simple app - val run = - Server.serve(app).provide(Server.default) -} -``` \ No newline at end of file diff --git a/docs/examples/advanced/middleware-basic-authentication.md b/docs/examples/advanced/middleware-basic-authentication.md deleted file mode 100644 index f97798a3b..000000000 --- a/docs/examples/advanced/middleware-basic-authentication.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -id: middleware-basic-authentication -title: "Middleware Basic Authentication Example" -sidebar_label: "Middleware Basic Authentication" ---- - -```scala mdoc:silent -import zio._ - -import zio.http._ -import zio.http.Middleware.basicAuth -import zio.http.codec.PathCodec.string - -object BasicAuth extends ZIOAppDefault { - - // Http app that requires a JWT claim - val user: HttpApp[Any] = Routes( - Method.GET / "user" / string("name") / "greet" -> - handler { (name: String, req: Request) => - Response.text(s"Welcome to the ZIO party! ${name}") - }, - ).toHttpApp - - // Composing all the HttpApps together - val app: HttpApp[Any] = user @@ basicAuth("admin", "admin") - - // Run it like any simple app - val run = Server.serve(app).provide(Server.default) -} -``` \ No newline at end of file diff --git a/docs/examples/advanced/middleware-cors-handling.md b/docs/examples/advanced/middleware-cors-handling.md deleted file mode 100644 index 611b2f94f..000000000 --- a/docs/examples/advanced/middleware-cors-handling.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -id: middleware-cors-handling -title: "Middleware CORS Handling Example" -sidebar_label: "Middleware CORS Handling" ---- - -```scala mdoc:silent -import zio._ - -import zio.http._ -import zio.http.Header.{AccessControlAllowMethods, AccessControlAllowOrigin, Origin} -import zio.http.Middleware.{cors, CorsConfig} - -object HelloWorldWithCORS extends ZIOAppDefault { - - // Create CORS configuration - val config: CorsConfig = - CorsConfig( - allowedOrigin = { - case origin @ Origin.Value(_, host, _) if host == "dev" => Some(AccessControlAllowOrigin.Specific(origin)) - case _ => None - }, - allowedMethods = AccessControlAllowMethods(Method.PUT, Method.DELETE), - ) - - // Create HTTP route with CORS enabled - val app: HttpApp[Any] = - Routes( - Method.GET / "text" -> handler(Response.text("Hello World!")), - Method.GET / "json" -> handler(Response.json("""{"greetings": "Hello World!"}""")), - ).toHttpApp @@ cors(config) - - // Run it like any simple app - val run = - Server.serve(app).provide(Server.default) -} -``` \ No newline at end of file diff --git a/docs/examples/advanced/server.md b/docs/examples/advanced/server.md deleted file mode 100644 index 947f16d03..000000000 --- a/docs/examples/advanced/server.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -id: server -title: "Advanced Server Example" -sidebar_label: "Server" ---- - -```scala mdoc:silent -import scala.util.Try - -import zio._ - -import zio.http._ -import zio.http.netty.NettyConfig -import zio.http.netty.NettyConfig.LeakDetectionLevel - -object HelloWorldAdvanced extends ZIOAppDefault { - // Set a port - val PORT = 0 - - val fooBar = - Routes( - Method.GET / "foo" -> Handler.from(Response.text("bar")), - Method.GET / "bar" -> Handler.from(Response.text("foo")), - ).toHttpApp - - val app = Routes( - Method.GET / "random" -> handler(Random.nextString(10).map(Response.text(_))), - Method.GET / "utc" -> handler(Clock.currentDateTime.map(s => Response.text(s.toString))), - ).toHttpApp - - val run = ZIOAppArgs.getArgs.flatMap { args => - // Configure thread count using CLI - val nThreads: Int = args.headOption.flatMap(x => Try(x.toInt).toOption).getOrElse(0) - - val config = Server.Config.default - .port(PORT) - val nettyConfig = NettyConfig.default - .leakDetection(LeakDetectionLevel.PARANOID) - .maxThreads(nThreads) - val configLayer = ZLayer.succeed(config) - val nettyConfigLayer = ZLayer.succeed(nettyConfig) - - (Server.install(fooBar ++ app).flatMap { port => - Console.printLine(s"Started server on port: $port") - } *> ZIO.never) - .provide(configLayer, nettyConfigLayer, Server.customized) - } -} -``` \ No newline at end of file diff --git a/docs/examples/advanced/static-files.md b/docs/examples/advanced/static-files.md deleted file mode 100644 index 810850955..000000000 --- a/docs/examples/advanced/static-files.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -id: static-files -title: "Serving Static Files" -sidebar_label: "Static Files" ---- - -```scala mdoc:silent -import zio._ -import zio.http._ - -object StaticFiles extends ZIOAppDefault { - - /** - * Creates an HTTP app that only serves static files from resources via - * "/static". For paths other than the resources directory, see - * [[Middleware.serveDirectory]]. - */ - val app = Routes.empty.toHttpApp @@ Middleware.serveResources(Path.empty / "static") - - override def run = Server.serve(app).provide(Server.default) -} - -``` \ No newline at end of file diff --git a/docs/examples/advanced/streaming-file.md b/docs/examples/advanced/streaming-file.md deleted file mode 100644 index 2e02449e1..000000000 --- a/docs/examples/advanced/streaming-file.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: streaming-file -title: "Streaming File Example" -sidebar_label: "Streaming File" ---- - -```scala mdoc -import java.io.File -import java.nio.file.Paths - -import zio._ - -import zio.stream.ZStream - -import zio.http._ - -object FileStreaming extends ZIOAppDefault { - - // Create HTTP route - val app = Routes( - Method.GET / "health" -> Handler.ok, - - // Read the file as ZStream - // Uses the blocking version of ZStream.fromFile - Method.GET / "blocking" -> Handler.fromStreamChunked(ZStream.fromPath(Paths.get("README.md"))), - - // Uses netty's capability to write file content to the Channel - // Content-type response headers are automatically identified and added - // Adds content-length header and does not use Chunked transfer encoding - Method.GET / "video" -> Handler.fromFile(new File("src/main/resources/TestVideoFile.mp4")), - Method.GET / "text" -> Handler.fromFile(new File("src/main/resources/TestFile.txt")), - ).sandbox.toHttpApp - - // Run it like any simple app - val run = - Server.serve(app).provide(Server.default) -} -``` \ No newline at end of file diff --git a/docs/examples/advanced/streaming-response.md b/docs/examples/advanced/streaming-response.md deleted file mode 100644 index c70f987b4..000000000 --- a/docs/examples/advanced/streaming-response.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: streaming-response -title: "Streaming Response Example" -sidebar_label: "Streaming Response" ---- - -```scala mdoc -import zio.{http, _} - -import zio.stream.ZStream - -import zio.http._ - -/** - * Example to encode content using a ZStream - */ -object StreamingResponse extends ZIOAppDefault { - // Starting the server (for more advanced startup configuration checkout `HelloWorldAdvanced`) - def run = Server.serve(app).provide(Server.default) - - // Create a message as a Chunk[Byte] - def message = Chunk.fromArray("Hello world !\r\n".getBytes(Charsets.Http)) - - def app: HttpApp[Any] = Routes( - // Simple (non-stream) based route - Method.GET / "health" -> handler(Response.ok), - - // ZStream powered response - Method.GET / "stream" -> - handler( - http.Response( - status = Status.Ok, - body = Body.fromStream(ZStream.fromChunk(message), message.length.toLong), // Encoding content using a ZStream - ), - ), - ).toHttpApp -} - -``` \ No newline at end of file diff --git a/docs/examples/advanced/websocket.md b/docs/examples/advanced/websocket.md deleted file mode 100644 index 174f330b8..000000000 --- a/docs/examples/advanced/websocket.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -id: websocket -title: "WebSocket Example" -sidebar_label: "WebSocket Server & Client" ---- - -This example shows how to create a WebSocket server using ZIO Http and how to write a client to connect to it. - -## Server - -First we define a `WebSocketApp` that will handle the WebSocket connection. -The `Handler.webSocket` constructor gives access to the `WebSocketChannel`. The channel can be used to receive messages from the client and send messages back. -We use the `receiveAll` method, to pattern match on the different channel events that could occur. -The most important events are `Read` and `UserEventTriggered`. The `Read` event is triggered when the client sends a message to the server. The `UserEventTriggered` event is triggered when the connection is established. -We can identify the successful connection of a client by receiving a `UserEventTriggered(UserEvent.HandshakeComplete)` event. And if the client sends us a text message, we will receive a `Read(WebSocketFrame.Text())` event. - -Our WebSocketApp will handle the following events send by the client: -* If the client connects to the server, we will send a "Greetings!" message to the client. -* If the client sends "foo", we will send a "bar" message back to the client. -* If the client sends "bar", we will send a "foo" message back to 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. - -```scala mdoc:silent - -import zio._ - -import zio.http.ChannelEvent.{ExceptionCaught, Read, UserEvent, UserEventTriggered} -import zio.http._ -import zio.http.codec.PathCodec.string - -object WebSocketAdvanced extends ZIOAppDefault { - - val socketApp: WebSocketApp[Any] = - Handler.webSocket { channel => - channel.receiveAll { - case Read(WebSocketFrame.Text("end")) => - channel.shutdown - - // Send a "bar" if the client sends a "foo" - case Read(WebSocketFrame.Text("foo")) => - channel.send(Read(WebSocketFrame.text("bar"))) - - // Send a "foo" if the client sends a "bar" - case Read(WebSocketFrame.Text("bar")) => - channel.send(Read(WebSocketFrame.text("foo"))) - - // Echo the same message 10 times if it's not "foo" or "bar" - case Read(WebSocketFrame.Text(text)) => - channel.send(Read(WebSocketFrame.text(text))).repeatN(10) - - // Send a "greeting" message to the client once the connection is established - case UserEventTriggered(UserEvent.HandshakeComplete) => - channel.send(Read(WebSocketFrame.text("Greetings!"))) - - // Log when the channel is getting closed - case Read(WebSocketFrame.Close(status, reason)) => - Console.printLine("Closing channel with status: " + status + " and reason: " + reason) - - // Print the exception if it's not a normal close - case ExceptionCaught(cause) => - Console.printLine(s"Channel error!: ${cause.getMessage}") - - case _ => - ZIO.unit - } - } - - val app: HttpApp[Any] = - Routes(Method.GET / "subscriptions" -> handler(socketApp.toResponse)).toHttpApp - - override val run = Server.serve(app).provide(Server.default) -} -``` - -A few things worth noting: - * `Server.default` starts a server on port 8080. - * `socketApp.toResponse` converts the `WebSocketApp` to a `Response`, so we can serve it with `handler`. - - -## Client - -The client will connect to the server and send a message to the server every time the user enters a message in the console. -For this we will use the `Console.readLine` method to read a line from the console. We will then send the message to the server using the `WebSocketChannel.send` method. -But since we don't want to reconnect to the server every time the user enters a message, we will use a `Queue` to store the messages. We will then use the `Queue.take` method to take a message from the queue and send it to the server, whenever a new message is available. -Adding a new message to the queue, as well as sending the messages to the server, should happen in a loop in the background. For this we will use the operators `forever` (looping) and `forkDaemon` (fork to a background fiber). - -Again we will use the `Handler.webSocket` constructor to define how to handle messages and create a `WebSocketApp`. But this time, instead of serving the `WebSocketApp` we will use the `connect` method to establish a connection to the server. -All we need for that, is the URL of the server. In our case it's `"ws://localhost:8080/subscriptions"`. - -```scala mdoc:silent -import zio._ - -import zio.http._ - -object WebSocketAdvancedClient 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") - - override val run = - (for { - _ <- webSocketHandler - _ <- Console.readLine.flatMap(sendChatMessage).forever.forkDaemon - _ <- ZIO.never - } yield ()) - .provideSome[Scope]( - Client.default, - ZLayer(Queue.bounded[String](100)), - ) - -} -``` -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. -See [ZIO Services](https://zio.dev/reference/service-pattern/) for more information. diff --git a/docs/examples/authentication.md b/docs/examples/authentication.md new file mode 100644 index 000000000..8ae29782a --- /dev/null +++ b/docs/examples/authentication.md @@ -0,0 +1,26 @@ +--- +id: authentication +title: "Authentication Example" +sidebar_label: "Authentication" +--- + +## Authentication Server Example + +```scala mdoc:passthrough +import utils._ +printSource("zio-http-example/src/main/scala/example/AuthenticationServer.scala") +``` + +## Authentication Client Example + +```scala mdoc:passthrough +import utils._ +printSource("zio-http-example/src/main/scala/example/AuthenticationClient.scala") +``` + +## Middleware Basic Authentication Example + +```scala mdoc:passthrough +import utils._ +printSource("zio-http-example/src/main/scala/example/BasicAuth.scala") +``` diff --git a/docs/examples/basic/http-client.md b/docs/examples/basic/http-client.md deleted file mode 100644 index c664a8cf4..000000000 --- a/docs/examples/basic/http-client.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -id: http-client -title: Http Client Example -sidebar_label: Http Client ---- - -```scala mdoc:silent -import zio._ - -import zio.http._ - -object SimpleClient extends ZIOAppDefault { - val url = URL.decode("http://sports.api.decathlon.com/groups/water-aerobics").toOption.get - - val program = for { - client <- ZIO.service[Client] - res <- client.url(url).get("/") - data <- res.body.asString - _ <- Console.printLine(data) - } yield () - - override val run = program.provide(Client.default, Scope.default) - -} - -``` \ No newline at end of file diff --git a/docs/examples/basic/http-server.md b/docs/examples/basic/http-server.md deleted file mode 100644 index 0e4b142bd..000000000 --- a/docs/examples/basic/http-server.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -id: http-server -title: Http Server Example -sidebar_label: Http Server ---- - -```scala mdoc:silent -import zio._ - -import zio.http._ - -object HelloWorld extends ZIOAppDefault { - val textRoute = - Method.GET / "text" -> handler(Response.text("Hello World!")) - - val jsonRoute = - Method.GET / "json" -> handler(Response.json("""{"greetings": "Hello World!"}""")) - - // Create HTTP route - val app = Routes(textRoute, jsonRoute).toHttpApp - - // Run it like any simple app - override val run = Server.serve(app).provide(Server.default) -} - -``` \ No newline at end of file diff --git a/docs/examples/basic/https-client.md b/docs/examples/basic/https-client.md deleted file mode 100644 index f5a07397b..000000000 --- a/docs/examples/basic/https-client.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -id: https-client -title: Https Client Example -sidebar_label: Https Client ---- - -```scala mdoc:silent -import zio._ - -import zio.http._ -import zio.http.netty.NettyConfig -import zio.http.netty.client.NettyClientDriver - -object HttpsClient extends ZIOAppDefault { - val url = URL.decode("https://sports.api.decathlon.com/groups/water-aerobics").toOption.get - val headers = Headers(Header.Host("sports.api.decathlon.com")) - - val sslConfig = ClientSSLConfig.FromTrustStoreResource( - trustStorePath = "truststore.jks", - trustStorePassword = "changeit", - ) - - val clientConfig = ZClient.Config.default.ssl(sslConfig) - - val program = for { - res <- ZClient.request(Request.get(url).addHeaders(headers)) - data <- res.body.asString - _ <- Console.printLine(data) - } yield () - - val run = - program.provide( - ZLayer.succeed(clientConfig), - Client.customized, - NettyClientDriver.live, - DnsResolver.default, - ZLayer.succeed(NettyConfig.default), - Scope.default, - ) - -} - -``` diff --git a/docs/examples/basic/https-server.md b/docs/examples/basic/https-server.md deleted file mode 100644 index 14f2c16ae..000000000 --- a/docs/examples/basic/https-server.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -id: https-server -title: Https Server Example -sidebar_label: Https Server ---- - -```scala mdoc:silent -import zio._ - -import zio.http._ - -object HttpsHelloWorld extends ZIOAppDefault { - // Create HTTP route - val app: HttpApp[Any] = Routes( - Method.GET / "text" -> handler(Response.text("Hello World!")), - Method.GET / "json" -> handler(Response.json("""{"greetings": "Hello World!"}""")), - ).toHttpApp - - /** - * In this example, a private key and certificate are loaded from resources. - * For testing this example with curl, make sure the private key "server.key", - * and the certificate "server.crt" are inside the resources directory, - * which is by default "src/main/resources". - * - * You can use the following command to create a self-signed TLS certificate. - * This command will create two files: "server.key" and "server.crt". - * - * openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ - * -keyout server.key -out server.crt \ - * -subj "/CN=example.com/OU=?/O=?/L=?/ST=?/C=??" \ - * -addext "subjectAltName=DNS:example.com,DNS:www.example.com,IP:10.0.0.1" - */ - - val sslConfig = SSLConfig.fromResource( - behaviour = SSLConfig.HttpBehaviour.Accept, - certPath = "server.crt", - keyPath = "server.key", - ) - - private val config = Server.Config.default - .port(8090) - .ssl(sslConfig) - - private val configLayer = ZLayer.succeed(config) - - override val run = - Server.serve(app).provide(configLayer, Server.live) - -} - -``` \ No newline at end of file diff --git a/docs/examples/basic/websocket.md b/docs/examples/basic/websocket.md deleted file mode 100644 index 045f3e4f6..000000000 --- a/docs/examples/basic/websocket.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: websocket -title: WebSocket Example -sidebar_label: WebSocket ---- - -```scala mdoc:silent -import zio._ - -import zio.http.ChannelEvent.Read -import zio.http._ -import zio.http.codec.PathCodec.string - -object WebSocketEcho extends ZIOAppDefault { - private val socketApp: WebSocketApp[Any] = - Handler.webSocket { channel => - channel.receiveAll { - case Read(WebSocketFrame.Text("FOO")) => - channel.send(Read(WebSocketFrame.Text("BAR"))) - case Read(WebSocketFrame.Text("BAR")) => - channel.send(Read(WebSocketFrame.Text("FOO"))) - case Read(WebSocketFrame.Text(text)) => - channel.send(Read(WebSocketFrame.Text(text))).repeatN(10) - case _ => - ZIO.unit - } - } - - private val app: HttpApp[Any] = - Routes( - Method.GET / "greet" / string("name") -> handler { (name: String, req: Request) => - Response.text(s"Greetings {$name}!") - }, - Method.GET / "subscriptions" -> handler(socketApp.toResponse), - ).toHttpApp - - override val run = Server.serve(app).provide(Server.default) -} - -``` diff --git a/docs/examples/cli.md b/docs/examples/cli.md new file mode 100644 index 000000000..0763bed2e --- /dev/null +++ b/docs/examples/cli.md @@ -0,0 +1,11 @@ +--- +id: cli +title: "CLI Client-Server Examples" +sidebar_label: "CLI" +--- + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/ConcreteEntity.scala") +``` diff --git a/docs/examples/concrete-entity.md b/docs/examples/concrete-entity.md new file mode 100644 index 000000000..1d58d99f7 --- /dev/null +++ b/docs/examples/concrete-entity.md @@ -0,0 +1,11 @@ +--- +id: concrete-entity +title: "Concrete Entity Example" +sidebar_label: "Concrete Entity" +--- + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/CliExamples.scala") +``` diff --git a/docs/examples/cookie.md b/docs/examples/cookie.md new file mode 100644 index 000000000..1514ad4e9 --- /dev/null +++ b/docs/examples/cookie.md @@ -0,0 +1,21 @@ +--- +id: cookies +title: "HTTP App Examples Using Cookie" +sidebar_label: "Cookies" +--- + +## Server Side Example + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/CookieServerSide.scala") +``` + +## Signed Cookies + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/SignCookies.scala") +``` diff --git a/docs/examples/endpoint.md b/docs/examples/endpoint.md new file mode 100644 index 000000000..32ab903b7 --- /dev/null +++ b/docs/examples/endpoint.md @@ -0,0 +1,11 @@ +--- +id: endpoint +title: "Endpoint Examples" +sidebar_label: "Endpoint" +--- + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/EndpointExamples.scala") +``` diff --git a/docs/examples/graceful-shutdown.md b/docs/examples/graceful-shutdown.md new file mode 100644 index 000000000..a390e36ce --- /dev/null +++ b/docs/examples/graceful-shutdown.md @@ -0,0 +1,11 @@ +--- +id: graceful-shutdown +title: "Graceful Shutdown Example" +sidebar_label: "Graceful Shutdown" +--- + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/GracefulShutdown.scala") +``` diff --git a/docs/examples/hello-world.md b/docs/examples/hello-world.md new file mode 100644 index 000000000..8b5d4d574 --- /dev/null +++ b/docs/examples/hello-world.md @@ -0,0 +1,37 @@ +--- +id: hello-world +title: "Hello World Example" +sidebar_label: "Hello World" +--- + +## Simple Example + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/HelloWorld.scala") +``` + +## Advanced Example + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/HelloWorldAdvanced.scala") +``` + +## Advanced with CORS Example + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/HelloWorldWithCORS.scala") +``` + +## Advanced with Middlewares Example + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/HelloWorldWithMiddlewares.scala") +``` diff --git a/docs/examples/html-templating.md b/docs/examples/html-templating.md new file mode 100644 index 000000000..862fac645 --- /dev/null +++ b/docs/examples/html-templating.md @@ -0,0 +1,11 @@ +--- +id: html-templating +title: "HTML Templating Example" +sidebar_label: "HTML Templating" +--- + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/HtmlTemplating.scala") +``` diff --git a/docs/examples/http-client-server.md b/docs/examples/http-client-server.md new file mode 100644 index 000000000..ab0acf542 --- /dev/null +++ b/docs/examples/http-client-server.md @@ -0,0 +1,21 @@ +--- +id: http-client-server +title: HTTP Client-Server Example +sidebar_label: HTTP Client-Server +--- + +## Client and Server Example + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/ClientServer.scala") +``` + +## Simple Client Example + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/SimpleClient.scala") +``` diff --git a/docs/examples/https-client-server.md b/docs/examples/https-client-server.md new file mode 100644 index 000000000..19b1ad1ce --- /dev/null +++ b/docs/examples/https-client-server.md @@ -0,0 +1,21 @@ +--- +id: https-client-server +title: HTTPS Client and Server Example +sidebar_label: Https Client and Server +--- + +## Client Example + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/HttpsClient.scala") +``` + +## Server Example + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/HttpsHelloWorld.scala") +``` diff --git a/docs/examples/middleware-cors-handling.md b/docs/examples/middleware-cors-handling.md new file mode 100644 index 000000000..eae54fffa --- /dev/null +++ b/docs/examples/middleware-cors-handling.md @@ -0,0 +1,11 @@ +--- +id: middleware-cors-handling +title: "Middleware CORS Handling Example" +sidebar_label: "Middleware CORS Handling" +--- + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/HelloWorldWithCORS.scala") +``` diff --git a/docs/examples/multipart-form-data.md b/docs/examples/multipart-form-data.md new file mode 100644 index 000000000..8571f5fea --- /dev/null +++ b/docs/examples/multipart-form-data.md @@ -0,0 +1,21 @@ +--- +id: multipart-form-data +title: "Multipart Form Data Example" +sidebar_label: "Multipart Form Data" +--- + +## Multipart Form Data Example + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/MultipartFormData.scala") +``` + +## Multipart Form Data Streaming Example + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/MultipartFormDataStreaming.scala") +``` diff --git a/docs/examples/server-sent-events-in-endpoints.md b/docs/examples/server-sent-events-in-endpoints.md new file mode 100644 index 000000000..925cbb79c --- /dev/null +++ b/docs/examples/server-sent-events-in-endpoints.md @@ -0,0 +1,11 @@ +--- +id: server-sent-events-in-endpoints +title: "Server Sent Events in Endpoints Example" +sidebar_label: "Server Sent Events in Endpoints" +--- + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/ServerSentEventEndpoint.scala") +``` diff --git a/docs/examples/serving-static-files.md b/docs/examples/serving-static-files.md new file mode 100644 index 000000000..a12081f75 --- /dev/null +++ b/docs/examples/serving-static-files.md @@ -0,0 +1,21 @@ +--- +id: serving-static-files +title: "Serving Static Files Example" +sidebar_label: "Serving Static Files" +--- + +## Serving Static Files + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/StaticFiles.scala") +``` + +## Serving Static Resource Files + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/StaticServer.scala") +``` diff --git a/docs/examples/static-files.md b/docs/examples/static-files.md new file mode 100644 index 000000000..b10b5321b --- /dev/null +++ b/docs/examples/static-files.md @@ -0,0 +1,11 @@ +--- +id: static-files +title: "Serving Static Files" +sidebar_label: "Static Files" +--- + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/StaticFiles.scala") +``` diff --git a/docs/examples/streaming.md b/docs/examples/streaming.md new file mode 100644 index 000000000..4b1140312 --- /dev/null +++ b/docs/examples/streaming.md @@ -0,0 +1,29 @@ +--- +id: streaming +title: "Streaming Examples" +sidebar_label: "Streaming" +--- + +## Streaming Request + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/RequestStreaming.scala") +``` + +## Streaming Response + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/StreamingResponse.scala") +``` + +## Streaming File + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/FileStreaming.scala") +``` diff --git a/docs/examples/websocket.md b/docs/examples/websocket.md new file mode 100644 index 000000000..ca6746d7e --- /dev/null +++ b/docs/examples/websocket.md @@ -0,0 +1,63 @@ +--- +id: websocket +title: "WebSocket Example" +sidebar_label: "WebSocket Server & Client" +--- + +This example shows how to create a WebSocket server using ZIO Http and how to write a client to connect to it. + +## Server + +First we define a `WebSocketApp` that will handle the WebSocket connection. +The `Handler.webSocket` constructor gives access to the `WebSocketChannel`. The channel can be used to receive messages from the client and send messages back. +We use the `receiveAll` method, to pattern match on the different channel events that could occur. +The most important events are `Read` and `UserEventTriggered`. The `Read` event is triggered when the client sends a message to the server. The `UserEventTriggered` event is triggered when the connection is established. +We can identify the successful connection of a client by receiving a `UserEventTriggered(UserEvent.HandshakeComplete)` event. And if the client sends us a text message, we will receive a `Read(WebSocketFrame.Text())` event. + +Our WebSocketApp will handle the following events send by the client: +* If the client connects to the server, we will send a "Greetings!" message to the client. +* If the client sends "foo", we will send a "bar" message back to the client. +* If the client sends "bar", we will send a "foo" message back to 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. + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/WebSocketAdvanced.scala", lines=Seq((3, 7), (9, 60)), showLineNumbers=false) +``` + +A few things worth noting: +* `Server.default` starts a server on port 8080. +* `socketApp.toResponse` converts the `WebSocketApp` to a `Response`, so we can serve it with `handler`. + + +## Client + +The client will connect to the server and send a message to the server every time the user enters a message in the console. +For this we will use the `Console.readLine` method to read a line from the console. We will then send the message to the server using the `WebSocketChannel.send` method. +But since we don't want to reconnect to the server every time the user enters a message, we will use a `Queue` to store the messages. We will then use the `Queue.take` method to take a message from the queue and send it to the server, whenever a new message is available. +Adding a new message to the queue, as well as sending the messages to the server, should happen in a loop in the background. For this we will use the operators `forever` (looping) and `forkDaemon` (fork to a background fiber). + +Again we will use the `Handler.webSocket` constructor to define how to handle messages and create a `WebSocketApp`. But this time, instead of serving the `WebSocketApp` we will use the `connect` method to establish a connection to the server. +All we need for that, is the URL of the server. In our case it's `"ws://localhost:8080/subscriptions"`. + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/WebSocketAdvanced.scala", lines=Seq((3, 7), (62, 99)), showLineNumbers=false) +``` + +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. +See [ZIO Services](https://zio.dev/reference/service-pattern/) for more information. + + +## WebSocket Echo + +```scala mdoc:passthrough +import utils._ + +printSource("zio-http-example/src/main/scala/example/WebSocketEcho.scala") +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 4215e9e00..7edf64f37 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -39,33 +39,22 @@ const sidebars = { collapsed: false, link: { type: "doc", id: "index" }, items: [ - { - type: "category", - label: "Basic Examples", - collapsed: false, - items: [ - "examples/basic/http-client", - "examples/basic/https-client", - "examples/basic/http-server", - "examples/basic/https-server", - "examples/basic/websocket", - ] - }, - { - type: "category", - label: "Advanced Examples", - collapsed: false, - items: [ - "examples/advanced/authentication-server", - "examples/advanced/concrete-entity", - "examples/advanced/middleware-basic-authentication", - "examples/advanced/middleware-cors-handling", - "examples/advanced/server", - "examples/advanced/streaming-file", - "examples/advanced/streaming-response", - "examples/advanced/websocket" - ] - } + "examples/hello-world", + "examples/http-client-server", + "examples/https-client-server", + "examples/serving-static-files", + "examples/html-templating", + "examples/websocket", + "examples/streaming", + "examples/endpoint", + "examples/middleware-cors-handling", + "examples/authentication", + "examples/cookies", + "examples/graceful-shutdown", + "examples/cli", + "examples/concrete-entity", + "examples/multipart-form-data", + "examples/server-sent-events-in-endpoints", ] } ] diff --git a/zio-http-docs/src/main/scala/utils.scala b/zio-http-docs/src/main/scala/utils.scala new file mode 100644 index 000000000..016d4c6b0 --- /dev/null +++ b/zio-http-docs/src/main/scala/utils.scala @@ -0,0 +1,44 @@ +import scala.io.Source + +object utils { + + def readSource(path: String, lines: Seq[(Int, Int)]): String = { + if (lines.isEmpty) { + val source = Source.fromFile("../" + path) + val content = source.getLines().mkString("\n") + content + } else { + val chunks = for { + (from, to) <- lines + source = Source.fromFile("../" + path) + content = source.getLines().toArray[String] + } yield content.slice(from - 1, to).mkString("\n") + + chunks.mkString("\n\n") + } + } + + def fileExtension(path: String): String = { + val javaPath = java.nio.file.Paths.get(path) + val fileExtension = + javaPath.getFileName.toString + .split('.') + .lastOption + .getOrElse("") + fileExtension + } + + def printSource( + path: String, + lines: Seq[(Int, Int)] = Seq.empty, + comment: Boolean = true, + showLineNumbers: Boolean = true, + ) = { + val title = if (comment) s"""title="$path"""" else "" + val showLines = if (showLineNumbers) "showLineNumbers" else "" + println(s"""```${fileExtension(path)} ${title} ${showLines}"""") + println(readSource(path, lines)) + println("```") + } + +} diff --git a/zio-http-example/src/main/scala/example/HttpsHelloWorld.scala b/zio-http-example/src/main/scala/example/HttpsHelloWorld.scala index 19224e692..12de2f40e 100644 --- a/zio-http-example/src/main/scala/example/HttpsHelloWorld.scala +++ b/zio-http-example/src/main/scala/example/HttpsHelloWorld.scala @@ -12,10 +12,20 @@ object HttpsHelloWorld extends ZIOAppDefault { ).toHttpApp /** - * In this example an inbuilt API using keystore is used. For testing this - * example using curl, setup the certificate named "server.crt" from resources - * for the OS. Alternatively you can create the keystore and certificate using - * the following link + * In this example, a private key and certificate are loaded from resources. + * For testing this example with curl, make sure the private key "server.key", + * and the certificate "server.crt" are inside the resources directory, which + * is by default "src/main/resources". + * + * You can use the following command to create a self-signed TLS certificate. + * This command will create two files: "server.key" and "server.crt". + * + * openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ -keyout + * server.key -out server.crt \ -subj "/CN=example.com/OU=?/O=?/L=?/ST=?/C=??" + * \ -addext "subjectAltName=DNS:example.com,DNS:www.example.com,IP:10.0.0.1" + * + * Alternatively you can create the keystore and certificate using the + * following link * https://medium.com/@maanadev/netty-with-https-tls-9bf699e07f01 */