|
| 1 | +package dagmendez.c |
| 2 | + |
| 3 | +import scala.compiletime.codeOf |
| 4 | +import scala.compiletime.error |
| 5 | +import scala.util.control.NoStackTrace |
| 6 | + |
| 7 | +object Advanced: |
| 8 | + |
| 9 | + /* |
| 10 | + * 1. Opaque Tyoes |
| 11 | + */ |
| 12 | + |
| 13 | + opaque type Name = String |
| 14 | + opaque type IBAN = String // International Bank Account Number |
| 15 | + opaque type Balance = Int |
| 16 | + |
| 17 | + /* |
| 18 | + * Validations on the apply method are restricted to those that can be evaluated at compile time. |
| 19 | + * Most of the API of the basic types (String, Int, etc.) are of no use here. |
| 20 | + * Notice that the validations are different in the apply method and the from method. |
| 21 | + * Both validations should be equal and defined in a single places. |
| 22 | + * `scala.compiletime.codeOf` returns the value of the parameter passed into the inlined method apply |
| 23 | + * `scala.compiletime.error` generates a custom compiler error. |
| 24 | + * */ |
| 25 | + |
| 26 | + object Name: |
| 27 | + |
| 28 | + inline def apply(name: String): Name = |
| 29 | + inline if name == "" |
| 30 | + then error(codeOf(name) + " is invalid.") |
| 31 | + else name |
| 32 | + |
| 33 | + def either(fn: String): Either[InvalidName, Name] = |
| 34 | + // Here we can access the underlying type API because it is evaluated during runtime. |
| 35 | + if fn.isBlank | (fn.trim.length < fn.length) |
| 36 | + then Left(InvalidName(s"First name is invalid with value <$fn>.")) |
| 37 | + else Right(fn) |
| 38 | + |
| 39 | + object IBAN: |
| 40 | + |
| 41 | + inline def apply(iban: String): IBAN = |
| 42 | + inline if iban == "" |
| 43 | + then error(codeOf(iban) + " in invalid.") |
| 44 | + else iban |
| 45 | + |
| 46 | + def either(iban: String): Either[InvalidIBAN, IBAN] = |
| 47 | + if iban.isBlank | iban.contains(" ") |
| 48 | + then Left(InvalidIBAN(s"First name is invalid with value <$iban>.")) |
| 49 | + else Right(iban) |
| 50 | + |
| 51 | + object Balance: |
| 52 | + |
| 53 | + inline def apply(balance: Int): Balance = |
| 54 | + inline if balance > 1000000 | balance < -1000 |
| 55 | + then error(codeOf(balance) + " in invalid.") |
| 56 | + else balance |
| 57 | + |
| 58 | + def either(balance: Int): Either[InvalidBalance, Balance] = |
| 59 | + if balance > 1000000 | balance < -1000 |
| 60 | + then Left(InvalidBalance(s"First name is invalid with value <$balance>.")) |
| 61 | + else Right(balance) |
| 62 | + |
| 63 | + /* |
| 64 | + * 2. Domain |
| 65 | + */ |
| 66 | + |
| 67 | + // The account holder is the person who signs the contract for said account with the bank |
| 68 | + final case class AccountHolder(firstName: Name, middleName: Option[Name], lastName: Name, secondLastName: Option[Name]) |
| 69 | + final case class Account(accountHolder: AccountHolder, iban: IBAN, balance: Balance) |
| 70 | + |
| 71 | + /* |
| 72 | + * 3. Errors |
| 73 | + */ |
| 74 | + |
| 75 | + // We add two custom error classes to handle invalid values |
| 76 | + final case class InvalidName(message: String) extends RuntimeException(message) with NoStackTrace |
| 77 | + final case class InvalidIBAN(message: String) extends RuntimeException(message) with NoStackTrace |
| 78 | + final case class InvalidBalance(message: String) extends RuntimeException(message) with NoStackTrace |
| 79 | + |
| 80 | + @main def run(): Unit = |
| 81 | + |
| 82 | + object HappyApply: |
| 83 | + private val firstName: Name = Name("John") |
| 84 | + private val middleName: Name = Name("Stuart") |
| 85 | + private val lastName: Name = Name("Mill") |
| 86 | + private val iban: IBAN = IBAN("GB33BUKB20201555555555") |
| 87 | + private val balance: Balance = Balance(-300) |
| 88 | + |
| 89 | + private val account: Account = |
| 90 | + Account( |
| 91 | + AccountHolder( |
| 92 | + firstName, |
| 93 | + Some(middleName), |
| 94 | + lastName, |
| 95 | + secondLastName = None |
| 96 | + ), |
| 97 | + iban, |
| 98 | + balance |
| 99 | + ) |
| 100 | + |
| 101 | + def print(): Unit = println(account) |
| 102 | + |
| 103 | + object UnhappyApply: |
| 104 | + private val firstName: Name = Name("John") // Comment this one an uncomment next line |
| 105 | + // private val firstName: Name = Name("") // Uncomment and won't compile |
| 106 | + private val middleName: Name = Name("Stuart") |
| 107 | + private val lastName: Name = Name("Mill") |
| 108 | + private val iban: IBAN = IBAN("GB33BUKB20201555555555") |
| 109 | + private val balance: Balance = Balance(-1000) |
| 110 | + |
| 111 | + private val account: Account = |
| 112 | + Account( |
| 113 | + AccountHolder( |
| 114 | + firstName, |
| 115 | + Some(middleName), |
| 116 | + lastName, |
| 117 | + secondLastName = None |
| 118 | + ), |
| 119 | + iban, |
| 120 | + balance |
| 121 | + ) |
| 122 | + |
| 123 | + def print(): Unit = println(account) |
| 124 | + |
| 125 | + object HappyFrom: |
| 126 | + private val firstName: Either[InvalidName, Name] = Name.either("John") |
| 127 | + private val middleName: Either[InvalidName, Name] = Name.either("Stuart") |
| 128 | + private val lastName: Either[InvalidName, Name] = Name.either("Mill") |
| 129 | + private val iban: Either[InvalidIBAN, IBAN] = IBAN.either("GB33BUKB20201555555555") |
| 130 | + private val balance: Either[InvalidBalance, Balance] = Balance.either(0) |
| 131 | + |
| 132 | + private val account: Either[RuntimeException & NoStackTrace, Account] = |
| 133 | + for |
| 134 | + fn <- firstName |
| 135 | + mn <- middleName |
| 136 | + ln <- lastName |
| 137 | + ib <- iban |
| 138 | + bl <- balance |
| 139 | + yield Account(AccountHolder(fn, Some(mn), ln, secondLastName = None), ib, bl) |
| 140 | + |
| 141 | + assert(account.isRight) |
| 142 | + |
| 143 | + def print(): Unit = println(account) |
| 144 | + |
| 145 | + object UnhappyFrom: |
| 146 | + |
| 147 | + // Play with any field that would crash the validation and return Left |
| 148 | + private val firstName: Either[InvalidName, Name] = Name.either("John") |
| 149 | + private val middleName: Either[InvalidName, Name] = Name.either("Stuart ") // This returns Left. |
| 150 | + private val lastName: Either[InvalidName, Name] = Name.either("Mill") |
| 151 | + private val iban: Either[InvalidIBAN, IBAN] = IBAN.either("GB33BUKB20201555555555") |
| 152 | + private val balance: Either[InvalidBalance, Balance] = Balance.either(-5000) // This returns Left. |
| 153 | + |
| 154 | + private val account: Either[RuntimeException & NoStackTrace, Account] = |
| 155 | + for |
| 156 | + fn <- firstName |
| 157 | + mn <- middleName |
| 158 | + ln <- lastName |
| 159 | + ib <- iban |
| 160 | + bl <- balance |
| 161 | + yield Account(AccountHolder(fn, Some(mn), ln, secondLastName = None), ib, bl) |
| 162 | + |
| 163 | + assert(account.isLeft) |
| 164 | + |
| 165 | + def print(): Unit = println(account) |
| 166 | + |
| 167 | + HappyApply.print() // Compiles |
| 168 | + UnhappyApply.print() // Won't compile |
| 169 | + HappyFrom.print() // Right |
| 170 | + UnhappyFrom.print() // Left |
0 commit comments