# 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

Qui suis-je ?

  • Freelance depuis 2011
  • Développeur d'API (SOAP,REST)
  • Développeur Scala
  • Fan des macros Scala
  • Moins fan d'écrire de la doc
@CestPasDur

Il y a la bonne API et la mauvaise API !

### 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

Questions