diff --git a/DEV.md b/DEV.md index 904e25e..d53958c 100644 --- a/DEV.md +++ b/DEV.md @@ -18,7 +18,7 @@ scala-cli compile examples\scala-cli ```sh # RELEASE -$VERSION="0.12.0" +$VERSION="0.13.0" git commit --allow-empty -m "Release $VERSION" git tag -a $VERSION -m "Release $VERSION" git push --atomic origin main $VERSION diff --git a/TODO.md b/TODO.md index ab80f82..453699a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,5 @@ # TODO -- some kind of middleware mechanism - - MiMa bin compat - add more validators https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/package-summary.html diff --git a/build.mill b/build.mill index 70e65fb..a37f4c6 100644 --- a/build.mill +++ b/build.mill @@ -8,14 +8,13 @@ import mill.vcs.VcsVersion object V: val tupson = "0.13.0" - val scalatags = "0.13.1" val hepek = "0.33.0" object `sharaf-core` extends Module: object jvm extends SharafCoreModule with ScalaJvmCommonModule: def moduleDeps = Seq(querson.jvm, formson.jvm, validson.jvm) def mvnDeps = super.mvnDeps() ++ Seq( - // TODO move to common when published for native, and remove scalatags + // TODO move to common when published for native mvn"org.playframework.twirl::twirl-api:2.1.0-M4" ) object test extends ScalaTests with SharafTestModule @@ -29,7 +28,6 @@ object `sharaf-core` extends Module: // all deps should be cross jvm/native def mvnDeps = super.mvnDeps() ++ Seq( mvn"ba.sake::tupson::${V.tupson}", - mvn"com.lihaoyi::scalatags::${V.scalatags}", mvn"com.lihaoyi::geny::1.1.1", mvn"com.softwaremill.sttp.client4::core::4.0.5" ) @@ -38,8 +36,7 @@ object `sharaf-undertow` extends SharafPublishModule: def artifactName = "sharaf-undertow" def mvnDeps = super.mvnDeps() ++ Seq( mvn"io.undertow:undertow-core:2.3.18.Final", - mvn"ba.sake::tupson-config:${V.tupson}", - mvn"ba.sake::hepek-components:${V.hepek}" + mvn"ba.sake::tupson-config:${V.tupson}" ) def moduleDeps = Seq(`sharaf-core`.jvm) object test extends ScalaTests with SharafTestModule : @@ -67,6 +64,18 @@ object `sharaf-snunit` extends ScalaNativeCommonModule with SharafPublishModule: ) def moduleDeps = Seq(`sharaf-core`.native) +object `sharaf-hepek-components` extends Module: + object jvm extends SharafHepekComponentsCoreModule with ScalaJvmCommonModule: + def moduleDeps = Seq(`sharaf-core`.jvm) + //object native extends SharafHepekComponentsCoreModule with ScalaNativeCommonModule: + // def moduleDeps = Seq(`sharaf-core`.native) + + trait SharafHepekComponentsCoreModule extends SharafPublishModule with PlatformScalaModule: + def artifactName = "sharaf-hepek-components" + def mvnDeps = super.mvnDeps() ++ Seq( + mvn"ba.sake::hepek-components:${V.hepek}" + ) + object querson extends Module: object jvm extends QuersonModule with ScalaJvmCommonModule: object test extends ScalaTests with SharafTestModule diff --git a/docs/_data/project.yaml b/docs/_data/project.yaml index a3ed06e..548efc5 100644 --- a/docs/_data/project.yaml +++ b/docs/_data/project.yaml @@ -9,5 +9,5 @@ gh: artifact: org: "ba.sake" name: "sharaf-undertow" - version: "0.11.1" + version: "0.12.1" diff --git a/docs/_includes/hello.sc b/docs/_includes/hello.sc new file mode 100644 index 0000000..3e265ac --- /dev/null +++ b/docs/_includes/hello.sc @@ -0,0 +1,13 @@ +//> using scala "3.7.0" +//> using dep ba.sake::sharaf-undertow:0.12.1 + +import ba.sake.sharaf.* +import ba.sake.sharaf.undertow.UndertowSharafServer + +val routes = Routes: + case GET -> Path("hello", name) => + Response.withBody(s"Hello $name") + +UndertowSharafServer("localhost", 8181, routes).start() + +println(s"Server started at http://localhost:8181") diff --git a/docs/_includes/json_api.test.scala b/docs/_includes/json_api.test.scala new file mode 100644 index 0000000..541e66e --- /dev/null +++ b/docs/_includes/json_api.test.scala @@ -0,0 +1,37 @@ +//> using scala "3.7.0" +//> using dep ba.sake::tupson:0.13.0 +//> using dep com.lihaoyi::requests:0.9.0 +//> using test.dep org.scalameta::munit::1.1.1 + +import ba.sake.tupson.* + +case class Car(brand: String, model: String, quantity: Int) derives JsonRW + +class JsonApiSuite extends munit.FunSuite { + + val baseUrl = "http://localhost:8181" + + test("create and get cars") { + locally { + val res = requests.get(s"$baseUrl/cars") + val resBody = res.text.parseJson[Seq[Car]] + assertEquals(res.statusCode, 200) + assertEquals(res.headers("content-type"), Seq("application/json; charset=utf-8")) + assertEquals(res.text.parseJson[Seq[Car]], Seq.empty) + } + + locally { + val body = Car("Mercedes", "ML350", 1) + val res = requests.post(s"$baseUrl/cars", data = body.toJson) + assertEquals(res.statusCode, 200) + } + + locally { + val res = requests.get(s"$baseUrl/cars/Mercedes") + val resBody = res.text.parseJson[Seq[Car]] + assertEquals(res.statusCode, 200) + assertEquals(res.headers("content-type"), Seq("application/json; charset=utf-8")) + assertEquals(resBody, Seq(Car("Mercedes", "ML350", 1))) + } + } +} diff --git a/docs/_includes/path_params.sc b/docs/_includes/path_params.sc new file mode 100644 index 0000000..8635334 --- /dev/null +++ b/docs/_includes/path_params.sc @@ -0,0 +1,16 @@ +//> using scala "3.7.0" +//> using dep ba.sake::sharaf-undertow:0.12.1 + +import ba.sake.sharaf.* +import ba.sake.sharaf.undertow.UndertowSharafServer + +val routes = Routes: + case GET -> Path("string", x) => + Response.withBody(s"string = ${x}") + + case GET -> Path("int", param[Int](x)) => + Response.withBody(s"int = ${x}") + +UndertowSharafServer("localhost", 8181, routes).start() + +println(s"Server started at http://localhost:8181") diff --git a/docs/_includes/query_params.sc b/docs/_includes/query_params.sc new file mode 100644 index 0000000..65f226c --- /dev/null +++ b/docs/_includes/query_params.sc @@ -0,0 +1,20 @@ +//> using scala "3.7.0" +//> using dep ba.sake::sharaf-undertow:0.12.1 + +import ba.sake.querson.QueryStringRW +import ba.sake.sharaf.* +import ba.sake.sharaf.undertow.UndertowSharafServer + +val routes = Routes: + case GET -> Path("raw") => + val qp = Request.current.queryParamsRaw + Response.withBody(s"params = ${qp}") + + case GET -> Path("typed") => + case class SearchParams(q: String, perPage: Int) derives QueryStringRW + val qp = Request.current.queryParams[SearchParams] + Response.withBody(s"params = ${qp}") + +UndertowSharafServer("localhost", 8181, routes).start() + +println(s"Server started at http://localhost:8181") diff --git a/docs/_includes/static_files.sc b/docs/_includes/static_files.sc new file mode 100644 index 0000000..5194d6e --- /dev/null +++ b/docs/_includes/static_files.sc @@ -0,0 +1,13 @@ +//> using scala "3.7.0" +//> using dep ba.sake::sharaf-undertow:0.12.1 + +import ba.sake.sharaf.* +import ba.sake.sharaf.undertow.UndertowSharafServer + +val routes = Routes: + case GET -> Path() => + Response.withBody("Try /example.js") + +UndertowSharafServer("localhost", 8181, routes).start() + +println(s"Server started at http://localhost:8181") diff --git a/docs/_includes/validation.sc b/docs/_includes/validation.sc new file mode 100644 index 0000000..4a16a1d --- /dev/null +++ b/docs/_includes/validation.sc @@ -0,0 +1,37 @@ +//> using scala "3.7.0" +//> using dep ba.sake::sharaf-undertow:0.12.1 + +import ba.sake.querson.QueryStringRW +import ba.sake.tupson.JsonRW +import ba.sake.validson.Validator +import ba.sake.sharaf.* +import ba.sake.sharaf.undertow.UndertowSharafServer + +val routes = Routes: + case GET -> Path("cars") => + val qp = Request.current.queryParamsValidated[CarQuery] + Response.withBody(CarApiResult(s"Query OK: ${qp}")) + + case POST -> Path("cars") => + val json = Request.current.bodyJsonValidated[Car] + Response.withBody(CarApiResult(s"JSON body OK: ${json}")) + +UndertowSharafServer("localhost", 8181, routes, exceptionMapper = ExceptionMapper.json).start() + +println(s"Server started at http://localhost:8181") + +case class Car(brand: String, model: String, quantity: Int) derives JsonRW +object Car: + given Validator[Car] = Validator + .derived[Car] + .notBlank(_.brand) + .notBlank(_.model) + .nonNegative(_.quantity) + +case class CarQuery(brand: String) derives QueryStringRW +object CarQuery: + given Validator[CarQuery] = Validator + .derived[CarQuery] + .notBlank(_.brand) + +case class CarApiResult(message: String) derives JsonRW diff --git a/docs/content/howtos/cors.md b/docs/content/howtos/cors.md index 5d5e423..aebdf87 100644 --- a/docs/content/howtos/cors.md +++ b/docs/content/howtos/cors.md @@ -8,12 +8,15 @@ description: Sharaf How To CORS By default, Sharaf sets no permitted origins. This means you can only use the API/website from the same domain. -If you want to configure it to be available for other domains, -use the `withCorsSettings` method and set desired config: +If you want it to be available for other domains, +use the `corsSettings` parameter to set desired config: ```scala -import ba.sake.sharaf.handlers.cors.CorsSettings -import ba.sake.sharaf.* - val corsSettings = CorsSettings.default.withAllowedOrigins(Set("https://example.com")) -UndertowSharafServer(routes).withCorsSettings(corsSettings)... + +val server = UndertowSharafServer( + "localhost", + port, + routes, + corsSettings = corsSettings + ) ``` diff --git a/docs/content/howtos/exception-handler.md b/docs/content/howtos/exception-handler.md index d72c101..34cc98c 100644 --- a/docs/content/howtos/exception-handler.md +++ b/docs/content/howtos/exception-handler.md @@ -7,7 +7,7 @@ description: Sharaf How To Exception Handler How to customize the Exception handler? -Use the `withExceptionMapper` on `UndertowSharafServer`: +Use the `exceptionMapper` parameter of `UndertowSharafServer`: ```scala val customExceptionMapper: ExceptionMapper = { case e: MyException => @@ -16,7 +16,13 @@ val customExceptionMapper: ExceptionMapper = { .withStatus(StatusCode.InternalServerError) } val finalExceptionMapper = customExceptionMapper.orElse(ExceptionMapper.default) -val server = UndertowSharafServer(routes).withExceptionMapper(finalExceptionMapper) + +val server = UndertowSharafServer( + "localhost", + port, + routes, + exceptionMapper = finalExceptionMapper + ) ``` The `ExceptionMapper` is a partial function from an exception to `Response`. diff --git a/docs/content/howtos/not-found.md b/docs/content/howtos/not-found.md index e68efba..a0b3ace 100644 --- a/docs/content/howtos/not-found.md +++ b/docs/content/howtos/not-found.md @@ -8,15 +8,20 @@ description: Sharaf How To NotFound How to customize 404 NotFound handler? -Use the `withNotFoundHandler` on `UndertowSharafServer`: +Use the `notFoundHandler` parameter of `UndertowSharafServer`: ```scala -UndertowSharafServer(routes).withNotFoundHandler { req => +val customNotFoundHandler: Request => Response[?] = req => Response.withBody(MyCustomNotFoundPage) .withStatus(StatusCode.NotFound) -} + +val server = UndertowSharafServer( + "localhost", + port, + routes, + notFoundHandler = customNotFoundHandler + ) ``` -The `withNotFoundHandler` accepts a `Request => Response[?]` parameter. You can use the request if you need to dynamically decide on what to return. Or ignore it and return a static not found response. diff --git a/docs/content/howtos/upload-file.md b/docs/content/howtos/upload-file.md index f680c06..5d66858 100644 --- a/docs/content/howtos/upload-file.md +++ b/docs/content/howtos/upload-file.md @@ -7,12 +7,15 @@ description: Sharaf How To Upload File Uploading a file is usually done via `multipart/form-data` form submission. +{% +set form_snippet = '
+... +
' +%} ```scala // 1. somewhere in a view, use enctype="multipart/form-data" -form(action := "/form-submit", method := "POST", enctype := "multipart/form-data")( - ... -) +{{ form_snippet | e }} // 2. define form data class with a NIO Path file import java.nio.file.Path diff --git a/docs/content/philosophy/index.md b/docs/content/philosophy/index.md index ce08dcd..0e27b3d 100644 --- a/docs/content/philosophy/index.md +++ b/docs/content/philosophy/index.md @@ -20,7 +20,7 @@ Sharaf bundles a set of standalone libraries: - [tupson](https://github.com/sake92/tupson) for JSON - [formson]({{site.data.project.gh.sourcesUrl}}/formson) for forms - [validson]({{site.data.project.gh.sourcesUrl}}/validson) for validation -- [scalatags](https://github.com/com-lihaoyi/scalatags) for HTML +- [html interpolator](https://github.com/playframework/twirl) for HTML - [sttp](https://sttp.softwaremill.com/en/latest/) for firing HTTP requests - [typesafe-config](https://github.com/lightbend/config) for configuration diff --git a/docs/content/philosophy/routes-matching.md b/docs/content/philosophy/routes-matching.md index 7047b2d..09abbd0 100644 --- a/docs/content/philosophy/routes-matching.md +++ b/docs/content/philosophy/routes-matching.md @@ -8,11 +8,11 @@ description: Sharaf Routes Matching ## Routes matching design - Web frameworks do their routes matching with various mechanisms: - - annotation + method param: [Spring](https://spring.io/guides/tutorials/rest/) and most other popular Java frameworks, [Cask](https://com-lihaoyi.github.io/cask/) etc - - special route file DSL: [PlayFramework](https://www.playframework.com/documentation/2.9.x/ScalaRouting#The-routes-file-syntax), Ruby on Rails - - in-language DSL: zio-http, akka-http - - pattern matching: Sharaf, http4s +Web frameworks do their routes matching with various mechanisms: +- annotation + method param: [Spring](https://spring.io/guides/tutorials/rest/) and most other popular Java frameworks, [Cask](https://com-lihaoyi.github.io/cask/) etc +- special route file DSL: [PlayFramework](https://www.playframework.com/documentation/2.9.x/ScalaRouting#The-routes-file-syntax), Ruby on Rails +- in-language DSL: zio-http, akka-http +- pattern matching: Sharaf, http4s ## Why not annotations? diff --git a/docs/content/reference/index.md b/docs/content/reference/index.md index 4576751..7581911 100644 --- a/docs/content/reference/index.md +++ b/docs/content/reference/index.md @@ -6,5 +6,7 @@ description: Sharaf Reference # {{ page.title }} -Take a look at [Sharaf scaladoc](https://javadoc.io/doc/ba.sake/sharaf_3). +Take a look at. +- [Sharaf Core scaladoc](https://javadoc.io/doc/ba.sake/sharaf-core_3/latest/index.html) +- [Sharaf Undertow scaladoc](https://javadoc.io/doc/ba.sake/sharaf-undertow_3/latest/index.html) diff --git a/docs/content/tutorials/forms.md b/docs/content/tutorials/forms.md index 563fbdc..80f7b1b 100644 --- a/docs/content/tutorials/forms.md +++ b/docs/content/tutorials/forms.md @@ -11,19 +11,39 @@ Form data can be extracted with `Request.current.bodyForm[MyData]`. The `MyData` needs to have a `FormDataRW` given instance. Create a file `form_handling.sc` and paste this code into it: + +{# need to HTML encode these snippets, so that Markdown doesnt process them! #} +{% set contact_us_view = 'html""" + + + +
+
+ +
+
+ +
+ +
+ + + """' +%} + ```scala //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 -import scalatags.Text.all.* import ba.sake.formson.FormDataRW -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val routes = Routes: case GET -> Path() => Response.withBody(ContactUsView) case POST -> Path("handle-form") => + case class ContactUsForm(fullName: String, email: String) derives FormDataRW val formData = Request.current.bodyForm[ContactUsForm] Response.withBody(s"Got form data: ${formData}") @@ -31,30 +51,13 @@ UndertowSharafServer("localhost", 8181, routes).start() println("Server started at http://localhost:8181") - -def ContactUsView = doctype("html")( - html( - body( - form(action := "/handle-form", method := "POST")( - div( - label("Full Name: ", input(name := "fullName", autofocus)) - ), - div( - label("Email: ", input(name := "email", tpe := "email")) - ), - input(tpe := "Submit") - ) - ) - ) -) - -case class ContactUsForm(fullName: String, email: String) derives FormDataRW - +def ContactUsView = + {{ contact_us_view | e }} ``` Then run it like this: ```sh -scala-cli form_handling.sc +scala form_handling.sc ``` Now go to [http://localhost:8181](http://localhost:8181) diff --git a/docs/content/tutorials/hello-world.md b/docs/content/tutorials/hello-world.md index 8158231..eecb120 100644 --- a/docs/content/tutorials/hello-world.md +++ b/docs/content/tutorials/hello-world.md @@ -9,24 +9,12 @@ description: Sharaf Tutorial Hello World Let's make a Hello World example with scala-cli. Create a file `hello.sc` and paste this code into it: ```scala -//> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 - -import ba.sake.sharaf.* -import ba.sake.sharaf.undertow.UndertowSharafServer - -val routes = Routes: - case GET -> Path("hello", name) => - Response.withBody(s"Hello $name") - -UndertowSharafServer("localhost", 8181, routes).start() - -println(s"Server started at http://localhost:8181") +{% include "hello.sc" %} ``` Then run it like this: ```sh -scala-cli hello.sc +scala hello.sc ``` Go to http://localhost:8181/hello/Bob. You will see a "Hello Bob" text response. diff --git a/docs/content/tutorials/html.md b/docs/content/tutorials/html.md index 42ac955..7e9dc01 100644 --- a/docs/content/tutorials/html.md +++ b/docs/content/tutorials/html.md @@ -5,68 +5,43 @@ description: Sharaf Tutorial HTML # {{ page.title }} -You can return a scalatags `doctype` directly in the `Response.withBody()`. +You can make an HTML snippet by using the `html""` interpolator. +Then you return it directly in the `Response.withBody()`. + Let's make a simple HTML page that greets the user. Create a file `html.sc` and paste this code into it: -```scala -//> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 - -import scalatags.Text.all.* -import ba.sake.sharaf.* -import ba.sake.sharaf.undertow.UndertowSharafServer - -val routes = Routes: - case GET -> Path() => - Response.withBody(IndexView) - case GET -> Path("hello", name) => - Response.withBody(HelloView(name)) -UndertowSharafServer("localhost", 8181, routes).start() - -println(s"Server started at http://localhost:8181") +{# need to HTML encode these snippets, so that Markdown doesnt process them! #} +{% set index_view = 'html""" + + + +
+

Welcome!

+ Hello world +
+ + + """' +%} +{% set hello_view = 'html""" + + + +
+ Hello ${name}! +
+ + + """' +%} -def IndexView = doctype("html")( - html( - p("Welcome!"), - a(href := "/hello/Bob")("Go to /hello/Bob") - ) -) - -def HelloView(name: String) = doctype("html")( - html( - p("Welcome!"), - div("Hello ", b(name), "!") - ) -) -``` - -and run it like this: -```sh -scala-cli html.sc -``` - -Go to http://localhost:8181 -to see how it works. - - -### Hepek Components -Sharaf supports the [hepek-components](https://sake92.github.io/hepek/hepek/components/reference/bundle-reference.html) too. -Hepek wraps scalatags with helpful utilities like Bootstrap 5 templates, form helpers etc. so you can focus on the important stuff. -It is *plain scala code* as a "template engine", so there is no separate language you need to learn. - ---- - -Let's make a simple HTML page that greets the user. -Create a file `html.sc` and paste this code into it: ```scala //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 -import scalatags.Text.all.* -import ba.sake.hepek.html.HtmlPage -import ba.sake.sharaf.* -import ba.sake.sharaf.undertow.{*, given} +import ba.sake.sharaf.{*, given} +import ba.sake.sharaf.undertow.UndertowSharafServer val routes = Routes: case GET -> Path() => @@ -78,24 +53,17 @@ UndertowSharafServer("localhost", 8181, routes).start() println(s"Server started at http://localhost:8181") +def IndexView = + {{ index_view | e }} -object IndexView extends HtmlPage: - override def pageContent = div( - p("Welcome!"), - a(href := "/hello/Bob")("Hello world") - ) - -class HelloView(name: String) extends HtmlPage: - override def pageContent = - div("Hello ", b(name), "!") - +def HelloView(name: String) = + {{ hello_view | e }} ``` and run it like this: ```sh -scala-cli html.sc +scala html.sc ``` -Go to [http://localhost:8181](http://localhost:8181) +Go to http://localhost:8181 to see how it works. - diff --git a/docs/content/tutorials/htmx.md b/docs/content/tutorials/htmx.md index 4a48ee5..5154fe5 100644 --- a/docs/content/tutorials/htmx.md +++ b/docs/content/tutorials/htmx.md @@ -9,54 +9,61 @@ description: Sharaf Tutorial HTMX Instead of going through HTML->JS->JSON-API loop/mess, you can go directly HTML->HTML-API. Basically you just return HTML snippets that get included where you want in your page. -Sharaf is using the [hepek-components](https://sake92.github.io/hepek/hepek/components/reference/bundle-reference.html) -as its template engine, which has support for HTMX attributes. - You can lots of examples in [examples/htmx]({{site.data.project.gh.sourcesUrl}}/examples/htmx) folder. --- Let's make a simple page that triggers a POST request to fetch a HTML snippet. Create a file `htmx_load_snippet.sc` and paste this code into it: + +{# need to HTML encode these snippets, so that Markdown doesnt process them! #} +{% set div_snippet = 'html""" +
+ WOW, it works! ๐Ÿ˜ฒ +
Look ma, no JS! ๐Ÿ˜Ž
+
+ """' +%} +{% set index_view = 'html""" + + + + + + + + + + """' +%} + + ```scala //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val routes = Routes: case GET -> Path() => Response.withBody(IndexView) case POST -> Path("html-snippet") => - Response.withBody( - div( - b("WOW, it works! ๐Ÿ˜ฒ"), - div("Look ma, no JS! ๐Ÿ˜Ž") - ) - ) + Response.withBody: + {{ div_snippet | e }} UndertowSharafServer("localhost", 8181, routes).start() println(s"Server started at http://localhost:8181") -def IndexView = doctype("html")( - html( - head( - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body( - button(hx.post := "/html-snippet", hx.swap := "outerHTML")("Click here!") - ) - ) -) +def IndexView = + {{ index_view | e }} + ``` and run it like this: ```sh -scala-cli html.sc +scala html.sc ``` Go to [http://localhost:8181](http://localhost:8181) diff --git a/docs/content/tutorials/json.md b/docs/content/tutorials/json.md index 6bd422c..82dd49c 100644 --- a/docs/content/tutorials/json.md +++ b/docs/content/tutorials/json.md @@ -12,7 +12,7 @@ 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.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 import ba.sake.tupson.JsonRW import ba.sake.sharaf.* @@ -63,16 +63,14 @@ Then we add it to the database. Finally, start up the server: ```scala -UndertowSharafServer("localhost", 8181, routes) - .withExceptionMapper(ExceptionMapper.json) - .start() +UndertowSharafServer("localhost", 8181, routes, exceptionMapper = ExceptionMapper.json).start() println("Server started at http://localhost:8181") ``` and run it like this: ```sh -scala-cli json_api.sc +scala json_api.sc ``` Then try the following requests: diff --git a/docs/content/tutorials/path-params.md b/docs/content/tutorials/path-params.md index d0d7efe..35d85ca 100644 --- a/docs/content/tutorials/path-params.md +++ b/docs/content/tutorials/path-params.md @@ -9,27 +9,12 @@ Path parameters can be extracted from the `Path(segments: Seq[String])` argument Create a file `path_params.sc` and paste this code into it: ```scala -//> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 - -import ba.sake.sharaf.* -import ba.sake.sharaf.undertow.UndertowSharafServer - -val routes = Routes: - case GET -> Path("string", x) => - Response.withBody(s"string = ${x}") - - case GET -> Path("int", param[Int](x)) => - Response.withBody(s"int = ${x}") - -UndertowSharafServer("localhost", 8181, routes).start() - -println(s"Server started at http://localhost:8181") +{% include "path_params.sc" %} ``` Then run it like this: ```sh -scala-cli path_params.sc +scala path_params.sc ``` --- diff --git a/docs/content/tutorials/query-params.md b/docs/content/tutorials/query-params.md index 43bcf28..6b0b55e 100644 --- a/docs/content/tutorials/query-params.md +++ b/docs/content/tutorials/query-params.md @@ -19,31 +19,12 @@ and then use it like this: `Request.current.queryParams[MyParams]` Create a file `query_params.sc` and paste this code into it: ```scala -//> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 - -import ba.sake.querson.QueryStringRW -import ba.sake.sharaf.* -import ba.sake.sharaf.undertow.UndertowSharafServer - -val routes = Routes: - case GET -> Path("raw") => - val qp = Request.current.queryParamsRaw - Response.withBody(s"params = ${qp}") - - case GET -> Path("typed") => - case class SearchParams(q: String, perPage: Int) derives QueryStringRW - val qp = Request.current.queryParams[SearchParams] - Response.withBody(s"params = ${qp}") - -UndertowSharafServer("localhost", 8181, routes).start() - -println(s"Server started at http://localhost:8181") +{% include "query_params.sc" %} ``` Then run it like this: ```sh -scala-cli query_params.sc +scala query_params.sc ``` --- diff --git a/docs/content/tutorials/quickstart.md b/docs/content/tutorials/quickstart.md index 009d4df..c8bad9f 100644 --- a/docs/content/tutorials/quickstart.md +++ b/docs/content/tutorials/quickstart.md @@ -34,14 +34,14 @@ Create a file `my_script.sc` with the following content: ``` and then run it with: ```bash -scala-cli my_script.sc --scala-option -Yretain-trees +scala my_script.sc --scala-option -Yretain-trees ``` ## Examples -- [scala-cli examples]({{site.data.project.gh.sourcesUrl}}/examples/scala-cli), standalone examples using scala-cli -- [scala-cli HTMX examples]({{site.data.project.gh.sourcesUrl}}/examples/htmx), standalone examples featuring HTMX +- [scala examples]({{site.data.project.gh.sourcesUrl}}/examples/scala-cli), standalone examples using scala-cli +- [scala HTMX examples]({{site.data.project.gh.sourcesUrl}}/examples/htmx), standalone examples featuring HTMX - [API example]({{site.data.project.gh.sourcesUrl}}/examples/api) featuring JSON and validation - [full-stack example]({{site.data.project.gh.sourcesUrl}}/examples/fullstack) featuring HTML, static files and forms - [sharaf-todo-backend](https://github.com/sake92/sharaf-todo-backend), implementation of the [todobackend.com](http://todobackend.com/) spec, featuring CORS handling diff --git a/docs/content/tutorials/sql.md b/docs/content/tutorials/sql.md index 94ea114..db9f44f 100644 --- a/docs/content/tutorials/sql.md +++ b/docs/content/tutorials/sql.md @@ -33,7 +33,7 @@ Create a file `sql_db.sc` and paste this code into it: //> using scala "3.7.0" //> using dep org.postgresql:postgresql:42.7.5 //> using dep com.zaxxer:HikariCP:6.3.0 -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 //> using dep ba.sake::squery:0.7.0 import ba.sake.tupson.JsonRW @@ -88,7 +88,7 @@ println(s"Server started at http://localhost:8181") and run it like this: ```sh -scala-cli sql_db.sc +scala sql_db.sc ``` Then you can try the following requests: diff --git a/docs/content/tutorials/static-files.md b/docs/content/tutorials/static-files.md index eeb1c7a..f89ccbf 100644 --- a/docs/content/tutorials/static-files.md +++ b/docs/content/tutorials/static-files.md @@ -8,7 +8,7 @@ description: Sharaf Tutorial Static Files The static files are automatically served from the `resources/public` folder (on the classpath): - in Mill those are under `my_project/resources/public` - in sbt those are under `src/main/resources/public` -- in scala-cli you need to manually tell it where to look for with `--resource-dir resources` +- in scala you need to manually tell it where to look for with `--resource-dir resources` --- @@ -18,24 +18,12 @@ Put this text into it: `console.log('Hello Sharaf!');`. Now create a file `static_files.sc` and paste this code into it: ```scala -//> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 - -import ba.sake.sharaf.* -import ba.sake.sharaf.undertow.UndertowSharafServer - -val routes = Routes: - case GET -> Path() => - Response.withBody("Try /example.js") - -UndertowSharafServer("localhost", 8181, routes).start() - -println(s"Server started at http://localhost:8181") +{% include "static_files.sc" %} ``` and run it like this: ```sh -scala-cli static_files.sc --resource-dir resources +scala static_files.sc --resource-dir resources ``` Go to http://localhost:8181/example.js. diff --git a/docs/content/tutorials/tests.md b/docs/content/tutorials/tests.md index 5c64956..ab099a0 100644 --- a/docs/content/tutorials/tests.md +++ b/docs/content/tutorials/tests.md @@ -11,52 +11,16 @@ Writing integration tests with Munit and Requests is straightforward. Here we are testing the API from the [JSON API tutorial](/tutorials/json.html#routes-definition). Create a file `json_api.test.scala` and paste this code into it: ```scala -//> using scala "3.7.0" -//> using dep ba.sake::tupson:0.13.0 -//> using dep com.lihaoyi::requests:0.9.0 -//> using test.dep org.scalameta::munit::1.1.1 - -import ba.sake.tupson.* - -case class Car(brand: String, model: String, quantity: Int) derives JsonRW - -class JsonApiSuite extends munit.FunSuite { - - val baseUrl = "http://localhost:8181" - - test("create and get cars") { - locally { - val res = requests.get(s"$baseUrl/cars") - val resBody = res.text.parseJson[Seq[Car]] - assertEquals(res.statusCode, 200) - assertEquals(res.headers("content-type"), Seq("application/json; charset=utf-8")) - assertEquals(res.text.parseJson[Seq[Car]], Seq.empty) - } - - locally { - val body = Car("Mercedes", "ML350", 1) - val res = requests.post(s"$baseUrl/cars", data = body.toJson) - assertEquals(res.statusCode, 200) - } - - locally { - val res = requests.get(s"$baseUrl/cars/Mercedes") - val resBody = res.text.parseJson[Seq[Car]] - assertEquals(res.statusCode, 200) - assertEquals(res.headers("content-type"), Seq("application/json; charset=utf-8")) - assertEquals(resBody, Seq(Car("Mercedes", "ML350", 1))) - } - } -} +{% include "json_api.test.scala" %} ``` First run the API server in one shell: ```sh -scala-cli test json_api.sc +scala test json_api.sc ``` and then run the tests in another shell: ```sh -scala-cli test json_api.test.scala +scala test json_api.test.scala ``` diff --git a/docs/content/tutorials/validation.md b/docs/content/tutorials/validation.md index f0ac4a9..82b6dd1 100644 --- a/docs/content/tutorials/validation.md +++ b/docs/content/tutorials/validation.md @@ -30,51 +30,12 @@ The `ValidatedData` can be any `case class`: json data, form data, query params. Create a file `validation.sc` and paste this code into it: ```scala -//> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 - -import ba.sake.querson.QueryStringRW -import ba.sake.tupson.JsonRW -import ba.sake.validson.Validator -import ba.sake.sharaf.* -import ba.sake.sharaf.undertow.UndertowSharafServer - -val routes = Routes: - case GET -> Path("cars") => - val qp = Request.current.queryParamsValidated[CarQuery] - Response.withBody(CarApiResult(s"Query OK: ${qp}")) - - case POST -> Path("cars") => - val json = Request.current.bodyJsonValidated[Car] - Response.withBody(CarApiResult(s"JSON body OK: ${json}")) - -UndertowSharafServer("localhost", 8181, routes) - .withExceptionMapper(ExceptionMapper.json) - .start() - -println(s"Server started at http://localhost:8181") - - -case class Car(brand: String, model: String, quantity: Int) derives JsonRW -object Car: - given Validator[Car] = Validator - .derived[Car] - .notBlank(_.brand) - .notBlank(_.model) - .nonNegative(_.quantity) - -case class CarQuery(brand: String) derives QueryStringRW -object CarQuery: - given Validator[CarQuery] = Validator - .derived[CarQuery] - .notBlank(_.brand) - -case class CarApiResult(message: String) derives JsonRW +{% include "validation.sc" %} ``` Then run it like this: ```sh -scala-cli validation.sc +scala validation.sc ``` Notice above that we used `queryParamsValidated` and not plain `queryParams` (does not validate query params). diff --git a/docs/copy-examples.ps1 b/docs/copy-examples.ps1 new file mode 100644 index 0000000..6ef5a8e --- /dev/null +++ b/docs/copy-examples.ps1 @@ -0,0 +1,17 @@ + +# a one off script to copy some examples to the docs folder +$examplesList = @( + "examples/scala-cli/hello.sc", + "examples/scala-cli/path_params.sc", + "examples/scala-cli/query_params.sc", + "examples/scala-cli/static_files.sc", + "examples/scala-cli/json_api.test.scala", + "examples/scala-cli/validation.sc" +) + +$targetFolder = "docs/_includes" + +foreach ($itemToCopy in $examplesList) +{ + Copy-Item -Path $itemToCopy -Destination $targetFolder -Force +} diff --git a/examples/fullstack/resources/public/images/icons8-screw-100.png b/examples/fullstack/resources/public/images/icons8-screw-100.png deleted file mode 100644 index 472acca..0000000 Binary files a/examples/fullstack/resources/public/images/icons8-screw-100.png and /dev/null differ diff --git a/examples/fullstack/resources/public/styles/classless.css b/examples/fullstack/resources/public/styles/classless.css new file mode 100644 index 0000000..d780998 --- /dev/null +++ b/examples/fullstack/resources/public/styles/classless.css @@ -0,0 +1,386 @@ +/* Classless.css v1.1 + +Table of Contents: + 1. Theme Settings + 2. Reset + 3. Base Style + 4. Extras (remove unwanted) + 5. Classes (remove unwanted) +*/ + +/* 1. Theme Settings โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“-โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“ */ + + +:root, html[data-theme='light'] { + --rem: 12pt; + --width: 50rem; + --navpos: absolute; /* fixed | absolute */ + --font-p: 1em/1.7 'Open Sans', 'DejaVu Sans', FreeSans, Helvetica, sans-serif; + --font-h: .9em/1.5 'Open Sans', 'DejaVu Sans', FreeSans, Helvetica, sans-serif; + --font-c: .9em/1.4 'DejaVu Sans Mono', monospace; + --border: 1px solid var(--cmed); + --ornament: "โ€นโ€นโ€น โ€บโ€บโ€บ"; + /* foreground | background color */ + --cfg: #433; --cbg: #fff; + --cdark: #888; --clight: #f5f6f7; + --cmed: #d1d1d1; + --clink: #07c; + --cemph: #088; --cemphbg: #0881; +} + + +/* 2. Reset โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“ */ + +/* reset block elements */ +* { box-sizing: border-box; border-spacing: 0; margin: 0; padding: 0; } +header, footer, figure, video, details, blockquote, +ul, ol, dl, fieldset, pre, pre > code { + display: block; + margin: .5rem 0 1rem; + width: 100%; + overflow: auto hidden; + text-align: left; +} +video, summary, input, select { outline: none; } + +/* reset clickable things (FF Bug: select:hover prevents usage) */ +a, button, select, summary { color: var(--clink); cursor: pointer; } + + +/* 3. Base Style โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“ */ +html { font-size: var(--rem); background: var(--cbg); } +body { + position: relative; + margin: auto; + max-width: var(--width); + font: var(--font-p); + color: var(--cfg); + padding: 3.0rem .6rem 0; + overflow-x: hidden; +} +body > footer { margin: 10rem 0 0; font-size: 90%; } +p { margin: .6em 0; } + +/* links */ +a[href]{ text-decoration: underline solid 1.5px var(--cmed); text-underline-position: under; } +a[href^="#"] {text-decoration: none; } +a:hover, button:not([disabled]):hover, summary:hover, select:hover { + filter: brightness(92%); color: var(--cemph); border-color: var(--cemph); +} + +/* lists */ +ul, ol, dl { margin: 1rem 0; padding: 0 0 0 2em; } +li:not(:last-child), dd:not(:last-child) { margin-bottom: .5rem; } +dt { font-weight: bold; } + +/* headings */ +h1, h2, h3, h4, h5 { margin: 1.5em 0 .5rem; font: var(--font-h); line-height: 1.2em; clear: both; } +h1+h2, h2+h3, h3+h4, h4+h5 { margin-top: .5em; padding-top: 0; } /* non-clashing headings */ +h1 { font-size: 2.2em; font-weight: 300; } +h2 { font-size: 2.0em; font-weight: 300; font-variant: small-caps; } +h3 { font-size: 1.5em; font-weight: 400; } +h4 { font-size: 1.1em; font-weight: 700; } +h5 { font-size: 1.2em; font-weight: 400; color: var(--cfg); } +h6 { font-size: 1.0em; font-weight: 700; font-style: italic; display: inline; } +h6 + p { display: inline; } + +/* tables */ +td, th { + padding: .5em .8em; + text-align: right; + border-bottom: var(--border); + white-space: nowrap; + font-size: 95%; +} +thead th[colspan] { padding: .2em .8em; text-align: center; } +thead tr:not(:only-child) td { padding: .2em .8em;} +thead+tbody tr:first-child td { border-top: var(--border); } +td:first-child, th:first-child { text-align: left; } +tr:hover{ background-color: var(--clight); } +table img { display: block; } + +/* figures */ +img, svg { max-width: 100%; vertical-align: text-top; object-fit: cover; } +p>img:not(:only-child) { float: right; margin: 0 0 .5em .5em; } +figure > img { display: inline-block; width: auto; } +figure > img:only-of-type, figure > svg:only-of-type { max-width: 100%; display: block; margin: 0 auto .4em; } +figure > *:not(:last-child) { margin-bottom: .4rem; } + +/* captions */ +figcaption, caption { text-align: left; font: var(--font-h); color: var(--cdark); width: 100%; } +figcaption > *:first-child, caption > *:first-child { display: inline-block; margin: 0; } +table caption:last-child { caption-side: bottom; margin: .5em 0;} + +/* code */ +pre > code { + margin: 0; + position: relative; + padding: .8em; + border-left: .4rem solid var(--cemph); +} +code, kbd, samp { + padding: .2em; + font: var(--font-c); + background: var(--clight); + border-radius: 4px; +} +kbd { border: 1px solid var(--cmed); } + +/* misc */ +blockquote { border-left: .4rem solid var(--cmed); padding: 0 0 0 1rem; } +time{ color: var(--cdark); } +hr { border: 0; border-top: .1rem solid var(--cmed); } +nav { width: 100%; background-color: var(--clight); } +::selection, mark { background: var(--clink); color: var(--cbg); } + + +/* 4. Extra Style โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“ */ + +/* Auto Numbering: figure/tables/headings/cite */ +article { counter-reset: h2 0 h3 0 tab 0 fig 0 lst 0 ref 0 eq 0; } +article figure figcaption:before { + color: var(--cemph); + counter-increment: fig; + content: "Figure " counter(fig) ": "; +} + +/* subfigures */ +figure { counter-reset: subfig 0 } +article figure figure { counter-reset: none; } +article figure > figure { display: inline-grid; width: auto; } +figure > figure:not(:last-of-type) { padding-right: 1rem; } +article figure figure figcaption:before { + counter-increment: subfig 1; + content: counter(subfig, lower-alpha) ": "; +} + +/* listings */ +article figure pre + figcaption:before { + counter-increment: lst 1; + content: "Listing " counter(lst) ": "; +} + +/* tables */ +figure > table:only-of-type { margin: .5em auto !important; width: fit-content; } +article figure > table caption { display: table-caption; caption-side: bottom; } +article figure > table + figcaption:before, +article table caption:before { + color: var(--cemph); + counter-increment: tab 1; + content: "Table " counter(tab) ": "; +} + +/* headings */ +article h2, h3 { position: relative; } +article h2:before, +article h3:before { + display: inline-block; + position: relative; + font-size: .6em; + text-align: right; + vertical-align: baseline; + left: -1rem; + width: 2.5em; + margin-left: -2.5em; +} +article h1 { counter-set: h2; } +article h2:before { counter-increment: h2; content: counter(h2) ". "; counter-set: h3; } +article h3:before { counter-increment: h3; content: counter(h2) "." counter(h3) ". ";} +@media (max-width: 60rem) { h2:before, h3:before { display: none; } } + +/* tooltip + citation */ +article p>cite:before { + padding: 0 .5em 0 0; + counter-increment: ref; content: " [" counter(ref) "] "; + vertical-align: super; font-size: .6em; +} +article p>cite > *:only-child { display: none; } +article p>cite:hover > *:only-child, +[data-tooltip]:hover:before { + display: inline-block; z-index: 40; + white-space: pre-wrap; + position: absolute; left: 1rem; right: 1rem; + padding: 1em 2em; + text-align: center; + transform:translateY( calc(-100%) ); + content: attr(data-tooltip); + color: var(--cbg); + background-color: var(--cemph); + box-shadow: 0 2px 10px 0 black; +} +[data-tooltip], article p>cite:before { + color: var(--clink); + border: .8rem solid transparent; margin: -.8rem; +} +abbr[title], [data-tooltip] { cursor: help; } + +/* navbar */ +nav+* { margin-top: 3rem; } +body>nav, header nav { + position: var(--navpos); + top: 0; left: 0; right: 0; + z-index: 41; + box-shadow: 0vw -50vw 0 50vw var(--clight), 0 calc(-50vw + 2px) 4px 50vw var(--cdark); +} +nav ul { list-style-type: none; } +nav ul:first-child { margin: 0; padding: 0; overflow: visible; } +nav ul:first-child > li { + display: inline-block; + margin: 0; + padding: .8rem .6rem; +} +nav ul > li > ul { + display: none; + width: auto; + position: absolute; + margin: .5rem 0; + padding: 1rem 2rem; + background-color: var(--clight); + border: var(--border); + border-radius: 4px; + z-index: 42; +} +nav ul > li > ul > li { white-space: nowrap; } +nav ul > li:hover > ul { display: block; } +@media (max-width: 40rem) { + nav ul:first-child > li:first-child:after { content: " \25BE"; } + nav ul:first-child > li:not(:first-child):not(.sticky) { display: none; } + nav ul:first-child:hover > li:not(:first-child):not(.sticky) { display: block; float: none !important; padding: .3rem .6rem; } +} + +/* details/cards */ +summary>* { display: inline; } +.card, details { + display: block; + margin: .5rem 0 1rem; + padding: 0 .6rem; + border-radius: 4px; + overflow: hidden; +} +.card, details[open] { outline: 1px solid var(--cmed); } +.card>img:first-child { margin: -3px -.6rem; max-width: calc(100% + 1.2rem); } +summary:hover, details[open] summary, .card>p:first-child { + box-shadow: inset 0 0 0 2em var(--clight), 0 -.8rem 0 .8rem var(--clight); +} +.hint { --cmed: var(--cemph); --clight: var(--cemphbg); background-color: var(--clight); } +.warn { --cmed: #c11; --clight: #e221; background-color: var(--clight); } + +/* big first letter */ +article > section:first-of-type > h2:first-of-type + p:first-letter, +article > h2:first-of-type + p:first-letter, .lettrine { + float: left; + font-size: 3.5em; + padding: .1em .1em 0 0; + line-height: .68em; + color: var(--cemph); +} + +/* ornaments */ +section:after { + display: block; + margin: 1em 0; + color: var(--cmed); + text-align: center; + font-size: 1.5em; + content: var(--ornament); +} + +/* side menu (aside is not intended for use in a paragraph!) */ +main aside { + position: absolute; + width: 8rem; right: -8.6rem; + font-size: .8em; line-height: 1.4em; +} +@media (max-width: 70rem) { main aside { display: none; } } + +/* forms and inputs */ +textarea, input:not([type=range]), button, select { + font: var(--font-h); + border-radius: 4px; + border: 1.5px solid var(--cmed); + padding: .4em .8em; + color: var(--cfg); + background-color: var(--clight); +} +fieldset select, input:not([type=checkbox]):not([type=radio]) { + display: block; + width: 100%; + margin: 0 0 1rem; +} +button, select { + font-weight: bold; + margin: .5em; + border: 1.5px solid var(--clink); + color: var(--clink); +} +button { padding: .4em 1em; font-size: 85%; letter-spacing: .1em; } +button[disabled] { color: var(--cdark); border-color: var(--cmed); } +fieldset { border-radius: 4px; border: var(--border); padding: .5em 1em; } +textarea:hover, input:not([type=checkbox]):not([type*='ra']):hover { + border: 1.5px solid var(--cemph); +} +textarea:focus, input:not([type=checkbox]):not([type*='ra']):focus { + border: 1.5px solid var(--clink); + box-shadow: 0 0 5px var(--clink); +} +p>button { padding: 0 .5em; margin: 0 .5em; } +p>select { padding: 0; margin: 0 .5em; } + + +/* 5. Bootstrap-compatible classes โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“โ€“ */ + +/* grid */ +.row { display: flex; margin: .5rem -.6rem; align-items: stretch; } +.row [class^="col"] { padding: 0 .6rem; } +.row .col { flex: 0 4 100%; } +.row .col-2 { flex: 0 2 16.66%; } +.row .col-3 { flex: 0 3 25%; } +.row .col-4 { flex: 0 4 33.33%; } +.row .col-5 { flex: 0 5 41.66%; } +.row .col-6 { flex: 0 6 50%; } +@media (max-width: 40rem) { .row { flex-direction: column; } } + +/* align */ +.text-left { text-align: left; } +.text-right { text-align: right; } +.text-center { text-align: center; } +.float-left { float: left !important; } +.float-right { float: right !important; } +.clearfix { clear: both; } + +/* colors */ +.text-black { color: #000; } +.text-white { color: #fff; } +.text-primary { color: var(--cemph); } +.text-secondary{ color: var(--cdark); } +.bg-white { background-color: #fff; } +.bg-light { background-color: var(--clight); } +.bg-primary { background-color: var(--cemph); } +.bg-secondary{ background-color: var(--cmed); } + +/* margins */ +.mx-auto { margin-left: auto; margin-right: auto; } +.m-0 { margin: 0 !important; } +.m-1, .mx-1, .mr-1 { margin-right: 1.0rem !important; } +.m-1, .mx-1, .ml-1 { margin-left: 1.0rem !important; } +.m-1, .my-1, .mt-1 { margin-top: 1.0rem !important; } +.m-1, .my-1, .mb-1 { margin-bottom: 1.0rem !important; } + +/* pading */ +.p-0 { padding: 0 !important; } +.p-1, .px-1, .pr-1 { padding-right: 1.0rem !important; } +.p-1, .px-1, .pl-1 { padding-left: 1.0rem !important; } +.p-1, .py-1, .pt-1 { padding-top: 1.0rem !important; } +.p-1, .py-1, .pb-1 { padding-bottom: 1.0rem !important; } + +/* be print friendly */ +@media print { + @page { margin: 1.5cm 2cm; } + html {font-size: 9pt!important; } + body { max-width: 27cm; } + p { orphans: 2; widows: 2; } + caption, figcaption { page-break-before: avoid; } + h2, h3, h4, h5 { page-break-after: avoid;} + .noprint, body>nav, section:after { display: none; } + .row { flex-direction: row; } +} \ No newline at end of file diff --git a/examples/fullstack/src/Main.scala b/examples/fullstack/src/Main.scala index de9b378..0b70cb4 100644 --- a/examples/fullstack/src/Main.scala +++ b/examples/fullstack/src/Main.scala @@ -1,10 +1,10 @@ package fullstack +import sttp.model.StatusCode import ba.sake.validson.* -import ba.sake.sharaf.* -import ba.sake.sharaf.undertow.{*, given} +import ba.sake.sharaf.{*, given} +import ba.sake.sharaf.undertow.UndertowSharafServer import fullstack.views.* -import sttp.model.StatusCode @main def main: Unit = val module = FullstackModule(8181) diff --git a/examples/fullstack/src/requests.scala b/examples/fullstack/src/requests.scala index 7adbe80..3298d7c 100644 --- a/examples/fullstack/src/requests.scala +++ b/examples/fullstack/src/requests.scala @@ -13,7 +13,7 @@ case class CreateCustomerForm( object CreateCustomerForm: - val empty = CreateCustomerForm("", Paths.get(""), Seq.empty) + val empty = CreateCustomerForm("", Paths.get(""), Seq("")) given Validator[CreateCustomerForm] = Validator .derived[CreateCustomerForm] diff --git a/examples/fullstack/src/views/ShowFormPage.scala b/examples/fullstack/src/views/ShowFormPage.scala index 87d1fd0..7a347bb 100644 --- a/examples/fullstack/src/views/ShowFormPage.scala +++ b/examples/fullstack/src/views/ShowFormPage.scala @@ -1,62 +1,71 @@ package fullstack.views import ba.sake.validson.ValidationError -import Bundle.*, Tags.* +import ba.sake.sharaf.* import fullstack.CreateCustomerForm +import play.twirl.api.Html -class ShowFormPage(formData: CreateCustomerForm, errors: Seq[ValidationError] = Seq.empty) extends MyPage { - - override def pageSettings = super.pageSettings.withTitle("Home") +def ShowFormPage(formData: CreateCustomerForm, errors: Seq[ValidationError] = Seq.empty) = { + // errors are returned as JSON Path, hence the $. prefix below! + def withInputErrors(fieldName: String, extract: CreateCustomerForm => String)( + f: (String, String, Seq[String]) => Html + ) = { + val fieldErrors = errors.filter(_.path == s"$$.$fieldName").map(_.msg) + f(fieldName, extract(formData), fieldErrors) + } - override def pageContent: Frag = Grid.row( - Panel.panel( - Panel.Companion.Type.Info, - body = Grid.row( - Grid.half( - if errors.isEmpty then """ - Hello there! - Please fill in the following form: - """.md - else """ - There were some errors in the form, please fix them: - """.md - ), - Grid.half(img(src := "images/icons8-screw-100.png")) - ) - ), - Form.form(action := "/form-submit", method := "POST", enctype := "multipart/form-data")( - withValueAndValidation("name", _.name) { case (fieldName, fieldValue, state, messages) => - Form.inputText(required, value := fieldValue)( - fieldName, - "Name", - _validationState = state, - _messages = messages - ) - }, - formData.hobbies.zipWithIndex.map { case (hobby, idx) => - withValueAndValidation(s"hobbies[${idx}]", _.hobbies.applyOrElse(idx, _ => "")) { - case (fieldName, fieldValue, state, messages) => - Form.inputText(required, value := fieldValue)( - fieldName, - s"Hobby ${idx + 1}", - _validationState = state, - _messages = messages - ) - } - }, - Form.inputFile(required)("file", "Document"), - Form.inputSubmit(Classes.btnPrimary)("Submit") - ) - ) + val message = + if errors.isEmpty then html"Hello there! Please fill in the following form:" + else html"There were some errors in the form, please fix them:" - // errors are returned as JSON Path, hence the $. prefix below! - private def withValueAndValidation(fieldName: String, extract: CreateCustomerForm => String)( - f: (String, String, Option[Form.ValidationState], Seq[String]) => Frag - ) = - val fieldErrors = errors.filter(_.path == s"$$.$fieldName") - val (state, errMsgs) = - if fieldErrors.isEmpty then None -> Seq.empty - else Some(Form.ValidationState.Error) -> fieldErrors.map(_.msg) - f(fieldName, extract(formData), state, errMsgs) + val nameInput = withInputErrors("name", _.name) { (fieldName, fieldValue, fieldErrors) => + html""" + + """ + } + val hobbiesInputs = formData.hobbies.zipWithIndex.map { case (hobby, idx) => + withInputErrors(s"hobbies[${idx}]", _.hobbies.applyOrElse(idx, _ => "")) { + case (fieldName, fieldValue, fieldErrors) => + html""" + + """ + } + } + html""" + + + + Codestin Search App + + + + + +
${message}
+
+ ${nameInput} + ${hobbiesInputs} + + +
+ + + """ } diff --git a/examples/fullstack/src/views/SucessPage.scala b/examples/fullstack/src/views/SucessPage.scala index d20a447..fc59dbe 100644 --- a/examples/fullstack/src/views/SucessPage.scala +++ b/examples/fullstack/src/views/SucessPage.scala @@ -1,24 +1,33 @@ package fullstack.views import java.nio.file.Files -import Bundle.*, Tags.* +import ba.sake.sharaf.* import fullstack.CreateCustomerForm -class SucessPage(formData: CreateCustomerForm) extends MyPage { +def SucessPage(formData: CreateCustomerForm) = { - private val fileAsString = Files.readString(formData.file) + val fileAsString = Files.readString(formData.file) - override def pageSettings = super.pageSettings.withTitle("Result") + html""" + + + + Codestin Search App + + + + + +
+ You have successfully submitted these values: + +
+ + + """ - override def pageContent: Frag = Grid.row( - Panel.panel( - Panel.Companion.Type.Success, - body = s""" - You have successfully submitted these values: - - name: ${formData.name} - - hobbies: ${formData.hobbies.mkString(",")} - - file: ${fileAsString} - """.md - ) - ) } diff --git a/examples/fullstack/src/views/package.scala b/examples/fullstack/src/views/package.scala deleted file mode 100644 index 4e92608..0000000 --- a/examples/fullstack/src/views/package.scala +++ /dev/null @@ -1,18 +0,0 @@ -package fullstack.views - -import ba.sake.hepek.bootstrap3.BootstrapBundle - -val Bundle = locally { - val b = BootstrapBundle.default - b.withGrid( - b.Grid.withScreenRatios( - b.Grid.screenRatios - .withLg(b.Ratios.default.withSingle(1, 4, 1)) - .withMd(b.Ratios.default.withSingle(1, 4, 1)) - .withSm(None) // stack on small - .withXs(None) // and extra-small screens - ) - ) -} - -trait MyPage extends Bundle.Page diff --git a/examples/htmx/htmx_active_search.sc b/examples/htmx/htmx_active_search.sc index 5b6dd75..ec2984a 100644 --- a/examples/htmx/htmx_active_search.sc +++ b/examples/htmx/htmx_active_search.sc @@ -1,10 +1,10 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/active-search/ import ba.sake.formson.FormDataRW -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val routes = Routes: @@ -12,6 +12,7 @@ val routes = Routes: Response.withBody(views.ContactsViewPage(Seq.empty)) case POST -> Path("search") => Thread.sleep(500) // simulate slow backend :) + case class SearchForm(search: String) derives FormDataRW val formData = Request.current.bodyForm[SearchForm] val contactsSlice = allContacts.filter(_.matches(formData.search)) Response.withBody(views.contactsRows(contactsSlice)) @@ -20,52 +21,58 @@ UndertowSharafServer("localhost", 8181, routes).start() println("Server started at http://localhost:8181") -case class SearchForm(search: String) derives FormDataRW - object views { - import scalatags.Text.all.* - import ba.sake.hepek.htmx.* - def ContactsViewPage(contacts: Seq[Contact]) = doctype("html")( - html( - head( - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body( - div( - h1("Active Search example"), - span(cls := "htmx-indicator")( - img(src := "/img/bars.svg"), - "Searching... " - ), - input( - tpe := "search", - name := "search", - placeholder := "Begin Typing To Search Users...", - hx.post := "/search", - hx.trigger := "input changed delay:500ms, search", - hx.target := "#search-results", - hx.indicator := ".htmx-indicator" - ), - table( - thead(tr(th("First Name"), th("Last Name"), th("Email"))), - tbody(id := "search-results")( - contactsRows(contacts) - ) - ) - ) - ) - ) - ) + def ContactsViewPage(contacts: Seq[Contact]) = + html""" + + + + + + +
+ Loading... + Searching... +
+

Active Search example

+ + + + + + + + + + + ${contactsRows(contacts)} + +
First NameLast NameEmail
+ + + """ - def contactsRows(contacts: Seq[Contact]): Frag = - contacts.zipWithIndex.map { case (contact, idx) => - tr( - td(contact.firstName), - td(contact.lastName), - td(contact.email) - ) - } + def contactsRows(contacts: Seq[Contact]) = + val contactsHtml = contacts.zipWithIndex + .map { case (contact, idx) => + html""" + + ${contact.firstName} + ${contact.lastName} + ${contact.email} + + """ + } + html"${contactsHtml}" } diff --git a/examples/htmx/htmx_animations.sc b/examples/htmx/htmx_animations.sc index b52e0f6..3f9951a 100644 --- a/examples/htmx/htmx_animations.sc +++ b/examples/htmx/htmx_animations.sc @@ -1,11 +1,10 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/animations/ -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* -import ba.sake.sharaf.* +import play.twirl.api.Html +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val routes = Routes: @@ -34,7 +33,7 @@ val routes = Routes: case GET -> Path("request-in-flight") => Response.withBody(views.RequestInFlightView) case POST -> Path("request-in-flight-name") => - Thread.sleep(1000) // simulate sloww + Thread.sleep(2000) // simulate sloww Response.withBody("Submitted!") UndertowSharafServer("localhost", 8181, routes).start() @@ -44,12 +43,14 @@ println("Server started at http://localhost:8181") object views { def IndexView = createPage( - ul( - li(a(href := "color-throb")("Color throb")), - li(a(href := "fade-out-on-swap")("Fade Out On Swap")), - li(a(href := "fade-in-on-addition")("Fade In On Addition")), - li(a(href := "request-in-flight")("Request In Flight")) - ) + html""" + + """ ) def ColorThrobView = createPage( @@ -61,21 +62,26 @@ object views { """ ) - def ColorThrobSnippet(color: String) = div( - id := "color-demo", // must stay same! - hx.get := "/colors", - hx.swap := "outerHTML", - hx.trigger := "every 1s", - cls := "smooth", - style := s"color:${color}" - )("Color Swap Demo") + def ColorThrobSnippet(color: String) = + // id must stay same! + html""" +
+ Color Swap Demo +
+ """ def FadeOutOnSwapView = createPage( - button( - cls := "fade-me-out", - hx.delete := "/fade_out_demo", - hx.swap := "outerHTML swap:1s" - )("Fade Me Out"), + html""" + + """ def FadeInOnAdditionView = createPage( theButton, @@ -104,13 +111,15 @@ object views { ) def RequestInFlightView = createPage( - form( - hx.post := "/request-in-flight-name", - hx.swap := "outerHTML" - )( - label("Name: ", input(name := "name")), - button("Submit") - ), + html""" +
+ + +
+ """, inlineStyle = """ form.htmx-request { opacity: .5; @@ -119,13 +128,19 @@ object views { """ ) - private def createPage(bodyContent: Frag, inlineStyle: String = "") = doctype("html")( - html( - head( - tag("style")(inlineStyle), - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body(bodyContent) - ) - ) + private def createPage(bodyContent: Html, inlineStyle: String = "") = + html""" + + + + + + + + ${bodyContent} + + + """ } diff --git a/examples/htmx/htmx_bulk_update.sc b/examples/htmx/htmx_bulk_update.sc index c20199c..910e3a6 100644 --- a/examples/htmx/htmx_bulk_update.sc +++ b/examples/htmx/htmx_bulk_update.sc @@ -1,13 +1,12 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/bulk-update/ -import scalatags.Text.all.* -import ba.sake.sharaf.* +import play.twirl.api.Html +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer import ba.sake.formson.FormDataRW -import ba.sake.hepek.htmx.* val routes = Routes: case GET -> Path() => @@ -32,21 +31,30 @@ println("Server started at http://localhost:8181") object views { def ContactsViewPage(contacts: Seq[Contact]) = createPage( - div( - h1("Bulk Updating example"), - div(hx.include := "#checked-contacts", hx.target := "#tbody")( - button(hx.put := "/activate")("Activate"), - button(hx.put := "/deactivate")("Deactivate") - ), - form(id := "checked-contacts")( - table( - thead(tr(th(""), th("Name"), th("Email"), th("Status"))), - tbody(id := "tbody")( - contactsRows(contacts, AffectedContacts(Set.empty, false)) - ) - ) - ) - ), + html""" +
+

Bulk Updating example

+
+ + +
+
+ + + + + + + + + + + ${contactsRows(contacts, AffectedContacts(Set.empty, false))} + +
NameEmailStatus
+
+
+ """, inlineStyle = """ .htmx-settling tr.deactivate td { background: lightcoral; @@ -60,27 +68,36 @@ object views { """ ) - def contactsRows(contacts: Seq[Contact], affectedContacts: AffectedContacts): Frag = contacts.map { contact => - val affectedClass = if affectedContacts.activated then "activate" else "deactivate" - tr( - Option.when(affectedContacts.ids(contact.id))(cls := affectedClass) - )( - td(input(name := "ids", value := contact.id, tpe := "checkbox")), - td(contact.name), - td(contact.email), - td(if contact.active then "Active" else "Inactive") - ) - } + def contactsRows(contacts: Seq[Contact], affectedContacts: AffectedContacts): Html = + val contactsHtml = contacts + .map { contact => + val affectedClass = if affectedContacts.activated then "activate" else "deactivate" + html""" + + + ${contact.name} + ${contact.email} + ${if contact.active then "Active" else "Inactive"} + + """ + } + html"${contactsHtml}" - private def createPage(bodyContent: Frag, inlineStyle: String = "") = doctype("html")( - html( - head( - tag("style")(inlineStyle), - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body(bodyContent) - ) - ) + private def createPage(bodyContent: Html, inlineStyle: String = "") = + html""" + + + + + + + + ${bodyContent} + + + """ } diff --git a/examples/htmx/htmx_cascading_selects.sc b/examples/htmx/htmx_cascading_selects.sc index ba5c2e8..3a9a48e 100644 --- a/examples/htmx/htmx_cascading_selects.sc +++ b/examples/htmx/htmx_cascading_selects.sc @@ -1,12 +1,11 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/value-select/ -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* +import play.twirl.api.Html import ba.sake.querson.QueryStringRW -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val routes = Routes: @@ -29,42 +28,51 @@ enum CarMake(val models: Seq[String]) derives QueryStringRW: object views { def IndexView(make: CarMake) = createPage( - div( - div( - label("Make"), - select( - name := "make", - hx.get := "/models", - hx.target := "#models", - hx.swap := "outerHTML", - hx.indicator := ".htmx-indicator" - )( - CarMake.values.map { make => - option(value := make.toString)(make.toString) - } - ) - ), - div( - label("Model"), - cascadingSelect(make) - ), - img(src := "/img/bars.svg", alt := "Result loading...", cls := "htmx-indicator") - ) + html""" +
+
+ + +
+
+ + ${cascadingSelect(make)} +
+ Result loading... +
+ """ ) - def cascadingSelect(make: CarMake) = select(id := "models", name := "model")( - make.models.map { model => - option(value := model)(model) - } - ) + def cascadingSelect(make: CarMake) = + html""" + + """ - private def createPage(bodyContent: Frag, inlineStyle: String = "") = doctype("html")( - html( - head( - tag("style")(inlineStyle), - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body(bodyContent) - ) - ) + private def createPage(bodyContent: Html, inlineStyle: String = "") = + html""" + + + + + + + + ${bodyContent} + + + """ } diff --git a/examples/htmx/htmx_click_edit.sc b/examples/htmx/htmx_click_edit.sc index 5519251..4e40a0a 100644 --- a/examples/htmx/htmx_click_edit.sc +++ b/examples/htmx/htmx_click_edit.sc @@ -1,11 +1,10 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/click-to-edit/ -import scalatags.Text.all.{param =>_, *} -import ba.sake.hepek.htmx.* -import ba.sake.sharaf.* +import play.twirl.api.Html +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer import ba.sake.formson.FormDataRW @@ -32,34 +31,58 @@ case class ContactForm(firstName: String, lastName: String, email: String) deriv object views { def ContactViewPage(formData: ContactForm) = createPage( - div( - h1("Click to Edit example"), - contactView(formData) - ) + html""" +
+

Click to Edit example

+ ${contactView(formData)} +
+ """ ) - def contactView(formData: ContactForm) = div(hx.target := "this", hx.swap := "outerHTML")( - div(label("First Name"), s": ${formData.firstName}"), - div(label("Last Name"), s": ${formData.lastName}"), - div(label("Email"), s": ${formData.email}"), - button(hx.get := "/contact/1/edit")("Click To Edit") - ) + def contactView(formData: ContactForm) = + html""" +
+

Contact Details

+
: ${formData.firstName}
+
: ${formData.lastName}
+
: ${formData.email}
+ +
+ """ - def contactEdit(formData: ContactForm) = form(hx.put := "/contact/1", hx.target := "this", hx.swap := "outerHTML")( - div(label("First Name"), input(tpe := "text", name := "firstName", value := formData.firstName)), - div(label("Last Name"), input(tpe := "text", name := "lastName", value := formData.lastName)), - div(label("Email"), input(tpe := "email", name := "email", value := formData.email)), - button("Submit"), - button(hx.get := "/contact/1")("Cancel") - ) + def contactEdit(formData: ContactForm) = + html""" +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ """ - private def createPage(bodyContent: Frag, inlineStyle: String = "") = doctype("html")( - html( - head( - tag("style")(inlineStyle), - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body(bodyContent) - ) - ) + private def createPage(bodyContent: Html, inlineStyle: String = "") = + html""" + + + + + + + + ${bodyContent} + + + """ } diff --git a/examples/htmx/htmx_click_to_load.sc b/examples/htmx/htmx_click_to_load.sc index db99778..546c9dc 100644 --- a/examples/htmx/htmx_click_to_load.sc +++ b/examples/htmx/htmx_click_to_load.sc @@ -1,13 +1,12 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/click-to-load/ import java.util.UUID -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* +import play.twirl.api.Html import ba.sake.querson.QueryStringRW -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val PageSize = 5 @@ -38,43 +37,61 @@ object Contact: object views { def ContactsViewPage(contacts: Seq[Contact], page: Int) = createPage( - div( - h1("Click to Load example"), - table( - thead(tr(th("ID"), th("Name"), th("Email"))), - tbody( - contactsRowsWithButton(contacts, page) - ) - ) - ) + html""" +
+

Click to Load example

+ + + + + + + + + + ${contactsRowsWithButton(contacts, page)} + +
IDNameEmail
+
+ """ ) - def contactsRowsWithButton(contacts: Seq[Contact], page: Int) = frag( - contacts.map { contact => - tr(td(contact.id), td(contact.name), td(contact.email)) - }, - tr(id := "replaceMe")( - td(colspan := "3")( - button( - hx.get := s"/contacts/?page=${page + 1}", - hx.target := "#replaceMe", - hx.swap := "outerHTML" - )( - "Load More Agents...", - img(src := "/img/bars.svg", cls := "htmx-indicator") - ) - ) - ) - ) + def contactsRowsWithButton(contacts: Seq[Contact], page: Int) = { + val contactsHtml = contacts.map { contact => + html""" + + ${contact.id} + ${contact.name} + ${contact.email} + + """ + } + html""" + ${contactsHtml} + + + + + """ + } - private def createPage(bodyContent: Frag, inlineStyle: String = "") = doctype("html")( - html( - head( - tag("style")(inlineStyle), - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body(bodyContent) - ) - ) + private def createPage(bodyContent: Html, inlineStyle: String = "") = + html""" + + + + + + + + ${bodyContent} + + + """ } diff --git a/examples/htmx/htmx_delete_row.sc b/examples/htmx/htmx_delete_row.sc index 96307ba..889c80a 100644 --- a/examples/htmx/htmx_delete_row.sc +++ b/examples/htmx/htmx_delete_row.sc @@ -1,11 +1,9 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/delete-row/ -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer var allContacts = Seq( @@ -29,33 +27,51 @@ case class Contact(id: String, name: String, email: String) object views { - def ContactsViewPage(contacts: Seq[Contact]) = doctype("html")( - html( - head( - tag("style")(""" - tr.htmx-swapping td { + def ContactsViewPage(contacts: Seq[Contact]) = + html""" + + + + + + + +
+

Delete Row example

+ + + + + + + + + + ${contactsRows(contacts)} + +
NameEmail
+
+ + + """ + + def contactsRows(contacts: Seq[Contact]) = + val contactsHtml = contacts.map { contact => + html""" + + ${contact.name} + ${contact.email} + + + + + """ + } + html"${contactsHtml}" } diff --git a/examples/htmx/htmx_dialogs_bootstrap.sc b/examples/htmx/htmx_dialogs_bootstrap.sc deleted file mode 100644 index b77e260..0000000 --- a/examples/htmx/htmx_dialogs_bootstrap.sc +++ /dev/null @@ -1,67 +0,0 @@ -//> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 - -// https://htmx.org/examples/modal-bootstrap/ - -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* -import ba.sake.sharaf.* -import ba.sake.sharaf.undertow.UndertowSharafServer - -val routes = Routes: - case GET -> Path() => - Response.withBody(views.IndexView) - case GET -> Path("modal") => - Response.withBody(views.bsDialog()) - -UndertowSharafServer("localhost", 8181, routes).start() - -println(s"Server started at http://localhost:8181") - -object views { - - def IndexView = doctype("html")( - html( - head( - link(rel := "stylesheet", href := "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"), - script(src := "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"), - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body( - div( - button( - hx.get := "/modal", - hx.trigger := "click", - hx.target := "#modals-here", - data.bs.toggle := "modal", - data.bs.target := "#modals-here", - cls := "btn btn-primary" - )("Open Modal"), - div( - id := "modals-here", - cls := "modal modal-blur fade", - style := "display: none", - aria.hidden := "false", - tabindex := "-1" - )( - div(cls := "modal-dialog modal-lg modal-dialog-centered", role := "document")( - div(cls := "modal-content") - ) - ) - ) - ) - ) - ) - - def bsDialog() = div(cls := "modal-dialog modal-dialog-centered")( - div(cls := "modal-content")( - div(cls := "modal-header")( - h5(cls := "modal-title")("Modal title") - ), - div(cls := "modal-body")(p("Modal body text goes here.")), - div(cls := "modal-footer")( - button(tpe := "button", cls := "btn btn-secondary", data.bs.dismiss := "modal")("Close") - ) - ) - ) -} diff --git a/examples/htmx/htmx_dialogs_bootstrap_form.sc b/examples/htmx/htmx_dialogs_bootstrap_form.sc index c90d60a..3d1ee13 100644 --- a/examples/htmx/htmx_dialogs_bootstrap_form.sc +++ b/examples/htmx/htmx_dialogs_bootstrap_form.sc @@ -1,12 +1,11 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // example of BS5 modal with a form +// https://htmx.org/examples/modal-bootstrap/ -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* import ba.sake.formson.FormDataRW -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val routes = Routes: @@ -17,58 +16,52 @@ val routes = Routes: case POST -> Path("submit-form") => case class DialogForm(stuff: String) derives FormDataRW val formData = Request.current.bodyForm[DialogForm] - Response.withBody(div(s"You submitted: $formData")) + Response.withBody(html"""
You submitted: $formData
""") UndertowSharafServer("localhost", 8181, routes).start() println("Server started at http://localhost:8181") object views { - def IndexView = doctype("html")( - html( - head( - link(rel := "stylesheet", href := "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"), - script(src := "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"), - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body( - div( - button( - hx.get := "/modal", - hx.trigger := "click", - hx.target := "#modals-here", - data.bs.toggle := "modal", - data.bs.target := "#modals-here", - cls := "btn btn-primary" - )("Open Modal"), - div( - id := "modals-here", - cls := "modal modal-blur fade", - style := "display: none", - aria.hidden := "false", - tabindex := "-1" - )( - div(cls := "modal-dialog modal-lg modal-dialog-centered", role := "document")( - div(cls := "modal-content") - ) - ), - div(id := "form-submission-result") - ) - ) - ) - ) + def IndexView = + html""" + + + + + + + + +
+

HTMX Bootstrap Dialog Form Example

+

Click the button below to open a modal dialog with a form.

+ + +
+
+ + + """ - def bsDialog() = div(cls := "modal-dialog modal-dialog-centered")( - div(cls := "modal-content")( - div(cls := "modal-header")( - h5(cls := "modal-title")("Modal title") - ), - div(cls := "modal-body")( - form(hx.post := "/submit-form", hx.target := "#form-submission-result")( - label("Stuff: ", input(tpe := "text", name := "stuff")), - button(tpe := "submit", cls := "btn btn-secondary", data.bs.dismiss := "modal")("Submit") - ) - ) - ) - ) + def bsDialog() = + html""" + + """ } diff --git a/examples/htmx/htmx_dialogs_browser.sc b/examples/htmx/htmx_dialogs_browser.sc index 003f66f..eb5e59e 100644 --- a/examples/htmx/htmx_dialogs_browser.sc +++ b/examples/htmx/htmx_dialogs_browser.sc @@ -1,11 +1,9 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/dialogs/ -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer import ba.sake.sharaf.htmx.* @@ -15,10 +13,11 @@ val routes = Routes: case POST -> Path("submit") => val submittedData = Request.current.headers(RequestHeaders.Prompt).head Response.withBody( - div( - p("You submitted data:"), - submittedData - ) + html""" +
+

You submitted: $submittedData

+
+ """ ) UndertowSharafServer("localhost", 8181, routes).start() @@ -26,23 +25,25 @@ UndertowSharafServer("localhost", 8181, routes).start() println(s"Server started at http://localhost:8181") object views { - def IndexView = doctype("html")( - html( - head( - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body( - div( - button( - hx.post := "/submit", - hx.prompt := "Enter a string", - hx.confirm := "Are you sure?", - hx.target := "#response" - )("Prompt Submission"), - div(id := "response") - ) - ) - ) - ) + def IndexView = + html""" + + + + + + +
+ +
+
+ + + """ } diff --git a/examples/htmx/htmx_edit_row.sc b/examples/htmx/htmx_edit_row.sc index 80373ed..f8c8fd1 100644 --- a/examples/htmx/htmx_edit_row.sc +++ b/examples/htmx/htmx_edit_row.sc @@ -1,12 +1,11 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/edit-row/ -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* +import play.twirl.api.Html import ba.sake.formson.FormDataRW -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer var allContacts = Seq( @@ -48,59 +47,76 @@ case class ContactForm(name: String, email: String) derives FormDataRW object views { def ContactsViewPage(contacts: Seq[Contact]) = createPage( - div( - h1("Click to Edit example"), - table( - thead(tr(th("Name"), th("Email"), th())), - tbody(hx.target := "closest tr", hx.swap := "outerHTML")( - contacts.map(viewContactRow) - ) - ) - ) + html""" +
+

Click to Edit example

+ + + + + + + + + + ${contacts.map(viewContactRow)} + +
NameEmail
+
+ """ ) - def viewContactRow(contact: Contact) = tr( - td(contact.name), - td(contact.email), - td( - button( - hx.get := s"/contact/${contact.id}/edit", - hx.trigger := "edit", - onclick := """ - let editing = document.querySelector('.editing') - if (editing) { - const doWant = confirm("You are already editing a row! Do you want to cancel that edit and continue?"); - if (doWant) { - htmx.trigger(editing, 'cancel') - htmx.trigger(this, 'edit') - } - } else { - htmx.trigger(this, 'edit') - }""" - )("Edit") - ) - ) + def viewContactRow(contact: Contact) = + html""" + + ${contact.name} + ${contact.email} + + + + + """ - def editContact(contact: Contact) = tr( - hx.trigger := "cancel", - hx.get := s"/contact/${contact.id}" - )( - td(input(name := "name", value := contact.name, autofocus)), - td(input(name := "email", value := contact.email)), - td( - button(hx.get := s"/contact/${contact.id}")("Cancel"), - button(hx.put := s"/contact/${contact.id}", hx.include := "closest tr")("Save") - ) - ) + def editContact(contact: Contact) = + html""" + + + + + + + + + """ - private def createPage(bodyContent: Frag, inlineStyle: String = "") = doctype("html")( - html( - head( - tag("style")(inlineStyle), - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body(bodyContent) - ) - ) + private def createPage(bodyContent: Html, inlineStyle: String = "") = + html""" + + + + + + + + ${bodyContent} + + + """ } diff --git a/examples/htmx/htmx_file_upload_js.sc b/examples/htmx/htmx_file_upload_js.sc index 302e91a..5215098 100644 --- a/examples/htmx/htmx_file_upload_js.sc +++ b/examples/htmx/htmx_file_upload_js.sc @@ -1,14 +1,12 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* +import play.twirl.api.{Html, HtmlFormat} import ba.sake.formson.FormDataRW -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer // https://htmx.org/examples/file-upload/ -// scala htmx_file_upload_js.sc --resource-dir resources val routes = Routes: case GET -> Path() => @@ -16,7 +14,7 @@ val routes = Routes: case POST -> Path("upload") => case class FileUpload(file: java.nio.file.Path) derives FormDataRW val fileUpload = Request.current.bodyForm[FileUpload] - Response.withBody(div(s"Upload done!")) + Response.withBody(html"
Upload done!
") UndertowSharafServer("localhost", 8181, routes).start() @@ -25,14 +23,13 @@ println(s"Server started at http://localhost:8181") object views { def IndexView = createPage( - form( - id := "form", - hx.encoding := "multipart/form-data", - hx.post := "/upload", - input(`type` := "file", name := "file"), - button("Upload"), - tag("progress")(id := "progress", value := "0", max := "100") - ), + html""" +
+ + + +
+ """, inlineScript = """ htmx.on('#form', 'htmx:xhr:progress', function(evt) { htmx.find('#progress').setAttribute('value', evt.detail.loaded/evt.detail.total * 100) @@ -40,13 +37,20 @@ object views { """ ) - private def createPage(bodyContent: Frag, inlineScript: String = "") = doctype("html")( - html( - head( - script(src := "https://unpkg.com/htmx.org@2.0.4"), - script(inlineScript) - ), - body(bodyContent) - ) - ) + private def createPage(bodyContent: Html, inlineScript: String = "") = + html""" + + + + + + + + ${bodyContent} + + + """ + } diff --git a/examples/htmx/htmx_infinite_scroll.sc b/examples/htmx/htmx_infinite_scroll.sc index 1414282..fcf2435 100644 --- a/examples/htmx/htmx_infinite_scroll.sc +++ b/examples/htmx/htmx_infinite_scroll.sc @@ -1,13 +1,12 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/click-to-load/ import java.util.UUID -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* +import play.twirl.api.Html import ba.sake.querson.QueryStringRW -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val PageSize = 10 @@ -38,36 +37,60 @@ object Contact: object views { def ContactsViewPage(contacts: Seq[Contact], page: Int) = createPage( - div( - h1("Infinite Scroll example"), - table(hx.indicator := ".htmx-indicator")( - thead(tr(th("ID"), th("Name"), th("Email"))), - tbody( - contactsRows(contacts, page) - ) - ), - img(src := "/img/bars.svg", cls := "htmx-indicator") - ) + html""" +
+

Infinite Scroll example

+ + + + + + + + + + ${contactsRows(contacts, page)} + +
IDNameEmail
+ Loading indicator +
+ """ ) - def contactsRows(contacts: Seq[Contact], page: Int): Frag = - contacts.zipWithIndex.map { case (contact, idx) => + def contactsRows(contacts: Seq[Contact], page: Int): Html = + val contactsHtmls = contacts.zipWithIndex.map { case (contact, idx) => if idx == contacts.length - 1 then - tr(hx.get := s"/contacts/?page=${page + 1}", hx.trigger := "revealed", hx.swap := "afterend")( - td(contact.id), - td(contact.name), - td(contact.email) - ) - else tr(td(contact.id), td(contact.name), td(contact.email)) + html""" + + ${contact.id} + ${contact.name} + ${contact.email} + + """ + else + html""" + + ${contact.id} + ${contact.name} + ${contact.email} + + """ } + html"${contactsHtmls}" - private def createPage(bodyContent: Frag, inlineStyle: String = "") = doctype("html")( - html( - head( - tag("style")(inlineStyle), - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body(bodyContent) - ) - ) + private def createPage(bodyContent: Html, inlineStyle: String = "") = + html""" + + + + + + + + ${bodyContent} + + + """ } diff --git a/examples/htmx/htmx_inline_validation.sc b/examples/htmx/htmx_inline_validation.sc index 02ac2df..f8acf66 100644 --- a/examples/htmx/htmx_inline_validation.sc +++ b/examples/htmx/htmx_inline_validation.sc @@ -1,11 +1,10 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/inline-validation/ -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* -import ba.sake.sharaf.* +import play.twirl.api.Html +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer import ba.sake.formson.FormDataRW @@ -30,11 +29,13 @@ case class ContactForm(email: String, firstName: String, lastName: String) deriv object views { def IndexView(formData: ContactForm) = createPage( - div( - h3("Inline Validation example"), - p("Only valid email is test@test.com"), - contactForm(formData) - ), + html""" +
+

HTMX Inline Validation Example

+

Only valid email is test@test.com

+ ${contactForm(formData)} +
+ """, inlineStyle = """ .error-message { color:red; @@ -48,30 +49,50 @@ object views { """ ) - def contactForm(formData: ContactForm) = form(hx.post := "/contact", hx.swap := "outerHTML")( - emailField(formData.email, isError = false), - div(label("First Name")(input(name := "firstName", value := formData.firstName))), - div(label("Last Name")(input(name := "lastName", value := formData.lastName))), - button("Submit") - ) + def contactForm(formData: ContactForm) = + html""" +
+ ${emailField(formData.email, isError = false)} +
+ + +
+
+ + +
+ +
+ """ def emailField(fieldValue: String, isError: Boolean) = - div(hx.target := "this", hx.swap := "outerHTML", Option.when(isError)(cls := "error"))( - label("Email Address")( - input(name := "email", value := fieldValue, hx.post := "/contact/email", hx.indicator := "#ind"), - img(id := "ind", src := "/img/bars.svg", cls := "htmx-indicator") - ), - span("This will trigger validation on input change!"), - Option.when(isError)(div(cls := "error-message")("That email is already taken. Please enter another email.")) - ) + val cls = if (isError) "error" else "" + html""" +
+ + This will trigger validation on input change! + ${Option.when(isError)( + html"""
That email is already taken. Please enter another email.
""" + )} +
+ """ - private def createPage(bodyContent: Frag, inlineStyle: String = "") = doctype("html")( - html( - head( - tag("style")(inlineStyle), - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body(bodyContent) - ) - ) + private def createPage(bodyContent: Html, inlineStyle: String = "") = + html""" + + + + + + + + ${bodyContent} + + + """ } diff --git a/examples/htmx/htmx_lazy_load.sc b/examples/htmx/htmx_lazy_load.sc index 7f99100..0b37ff0 100644 --- a/examples/htmx/htmx_lazy_load.sc +++ b/examples/htmx/htmx_lazy_load.sc @@ -1,11 +1,9 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 // https://htmx.org/examples/lazy-load/ -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val routes = Routes: @@ -13,7 +11,7 @@ val routes = Routes: Response.withBody(views.IndexView) case GET -> Path("graph") => Thread.sleep(1000) // simulate slow, stonks - val graph = img(src := "/img/tokyo.png") + val graph = html""" + + + + + + +
+ Result loading... +
+ + + """ } diff --git a/examples/htmx/htmx_load_snippet.sc b/examples/htmx/htmx_load_snippet.sc index 9e89de8..c7065fc 100644 --- a/examples/htmx/htmx_load_snippet.sc +++ b/examples/htmx/htmx_load_snippet.sc @@ -1,33 +1,34 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val routes = Routes: case GET -> Path() => Response.withBody(IndexView) case POST -> Path("html-snippet") => - Response.withBody( - div( - b("WOW, it works! ๐Ÿ˜ฒ"), - div("Look ma, no JS! ๐Ÿ˜Ž") - ) - ) + Response.withBody: + html""" +
+ WOW, it works! ๐Ÿ˜ฒ +
Look ma, no JS! ๐Ÿ˜Ž
+
+ """ UndertowSharafServer("localhost", 8181, routes).start() println(s"Server started at http://localhost:8181") -def IndexView = doctype("html")( - html( - head( - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body( - button(hx.post := "/html-snippet", hx.swap := "outerHTML")("Click here!") - ) - ) -) +def IndexView = + html""" + + + + + + + + + + """ diff --git a/examples/htmx/htmx_progress_bar.sc b/examples/htmx/htmx_progress_bar.sc index f15c681..627470c 100644 --- a/examples/htmx/htmx_progress_bar.sc +++ b/examples/htmx/htmx_progress_bar.sc @@ -1,13 +1,11 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 import java.util.concurrent.TimeUnit // https://htmx.org/examples/progress-bar/ import java.util.concurrent.Executors -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer import ba.sake.sharaf.htmx.ResponseHeaders @@ -43,11 +41,14 @@ UndertowSharafServer("localhost", 8181, routes).start() println(s"Server started at http://localhost:8181") -def IndexView = doctype("html")( - html( - head( - tag("style")(""" - .progress { +def IndexView = + html""" + + + + + + + +
+

Start Progress

+ +
+ + + """ def progressBarWrapper(currentPercentage: Int) = val completed = currentPercentage >= 100 - div(hx.get := "/job", hx.trigger := "done", hx.target := "this", hx.swap := "outerHTML")( - h3(role := "status", id := "pblabel", tabindex := "-1")(if completed then "Completed" else "Running"), - progressBar(currentPercentage), - Option.when(completed)( - button(hx.post := "/start")("Restart Job") - ) - ) + html""" +
+

+ ${if completed then "Completed" else "Running"} +

+ ${progressBar(currentPercentage)} + ${Option.when(completed)( + html""" """ + )} +
+ """ def progressBar(currentPercentage: Int) = val completed = currentPercentage >= 100 - div( - hx.get := "/job/progress", - Option.unless(completed)(hx.trigger := "every 600ms"), - hx.target := "this", - hx.swap := "innerHTML" - )( - div( - cls := "progress", - role := "progressbar", - aria.valuemin := "0", - aria.valuemax := "100", - aria.valuenow := currentPercentage, - aria.labelledby := "pblabel" - )( - div(id := "pb", cls := "progress-bar", style := s"width:${currentPercentage}%") - ) - ) + html""" +
+
+
+
+
+ """ diff --git a/examples/htmx/htmx_tabs_hateoas.sc b/examples/htmx/htmx_tabs_hateoas.sc index cdc27c8..a2fe5f6 100644 --- a/examples/htmx/htmx_tabs_hateoas.sc +++ b/examples/htmx/htmx_tabs_hateoas.sc @@ -1,9 +1,7 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 -import scalatags.Text.all.* -import ba.sake.hepek.htmx.* -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val routes = Routes: @@ -20,30 +18,28 @@ UndertowSharafServer("localhost", 8181, routes).start() println(s"Server started at http://localhost:8181") +def IndexView = + html""" + + + + + + +
+
+ + + """ -def IndexView = doctype("html")( - html( - head( - script(src := "https://unpkg.com/htmx.org@2.0.4") - ), - body( - div( - id := "tabs", - hx.get := "/tab1", - hx.trigger := "load delay:100ms", - hx.target := "#tabs", - hx.swap := "innerHTML" - ) - ) - ) -) - -def tabSnippet(tabNum: Int) = div( - div( - cls := "tab-list", - button(hx.get := "/tab1", Option.when(tabNum == 1)(cls := "selected"), "Tab 1"), - button(hx.get := "/tab2", Option.when(tabNum == 2)(cls := "selected"), "Tab 2"), - button(hx.get := "/tab3", Option.when(tabNum == 3)(cls := "selected"), "Tab 3") - ), - div(id := "tab-content", cls := "tab-content")(s"TAB ${tabNum} content ....") -) +def tabSnippet(tabNum: Int) = + html""" +
+ + + +
+ ${s"TAB ${tabNum} content ...."} +
+
+ """ diff --git a/examples/oauth2/src/AppRoutes.scala b/examples/oauth2/src/AppRoutes.scala index 51ad185..724a78b 100644 --- a/examples/oauth2/src/AppRoutes.scala +++ b/examples/oauth2/src/AppRoutes.scala @@ -1,61 +1,61 @@ package demo -import scalatags.Text.all.* -import ba.sake.sharaf.* -import ba.sake.sharaf.routing.* -import ba.sake.hepek.html.HtmlPage -import ba.sake.sharaf.undertow.given +import ba.sake.sharaf.{*, given} class AppRoutes(securityService: SecurityService) { val routes = Routes: case GET -> Path("protected") => Response.withBody(ProtectedPage) - case GET -> Path("login") => Response.redirect("/") - case GET -> Path() => Response.withBody(IndexPage(securityService.currentUser)) - case _ => Response.withBody("Not found. ยฏ\\_(ใƒ„)_/ยฏ") } -class IndexPage(userOpt: Option[CustomUserProfile]) extends HtmlPage { - override def pageContent = frag( - userOpt match { - case None => - frag( - div("Hello there!"), - div( - // any protected route would work here actually.. - // just need to set ?provider=GitHubClient - a(href := "/login?provider=GitHubClient")("Login with GitHub") - ) - ) - case Some(user) => - frag( - div( - s"Hello ${user.name} !" - ), - div( - a(href := "/protected")("Protected page") - ), - div( - a(href := "/logout")("Logout") - ) - ) - } - ) -} - -object ProtectedPage extends HtmlPage { - override def pageContent = frag( - div("This is a protected page"), - div( - a(href := "/")("Home") - ) - ) -} +def IndexPage(userOpt: Option[CustomUserProfile]) = + userOpt match { + case None => + html""" + + + +
Hello there!
+
+ Login with GitHub +
+ + + """ + case Some(user) => + html""" + + + +
Hello ${user.name} !
+
+ Protected page +
+
+ Logout +
+ + + """ + } + +def ProtectedPage = + html""" + + + +
This is a protected page. You must be logged in to see this.
+
+ Home +
+ + + """ diff --git a/examples/scala-cli/README.md b/examples/scala-cli/README.md index 65674f8..2bacf6d 100644 --- a/examples/scala-cli/README.md +++ b/examples/scala-cli/README.md @@ -3,12 +3,12 @@ Example implementations of https://htmx.org/examples/ Run any of these from this folder: ```sh -scala-cli hello.sc --resource-dir resources +scala hello.sc --resource-dir resources ``` If you want to restart the server when files change, just add the `--restart` flag: ```sh -scala-cli hello.sc --resource-dir resources --restart +scala hello.sc --resource-dir resources --restart ``` diff --git a/examples/scala-cli/demo.sc b/examples/scala-cli/demo.sc index 5c4fc7b..07e81dc 100644 --- a/examples/scala-cli/demo.sc +++ b/examples/scala-cli/demo.sc @@ -1,5 +1,5 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 import ba.sake.querson.QueryStringRW import ba.sake.tupson.JsonRW @@ -20,13 +20,10 @@ val routes = Routes: CarsDb.add(newCar) Response.withBody(newCar) -UndertowSharafServer("localhost", 8181, routes) - .withExceptionMapper(ExceptionMapper.json) - .start() +UndertowSharafServer("localhost", 8181, routes, exceptionMapper = ExceptionMapper.json).start() println("Server started at http://localhost:8181") - object CarsDb { var db: Seq[Car] = Seq() def findAll(): Seq[Car] = db diff --git a/examples/scala-cli/form_handling.sc b/examples/scala-cli/form_handling.sc index 36f24e7..61d6cf3 100644 --- a/examples/scala-cli/form_handling.sc +++ b/examples/scala-cli/form_handling.sc @@ -1,15 +1,15 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 -import scalatags.Text.all.* import ba.sake.formson.FormDataRW -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} import ba.sake.sharaf.undertow.UndertowSharafServer val routes = Routes: case GET -> Path() => Response.withBody(ContactUsView) case POST -> Path("handle-form") => + case class ContactUsForm(fullName: String, email: String) derives FormDataRW val formData = Request.current.bodyForm[ContactUsForm] Response.withBody(s"Got form data: ${formData}") @@ -17,21 +17,20 @@ UndertowSharafServer("localhost", 8181, routes).start() println("Server started at http://localhost:8181") - -def ContactUsView = doctype("html")( - html( - body( - form(action := "/handle-form", method := "POST")( - div( - label("Full Name: ", input(name := "fullName", autofocus)) - ), - div( - label("Email: ", input(name := "email", tpe := "email")) - ), - input(tpe := "Submit") - ) - ) - ) -) - -case class ContactUsForm(fullName: String, email: String) derives FormDataRW +def ContactUsView = + html""" + + + +
+
+ +
+
+ +
+ +
+ + + """ diff --git a/examples/scala-cli/hello.sc b/examples/scala-cli/hello.sc index c9d81eb..3e265ac 100644 --- a/examples/scala-cli/hello.sc +++ b/examples/scala-cli/hello.sc @@ -1,5 +1,5 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 import ba.sake.sharaf.* import ba.sake.sharaf.undertow.UndertowSharafServer diff --git a/examples/scala-cli/html.sc b/examples/scala-cli/html.sc new file mode 100644 index 0000000..ce109d0 --- /dev/null +++ b/examples/scala-cli/html.sc @@ -0,0 +1,40 @@ +//> using scala "3.7.0" +//> using dep ba.sake::sharaf-undertow:0.12.1 + +import ba.sake.sharaf.{*, given} +import ba.sake.sharaf.undertow.UndertowSharafServer + +val routes = Routes: + case GET -> Path() => + Response.withBody(IndexView) + case GET -> Path("hello", name) => + Response.withBody(HelloView(name)) + +UndertowSharafServer("localhost", 8181, routes).start() + +println(s"Server started at http://localhost:8181") + +def IndexView = + html""" + + + +
+

Welcome!

+ Hello world +
+ + + """ + +def HelloView(name: String) = + html""" + + + +
+ Hello ${name}! +
+ + + """ diff --git a/examples/scala-cli/html_hepek.sc b/examples/scala-cli/html_hepek.sc index 5063c1c..44c85e9 100644 --- a/examples/scala-cli/html_hepek.sc +++ b/examples/scala-cli/html_hepek.sc @@ -1,5 +1,5 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 import scalatags.Text.all.* import ba.sake.hepek.html.HtmlPage diff --git a/examples/scala-cli/html_scalatags.sc b/examples/scala-cli/html_scalatags.sc index fa2e3ed..36db66a 100644 --- a/examples/scala-cli/html_scalatags.sc +++ b/examples/scala-cli/html_scalatags.sc @@ -1,5 +1,5 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 import scalatags.Text.all.* import ba.sake.sharaf.* diff --git a/examples/scala-cli/json_api.sc b/examples/scala-cli/json_api.sc index 8bb2976..3af69a9 100644 --- a/examples/scala-cli/json_api.sc +++ b/examples/scala-cli/json_api.sc @@ -1,5 +1,5 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 import ba.sake.tupson.JsonRW import ba.sake.sharaf.* @@ -14,7 +14,7 @@ object CarsDb { def add(car: Car): Unit = db = db.appended(car) } -val routes = Routes: +val routes = Routes: case GET -> Path("cars") => Response.withBody(CarsDb.findAll()) @@ -27,8 +27,6 @@ val routes = Routes: CarsDb.add(reqBody) Response.withBody(reqBody) -UndertowSharafServer("localhost", 8181, routes) - .withExceptionMapper(ExceptionMapper.json) - .start() +UndertowSharafServer("localhost", 8181, routes, exceptionMapper = ExceptionMapper.json).start() println("Server started at http://localhost:8181") diff --git a/examples/scala-cli/path_params.sc b/examples/scala-cli/path_params.sc index de1bb41..8635334 100644 --- a/examples/scala-cli/path_params.sc +++ b/examples/scala-cli/path_params.sc @@ -1,5 +1,5 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 import ba.sake.sharaf.* import ba.sake.sharaf.undertow.UndertowSharafServer diff --git a/examples/scala-cli/query_params.sc b/examples/scala-cli/query_params.sc index 45f1868..65f226c 100644 --- a/examples/scala-cli/query_params.sc +++ b/examples/scala-cli/query_params.sc @@ -1,5 +1,5 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 import ba.sake.querson.QueryStringRW import ba.sake.sharaf.* diff --git a/examples/scala-cli/sql_db.sc b/examples/scala-cli/sql_db.sc index 63d9a6e..5094d08 100644 --- a/examples/scala-cli/sql_db.sc +++ b/examples/scala-cli/sql_db.sc @@ -1,7 +1,7 @@ //> using scala "3.7.0" //> using dep org.postgresql:postgresql:42.7.5 //> using dep com.zaxxer:HikariCP:6.3.0 -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 //> using dep ba.sake::squery:0.7.0 import ba.sake.tupson.JsonRW diff --git a/examples/scala-cli/static_files.sc b/examples/scala-cli/static_files.sc index c7d3acf..5194d6e 100644 --- a/examples/scala-cli/static_files.sc +++ b/examples/scala-cli/static_files.sc @@ -1,5 +1,5 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 import ba.sake.sharaf.* import ba.sake.sharaf.undertow.UndertowSharafServer diff --git a/examples/scala-cli/validation.sc b/examples/scala-cli/validation.sc index c60d393..4a16a1d 100644 --- a/examples/scala-cli/validation.sc +++ b/examples/scala-cli/validation.sc @@ -1,5 +1,5 @@ //> using scala "3.7.0" -//> using dep ba.sake::sharaf-undertow:0.10.0 +//> using dep ba.sake::sharaf-undertow:0.12.1 import ba.sake.querson.QueryStringRW import ba.sake.tupson.JsonRW @@ -16,13 +16,10 @@ val routes = Routes: val json = Request.current.bodyJsonValidated[Car] Response.withBody(CarApiResult(s"JSON body OK: ${json}")) -UndertowSharafServer("localhost", 8181, routes) - .withExceptionMapper(ExceptionMapper.json) - .start() +UndertowSharafServer("localhost", 8181, routes, exceptionMapper = ExceptionMapper.json).start() println(s"Server started at http://localhost:8181") - case class Car(brand: String, model: String, quantity: Int) derives JsonRW object Car: given Validator[Car] = Validator @@ -37,4 +34,4 @@ object CarQuery: .derived[CarQuery] .notBlank(_.brand) -case class CarApiResult(message: String) derives JsonRW \ No newline at end of file +case class CarApiResult(message: String) derives JsonRW diff --git a/examples/user-pass-form/src/userpassform/AppRoutes.scala b/examples/user-pass-form/src/userpassform/AppRoutes.scala index 871f4ec..d054d02 100644 --- a/examples/user-pass-form/src/userpassform/AppRoutes.scala +++ b/examples/user-pass-form/src/userpassform/AppRoutes.scala @@ -1,7 +1,6 @@ package userpassform -import scalatags.Text.all.* -import ba.sake.sharaf.* +import ba.sake.sharaf.{*, given} class AppRoutes(callbackUrl: String, securityService: SecurityService) { val routes = Routes { @@ -19,50 +18,67 @@ class AppRoutes(callbackUrl: String, securityService: SecurityService) { } object views { - def index(currentUserOpt: Option[CustomUserProfile]) = doctype("html")( - html( - body( - a(href := "/protected-resource")("Protected resource"), - currentUserOpt.map { user => - div( - s"Hello ${user.name} !", - div( - a(href := "/logout")("Logout") - ) - ) - } - ) - ) - ) + def index(currentUserOpt: Option[CustomUserProfile]) = { + val currentUserHtml = currentUserOpt.map { user => + html""" +
+ Hello ${user.name} ! +
+ Logout +
+
+ """ + } + html""" + + + +
Hello there!
+
+ Protected resource + ${currentUserHtml} +
+ + + """ + } - def protectedResource(using currentUser: CustomUserProfile) = doctype("html")( - html( - body( - a(href := "/")("Home"), - div(s"Hello ${currentUser.name}! You are logged in!") - ) - ) - ) + def protectedResource(using currentUser: CustomUserProfile) = + html""" + + + +
+ Home +
+ Hello ${currentUser.name}! You are logged in! +
+
+ + + """ - def showForm(callbackUrl: String) = doctype("html")( - html( - body( - form(action := s"${callbackUrl}?client_name=FormClient", method := "POST")( - label( - "Username", - input(tpe := "text", name := "username") - ), - label( - "Password", - input(tpe := "text", name := "password") - ), - input(tpe := "submit", value := "Login") - ) - ), - div( - "Use johndoe/johndoe as username/password to login." - ) - ) - ) + def showForm(callbackUrl: String) = + html""" + + + +
+
+ + + +
+
+ Use johndoe/johndoe as username/password to login. +
+
+ + + """ } diff --git a/examples/user-pass-form/src/userpassform/Main.scala b/examples/user-pass-form/src/userpassform/Main.scala index 4def9ff..0c024a2 100644 --- a/examples/user-pass-form/src/userpassform/Main.scala +++ b/examples/user-pass-form/src/userpassform/Main.scala @@ -1,10 +1,9 @@ package userpassform import scala.jdk.CollectionConverters.* -import ba.sake.sharaf.* -import ba.sake.sharaf.undertow.SharafUndertowHandler import io.undertow.server.session.{InMemorySessionManager, SessionAttachmentHandler, SessionCookieConfig} import io.undertow.{Handlers, Undertow} +import io.undertow.server.handlers.BlockingHandler import org.pac4j.core.client.Clients import org.pac4j.core.config.Config import org.pac4j.core.credentials.password.JBCryptPasswordEncoder @@ -17,6 +16,8 @@ import org.pac4j.core.profile.service.InMemoryProfileService import org.pac4j.core.util.Pac4jConstants import org.pac4j.http.client.indirect.FormClient import org.pac4j.undertow.handler.{CallbackHandler, LogoutHandler, SecurityHandler} +import ba.sake.sharaf.* +import ba.sake.sharaf.undertow.* @main def main: Unit = val module = UserPassFormModule(8181) @@ -53,7 +54,10 @@ class UserPassFormModule(port: Int) { val securityService = SecurityService(pac4jConfig) private val securityHandler = SecurityHandler.build( - SharafUndertowHandler(SharafHandler.routes(AppRoutes(callbackUrl, securityService).routes)), + UndertowExceptionHandler( + ExceptionMapper.default, + SharafUndertowHandler(SharafHandler.routes(AppRoutes(callbackUrl, securityService).routes)) + ), pac4jConfig, clientNames.mkString(","), null, @@ -67,7 +71,9 @@ class UserPassFormModule(port: Int) { .addPrefixPath("/", securityHandler) private val finalHandler = - SessionAttachmentHandler(pathHandler, InMemorySessionManager("SessionManager"), SessionCookieConfig()) + new BlockingHandler( + SessionAttachmentHandler(pathHandler, InMemorySessionManager("SessionManager"), SessionCookieConfig()) + ) val server = Undertow .builder() diff --git a/querson/src/ba/sake/querson/QueryStringRW.scala b/querson/src/ba/sake/querson/QueryStringRW.scala index 8563b7b..5a1fa58 100644 --- a/querson/src/ba/sake/querson/QueryStringRW.scala +++ b/querson/src/ba/sake/querson/QueryStringRW.scala @@ -323,7 +323,7 @@ object QueryStringRW { try { $tryBlock } catch { - case e: IllegalArgumentException => + case _: IllegalArgumentException => throw ParsingException( ParseError( path, diff --git a/sharaf-core/src-jvm/ba/sake/sharaf/instances.scala b/sharaf-core/src-jvm/ba/sake/sharaf/instances.scala index 13e519b..15fbb45 100644 --- a/sharaf-core/src-jvm/ba/sake/sharaf/instances.scala +++ b/sharaf-core/src-jvm/ba/sake/sharaf/instances.scala @@ -6,6 +6,9 @@ import play.twirl.api.{Html, Xml} // TODO move to common when published for native export play.twirl.api.StringInterpolation +export play.twirl.api.Html +export play.twirl.api.Xml + // twirl HTML and XML given ResponseWritable[Html] with { override def write(value: Html, outputStream: OutputStream): Unit = diff --git a/sharaf-core/src/ba/sake/sharaf/Cookie.scala b/sharaf-core/src/ba/sake/sharaf/Cookie.scala index da57da1..7e4cf63 100644 --- a/sharaf-core/src/ba/sake/sharaf/Cookie.scala +++ b/sharaf-core/src/ba/sake/sharaf/Cookie.scala @@ -1,7 +1,6 @@ package ba.sake.sharaf import java.time.Instant -import java.util.Date final case class Cookie( name: String, diff --git a/sharaf-core/src/ba/sake/sharaf/ResponseWritable.scala b/sharaf-core/src/ba/sake/sharaf/ResponseWritable.scala index c03881d..b86d6e3 100644 --- a/sharaf-core/src/ba/sake/sharaf/ResponseWritable.scala +++ b/sharaf-core/src/ba/sake/sharaf/ResponseWritable.scala @@ -3,11 +3,8 @@ package ba.sake.sharaf import java.nio.charset.StandardCharsets import java.nio.file.Path import java.io.{FileInputStream, InputStream, OutputStream} -import scala.jdk.CollectionConverters.* import scala.util.Using import sttp.model.HeaderNames -import scalatags.Text.all.doctype -import scalatags.Text.Frag import ba.sake.tupson.{JsonRW, toJson} private val ContentTypeHttpString = HttpString(HeaderNames.ContentType) @@ -56,23 +53,6 @@ object ResponseWritable extends LowPriResponseWritableInstances { ) } - // really handy when working with HTMX ! - given ResponseWritable[Frag] with { - override def write(value: Frag, outputStream: OutputStream): Unit = - ResponseWritable[String].write(value.render, outputStream) - override def headers(value: Frag): Seq[(HttpString, Seq[String])] = Seq( - ContentTypeHttpString -> Seq("text/html; charset=utf-8") - ) - } - - given ResponseWritable[doctype] with { - override def write(value: doctype, outputStream: OutputStream): Unit = - ResponseWritable[String].write(value.render, outputStream) - override def headers(value: doctype): Seq[(HttpString, Seq[String])] = Seq( - ContentTypeHttpString -> Seq("text/html; charset=utf-8") - ) - } - given [T: JsonRW]: ResponseWritable[T] with { override def write(value: T, outputStream: OutputStream): Unit = ResponseWritable[String].write(value.toJson, outputStream) diff --git a/sharaf-core/src/ba/sake/sharaf/handlers/CorsHandler.scala b/sharaf-core/src/ba/sake/sharaf/handlers/CorsHandler.scala index 1c2dae3..75a4101 100644 --- a/sharaf-core/src/ba/sake/sharaf/handlers/CorsHandler.scala +++ b/sharaf-core/src/ba/sake/sharaf/handlers/CorsHandler.scala @@ -1,8 +1,7 @@ package ba.sake.sharaf.handlers -import scala.jdk.CollectionConverters.* -import ba.sake.sharaf.* import sttp.model.HeaderNames +import ba.sake.sharaf.* // https://www.moesif.com/blog/technical/cors/Authoritative-Guide-to-CORS-Cross-Origin-Resource-Sharing-for-REST-APIs/ final class CorsHandler(corsSettings: CorsSettings, next: SharafHandler) extends SharafHandler { diff --git a/sharaf-core/src/ba/sake/sharaf/routing/FromPathParam.scala b/sharaf-core/src/ba/sake/sharaf/routing/FromPathParam.scala index 7ecfce9..a223c30 100644 --- a/sharaf-core/src/ba/sake/sharaf/routing/FromPathParam.scala +++ b/sharaf-core/src/ba/sake/sharaf/routing/FromPathParam.scala @@ -63,7 +63,7 @@ object FromPathParam { try { Option($tryBlock) } catch { - case e: IllegalArgumentException => + case _: IllegalArgumentException => None } } @@ -71,7 +71,7 @@ object FromPathParam { } } - case hmm => report.errorAndAbort("Not supported") + case _ => report.errorAndAbort("Not supported") } private def isSingletonCasesEnum[T: Type](using Quotes): Boolean = diff --git a/sharaf-core/src/ba/sake/sharaf/routing/Routes.scala b/sharaf-core/src/ba/sake/sharaf/routing/Routes.scala index 1f0913f..be244fb 100644 --- a/sharaf-core/src/ba/sake/sharaf/routing/Routes.scala +++ b/sharaf-core/src/ba/sake/sharaf/routing/Routes.scala @@ -1,7 +1,6 @@ package ba.sake.sharaf.routing import ba.sake.sharaf.{HttpMethod, Request, Response} -import ba.sake.sharaf.exceptions.NotFoundException type RequestParams = (HttpMethod, Path) diff --git a/sharaf-helidon/src/ba/sake/sharaf/helidon/SharafHelidonHandler.scala b/sharaf-helidon/src/ba/sake/sharaf/helidon/SharafHelidonHandler.scala index 5f4ffe9..42e0f99 100644 --- a/sharaf-helidon/src/ba/sake/sharaf/helidon/SharafHelidonHandler.scala +++ b/sharaf-helidon/src/ba/sake/sharaf/helidon/SharafHelidonHandler.scala @@ -10,7 +10,7 @@ class SharafHelidonHandler(sharafHandler: SharafHandler) extends Handler { val reqParams = fillReqParams(helidonReq) val req = HelidonSharafRequest.create(helidonReq) val requestContext = RequestContext(reqParams, req) - val res = sharafHandler.handle(requestContext) + val res = sharafHandler.handle(requestContext) ResponseUtils.writeResponse(res, helidonRes) } diff --git a/sharaf-hepek-components/src/ba/sake/sharaf/hepek/instances.scala b/sharaf-hepek-components/src/ba/sake/sharaf/hepek/instances.scala new file mode 100644 index 0000000..275bdc6 --- /dev/null +++ b/sharaf-hepek-components/src/ba/sake/sharaf/hepek/instances.scala @@ -0,0 +1,17 @@ +package ba.sake.sharaf.hepek + +import java.io.OutputStream +import sttp.model.HeaderNames +import ba.sake.hepek.html.HtmlPage +import ba.sake.sharaf.* + +private val ContentTypeHttpString = HttpString(HeaderNames.ContentType) + +given ResponseWritable[HtmlPage] with { + override def write(value: HtmlPage, outputStream: OutputStream): Unit = + val htmlText = "" + value.contents + ResponseWritable[String].write(htmlText, outputStream) + override def headers(value: HtmlPage): Seq[(HttpString, Seq[String])] = Seq( + ContentTypeHttpString -> Seq("text/html; charset=utf-8") + ) +} diff --git a/sharaf-snunit/src/ba/sake/sharaf/snunit/SnunitSharafRequest.scala b/sharaf-snunit/src/ba/sake/sharaf/snunit/SnunitSharafRequest.scala index 414fbe0..79ae10d 100644 --- a/sharaf-snunit/src/ba/sake/sharaf/snunit/SnunitSharafRequest.scala +++ b/sharaf-snunit/src/ba/sake/sharaf/snunit/SnunitSharafRequest.scala @@ -21,11 +21,13 @@ class SnunitSharafRequest(underlyingRequest: SnunitRequest) extends Request { /* *** QUERY *** */ override lazy val queryParamsRaw: QueryStringMap = - underlyingRequest.query.split("&").flatMap( _.split("=") match { - case Array(key, value) => Seq(key -> Seq(value)) - case _ => Seq.empty - }) - .toMap + underlyingRequest.query + .split("&") + .flatMap(_.split("=") match { + case Array(key, value) => Seq(key -> Seq(value)) + case _ => Seq.empty + }) + .toMap /* *** BODY *** */ override lazy val bodyString: String = diff --git a/sharaf-undertow/src/ba/sake/sharaf/undertow/UndertowExceptionHandler.scala b/sharaf-undertow/src/ba/sake/sharaf/undertow/UndertowExceptionHandler.scala index 6c3fcc2..7945689 100644 --- a/sharaf-undertow/src/ba/sake/sharaf/undertow/UndertowExceptionHandler.scala +++ b/sharaf-undertow/src/ba/sake/sharaf/undertow/UndertowExceptionHandler.scala @@ -3,8 +3,6 @@ package ba.sake.sharaf.undertow import scala.util.control.NonFatal import io.undertow.server.HttpHandler import io.undertow.server.HttpServerExchange -import ba.sake.sharaf.* -import ba.sake.sharaf.undertow.* import ba.sake.sharaf.exceptions.ExceptionMapper final class UndertowExceptionHandler(exceptionMapper: ExceptionMapper, next: HttpHandler) extends HttpHandler { diff --git a/sharaf-undertow/src/ba/sake/sharaf/undertow/UndertowSharafRequest.scala b/sharaf-undertow/src/ba/sake/sharaf/undertow/UndertowSharafRequest.scala index 8c6c833..a3f060d 100644 --- a/sharaf-undertow/src/ba/sake/sharaf/undertow/UndertowSharafRequest.scala +++ b/sharaf-undertow/src/ba/sake/sharaf/undertow/UndertowSharafRequest.scala @@ -49,7 +49,7 @@ final class UndertowSharafRequest(val underlyingHttpServerExchange: HttpServerEx val uFormData = parser.parseBlocking() UndertowSharafRequest.undertowFormData2FormsonMap(uFormData) - override def toString(): String = + override def toString(): String = s"UndertowSharafRequest(headers=${headers}, cookies=${cookies}, queryParamsRaw=${queryParamsRaw}, bodyString=...)" } diff --git a/sharaf-undertow/src/ba/sake/sharaf/undertow/package.scala b/sharaf-undertow/src/ba/sake/sharaf/undertow/package.scala index a7f5a5c..2b30f54 100644 --- a/sharaf-undertow/src/ba/sake/sharaf/undertow/package.scala +++ b/sharaf-undertow/src/ba/sake/sharaf/undertow/package.scala @@ -1,19 +1,6 @@ package ba.sake.sharaf.undertow -import java.io.OutputStream -import ba.sake.hepek.html.HtmlPage import ba.sake.sharaf.* -import sttp.model.HeaderNames - -// TODO separate library -given ResponseWritable[HtmlPage] with { - override def write(value: HtmlPage, outputStream: OutputStream): Unit = - val htmlText = "" + value.contents - ResponseWritable[String].write(htmlText, outputStream) - override def headers(value: HtmlPage): Seq[(HttpString, Seq[String])] = Seq( - HttpString(HeaderNames.ContentType) -> Seq("text/html; charset=utf-8") - ) -} given (using r: Request): Session = val undertowReq = r.asInstanceOf[UndertowSharafRequest] diff --git a/sharaf-undertow/test/src/ba/sake/sharaf/undertow/ResponseWritableTest.scala b/sharaf-undertow/test/src/ba/sake/sharaf/undertow/ResponseWritableTest.scala index 301a5df..2d916b4 100644 --- a/sharaf-undertow/test/src/ba/sake/sharaf/undertow/ResponseWritableTest.scala +++ b/sharaf-undertow/test/src/ba/sake/sharaf/undertow/ResponseWritableTest.scala @@ -5,7 +5,7 @@ import java.nio.file.Paths import sttp.model.* import sttp.client4.quick.* import ba.sake.sharaf.{*, given} -import ba.sake.sharaf.undertow.{*, given} +import ba.sake.sharaf.undertow.UndertowSharafServer import ba.sake.sharaf.utils.NetworkUtils import ba.sake.tupson.JsonRW @@ -39,10 +39,6 @@ class ResponseWritableTest extends munit.FunSuite { case class JsonCaseClass(name: String, age: Int) derives JsonRW val json = JsonCaseClass("Meho", 40) Response.withBody(json) - case GET -> Path("scalatags", "frag") => - import scalatags.Text.all.* - val res = div("this is a div") - Response.withBody(res) case GET -> Path("twirl", "html") => Response.withBody(html""" @@ -64,27 +60,6 @@ class ResponseWritableTest extends munit.FunSuite { Don't forget me this weekend! """) - case GET -> Path("scalatags", "doctype") => - import scalatags.Text.all.{title => _, *} - import scalatags.Text.tags2.title - val res = doctype("html")( - html( - head( - title("doctype title") - ), - body( - "this is doctype body" - ) - ) - ) - Response.withBody(res) - case GET -> Path("hepek", "htmlpage") => - import scalatags.Text.all.* - import ba.sake.hepek.html.HtmlPage - val page = new HtmlPage { - override def pageContent = div("this is body") - } - Response.withBody(page) } val server = UndertowSharafServer("localhost", port, routes) @@ -166,28 +141,4 @@ class ResponseWritableTest extends munit.FunSuite { assertEquals(res.headers(HeaderNames.ContentType), Seq("application/xml; charset=utf-8")) } - test("Write response scalatags Frag") { - val res = quickRequest.get(uri"${baseUrl}/scalatags/frag").send() - assertEquals(res.body, """
this is a div
""".trim) - assertEquals(res.headers(HeaderNames.ContentType), Seq("text/html; charset=utf-8")) - } - - test("Write response scalatags doctype") { - val res = quickRequest.get(uri"${baseUrl}/scalatags/doctype").send() - assertEquals( - res.body, - """ Codestin Search Appthis is doctype body """.trim - ) - assertEquals(res.headers(HeaderNames.ContentType), Seq("text/html; charset=utf-8")) - } - - test("Write response hepek HtmlPage") { - val res = quickRequest.get(uri"${baseUrl}/hepek/htmlpage").send() - assertEquals( - res.body, - """ Codestin Search App
this is body
""".trim - ) - assertEquals(res.headers(HeaderNames.ContentType), Seq("text/html; charset=utf-8")) - } - } diff --git a/sharaf-undertow/test/src/ba/sake/sharaf/undertow/WebJarsTest.scala b/sharaf-undertow/test/src/ba/sake/sharaf/undertow/WebJarsTest.scala index 8564d2c..b4662da 100644 --- a/sharaf-undertow/test/src/ba/sake/sharaf/undertow/WebJarsTest.scala +++ b/sharaf-undertow/test/src/ba/sake/sharaf/undertow/WebJarsTest.scala @@ -13,14 +13,14 @@ class WebJarsTest extends munit.FunSuite { val routes = Routes { case GET -> Path() => Response.withBody("WebJars!") } - + val server = UndertowSharafServer("localhost", port, routes) - + override def beforeAll(): Unit = server.start() override def afterAll(): Unit = server.stop() - - // WebJars + + // WebJars test("WebJars should work") { val res = quickRequest.get(uri"${baseUrl}/jquery/3.7.1/jquery.js").send() assertEquals(res.headers(HeaderNames.ContentType), Seq("application/javascript"))