Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 0 additions & 105 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3348,111 +3348,6 @@ val a: Int < Async =

Although multiple memoized functions can reuse the same `Cache`, each function operates as an isolated cache and doesn't share any values with others. Internally, cache entries include the instance of the function as part of the key to ensure this separation. Only the cache space is shared, allowing for efficient use of resources without compromising the independence of each function's cache.

### Requests: HTTP Client via Sttp

`Requests` provides a simplified API for [Sttp 3](https://github.com/softwaremill/sttp) implemented on top of Kyo's concurrent package.

To perform a request, use the `apply` method. It takes a builder function based on Sttp's request building API.

```scala
import kyo.*
import kyo.Requests.Backend
import sttp.client3.*

// Perform a request using a builder function
val a: String < (Async & Abort[FailedRequest]) =
Requests(_.get(uri"https://httpbin.org/get"))

// Alternatively, requests can be
// defined separately
val b: String < (Async & Abort[FailedRequest]) =
Requests.request(Requests.basicRequest.get(uri"https://httpbin.org/get"))

// It's possible to use the default implementation or provide
// a custom `Backend` via `let`

// An example request
val c: String < (Async & Abort[FailedRequest]) =
Requests(_.get(uri"https://httpbin.org/get"))

// Implementing a custom mock backend
val backend: Backend =
new Backend:
def send[T](r: Request[T, Any])(using Frame) =
Response.ok(Right("mocked")).asInstanceOf[Response[T]]

// Use the custom backend
val d: String < (Async & Abort[FailedRequest]) =
Requests.let(backend)(a)
```

Please refer to Sttp's documentation for details on how to build requests. Streaming is currently unsupported.

Users are free to use any JSON libraries supported by Sttp; however, [zio-json](https://github.com/zio/zio-json) is recommended, as it is used in Kyo's tests and modules requiring HTTP communication, such as `AIs`.

### Routes: HTTP Server via Tapir

`Routes` integrates with the Tapir library to help set up HTTP servers. The method `Routes.add` is used for adding routes. This method requires the definition of a route, which can be a Tapir Endpoint instance or a builder function. Additionally, the method requires the implementation of the endpoint, which is provided as the second parameter group. To start the server, the `Routes` effect is handled, which initializes the HTTP server with the specified routes.

```scala
import kyo.*
import sttp.tapir.*
import sttp.tapir.server.netty.*

// A simple health route using an endpoint builder
val a: Unit < Routes =
Routes.add(
_.get.in("health")
.out(stringBody)
) { _ =>
"ok"
}

// The endpoint can also be defined separately
val health2 = endpoint.get.in("health2").out(stringBody)

val b: Unit < Routes =
Routes.add(health2)(_ => "ok")

// Starting the server by handling the effect
val c: NettyKyoServerBinding < Async =
Routes.run(a.andThen(b))

// Alternatively, a customized server configuration can be used
val d: NettyKyoServerBinding < Async =
Routes.run(NettyKyoServer().port(9999))(a.andThen(b))
```

The parameters for Tapir's endpoint type are aligned with Kyo effects as follows:

`Endpoint[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, CAPABILITIES]`

This translates to the endpoint function format:

`INPUT => OUTPUT < (Env[SECURITY_INPUT] & Abort[ERROR_OUTPUT])`

Currently, the `CAPABILITIES` parameter is not supported in Kyo since streaming functionality is not available. An example of using these parameters is shown below:

```scala
import kyo.*
import sttp.model.*
import sttp.tapir.*

// An endpoint with an 'Int' path input and 'StatusCode' error output
val a: Unit < Routes =
Routes.add(
_.get.in("test" / path[Int]("id"))
.errorOut(statusCode)
.out(stringBody)
) { (id: Int) =>
if id == 42 then "ok"
else Abort.fail(StatusCode.NotFound)
// returns a 'String < Abort[StatusCode]'
}
```

For further examples, refer to TechEmpower's benchmark [subproject](https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/Scala/kyo-tapir) for a simple runnable demonstration, and Kyo's [example ledger service](https://github.com/getkyo/kyo/tree/main/kyo-examples/jvm/src/main/scala/examples/ledger) for practical applications of these concepts.

### ZIOs: Integration with ZIO

The `ZIOs` effect provides seamless integration between Kyo and the ZIO library. The effect is designed to enable gradual adoption of Kyo within a ZIO codebase. The integration properly suspends side effects and propagates fiber cancellations/interrupts between both libraries.
Expand Down
47 changes: 45 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ lazy val kyoJVM = project
`kyo-aeron`.jvm,
`kyo-sttp`.jvm,
`kyo-tapir`.jvm,
`kyo-http`.jvm,
`kyo-caliban`.jvm,
`kyo-bench`.jvm,
`kyo-zio-test`.jvm,
Expand Down Expand Up @@ -150,7 +151,8 @@ lazy val kyoJS = project
`kyo-zio`.js,
`kyo-cats`.js,
`kyo-combinators`.js,
`kyo-actor`.js
`kyo-actor`.js,
`kyo-http`.js
)

lazy val kyoNative = project
Expand All @@ -172,7 +174,8 @@ lazy val kyoNative = project
`kyo-direct`.native,
`kyo-combinators`.native,
`kyo-sttp`.native,
`kyo-actor`.native
`kyo-actor`.native,
`kyo-http`.native
)

lazy val `kyo-scheduler` =
Expand Down Expand Up @@ -530,6 +533,43 @@ lazy val `kyo-tapir` =
)
.jvmSettings(mimaCheck(false))

