Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
fix clean-up; add support for functional effects
Browse files Browse the repository at this point in the history
  • Loading branch information
goshacodes committed Sep 30, 2024
1 parent fe40aa2 commit c132013
Show file tree
Hide file tree
Showing 24 changed files with 891 additions and 361 deletions.
16 changes: 8 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
!modules/core/src/main/**/target/
!modules/core/src/test/**/target/

### IntelliJ IDEA ###
.idea/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
!modules/core/src/main/**/out/
!modules/core/src/test/**/out/

### Eclipse ###
.apt_generated
Expand All @@ -21,8 +21,8 @@ out/
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
!modules/core/src/main/**/bin/
!modules/core/src/test/**/bin/

### NetBeans ###
/nbproject/private/
Expand All @@ -31,8 +31,8 @@ bin/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
!modules/core/src/main/**/build/
!modules/core/src/test/**/build/

### VS Code ###
.vscode/
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,9 @@ val overloaded = stub[Overloaded]:
```

## Example
Model - [SessionCheckService.scala](./src/test/scala/backstub/SessionCheckService.scala)
Model - [SessionCheckService.scala](modules/core/src/test/scala/backstub/SessionCheckService.scala)

Suite - [SessionCheckServiceSpec.scala](./src/test/scala/backstub/SessionCheckServiceSpec.scala)
Suite - [SessionCheckServiceSpec.scala](modules/core/src/test/scala/backstub/SessionCheckServiceSpec.scala)

## Notes
Only basic functionality is supported by now.
Expand Down
55 changes: 47 additions & 8 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,19 +1,61 @@
lazy val modules = file("modules")

lazy val backstub = (project in file("."))
lazy val backstub = project
.in(file("."))
.settings(
name := "backstub",
commonSettings,
publishTo := None
)
.dependsOn(core, cats, zio)
.aggregate(core, cats, zio)

lazy val commonSettings =
Seq(
scalaVersion := "3.4.3",
scalacOptions ++=
Seq(
"-experimental",
//"-Xcheck-macros",
//"-explain"
"-experimental"
// "-Xcheck-macros",
// "-explain"
),
libraryDependencies ++= Seq(
"org.scalameta" %% "munit" % "1.0.0" % Test
)
)

lazy val core = project.in(modules / "core")
.settings(
name := "backstub",
commonSettings
)

lazy val zio = project.in(modules / "zio")
.dependsOn(core % "compile->compile;compile->test")
.settings(
name := "backstub-zio",
commonSettings,
libraryDependencies ++= {
val zioVersion = "2.1.9"
Seq(
"dev.zio" %% "zio" % zioVersion,
"dev.zio" %% "zio-test" % zioVersion % Test,
"dev.zio" %% "zio-test-sbt" % zioVersion % Test
)
},
testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")
)

lazy val cats = project.in(modules / "cats")
.dependsOn(core)
.settings(
name := "backstub-cats-effect",
commonSettings,
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-effect" % "3.5.4",
"org.typelevel" %% "munit-cats-effect" % "2.0.0" % Test
)
)

inThisBuild(
Seq(
organization := "io.github.goshacodes",
Expand All @@ -32,6 +74,3 @@ inThisBuild(
)

sonatypeRepository := "https://s01.oss.sonatype.org/service/local"



10 changes: 10 additions & 0 deletions modules/cats/src/main/scala/backstub/CatsEffectStubs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package backstub

import backstub.effect.StubEffect
import cats.effect.IO

trait CatsEffectStubs extends Stubs:
given StubEffect.Mono[IO] = new StubEffect.Mono[IO]:
def unit[T](t: => T): IO[T] = IO(t)
def flatMap[E, EE >: E, T, T2](fa: IO[T])(f: T => IO[T2]): IO[T2] = fa.flatMap(f)

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package catseffecttests

import backstub.*
import cats.effect.IO
import munit.CatsEffectSuite

class IOExpectationsSpec extends CatsEffectSuite, CatsEffectStubs:
override def beforeEach(context: BeforeEach) =
resetStubs()

trait Foo:
def zeroArgsIO: IO[Option[String]]

def oneArgIO(x: Int): IO[Option[String]]

def twoArgsIO(x: Int, y: String): IO[Option[String]]

def overloaded(x: Int, y: Boolean): IO[Int]

def overloaded(x: String): IO[Boolean]

def overloaded: IO[String]

val foo: Stub[Foo] = stub[Foo]:
Expect[Foo]
.methodF0(_.zeroArgsIO).returnsOnly(IO(Some("foo")))
.methodF(_.oneArgIO).returns(_ => IO(None))
.methodF(_.twoArgsIO).returns((x, y) => IO(None))
.methodF0(_.overloaded: IO[String]).returnsOnly(IO(""))
.methodF(_.overloaded: String => IO[Boolean]).returns(x => IO(true))
.methodF(_.overloaded: (Int, Boolean) => IO[Int]).returns((x, y) => IO(1))

test("zero args"):
val result = for
_ <- foo.zeroArgsIO
_ <- foo.zeroArgsIO
times <- foo.timesF(_.zeroArgsIO)
yield times

assertIO(result, 2)

test("one arg"):
val result = for
_ <- foo.oneArgIO(1)
_ <- foo.oneArgIO(2)
times <- foo.timesF(_.oneArgIO)
calls <- foo.callsF(_.oneArgIO)
yield (times, calls)

assertIO(result, (2, List(1, 2)))


test("two args"):
val result = for
_ <- foo.twoArgsIO(1, "foo")
_ <- foo.twoArgsIO(2, "bar")
times <- foo.timesF(_.twoArgsIO)
calls <- foo.callsF(_.twoArgsIO)
yield (times, calls)

assertIO(result, (2, List((1, "foo"), (2, "bar"))))

62 changes: 62 additions & 0 deletions modules/core/src/main/scala/backstub/Expect.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package backstub

import effect.StubEffect

import scala.annotation.compileTimeOnly
import scala.util.{NotGiven, TupledFunction}

trait Expect[T]:
def method[R](
select: T => R
)(using
NotGiven[<:<[R, Tuple => ?]]
): Expect.Returns0[T, R]

def method[Arg, R](select: T => Arg => R): Expect.Returns1[T, Arg, R]

def method[F, Args <: ? *: ? *: EmptyTuple, R](
select: T => F
)(using
TupledFunction[F, Args => R]
): Expect.Returns[T, Args, R]

def methodF0[F[+_]: StubEffect.Mono, R](
select: T => F[R]
): Expect.Returns0[T, F[R]]

def methodF[Arg, F[+_]: StubEffect.Mono, R](
select: T => Arg => F[R]
): Expect.Returns1[T, Arg, F[R]]

def methodF[Fun, Args <: ? *: ? *: EmptyTuple, F[+_]: StubEffect.Mono, R](
select: T => Fun
)(using
TupledFunction[Fun, Args => F[R]]
): Expect.Returns[T, Args, F[R]]

def methodIO[F[+_, +_]: StubEffect, E, R](
select: T => F[E, R]
): Expect.Returns0[T, F[E, R]]

def methodIO[Arg, F[+_, +_]: StubEffect, E, R](
select: T => Arg => F[E, R]
): Expect.Returns1[T, Arg, F[E, R]]

def methodIO[Fun, Args <: ? *: ? *: EmptyTuple, F[+_, +_]: StubEffect, E, R](
select: T => Fun
)(using
TupledFunction[Fun, Args => F[E, R]]
): Expect.Returns[T, Args, F[E, R]]

object Expect:
@compileTimeOnly("Expect[T] is considered to be an inline given or passed directly to stub[T]")
def apply[T]: Expect[T] = throw IllegalAccessError()

trait Returns0[T, R]:
def returnsOnly[RR <: R](value: RR): Expect[T]

trait Returns1[T, Arg, R]:
def returns[RR <: R](value: Arg => RR): Expect[T]

trait Returns[T, Args <: Tuple, R]:
def returns[RR <: R](value: Args => RR): Expect[T]
11 changes: 11 additions & 0 deletions modules/core/src/main/scala/backstub/Stubs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package backstub

import effect.StubEffect

trait Stubs:
final given stubs: CreatedStubs = CreatedStubs()

final def resetStubs(): Unit = stubs.clearAll()

final def resetStubsIO[F[+_, +_]: StubEffect]: F[Nothing, Unit] =
summon[StubEffect[F]].unit(resetStubs())
108 changes: 108 additions & 0 deletions modules/core/src/main/scala/backstub/calls.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package backstub

import backstub.effect.StubEffect
import backstub.internal.UntupleOne

import scala.quoted.{Expr, Quotes, Type}
import scala.util.{NotGiven, TupledFunction}

extension [T](service: Stub[T])
inline def calls[Fun, Args <: Tuple, R](
inline select: T => Fun
)(using
TupledFunction[Fun, Args => R]
): List[UntupleOne[Args]] =
${ callsMacro[T, Fun, Args, R]('{ service }, '{ select }) }

inline def callsF[F[+_]: StubEffect.Mono, Fun, Args <: Tuple, R](
inline select: T => Fun
)(using
TupledFunction[Fun, Args => F[R]]
): F[List[UntupleOne[Args]]] =
summon[StubEffect.Mono[F]]
.unit(calls[Fun, Args, F[R]](select))

inline def callsIO[F[+_, +_]: StubEffect, Fun, Args <: Tuple, E, R](
inline select: T => Fun
)(using
TupledFunction[Fun, Args => F[E, R]]
): F[Nothing, List[UntupleOne[Args]]] =
summon[StubEffect[F]].unit(calls[Fun, Args, F[E, R]](select))

inline def times[F, Args <: Tuple, R](
inline select: T => F
): Int =
scala.compiletime.summonFrom {
case given NotGiven[TupledFunction[F, _]] =>
times0[F](select)

case tf: TupledFunction[F, args => r] =>
type Tupled[X] <: Tuple = X match
case head *: tail => head *: tail

calls[F, Tupled[args], r](select)(using
tf.asInstanceOf).size
}

inline def timesF[Fun, Args <: Tuple, R, F[+_]: StubEffect.Mono](
inline select: T => Fun
): F[Int] =
scala.compiletime.summonFrom {
case given NotGiven[TupledFunction[Fun, _]] =>
summon[StubEffect.Mono[F]].unit(times0[Fun](select))

case tf: TupledFunction[Fun, args => F[r]] =>
type Tupled[X] <: Tuple = X match
case head *: tail => head *: tail

summon[StubEffect.Mono[F]].unit(
calls[Fun, Tupled[args], F[r]](select)(using
tf.asInstanceOf).size
)
}

inline def timesIO[Fun, Args <: Tuple, R, F[+_, +_]: StubEffect](
inline select: T => Fun
): F[Nothing, Int] =
scala.compiletime.summonFrom {
case given NotGiven[TupledFunction[Fun, _]] =>
summon[StubEffect[F]].unit(times0[Fun](select))

case tf: TupledFunction[Fun, args => F[e, r]] =>
type Tupled[X] <: Tuple = X match
case head *: tail => head *: tail

summon[StubEffect[F]].unit(
calls[Fun, Tupled[args], F[e, r]](select)(using
tf.asInstanceOf).size
)
}

inline private def times0[F](inline select: T => F): Int =
${ times0Macro[T, F]('{ service }, '{ select }) }

inline private[backstub] def clear(): Unit =
${ clearMacro[T]('{ service }) }

private def times0Macro[T: Type, R: Type](
service: Expr[T],
select: Expr[T => R]
)(using
quotes: Quotes
): Expr[Int] =
new internal.Calls().times0[T, R](service, select)

private def callsMacro[T: Type, F: Type, Args <: Tuple: Type, R: Type](
service: Expr[T],
select: Expr[T => F]
)(using
quotes: Quotes
): Expr[List[UntupleOne[Args]]] =
new internal.Calls().calls[T, F, Args, R](service, select)

private def clearMacro[T: Type](
service: Expr[T]
)(using
quotes: Quotes
): Expr[Unit] =
new internal.Calls().clearAll[T](service)
Loading

0 comments on commit c132013

Please sign in to comment.