Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OAuth2 example #2

Merged
merged 4 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ target/
*.class
*.log

.idea/

.bloop/
.metals/
.bsp/
Expand Down
8 changes: 7 additions & 1 deletion DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@ git push origin $VERSION

# TODOs

- cookies ?
- cookies ?
- adapt query params to requests lib
- add Docker / Watchtower example




16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@

# Sharaf

Simple, intuitive, batteries-included HTTP library.

## Why sharaf?

**Simplicity and ease of use** is the main focus of sharaf.
Simplicity and ease of use is the main focus of sharaf.

It is built on top of [undertow](https://undertow.io/).
This means you can use some awesome libraries built for undertow, like [pac4j](https://github.com/pac4j/undertow-pac4j) for security and similar.
This means you can use awesome libraries built for undertow, like [pac4j](https://github.com/pac4j/undertow-pac4j) for security and similar.
Also, you can use undertow's lower level API, to use WebSockets for example.

Sharaf bundles a set of libraries:
Expand All @@ -15,8 +17,14 @@ Sharaf bundles a set of libraries:
- [formson](./formson) for forms
- [validson](./formson) for validation
- [hepek-components](https://github.com/sake92/hepek) for HTML (with [scalatags](https://github.com/com-lihaoyi/scalatags))

There are a bunch of [examples](./examples) to get you started.
- [requests](https://github.com/com-lihaoyi/requests-scala) for firing HTTP requests

## Examples
- handling [json](examples/json)
- handling [form data](examples/form)
- rendering [html](examples/html) and serving static files
- implementation of [todobackend.com](examples/todo) featuring CORS handling
- [OAuth2 login](examples/oauth2) with [Pac4J library](https://www.pac4j.org/)

## Misc

Expand Down
25 changes: 22 additions & 3 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ object sharaf extends SharafPublishModule {
def ivyDeps = Agg(
ivy"io.undertow:undertow-core:2.3.7.Final",
ivy"ba.sake::tupson:0.7.0",
ivy"ba.sake::hepek-components:0.13.0"
ivy"ba.sake::hepek-components:0.13.0",
ivy"com.lihaoyi::requests:0.8.0"
)

def moduleDeps = Seq(querson, formson)
Expand All @@ -25,6 +26,8 @@ object querson extends SharafPublishModule {

def moduleDeps = Seq(validson)

def pomSettings = super.pomSettings().copy(description = "Simple query params library")

def ivyDeps = Agg(
ivy"com.lihaoyi::fastparse:3.0.1"
)
Expand All @@ -38,11 +41,12 @@ object formson extends SharafPublishModule {

def moduleDeps = Seq(validson)

def pomSettings = super.pomSettings().copy(description = "Simple form binding library")

object test extends ScalaTests with SharafTestModule

def ivyDeps = Agg(
ivy"com.lihaoyi::fastparse:3.0.1",
ivy"com.lihaoyi::requests:0.8.0" // TODO move to a separate module
ivy"com.lihaoyi::fastparse:3.0.1"
)
}

Expand All @@ -54,6 +58,8 @@ object validson extends SharafPublishModule {
ivy"com.lihaoyi::sourcecode::0.3.0"
)

def pomSettings = super.pomSettings().copy(description = "Simple validation library")

object test extends ScalaTests with SharafTestModule
}

Expand Down Expand Up @@ -104,4 +110,17 @@ object examples extends mill.Module {
def moduleDeps = Seq(sharaf)
object test extends ScalaTests with SharafTestModule
}
object oauth2 extends SharafCommonModule {
def moduleDeps = Seq(sharaf)
def ivyDeps = Agg(
ivy"ch.qos.logback:logback-classic:1.4.6",
ivy"org.pac4j:undertow-pac4j:5.0.1",
ivy"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"
)
}
}
}
4 changes: 2 additions & 2 deletions examples/form/src/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import ba.sake.validson.*

@main def main: Unit = {

val server = FormApiServer(8181).server
val server = FormApiModule(8181).server
server.start()

val serverInfo = server.getListenerInfo().get(0)
Expand All @@ -20,7 +20,7 @@ import ba.sake.validson.*

}

class FormApiServer(port: Int) {
class FormApiModule(port: Int) {
private val routes: Routes = { case POST() -> Path("form") =>
val req = Request.current.bodyForm[CreateCustomerForm].validateOrThrow
val fileAsString = Files.readString(req.file)
Expand Down
39 changes: 12 additions & 27 deletions examples/form/test/src/FormApiSuite.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
package demo

import scala.util.Random
import io.undertow.Undertow
import ba.sake.formson.*
import ba.sake.tupson.*
import ba.sake.sharaf.Resource
import ba.sake.sharaf.*

class FormApiSuite extends munit.FunSuite {

override def munitFixtures = List(serverFixture)
override def munitFixtures = List(moduleFixture)

test("customer can be created") {

val server = serverFixture()
val serverInfo = server.getListenerInfo().get(0)
val module = moduleFixture()
val serverInfo = module.server.getListenerInfo().get(0)
val baseUrl = s"${serverInfo.getProtcol}:/${serverInfo.getAddress}"

val exampleFile =
Expand All @@ -23,39 +21,26 @@ class FormApiSuite extends munit.FunSuite {
CreateCustomerForm("Meho", exampleFile, CreateAddressForm("street123ž"), List("hobby1", "hobby2"))
val res = requests.post(
s"$baseUrl/form",
data = formData2RequestsMultipart(reqBody.toFormDataMap())
data = reqBody.toFormDataMap().toRequestsMultipart()
)

assertEquals(res.statusCode, 200)
assertEquals(res.headers("content-type"), Seq("application/json")) // it returns JSON content..
val resBody = res.text.parseJson[CreateCustomerResponse]
// this tests utf-8 encoding too :)
assertEquals(resBody.street, "street123ž")
assertEquals(resBody.fileContents, "This is a text file :)")
}

// TODO extract into a separate requests-integration module
private def formData2RequestsMultipart(formDataMap: FormDataMap) = {
val multiItems = formDataMap.flatMap { case (key, values) =>
values.map {
case FormValue.Str(value) => requests.MultiItem(key, value)
case FormValue.File(value) => requests.MultiItem(key, value, value.getFileName.toString)
case FormValue.ByteArray(value) => requests.MultiItem(key, value)
}
}
requests.MultiPart(
multiItems.toSeq*
)
}
val moduleFixture = new Fixture[FormApiModule]("FormApiModule") {
private var module: FormApiModule = _

def apply() = module

val serverFixture = new Fixture[Undertow]("JsonApiServer") {
private var underlyingServer: Undertow = _
def apply() = underlyingServer
override def beforeEach(context: BeforeEach): Unit =
underlyingServer = FormApiServer(Random.between(1_024, 65_535)).server
underlyingServer.start()
module = FormApiModule(SharafUtils.getFreePort())
module.server.start()
override def afterEach(context: AfterEach): Unit =
underlyingServer.stop
module.server.stop()
}

}
Binary file removed examples/html/resources/static/scala.png
Binary file not shown.
13 changes: 7 additions & 6 deletions examples/html/src/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import scalatags.Text.all._
@main def main: Unit = {

val routes: Routes = {
case GET() -> Path("html") =>
Response.withBody(MyPage)
case GET() -> Path("scala.png") =>
val resource = Resource.fromClassPath("static/scala.png")
case GET() -> Path("images", imageName) =>
val resource = Resource.fromClassPath(s"static/images/$imageName")
Response.withBodyOpt(resource, "NotFound")

case GET() -> Path() =>
Response.withBody(MyPage)
}

val server = Undertow
Expand All @@ -33,7 +34,7 @@ import scalatags.Text.all._

val MyPage = new HtmlPage {
override def bodyContent: Frag = div(
"oppppppp",
img(src := "scala.png")
"Hello sharaf!",
img(src := "images/scala.png")
)
}
11 changes: 4 additions & 7 deletions examples/json/src/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@ package demo
import java.util.UUID
import io.undertow.Undertow

import ba.sake.tupson.*
import ba.sake.sharaf.*
import ba.sake.sharaf.routing.*
import ba.sake.sharaf.handlers.*
import ba.sake.querson.*
import ba.sake.tupson.*
import ba.sake.validson.*

@main def main: Unit = {

val server = JsonApiServer(8181).server
val server = JsonApiModule(8181).server
server.start()

val serverInfo = server.getListenerInfo().get(0)
val url = s"${serverInfo.getProtcol}:/${serverInfo.getAddress}"
println(s"Started JsonApiServer at $url")
println(s"Started HTTP server at $url")
}

class JsonApiServer(port: Int) {
class JsonApiModule(port: Int) {

private var db = Seq.empty[CustomerRes]

Expand All @@ -47,5 +46,3 @@ class JsonApiServer(port: Int) {
.setHandler(ErrorHandler(RoutesHandler(routes), ErrorMapper.json))
.build()
}

case class UserQuery(name: Set[String]) derives QueryStringRW
3 changes: 3 additions & 0 deletions examples/json/src/requests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package demo

import ba.sake.tupson.JsonRW
import ba.sake.validson.*
import ba.sake.querson.QueryStringRW

case class CreateCustomerReq private (name: String, address: CreateAddressReq) derives JsonRW

Expand All @@ -23,3 +24,5 @@ object CreateAddressReq:
.derived[CreateAddressReq]
.and(_.street, !_.isBlank, "must not be blank")
.and(_.street, _.length >= 3, "must be >= 3")

case class UserQuery(name: Set[String]) derives QueryStringRW
32 changes: 17 additions & 15 deletions examples/json/test/src/JsonApiSuite.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package demo

import ba.sake.querson.*
import ba.sake.tupson.*
import scala.util.Random
import io.undertow.Undertow
import ba.sake.sharaf.handlers.ProblemDetails
import ba.sake.sharaf.handlers.ArgumentProblem
import ba.sake.sharaf.SharafUtils
import ba.sake.querson.*
import ba.sake.tupson.*

class JsonApiSuite extends munit.FunSuite {

override def munitFixtures = List(serverFixture)
override def munitFixtures = List(moduleFixture)

test("customers can be created and fetched") {
val server = serverFixture()
val serverInfo = server.getListenerInfo().get(0)
val module = moduleFixture()
val serverInfo = module.server.getListenerInfo().get(0)
val baseUrl = s"${serverInfo.getProtcol}:/${serverInfo.getAddress}"

// first GET -> empty
Expand Down Expand Up @@ -80,8 +79,8 @@ class JsonApiSuite extends munit.FunSuite {
}

test("400 BadRequest when body not valid") {
val server = serverFixture()
val serverInfo = server.getListenerInfo().get(0)
val module = moduleFixture()
val serverInfo = module.server.getListenerInfo().get(0)
val baseUrl = s"${serverInfo.getProtcol}:/${serverInfo.getAddress}"

// blank name not allowed
Expand Down Expand Up @@ -118,14 +117,17 @@ class JsonApiSuite extends munit.FunSuite {

}

val serverFixture = new Fixture[Undertow]("JsonApiServer") {
private var underlyingServer: Undertow = _
def apply() = underlyingServer
val moduleFixture = new Fixture[JsonApiModule]("JsonApiModule") {
private var module: JsonApiModule = _

def apply() = module

override def beforeEach(context: BeforeEach): Unit =
underlyingServer = JsonApiServer(Random.between(1_024, 65_535)).server
underlyingServer.start()
module = JsonApiModule(SharafUtils.getFreePort())
module.server.start()

override def afterEach(context: AfterEach): Unit =
underlyingServer.stop
module.server.stop()
}

}
Loading