lazy val `kyo-http` =
crossProject(JSPlatform, JVMPlatform, NativePlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Full)
.in(file("kyo-http"))
.dependsOn(`kyo-core`)
.settings(
`kyo-settings`,
libraryDependencies += "dev.zio" %%% "zio-schema" % "1.6.4",
libraryDependencies += "dev.zio" %%% "zio-schema-json" % "1.6.4",
libraryDependencies += "dev.zio" %%% "zio-schema-derivation" % "1.6.4"
)
.jvmSettings(
mimaCheck(false),
libraryDependencies += "io.netty" % "netty-codec-http" % "4.2.1.Final",
libraryDependencies += "io.netty" % "netty-transport-native-epoll" % "4.2.1.Final" % Runtime classifier "linux-x86_64",
libraryDependencies += "io.netty" % "netty-transport-native-epoll" % "4.2.1.Final" % Runtime classifier "linux-aarch_64",
libraryDependencies += "io.netty" % "netty-transport-native-kqueue" % "4.2.1.Final" % Runtime classifier "osx-x86_64",
libraryDependencies += "io.netty" % "netty-transport-native-kqueue" % "4.2.1.Final" % Runtime classifier "osx-aarch_64"
)
.jsSettings(
`js-settings`,
libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.8.0",
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }
)
.nativeSettings(
`native-settings`,
nativeConfig ~= { c =>
import scala.sys.process.*
val h2oCompileFlags = try "pkg-config --cflags libh2o-evloop".!!.trim.split("\\s+").toSeq catch { case _: Exception => Seq.empty }
val h2oLinkFlags = try "pkg-config --libs libh2o-evloop".!!.trim.split("\\s+").toSeq catch { case _: Exception => Seq("-lh2o-evloop") }
val curlLinkFlags = try "pkg-config --libs libcurl".!!.trim.split("\\s+").toSeq catch { case _: Exception => Seq("-lcurl") }
c.withCompileOptions(c.compileOptions ++ Seq("-DH2O_USE_LIBUV=0") ++ h2oCompileFlags)
.withLinkingOptions(c.linkingOptions ++ curlLinkFlags ++ h2oLinkFlags)
}
)

