diff --git a/.github/workflows/ghpages.yml b/.github/workflows/ghpages.yml
index 1c27ac4..b9a6d6c 100644
--- a/.github/workflows/ghpages.yml
+++ b/.github/workflows/ghpages.yml
@@ -19,8 +19,14 @@ jobs:
distribution: temurin
java-version: 21
- name: Build
- run: ./mill -i docs.hepek
+ env:
+ FLATMARK_BASE_URL: https://sake92.github.io/sharaf
+ run: |
+ FLATMARK_VERSION=0.0.24
+ curl -L https://github.com/sake92/flatmark/releases/download/v${FLATMARK_VERSION}/flatmark_${FLATMARK_VERSION}_amd64.deb -o flatmark.deb
+ sudo apt install -y ./flatmark.deb
+ flatmark build -i docs
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4
with:
- folder: docs/hepek_output
+ folder: docs/_site
diff --git a/.gitignore b/.gitignore
index 4dca834..b8c8f59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,5 +16,6 @@ out/
.env
-hepek_output/
+_site/
+.flatmark-cache
diff --git a/.mill-version b/.mill-version
deleted file mode 100644
index dfa546b..0000000
--- a/.mill-version
+++ /dev/null
@@ -1 +0,0 @@
-0.12.14
\ No newline at end of file
diff --git a/DEV.md b/DEV.md
index 9fa0163..904e25e 100644
--- a/DEV.md
+++ b/DEV.md
@@ -18,10 +18,10 @@ scala-cli compile examples\scala-cli
```sh
# RELEASE
-$VERSION="0.11.0"
+$VERSION="0.12.0"
git commit --allow-empty -m "Release $VERSION"
git tag -a $VERSION -m "Release $VERSION"
-git push --atomic origin main --tags
+git push --atomic origin main $VERSION
```
diff --git a/TODO.md b/TODO.md
index 8a7729d..ab80f82 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2,8 +2,6 @@
- some kind of middleware mechanism
-- migrate docs to Pico.css
-
- 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 4c5e246..70e65fb 100644
--- a/build.mill
+++ b/build.mill
@@ -1,125 +1,116 @@
+//| mill-version: 1.0.0-RC3
package build
-
-import $ivy.`de.tototec::de.tobiasroeser.mill.vcs.version::0.4.1`
-import $ivy.`ba.sake::mill-hepek::0.1.0`
-
import mill._
import mill.scalalib._, scalajslib._, scalanativelib._
import mill.scalalib.publish._
-import mill.scalalib.SonatypeCentralPublishModule
-import de.tobiasroeser.mill.vcs.version.VcsVersion
-import ba.sake.millhepek.MillHepekModule
+import mill.javalib.SonatypeCentralPublishModule
+import mill.vcs.VcsVersion
-object V {
+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 {
+object `sharaf-core` extends Module:
+ object jvm extends SharafCoreModule with ScalaJvmCommonModule:
def moduleDeps = Seq(querson.jvm, formson.jvm, validson.jvm)
- }
- object native extends SharafCoreModule with ScalaNativeCommonModule {
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ // TODO move to common when published for native, and remove scalatags
+ mvn"org.playframework.twirl::twirl-api:2.1.0-M4"
+ )
+ object test extends ScalaTests with SharafTestModule
+
+ object native extends SharafCoreModule with ScalaNativeCommonModule:
def moduleDeps = Seq(querson.native, formson.native, validson.native)
object test extends ScalaNativeTests with SharafTestModule
- }
- trait SharafCoreModule extends SharafPublishModule with PlatformScalaModule {
+
+ trait SharafCoreModule extends SharafPublishModule with PlatformScalaModule:
def artifactName = "sharaf-core"
// all deps should be cross jvm/native
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"ba.sake::tupson::${V.tupson}",
- ivy"com.lihaoyi::scalatags::${V.scalatags}",
- ivy"com.lihaoyi::geny::1.1.1",
- ivy"com.softwaremill.sttp.client4::core::4.0.5"
+ 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"
)
- }
-}
-object `sharaf-undertow` extends SharafPublishModule {
+object `sharaf-undertow` extends SharafPublishModule:
def artifactName = "sharaf-undertow"
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"io.undertow:undertow-core:2.3.18.Final",
- ivy"ba.sake::tupson-config:${V.tupson}",
- ivy"ba.sake::hepek-components:${V.hepek}"
+ 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}"
)
def moduleDeps = Seq(`sharaf-core`.jvm)
- object test extends ScalaTests with SharafTestModule {
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"org.webjars:jquery:3.7.1"
+ object test extends ScalaTests with SharafTestModule :
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"org.webjars:jquery:3.7.1"
)
- }
-}
-object `sharaf-helidon` extends SharafPublishModule {
+object `sharaf-helidon` extends SharafPublishModule:
def artifactName = "sharaf-helidon"
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"io.helidon.webserver:helidon-webserver:4.2.2",
- ivy"io.helidon.config:helidon-config-yaml:4.2.2"
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"io.helidon.webserver:helidon-webserver:4.2.2",
+ mvn"io.helidon.config:helidon-config-yaml:4.2.2"
)
def moduleDeps = Seq(`sharaf-core`.jvm)
- object test extends ScalaTests with SharafTestModule {
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"com.lihaoyi::requests:0.9.0"
+ object test extends ScalaTests with SharafTestModule:
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"com.lihaoyi::requests:0.9.0"
)
- }
-}
-object `sharaf-snunit` extends ScalaNativeCommonModule with SharafPublishModule {
+object `sharaf-snunit` extends ScalaNativeCommonModule with SharafPublishModule:
def artifactName = "sharaf-snunit"
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"com.github.lolgab::snunit::0.10.3"
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"com.github.lolgab::snunit::0.10.3"
)
def moduleDeps = Seq(`sharaf-core`.native)
-}
-object querson extends Module {
- object jvm extends QuersonModule with ScalaJvmCommonModule
- object js extends QuersonModule with ScalaJSCommonModule
- object native extends QuersonModule with ScalaNativeCommonModule {
+object querson extends Module:
+ object jvm extends QuersonModule with ScalaJvmCommonModule:
+ object test extends ScalaTests with SharafTestModule
+ object js extends QuersonModule with ScalaJSCommonModule:
+ object test extends ScalaJSTests with SharafTestModule
+ object native extends QuersonModule with ScalaNativeCommonModule:
object test extends ScalaNativeTests with SharafTestModule
- }
- trait QuersonModule extends SharafPublishModule with PlatformScalaModule {
+ trait QuersonModule extends SharafPublishModule with PlatformScalaModule:
def artifactName = "querson"
def pomSettings = super.pomSettings().copy(description = "Sharaf query params library")
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"com.lihaoyi::fastparse::3.1.1"
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"com.lihaoyi::fastparse::3.1.1"
)
- }
-}
-object formson extends Module {
- object jvm extends FormsonModule with ScalaJvmCommonModule
- //object js extends FormsonModule with ScalaJSCommonModule // java.nio.Path not supported
- object native extends FormsonModule with ScalaNativeCommonModule {
+object formson extends Module:
+ object jvm extends FormsonModule with ScalaJvmCommonModule:
+ object test extends ScalaTests with SharafTestModule
+ //object js extends FormsonModule with ScalaJSCommonModule: // java.nio.Path not supported
+ // object test extends ScalaJSTests with SharafTestModule
+ object native extends FormsonModule with ScalaNativeCommonModule:
object test extends ScalaNativeTests with SharafTestModule
- }
- trait FormsonModule extends SharafPublishModule with PlatformScalaModule {
+ trait FormsonModule extends SharafPublishModule with PlatformScalaModule:
def artifactName = "formson"
def pomSettings = super.pomSettings().copy(description = "Sharaf form binding library")
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"com.lihaoyi::fastparse::3.1.1"
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"com.lihaoyi::fastparse::3.1.1"
)
- }
-}
-object validson extends Module {
- object jvm extends ValidsonModule with ScalaJvmCommonModule
- object js extends ValidsonModule with ScalaJSCommonModule
- object native extends ValidsonModule with ScalaNativeCommonModule {
+
+object validson extends Module:
+ object jvm extends ValidsonModule with ScalaJvmCommonModule:
+ object test extends ScalaTests with SharafTestModule
+ object js extends ValidsonModule with ScalaJSCommonModule:
+ object test extends ScalaJSTests with SharafTestModule
+ object native extends ValidsonModule with ScalaNativeCommonModule:
object test extends ScalaNativeTests with SharafTestModule
- }
- trait ValidsonModule extends SharafPublishModule with PlatformScalaModule {
+ trait ValidsonModule extends SharafPublishModule with PlatformScalaModule:
def artifactName = "validson"
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"com.lihaoyi::sourcecode::0.4.2"
- )
def pomSettings = super.pomSettings().copy(description = "Sharaf validation library")
- }
-}
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"com.lihaoyi::sourcecode::0.4.2"
+ )
-trait SharafPublishModule extends SharafCommonModule with SonatypeCentralPublishModule {
+trait SharafPublishModule extends SharafCommonModule with SonatypeCentralPublishModule:
def publishVersion = VcsVersion.vcsState().format()
def pomSettings = PomSettings(
organization = "ba.sake",
@@ -131,90 +122,68 @@ trait SharafPublishModule extends SharafCommonModule with SonatypeCentralPublish
Developer("sake92", "Sakib Hadžiavdić", "https://sake.ba")
)
)
-}
-trait SharafCommonModule extends ScalaModule {
- def scalaVersion = "3.4.2"
+
+trait SharafCommonModule extends ScalaModule:
+ def scalaVersion = "3.7.1"
def scalacOptions = super.scalacOptions() ++ Seq(
"-Yretain-trees", // needed for default parameters
"-deprecation",
"-Wunused:all",
"-explain"
)
-}
-trait ScalaJvmCommonModule extends ScalaModule {
- object test extends ScalaTests with SharafTestModule
-}
+trait ScalaJvmCommonModule extends ScalaModule
-trait ScalaJSCommonModule extends ScalaJSModule {
+trait ScalaJSCommonModule extends ScalaJSModule:
def scalaJSVersion = "1.19.0"
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"io.github.cquiroz::scala-java-time::2.6.0"
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"io.github.cquiroz::scala-java-time::2.6.0"
)
- object test extends ScalaJSTests with SharafTestModule
-}
-trait ScalaNativeCommonModule extends ScalaNativeModule {
+trait ScalaNativeCommonModule extends ScalaNativeModule:
def scalaNativeVersion = "0.5.7"
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"io.github.cquiroz::scala-java-time::2.6.0"
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"io.github.cquiroz::scala-java-time::2.6.0"
)
-
-}
-trait SharafTestModule extends TestModule.Munit {
- def ivyDeps = Agg(
- ivy"org.scalameta::munit::1.1.0"
+
+trait SharafTestModule extends TestModule.Munit:
+ def mvnDeps = Seq(
+ mvn"org.scalameta::munit::1.1.0"
)
-}
//////////////////// examples
-trait SharafExampleModule extends SharafCommonModule {
- def ivyDeps = Agg(
- ivy"ch.qos.logback:logback-classic:1.4.6"
+trait SharafExampleModule extends SharafCommonModule:
+ def mvnDeps = Seq(
+ mvn"ch.qos.logback:logback-classic:1.4.6"
)
-}
-object examples extends mill.Module {
- object api extends SharafExampleModule {
+object examples extends mill.Module:
+ object api extends SharafExampleModule:
def moduleDeps = Seq(`sharaf-undertow`)
object test extends ScalaTests with SharafTestModule
- }
- object fullstack extends SharafExampleModule {
+ object fullstack extends SharafExampleModule:
def moduleDeps = Seq(`sharaf-undertow`)
object test extends ScalaTests with SharafTestModule
- }
- object `user-pass-form` extends SharafExampleModule {
+ object `user-pass-form` extends SharafExampleModule:
def moduleDeps = Seq(`sharaf-undertow`)
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"org.pac4j:undertow-pac4j:5.0.1",
- ivy"org.pac4j:pac4j-http:5.7.0",
- ivy"org.mindrot:jbcrypt:0.4"
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"org.pac4j:undertow-pac4j:5.0.1",
+ mvn"org.pac4j:pac4j-http:5.7.0",
+ mvn"org.mindrot:jbcrypt:0.4"
)
object test extends ScalaTests with SharafTestModule
- }
- object oauth2 extends SharafExampleModule {
+ object oauth2 extends SharafExampleModule:
def moduleDeps = Seq(`sharaf-undertow`)
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"org.pac4j:undertow-pac4j:5.0.1",
- ivy"org.pac4j:pac4j-oauth:5.7.0"
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"org.pac4j:undertow-pac4j:5.0.1",
+ mvn"org.pac4j:pac4j-oauth:5.7.0"
)
- object test extends ScalaTests with SharafTestModule {
- def ivyDeps = super.ivyDeps() ++ Agg(
- ivy"no.nav.security:mock-oauth2-server:0.5.10"
+ object test extends ScalaTests with SharafTestModule:
+ def mvnDeps = super.mvnDeps() ++ Seq(
+ mvn"no.nav.security:mock-oauth2-server:0.5.10"
)
- }
- }
- object snunit extends SharafExampleModule with ScalaNativeCommonModule {
+ object snunit extends SharafExampleModule with ScalaNativeCommonModule:
def moduleDeps = Seq(`sharaf-snunit`)
- }
-}
-
-//////////////////// docs
-object docs extends MillHepekModule with SharafCommonModule {
- def ivyDeps = Agg(
- ivy"ba.sake::hepek:${V.hepek}",
- ivy"com.lihaoyi::os-lib:0.9.3"
- )
-}
+end examples
\ No newline at end of file
diff --git a/docs/_config.yaml b/docs/_config.yaml
new file mode 100644
index 0000000..ee888bb
--- /dev/null
+++ b/docs/_config.yaml
@@ -0,0 +1,17 @@
+
+name: Sharaf Documentation
+description: Sharaf Framework Documentation
+
+categories:
+ tutorials:
+ label: Tutorials
+ description: Sharaf Tutorials
+ howtos:
+ label: How-Tos
+ description: Sharaf How-Tos
+ reference:
+ label: Reference
+ description: Sharaf Reference Documentation
+ philosophy:
+ label: Philosophy
+ description: Sharaf Philosophy
diff --git a/docs/_data/project.yaml b/docs/_data/project.yaml
new file mode 100644
index 0000000..a3ed06e
--- /dev/null
+++ b/docs/_data/project.yaml
@@ -0,0 +1,13 @@
+name: Sharaf
+
+gh:
+ handle: "sake92"
+ projectName: "sharaf"
+ url: "https://github.com/sake92/sharaf"
+ sourcesUrl: "https://github.com/sake92/sharaf/tree/main"
+
+artifact:
+ org: "ba.sake"
+ name: "sharaf-undertow"
+ version: "0.11.1"
+
diff --git a/docs/_includes/footer.html b/docs/_includes/footer.html
new file mode 100644
index 0000000..556c08c
--- /dev/null
+++ b/docs/_includes/footer.html
@@ -0,0 +1,18 @@
+
\ No newline at end of file
diff --git a/docs/content/howtos/cors.md b/docs/content/howtos/cors.md
new file mode 100644
index 0000000..5d5e423
--- /dev/null
+++ b/docs/content/howtos/cors.md
@@ -0,0 +1,19 @@
+---
+title: CORS
+description: Sharaf How To CORS
+---
+
+# {{ page.title }}
+
+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:
+```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)...
+```
diff --git a/docs/content/howtos/exception-handler.md b/docs/content/howtos/exception-handler.md
new file mode 100644
index 0000000..d72c101
--- /dev/null
+++ b/docs/content/howtos/exception-handler.md
@@ -0,0 +1,24 @@
+---
+title: Exception Handler
+description: Sharaf How To Exception Handler
+---
+
+# {{ page.title }}
+
+How to customize the Exception handler?
+
+Use the `withExceptionMapper` on `UndertowSharafServer`:
+```scala
+val customExceptionMapper: ExceptionMapper = {
+ case e: MyException =>
+ val errorPage = MyErrorPage(e.getMessage())
+ Response.withBody(errorPage)
+ .withStatus(StatusCode.InternalServerError)
+}
+val finalExceptionMapper = customExceptionMapper.orElse(ExceptionMapper.default)
+val server = UndertowSharafServer(routes).withExceptionMapper(finalExceptionMapper)
+```
+
+The `ExceptionMapper` is a partial function from an exception to `Response`.
+Here we need to chain our custom exception mapper before the default one.
+
diff --git a/docs/content/howtos/external-config.md b/docs/content/howtos/external-config.md
new file mode 100644
index 0000000..c9344f1
--- /dev/null
+++ b/docs/content/howtos/external-config.md
@@ -0,0 +1,34 @@
+---
+title: External Config
+description: Sharaf How To External Config
+---
+
+# {{ page.title }}
+
+The [typesafe config](https://github.com/lightbend/config) library is already included in Sharaf.
+Also included is the [tupson-config](https://sake92.github.io/tupson/tutorials/parsing-config.html#usage-1) which simplifies the process:
+
+```scala
+import java.net.URL
+import com.typesafe.config.ConfigFactory
+import ba.sake.tupson.{given, *}
+import ba.sake.tupson.config.*
+
+case class MyConf(
+ port: Int,
+ url: URL,
+ string: String,
+ seq: Seq[String]
+) derives JsonRW
+
+val rawConfig = ConfigFactory.parseString("""
+ port = 7777
+ url = "http://example.com"
+ string = "str"
+ seq = [a, "b", c]
+""")
+
+val myConf = rawConfig.parseConfig[MyConf]
+// MyConf(7777,http://example.com,str,List(a, b, c))
+```
+
diff --git a/docs/content/howtos/index.md b/docs/content/howtos/index.md
new file mode 100644
index 0000000..221d217
--- /dev/null
+++ b/docs/content/howtos/index.md
@@ -0,0 +1,12 @@
+---
+title: How Tos
+description: Sharaf How Tos
+pagination:
+ sort_by: title
+---
+
+# {{ page.title }}
+
+
+Here are some common questions and answers you might have when using Sharaf.
+
diff --git a/docs/content/howtos/not-found.md b/docs/content/howtos/not-found.md
new file mode 100644
index 0000000..e68efba
--- /dev/null
+++ b/docs/content/howtos/not-found.md
@@ -0,0 +1,22 @@
+---
+title: NotFound
+description: Sharaf How To NotFound
+---
+
+# {{ page.title }}
+
+How to customize 404 NotFound handler?
+
+
+Use the `withNotFoundHandler` on `UndertowSharafServer`:
+```scala
+UndertowSharafServer(routes).withNotFoundHandler { req =>
+ Response.withBody(MyCustomNotFoundPage)
+ .withStatus(StatusCode.NotFound)
+}
+```
+
+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/query-params.md b/docs/content/howtos/query-params.md
new file mode 100644
index 0000000..3ef1a0e
--- /dev/null
+++ b/docs/content/howtos/query-params.md
@@ -0,0 +1,96 @@
+---
+title: Query Parameters
+description: Sharaf How To Query Parameters
+---
+
+# {{ page.title }}
+
+
+## How to bind query parameter as an enum?
+
+Sharaf needs a `QueryStringRW[T]` instance for query params.
+It can automatically derive an instance for singleton enums:
+
+```scala
+enum Cloud derives QueryStringRW:
+ case aws, gcp, azure
+
+case class MyQueryParams(
+ cloud: Cloud
+) derives QueryStringRW
+```
+
+## How to bind optional query parameter?
+
+The first option is to set the parameter to `Option[T]`:
+```scala
+case class MyQP(mandatory: String, opt: Option[Int]) derives QueryStringRW
+```
+If you make a request with params `?mandatory=abc`, `opt` will have value of `None`.
+
+---
+The second option is to set the parameter to some default value:
+```scala
+case class MyQP2(mandatory: String, opt: Int = 42) derives QueryStringRW
+```
+Here if you make a request with params `?mandatory=abc` the `opt` will have value of `42`.
+
+> Note that you need the `-Yretain-trees` scalac flag turned on, otherwise it won't work!
+
+## How to bind sequence query parameter?
+
+Set the parameter to `Seq[T]`:
+```scala
+case class MyQP(seq: Seq[Int]) derives QueryStringRW
+```
+
+Let's consider a few possible requests with these query params:
+- `?` (empty) -> `seq` will be empty `Seq()`
+- `?seq=123` -> `seq` will be `Seq(123)`
+- `?seq[]=123&seq[]=456` -> `seq` will be `Seq(123, 456)`
+- `?seq[1]=123&seq[0]=456` -> `seq` will be `Seq(456, 123)` (note it is sorted here)
+
+## How to bind composite query parameter?
+
+You can make a common query params class and use it in multiple top-level query params, or standalone:
+```scala
+case class PageQP(page: Int, size: Int) derives QueryStringRW
+case class MyQP(q: String, p: PageQP) derives QueryStringRW
+```
+
+Sharaf is quite lenient when parsing the query parameters, so all these combinations will work:
+- `?q=abc&p.page=0&p.size=10` -> object style
+- `?q=abc&p[page]=0&p[size]=10` -> brackets style
+- `?q=abc&p[page]=0&p.size=10` -> mixed style (dont)
+
+
+## How to bind a custom query parameter?
+
+When you want to handle a custom *scalar* value in query params,
+you need to implement a `QueryStringRW[T]` instance manually:
+```scala
+import ba.sake.querson.*
+
+given QueryStringRW[MyType] with {
+ override def write(path: String, value: MyType): QueryStringData =
+ QueryStringRW[String].write(path, value.toString)
+
+ override def parse(path: String, qsData: QueryStringData): MyType =
+ val str = QueryStringRW[String].parse(path, qsData)
+ Try(MyType.fromString(str)).toOption.getOrElse(typeError(path, "MyType", str))
+}
+
+private def typeError(path: String, tpe: String, value: Any): Nothing =
+ throw ParsingException(ParseError(path, s"invalid \$tpe", Some(value)))
+```
+
+Then you can use it:
+```scala
+case class MyQueryParams(
+ myType: MyType
+) derives QueryStringRW
+```
+
+---
+Note that Sharaf can automatically derive an instance for singleton enums.
+
diff --git a/docs/content/howtos/redirect.md b/docs/content/howtos/redirect.md
new file mode 100644
index 0000000..0a069f0
--- /dev/null
+++ b/docs/content/howtos/redirect.md
@@ -0,0 +1,16 @@
+---
+title: Redirect
+description: Sharaf How To Redirect
+---
+
+# {{ page.title }}
+
+
+Use the `Response.redirect` function:
+```scala
+case GET -> Path("a-deprecated-route") =>
+ Response.redirect("/this-other-place")
+```
+
+This will redirect the request to "/this-other-place",
+with status `301 MOVED_PERMANENTLY`
diff --git a/docs/content/howtos/response-body.md b/docs/content/howtos/response-body.md
new file mode 100644
index 0000000..41297d2
--- /dev/null
+++ b/docs/content/howtos/response-body.md
@@ -0,0 +1,28 @@
+---
+title: Response Body
+description: Sharaf How To Response Body
+---
+
+# {{ page.title }}
+
+How to use a custom response body?
+
+You need to define a custom `ResponseWritable[T]` for your type `T`.
+
+Let's say you have a `MyXML` class, and you want to use it as a response body.
+You would write something like this:
+```scala
+given ResponseWritable[MyXML] with {
+ override def write(value: MyXML, exchange: HttpServerExchange): Unit =
+ exchange.getResponseSender.send(value.asString)
+ override def headers(value: String): Seq[(HttpString, Seq[String])] = Seq(
+ HttpString(HeaderNames.ContentType) -> Seq("text/xml")
+ )
+}
+```
+
+Now you can use `MyXML` as a response body:
+```scala
+val myXml = MyXML(...)
+Response.withBody(myXml)
+```
diff --git a/docs/content/howtos/routes.md b/docs/content/howtos/routes.md
new file mode 100644
index 0000000..02a8479
--- /dev/null
+++ b/docs/content/howtos/routes.md
@@ -0,0 +1,122 @@
+---
+title: Routes
+description: Sharaf How To Routes
+---
+
+# {{ page.title }}
+
+## How to match on multiple methods?
+
+You can use the `|` operator in a pattern match:
+```scala
+case (GET | POST) -> Path() =>
+...
+```
+You can always check the [Scala docs](https://docs.scala-lang.org/scala3/book/control-structures.html#handling-multiple-possible-matches-on-one-line)
+for more help.
+
+---
+If you want to handle all possible methods, just don't use any extractors:
+```scala
+case method -> Path() =>
+...
+```
+
+
+## How to match on multiple paths?
+You can use the `|` operator in a pattern match:
+```scala
+case GET -> (Path("hello") | Path("hello-world")) =>
+ ...
+```
+You can always check the [Scala docs](https://docs.scala-lang.org/scala3/book/control-structures.html#handling-multiple-possible-matches-on-one-line)
+for more help.
+
+---
+If you want to handle all paths that start with "my-prefix/":
+```scala
+case GET -> Path("my-prefix", segments*) =>
+ ...
+```
+
+---
+If you want to handle all possible paths:
+```scala
+case GET -> Path(segments*) =>
+ ...
+```
+
+## How to bind path parameter as an enum?
+
+Sharaf needs a `FromPathParam[T]` instance for the `param[T]` extractor.
+It can automatically derive an instance for singleton enums:
+
+```scala
+enum Cloud derives FromPathParam:
+ case aws, gcp, azure
+
+val routes = Routes:
+ case GET -> Path("pricing", param[Cloud](cloud)) =>
+ Response.withBody(s"cloud = ${cloud}")
+```
+
+## How to bind path parameter as a regex?
+
+```scala
+val userIdRegex = "user_id_(\\d+)".r
+
+val routes = Routes:
+ case GET -> Path("pricing", userIdRegex(userId)) =>
+ Response.withBody(s"userId = ${userId}")
+```
+
+Note that the `userId` is bound as a `String`.
+
+You could further match on it, for example:
+```scala
+val routes = Routes:
+ case GET -> Path("pricing", userIdRegex(param[Int](userId))) =>
+```
+would extract `userId` as an `Int`.
+
+
+## How to bind a custom path parameter?
+
+Sharaf needs a `FromPathParam[T]` instance available:
+```scala
+import ba.sake.sharaf.routing.*
+
+given FromPathParam[MyType] with {
+ def parse(str: String): Option[MyType] =
+ parseMyType(str) // impl here
+}
+
+val routes = Routes:
+ case GET -> Path("pricing", param[MyType](myType)) =>
+ Response.withBody(s"myType = ${myType}")
+```
+
+## How to split Routes?
+
+When you have lots of routes, you will want to split them into multiple `Routes` handlers.
+Combining them is done with `Routes.merge`.
+The order of routes is preserved, of course:
+```scala
+val routes: Seq[Routes] = Seq(routes1, routes2, ... )
+
+val allRoutes: Routes = Routes.merge(routes)
+```
+
+You can also `extend SharafController` instead of `Routes` directly.
+```scala
+class MyController1 extends SharafController:
+ override def routes: Routes = Routes:
+ case ...
+class MyController2 extends SharafController:
+ override def routes: Routes = Routes:
+ case ...
+
+val server = UndertowSharafServer(
+ new MyController1, new MyController2
+)
+```
diff --git a/docs/content/howtos/upload-file.md b/docs/content/howtos/upload-file.md
new file mode 100644
index 0000000..f680c06
--- /dev/null
+++ b/docs/content/howtos/upload-file.md
@@ -0,0 +1,29 @@
+---
+title: Upload File
+description: Sharaf How To Upload File
+---
+
+# {{ page.title }}
+
+Uploading a file is usually done via `multipart/form-data` form submission.
+
+
+```scala
+// 1. somewhere in a view, use enctype="multipart/form-data"
+form(action := "/form-submit", method := "POST", enctype := "multipart/form-data")(
+ ...
+)
+
+// 2. define form data class with a NIO Path file
+import java.nio.file.Path
+import ba.sake.formson.*
+
+case class MyData(file: Path) derives FormDataRW
+
+// 3. handle the file however you want
+case POST -> Path("form-submit") =>
+ val formData = Request.current.bodyForm[MyData]
+ val fileAsString = Files.readString(formData.file)
+```
+
+You can find a working example in the [repo]({{site.data.project.gh.sourcesUrl}}/examples/fullstack).
diff --git a/docs/content/index.md b/docs/content/index.md
new file mode 100644
index 0000000..44d9f87
--- /dev/null
+++ b/docs/content/index.md
@@ -0,0 +1,27 @@
+---
+title: Sharaf
+description: Sharaf - a minimalistic Scala 3 web framework.
+pagination:
+ enabled: false
+---
+
+# {{ page.title }}
+
+Sharaf is a minimalistic Scala 3 web framework.
+
+Jump right into:
+- [Tutorials](/tutorials) to get you started
+- [How-Tos](/howtos) to get answers for some common questions
+- [Reference](/reference) to see detailed information
+- [Philosophy](/philosophy) to get insights into design decisions
+
+---
+Site map:
+
+TODO
+
+
+
+
+
+
diff --git a/docs/src/files/philosophy/Authorization.scala b/docs/content/philosophy/Authorization.scala
similarity index 100%
rename from docs/src/files/philosophy/Authorization.scala
rename to docs/content/philosophy/Authorization.scala
diff --git a/docs/content/philosophy/alternatives.md b/docs/content/philosophy/alternatives.md
new file mode 100644
index 0000000..a0fc506
--- /dev/null
+++ b/docs/content/philosophy/alternatives.md
@@ -0,0 +1,29 @@
+---
+title: Alternatives
+description: Sharaf Alternatives
+---
+
+# {{ page.title }}
+
+What about other frameworks?
+
+## Async frameworks like Play, Akka HTTP etc
+Synchronous programming is much, much easier to understand, debug, profile etc..
+Benefits (performance/throughput) of async handling are mostly void in Java 21, with introduction of Virtual threads. Yay!
+
+Only bummer for now is that Undertow doesn't still support them.. :/
+But undertow is very performant in the current shape too, so for most use cases it will be enough.
+Java 24 is a game changer for Undertow, because it solves the problem of [Synchronize Virtual Threads without Pinning](https://openjdk.org/jeps/491).
+
+## Pure FP libs like http4s, zio-http etc
+
+Too much focus on purely functional programming and (mostly unnecessary) math concepts.
+Easy to get lost in that and overcomplicate your code.
+
+## Enterprise frameworks like Spring Framework, Quarkus etc
+Too much annotations, autoconfigurations, dependency injection, proxies and complexity.
+
+## Standalone JEE servers like Tomcat, Jetty etc
+I was looking into these, but then sharaf would have to depend on Servlets API,
+use `@Inject` and gazzilion of god-knows-what-they-do annotations just to configure OAuth2 for example...
+
diff --git a/docs/content/philosophy/authentication.md b/docs/content/philosophy/authentication.md
new file mode 100644
index 0000000..121b143
--- /dev/null
+++ b/docs/content/philosophy/authentication.md
@@ -0,0 +1,67 @@
+---
+title: Authentication
+description: Sharaf Authentication
+---
+
+
+# {{ page.title }}
+
+Some important security principles from OWASP guidelines:
+- use HTTPS
+- use random user ids to prevent enumeration and other attacks
+- use strong passwords, store them hashed, implement password recovery
+- use MFA, CAPTCHA, rate limiting etc to prevent automated attacks
+- etc.
+
+Read all of them in the [OWASP auth cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html).
+
+
+## Pac4j
+
+Authentication in Sharaf is done usually by delegating it to [pac4j](https://www.pac4j.org/index.html).
+Pac4j is a battle-tested and widely used library for authentication and authorization.
+It supports many authentication mechanisms, including:
+- form based authentication (username + password)
+- OAuth2, with many providers (Google, Facebook, GitHub, etc)
+
+Pac4j has a concept of `Client`, which is a type of authentication mechanism.
+The main split is between `IndirectClient` and `DirectClient`.
+
+### Indirect clients
+Indirect clients are used for form based authentication, OAuth2, etc.
+An important thing to mention here is the callback URL:
+- for username + password authentication, the callback URL where the form is submitted to. Then a server-side session is created and user is signed in.
+- for OAuth2 (and similar mechanisms), the callback URL where the user is redirected to *after authentication*.
+ The server will then exchange the code for an *access token* and create a server-side session.
+
+### Direct clients
+Direct clients are used for API authentication *on every request* (e.g. Basic Auth, JWT, etc).
+On every request, the client will extract the credentials from the request and authenticate the user.
+
+
+## Deny by Default Principle
+
+One important principle in security is the "deny by default" principle.
+You should use whitelisting, allow access only to what is needed.
+This is because it is easy to forget to deny something, and it is hard to remember everything that should be denied.
+
+Concretely in pac4j, you can use `PathMatcher()`, to exclude certain paths from authentication:
+```scala
+val publicRoutesMatcher = PathMatcher()
+publicRoutesMatcher.excludePaths("/", "/login-form")
+pac4jConfig.addMatcher("publicRoutesMatcher", publicRoutesMatcher)
+..
+SecurityHandler.build(
+ SharafHandler(..),
+ pac4jConfig,
+ "client1,client2...",
+ null,
+ "securityheaders,publicRoutesMatcher", // use publicRoutesMatcher here!
+ DefaultSecurityLogic()
+)
+```
+
+There are also:
+- `excludeBranch("/somepath")` to exclude all paths starting with "/somepath"
+- `excludeRegex("^/somepath/.*\$")` to exclude all paths matching the regex (be careful with this one!)
+
diff --git a/docs/content/philosophy/dependency-injection.md b/docs/content/philosophy/dependency-injection.md
new file mode 100644
index 0000000..2522604
--- /dev/null
+++ b/docs/content/philosophy/dependency-injection.md
@@ -0,0 +1,65 @@
+---
+title: Dependency Injection
+description: Sharaf Dependency Injection
+---
+
+# {{ page.title }}
+
+Do you even Dependency Injection??
+
+Not in a classical / "dependency container" / Spring / JEE style.
+
+Not in a purely-functional-monadic style.
+
+Yes in a direct style:
+- for singletons: just *instantiate a class* and pass the object around.
+- for request/session-scoped instances: use scala 3 *context functions* (implicit functions).
+
+If you ever used PlayFramework, Slick 2 and similar, you might have used this pattern:
+```scala
+someFunction { implicit ctx =>
+ // some code that needs an implicit Ctx
+}
+```
+
+In Scala 3 there is a new concept called "context function" which represents the pattern from above with a type:
+```scala
+type ContextualAction = Ctx ?=> Unit
+```
+Now, instead of manually writing `implicit ctx` we can skip it:
+```scala
+someFunction {
+ // some code that needs an implicit Ctx
+}
+```
+and compiler will fill it in for us.
+The `Ctx` parameter is implicitly/contextually available (only) in the function body.
+You can get it with `summon[Ctx]` or `using ctx: Ctx` (which is a bit more readable).
+
+
+---
+Sharaf has the `Routes` type that is defined as `Request ?=> PartialFunction[RequestParams, Response[?]]`.
+This means that you can call `Request.current` only in a `Routes` definition body (because it requires a `given Request` to be present).
+
+If you need a request-scoped instance (a-la `@RequestScoped @Bean` in Spring),
+you need to define a function that is `using Request`:
+```scala
+def currentUser(using req: Request): User =
+ // extract stuff from request
+```
+Same as `Request.current`, you can only use the `currentUser` function in a context of a request!
+
+---
+
+By using context functions, you avoid [banging your head against the wall](https://stackoverflow.com/questions/26305295/how-is-the-requestscoped-bean-instance-provided-to-sessionscoped-bean-in-runti)
+while trying to figure out how-the-hell can you inject a request-scoped-thing into a singleton/session-scoped thing...
+Proxy to proxy to proxy, something, something.. ok.
+
+You also avoid reading yet-another-lousy-monad-tutorial, losing your brain-battle agains `State`, `RWS`, `Kleisli`, higher-kinded-types, weird macros, compile times and type inference...
+
+
+
+
+
+
+
diff --git a/docs/content/philosophy/index.md b/docs/content/philosophy/index.md
new file mode 100644
index 0000000..ce08dcd
--- /dev/null
+++ b/docs/content/philosophy/index.md
@@ -0,0 +1,33 @@
+---
+title: Philosophy
+description: Sharaf Philosophy
+pagination:
+ sort_by: title
+---
+
+# {{ page.title }}
+
+## Why Sharaf?
+
+Simplicity and ease of use is the main focus of Sharaf.
+
+Sharaf is built on top of [Undertow](https://undertow.io/).
+This means you can use awesome libraries built for Undertow, like [pac4j](https://github.com/pac4j/undertow-pac4j) for security and others.
+You can leverage Undertow's lower level API, e.g. for WebSockets.
+
+Sharaf bundles a set of standalone libraries:
+- [querson]({{site.data.project.gh.sourcesUrl}}/querson) for query parameters
+- [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
+- [sttp](https://sttp.softwaremill.com/en/latest/) for firing HTTP requests
+- [typesafe-config](https://github.com/lightbend/config) for configuration
+
+You can use any of above separately in your projects.
+
+
+## Why name "Sharaf"
+
+Šaraf means "a screw" in Bosnian, which reminds me of scala spiral logo.
+It's a germanism I think.
diff --git a/docs/content/philosophy/query-params-handling.md b/docs/content/philosophy/query-params-handling.md
new file mode 100644
index 0000000..04c711b
--- /dev/null
+++ b/docs/content/philosophy/query-params-handling.md
@@ -0,0 +1,56 @@
+---
+title: Query Params Handling
+description: Sharaf Query Params Handling
+---
+
+# {{ page.title }}
+
+
+## Query params handling design
+
+Web frameworks do handle query params 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
+- pattern matching: http4s
+- parsing from request: Sharaf
+
+
+## Why not annotations?
+
+This approach is mostly fine, as long as you know from where a parameter comes.
+
+In Spring you use the `@RequestParam` annotation when you have simple parameters.
+But when you want to group them in a class [you don't use it](https://stackoverflow.com/questions/16942193/spring-mvc-complex-object-as-get-requestparam).. #wtf
+Also, that same class can be bound from the form body too... convenient? eh.
+
+In [Cask](https://com-lihaoyi.github.io/cask/#variable-routes) there is no annotation, so it is ambiguous in my opinion.
+
+
+## Why not special route file?
+You need a special compiler for this, essentially a new language.
+People have to learn how it works, there's probably no syntax highlighting, no autocomplete etc.
+
+
+## Why not in-language DSL?
+
+Similar to special route file approach, people need to learn it.
+Not a huge deal I guess.
+
+
+## Why not pattern matching?
+If you look at [http4s' approach](https://http4s.org/v0.23/docs/dsl.html#handling-query-parameters),
+you can see that if the query param is not found, it falls through.
+It is customizable, but more work for you. eh.
+Essentially you'll get a 404.. which is not a good choice IMO.
+
+Rarely any framework does this, and you rarely want to handle *the same path* in 2 places.
+
+
+## Sharaf's approach
+
+Sharaf parses query params from the `Request`.
+Admittedly, you do have to make a new class if you want to parse them in a typesafe way.
+But you usually do grouping of these parameters when passing them further, so why not do it immediatelly.
+
+[Composition](/howtos/query-params.html#how-to-bind-composite-query-parameter) adds even more benefits, which I rarely saw implemented in any framework.
diff --git a/docs/content/philosophy/routes-matching.md b/docs/content/philosophy/routes-matching.md
new file mode 100644
index 0000000..7047b2d
--- /dev/null
+++ b/docs/content/philosophy/routes-matching.md
@@ -0,0 +1,67 @@
+---
+title: Routes Matching
+description: Sharaf Routes Matching
+---
+
+# {{ page.title }}
+
+
+## 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
+
+
+## Why not annotations?
+
+Let's see an example:
+```scala
+@GetMapping(value = "/student/{studentId}")
+public Student studentData1(@PathVariable Integer studentId) {}
+
+@GetMapping(value = "/student/{studentId}")
+public Student studentData2(@PathVariable Integer studentId) {}
+
+@GetMapping(value = "/student/umm")
+public Student studentData3(@PathVariable Integer studentId) {}
+```
+Issues:
+- the `studentId` appears in 2 places, you can make a typo and nothing will work.
+- the `"/student/{studentId}"` route is duplicated, there is no compiler support and it will fail only in runtime..
+- you have to [wonder](https://stackoverflow.com/questions/2326912/ordered-requestmapping-in-spring-mvc) if `studentData1` will be picked up before `studentData3`..!?
+
+
+## Why not special route file?
+Well, you need a special compiler for this, essentially a new language.
+People have to learn how it works, there's probably no syntax highlighting, no autocomplete etc.
+
+
+## Why not in-language DSL?
+Similar to special route file approach, people need to learn it.
+And again, you don't leverage compiler's support like exhaustive pattern matching and extractors.
+
+
+## Sharaf's approach
+
+Sharaf does its route matching in plain scala code.
+Scala's pattern matching warns you when you have *duplicate routes*, or *impossible* routes.
+For example, if you write this:
+```scala
+case GET -> Path("cars", brand) => ???
+case GET -> Path("cars", model) => ??? // Unreachable case
+
+case GET -> Path("files", segments*) => ???
+case GET -> Path("files", "abc.txt") => ??? // Unreachable case
+```
+you will get nice warnings, thanks compiler!
+
+---
+You can extract path variables with pattern matching:
+```scala
+case GET -> Path("cars", param[Int](carId)) => ???
+```
+Here, the `carId` is parsed as `Int` and it *mentioned only once*, unlike with the annotation approach.
+
diff --git a/docs/content/reference/index.md b/docs/content/reference/index.md
new file mode 100644
index 0000000..4576751
--- /dev/null
+++ b/docs/content/reference/index.md
@@ -0,0 +1,10 @@
+---
+title: Reference
+description: Sharaf Reference
+---
+
+# {{ page.title }}
+
+
+Take a look at [Sharaf scaladoc](https://javadoc.io/doc/ba.sake/sharaf_3).
+
diff --git a/docs/content/search/results.html b/docs/content/search/results.html
new file mode 100644
index 0000000..fea2423
--- /dev/null
+++ b/docs/content/search/results.html
@@ -0,0 +1,3 @@
+---
+layout: search-results.html
+---
\ No newline at end of file
diff --git a/docs/content/tutorials/forms.md b/docs/content/tutorials/forms.md
new file mode 100644
index 0000000..563fbdc
--- /dev/null
+++ b/docs/content/tutorials/forms.md
@@ -0,0 +1,66 @@
+---
+title: Forms
+description: Sharaf Tutorial Forms
+---
+
+# {{ page.title }}
+
+
+
+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:
+```scala
+//> using scala "3.7.0"
+//> using dep ba.sake::sharaf-undertow:0.10.0
+
+import scalatags.Text.all.*
+import ba.sake.formson.FormDataRW
+import ba.sake.sharaf.*
+import ba.sake.sharaf.undertow.UndertowSharafServer
+
+val routes = Routes:
+ case GET -> Path() =>
+ Response.withBody(ContactUsView)
+ case POST -> Path("handle-form") =>
+ val formData = Request.current.bodyForm[ContactUsForm]
+ Response.withBody(s"Got form data: ${formData}")
+
+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
+
+```
+
+Then run it like this:
+```sh
+scala-cli form_handling.sc
+```
+
+Now go to [http://localhost:8181](http://localhost:8181)
+and fill in the page with some data.
+
+When you click the "Submit" button you will see a response like this:
+```
+Got form data: ContactUsForm(Bob,bob@example.com)
+```
diff --git a/docs/content/tutorials/hello-world.md b/docs/content/tutorials/hello-world.md
new file mode 100644
index 0000000..8158231
--- /dev/null
+++ b/docs/content/tutorials/hello-world.md
@@ -0,0 +1,38 @@
+---
+title: Hello World
+description: Sharaf Tutorial Hello World
+---
+
+# {{ page.title }}
+
+
+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")
+```
+
+Then run it like this:
+```sh
+scala-cli hello.sc
+```
+Go to http://localhost:8181/hello/Bob.
+You will see a "Hello Bob" text response.
+
+---
+The most interesting part is the `Routes` definition.
+Here we pattern match on `(HttpMethod, Path)`.
+The `Path` contains a `Seq[String]`, which are the parts of the URL you can match on.
+
diff --git a/docs/content/tutorials/html.md b/docs/content/tutorials/html.md
new file mode 100644
index 0000000..42ac955
--- /dev/null
+++ b/docs/content/tutorials/html.md
@@ -0,0 +1,101 @@
+---
+title: HTML
+description: Sharaf Tutorial HTML
+---
+
+# {{ page.title }}
+
+You can return a scalatags `doctype` 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")
+
+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
+
+import scalatags.Text.all.*
+import ba.sake.hepek.html.HtmlPage
+import ba.sake.sharaf.*
+import ba.sake.sharaf.undertow.{*, given}
+
+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")
+
+
+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), "!")
+
+```
+
+and run it like this:
+```sh
+scala-cli html.sc
+```
+
+Go to [http://localhost:8181](http://localhost:8181)
+to see how it works.
+
diff --git a/docs/content/tutorials/htmx.md b/docs/content/tutorials/htmx.md
new file mode 100644
index 0000000..4a48ee5
--- /dev/null
+++ b/docs/content/tutorials/htmx.md
@@ -0,0 +1,65 @@
+---
+title: HTMX
+description: Sharaf Tutorial HTMX
+---
+
+# {{ page.title }}
+
+[HTMX]("https://htmx.org/") is an incredibly simple, HTML-first library.
+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:
+```scala
+//> using scala "3.7.0"
+//> using dep ba.sake::sharaf-undertow:0.10.0
+
+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(IndexView)
+ case POST -> Path("html-snippet") =>
+ Response.withBody(
+ div(
+ b("WOW, it works! 😲"),
+ div("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!")
+ )
+ )
+)
+```
+
+and run it like this:
+```sh
+scala-cli html.sc
+```
+
+Go to [http://localhost:8181](http://localhost:8181)
+to see how it works.
+
+
diff --git a/docs/content/tutorials/index.md b/docs/content/tutorials/index.md
new file mode 100644
index 0000000..de29e70
--- /dev/null
+++ b/docs/content/tutorials/index.md
@@ -0,0 +1,37 @@
+---
+title: Tutorials
+description: Sharaf Tutorials
+pagination:
+ enabled: false
+---
+
+# {{ page.title }}
+
+{%
+
+set tutorials = [
+ { label: "Quickstart", url: "/tutorials/quickstart.html" },
+ { label: "Hello World", url: "/tutorials/hello-world.html" },
+ { label: "Path Params", url: "/tutorials/path-params.html" },
+ { label: "Query Params", url: "/tutorials/query-params.html" },
+ { label: "Static Files", url: "/tutorials/static-files.html" },
+ { label: "HTML", url: "/tutorials/html.html" },
+ { label: "Forms", url: "/tutorials/forms.html" },
+ { label: "JSON", url: "/tutorials/json.html" },
+ { label: "Validation", url: "/tutorials/validation.html" },
+ { label: "SQL", url: "/tutorials/sql.html" },
+ { label: "Tests", url: "/tutorials/tests.html" },
+ { label: "HTMX", url: "/tutorials/htmx.html" }
+]
+
+%}
+
+{% for tut in tutorials %}
+- [{{ tut.label }}]({{ tut.url}})
+{% endfor %}
+
+
+
+
+
+
diff --git a/docs/content/tutorials/json.md b/docs/content/tutorials/json.md
new file mode 100644
index 0000000..6bd422c
--- /dev/null
+++ b/docs/content/tutorials/json.md
@@ -0,0 +1,95 @@
+---
+title: JSON
+description: Sharaf Tutorial JSON
+---
+
+# {{ page.title }}
+
+
+## Model definition
+
+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
+
+import ba.sake.tupson.JsonRW
+import ba.sake.sharaf.*
+import ba.sake.sharaf.undertow.UndertowSharafServer
+
+case class Car(brand: String, model: String, quantity: Int) derives JsonRW
+
+object CarsDb {
+ var db: Seq[Car] = Seq()
+ def findAll(): Seq[Car] = db
+ def findByBrand(brand: String): Seq[Car] = db.filter(_.brand == brand)
+ def add(car: Car): Unit = db = db.appended(car)
+}
+```
+
+Here we defined a `Car` model, which `derives JsonRW`, so we can use the JSON support from Sharaf.
+
+We also use a `var db: Seq[Car]` to store our data.
+(don't do this for real projects)
+
+
+## Routes definition
+Next step is to define a few routes for getting and adding cars:
+```scala
+val routes = Routes:
+ case GET -> Path("cars") =>
+ Response.withBody(CarsDb.findAll())
+
+ case GET -> Path("cars", brand) =>
+ val res = CarsDb.findByBrand(brand)
+ Response.withBody(res)
+
+ case POST -> Path("cars") =>
+ val reqBody = Request.current.bodyJson[Car]
+ CarsDb.add(reqBody)
+ Response.withBody(reqBody)
+```
+
+The first route returns all data in the database.
+
+The second route does some filtering on the database.
+
+The third route binds the JSON body from the HTTP request.
+Then we add it to the database.
+
+
+## Running the server
+
+Finally, start up the server:
+```scala
+UndertowSharafServer("localhost", 8181, routes)
+ .withExceptionMapper(ExceptionMapper.json)
+ .start()
+
+println("Server started at http://localhost:8181")
+```
+
+and run it like this:
+```sh
+scala-cli json_api.sc
+```
+
+Then try the following requests:
+```sh
+# get all cars
+curl http://localhost:8181/cars
+
+# add a car
+curl --request POST \
+ --url http://localhost:8181/cars \
+ --data '{
+ "brand": "Mercedes",
+ "model": "ML350",
+ "quantity": 1
+ }'
+
+# get cars by brand
+curl http://localhost:8181/cars/Mercedes
+```
+
diff --git a/docs/content/tutorials/path-params.md b/docs/content/tutorials/path-params.md
new file mode 100644
index 0000000..d0d7efe
--- /dev/null
+++ b/docs/content/tutorials/path-params.md
@@ -0,0 +1,42 @@
+---
+title: Path Params
+description: Sharaf Tutorial Path Params
+---
+
+# {{ page.title }}
+
+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")
+```
+
+Then run it like this:
+```sh
+scala-cli path_params.sc
+```
+
+---
+Now go to [http://localhost:8181/string/abc](http://localhost:8181/string/abc)
+and you will get the param returned: `string = abc`.
+
+When you go to [http://localhost:8181/int/123](http://localhost:8181/int/123),
+Sharaf will *try to extract* an `Int` from the path parameter.
+If it doesn't match, it will fall through, try the next route.
+
diff --git a/docs/content/tutorials/query-params.md b/docs/content/tutorials/query-params.md
new file mode 100644
index 0000000..43bcf28
--- /dev/null
+++ b/docs/content/tutorials/query-params.md
@@ -0,0 +1,60 @@
+---
+title: Query Params
+description: Sharaf Tutorial Query Params
+---
+
+# {{ page.title }}
+
+### Raw
+Raw query parameters can be accessed through `Request.current.queryParamsRaw`.
+This is a `Map[String, Seq[String]]` which you can use to extract query parameters.
+The raw approach is useful for simple cases and dynamic query parameters.
+
+### Typed
+For more type safety you can use the `QueryStringRW` typeclass.
+Make a `case class MyParams(..) derives QueryStringRW`
+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")
+```
+
+Then run it like this:
+```sh
+scala-cli query_params.sc
+```
+
+---
+Now go to http://localhost:8181/raw?q=what&perPage=10
+and you will get the raw query params map:
+```
+params = Map(perPage -> List(10), q -> List(what))
+```
+
+and if you go to http://localhost:8181/typed?q=what&perPage=10
+you will get a type-safe, parsed query params object:
+```
+params = SearchParams(what,10)
+```
\ No newline at end of file
diff --git a/docs/content/tutorials/quickstart.md b/docs/content/tutorials/quickstart.md
new file mode 100644
index 0000000..009d4df
--- /dev/null
+++ b/docs/content/tutorials/quickstart.md
@@ -0,0 +1,50 @@
+---
+title: Quickstart
+description: Sharaf Tutorial Quickstart
+---
+
+# {{ page.title }}
+
+Get started quickly with Sharaf framework.
+
+## Mill
+
+```scala
+def ivyDeps = super.ivyDeps() ++ Agg(
+ ivy"{{site.data.project.artifact.org}}::{{site.data.project.artifact.name}}:{{site.data.project.artifact.version}}"
+)
+def scalacOptions = super.scalacOptions() ++ Seq("-Yretain-trees")
+```
+
+## Sbt
+
+```scala
+libraryDependencies ++= Seq(
+ "{{site.data.project.artifact.org}}" %% "{{site.data.project.artifact.name}}" % "{{site.data.project.artifact.version}}"
+),
+scalacOptions ++= Seq("-Yretain-trees")
+```
+
+
+## Scala CLI
+
+Create a file `my_script.sc` with the following content:
+```scala
+//> using dep {{site.data.project.artifact.org}}::{{site.data.project.artifact.name}}:{{site.data.project.artifact.version}}
+```
+and then run it with:
+```bash
+scala-cli 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
+- [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
+- [OAuth2 login]({{site.data.project.gh.sourcesUrl}}/examples/oauth2) with [Pac4J library](https://www.pac4j.org/)
+- [PetClinic](https://github.com/sake92/sharaf-petclinic) implementation, featuring full-stack app with Postgres db, config, integration tests etc.
+- [Giter8 template for fullstack app](https://github.com/sake92/sharaf-fullstack.g8)
diff --git a/docs/content/tutorials/sql.md b/docs/content/tutorials/sql.md
new file mode 100644
index 0000000..94ea114
--- /dev/null
+++ b/docs/content/tutorials/sql.md
@@ -0,0 +1,105 @@
+---
+title: SQL
+description: Sharaf Tutorial SQL
+---
+
+# {{ page.title }}
+
+
+## DB setup
+
+Create a new Postgres database with Docker:
+```sh
+docker run --name sharaf-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres
+```
+
+Then connect to it via `psql` (or your favorite SQL tool):
+```sh
+docker exec -it sharaf-postgres psql -U postgres postgres
+```
+and create a table:
+```sql
+CREATE TABLE customers(
+ id SERIAL PRIMARY KEY,
+ name VARCHAR
+);
+```
+
+## Squery setup
+Sharaf recommends the [Squery](https://sake92.github.io/squery/) library for accessing databases with a JDBC driver.
+
+Create a file `sql_db.sc` and paste this code into it:
+```scala
+//> 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::squery:0.7.0
+
+import ba.sake.tupson.JsonRW
+import ba.sake.squery.{*, given}
+import ba.sake.sharaf.*
+import ba.sake.sharaf.undertow.UndertowSharafServer
+
+val ds = com.zaxxer.hikari.HikariDataSource()
+ds.setJdbcUrl("jdbc:postgresql://localhost:5432/postgres")
+ds.setUsername("postgres")
+ds.setPassword("mysecretpassword")
+
+val ctx = new SqueryContext(ds)
+```
+
+Here we set up the `SqueryContext` which we can use for accessing the database.
+
+
+## Querying
+Now we can do some querying on the db:
+```scala
+case class Customer(name: String) derives JsonRW
+
+val routes = Routes:
+ case GET -> Path("customers") =>
+ val customerNames = ctx.run {
+ sql"SELECT name FROM customers".readValues[String]()
+ }
+ Response.withBody(customerNames)
+
+ case POST -> Path("customers") =>
+ val customer = Request.current.bodyJson[Customer]
+ ctx.run {
+ sql"""
+ INSERT INTO customers(name)
+ VALUES (${customer.name})
+ """.insert()
+ }
+ Response.withBody(customer)
+
+```
+
+
+## Running the server
+
+Finally, we need to start up the server:
+```scala
+UndertowSharafServer("localhost", 8181, routes).start()
+
+println(s"Server started at http://localhost:8181")
+```
+
+and run it like this:
+```sh
+scala-cli sql_db.sc
+```
+
+Then you can try the following requests:
+```sh
+# get all customers
+curl http://localhost:8181/customers
+
+# add a customer
+curl --request POST \
+ --url http://localhost:8181/customers \
+ --data '{
+ "name": "Bob"
+ }'
+```
diff --git a/docs/content/tutorials/static-files.md b/docs/content/tutorials/static-files.md
new file mode 100644
index 0000000..eeb1c7a
--- /dev/null
+++ b/docs/content/tutorials/static-files.md
@@ -0,0 +1,43 @@
+---
+title: Static Files
+description: Sharaf Tutorial Static Files
+---
+
+# {{ page.title }}
+
+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`
+
+---
+
+Let's serve an `example.js` file with Sharaf.
+First create a file `resources/public/example.js`.
+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")
+```
+
+and run it like this:
+```sh
+scala-cli static_files.sc --resource-dir resources
+```
+
+Go to http://localhost:8181/example.js.
+You will see the `example.js` contents served.
+
diff --git a/docs/content/tutorials/tests.md b/docs/content/tutorials/tests.md
new file mode 100644
index 0000000..5c64956
--- /dev/null
+++ b/docs/content/tutorials/tests.md
@@ -0,0 +1,62 @@
+---
+title: Tests
+description: Sharaf Tutorial Tests
+---
+
+# {{ page.title }}
+
+Tests are essential to any serious software component.
+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)))
+ }
+ }
+}
+```
+
+First run the API server in one shell:
+```sh
+scala-cli test json_api.sc
+```
+
+and then run the tests in another shell:
+```sh
+scala-cli test json_api.test.scala
+```
+
diff --git a/docs/content/tutorials/validation.md b/docs/content/tutorials/validation.md
new file mode 100644
index 0000000..f0ac4a9
--- /dev/null
+++ b/docs/content/tutorials/validation.md
@@ -0,0 +1,137 @@
+---
+title: Validation
+description: Sharaf Tutorial Validation
+---
+
+# {{ page.title }}
+
+
+
+For validating data you need to use the `Validator` typeclass.
+A small example:
+
+```scala
+import ba.sake.validson.Validator
+
+case class ValidatedData(num: Int, str: String, seq: Seq[String])
+
+object ValidatedData:
+ given Validator[ValidatedData] = Validator
+ .derived[ValidatedData]
+ .positive(_.num)
+ .notBlank(_.str)
+ .minItems(_.seq, 1)
+```
+
+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
+```
+
+Then run it like this:
+```sh
+scala-cli validation.sc
+```
+
+Notice above that we used `queryParamsValidated` and not plain `queryParams` (does not validate query params).
+Also, for JSON body parsing+validation we use `bodyJsonValidated` and not plain `bodyJson` (does not validate JSON body).
+
+---
+When you do a GET [http://localhost:8181/cars?brand= ](http://localhost:8181/cars?brand= )
+you will get a nice JSON error message with HTTP Status of `400 Bad Request`:
+```json
+{
+ "instance": null,
+ "invalidArguments": [
+ {
+ "reason": "must not be blank",
+ "path": "$.brand",
+ "value": ""
+ }
+ ],
+ "detail": "",
+ "type": null,
+ "title": "Validation errors",
+ "status": 400
+}
+```
+
+The error message format follows the [RFC 7807 problem detail](https://datatracker.ietf.org/doc/html/rfc7807).
+
+---
+
+When you do a POST [http://localhost:8181/cars](http://localhost:8181/cars) with a malformed body:
+```json
+{
+ "brand": " ",
+ "model": "ML350",
+ "quantity": -5
+}
+```
+
+you will get these errors:
+```json
+{
+ "instance": null,
+ "invalidArguments": [
+ {
+ "reason": "must not be blank",
+ "path": "$.brand",
+ "value": " "
+ },
+ {
+ "reason": "must not be negative",
+ "path": "$.quantity",
+ "value": "-5"
+ }
+ ],
+ "detail": "",
+ "type": null,
+ "title": "Validation errors",
+ "status": 400
+}
+```
diff --git a/docs/src/files/Index.scala b/docs/src/files/Index.scala
deleted file mode 100644
index 5d3f3f0..0000000
--- a/docs/src/files/Index.scala
+++ /dev/null
@@ -1,46 +0,0 @@
-package files
-
-import scalatags.Text.all.*
-import ba.sake.hepek.html.statik.BlogPostPage
-import utils.*
-
-object Index extends DocStaticPage {
-
- override def pageSettings = super.pageSettings
- .withTitle(Consts.ProjectName)
-
- override def mainContent =
- div(
- h1(Consts.ProjectName),
- s"""
- ${Consts.ProjectName} is a minimalistic Scala 3 web framework.
-
- Jump right into:
- - [Tutorials](${files.tutorials.Index.ref}) to get you started
- - [How-Tos](${files.howtos.Index.ref}) to get answers for some common questions
- - [Reference](${files.reference.Index.ref}) to see detailed information
- - [Philosophy](${files.philosophy.Index.ref}) to get insights into design decisions
-
- ---
- Site map:
- """.md,
- div(cls := "site-map")(
- siteMap.md
- )
- )
-
- private def siteMap =
- Index.staticSiteSettings.mainPages
- .map {
- case mp: BlogPostPage =>
- val subPages = mp.categoryPosts
- .drop(1) // skip Index ..
- .map { cp =>
- s" - [${cp.pageSettings.label}](${cp.ref})"
- }
- .mkString("\n")
- s"- [${mp.pageSettings.label}](${mp.ref})\n" + subPages
- case _ => ""
- }
- .mkString("\n")
-}
diff --git a/docs/src/files/SearchIndex.scala b/docs/src/files/SearchIndex.scala
deleted file mode 100644
index 8d94120..0000000
--- a/docs/src/files/SearchIndex.scala
+++ /dev/null
@@ -1,12 +0,0 @@
-package files
-
-import ba.sake.hepek.fusejs.FusejsIndex
-import ba.sake.hepek.html.statik.StaticPage
-import utils.Consts
-
-object SearchIndex extends FusejsIndex {
-
- override def indexedPages: Seq[StaticPage] =
- Consts.allSearchIndexedPages
-
-}
diff --git a/docs/src/files/SearchResults.scala b/docs/src/files/SearchResults.scala
deleted file mode 100644
index 9309c4a..0000000
--- a/docs/src/files/SearchResults.scala
+++ /dev/null
@@ -1,45 +0,0 @@
-package files
-
-import ba.sake.hepek.html.PageSettings
-import ba.sake.tupson.toJson
-import scalatags.Text.all.*
-
-object SearchResults extends utils.DocStaticPage {
-
- override def pageSettings: PageSettings = super.pageSettings
- .withTitle("Search results")
- .withLabel("Search results")
-
- override def mainContent = div(
- h1("Search results"),
- div(id := "search-results-content")(
- p("Loading...")
- )
- )
-
- private lazy val fuseList = SearchIndex.fusejsIndexedPagesData.toJson
-
- override def scriptsInline: List[String] = super.scriptsInline ++ List(s"""
- import Fuse from 'https://cdn.jsdelivr.net/npm/fuse.js@7.1.0/dist/fuse.mjs'
-
- const urlParams = new URLSearchParams(window.location.search);
- const qParam = urlParams.get('q');
-
- const fuseIndex = await fetch('${SearchIndex.ref}').then(r => r.json())
- const myIndex = Fuse.parseIndex(fuseIndex);
- const fuse = new Fuse(${fuseList}, {
- keys: [ "title", "text" ]
- }, myIndex);
-
- const searchRes = fuse.search(qParam);
- console.log(JSON.stringify(searchRes));
- const searchResultsContentElem = document.getElementById("search-results-content");
- searchResultsContentElem.innerHTML = searchRes.map(r => {
- const page = r.item;
- return `