37
loading...
This website collects cookies to deliver better user experience
/register
asking for a JSON body{
"_comment": "Example request body",
"username":"Iltotore",
"email":"[email protected]",
"password":"Abc123"
}
import mill._, scalalib._
object main extends ScalaModule {
def scalaVersion = "3.0.0"
}
import mill._, scalalib._
object main extends ScalaModule {
def scalaVersion = "3.0.0"
def http4sVersion = "0.23.0-RC1"
//Http4s dependencies
ivy"org.http4s::http4s-core:$http4sVersion",
ivy"org.http4s::http4s-dsl:$http4sVersion",
ivy"org.http4s::http4s-blaze-server:$http4sVersion",
ivy"org.http4s::http4s-circe:$http4sVersion",
//Circe dependencies
ivy"io.circe::circe-core:0.14.1",
ivy"io.circe::circe-generic:0.14.1",
//Iron with String, Cats and Circe modules
ivy"io.github.iltotore::iron:1.1",
ivy"io.github.iltotore::iron-string:1.1-0.1.0",
ivy"io.github.iltotore::iron-cats:1.1-0.1.0",
ivy"io.github.iltotore::iron-circe:1.1-0.1.0"
}
//Basic http4s imports
import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._
object HttpServer {
val service = HttpRoutes.of[IO] {
case _ => Ok("Hello World")
}
}
HttpRoutes.of
block, we can pattern match on the input request (http method, route...). We will come back to our service later.import cats.effect._
import org.http4s.implicits._
import org.http4s.blaze.server.BlazeServerBuilder
import scala.concurrent.ExecutionContext
//An IOApp will handle shutdown gracefully for us when receiving the SIGTERM signal
object Main extends IOApp {
override def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO](ExecutionContext.global)
.bindHttp(8080, "localhost")
.withHttpApp(HttpServer.service.orNotFound)
.serve
.compile //Allow conversion to IO
.drain //Remove output
.as(ExitCode.Success) //Set the output as Success
}
Main
. But our service in HttpServer
is currently not that useful: it only returns Ok - "Hello World".case class Account(username: String, email: String, password: String)
import cats.data._, cats.implicits._, cats.syntax.apply._
case class Account(username: String, email: String, password: String)
object Account {
def validateUsername(username: String): ValidatedNec[String, String] =
Validated.condNec(username.matches("^[a-zA-Z0-9]+"), username, s"$username should be alphanumeric")
def validateEmail(email: String): ValidatedNec[String, String] =
Validated.condNec(email.matches("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"), email, s"$email should be a valid email")
def validatePassword(password: String): ValidatedNec[String, String] =
Validated.condNec(password.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]+$"), password, s"$password should contain at least an upper, a lower and a number")
def createAccount(username: String, email: String, password: String): ValidatedNec[String, Account] = (
validateUsername(username),
validateEmail(email),
validatePassword(password)
).parMapN(Account.apply)
}
Account.createAccount
://Valid(Account(...))
Account.createAccount("Iltotore", "[email protected]", "SafePassword1")
//Invalid(NonEmptyChain("Value should be alphanumeric")))
Account.createAccount("Il_totore", "[email protected]", "SafePassword1")
/*
* Invalid(NonEmptyChain(
* "Value should be alphanumeric"),
* "Value must contain at least an upper, a lower and a number")
* ))
*/
Account.createAccount("Il_totore", "[email protected]", "this_is_not_fine")
import cats.implicits._, cats.syntax.apply._
import io.circe.{Decoder, Encoder}
import io.circe.generic.semiauto._
case class Account(username: String, email: String, password: String)
object Account {
//... (see code above)
//Get the fields `username`, `email` and `password` and pass them into Account.createAccount
inline given Decoder[ValidatedNec[String, Account]] =
Decoder.forProduct3("username", "email", "password")(createAccount)
//Automatically creates an Encoder from the case class Account
inline given Encoder[Account] = deriveEncoder
}
//Circe imports
import io.circe.syntax._, io.circe.disjunctionCodecs._, io.circe.Encoder._, io.circe.Encoder
//Http4s imports
import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._
object HttpServer {
//Create an Http4s decoder from our Decoder[ValidatedNec[String, Account]]
given EntityDecoder[IO, ValidatedNec[String, Account]] = accumulatingJsonOf[IO, ValidatedNec[String, Account]]
val service = HttpRoutes.of[IO] {
case request =>
request.as[ValidatedNec[String, Account]] //Convert our request into a ValidatedNec[String, Account]
.handleErrorWith(IO.raiseError) //Raise eventual exceptions
.map(_.asJson) //Convert the result into JSON
.flatMap(Ok(_)) //Create a "OK" request from our JSON
}
}
{
"username":"Iltotore",
"email":"[email protected]",
"password":"Abc123"
}
{
"Valid": {
"username": "Iltotore",
"email": "[email protected]",
"password": "Abc123"
}
}
{
"username": "Iltotore",
"email": "memyemail.com",
"password": "abc123"
}
{
"Invalid": [
"memyemail.com should be an email",
"abc123 should contain at least an upper, a lower and a number"
]
}
HttpRoutes.io
block in our service. Let's only accept POST requests to /register
using Http4s' DSL:val service = HttpRoutes.of[IO] {
case request@POST -> Root / "register" =>
request.as[RefinedFieldNec[Account]]
.handleErrorWith(IO.raiseError)
.map(_.asJson)
.flatMap(Ok(_))
case unknown =>
NotFound()
}
Either[IllegalValueError[A], A]
where A
is the original type.createAccount
method looks like with Iron.type Username = String ==> Alphanumeric
type Email = String ==> (Match["^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"] DescribedAs "Value should be an email")
//At least one upper, one lower and one number
type Password = String ==> (Match["^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]+$"] DescribedAs
"Value should contain at least an upper, a lower and a number")
createAccount
method:def createAccount(username: Username, email: Email, password: Password): RefinedFieldNec[Account] = (
username.toField("username").toValidatedNec,
email.toField("email").toValidatedNec,
password.toField("password").toValidatedNec
).mapN(Account.apply)
RefinedFieldNec[A]
is an alias for ValidatedNec[IllegalValueError.Field, A]
toField(String)
converts the potential value-based IllegalValueError[A] contained by our constrained type to a field-based IllegalValueError.Field.toValidatedNec
(provided by cats) converts our constrained type to an accumulative Validated. See Cats page on Validated for further information.Either
or Validated
.