# Une API affinée 18 mois
Note:
* Une API = Façade de logiciel offrant de services
* Versionning et documentation
* API Scala + API REST
* Qu'est ce que Refined
* Comment utiliser les prédicats Refined et plus des types Scala
* Meilleurs messages d'erreurs,
* générer de la documentation
### Mauvaise API bancaire ?
```scala
def deposit(amount : BigDecimal) = amount match {
case value if value> 0 => Right(AmountDeposited(value))
case _ => Left("invalid amount for deposit")
}
//tests
deposit(BigDecimal(-100)) mustBe Symbol("left")
deposit(BigDecimal(100)) mustBe Right(AmountDeposited(100))
```
Notes:
Sur celle ci je devrais en plus ajouter des tests unitaires
Il faut regarder l'implémentation pour savoir quelles sont les valeurs acceptées
C'est aussi vrai pour les méthodes acceptant un string en entrée alors que l'on souhaite passer,
une url, une IP
### Une solution ?
```scala
import shapeless.tag._
import shapeless.tag
trait Positive
type PositiveInt = Int @@ Positive
def validate(value : PositiveInt) = value match {
case v if v <0 => Left(s"Predicate failed $v < 0")
case v => Right(v)
}
validate(tag[PositiveInt](10)) //Right(10)
validate(tag[PositiveInt](-10))//Left(Predicate failed -10<0)
```
Notes:
Les types en entrée et sortie sont explicites
Il faudra l'implémenter pour tous les types
Ajoute du bruit
Et la validation se fait toujours au runtime
### refined: simple refinement types
~Type~ Refined _Predicate_
Des erreurs remontées à la compilation
```scala
scala> val i: Int Refined Positive = -5
error: Predicate failed: (-5 > 0).
val i: Int Refined Positive = -5
^
```
ou au runtime
```scala
val j = refineMV[Positive](-5)
//j: Either[String, Int Refined Positive] =
// Left(Predicate failed: (-5 > 0).)
```
https://github.com/fthomas/refined
Notes:
Un type de base sur lequel on applique un prédicat
### Prédicats existants :
* _numeric_
* string
* boolean
* char
* collection
* generic
### Prédicats numériques :
* Less[~N~], LessEqual[~N~], Greater[~N~], GreaterEqual[~N~]
* Positive, NonPositive, Negative
* Divisible[~N~]
* Interval.Open[~L~, ~H~]
* Even ...
Notes:
N étant un entier naturel (shapeless)
On peut simplement créer ses propres prédicats
### Inférence sur du code existant
```scala
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.numeric._
def plusOne(a: Int Refined Positive) = a + 1
plusOne(42) // 43
```
Notes:
Refined fournit également des fonctions de conversion entre les types
ex N > 10, alors N est également Positif
### Bonne API ?
```scala
def deposit(amount : BigDecimal Refined Positive) = {
AmountDeposited(value)
}
deposit(BigDecimal(100)) // compile
deposit(BigDecimal(-100)) //Predicate failed: (-100 > 0). 🙌
```
Notes:
Signature explicite
### Une bonne API REST
des URIS avec des verbes cohérents :
_POST /users return 201_
~~GET createUser return 200~~
### Qu'est ce qu'un schéma json
```json
{
"type": "string",
"minLength": 2,
"maxLength": 3
}
```
```json
"street_type": {
"type": "string",
"enum": ["Street", "Avenue", "Boulevard"]
}
```
https://json-schema.org/understanding-json-schema/
Notes:
Json permet de réprésenter toute sorte de données structurées
Vous avez certainement du voir des API json avec des int, booléens sous la forme de "string"
Il existe même une convention pour représenter les énumérations
Ressemblance avec les prédicats de Refined ?
### Aller plus loin ?
```scala
def deposit(amount : BigDecimal Refined Positive) = {
AmountDeposited(value)
}
```
1. Enrichir les messages d'erreurs
2. Fournir une documentation
Notes:
1 et 2 se basent sur un schéma json
### Une API bien documentée
![open API](img/openapi-example.png)
### Des messages d'erreurs explicites
```bash
curl -X POST http://localhost:9000/login
-d '{"username" : "john.doe@yopmail.com",
"password" : ""}'
```
```json
{
"obj.email":[
{"msg":["error.path.missing"]}
],
"obj.password":[
{"msg":["Predicate isEmpty() did not fail."]}
]
}
```
### Des messages d'erreurs encore plus explicites
```bash
curl -X POST http://localhost:9000/login
-d '{"username" : "john.doe@yopmail.com",
"password" : ""}'
```
```json
{
"_schema": {
"email": {
"minLength": 1,
"type": "string"
},
"password": {
"minLength": 1,
"type": "string"
}
},
...
}
```
### play-api-refiner
```scala
resolvers += Resolver.bintrayRepo("dgouyette", "maven")
libraryDependencies +=
"org.dgouyette"%% "play-api-refiner"% "1.0.0-M1"
```
* Schéma json
* Enrichi les messages d'erreurs Play
* Documentation Swagger
* License Apache 2
https://github.com/dgouyette/play-api-refiner
Notes:
Basée sur les types & prédicats :
Version scala 2.13 `-Ymacro-annotations`
Il existe aussi play-json-refined mais ne fournit que des formatteurs json play
#### Type primitif 1/2
```scala
case class SimpleString(value : String)
JsonSchema.jsonSchema[SimpleString]
```
```json
{
"value" : {
"type" : "string"
}
}
```
#### Prédicats Refined 1/2
```scala
case class StringNonEmpty(value : String Refined NonEmpty)
JsonSchema.jsonSchema[StringNonEmpty]
```
```json
{
"value":{
"minLength":1,
"type":"string"
}
}
```
#### Prédicats Refined 2/2
```scala
case class CollectionIntNonEmpty(
value : List[Int] Refined NonEmpty)
JsonSchema.jsonSchema[CollectionIntNonEmpty]
```
```json
{
"value":{
"minLength":1,
"type":"array",
"items":{
"type":"integer"
}
}
}
```
#### sealed trait
```scala
sealed trait TrafficLight
case object Red extends TrafficLight
case object Orange extends TrafficLight
case object Green extends TrafficLight
case class SimpleEnum(e : TrafficLight)
JsonSchema.jsonSchema[SimpleEnum]
```
```json
{
"e":{
"enum":["red","orange","green"],
"type":"string"
}
}
```
#### Json schéma : Comment ça marche ? 1/2
```scala
case class Login(email : String Refined NonEmpty)
JsonSchema.asJsValue[SimpleString]
//{"email":{"minLength":1,"type":"string"}}
```
```scala
object JsonSchema {
def asJsValue[T]: JsValue = macro impl[T]
def impl[T: c.WeakTypeTag] = {
typeOf[T] // type de la classe = Login
.decls.collect {
//champs = List(email)
case m: MethodSymbol if m.isCaseAccessor =>
m.name // email
//eu.timepit.refined.api.Refined[T,P]
m.info.typeSymbol
//typeArgs=List(String,refined.collection.NonEmpty)
m.info.typeArgs
}
}
}
```
Notes:
Les macros permettent d'effectuer des traitements à la compilation
WeakTypeTag + typeOf permet d'extraire et d'utiliser le type passé à la macro
#### Json schéma : Comment ça marche ? 2/2
```scala
def extractArgs(typeArgs: Seq[Type])=typeArgs match {
case _refinedType :: _type :: _predicate :: Nil =>
extractArgs(List(_refinedType)) ++
extractArgs(List(_type)) ++
extractArgs(List(_predicate))
case _type :: Nil if _type =:= typeOf[String] =>
Json.obj("type" -> "string")
case _predicate :: Nil if _predicate =:= typeOf[NonEmpty] =>
Json.obj("minLength" -> 1)
}
```
#### Open API : Comment ça marche ?
```scala
case class Login(email : String Refined NonEmpty,
password : String Refined NonEmpty)
val loginFmt = Json.format[Login]
val loginSchema = JsonSchema.asJsValue[Login]
//Action[Login]
def login()=Action(bp.jsonRefined(loginFmt, loginSchema)) {
implicit request => Ok
}
```
```scala
POST /login controllers.HomeController.login
```
Notes:
De quelles informations avons nous besoin ?
On parse le route file de play en utilisant le play route compiler
### Validation au runtime
```scala
implicit def refinedR[T,P](
implicit readsT : Reads[T],
v : Validate[T,P]) : Reads[T Refined P] =
(json: JsValue) => {
readsT
.reads(json)
.flatMap { t: T =>
refineV[P](t) match {//validation refined au runtime
case Left(error) => JsError(error)
case Right(value) => JsSuccess(value)
}
}
}
```
Notes:
Fait le pont entre les annotation à la compilation et au runtime
### En synthèse