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"""
+
+
+
+
+
+
+ """'
+%}
-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
+
+
+
+
+
+