For a better sleep at night
Let me tell you a story
Another story
Because it's not the middle of the night
class BeerOrder {
  quantity: String
}
QA orders:
“1”
“9999999”
“A beer”
“0”
“-3”
“0.99999”
“A pint of beer”
“Pomidor”
“”
null
class BeerOrder {
  quantity: Double
}
QA orders:
1
9999999
“A beer”
0
-3
0.99999
“A pint of beer”
“Pomidor”
“”
null
class BeerOrder {
  quantity: Int
}
QA orders:
1
9999999
“A beer”
0
-3
0.99999
“A pint of beer”
“Pomidor”
“”
null
class BeerOrder {
  quantity: PosInt
}
QA orders:
1
9999999
“A beer”
0
-3
0.99999
“A pint of beer”
“Pomidor”
“”
null
val uuid1: String Refined Uuid = "6736a2fb-0848-4eb7-a9f3-0556f384c952"
val uuid2: String Refined Uuid = "123"
val posint1: PosInt = 123
val posint2: PosInt = -10
val xml1: String Refined Xml = "<div>hello world</div>"
val xml2: String Refined Xml = "<div>hello /div>"
val even: Int Refined Even = 10
val odd: Int Refined Odd = 11
case class UnsafeOrderLine(product: String, quantity: Int)
// Valid order line, we want those!
UnsafeOrderLine("123", 10)
// Wait that's illegal
UnsafeOrderLine("", 10)
UnsafeOrderLine("banana", -2)
// ☝️ this will cause runtime errors 😱How would we fix it without refined?
case class UnsafeOrderLine(product: String, quantity: Int)
object UnsafeOrderLine {
 def safeApply(product: String, quantity: Int): UnsafeOrderLine = {
   if(product.isEmpty())
     throw new RuntimeException("Product is empty")
   else if(quantity <= 0)
     throw new RuntimeException("Quantity lower than 1")
   else
     UnsafeOrderLine(product, quantity)
 }
}
// Works fine!
UnsafeOrderLine.safeApply("123", 10)
// Throws runtime exception 👇
UnsafeOrderLine.safeApply("", 10)
It works... kind of
case class UnsafeOrderLine(product: String, quantity: Int)
object UnsafeOrderLine {
 def safeApply(product: String, quantity: Int): UnsafeOrderLine = {
   if(product.isEmpty())
     throw new RuntimeException("Product is empty")
   else if(quantity <= 0)
     throw new RuntimeException("Quantity lower than 1")
   else
     UnsafeOrderLine(product, quantity)
 }
}import eu.timepit.refined.auto._
import eu.timepit.refined.types.string._
import eu.timepit.refined.types.numeric._
case class OrderLine(product: NonEmptyString, quantity: PosInt)
OrderLine("123", 10)  // Returns OrderLine
OrderLine("", 10)     // Doesn't compile
Glad that you asked
import cats.data.NonEmptyList
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.types.string._
import eu.timepit.refined.types.numeric._
case class OrderLine(product: NonEmptyString, quantity: PosInt)
case class Order(orderId: String Refined Uuid, lines: NonEmptyList[OrderLine])
Let's add JSON codecs
import cats.data.NonEmptyList
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.types.string._
import eu.timepit.refined.types.numeric._
import io.circe.Codec
import io.circe.generic.semiauto._
import io.circe.refined._
case class OrderLine(product: NonEmptyString, quantity: PosInt)
object OrderLine {
  implicit val codec: Codec[OrderLine] = deriveCodec
}
case class Order(orderId: String Refined Uuid, lines: NonEmptyList[OrderLine])
object Order {
  implicit val codec: Codec[Order] = deriveCodec
}import sttp.tapir._
import sttp.tapir.json.circe._
import sttp.tapir.generic.auto._
object OrderEndpoints {
  val validate: PublicEndpoint[Order, Unit, Unit, Any] =
    endpoint
      .post
      .in("validate") 	    // defines a path http://localhost:8080/validate
      .in(jsonBody[Order])  // takes Order as an input
      .out(jsonBody[Unit])  // the response content is always `{}`
}
Validation
endpoint
Order in JSON format
200 for success
400 for failure
import cats.effect._
import cats.implicits._
object OrderRoutes {
  val logic: Order => Unit = _ => () // define the logic
  // bind the endpoint to the logic
  val validate =
    OrderEndpoints.validate.serverLogic(logic(_).asRight[Unit].pure[IO])
}Tapir does the validation based on the types, so the endpoint does nothing
import cats.effect._
import org.http4s.HttpRoutes
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.server.Router
import sttp.tapir.server.http4s.Http4sServerInterpreter
import scala.concurrent.ExecutionContext
object Server {
  private val routes: HttpRoutes[IO] =
    Http4sServerInterpreter[IO]().toRoutes(
      OrderRoutes.validate // the thing from the previous slide ;)
    )
  val resource = BlazeServerBuilder[IO]
    .withExecutionContext(ExecutionContext.global)
    .bindHttp(8080, "localhost")
    .withHttpApp(Router("/" -> routes).orNotFound)
    .resource
}
object Main extends IOApp {
  override def run(args: List[String]): IO[ExitCode] = {
    Server.resource
      .useForever
      .as(ExitCode.Success)
  }
}
Let's wire the things together
$ echo -n '{
  "orderId" : "17da8323-6e08-4519-aab6-ee0a9f9a30b3",
  "lines" : [
    {
      "product" : "product1",
      "quantity" : 10
    }
  ]
}
' | http POST localhost:8080/validate
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: application/json
Date: Tue, 28 Jun 2022 10:44:10 GMT
{}Let's see how it work
$ echo -n '{}' | http POST localhost:8080/validate 
HTTP/1.1 400 Bad Request
Content-Length: 96
Content-Type: text/plain; charset=UTF-8
Date: Tue, 28 Jun 2022 10:45:47 GMT
Invalid value for: body (Missing required field at 'orderId', Missing required field at 'lines')$ echo -n '{
  "orderId" : "17da8323-6e08-4519-aab6-ee0a9f9a30b3",
  "lines" : [
    {
      "product" : "product1",
      "quantity" : -1
    }
  ]
}
' | http POST localhost:8080/validate
HTTP/1.1 400 Bad Request
Content-Length: 76
Content-Type: text/plain; charset=UTF-8
Date: Tue, 28 Jun 2022 12:38:26 GMT
Invalid value for: body (Predicate failed: (-1 > 0). at 'lines[0].quantity')Let's see how it work - negative quantity
$ echo -n '{
  "orderId" : "17da8323-6e08-4519-aab6-ee0a9f9a30b3",
  "lines" : []
}
' | http POST localhost:8080/validate
HTTP/1.1 400 Bad Request
Content-Length: 100
Content-Type: text/plain; charset=UTF-8
Date: Tue, 28 Jun 2022 12:39:59 GMT
Invalid value for: body (Missing required field at 'lines[0]', Missing required field at 'lines[0]')Let's see how it work - no lines
Pretty impressive for no logic, isn't it?
object Server {
  import sttp.tapir._
  import sttp.tapir.swagger.bundle.SwaggerInterpreter
  
  val myEndpoints: List[AnyEndpoint] = List(OrderEndpoints.validate)
  val swaggerEndpoints = SwaggerInterpreter().fromEndpoints[IO](myEndpoints, "My App", "1.0")
  private val routes: HttpRoutes[IO] =
    Http4sServerInterpreter[IO]().toRoutes(
      List(
        OrderRoutes.validate,
      ) ++ swaggerEndpoints
    )
  val resource = BlazeServerBuilder[IO]
    .withExecutionContext(ExecutionContext.global)
    .bindHttp(8080, "localhost")
    .withHttpApp(Router("/" -> routes).orNotFound)
    .resource
}Open API docs for free
Our OSS
Thank you!
My blog and OSS: