From df59cd645c35870c6520a7880bdbf8ca2df6cbc6 Mon Sep 17 00:00:00 2001 From: "joel.vonderwe" <joel.von-der-weid@etu.hesge.ch> Date: Thu, 19 Mar 2020 15:47:14 +0100 Subject: [PATCH] Added result --- base_tp/src/result/result.scala | 102 +++++++++++++++++++++++++ base_tp/test/resultTest.scala | 131 ++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 base_tp/src/result/result.scala create mode 100644 base_tp/test/resultTest.scala diff --git a/base_tp/src/result/result.scala b/base_tp/src/result/result.scala new file mode 100644 index 0000000..fcf40ad --- /dev/null +++ b/base_tp/src/result/result.scala @@ -0,0 +1,102 @@ +package ch.hepia.tpscala.result + + +sealed trait Result[+E,+A] { + + def map[B]( f: A=>B ): Result[E,B] = { + this match { + case OK(a) => OK(f(a)) + case Err(e) => Err(e) + } + } + + //F >: E, signifie "F est un supertype de E" + def flatMap[B, F >: E]( f: A=>Result[F,B] ): Result[F,B] = { + this match { + case OK(a) => f(a) + case Err(e) => Err(e) + } + } + + //Retourne le résultat si OK + //transforme l'erreur en une valeur valide sinon + //B >: A signifie "B est un supertype de A + //Une seul implémentation possible ! + def get[B >: A]( f: E => B ): B = { + this match { + case OK(a) => a + case Err(e) => f(e) + } + } + + //Une seul implémentation possible ! + def fold[B]( f: E => B, g: A => B ): B = { + this match { + case OK(a) => g(a) + case Err(e) => f(e) + } + } + + def toOption: Option[A] = { + this match { + case OK(a) => Some(a) + case Err(_) => None + } + } + +} +case class OK[A]( value: A ) extends Result[Nothing,A] +case class Err[E]( value: E ) extends Result[E,Nothing] +object Result { + //Si il y a au moins une erreur dans la liste, le résultat est une erreur + //Si il n'y a que des succès, liste les succès + def sequence[E,A]( res: List[Result[E,A]] ): Result[E,List[A]] = { + def seqRec(res: List[Result[E,A]], acc: List[A]): Result[E,List[A]] = { + res match { + case Nil => OK(acc.reverse) + case OK(a) :: tl => seqRec(tl, a :: acc) + case Err(e) :: _ => Err(e) + } + } + seqRec(res, Nil) + } +} + + +case class Config( hostname: String, port: Int ) +object Config { + private def line2keyValue( line: String ): Result[String,(String,String)] = { + val elements = line.split("=") + if( elements.size != 2 ) Err("Syntax error: " + line ) + else OK( (elements(0), elements(1) ) ) + } + + private def lines2map( lines: List[String] ): Result[String,Map[String,String]] = + Result.sequence( lines.map(line2keyValue) ).map( _.toMap ) + + private def safeInt(str: String): Result[String, Int] = { + try { + OK(str.toInt) + } catch { + case _:Throwable => Err("Invalid port") + } + } + + private def mGet[K,V](mkv: Map[K,V], key: K): Result[String, V] = { + mkv.get(key) match { + case Some(value) => OK(value) + case None => Err(key.toString + " not found") + } + } + + //Utilisez lines2map pour parvenir au résultat + //Attention au clés manquantes + def parse( lines: List[String] ): Result[String,Config] = { + for { + keyVal <- lines2map(lines) + h <- mGet(keyVal, "hostname") + pStr <- mGet(keyVal, "port") + p <- safeInt(pStr) + } yield Config(h,p) + } +} diff --git a/base_tp/test/resultTest.scala b/base_tp/test/resultTest.scala new file mode 100644 index 0000000..abfcc73 --- /dev/null +++ b/base_tp/test/resultTest.scala @@ -0,0 +1,131 @@ +package ch.hepia.tpscala + +import org.scalatest.funsuite.AnyFunSuite + +import result._ + +/**** + * + * Dans SBT vous pouvez n'exécuter que ces tests: + * + * > testOnly ch.hepia.tpscala.Result7Suite + * + */ + +class Result7Suite extends AnyFunSuite { + + + test( "map" ) { + val hello: Result[String,String] = OK("hello") + val notFound: Result[Int,String] = Err(404) + + assert( hello.map(_.size) == OK(5) ) + assert( notFound.map(_.toString) == notFound ) + + assert( hello.map(identity) == hello ) + assert( notFound.map(identity) == notFound ) + + assert( hello.map(_.size).map( _ > 2) == + hello.map( _.size > 2 ) ) + + assert( notFound.map(_ * 2 ).map( _.toString) == + notFound.map( i => (i*2).toString ) ) + } + + test( "flatMap" ) { + type Res[A] = Result[String,A] + val goodHost: Res[String] = OK("hello.com") + val badHost: Res[String] = Err("NA") + val goodPort: Res[Int] = OK(80) + val badPort: Res[Int] = Err("undefined") + + assert( + goodHost.flatMap{ host => + goodPort.map{ port => + Config(host,port) + } + } == OK( Config("hello.com",80) ) + ) + + assert( + goodHost.flatMap{ host => + badPort.map{ port => + Config(host,port) + } + } == badPort + ) + + assert( + badHost.flatMap{ host => + goodPort.map{ port => + Config(host,port) + } + } == badHost + ) + + assert( + badHost.flatMap{ host => + badPort.map{ port => + Config(host,port) + } + } == badHost + ) + } + + test("get") { + val found: Result[String, Long] = OK(665) + val notFound: Result[String,Long] = Err("not found") + + assert( found.get( _ => -1 ) == 665 ) + assert( notFound.get( _ => -1 ) == -1 ) + + def str2status( str: String ) = str match { + case "not found" => 404 + case "conflict" => 409 + case "unauthorized" => 403 + case _ => 400 + } + + assert( found.map( _ => 200).get(str2status) == 200 ) + assert( notFound.map( _ => 200).get(str2status) == 404 ) + } + + + test( "parse config" ) { + val lines1 = List( + "port=8888", + "hostname=www.example.com", + ) + + val lines2 = List( + "port: 8888", + "hostname: www.example.com", + ) + + val lines3 = List( + "port=8888", + ) + + val lines4 = List( + "port=HTTP", + "hostname=www.example.com", + ) + + + val default = Config("localhost", 8080 ) + + assert( Config.parse( lines1 ).get( _ => default ) + == Config("www.example.com",8888) ) + + assert( Config.parse( lines2 ).get( _ => default ) + == default ) + + assert( Config.parse( lines3 ).get( _ => default ) + == default ) + + assert( Config.parse( lines4 ).get( _ => default ) + == default ) + + + } +} -- GitLab