lazy val `kyo-caliban` =
crossProject(JVMPlatform)
.withoutSuffixFor(JVMPlatform)
Expand Down Expand Up @@ -651,6 +691,8 @@ lazy val `kyo-bench` =
.dependsOn(`kyo-core`)
.dependsOn(`kyo-parse`)
.dependsOn(`kyo-sttp`)
.dependsOn(`kyo-tapir`)
.dependsOn(`kyo-http`)
.dependsOn(`kyo-stm`)
.dependsOn(`kyo-direct`)
.dependsOn(`kyo-scheduler-zio`)
Expand Down Expand Up @@ -697,6 +739,7 @@ lazy val `kyo-bench` =
libraryDependencies += "dev.zio" %% "zio-prelude" % "1.0.0-RC45",
libraryDependencies += "co.fs2" %% "fs2-core" % "3.12.2",
libraryDependencies += "org.http4s" %% "http4s-ember-client" % "1.0.0-M44",
libraryDependencies += "org.http4s" %% "http4s-ember-server" % "1.0.0-M44",
libraryDependencies += "org.http4s" %% "http4s-dsl" % "1.0.0-M44",
libraryDependencies += "dev.zio" %% "zio-http" % "3.8.0",
libraryDependencies += "io.vertx" % "vertx-core" % "5.0.7",
Expand Down
57 changes: 57 additions & 0 deletions kyo-data/shared/src/main/scala/kyo/Fields.scala
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,61 @@ object Fields:
${ internal.FieldsMacros.sameNamesImpl[A, B] }
end SameNames

/** Prevents Scala from merging field names when chaining field definitions.
*
* Because `~` is contravariant in its name parameter, Scala normalizes intersections like `"name" ~ Int & "age" ~ Int` into
* `("name" | "age") ~ Int`, collapsing distinct fields into a single union. Requiring `using Pin[N]` pins each field name during
* inference, keeping them as separate intersection members. The runtime value is just `()` — this is purely a compile-time mechanism.
*
* {{{
* // Without Pin — chaining merges names: "name" ~ A & "age" ~ B becomes ("name" | "age") ~ (A & B)
* def field[N <: String & Singleton](name: N): Def[In & N ~ Int]
*
* // With Pin — each field name is preserved: "name" ~ A & "age" ~ B stays separate
* def field[N <: String & Singleton](name: N)(using Pin[N]): Def[In & N ~ Int]
* }}}
*
* Note: both `Pin` and `Exact` can be replaced by `Precise` (from `scala.language.experimental.modularity`) once it becomes stable.
* `Precise` prevents type widening directly on the type parameter:
* {{{
* def field[N <: String & Singleton : Precise](name: N): Def[In & N ~ Int]
* }}}
*/
private object Pin:
opaque type Pin[+N <: String] = Unit
given [N <: String]: Pin[N] = ()
end Pin
export Pin.*

/** Decomposes a function's return type into a type constructor and its field types, preserving field information that Scala would
* otherwise widen.
*
* With a direct approach like `def modify[A >: Fields](f: Def[Fields] => Def[A])`, Scala widens `A` to the lower bound through the
* lambda, losing field types. `Exact` avoids this by first inferring `R` as the full return type (with no bound forcing widening),
* then extracting the field types.
*
* {{{
* // Without Exact — A is widened to Any, field types are lost
* def modify[A >: Fields](f: Def[Fields] => Def[A]): Record[A]
*
* // With Exact — R preserves the full type, field types are extracted as s.Out
* def modify[R](f: Def[Fields] => R)(using s: Exact[Def, R]): Record[s.Out]
* }}}
*
* Note: both `Pin` and `Exact` can be replaced by `Precise` (from `scala.language.experimental.modularity`) once it becomes stable.
* `Precise` prevents the widening directly, removing the need to decompose `R`:
* {{{
* def modify[A : Precise](f: Def[Fields] => Def[A]): Record[A]
* }}}
*/
sealed trait Exact[F[_], R]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sealed trait Exact[F[_], R]:
sealed abstract class Exact[F[_], R]:

type Out
def apply(r: R): F[Out]

object Exact:
given [F[_], A]: Exact[F, F[A]] with
type Out = A
def apply(r: F[A]): F[A] = r
end Exact

end Fields
Loading
Loading