-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of kebs-opaque. (#196)
* Initial implementation of kebs-opaque. * change paths, disable scala 2 build Co-authored-by: Paweł Kiersznowski <pkiersznowski@iterato.rs>
- Loading branch information
Showing
4 changed files
with
200 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
opaque/src/main/scala-3/pl/iterators/kebs/opaque/Opaque.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package pl.iterators.kebs.opaque | ||
|
||
import pl.iterators.kebs.macros.CaseClass1Rep | ||
|
||
trait Opaque[OpaqueType, Unwrapped](using ev: OpaqueType =:= Unwrapped) { | ||
/** | ||
* Validates and transforms (ex. sanitizes) unwrapped value. | ||
* @param unwrapped value to be validated and transformed | ||
* @return Left(reason) if validation fails; Right(opaqueType) if validation succeeds | ||
*/ | ||
protected def validate(unwrapped: Unwrapped): Either[String, OpaqueType] = Right(ev.flip.apply(unwrapped)) | ||
|
||
/** | ||
* Validates and transforms (ex. sanitizes) unwrapped value. By default, there is no validation or transformation. | ||
* @param unwrapped value to be validated and transformed | ||
* @return Left(reason) if validation fails; Right(opaqueType) if validation succeeds | ||
*/ | ||
def from(unwrapped: Unwrapped): Either[String, OpaqueType] = validate(unwrapped) | ||
|
||
/** | ||
* Creates an instance of OpaqueType from unwrapped value. | ||
* @param unwrapped value to be validated, transformed and wrapped in OpaqueType | ||
* @throws IllegalArgumentException with reason, if validation fails | ||
* @return OpaqueType wrapping validated & transformed unwrapped value | ||
*/ | ||
def apply(unwrapped: Unwrapped): OpaqueType = validate(unwrapped).fold(l => throw new IllegalArgumentException(l), identity) | ||
|
||
/** | ||
* Creates an instance of OpaqueType from unwrapped value in an unsafe manner - without validation or transformation. | ||
* @param unwrapped value to be wrapped in OpaqueType | ||
* @return OpaqueType wrapping unwrapped value | ||
*/ | ||
def unsafe(unwrapped: Unwrapped): OpaqueType = ev.flip.apply(unwrapped) | ||
|
||
extension (w: OpaqueType) { | ||
/** | ||
* Unwraps value wrapped in OpaqueType. | ||
* @return unwrapped value | ||
*/ | ||
def unwrap: Unwrapped = ev.apply(w) | ||
} | ||
|
||
given cc1Rep: CaseClass1Rep[OpaqueType, Unwrapped] = CaseClass1Rep(apply, _.unwrap) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import org.scalatest.funsuite.AnyFunSuite | ||
import org.scalatest.matchers.should.Matchers | ||
import pl.iterators.kebs.opaque.Opaque | ||
import pl.iterators.kebs.macros.CaseClass1Rep | ||
|
||
object OpaqueTestDomain { | ||
opaque type TestWrappedInt = Int | ||
object TestWrappedInt extends Opaque[TestWrappedInt, Int] | ||
|
||
opaque type ValidatedTestWrappedString = String | ||
object ValidatedTestWrappedString extends Opaque[ValidatedTestWrappedString, String] { | ||
override def validate(value: String): Either[String, ValidatedTestWrappedString] = | ||
if (value.isEmpty) Left("Empty string") else Right(value.trim) | ||
} | ||
|
||
extension (s: ValidatedTestWrappedString) { | ||
def myMap(f: Char => Char): ValidatedTestWrappedString = s.map(f) | ||
} | ||
} | ||
|
||
object OpaqueTestTypeclass { | ||
trait Showable[A] { | ||
def show(a: A): String | ||
} | ||
|
||
given Showable[Int] = (a: Int) => a.toString | ||
given[S, A](using showable: Showable[S], cc1Rep: CaseClass1Rep[A, S]): Showable[A] = (a: A) => showable.show(cc1Rep.unapply(a)) | ||
} | ||
|
||
class OpaqueTest extends AnyFunSuite with Matchers { | ||
import OpaqueTestDomain._ | ||
test("Equality") { | ||
TestWrappedInt(42) shouldEqual TestWrappedInt(42) | ||
TestWrappedInt(42) shouldNot equal (TestWrappedInt(1337)) | ||
"""TestWrappedString("foo") == "foo"""" shouldNot compile | ||
"""implicitly[=:=[TestWrappedString, String]]""" shouldNot compile | ||
} | ||
|
||
test("Basic ops") { | ||
TestWrappedInt(42).unwrap shouldEqual 42 | ||
TestWrappedInt.from(42) should equal (Right(TestWrappedInt(42))) | ||
} | ||
|
||
test("Validation & sanitization") { | ||
an[IllegalArgumentException] should be thrownBy ValidatedTestWrappedString("") | ||
ValidatedTestWrappedString.unsafe("").unwrap should equal ("") | ||
ValidatedTestWrappedString(" foo ").unwrap should equal ("foo") | ||
ValidatedTestWrappedString.from("") should equal (Left("Empty string")) | ||
ValidatedTestWrappedString.from(" foo ") should equal (Right(ValidatedTestWrappedString("foo"))) | ||
} | ||
|
||
test("Extension") { | ||
ValidatedTestWrappedString("foo").myMap(_.toUpper) shouldEqual ValidatedTestWrappedString("FOO") | ||
} | ||
|
||
test("Basic derivation") { | ||
"implicitly[CaseClass1Rep[ValidatedTestWrappedString, String]]" should compile | ||
implicitly[CaseClass1Rep[ValidatedTestWrappedString, String]].apply("foo") shouldEqual ValidatedTestWrappedString("foo") | ||
implicitly[CaseClass1Rep[ValidatedTestWrappedString, String]].unapply(ValidatedTestWrappedString("foo")) shouldEqual "foo" | ||
an[IllegalArgumentException] should be thrownBy implicitly[CaseClass1Rep[ValidatedTestWrappedString, String]].apply("") | ||
} | ||
|
||
test("Typeclass derivation") { | ||
import OpaqueTestTypeclass._ | ||
"implicitly[Showable[TestWrappedInt]]" should compile | ||
implicitly[Showable[TestWrappedInt]].show(TestWrappedInt(42)) shouldEqual "42" | ||
} | ||
} |