From efd0976cd90df8dd5f0ef7fa17956a46da37e1b8 Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Fri, 13 Sep 2024 07:58:54 -0700 Subject: [PATCH] integrations: scaladocs (#642) --- .../shared/src/main/scala/kyo/Cache.scala | 143 ++++++++++++++++++ .../src/main/scala/kyo/Resolvers.scala | 75 +++++++++ .../scala/kyo/{direct.scala => Direct.scala} | 27 ++++ .../shared/src/main/scala/kyo/Requests.scala | 58 +++++++ .../scala/kyo/{routes.scala => Routes.scala} | 43 ++++++ kyo-zio/shared/src/main/scala/kyo/ZIOs.scala | 51 +++++++ .../kyo/{ziosTest.scala => ZIOsTest.scala} | 0 7 files changed, 397 insertions(+) rename kyo-direct/shared/src/main/scala/kyo/{direct.scala => Direct.scala} (69%) rename kyo-tapir/shared/src/main/scala/kyo/{routes.scala => Routes.scala} (58%) rename kyo-zio/shared/src/test/scala/kyo/{ziosTest.scala => ZIOsTest.scala} (100%) diff --git a/kyo-cache/shared/src/main/scala/kyo/Cache.scala b/kyo-cache/shared/src/main/scala/kyo/Cache.scala index 30b8b0bd0..249f92985 100644 --- a/kyo-cache/shared/src/main/scala/kyo/Cache.scala +++ b/kyo-cache/shared/src/main/scala/kyo/Cache.scala @@ -5,7 +5,30 @@ import com.github.benmanes.caffeine import com.github.benmanes.caffeine.cache.Caffeine import java.util.concurrent.TimeUnit +/** A caching utility class that provides memoization functionality via Caffeine. + * + * Each memoized function created by this cache has its own isolated keyspace. This means that different memoized functions, even if they + * have the same input types, will not interfere with each other's cached results. The isolation is achieved by including a reference to + * the cache instance itself in the key, along with the input value. + * + * @param store + * The underlying cache store + */ class Cache(private[kyo] val store: Store): + + /** Memoizes a function with a single argument. + * + * @tparam A + * The input type + * @tparam B + * The output type (must have a Flat instance) + * @tparam S + * The effect type + * @param f + * The function to memoize + * @return + * A memoized version of the input function + */ def memo[A, B: Flat, S]( f: A => B < S )(using Frame): A => B < (Async & S) = @@ -39,6 +62,21 @@ class Cache(private[kyo] val store: Store): } } + /** Memoizes a function with two arguments. + * + * @tparam T1 + * The first input type + * @tparam T2 + * The second input type + * @tparam S + * The effect type + * @tparam B + * The output type (must have a Flat instance) + * @param f + * The function to memoize + * @return + * A memoized version of the input function + */ def memo2[T1, T2, S, B: Flat]( f: (T1, T2) => B < S )(using Frame): (T1, T2) => B < (Async & S) = @@ -46,6 +84,23 @@ class Cache(private[kyo] val store: Store): (t1, t2) => m((t1, t2)) end memo2 + /** Memoizes a function with three arguments. + * + * @tparam T1 + * The first input type + * @tparam T2 + * The second input type + * @tparam T3 + * The third input type + * @tparam S + * The effect type + * @tparam B + * The output type (must have a Flat instance) + * @param f + * The function to memoize + * @return + * A memoized version of the input function + */ def memo3[T1, T2, T3, S, B: Flat]( f: (T1, T2, T3) => B < S )(using Frame): (T1, T2, T3) => B < (Async & S) = @@ -53,6 +108,25 @@ class Cache(private[kyo] val store: Store): (t1, t2, t3) => m((t1, t2, t3)) end memo3 + /** Memoizes a function with four arguments. + * + * @tparam T1 + * The first input type + * @tparam T2 + * The second input type + * @tparam T3 + * The third input type + * @tparam T4 + * The fourth input type + * @tparam S + * The effect type + * @tparam B + * The output type (must have a Flat instance) + * @param f + * The function to memoize + * @return + * A memoized version of the input function + */ def memo4[T1, T2, T3, T4, S, B: Flat]( f: (T1, T2, T3, T4) => B < S )(using Frame): (T1, T2, T3, T4) => B < (Async & S) = @@ -62,25 +136,94 @@ class Cache(private[kyo] val store: Store): end Cache object Cache: + + /** The type of the underlying cache store. */ type Store = caffeine.cache.Cache[Any, Promise[Nothing, Any]] + /** A builder class for configuring Cache instances. + * + * @param b + * The underlying Caffeine builder + */ case class Builder(private[kyo] val b: Caffeine[Any, Any]) extends AnyVal: + /** Sets the maximum size of the cache. + * + * @param v + * The maximum number of entries the cache may contain + * @return + * An updated Builder + */ def maxSize(v: Int): Builder = copy(b.maximumSize(v)) + + /** Specifies that cache should use weak references for keys. + * + * @return + * An updated Builder + */ def weakKeys(): Builder = copy(b.weakKeys()) + + /** Specifies that cache should use weak references for values. + * + * @return + * An updated Builder + */ def weakValues(): Builder = copy(b.weakValues()) + + /** Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry's + * creation, or the most recent replacement of its value. + * + * @param d + * The length of time after an entry is created that it should be automatically removed + * @return + * An updated Builder + */ def expireAfterAccess(d: Duration): Builder = copy(b.expireAfterAccess(d.toMillis, TimeUnit.MILLISECONDS)) + + /** Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry's + * creation, the most recent replacement of its value, or its last access. + * + * @param d + * The length of time after an entry is last accessed that it should be automatically removed + * @return + * An updated Builder + */ def expireAfterWrite(d: Duration): Builder = copy(b.expireAfterWrite(d.toMillis, TimeUnit.MILLISECONDS)) + + /** Sets the minimum total size for the internal data structures. + * + * @param v + * The minimum total size for the internal data structures + * @return + * An updated Builder + */ def initialCapacity(v: Int) = copy(b.initialCapacity(v)) + + /** Specifies that active entries are eligible for automatic refresh once a fixed duration has elapsed after the entry's creation, + * or the most recent replacement of its value. + * + * @param d + * The duration after which an entry should be considered stale + * @return + * An updated Builder + */ def refreshAfterWrite(d: Duration) = copy(b.refreshAfterWrite(d.toMillis, TimeUnit.MILLISECONDS)) + end Builder + /** Initializes a new Cache instance with the given configuration. + * + * @param f + * A function that configures the Cache using a Builder + * @return + * A new Cache instance wrapped in an IO effect + */ def init(f: Builder => Builder)(using Frame): Cache < IO = IO { new Cache( diff --git a/kyo-caliban/src/main/scala/kyo/Resolvers.scala b/kyo-caliban/src/main/scala/kyo/Resolvers.scala index 6e5c8e697..4d7945d6c 100644 --- a/kyo-caliban/src/main/scala/kyo/Resolvers.scala +++ b/kyo-caliban/src/main/scala/kyo/Resolvers.scala @@ -19,6 +19,7 @@ import zio.ZEnvironment import zio.ZIO import zio.stream.ZStream +/** Effect for interacting with Caliban GraphQL resolvers. */ opaque type Resolvers <: (Abort[CalibanError] & ZIOs) = Abort[CalibanError] & ZIOs object Resolvers: @@ -26,16 +27,49 @@ object Resolvers: private given StreamConstructor[Nothing] = (_: ZStream[Any, Throwable, Byte]) => throw new Throwable("Streaming is not supported") + /** Runs a GraphQL server with default NettyKyoServer configuration. + * + * @param v + * The HttpInterpreter to be used + * @param Frame + * Implicit Frame parameter + * @return + * A NettyKyoServerBinding wrapped in ZIOs and Abort effects + */ def run[A, S](v: HttpInterpreter[Any, CalibanError] < (Resolvers & S))( using Frame ): NettyKyoServerBinding < (ZIOs & Abort[CalibanError] & S) = run[A, S](NettyKyoServer())(v) + /** Runs a GraphQL server with a custom NettyKyoServer configuration. + * + * @param server + * The custom NettyKyoServer configuration + * @param v + * The HttpInterpreter to be used + * @param Frame + * Implicit Frame parameter + * @return + * A NettyKyoServerBinding wrapped in ZIOs and Abort effects + */ def run[A, S](server: NettyKyoServer)(v: HttpInterpreter[Any, CalibanError] < (Resolvers & S))( using Frame ): NettyKyoServerBinding < (ZIOs & Abort[CalibanError] & S) = ZIOs.get(ZIO.runtime[Any]).map(runtime => run(server, runtime)(v)) + /** Runs a GraphQL server with a custom Runner. + * + * @param runner + * The custom Runner to be used + * @param v + * The HttpInterpreter to be used + * @param tag + * Implicit Tag for Runner[R] + * @param frame + * Implicit Frame parameter + * @return + * A NettyKyoServerBinding wrapped in ZIOs and Abort effects + */ def run[R, A, S](runner: Runner[R])(v: HttpInterpreter[Runner[R], CalibanError] < (Resolvers & S))( using tag: Tag[Runner[R]], @@ -43,6 +77,21 @@ object Resolvers: ): NettyKyoServerBinding < (ZIOs & Abort[CalibanError] & S) = run[R, A, S](NettyKyoServer(), runner)(v) + /** Runs a GraphQL server with a custom NettyKyoServer configuration and Runner. + * + * @param server + * The custom NettyKyoServer configuration + * @param runner + * The custom Runner to be used + * @param v + * The HttpInterpreter to be used + * @param tag + * Implicit Tag for Runner[R] + * @param frame + * Implicit Frame parameter + * @return + * A NettyKyoServerBinding wrapped in ZIOs and Abort effects + */ def run[R, A, S](server: NettyKyoServer, runner: Runner[R])(v: HttpInterpreter[Runner[R], CalibanError] < (Resolvers & S))( using tag: Tag[Runner[R]], @@ -50,6 +99,19 @@ object Resolvers: ): NettyKyoServerBinding < (ZIOs & Abort[CalibanError] & S) = ZIOs.get(ZIO.runtime[Any]).map(runtime => run(server, runtime.withEnvironment(ZEnvironment(runner)))(v)) + /** Runs a GraphQL server with a custom NettyKyoServer configuration and Runtime. + * + * @param server + * The custom NettyKyoServer configuration + * @param runtime + * The custom Runtime to be used + * @param v + * The HttpInterpreter to be used + * @param Frame + * Implicit Frame parameter + * @return + * A NettyKyoServerBinding wrapped in ZIOs and Abort effects + */ def run[R, A, S]( server: NettyKyoServer, runtime: Runtime[R] @@ -60,6 +122,19 @@ object Resolvers: bindings <- IO(server.addEndpoints(endpoints).start()) yield bindings + /** Creates an HttpInterpreter from a GraphQL API. + * + * @param api + * The GraphQL API to be interpreted + * @param requestCodec + * Implicit JsonCodec for GraphQLRequest + * @param responseValueCodec + * Implicit JsonCodec for ResponseValue + * @param Frame + * Implicit Frame parameter + * @return + * An HttpInterpreter wrapped in Resolvers effect + */ def get[R](api: GraphQL[R])(using requestCodec: JsonCodec[GraphQLRequest], responseValueCodec: JsonCodec[ResponseValue] diff --git a/kyo-direct/shared/src/main/scala/kyo/direct.scala b/kyo-direct/shared/src/main/scala/kyo/Direct.scala similarity index 69% rename from kyo-direct/shared/src/main/scala/kyo/direct.scala rename to kyo-direct/shared/src/main/scala/kyo/Direct.scala index 034837bcb..7d9f56bed 100644 --- a/kyo-direct/shared/src/main/scala/kyo/direct.scala +++ b/kyo-direct/shared/src/main/scala/kyo/Direct.scala @@ -7,8 +7,35 @@ import directInternal.* import scala.annotation.tailrec import scala.quoted.* +/** Defers the execution of a block of code, allowing the use of `await` within it. + * + * This macro transforms the given block of code to work with effectful computations, enabling the use of `await` to handle various types + * of effects. The `defer` block is desugared into regular monadic composition using `map`, making it equivalent to writing out the monadic + * code explicitly. + * + * @tparam A + * The type of the value returned by the deferred block + * @param f + * The block of code to be deferred + * @return + * A value of type `A < S`, where `S` represents the combined effects of all `await` calls + */ transparent inline def defer[A](inline f: A) = ${ impl[A]('f) } +/** Awaits the result of an effectful computation. + * + * This function can only be used within a `defer` block. It suspends the execution of the current block until the result of the effectful + * computation is available. In the desugared monadic composition, `await` calls are transformed into appropriate `map` operations. + * + * @tparam A + * The type of the value being awaited + * @tparam S + * The type of the effect (e.g., IO, Abort, Choice, etc.) + * @param v + * The effectful computation to await + * @return + * The result of type A, once the computation completes + */ inline def await[A, S](v: A < S): A = compiletime.error("`await` must be used within a `defer` block") diff --git a/kyo-sttp/shared/src/main/scala/kyo/Requests.scala b/kyo-sttp/shared/src/main/scala/kyo/Requests.scala index a643eb062..c2106f11a 100644 --- a/kyo-sttp/shared/src/main/scala/kyo/Requests.scala +++ b/kyo-sttp/shared/src/main/scala/kyo/Requests.scala @@ -2,38 +2,96 @@ package kyo import sttp.client3.* +/** Represents a failed HTTP request. + * + * This exception is thrown when an HTTP request fails for any reason. + * + * @param cause + * A string describing the reason for the failure + */ case class FailedRequest(cause: String) extends Exception object Requests: + /** Abstract class representing a backend for sending HTTP requests */ abstract class Backend: self => + /** Sends an HTTP request + * + * @tparam A + * The type of the response body + * @param r + * The request to send + * @return + * The response wrapped in an effect + */ def send[A](r: Request[A, Any]): Response[A] < Async + /** Wraps the Backend with a meter + * + * @param m + * The meter to use + * @return + * A new Backend that uses the meter + */ def withMeter(m: Meter)(using Frame): Backend = new Backend: def send[A](r: Request[A, Any]) = m.run(self.send(r)) end Backend + /** The default live backend implementation */ val live: Backend = PlatformBackend.default private val local = Local.init[Backend](live) + /** Executes an effect with a custom backend + * + * @param b + * The backend to use + * @param v + * The effect to execute + * @return + * The result of the effect + */ def let[A, S](b: Backend)(v: A < S)(using Frame): A < (Async & S) = local.let(b)(v) + /** Type alias for a basic request */ type BasicRequest = RequestT[Empty, Either[FailedRequest, String], Any] + /** A basic request with error handling */ val basicRequest: BasicRequest = sttp.client3.basicRequest.mapResponse { case Left(value) => Left(FailedRequest(value)) case Right(value) => Right(value) } + /** Sends an HTTP request using a function to modify the basic request + * + * @tparam E + * The error type + * @tparam A + * The success type of the response + * @param f + * A function that takes a BasicRequest and returns a Request + * @return + * The response body wrapped in an effect + */ def apply[E, A](f: BasicRequest => Request[Either[E, A], Any])(using Frame): A < (Async & Abort[E]) = request(f(basicRequest)) + /** Sends an HTTP request + * + * @tparam E + * The error type + * @tparam A + * The success type of the response + * @param req + * The request to send + * @return + * The response body wrapped in an effect + */ def request[E, A](req: Request[Either[E, A], Any])(using Frame): A < (Async & Abort[E]) = local.use(_.send(req)).map { r => Abort.get(r.body) diff --git a/kyo-tapir/shared/src/main/scala/kyo/routes.scala b/kyo-tapir/shared/src/main/scala/kyo/Routes.scala similarity index 58% rename from kyo-tapir/shared/src/main/scala/kyo/routes.scala rename to kyo-tapir/shared/src/main/scala/kyo/Routes.scala index 255eac001..367398b5c 100644 --- a/kyo-tapir/shared/src/main/scala/kyo/routes.scala +++ b/kyo-tapir/shared/src/main/scala/kyo/Routes.scala @@ -8,21 +8,48 @@ import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.netty.NettyKyoServer import sttp.tapir.server.netty.NettyKyoServerBinding +/** Represents a single route with a server endpoint. */ case class Route(endpoint: ServerEndpoint[Any, KyoSttpMonad.M]) extends AnyVal +/** Represents an effectful collection of routes with asynchronous capabilities. */ opaque type Routes <: (Emit[Route] & Async) = Emit[Route] & Async object Routes: + /** Runs the routes using the default NettyKyoServer. + * + * @param v + * The routes to run + * @return + * A NettyKyoServerBinding wrapped in an asynchronous effect + */ def run[A, S](v: Unit < (Routes & S))(using Frame): NettyKyoServerBinding < (Async & S) = run[A, S](NettyKyoServer())(v) + /** Runs the routes using a specified NettyKyoServer. + * + * @param server + * The NettyKyoServer to use + * @param v + * The routes to run + * @return + * A NettyKyoServerBinding wrapped in an asynchronous effect + */ def run[A, S](server: NettyKyoServer)(v: Unit < (Routes & S))(using Frame): NettyKyoServerBinding < (Async & S) = Emit.run[Route].apply[Unit, Async & S](v).map { (routes, _) => IO(server.addEndpoints(routes.toSeq.map(_.endpoint).toList).start()): NettyKyoServerBinding < (Async & S) } end run + /** Adds a new route to the collection. + * + * @param e + * The endpoint to add + * @param f + * The function to handle the endpoint logic + * @return + * Unit wrapped in Routes effect + */ def add[A: Tag, I, E: Tag: ClassTag, O: Flat](e: Endpoint[A, I, E, O, Any])( f: I => O < (Async & Env[A] & Abort[E]) )(using Frame): Unit < Routes = @@ -39,6 +66,15 @@ object Routes: ) ).unit + /** Adds a new route to the collection, starting from a PublicEndpoint. + * + * @param e + * A function to create an Endpoint from a PublicEndpoint + * @param f + * The function to handle the endpoint logic + * @return + * Unit wrapped in Routes effect + */ def add[A: Tag, I, E: Tag: ClassTag, O: Flat]( e: PublicEndpoint[Unit, Unit, Unit, Any] => Endpoint[A, I, E, O, Any] )( @@ -46,6 +82,13 @@ object Routes: )(using Frame): Unit < Routes = add(e(endpoint))(f) + /** Collects multiple route initializations into a single Routes effect. + * + * @param init + * A sequence of route initializations + * @return + * Unit wrapped in Routes effect + */ def collect(init: (Unit < Routes)*)(using Frame): Unit < Routes = Kyo.collect(init).unit diff --git a/kyo-zio/shared/src/main/scala/kyo/ZIOs.scala b/kyo-zio/shared/src/main/scala/kyo/ZIOs.scala index 412544683..a338e850d 100644 --- a/kyo-zio/shared/src/main/scala/kyo/ZIOs.scala +++ b/kyo-zio/shared/src/main/scala/kyo/ZIOs.scala @@ -6,20 +6,71 @@ import scala.util.control.NonFatal import zio.Exit import zio.ZIO +/** Effect to integrate ZIO with Kyo */ opaque type ZIOs <: Async = GetZIO & Async object ZIOs: + /** Executes a ZIO effect and returns its result within the Kyo effect system. + * + * @param v + * The ZIO effect to execute + * @param ev + * Evidence that E is a subtype of Nothing + * @param tag + * Tag for E + * @param frame + * Implicit Frame + * @tparam E + * The error type (must be a subtype of Nothing) + * @tparam A + * The result type + * @return + * A Kyo effect that will produce A or abort with E + */ def get[E >: Nothing: Tag, A](v: ZIO[Any, E, A])(using Frame): A < (Abort[E] & ZIOs) = val task = v.fold(Result.fail, Result.success) ArrowEffect.suspendMap(Tag[GetZIO], task)(Abort.get(_)) + /** Executes a ZIO effect that cannot fail and returns its result within the Kyo effect system. + * + * @param v + * The ZIO effect to execute + * @param frame + * Implicit Frame + * @tparam A + * The result type + * @return + * A Kyo effect that will produce A + */ def get[A](v: ZIO[Any, Nothing, A])(using Frame): A < ZIOs = ArrowEffect.suspend(Tag[GetZIO], v) + /** Placeholder for ZIO effects with environments (currently not supported). + * + * @tparam R + * The environment type + * @tparam E + * The error type + * @tparam A + * The result type + */ inline def get[R: zio.Tag, E, A](v: ZIO[R, E, A])(using Tag[Env[R]], Frame): A < (Env[R] & ZIOs) = compiletime.error("ZIO environments are not supported yet. Please handle them before calling this method.") + /** Runs a Kyo effect within the ZIO runtime. + * + * @param v + * The Kyo effect to run + * @param frame + * Implicit Frame + * @tparam E + * The error type + * @tparam A + * The result type + * @return + * A ZIO effect that will produce A or fail with E + */ def run[E, A](v: => A < (Abort[E] & ZIOs))(using frame: Frame): ZIO[Any, E, A] = ZIO.suspendSucceed { try diff --git a/kyo-zio/shared/src/test/scala/kyo/ziosTest.scala b/kyo-zio/shared/src/test/scala/kyo/ZIOsTest.scala similarity index 100% rename from kyo-zio/shared/src/test/scala/kyo/ziosTest.scala rename to kyo-zio/shared/src/test/scala/kyo/ZIOsTest.scala