From 3b2514b394fd39f42147f0998c84495fecd2cd19 Mon Sep 17 00:00:00 2001 From: Grigorii Berezin Date: Sat, 27 Apr 2024 09:06:14 +0300 Subject: [PATCH] feat: add zio-prelude-quickstart --- build.sbt | 4 +- zio-quickstart-prelude/build.sbt | 8 ++ .../src/test/scala/AssociativeSpec.scala | 54 ++++++++ .../src/test/scala/NewTypeSpec.scala | 50 +++++++ .../src/test/scala/ValidationSpec.scala | 123 ++++++++++++++++++ 5 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 zio-quickstart-prelude/build.sbt create mode 100644 zio-quickstart-prelude/src/test/scala/AssociativeSpec.scala create mode 100644 zio-quickstart-prelude/src/test/scala/NewTypeSpec.scala create mode 100644 zio-quickstart-prelude/src/test/scala/ValidationSpec.scala diff --git a/build.sbt b/build.sbt index e9c82bc..d590208 100644 --- a/build.sbt +++ b/build.sbt @@ -32,7 +32,8 @@ lazy val root = `zio-quickstart-kafka`, `zio-quickstart-graphql-webservice`, `zio-quickstart-streams`, - `zio-quickstart-encode-decode-json` + `zio-quickstart-encode-decode-json`, + `zio-quickstart-prelude` ) lazy val `zio-quickstart-hello-world` = project @@ -48,3 +49,4 @@ lazy val `zio-quickstart-graphql-webservice` = project lazy val `zio-quickstart-streams` = project lazy val `zio-quickstart-encode-decode-json` = project lazy val `zio-quickstart-reloadable-services` = project +lazy val `zio-quickstart-prelude` = project diff --git a/zio-quickstart-prelude/build.sbt b/zio-quickstart-prelude/build.sbt new file mode 100644 index 0000000..3c2a8b8 --- /dev/null +++ b/zio-quickstart-prelude/build.sbt @@ -0,0 +1,8 @@ +scalaVersion := "2.13.13" + +libraryDependencies ++= Seq( + "dev.zio" %% "zio-prelude" % "1.0.0-RC23", + "dev.zio" %% "zio-test" % "2.0.22" % Test, + "dev.zio" %% "zio-test-sbt" % "2.0.22" % Test, + "dev.zio" %% "zio-test-junit" % "2.0.22" % Test +) diff --git a/zio-quickstart-prelude/src/test/scala/AssociativeSpec.scala b/zio-quickstart-prelude/src/test/scala/AssociativeSpec.scala new file mode 100644 index 0000000..6efc8a0 --- /dev/null +++ b/zio-quickstart-prelude/src/test/scala/AssociativeSpec.scala @@ -0,0 +1,54 @@ +import zio._ +import zio.test.assertTrue +import zio.prelude._ +import zio.prelude.newtypes.Max +import zio.test.{Spec, TestEnvironment} +import zio.test.junit.JUnitRunnableSpec + +object AssociativeSpec extends JUnitRunnableSpec { + object Topic extends Newtype[String] + type Topic = Topic.Type + + object Votes extends Subtype[Int] { + implicit val Associative: Associative[Votes] = new Associative[Votes] { + override def combine(l: => Votes, r: => Votes): Votes = Votes(l + r) + } + } + type Votes = Votes.Type + + override def spec: Spec[TestEnvironment with Scope, Any] = + suite("Associative")( + test("combine custom class") { + + case class VoteMap(wrapped: Map[Topic, Votes]) + object VoteMap { + implicit val Associative: Associative[VoteMap] = + new Associative[VoteMap] { + override def combine(l: => VoteMap, r: => VoteMap): VoteMap = + VoteMap(l.wrapped <> r.wrapped) + } + } + + val vm1 = VoteMap( + Map(Topic("newType") -> Votes(3), Topic("associative") -> Votes(1)) + ) + val vm2 = VoteMap( + Map(Topic("associative") -> Votes(6), Topic("prelude") -> Votes(2)) + ) + val resultVm = + VoteMap( + Map( + Topic("newType") -> Votes(3), + Topic("associative") -> Votes(7), + Topic("prelude") -> Votes(2) + ) + ) + assertTrue(vm1 <> vm2 == resultVm) + }, + test("combine as max using Max newtype") { + val rawValues = Seq(100, 262, 131, 66) + val maxValues: Seq[Max[Int]] = Max.wrapAll(rawValues) + assertTrue(maxValues.reduce(_ <> _) === rawValues.max) + } + ) +} diff --git a/zio-quickstart-prelude/src/test/scala/NewTypeSpec.scala b/zio-quickstart-prelude/src/test/scala/NewTypeSpec.scala new file mode 100644 index 0000000..7ed7552 --- /dev/null +++ b/zio-quickstart-prelude/src/test/scala/NewTypeSpec.scala @@ -0,0 +1,50 @@ +import zio._ +import zio.test.assertTrue +import zio.prelude._ +import zio.prelude.newtypes._ +import zio.test.junit.JUnitRunnableSpec + +object NewTypeSpec extends JUnitRunnableSpec { + override def spec = suite("Newtype")( + test("natural") { + // new types to increase the type safety without compromising performance or ergonomics + // Natural new type is an Int type with assertion on greater or equal to 0 + val five: Validation[String, Int] = Natural.make(5) + val notNatural: Validation[String, Natural] = Natural.make(-2) + + assertTrue(five.toOption.contains(5)) + assertTrue( + notNatural == Validation.failNonEmptyChunk( + NonEmptyChunk("-2 did not satisfy greaterThanOrEqualTo(0)") + ) + ) + assertTrue(Natural.zero - Natural.one == -1) // unsafe manipulations + assertTrue( + Natural.minus(Natural.zero, Natural.one) == Natural.zero + ) // safe manipulations + }, + test("custom assertion") { + // you can define your own value type with assertion + object MyType extends Subtype[String] { + override def assertion: QuotedAssertion[String] = (value: String) => + Either.cond( + value.startsWith("!"), + value, + AssertionError.failure(s"must start with exclamation mark") + ) + } + + val valid: Validation[String, String] = MyType.make("!Hello!") + val notValid: Validation[String, String] = MyType.make("NotValidString") + + assertTrue(valid.toOption.contains("!Hello!")) + assertTrue( + notValid == Validation.failNonEmptyChunk( + NonEmptyChunk.single( + "NotValidString did not satisfy must start with exclamation mark" + ) + ) + ) + } + ) +} diff --git a/zio-quickstart-prelude/src/test/scala/ValidationSpec.scala b/zio-quickstart-prelude/src/test/scala/ValidationSpec.scala new file mode 100644 index 0000000..1cb9496 --- /dev/null +++ b/zio-quickstart-prelude/src/test/scala/ValidationSpec.scala @@ -0,0 +1,123 @@ +import zio._ +import zio.prelude._ +import zio.test.{Spec, TestEnvironment, assertTrue} +import zio.test.junit.JUnitRunnableSpec + +object ValidationSpec extends JUnitRunnableSpec { + override def spec: Spec[TestEnvironment with Scope, Any] = + suite("Validation")( + test("validate class") { + case class Person(name: String, age: Int) + + def validateName(name: String): Validation[String, String] = + if (name.isEmpty) Validation.fail("Name was empty") + else Validation.succeed(name) + + def validateAge(age: Int): Validation[String, Int] = + Validation.fromPredicateWith(s"Age $age was less than zero")(age)( + _ >= 0 + ) + + def validatePerson(name: String, age: Int): Validation[String, Person] = + Validation.validateWith(validateName(name), validateAge(age))( + Person.apply + ) + + assertTrue( + validatePerson("Grisha", 25).toOption.contains(Person("Grisha", 25)) + ) + assertTrue( + validatePerson("", -5) == Validation.failNonEmptyChunk( + NonEmptyChunk("Name was empty", "Age -5 was less than zero") + ) + ) + }, + test("validate newtype") { + object Name extends Subtype[String] { + override def assertion = assert(!Assertion.isEmptyString) + } + type Name = Name.Type + object Age extends Subtype[Int] { + override def assertion = assert(Assertion.greaterThanOrEqualTo(0)) + } + type Age = Age.Type + + case class Person(name: Name, age: Age) + + def validatePerson(name: String, age: Int) = + Validation.validateWith(Name.make(name), Age.make(age))(Person.apply) + + assertTrue( + validatePerson("Grisha", 25).toOption.contains( + Person(Name("Grisha"), Age(25)) + ) + ) + assertTrue( + validatePerson("", -5) == Validation.failNonEmptyChunk( + NonEmptyChunk( + " did not satisfy hasLength(notEqualTo(0))", + "-5 did not satisfy greaterThanOrEqualTo(0)" + ) + ) + ) + }, + test("chaining validations") { + object Age extends Subtype[Int] { + override def assertion = assert(Assertion.greaterThanOrEqualTo(0)) + } + type Age = Age.Type + + def validateNonEmpty( + s: String + ): Validation[String, NonEmptyList[String]] = + Validation.fromOptionWith( + "String must contain at least one value divided by space character" + )(NonEmptyList.fromIterableOption(s.split(" "))) + + def validateInt(s: String): Validation[String, Int] = + Validation.fromOptionWith(s"String must be int like, but got $s")( + s.toIntOption + ) + + def validateAge(i: Int): Validation[String, Age] = Age.make(i) + + def calculateResult( + line: String + ): Validation[String, NonEmptyList[Age]] = + for { + strAges <- validateNonEmpty(line) + intAges <- Validation.validateAll(strAges.map(validateInt)) + ages <- Validation.validateAll(intAges.map(validateAge)) + } yield ages + + val result1 = calculateResult("12 10 5") + val result2 = calculateResult("12 -5 -2") + val result3 = calculateResult("") + val result4 = calculateResult("12 _f") + + assertTrue( + result1.toOption.get === NonEmptyList(Age(12), Age(10), Age(5)) + ) + assertTrue( + result2 == Validation.failNonEmptyChunk( + NonEmptyChunk( + "-5 did not satisfy greaterThanOrEqualTo(0)", + "-2 did not satisfy greaterThanOrEqualTo(0)" + ) + ) + ) + assertTrue( + result3 == Validation.failNonEmptyChunk( + NonEmptyChunk.single( + "String must contain at least one value divided by space character" + ) + ) + ) + assertTrue( + result4 == Validation.failNonEmptyChunk( + NonEmptyChunk.single("String must be int like, but got _f") + ) + ) + } + ) +}