-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #154 from johnhungerford/add-layers
Add Layer trait and Layer factory methods on Envs, Aborts, Tries, and Options
- Loading branch information
Showing
6 changed files
with
301 additions
and
2 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
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,131 @@ | ||
package kyo | ||
|
||
object layers { | ||
|
||
trait Layer[In, Out] { self => | ||
def run[T, S](effect: T > (In with S))(implicit fl: Flat[T > (In with S)]): T > (S with Out) | ||
|
||
final def andThen[Out1, In1](other: Layer[In1, Out1]): Layer[In with In1, Out with Out1] = | ||
new Layer[In with In1, Out with Out1] { | ||
override def run[T, S]( | ||
effect: T > (In with In1 with S) | ||
)( | ||
implicit fl: Flat[T > (In with In1 with S)] | ||
): T > (S with Out with Out1) = { | ||
val selfRun: T > (In1 with Out with S) = | ||
self.run[T, S with In1](effect: T > (In1 with In with S)) | ||
val otherRun: T > (Out with Out1 with S) = | ||
other.run[T, S with Out](selfRun)(Flat.unsafe.unchecked) | ||
otherRun | ||
} | ||
} | ||
|
||
final def chain[In2, Out2](other: Layer[In2, Out2])( | ||
implicit ap: ChainLayer[Out, In2] | ||
): Layer[In, Out2 with ap.RemainingOut1] = { | ||
ap.applyLayer[In, Out2](self, other) | ||
} | ||
} | ||
|
||
sealed trait ChainLayer[Out1, In2] { | ||
type RemainingOut1 | ||
|
||
def applyLayer[In1, Out2]( | ||
layer1: Layer[In1, Out1], | ||
layer2: Layer[In2, Out2] | ||
): Layer[In1, RemainingOut1 with Out2] | ||
} | ||
|
||
trait ChainLayers2 { | ||
implicit def application[Out1, Shared, In2] | ||
: ChainLayer.Aux[Out1 with Shared, In2 with Shared, Out1] = | ||
new ChainLayer[Out1 with Shared, In2 with Shared] { | ||
type RemainingOut1 = Out1 | ||
override def applyLayer[In1, Out2]( | ||
layer1: Layer[In1, Out1 with Shared], | ||
layer2: Layer[In2 with Shared, Out2] | ||
): Layer[In1, Out1 with Out2] = | ||
new Layer[In1, Out1 with Out2] { | ||
override def run[T, S](effect: T > (In1 with S))(implicit | ||
fl: Flat[T > (In1 with S)] | ||
): T > (S with Out2 with Out1) = { | ||
val handled1: T > (S with Out1 with Shared) = layer1.run[T, S](effect) | ||
val handled2: T > (S with Out2 with Out1) = | ||
layer2.run[T, S with Out1](handled1)(Flat.unsafe.unchecked) | ||
handled2 | ||
} | ||
} | ||
|
||
} | ||
} | ||
|
||
trait ChainLayers1 { | ||
implicit def applyAll1[Shared, In2]: ChainLayer.Aux[Shared, In2 with Shared, Any] = | ||
new ChainLayer[Shared, In2 with Shared] { | ||
type RemainingOut1 = Any | ||
|
||
override def applyLayer[In1, Out2]( | ||
layer1: Layer[In1, Shared], | ||
layer2: Layer[In2 with Shared, Out2] | ||
): Layer[In1, Out2] = | ||
new Layer[In1, Out2] { | ||
override def run[T, S](effect: T > (In1 with S))(implicit | ||
fl: Flat[T > (In1 with S)] | ||
): T > (S with Out2) = { | ||
val handled1: T > (S with Shared) = layer1.run[T, S](effect) | ||
val handled2: T > (S with Out2) = | ||
layer2.run[T, S](handled1)(Flat.unsafe.unchecked) | ||
handled2 | ||
} | ||
} | ||
|
||
} | ||
|
||
implicit def applyAll2[Out1, Shared]: ChainLayer.Aux[Out1 with Shared, Shared, Out1] = | ||
new ChainLayer[Out1 with Shared, Shared] { | ||
type RemainingOut1 = Out1 | ||
|
||
override def applyLayer[In1, Out2]( | ||
layer1: Layer[In1, Out1 with Shared], | ||
layer2: Layer[Shared, Out2] | ||
): Layer[In1, Out1 with Out2] = | ||
new Layer[In1, Out1 with Out2] { | ||
override def run[T, S](effect: T > (In1 with S))(implicit | ||
fl: Flat[T > (In1 with S)] | ||
): T > (S with Out1 with Out2) = { | ||
val handled1: T > (S with Out1 with Shared) = layer1.run[T, S](effect) | ||
val handled2: T > (S with Out1 with Out2) = | ||
layer2.run[T, S with Out1](handled1)(Flat.unsafe.unchecked) | ||
handled2 | ||
} | ||
} | ||
|
||
} | ||
} | ||
|
||
object ChainLayer extends ChainLayers1 { | ||
type Aux[Out1, In2, R] = ChainLayer[Out1, In2] { type RemainingOut1 = R } | ||
|
||
implicit def simpleChain[Out]: ChainLayer.Aux[Out, Out, Any] = | ||
new ChainLayer[Out, Out] { | ||
type RemainingOut1 = Any | ||
|
||
override def applyLayer[In1, Out2]( | ||
layer1: Layer[In1, Out], | ||
layer2: Layer[Out, Out2] | ||
): Layer[In1, Any with Out2] = | ||
new Layer[In1, Any with Out2] { | ||
override def run[T, S](effect: T > (In1 with S))(implicit | ||
fl: Flat[T > (In1 with S)] | ||
): T > (S with Out2) = { | ||
val handled1: T > (Out with S) = layer1.run[T, S](effect) | ||
val handled2: T > (S with Out2) = | ||
layer2.run[T, S](handled1)(Flat.unsafe.unchecked) | ||
handled2 | ||
} | ||
} | ||
|
||
} | ||
} | ||
|
||
} |
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
120 changes: 120 additions & 0 deletions
120
kyo-core/shared/src/test/scala/kyoTest/layersTest.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,120 @@ | ||
package kyoTest | ||
|
||
import kyo._ | ||
import kyo.envs._ | ||
import kyo.aborts._ | ||
import kyo.tries._ | ||
import kyo.options._ | ||
import kyo.layers._ | ||
import scala.util.Failure | ||
|
||
class layersTest extends KyoTest { | ||
|
||
final case class Dep1(int: Int) | ||
final case class Dep2(str: String) | ||
final case class Dep3(bool: Boolean) | ||
|
||
final case class Dep(dep1: Int, dep2: String, dep3: Boolean) | ||
|
||
val depLayer = Envs[Dep].layer(Dep(1, "hello", true)) | ||
val dep1Layer = Envs[Dep1].layer(Envs[Dep].get.map(v => Dep1(v.dep1))) | ||
val dep2Layer = Envs[Dep2].layer(Envs[Dep].get.map(v => Dep2(v.dep2))) | ||
val dep3Layer = Envs[Dep3].layer(Envs[Dep].get.map(v => Dep3(v.dep3))) | ||
|
||
"Envs layers should be composable and provide multiple dependencies" in { | ||
val layer = (dep1Layer andThen dep2Layer andThen dep3Layer) chain depLayer | ||
|
||
val effect = for { | ||
dep1 <- Envs[Dep1].get | ||
dep2 <- Envs[Dep2].get | ||
dep3 <- Envs[Dep3].get | ||
} yield s"${dep1.int}-${dep2.str}-${dep3.bool}" | ||
|
||
val handledEffect = layer.run(effect) | ||
|
||
assert(handledEffect == "1-hello-true") | ||
} | ||
|
||
final case class TestError1(msg: String) | ||
final case class TestError2(msg: String) | ||
val stringToTE1Layer = Aborts[String].layer(str => Aborts[TestError1].fail(TestError1(str))) | ||
val dep1ToTE1Layer = Aborts[Dep2].layer(dep => Aborts[TestError1].fail(TestError1(dep.str))) | ||
val throwableToTE1Layer = | ||
Aborts[Throwable].layer(err => Aborts[TestError1].fail(TestError1(err.getMessage()))) | ||
val testError1ToTE2Layer = | ||
Aborts[TestError1].layer(err => Aborts[TestError2].fail(TestError2(err.msg))) | ||
|
||
"Aborts layers should be composable and handle multiple error types" in { | ||
val layer = | ||
(stringToTE1Layer andThen dep1ToTE1Layer andThen throwableToTE1Layer) chain testError1ToTE2Layer | ||
|
||
val effect1 = for { | ||
_ <- Aborts[String].fail("string failure") | ||
_ <- Aborts[Dep2].fail(Dep2("dep2 failure")) | ||
_ <- Aborts[Throwable].fail(new Exception("throwable failure")) | ||
} yield () | ||
|
||
val effect2 = for { | ||
_ <- Aborts[Throwable].fail(new Exception("throwable failure")) | ||
_ <- Aborts[String].fail("string failure") | ||
_ <- Aborts[Dep2].fail(Dep2("dep2 failure")) | ||
} yield () | ||
|
||
val effect3 = for { | ||
_ <- Aborts[Dep2].fail(Dep2("dep2 failure")) | ||
_ <- Aborts[Throwable].fail(new Exception("throwable failure")) | ||
_ <- Aborts[String].fail("string failure") | ||
} yield () | ||
|
||
assert(Aborts[TestError2].run(layer.run(effect1)) == Left(TestError2("string failure"))) | ||
assert(Aborts[TestError2].run(layer.run(effect2)) == Left(TestError2("throwable failure"))) | ||
assert(Aborts[TestError2].run(layer.run(effect3)) == Left(TestError2("dep2 failure"))) | ||
} | ||
|
||
val triesToAbortsLayer = Tries.layer(err => Aborts[Throwable].fail(err)) | ||
val triesToOptionsLayer = Tries.layer(_ => Options.get(None)) | ||
|
||
"Tries layer should handle tries as other failures" in { | ||
val effect = for { | ||
_ <- Tries.fail("fail") | ||
} yield () | ||
|
||
val effectHandledToAborts = triesToAbortsLayer.run(effect) | ||
val effectHandledToOptions = triesToOptionsLayer.run(effect) | ||
|
||
assert { | ||
Aborts[Throwable].run(effectHandledToAborts) match { | ||
case Left(err: Throwable) => err.getMessage == "fail" | ||
case _ => false | ||
} | ||
} | ||
assert(Options.run(effectHandledToOptions) == None) | ||
} | ||
|
||
val optionsToAbortsLayer = Options.layer(Aborts[String].fail("missing value")) | ||
val optionsToTriesLayer = Options.layer(Tries.fail("missing value")) | ||
|
||
"Options layer should handle None as other failures" in { | ||
val effect = for { | ||
_ <- Options.get(None) | ||
} yield () | ||
|
||
val effectHandledToAborts = optionsToAbortsLayer.run(effect) | ||
val effectHandledToTries = optionsToTriesLayer.run(effect) | ||
|
||
assert { | ||
Aborts[String].run(effectHandledToAborts) match { | ||
case Left("missing value") => true | ||
case _ => false | ||
} | ||
} | ||
|
||
assert { | ||
Tries.run(effectHandledToTries) match { | ||
case Failure(err: Throwable) => err.getMessage == "missing value" | ||
case _ => false | ||
} | ||
} | ||
} | ||
|
||
} |