def parsePort(s: String): Int = s.toInt parsePort("Ooops!") def startSocket(port: Int): Unit = { val serverSocket = new ServerSocket(port) // ... } startSocket(parsePort("-1")) // Ooops... That is only one example of many.
types by further constraints. type PortNumber = Int Refined Interval.Closed[W.‘1‘.T, W.‘65535‘.T] def startSocket(p: PortNumber): Unit = { val serverSocket = new ServerSocket(p) // ... } startSocket(-1) // Does not compile!
types are, the easier is the implementation. final case class Service(host: String, port: Int) def hashName(name: String) versus final case class Service(host: String Refined IPv4, port: PortNumber) def hashName(name: String Refined NonEmpty)
an existing system • product data via external provider • mostly compatible • old system does only support article number larger than final case class ArtNo(n: Int) { require(n > 4096, "Illegal article number!") } This is horrible! Don’t do that!
better: final case class ArtNo(n: Int) object ArtNo { def apply(n: Int): Option[ArtNo] = if (n > 4096) Option(new ArtNo(n)) else None } But we can have this... :-) type ArtNo = Int Refined Greater[4096]
case class DatabaseConfig(driver: DatabaseDriverName, url: DatabaseUrl, user: DatabaseLogin, pass: DatabasePassword) object DatabaseConfig { implicit val configReader: ConfigReader[DatabaseConfig] = deriveReader[DatabaseConfig] } ⇒ Validated at runtime! (No own code necessary!)
or create of different formats • nowadays mostly JSON • gain more safety by stronger typing of the models • integration of refined with Circe (JSON library) • Decoder and Encoder for free
we really test? • very often simply input values (allowed range) "Creating a user" when { "name is empty" must { "throw an exception" in { the [IllegalArgumentException] thrownBy createUser(name = "", age = 18) must have message "Name must be present!" } } }
email: String, collectedStars: Int) versus final case class Foo(login: LoginName, email: EMail, stars: CollectedStars) The compiler can help you, so let it!