Skip to content

Commit

Permalink
Add JSON API tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
sake92 committed Dec 22, 2023
1 parent bf1cf27 commit 492d42b
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 18 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

Your new favorite, simple, intuitive, batteries-included web framework.

Still WIP :construction: but very much usable. :construction_worker:
WIP :construction: but very much usable. :construction_worker:
10 changes: 8 additions & 2 deletions docs/src/files/tutorials/HelloWorld.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ object HelloWorld extends TutorialPage {
Let's make a quick Hello World example in scala-cli.
Create a file `hello_sharaf.sc` and paste this code into it:
```scala
//> using scala "3.3.1"
//> using dep ba.sake::sharaf:${Consts.ArtifactVersion}

import io.undertow.Undertow
Expand All @@ -28,10 +29,10 @@ object HelloWorld extends TutorialPage {
Response.withBody(s"Hello $$name")

val server = Undertow
.builder()
.builder
.addHttpListener(8181, "localhost")
.setHandler(SharafHandler(routes))
.build()
.build

server.start()

Expand All @@ -44,6 +45,11 @@ object HelloWorld extends TutorialPage {
```
Then you can go to [http://localhost:8181/hello/Bob](http://localhost:8181/hello/Bob)
to try it out.

---
The most interesting part is the `Routes` definition.
Here we pattern match on `(HttpMethod, Path)`.
The `Path` contains a `Seq[String]`, which are the parts of the URL you can match on.
""".md,
)
)
Expand Down
107 changes: 107 additions & 0 deletions docs/src/files/tutorials/JsonAPI.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package files.tutorials

import utils.*
import Bundle.*, Tags.*

object JsonAPI extends TutorialPage {

override def pageSettings = super.pageSettings
.withTitle("JSON API")

override def blogSettings =
super.blogSettings.withSections(modelSection, routesSection, runSection)

val modelSection = Section(
"Model definition",
s"""
Let's make a simple JSON API in scala-cli.
Create a file `json_api.sc` and paste this code into it:
```scala
//> using scala "3.3.1"
//> using dep ba.sake::sharaf:${Consts.ArtifactVersion}

import io.undertow.Undertow
import ba.sake.tupson.JsonRW
import ba.sake.sharaf.*, routing.*

case class Car(brand: String, model: String, quantity: Int) derives JsonRW

var db: Seq[Car] = Seq()
```

Here we defined a `Car` model, which `derives JsonRW`, so we can use the JSON support from Sharaf.

We also use a `var db: Seq[Car]` to store our data.
(don't do this for real projects)
""".md
)

val routesSection = Section(
"Routes definition",
s"""
Next step is to define a few routes for getting and adding cars:
```scala
val routes = Routes {
case GET() -> Path("cars") =>
Response.withBody(db)

case GET() -> Path("cars", brand) =>
val res = db.filter(_.brand == brand)
Response.withBody(res)

case POST() -> Path("cars") =>
val qp = Request.current.bodyJson[Car]
db = db.appended(qp)
Response.withBody(db)
}
```
The first route just returns all data in the "database".
The second route does some filtering on the database.
The third route binds the JSON body from the HTTP request.
And then we add it to the database.
""".md
)

val runSection = Section(
"Running the server",
s"""
Finally, we need to start up the server:
```scala
val server = Undertow
.builder
.addHttpListener(8181, "localhost")
.setHandler(SharafHandler(routes))
.build

server.start()

println(s"Server started at http://localhost:8181")
```

and run it like this:
```sh
scala-cli json_api.sc
```

Then you can try the following requests:
```sh
# get all cars
curl http://localhost:8181/cars

# add a car
curl --request POST \\
--url http://localhost:8181/cars \\
--data '{
"brand": "Mercedes",
"model": "ML350",
"quantity": 1
}'

# get cars by brand
curl http://localhost:8181/cars/Mercedes
```
""".md
)
}
3 changes: 2 additions & 1 deletion docs/src/files/tutorials/TutorialPage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ trait TutorialPage extends DocPage {

override def categoryPosts = List(
Index,
HelloWorld
HelloWorld,
JsonAPI
)

override def pageCategory = Some("Tutorials")
Expand Down
6 changes: 3 additions & 3 deletions examples/scala-cli/hello.sc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//> using scala "3.3.1"
//> using dep ba.sake::sharaf:0.0.15
//> using dep ba.sake::sharaf:0.0.17

import io.undertow.Undertow
import ba.sake.sharaf.*, routing.*
Expand All @@ -9,10 +9,10 @@ val routes = Routes:
Response.withBody(s"Hello $name")

val server = Undertow
.builder()
.builder
.addHttpListener(8181, "localhost")
.setHandler(SharafHandler(routes))
.build()
.build

server.start()

Expand Down
33 changes: 33 additions & 0 deletions examples/scala-cli/json_api.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//> using scala "3.3.1"
//> using dep ba.sake::sharaf:0.0.17

import io.undertow.Undertow
import ba.sake.tupson.JsonRW
import ba.sake.sharaf.*, routing.*

case class Car(brand: String, model: String, quantity: Int) derives JsonRW

var db: Seq[Car] = Seq()

val routes = Routes {
case GET() -> Path("cars") =>
Response.withBody(db)

case GET() -> Path("cars", brand) =>
val res = db.filter(_.brand == brand)
Response.withBody(res)

case POST() -> Path("cars") =>
val qp = Request.current.bodyJson[Car]
db = db.appended(qp)
Response.withBody(db)
}

val server = Undertow.builder
.addHttpListener(8181, "localhost")
.setHandler(SharafHandler(routes))
.build

server.start()

println(s"Server started at http://localhost:8181")
2 changes: 1 addition & 1 deletion querson/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Singleton-cases enums are supported, nesting etc:
enum SortOrderQS derives QueryStringRW:
case asc, desc

case class PageQS() derives QueryStringRW
case class PageQS(num: Int, size: Int) derives QueryStringRW

// these are specific for users for example
enum SortByQS derives QueryStringRW:
Expand Down
20 changes: 10 additions & 10 deletions sharaf/src/ba/sake/sharaf/Request.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import io.undertow.server.handlers.form.FormData as UFormData
import io.undertow.server.handlers.form.FormParserFactory
import io.undertow.util.HttpString

import ba.sake.tupson, tupson.*
import ba.sake.formson, formson.*
import ba.sake.querson, querson.*
import ba.sake.validson, validson.*
import ba.sake.tupson.*
import ba.sake.formson.*
import ba.sake.querson.*
import ba.sake.validson.*

final class Request private (
private val ex: HttpServerExchange
Expand All @@ -28,11 +28,11 @@ final class Request private (

def queryParams[T <: Product: QueryStringRW]: T =
try queryParamsMap.parseQueryStringMap
catch case e: querson.ParsingException => throw RequestHandlingException(e)
catch case e: QuersonException => throw RequestHandlingException(e)

def queryParamsValidated[T <: Product: QueryStringRW: Validator]: T =
try queryParams[T].validateOrThrow
catch case e: validson.ValidationException => throw RequestHandlingException(e)
catch case e: ValidationException => throw RequestHandlingException(e)

/* BODY */
private val formBodyParserFactory = locally {
Expand All @@ -47,11 +47,11 @@ final class Request private (
// JSON
def bodyJson[T: JsonRW]: T =
try bodyString.parseJson[T]
catch case e: tupson.ParsingException => throw RequestHandlingException(e)
catch case e: TupsonException => throw RequestHandlingException(e)

def bodyJsonValidated[T: JsonRW: Validator]: T =
try bodyJson[T].validateOrThrow
catch case e: validson.ValidationException => throw RequestHandlingException(e)
catch case e: ValidationException => throw RequestHandlingException(e)

// FORM
def bodyForm[T <: Product: FormDataRW]: T =
Expand All @@ -63,11 +63,11 @@ final class Request private (
val uFormData = parser.parseBlocking()
val formDataMap = Request.undertowFormData2FormsonMap(uFormData)
try formDataMap.parseFormDataMap[T]
catch case e: formson.ParsingException => throw RequestHandlingException(e)
catch case e: FormsonException => throw RequestHandlingException(e)

def bodyFormValidated[T <: Product: FormDataRW: Validator]: T =
try bodyForm[T].validateOrThrow
catch case e: validson.ValidationException => throw RequestHandlingException(e)
catch case e: ValidationException => throw RequestHandlingException(e)

/* HEADERS */
def headers: Map[HttpString, Seq[String]] =
Expand Down

0 comments on commit 492d42b

Please sign in to comment.