{"id":13566504,"url":"https://github.com/cornerman/sloth","last_synced_at":"2025-04-05T04:10:20.114Z","repository":{"id":26278938,"uuid":"107826859","full_name":"cornerman/sloth","owner":"cornerman","description":"Type safe RPC in scala","archived":false,"fork":false,"pushed_at":"2025-03-07T16:24:05.000Z","size":415,"stargazers_count":100,"open_issues_count":11,"forks_count":12,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-29T03:06:07.067Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cornerman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-10-22T00:47:27.000Z","updated_at":"2025-03-18T23:00:10.000Z","dependencies_parsed_at":"2023-02-16T20:30:36.170Z","dependency_job_id":"d9d9cdcf-307c-4259-b0eb-f8a3c0a810c7","html_url":"https://github.com/cornerman/sloth","commit_stats":null,"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cornerman%2Fsloth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cornerman%2Fsloth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cornerman%2Fsloth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cornerman%2Fsloth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cornerman","download_url":"https://codeload.github.com/cornerman/sloth/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247284949,"owners_count":20913704,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T13:02:10.939Z","updated_at":"2025-04-05T04:10:20.091Z","avatar_url":"https://github.com/cornerman.png","language":"Scala","funding_links":[],"categories":["Scala","开发框架"],"sub_categories":["RPC框架"],"readme":"# sloth :sloth:\n[![sloth Scala version support](https://index.scala-lang.org/cornerman/sloth/sloth/latest-by-scala-version.svg?platform=sjs1)](https://index.scala-lang.org/cornerman/sloth/sloth)\n[![sloth Scala version support](https://index.scala-lang.org/cornerman/sloth/sloth/latest-by-scala-version.svg?platform=jvm)](https://index.scala-lang.org/cornerman/sloth/sloth)\n\nType safe RPC in scala (scala 2 and scala 3)\n\nSloth is essentially a pair of macros (server and client) which takes an API definition in the form of a scala trait and then generates code for routing in the server as well as generating an API implementation in the client.\n\nThis library is inspired by [autowire](https://github.com/lihaoyi/autowire). Some differences:\n* No macro application on the call-site in the client (`.call()`), just one macro for creating an instance of an API trait\n* Return types of Api traits are not restricted to `Future`. You can use any higher-kinded generic return types:\n  - Server: `cats.Functor` (or `cats.data.Kleisli` with `cats.ApplicativeError`)\n  - Client: `cats.MonadError` (or `cats.data.Kleisli` with `cats.ApplicativeError`)\n\n## Get started\n\nGet latest release:\n```scala\nlibraryDependencies += \"com.github.cornerman\" %%% \"sloth\" % \"0.8.0\"\n```\n\nWe additonally publish snapshot releases for every commit.\n\n## Example usage\n\nDefine a trait as your `Api`:\n```scala\ntrait Api {\n    def fun(a: Int): Future[Int]\n}\n```\n\n### Server\n\nImplement your `Api`:\n```scala\nobject ApiImpl extends Api {\n    def fun(a: Int): Future[Int] = Future.successful(a + 1)\n}\n```\n\nDefine a router where we can use, e.g., [boopickle](https://github.com/suzaku-io/boopickle) for serializing the arguments and result of a method:\n```scala\nimport sloth._\nimport boopickle.Default._\nimport chameleon.ext.boopickle._\nimport java.nio.ByteBuffer\nimport cats.implicits._\n\nval router = Router[ByteBuffer, Future].route[Api](ApiImpl)\n```\n\nUse it to route requests to your Api implementation:\n```scala\nval result = router(Request[ByteBuffer](Method(traitName = \"Api\", methodName = \"fun\"), bytes))\n// Now result contains the serialized Int result returned by the method ApiImpl.fun\n```\n\n### Client\n\nGenerate an implementation for `Api` on the client side:\n```scala\nimport sloth._\nimport boopickle.Default._\nimport chameleon.ext.boopickle._\nimport java.nio.ByteBuffer\nimport cats.implicits._\n\nobject Transport extends RequestTransport[PickleType, Future] {\n    // implement the transport layer. this example just calls the router directly.\n    // in reality, the request would be sent over a connection.\n    override def apply(request: Request[PickleType]): Future[PickleType] =\n        router(request).toEither match {\n            case Right(result) =\u003e result\n            case Left(err) =\u003e Future.failed(new Exception(err.toString))\n        }\n}\n\nval client = Client[PickleType, Future](Transport)\nval api: Api = client.wire[Api]\n```\n\nMake requests to the server like normal method calls:\n```scala\napi.fun(1).foreach { num =\u003e\n  println(s\"Got response: $num\")\n}\n```\n\n## Additional features\n\n### Generic return type\n\nSometimes it can be useful to have a different return type on the server and client, you can do so by making your API generic:\n```scala\ntrait Api[F[_]] {\n    def fun(a: Int): F[String]\n}\n```\n\n#### Router\n\nIn your server, you can use any `cats.Functor` as `F`, for example:\n```scala\ntype ServerResult[T] = User =\u003e T\n\ntrait Api[F[_]] {\n    def fun(a: Int): F[String]\n}\n\nobject ApiImpl extends Api[ServerResult] {\n    def fun(a: Int): User =\u003e String = { user =\u003e\n        println(s\"User: $user\")\n        s\"Number: $a\"\n    }\n}\n\nval router = Router[ByteBuffer, ServerResult]\n    .route[Api[ServerResult]](ApiImpl)\n```\n\nIt is also possible to have a contravariant return type in your server. You can use `Kleisli` (or a plain function) with any `cats.ApplicativeError` that can capture a `Throwable` or `ServerFailure` (see `ServerFailureConvert` / `RouterContraHandler` for more customization):\n```scala\ntype ServerResult[T] = T =\u003e Either[ServerFailure, Unit]\n\ntrait Api[F[_]] {\n    def fun(a: Int): F[String]\n}\n\nobject ApiImpl extends Api[ServerResult] {\n    def fun(a: Int): String =\u003e Either[ServerFailure, Unit] = { string =\u003e\n        println(s\"Argument: $a\")\n        println(s\"Return: $string\")\n        Right(())\n    }\n}\n\nval router = Router.contra[ByteBuffer, ServerResult]\n    .route[Api[ServerResult]](ApiImpl)\n```\n\n#### Client\n\nIn your client, you can use any `cats.MonadError` that can capture a `Throwable` or `ClientFailure` (see `ClientFailureConvert` / `ClientHandler` for more customization):\n```scala\ntype ClientResult[T] = Either[ClientFailure, T]\n\nval client = Client[PickleType, ClientResult](Transport)\nval api: Api = client.wire[Api[ClientResult]]\n\napi.fun(1): Either[ClientFailure, String]\n```\n\nIt is also possible to have a contravariant return type in your client. You can use `Kleisli` (or a plain function) with any `cats.ApplicativeError` that can capture a `Throwable` or `ClientFailure` (see `ClientFailureConvert` / `ClientContraHandler` for more customization):\n```scala\ntype ClientResult[T] = T =\u003e Either[ClientFailure, Unit]\n\nval client = Client.contra[PickleType, ClientResult](Transport)\nval api: Api = client.wire[Api[ClientResult]]\n\napi.fun(1): String =\u003e Either[ClientFailure, Unit]\n```\n\n### Multiple routes\n\nIt is possible to have multiple APIs routed through the same router:\n```scala\nval router = Router[ByteBuffer, Future]\n    .route[Api](ApiImpl)\n    .route[OtherApi](OtherApiImpl)\n```\n\n### Router result\n\nThe router in the server returns an `Either[ServerFailure, Result[PickleType]]`, as the request can either fail or return the serialized result:\n```scala\nrouter(request) match {\n    case Right(result) =\u003e println(s\"Success: $result\")\n    case Left(error) =\u003e println(s\"Error: $error\")\n}\n```\n\n### Logging\n\nFor logging, you can define a `LogHandler`, which can log each request including the deserialized request and response.\n\nDefine it when creating the `Client`:\n```scala\nobject MyLogHandler extends LogHandler[ClientResult[_]] {\n  def logRequest[T](method: Method, argumentObject: Any, result: ClientResult[T]): ClientResult[T] = ???\n}\n\nval client = Client[PickleType, ClientResult](Transport, MyLogHandler)\n```\n\nDefine it when creating the `Router`:\n```scala\nobject MyLogHandler extends LogHandler[ServerResult[_]] {\n  def logRequest[T](method: Method, argumentObject: Any, result: ServerResult[T]): ServerResult[T] = ???\n}\n\nval router = Router[PickleType, ServerResult](MyLogHandler)\n```\n\n### Method overloading\n\nWhen overloading methods with different parameter lists, sloth cannot uniquely identify the method (because it is referenced with the trait name and the method name). Here you will need to provide a custom name:\n```scala\ntrait Api {\n    def fun(i: Int): F[Int]\n    @Name(\"funWithString\")\n    def fun(i: Int, s: String): F[Int]\n}\n```\n\n### Serialization\n\nFor serialization, we make use of the typeclasses provided by [chameleon](https://github.com/cornerman/chameleon). You can use existing libraries like circe, upickle, scodec or boopickle out of the box or define a serializer yourself (see the project readme). So you need a `Serializer` and `Deserializer` for each type you are using in the method signature of your API methods.\n\nIn the above examples, we used the type `ByteBuffer` to select the serialization method. We get implicit serializers/deserializers for `ByteBuffer` through the import `chameleon.ext.boopickle._`. Or you can use circe by providing the type `Json` (or String) and importing `chameleon.ext.circe._`. There are more available in chameleon.\n\n## How does it work\n\nSloth derives all information about an API from a scala trait. For example:\n```scala\n// @Name(\"traitName\")\ntrait Api {\n    // @Name(\"funName\")\n    def fun(a: Int, b: String)(c: Double): F[Int]\n}\n```\n\nFor each declared method in this trait (in this case `fun`):\n* Calculate method name: `Method(\"Api\", \"fun\")` (`Name` annotations on the trait or method are taken into account).\n* Serialize the method parameters as a tuple: `(a, b, c)`.\n\n### Server\n\nWhen calling `router.route[Api](impl)`, a macro generates a function that maps a method (trait-name + method-name) and the pickled arguments to a pickled result. This basically boils down to:\n\n```scala\n{ (method: sloth.Method) =\u003e\n  if (method.traitName = \"Api\") method.methodName match {\n    case \"fun\" =\u003e Some({ payload =\u003e\n        // deserialize payload\n        // call Api implementation impl with arguments\n        // return serialized response\n    })\n    case _ =\u003e None\n  } else None\n}\n```\n\n### Client\n\nWhen calling `client.wire[Api]`, a macro generates an instance of `Api` by implementing each method using the provided transport:\n\n```scala\nnew Api {\n    def fun(a: Int, b: String)(c: Double): F[Int] = {\n        // serialize arguments\n        // call RequestTransport transport with method and arguments\n        // return deserialized response\n    }\n}\n```\n\n## Integrations\n\n### http4s\n\nUse with:\n```\nlibraryDependencies += \"com.github.cornerman\" %%% \"sloth-http4s-server\" % \"0.8.0\"\nlibraryDependencies += \"com.github.cornerman\" %%% \"sloth-http4s-client\" % \"0.8.0\"\n```\n\nOn the server:\n```scala\nimport sloth.Router\nimport sloth.ext.http4s.server.HttpRpcRoutes\n\n// for usual rpc\nval router = Router[String, IO]\nval rpcRoutes: HttpRoutes[IO] = HttpRpcRoutes[String, IO](router)\n\n// for server sent event over rpc\nval router = Router[String, fs2.Stream[IO, *]]\nval rpcRoutes: HttpRoutes[IO] = HttpRpcRoutes.eventStream[IO](router)\n```\n\nIn the client:\n```scala\nimport sloth.Client\nimport sloth.ext.http4s.client.HttpRpcTransport\n\n// for usual rpc\nval client = Client[String, IO](HttpRpcTransport[String, IO])\nval api: MyApi[IO] = client.wire[MyApi[IO]]\n\n// for server sent events over rpc\nval client = Client[String, fs2.Stream[IO, *]](HttpRpcTransport.eventStream[IO])\nval api: MyApi[fs2.Stream[IO, *]] = client.wire[MyApi[fs2.Stream[IO, *]]]\n\n```\n\n### fetch (browser)\n\nThis is useful when running in the browser, because it will have a smaller bundle-size then using the http4s client.\n\nUse with:\n```\nlibraryDependencies += \"com.github.cornerman\" %%% \"sloth-jsdom-client\" % \"0.8.0\"\n```\n\nIn the client:\n```scala\nimport sloth.Client\nimport sloth.ext.jsdom.client.HttpRpcTransport\n\n// for usual rpc\nval client = Client[String, IO](HttpRpcTransport[IO])\nval api: MyApi[IO] = client.wire[MyApi[IO]]\n```\n\n## Experimental: Checksum for Apis\n\nCurrently scala-2 only.\n\nIn order to check the compatability of the client and server Api trait, you can calculate a checksum of your Api:\n\n```scala\nimport sloth.ChecksumCalculator._\n\ntrait Api {\n    def fun(s: String): Int\n}\n\nval checksum:Int = checksumOf[Api]\n\n```\n\nThe checksum of an Api trait is calculated from its *Name* and its *methods* (including names and types of parameters and result type).\n\n## Limitations\n\n* Type parameters on methods in the API trait are not supported.\n* All public methods in an API trait need to return the same higher kinded result type.\n* Your chosen serialization library needs to support serializing tuples, which are generated by the macro for the parameter lists of each method in the API trait. This is normally the case.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcornerman%2Fsloth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcornerman%2Fsloth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcornerman%2Fsloth/lists"}