├── .github └── workflows │ └── build.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sc ├── circe ├── src │ └── poppet │ │ └── codec │ │ └── circe │ │ ├── all │ │ └── package.scala │ │ └── instances │ │ ├── CirceCodecInstances.scala │ │ └── package.scala └── test │ └── src │ └── poppet │ └── codec │ └── circe │ └── CirceCodecSpec.scala ├── core ├── src-2 │ └── poppet │ │ ├── consumer │ │ └── core │ │ │ └── ConsumerProcessorObjectBinCompat.scala │ │ ├── core │ │ └── ProcessorMacro.scala │ │ └── provider │ │ └── core │ │ └── ProviderProcessorObjectBinCompat.scala ├── src-3 │ └── poppet │ │ ├── consumer │ │ └── core │ │ │ └── ConsumerProcessorObjectBinCompat.scala │ │ ├── core │ │ └── ProcessorMacro.scala │ │ └── provider │ │ └── core │ │ └── ProviderProcessorObjectBinCompat.scala ├── src │ └── poppet │ │ ├── CoreDsl.scala │ │ ├── consumer │ │ ├── ConsumerDsl.scala │ │ ├── all │ │ │ └── all.scala │ │ ├── core │ │ │ ├── Consumer.scala │ │ │ └── ConsumerProcessor.scala │ │ └── package.scala │ │ ├── core │ │ ├── Codec.scala │ │ ├── CodecK.scala │ │ ├── Failure.scala │ │ ├── FailureHandler.scala │ │ ├── Request.scala │ │ └── Response.scala │ │ ├── package.scala │ │ └── provider │ │ ├── ProviderDsl.scala │ │ ├── all │ │ └── package.scala │ │ ├── core │ │ ├── Provider.scala │ │ └── ProviderProcessor.scala │ │ └── package.scala └── test │ └── src │ └── poppet │ ├── PoppetSpec.scala │ ├── codec │ └── CodecSpec.scala │ ├── consumer │ ├── ConsumerProcessorSpec.scala │ └── ConsumerSpec.scala │ ├── core │ └── ProcessorSpec.scala │ └── provider │ ├── ProviderProcessorSpec.scala │ └── ProviderSpec.scala ├── example ├── http4s-circe │ ├── api │ │ └── src │ │ │ └── poppet │ │ │ └── example │ │ │ └── http4s │ │ │ ├── model │ │ │ ├── User.scala │ │ │ └── package.scala │ │ │ ├── poppet │ │ │ └── package.scala │ │ │ └── service │ │ │ └── UserService.scala │ ├── consumer │ │ └── src │ │ │ └── poppet │ │ │ └── example │ │ │ └── http4s │ │ │ └── consumer │ │ │ ├── Application.scala │ │ │ ├── Config.scala │ │ │ ├── api │ │ │ └── UserApi.scala │ │ │ └── service │ │ │ └── UserServiceProvider.scala │ └── provider │ │ └── src │ │ └── poppet │ │ └── example │ │ └── http4s │ │ └── provider │ │ ├── Application.scala │ │ ├── api │ │ └── ProviderApi.scala │ │ └── service │ │ └── UserInternalService.scala ├── play │ ├── api │ │ └── src │ │ │ └── poppet │ │ │ └── example │ │ │ └── play │ │ │ ├── model │ │ │ └── User.scala │ │ │ └── service │ │ │ └── UserService.scala │ ├── consumer │ │ ├── app │ │ │ └── poppet │ │ │ │ └── example │ │ │ │ └── play │ │ │ │ ├── controller │ │ │ │ └── UserController.scala │ │ │ │ ├── module │ │ │ │ └── CustomModule.scala │ │ │ │ └── service │ │ │ │ └── UserServiceProvider.scala │ │ └── conf │ │ │ ├── application.conf │ │ │ └── routes │ └── provider │ │ ├── app │ │ └── poppet │ │ │ └── example │ │ │ └── play │ │ │ ├── controller │ │ │ └── ProviderController.scala │ │ │ ├── module │ │ │ └── CustomModule.scala │ │ │ └── service │ │ │ └── UserInternalService.scala │ │ └── conf │ │ ├── application.conf │ │ └── routes ├── spring-jackson │ ├── api │ │ └── src │ │ │ └── poppet │ │ │ └── example │ │ │ └── spring │ │ │ ├── model │ │ │ └── User.java │ │ │ └── service │ │ │ └── UserService.java │ ├── consumer │ │ ├── resources │ │ │ └── application.yml │ │ └── src │ │ │ └── poppet │ │ │ └── example │ │ │ └── spring │ │ │ └── consumer │ │ │ ├── Application.java │ │ │ ├── controller │ │ │ └── UserController.java │ │ │ └── service │ │ │ └── UserServiceProvider.scala │ └── provider │ │ ├── resources │ │ └── application.yml │ │ └── src │ │ └── poppet │ │ └── example │ │ └── spring │ │ └── provider │ │ ├── Application.java │ │ ├── controller │ │ └── ProviderController.java │ │ └── service │ │ ├── ProviderGenerator.scala │ │ └── UserInternalService.java └── tapir-sttp-fs2-circe │ ├── api │ └── src │ │ └── poppet │ │ └── example │ │ └── tapir │ │ ├── Util.scala │ │ ├── model │ │ ├── CustomCodecs.scala │ │ ├── PoppetArgumentEvent.scala │ │ ├── PoppetRequestInitEvent.scala │ │ ├── PoppetResponseInitEvent.scala │ │ ├── PoppetResultEvent.scala │ │ └── User.scala │ │ └── service │ │ └── UserService.scala │ ├── consumer │ └── src │ │ └── poppet │ │ └── example │ │ └── tapir │ │ └── consumer │ │ ├── Application.scala │ │ ├── Config.scala │ │ ├── api │ │ └── UserApi.scala │ │ └── service │ │ └── UserServiceProvider.scala │ └── provider │ └── src │ └── poppet │ └── example │ └── tapir │ └── provider │ ├── Application.scala │ ├── api │ └── ProviderApi.scala │ └── service │ └── UserInternalService.scala ├── jackson ├── src-2 │ └── poppet │ │ └── codec │ │ └── jackson │ │ └── instances │ │ └── JacksonCodecInstancesBinCompat.scala ├── src-3 │ └── poppet │ │ └── codec │ │ └── jackson │ │ └── instances │ │ └── JacksonCodecInstancesBinCompat.scala ├── src │ └── poppet │ │ └── codec │ │ └── jackson │ │ ├── all │ │ └── package.scala │ │ └── instances │ │ ├── JacksonCodecInstances.scala │ │ └── package.scala └── test │ └── src │ └── poppet │ └── codec │ └── jackson │ └── JacksonCodecSpec.scala ├── mill ├── play-json ├── src │ └── poppet │ │ └── codec │ │ └── play │ │ ├── all │ │ └── package.scala │ │ └── instances │ │ ├── PlayJsonCodecInstances.scala │ │ └── package.scala └── test │ └── src │ └── poppet │ └── codec │ └── play │ └── PlayJsonCodecSpec.scala └── upickle ├── src └── poppet │ └── codec │ └── upickle │ ├── binary │ ├── all │ │ └── package.scala │ └── instances │ │ ├── UpickleBinaryCodecInstances.scala │ │ └── package.scala │ └── json │ ├── all │ └── package.scala │ └── instances │ ├── UpickleJsonCodecInstances.scala │ └── package.scala └── test └── src └── poppet └── codec └── upickle ├── binary └── UpickleBinaryCodecSpec.scala └── json └── UpickleJsonCodecSpec.scala /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | jobs: 9 | core: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: coursier/cache-action@v6 14 | - uses: actions/setup-java@v2 15 | with: 16 | distribution: 'temurin' 17 | java-version: '8' 18 | - run: ./mill __.__.test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | .idea 3 | .bsp 4 | RUNNING_PID -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.7.12 2 | runner.dialect = scala3 3 | maxColumn = 120 4 | assumeStandardLibraryStripMargin = true 5 | indent { 6 | main = 4 7 | callSite = 4 8 | } 9 | indentOperator.exemptScope = aloneEnclosed 10 | align.tokens = [] 11 | rewrite { 12 | rules = [Imports, SortModifiers] 13 | trailingCommas.style = keep 14 | imports { 15 | expand = true 16 | sort = original 17 | } 18 | } 19 | newlines { 20 | source = keep 21 | avoidForSimpleOverflow = [tooLong, slc] 22 | topLevelStatementBlankLines = [ 23 | { 24 | maxNest = 0 25 | blanks = 1 26 | }, 27 | { 28 | minBreaks = 2 29 | blanks = 1 30 | } 31 | ] 32 | } 33 | docstrings { 34 | wrap = no 35 | style = Asterisk 36 | removeEmpty = true 37 | } 38 | binPack.parentConstructors = keep 39 | project.git = false 40 | 41 | fileOverride { 42 | "glob:**/src-2/**" { 43 | runner.dialect = scala213 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yakiv Yereskovskyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Poppet 2 | [![Maven Central](https://img.shields.io/maven-central/v/com.github.yakivy/poppet-core_2.13.svg)](https://mvnrepository.com/search?q=poppet) 3 | [![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/https/oss.sonatype.org/com.github.yakivy/poppet-core_2.13.svg)](https://oss.sonatype.org/content/repositories/snapshots/com/github/yakivy/poppet-core_2.13/) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | Cats friendly 6 | 7 | Poppet is a minimal, type-safe RPC Scala library. 8 | 9 | Essential differences from [autowire](https://github.com/lihaoyi/autowire): 10 | - has no explicit macro application `.call`, result of a consumer is an instance of original trait 11 | - has no restricted HKT `Future`, you can specify any monad (has `cats.Monad` typeclass) as HKT for the provider/consumer 12 | - has no forced codec dependencies `uPickle`, you can choose from the list of predefined codecs or easily implement your own codec 13 | - has robust failure handling mechanism 14 | - supports Scala 3 (however method/class generation with macros is still an experimental feature) 15 | 16 | ### Table of contents 17 | 1. [Quick start](#quick-start) 18 | 1. [Customizations](#customizations) 19 | 1. [Failure handling](#failure-handling) 20 | 1. [Manual calls](#manual-calls) 21 | 1. [Limitations](#limitations) 22 | 1. [API versioning](#api-versioning) 23 | 1. [Examples](#examples) 24 | 1. [Changelog](#changelog) 25 | 26 | ### Quick start 27 | Put cats and poppet dependencies in the build file, let's assume you are using SBT: 28 | ```scala 29 | val version = new { 30 | val cats = "2.10.0" 31 | val circe = "0.14.6" 32 | val poppet = "0.4.0" 33 | } 34 | 35 | libraryDependencies ++= Seq( 36 | "org.typelevel" %% "cats-core" % version.cats, 37 | 38 | //to use circe 39 | "io.circe" %% "circe-core" % version.circe, 40 | "com.github.yakivy" %% "poppet-circe" % version.poppet, 41 | 42 | //"com.github.yakivy" %% "poppet-upickle" % version.poppet, //to use upickle 43 | //"com.github.yakivy" %% "poppet-play-json" % version.poppet, //to use play json 44 | //"com.github.yakivy" %% "poppet-jackson" % version.poppet, //to use jackson 45 | //"com.github.yakivy" %% "poppet-core" % version.poppet, //to build custom codec 46 | ) 47 | ``` 48 | Define service trait and share it between provider and consumer apps: 49 | ```scala 50 | case class User(email: String, firstName: String) 51 | trait UserService { 52 | def findById(id: String): Future[User] 53 | } 54 | ``` 55 | Implement service trait with actual logic: 56 | ```scala 57 | class UserInternalService extends UserService { 58 | override def findById(id: String): Future[User] = { 59 | //emulation of business logic 60 | if (id == "1") Future.successful(User(id, "Antony")) 61 | else Future.failed(new IllegalArgumentException("User is not found")) 62 | } 63 | } 64 | ``` 65 | Create service provider (can be created once and shared for all incoming calls), keep in mind that only abstract methods of the service type will be exposed, so you need to explicitly specify a trait type: 66 | ```scala 67 | import cats.implicits._ 68 | import io.circe._ 69 | import io.circe.generic.auto._ 70 | import poppet.codec.circe.all._ 71 | import poppet.provider.all._ 72 | 73 | //replace with serious pool 74 | implicit val ec: ExecutionContext = ExecutionContext.global 75 | 76 | val provider = Provider[Future, Json]() 77 | .service[UserService](new UserInternalService) 78 | //.service[OtherService](otherService) 79 | ``` 80 | Create service consumer (can be created once and shared everywhere): 81 | ```scala 82 | import cats.implicits._ 83 | import io.circe._ 84 | import io.circe.generic.auto._ 85 | import poppet.codec.circe.all._ 86 | import poppet.consumer.all._ 87 | import scala.concurrent.ExecutionContext 88 | 89 | //replace with serious pool 90 | implicit val ec: ExecutionContext = ExecutionContext.global 91 | //replace with actual transport call 92 | val transport: Transport[Future, Json] = request => provider(request) 93 | 94 | val userService = Consumer[Future, Json](transport) 95 | .service[UserService] 96 | ``` 97 | Enjoy 👌 98 | ```scala 99 | userService.findById("1") 100 | ``` 101 | 102 | ### Customizations 103 | The library is build on following abstractions: 104 | - `F[_]` - is your service HKT, can be any monad (has `cats.Monad` typeclass); 105 | - `I` - is an intermediate data type that your coding framework works with, can be any serialization format, but it would be easier to choose from existed codec modules as they come with a bunch of predefined codecs; 106 | - `poppet.consumer.Transport` - used to transfer the data between consumer and provider apps, technically it is just a function from `I` to `F[I]`, so you can use anything as long as it can receive/pass the chosen data type; 107 | - `poppet.Codec` - used to convert `I` to domain models and vice versa. Poppet comes with a bunch of modules, where you will hopefully find a favourite codec. If it is not there, you can always try to write your own by providing 2 basic implicits like [here](https://github.com/yakivy/poppet/blob/master/circe/src/poppet/codec/circe/instances/CirceCodecInstances.scala); 108 | - `poppet.CodecK` - used to convert method return HKT to `F` and vice versa. It's needed only if return HKT differs from your service HKT, compilation errors will hint you what codecs are absent; 109 | - `poppet.FailureHandler[F[_]]` - used to handle internal failures, more info you can find [here](#failure-handling); 110 | 111 | #### Failure handling 112 | All meaningful failures that can appear in the library are being transformed into `poppet.Failure`, after what, handled with `poppet.FailureHandler`. Failure handler is a simple polymorphic function from failure to lifted result: 113 | ```scala 114 | trait FailureHandler[F[_]] { 115 | def apply[A](f: Failure): F[A] 116 | } 117 | ``` 118 | by default, throwing failure handler is being used: 119 | ```scala 120 | def throwing[F[_]]: FailureHandler[F] = new FailureHandler[F] { 121 | override def apply[A](f: Failure): F[A] = throw f 122 | } 123 | ``` 124 | so if your don't want to deal with JVM exceptions, you can provide your own instance of failure handler. Let's assume you want to pack a failure with `EitherT[Future, String, *]` HKT, then failure handler can look like: 125 | ```scala 126 | type SR[A] = EitherT[Future, String, A] 127 | val SRFailureHandler = new FailureHandler[SR] { 128 | override def apply[A](f: Failure): SR[A] = EitherT.leftT(f.getMessage) 129 | } 130 | ``` 131 | For more info you can check [Http4s with Circe](#examples) example project, it is built around `EitherT[IO, String, *]` HKT. 132 | 133 | ### Manual calls 134 | If your codec has a human-readable format (JSON for example), you can use a provider without consumer (mostly for debug purposes) by generating requests manually. Here is an example of curl call: 135 | ```shell script 136 | curl --location --request POST '${providerUrl}' \ 137 | --data-raw '{ 138 | "service": "poppet.UserService", #full class name of the service 139 | "method": "findById", #method name 140 | "arguments": { 141 | "id": "1" #argument name: encoded value 142 | } 143 | }' 144 | ``` 145 | 146 | ### Limitations 147 | You can generate consumer/provider almost from any Scala trait (or Java interface 😲). It can have non-abstract members, methods with default arguments, methods with multiple argument lists, varargs, etc... But there are several limitations: 148 | - you cannot overload methods with the same argument names, because for the sake of simplicity argument names are being used as a part of the request, for more info check [manual calls](#manual-calls) section: 149 | ```scala 150 | //compiles 151 | def apply(a: String): Boolean = ??? 152 | def apply(b: Int): Boolean = ??? 153 | 154 | // doesn't compile 155 | def apply(a: String): Boolean = ??? 156 | def apply(a: Int): Boolean = ??? 157 | ``` 158 | - trait/method type parameters should be fully qualified, because codecs are resolved at consumer/provider generation rather than at the method call: 159 | ```scala 160 | //compiles 161 | trait A[T] { 162 | def apply(t: T): Boolean 163 | } 164 | 165 | //doesn't compile 166 | trait A { 167 | def apply[T](t: T): Boolean 168 | } 169 | trait A { 170 | type T 171 | def apply(t: T): Boolean 172 | } 173 | ``` 174 | - trait should not have arguments 175 | 176 | ### API versioning 177 | The goal of the library is to closely resemble typical Scala traits, so same binary compatibility approaches can also be applied for API versioning, for example: 178 | - when you want to change method signature, add new method and deprecate old one, (important note: argument name is a part of signature in poppet, for more info check [limitations](#limitations) section): 179 | ```scala 180 | @deprecared def apply(a: String): Boolean = ??? 181 | def apply(b: Int): Boolean = ??? 182 | ``` 183 | - if you are tolerant to binary incompatible changes, you can modify argument/return types without creating new method, but ensure that codecs are compatible: 184 | ```scala 185 | def apply(a: String): Boolean = ??? 186 | //if Email is serialized as a String, method can be updated to 187 | def apply(a: Email): Boolean = ??? 188 | ``` 189 | - when you want to remove method, deprecate it and remove after all consumers are updated to the new version 190 | - when you want to change service name, provide new service (you can extend it from the old one) and deprecate old one: 191 | ```scala 192 | @deprecated trait A 193 | trait B extends A 194 | 195 | Provider[..., ...]() 196 | .service[A](bImpl) 197 | .service[B](bImpl) 198 | ``` 199 | 200 | ### Examples 201 | - run desired example: 202 | - Http4s with Circe: https://github.com/yakivy/poppet/tree/master/example/http4s-circe 203 | - run provider: `./mill example.http4s-circe.provider.run` 204 | - run consumer: `./mill example.http4s-circe.consumer.run` 205 | - Play Framework with Play Json: https://github.com/yakivy/poppet/tree/master/example/play 206 | - run provider: `./mill example.play.provider.run` 207 | - run consumer: `./mill example.play.consumer.run` 208 | - remove `RUNNING_PID` file manually if services are conflicting with each other 209 | - And even Spring Framework with Jackson 😲: https://github.com/yakivy/poppet/tree/master/example/spring-jackson 210 | - run provider: `./mill example.spring-jackson.provider.run` 211 | - run consumer: `./mill example.spring-jackson.consumer.run` 212 | - Tapir with Sttp with FS2 with Circe (supports streaming): https://github.com/yakivy/poppet/tree/master/example/tapir-sttp-fs2-circe 213 | - run provider: `./mill example.tapir-sttp-fs2-circe.provider.run` 214 | - run consumer: `./mill example.tapir-sttp-fs2-circe.consumer.run` 215 | - put `http://localhost:9002/api/user/1` in the address bar 216 | - put `http://localhost:9002/api/user` in the address bar if transport supports streaming 217 | 218 | ### Roadmap 219 | - add action (including argument name) to codec 220 | - throw an exception on duplicated service processor 221 | - separate `.service[S]` and `.service[G[_], S]` to simplify codec resolution 222 | - check that passed class is a trait and doesn't have arguments to prevent obscure error from compiler 223 | - check that all abstract methods are public 224 | 225 | ### Changelog 226 | #### 0.4.x: 227 | - simplify transport and provider response 228 | - remove peek 229 | - remove ObjectMapper creation from Jackson codec, ask for it implicitly 230 | 231 | #### 0.3.x: 232 | - fix compilation errors for methods with varargs 233 | - fix codec resolution for id (`I => I`) codecs 234 | - add Scala 3 support 235 | 236 | #### 0.2.x: 237 | - fix compilation error message for ambiguous implicits 238 | - fix processor compilation for complex types 239 | - migrate to mill build tool 240 | - add Scala JS and Scala Native support 241 | - add more details to `Can't find processor` exception 242 | - make `FailureHandler` explicit 243 | - rename `poppet.coder` package to `poppet.codec` 244 | - various refactorings and cleanups -------------------------------------------------------------------------------- /build.sc: -------------------------------------------------------------------------------- 1 | import $ivy.`com.lihaoyi::mill-contrib-playlib:$MILL_VERSION` 2 | 3 | import mill._ 4 | import mill.scalalib._ 5 | import mill.scalajslib._ 6 | import mill.scalanativelib._ 7 | import mill.scalalib.publish._ 8 | import mill.playlib._ 9 | 10 | object versions { 11 | val publish = "0.4.0" 12 | 13 | val scala212 = "2.12.19" 14 | val scala213 = "2.13.12" 15 | val scala3 = "3.3.0" 16 | val scalaJs = "1.13.2" 17 | val scalaNative = "0.4.16" 18 | val scalatest = "3.2.14" 19 | val cats = "2.10.0" 20 | 21 | val upickle = "2.0.0" 22 | val circe = "0.14.6" 23 | val playJson = "2.9.4" 24 | val jackson = "2.13.5" 25 | 26 | val catsEffect = "3.5.4" 27 | val fs2 = "3.10.0" 28 | val http4s = "0.23.16" 29 | val tapir = "1.10.0" 30 | val sttp = "3.9.4" 31 | val play = "2.8.18" 32 | val logback = "1.2.11" 33 | val springBoot = "2.7.5" 34 | 35 | val cross2 = Seq(scala212, scala213) 36 | val cross3 = Seq(scala3) 37 | val cross = cross2 ++ cross3 38 | } 39 | 40 | trait CommonPublishModule extends PublishModule with CrossScalaModule { 41 | override def publishVersion = versions.publish 42 | override def pomSettings = PomSettings( 43 | description = artifactName(), 44 | organization = "com.github.yakivy", 45 | url = "https://github.com/yakivy/poppet", 46 | licenses = Seq(License.MIT), 47 | versionControl = VersionControl.github("yakivy", "poppet"), 48 | developers = Seq(Developer("yakivy", "Yakiv Yereskovskyi", "https://github.com/yakivy")) 49 | ) 50 | override def compileIvyDeps = super.compileIvyDeps() ++ Agg( 51 | ivy"org.typelevel::cats-core:${versions.cats}", 52 | ) ++ ( 53 | if (crossScalaVersion == versions.scala3) Agg.empty[Dep] 54 | else Agg(ivy"org.scala-lang:scala-reflect:${scalaVersion()}") 55 | ) 56 | override def millSourcePath = super.millSourcePath / os.up 57 | override def scalacOptions = super.scalacOptions() ++ ( 58 | if (crossScalaVersion == versions.scala3) Seq("-Xcheck-macros", "-explain") 59 | else Seq.empty[String] 60 | ) 61 | } 62 | 63 | trait CommonPublishTestModule extends ScalaModule with TestModule { 64 | override def ivyDeps = super.ivyDeps() ++ Agg( 65 | ivy"org.scalatest::scalatest::${versions.scalatest}", 66 | ivy"org.typelevel::cats-core::${versions.cats}", 67 | ) 68 | override def testFramework = "org.scalatest.tools.Framework" 69 | } 70 | 71 | trait CommonPublishJvmModule extends CommonPublishModule { 72 | trait CommonPublishCrossModuleTests extends CommonPublishTestModule with ScalaTests 73 | } 74 | 75 | trait CommonPublishJsModule extends CommonPublishModule with ScalaJSModule { 76 | def scalaJSVersion = versions.scalaJs 77 | trait CommonPublishCrossModuleTests extends CommonPublishTestModule with ScalaTests 78 | } 79 | 80 | trait CommonPublishNativeModule extends CommonPublishModule with ScalaNativeModule { 81 | def scalaNativeVersion = versions.scalaNative 82 | trait CommonPublishCrossModuleTests extends CommonPublishTestModule with ScalaTests 83 | } 84 | 85 | object core extends Module { 86 | trait CommonModule extends CommonPublishModule { 87 | override def artifactName = "poppet-core" 88 | 89 | trait CommonModuleTests extends ScalaTests { 90 | override def ivyDeps = super.ivyDeps() ++ Agg( 91 | ivy"com.lihaoyi::upickle::${versions.upickle}", 92 | ) 93 | } 94 | } 95 | 96 | object jvm extends Cross[JvmModule](versions.cross) 97 | trait JvmModule extends CommonModule with CommonPublishJvmModule { 98 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 99 | override def moduleDeps = super.moduleDeps ++ Seq(upickle.jvm()) 100 | } 101 | } 102 | 103 | object js extends Cross[JsModule](versions.cross) 104 | trait JsModule extends CommonModule with CommonPublishJsModule { 105 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 106 | override def moduleDeps = super.moduleDeps ++ Seq(upickle.js()) 107 | } 108 | } 109 | 110 | object native extends Cross[NativeModule](versions.cross) 111 | trait NativeModule extends CommonModule with CommonPublishNativeModule { 112 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 113 | override def moduleDeps = super.moduleDeps ++ Seq(upickle.native()) 114 | } 115 | } 116 | } 117 | 118 | object upickle extends Module { 119 | trait CommonModule extends CommonPublishModule { 120 | override def artifactName = "poppet-upickle" 121 | 122 | override def compileIvyDeps = super.compileIvyDeps() ++ Agg( 123 | ivy"com.lihaoyi::upickle::${versions.upickle}", 124 | ) 125 | 126 | trait CommonModuleTests extends ScalaTests { 127 | override def ivyDeps = super.ivyDeps() ++ Agg( 128 | ivy"com.lihaoyi::upickle::${versions.upickle}", 129 | ) 130 | } 131 | } 132 | 133 | object jvm extends Cross[JvmModule](versions.cross) 134 | trait JvmModule extends CommonModule with CommonPublishJvmModule { 135 | override def moduleDeps = super.moduleDeps ++ Seq(core.jvm()) 136 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 137 | override def moduleDeps = super.moduleDeps ++ Seq(core.jvm().test) 138 | } 139 | } 140 | 141 | object js extends Cross[JsModule](versions.cross) 142 | trait JsModule extends CommonModule with CommonPublishJsModule { 143 | override def moduleDeps = super.moduleDeps ++ Seq(core.js()) 144 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 145 | override def moduleDeps = super.moduleDeps ++ Seq(core.js().test) 146 | } 147 | } 148 | 149 | object native extends Cross[NativeModule](versions.cross) 150 | trait NativeModule extends CommonModule with CommonPublishNativeModule { 151 | override def moduleDeps = super.moduleDeps ++ Seq(core.native()) 152 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 153 | override def moduleDeps = super.moduleDeps ++ Seq(core.native().test) 154 | } 155 | } 156 | } 157 | 158 | object circe extends Module { 159 | trait CommonModule extends CommonPublishModule { 160 | override def artifactName = "poppet-circe" 161 | 162 | override def compileIvyDeps = super.compileIvyDeps() ++ Agg( 163 | ivy"io.circe::circe-core::${versions.circe}", 164 | ) 165 | 166 | trait CommonModuleTests extends ScalaTests { 167 | override def ivyDeps = super.ivyDeps() ++ Agg( 168 | ivy"io.circe::circe-core::${versions.circe}", 169 | ivy"io.circe::circe-generic::${versions.circe}", 170 | ) 171 | } 172 | } 173 | 174 | object jvm extends Cross[JvmModule](versions.cross) 175 | trait JvmModule extends CommonModule with CommonPublishJvmModule { 176 | override def moduleDeps = super.moduleDeps ++ Seq(core.jvm()) 177 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 178 | override def moduleDeps = super.moduleDeps ++ Seq(core.jvm().test) 179 | } 180 | } 181 | 182 | object js extends Cross[JsModule](versions.cross) 183 | trait JsModule extends CommonModule with CommonPublishJsModule { 184 | override def moduleDeps = super.moduleDeps ++ Seq(core.js()) 185 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 186 | override def moduleDeps = super.moduleDeps ++ Seq(core.js().test) 187 | } 188 | } 189 | 190 | object native extends Cross[NativeModule](versions.cross) 191 | trait NativeModule extends CommonModule with CommonPublishNativeModule { 192 | override def moduleDeps = super.moduleDeps ++ Seq(core.native()) 193 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 194 | override def moduleDeps = super.moduleDeps ++ Seq(core.native().test) 195 | } 196 | } 197 | } 198 | 199 | object `play-json` extends Module { 200 | trait CommonModule extends CommonPublishModule { 201 | override def artifactName = "poppet-play-json" 202 | 203 | override def compileIvyDeps = super.compileIvyDeps() ++ Agg( 204 | ivy"com.typesafe.play::play-json::${versions.playJson}", 205 | ) 206 | 207 | trait CommonModuleTests extends ScalaTests { 208 | override def ivyDeps = super.ivyDeps() ++ Agg( 209 | ivy"com.typesafe.play::play-json::${versions.playJson}", 210 | ) 211 | } 212 | } 213 | 214 | object jvm extends Cross[JvmModule](versions.cross2) 215 | trait JvmModule extends CommonModule with CommonPublishJvmModule { 216 | override def moduleDeps = super.moduleDeps ++ Seq(core.jvm()) 217 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 218 | override def moduleDeps = super.moduleDeps ++ Seq(core.jvm().test) 219 | } 220 | } 221 | 222 | object js extends Cross[JsModule](versions.cross2) 223 | trait JsModule extends CommonModule with CommonPublishJsModule { 224 | override def moduleDeps = super.moduleDeps ++ Seq(core.js()) 225 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 226 | override def moduleDeps = super.moduleDeps ++ Seq(core.js().test) 227 | } 228 | } 229 | } 230 | 231 | object jackson extends Module { 232 | trait CommonModule extends CommonPublishModule { 233 | override def artifactName = "poppet-jackson" 234 | 235 | override def compileIvyDeps = super.compileIvyDeps() ++ Agg( 236 | ivy"com.fasterxml.jackson.core:jackson-databind::${versions.jackson}", 237 | ivy"com.fasterxml.jackson.module::jackson-module-scala::${versions.jackson}", 238 | ) 239 | 240 | trait CommonModuleTests extends ScalaTests { 241 | override def ivyDeps = super.ivyDeps() ++ Agg( 242 | ivy"com.fasterxml.jackson.core:jackson-databind::${versions.jackson}", 243 | ivy"com.fasterxml.jackson.module::jackson-module-scala::${versions.jackson}", 244 | ) 245 | } 246 | } 247 | 248 | object jvm extends Cross[JvmModule](versions.cross) 249 | trait JvmModule extends CommonModule with CommonPublishJvmModule { 250 | override def moduleDeps = super.moduleDeps ++ Seq(core.jvm()) 251 | object test extends CommonModuleTests with CommonPublishCrossModuleTests { 252 | override def moduleDeps = super.moduleDeps ++ Seq(core.jvm().test) 253 | } 254 | } 255 | } 256 | 257 | object example extends Module { 258 | object `http4s-circe` extends Module { 259 | trait CommonModule extends ScalaModule { 260 | override def scalaVersion = versions.scala3 261 | override def ivyDeps = super.ivyDeps() ++ Agg( 262 | ivy"org.typelevel::cats-core::${versions.cats}", 263 | ivy"org.typelevel::cats-effect::${versions.catsEffect}", 264 | ivy"io.circe::circe-generic::${versions.circe}", 265 | ) 266 | override def moduleDeps = super.moduleDeps ++ Seq(circe.jvm(versions.scala3)) 267 | } 268 | object api extends CommonModule 269 | object consumer extends CommonModule { 270 | override def ivyDeps = super.ivyDeps() ++ Agg( 271 | ivy"org.http4s::http4s-circe::${versions.http4s}", 272 | ivy"org.http4s::http4s-dsl::${versions.http4s}", 273 | ivy"org.http4s::http4s-blaze-server::${versions.http4s}", 274 | ivy"org.http4s::http4s-blaze-client::${versions.http4s}", 275 | ivy"ch.qos.logback:logback-classic:${versions.logback}", 276 | ) 277 | override def moduleDeps = super.moduleDeps ++ Seq(api) 278 | } 279 | object provider extends CommonModule { 280 | override def ivyDeps = super.ivyDeps() ++ Agg( 281 | ivy"org.http4s::http4s-circe::${versions.http4s}", 282 | ivy"org.http4s::http4s-dsl::${versions.http4s}", 283 | ivy"org.http4s::http4s-blaze-server::${versions.http4s}", 284 | ivy"ch.qos.logback:logback-classic:${versions.logback}", 285 | ) 286 | override def moduleDeps = super.moduleDeps ++ Seq(api) 287 | } 288 | } 289 | 290 | object play extends Module { 291 | trait CommonModule extends ScalaModule { 292 | override def scalaVersion = versions.scala213 293 | override def ivyDeps = super.ivyDeps() ++ Agg( 294 | ivy"org.typelevel::cats-core::${versions.cats}", 295 | ivy"com.typesafe.play::play-json::${versions.playJson}", 296 | ) 297 | override def moduleDeps = super.moduleDeps ++ Seq(`play-json`.jvm(versions.scala213)) 298 | } 299 | object api extends CommonModule 300 | object consumer extends CommonModule with PlayApiModule { 301 | override def playVersion = versions.play 302 | override def ivyDeps = super.ivyDeps() ++ Agg( 303 | ws() 304 | ) 305 | override def moduleDeps = super.moduleDeps ++ Seq(api) 306 | } 307 | object provider extends CommonModule with PlayApiModule { 308 | override def playVersion = versions.play 309 | override def moduleDeps = super.moduleDeps ++ Seq(api) 310 | } 311 | } 312 | 313 | object `spring-jackson` extends Module { 314 | trait CommonModule extends ScalaModule { 315 | override def scalaVersion = versions.scala213 316 | override def ivyDeps = super.ivyDeps() ++ Agg( 317 | ivy"org.typelevel::cats-core::${versions.cats}", 318 | ivy"com.fasterxml.jackson.core:jackson-databind::${versions.jackson}", 319 | ivy"com.fasterxml.jackson.module::jackson-module-scala::${versions.jackson}", 320 | ) 321 | override def moduleDeps = super.moduleDeps ++ Seq(jackson.jvm(versions.scala213)) 322 | override def javacOptions = Seq("-source", "1.8", "-target", "1.8") 323 | } 324 | object api extends CommonModule 325 | object consumer extends CommonModule { 326 | override def finalMainClass = "poppet.example.spring.consumer.Application" 327 | override def ivyDeps = super.ivyDeps() ++ Agg( 328 | ivy"org.springframework.boot:spring-boot-starter-web:${versions.springBoot}", 329 | ) 330 | override def moduleDeps = super.moduleDeps ++ Seq(api) 331 | } 332 | object provider extends CommonModule { 333 | override def finalMainClass = "poppet.example.spring.provider.Application" 334 | override def ivyDeps = super.ivyDeps() ++ Agg( 335 | ivy"org.springframework.boot:spring-boot-starter-web:${versions.springBoot}", 336 | ) 337 | override def moduleDeps = super.moduleDeps ++ Seq(api) 338 | } 339 | } 340 | 341 | object `tapir-sttp-fs2-circe` extends Module { 342 | trait CommonModule extends ScalaModule { 343 | override def scalaVersion = versions.scala3 344 | override def ivyDeps = super.ivyDeps() ++ Agg( 345 | ivy"org.typelevel::cats-core::${versions.cats}", 346 | ivy"org.typelevel::cats-effect::${versions.catsEffect}", 347 | ivy"io.circe::circe-generic::${versions.circe}", 348 | ivy"co.fs2::fs2-io::${versions.fs2}", 349 | ) 350 | override def moduleDeps = super.moduleDeps ++ Seq(circe.jvm(versions.scala3)) 351 | } 352 | object api extends CommonModule 353 | object consumer extends CommonModule { 354 | override def ivyDeps = super.ivyDeps() ++ Agg( 355 | ivy"org.http4s::http4s-blaze-server::${versions.http4s}", 356 | ivy"org.http4s::http4s-blaze-client::${versions.http4s}", 357 | ivy"com.softwaremill.sttp.tapir::tapir-http4s-server::${versions.tapir}", 358 | ivy"com.softwaremill.sttp.client3::http4s-backend::${versions.sttp}", 359 | ivy"com.softwaremill.sttp.tapir::tapir-cats::${versions.tapir}", 360 | ivy"com.softwaremill.sttp.tapir::tapir-json-circe::${versions.tapir}", 361 | ivy"ch.qos.logback:logback-classic:${versions.logback}", 362 | ) 363 | override def moduleDeps = super.moduleDeps ++ Seq(api) 364 | } 365 | object provider extends CommonModule { 366 | override def ivyDeps = super.ivyDeps() ++ Agg( 367 | ivy"org.http4s::http4s-blaze-server::${versions.http4s}", 368 | ivy"com.softwaremill.sttp.tapir::tapir-http4s-server::${versions.tapir}", 369 | ivy"com.softwaremill.sttp.tapir::tapir-cats::${versions.tapir}", 370 | ivy"com.softwaremill.sttp.tapir::tapir-json-circe::${versions.tapir}", 371 | ivy"ch.qos.logback:logback-classic:${versions.logback}", 372 | ) 373 | override def moduleDeps = super.moduleDeps ++ Seq(api) 374 | } 375 | } 376 | } -------------------------------------------------------------------------------- /circe/src/poppet/codec/circe/all/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.circe 2 | 3 | import poppet.codec.circe.instances.CirceCodecInstances 4 | 5 | package object all extends CirceCodecInstances 6 | -------------------------------------------------------------------------------- /circe/src/poppet/codec/circe/instances/CirceCodecInstances.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.circe.instances 2 | 3 | import io.circe.Decoder 4 | import io.circe.Encoder 5 | import io.circe.Json 6 | import poppet._ 7 | 8 | trait CirceCodecInstancesLp0 { 9 | implicit def circeDecoderToCodec[A: Decoder]: Codec[Json, A] = a => Decoder[A].apply(a.hcursor) 10 | .left.map(f => new CodecFailure(f.getMessage(), a.hcursor.value, f)) 11 | } 12 | 13 | trait CirceCodecInstances extends CirceCodecInstancesLp0 { 14 | implicit def circeEncoderToCodec[A: Encoder]: Codec[A, Json] = a => Right(Encoder[A].apply(a)) 15 | } 16 | -------------------------------------------------------------------------------- /circe/src/poppet/codec/circe/instances/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.circe 2 | 3 | package object instances extends CirceCodecInstances 4 | -------------------------------------------------------------------------------- /circe/test/src/poppet/codec/circe/CirceCodecSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.circe 2 | 3 | import io.circe.Json 4 | import io.circe.generic.auto._ 5 | import org.scalatest.freespec.AnyFreeSpec 6 | import poppet.codec.CodecSpec 7 | import poppet.codec.CodecSpec.A 8 | import poppet.codec.circe.all._ 9 | 10 | class CirceCodecSpec extends AnyFreeSpec with CodecSpec { 11 | "Circe codec should parse" - { 12 | "custom data structures" in { 13 | assertCustomCodec[Json, Unit](()) 14 | assertCustomCodec[Json, Int](intExample) 15 | assertCustomCodec[Json, String](stringExample) 16 | assertCustomCodec[Json, A](caseClassExample) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/src-2/poppet/consumer/core/ConsumerProcessorObjectBinCompat.scala: -------------------------------------------------------------------------------- 1 | package poppet.consumer.core 2 | 3 | import poppet.core.ProcessorMacro 4 | import scala.language.experimental.macros 5 | import scala.reflect.macros.blackbox 6 | 7 | trait ConsumerProcessorObjectBinCompat { 8 | implicit def generate[F[_], I, S]: ConsumerProcessor[F, I, S] = 9 | macro ConsumerProcessorObjectBinCompat.generateImpl[F, I, S] 10 | } 11 | 12 | object ConsumerProcessorObjectBinCompat { 13 | def generateImpl[F[_], I, S]( 14 | c: blackbox.Context)( 15 | implicit FT: c.WeakTypeTag[F[_]], IT: c.WeakTypeTag[I], ST: c.WeakTypeTag[S] 16 | ): c.Expr[ConsumerProcessor[F, I, S]] = { 17 | import c.universe._ 18 | val serviceName = ST.tpe.typeSymbol.fullName 19 | val fmonad = q"_root_.scala.Predef.implicitly[_root_.cats.Monad[$FT]]" 20 | val implementations = ProcessorMacro.getAbstractMethods(c)(ST.tpe).map { m => 21 | val mInS = m.typeSignatureIn(ST.tpe) 22 | val methodName = m.name 23 | val arguments = mInS.paramLists.map(ps => ps.map(p => q"${Ident(p.name)}: ${p.typeSignature}")) 24 | val (returnKind, returnType) = ProcessorMacro.separateReturnType(c)(FT.tpe, mInS.finalResultType, false) 25 | val codedArgument: c.universe.Symbol => Tree = a => q"""_root_.scala.Predef.implicitly[ 26 | _root_.poppet.core.Codec[${ProcessorMacro.unwrapVararg(c)(a.typeSignature)},${IT.tpe}] 27 | ].apply(${Ident(a.name)}).fold($$fh.apply, $fmonad.pure)""" 28 | val withCodedArguments: Tree => Tree = tree => mInS.paramLists.flatten match { 29 | case Nil => tree 30 | case h :: Nil => 31 | q"""$fmonad.flatMap(${codedArgument(h)})((${Ident(h.name)}: ${h.typeSignature}) => $tree)""" 32 | case hs => q"""$fmonad.flatten( 33 | $fmonad.${TermName("map" + hs.size)}(..${hs.map(codedArgument)})( 34 | ..${hs.map(h => q"${Ident(h.name)}: $IT")} => $tree 35 | ) 36 | )""" 37 | } 38 | //don't use returnTypeCodec, in some cases typecheck corrupts macro-based implicits 39 | val (returnKindCodec, returnTypeCodec) = ProcessorMacro.inferReturnCodecs(c)( 40 | FT.tpe, IT.tpe, appliedType(FT.tpe, IT.tpe), 41 | returnKind, returnType, mInS.finalResultType 42 | ) 43 | q"""override def $methodName(...$arguments): ${mInS.finalResultType} = { 44 | val result = $fmonad.map(${withCodedArguments(q""" 45 | $$client.apply(_root_.poppet.core.Request( 46 | $serviceName, ${methodName.toString}, _root_.scala.Predef.Map( 47 | ..${m.paramLists.flatten.map(p => q"""( 48 | ${p.name.toString}, ${Ident(p.name)} 49 | )""")} 50 | ) 51 | ))""")})(_.value) 52 | $returnKindCodec.apply($fmonad.flatMap(result)( 53 | _root_.scala.Predef.implicitly[ 54 | _root_.poppet.core.Codec[${IT.tpe},$returnType] 55 | ].apply(_).fold($$fh.apply, $fmonad.pure) 56 | )) 57 | }""" 58 | } 59 | c.Expr(q"""(($$client, $$fh) => new $ST { ..$implementations })""") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /core/src-2/poppet/core/ProcessorMacro.scala: -------------------------------------------------------------------------------- 1 | package poppet.core 2 | 3 | import scala.reflect.macros.TypecheckException 4 | import scala.reflect.macros.blackbox 5 | 6 | object ProcessorMacro { 7 | def getAbstractMethods(c: blackbox.Context)(tpe: c.Type): List[c.universe.MethodSymbol] = { 8 | tpe.members.view.filter(m => m.isType && m.isAbstract).foreach { t => 9 | c.abort(c.enclosingPosition, s"Abstract types are not supported: $tpe.${t.name}") 10 | } 11 | val methods = tpe.members.toList.filter(m => m.isAbstract && m.isMethod).map(_.asMethod).sortBy(_.fullName) 12 | methods.foreach { m => 13 | if (m.typeParams.nonEmpty) c.abort(c.enclosingPosition, s"Generic methods are not supported: $tpe.${m.name}") 14 | } 15 | if (methods.isEmpty) c.abort(c.enclosingPosition, 16 | s"$tpe has no abstract methods. Make sure that service method is parametrized with a trait." 17 | ) 18 | if (methods.map(m => (m.name, m.paramLists.flatten.map(_.name.toString))).toSet.size != methods.size) 19 | c.abort(c.enclosingPosition, "Use unique argument name lists for overloaded methods.") 20 | methods 21 | } 22 | 23 | def inferImplicit[A: c.universe.Liftable](c: blackbox.Context)(tpe: A): Option[c.Tree] = { 24 | import c.universe._ 25 | try c.typecheck(q"""_root_.scala.Predef.implicitly[$tpe]""") match { 26 | case EmptyTree => None 27 | case tree => Option(tree) 28 | } 29 | catch { 30 | case e: TypecheckException if e.msg.contains("could not find implicit value for") => None 31 | case e: TypecheckException => c.abort(c.enclosingPosition, e.msg) 32 | } 33 | } 34 | 35 | def unwrapVararg(c: blackbox.Context)(t: c.Type): c.Type = { 36 | import c.universe._ 37 | if (t.typeSymbol == definitions.RepeatedParamClass) appliedType(typeOf[Seq[_]], t.typeArgs) 38 | else t 39 | } 40 | 41 | def separateReturnType( 42 | c: blackbox.Context)(fType: c.Type, returnType: c.Type, fromReturn: Boolean 43 | ): (c.Type, c.Type) = { 44 | import c.universe._ 45 | val typeArgs = returnType.typeArgs 46 | (if (typeArgs.size == 1) { 47 | ProcessorMacro.inferImplicit(c)( 48 | if (fromReturn) tq"_root_.poppet.CodecK[${returnType.typeConstructor}, $fType]" 49 | else tq"_root_.poppet.CodecK[$fType,${returnType.typeConstructor}]" 50 | ).map(_ => returnType.typeConstructor -> typeArgs.last) 51 | } else None).getOrElse(typeOf[cats.Id[_]].typeConstructor -> returnType) 52 | } 53 | 54 | def inferReturnCodecs( 55 | c: blackbox.Context)( 56 | fType: c.Type, faType: c.Type, ffaType: c.Type, 57 | tType: c.Type, taType: c.Type, ttaType: c.Type, 58 | ): (c.Tree, c.Tree) = { 59 | import c.universe._ 60 | val codecK = ProcessorMacro.inferImplicit(c)(tq"_root_.poppet.CodecK[$fType,$tType]") 61 | val codec = ProcessorMacro.inferImplicit(c)(tq"_root_.poppet.Codec[$faType,$taType]") 62 | if (codecK.nonEmpty && codec.nonEmpty) (codecK.get, codec.get) 63 | else c.abort( 64 | c.enclosingPosition, 65 | s"Unable to convert $ffaType to $ttaType. Try to provide " + 66 | (if (codecK.isEmpty) s"poppet.CodecK[$fType,$tType]" else "") + 67 | (if (codecK.isEmpty && codec.isEmpty) " with " else "") + 68 | (if (codec.isEmpty) s"poppet.Codec[$faType,$taType]" else "") + 69 | (if ( 70 | !(ttaType.typeConstructor =:= typeOf[cats.Id[_]].typeConstructor) && 71 | tType =:= typeOf[cats.Id[_]].typeConstructor && 72 | taType.typeArgs.size == 1 73 | ) { 74 | val stType = taType.typeConstructor 75 | val staType = taType.typeArgs.head 76 | val scodec = ProcessorMacro.inferImplicit(c)(tq"_root_.poppet.Codec[$faType,$staType]") 77 | s" or poppet.CodecK[$fType,$stType]" + 78 | (if (scodec.isEmpty) s" with poppet.Codec[$faType,$staType]" else "") 79 | } else "") + 80 | (if ( 81 | !(ffaType.typeConstructor =:= typeOf[cats.Id[_]].typeConstructor) && 82 | fType =:= typeOf[cats.Id[_]].typeConstructor && 83 | faType.typeArgs.size == 1 84 | ) { 85 | val stType = faType.typeConstructor 86 | val staType = faType.typeArgs.head 87 | val scodec = ProcessorMacro.inferImplicit(c)(tq"_root_.poppet.Codec[$staType,$taType]") 88 | s" or poppet.CodecK[$stType,$tType]" + 89 | (if (scodec.isEmpty) s" with poppet.Codec[$staType,$taType]" else "") 90 | } else "") + 91 | "." 92 | ) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /core/src-2/poppet/provider/core/ProviderProcessorObjectBinCompat.scala: -------------------------------------------------------------------------------- 1 | package poppet.provider.core 2 | 3 | import poppet.core.ProcessorMacro 4 | import scala.language.experimental.macros 5 | import scala.reflect.macros.blackbox 6 | 7 | trait ProviderProcessorObjectBinCompat { 8 | implicit def generate[F[_], I, S]: ProviderProcessor[F, I, S] = 9 | macro ProviderProcessorObjectBinCompat.generateImpl[F, I, S] 10 | } 11 | 12 | object ProviderProcessorObjectBinCompat { 13 | def generateImpl[F[_], I, S]( 14 | c: blackbox.Context)( 15 | implicit FT: c.WeakTypeTag[F[_]], IT: c.WeakTypeTag[I], ST: c.WeakTypeTag[S] 16 | ): c.Expr[ProviderProcessor[F, I, S]] = { 17 | import c.universe._ 18 | val fmonad = q"_root_.scala.Predef.implicitly[_root_.cats.Monad[$FT]]" 19 | val methodProcessors = ProcessorMacro.getAbstractMethods(c)(ST.tpe).map { m => 20 | val mInS = m.typeSignatureIn(ST.tpe) 21 | val argumentNames = m.paramLists.flatten.map(_.name.toString) 22 | val (returnKind, returnType) = ProcessorMacro.separateReturnType(c)(FT.tpe, mInS.finalResultType, true) 23 | val codedArgument: c.universe.Symbol => Tree = a => q"""_root_.scala.Predef.implicitly[ 24 | _root_.poppet.core.Codec[$IT,${ProcessorMacro.unwrapVararg(c)(a.typeSignature)}] 25 | ].apply(as(${a.name.toString})).fold($$fh.apply, $fmonad.pure)""" 26 | val withCodedArguments: Tree => Tree = tree => mInS.paramLists.flatten match { 27 | case Nil => tree 28 | case h :: Nil => 29 | q"$fmonad.flatMap(${codedArgument(h)})((${Ident(h.name)}: ${h.typeSignature}) => $tree)" 30 | case hs => q"""$fmonad.flatten( 31 | $fmonad.${TermName("map" + hs.size)}(..${hs.map(codedArgument)})( 32 | ..${hs.map(h => q"${Ident(h.name)}: ${h.typeSignature}")} => $tree 33 | ) 34 | )""" 35 | } 36 | //don't use returnTypeCodec, in some cases typecheck corrupts macro-based implicits 37 | val (returnKindCodec, returnTypeCodec) = ProcessorMacro.inferReturnCodecs(c)( 38 | returnKind, returnType, mInS.finalResultType, 39 | FT.tpe, IT.tpe, appliedType(FT.tpe, IT.tpe), 40 | ) 41 | val groupedArguments = m.paramLists.map(pl => pl.map(p => p.typeSignature.typeSymbol -> Ident(p.name))) 42 | q"""new _root_.poppet.provider.core.MethodProcessor[$FT, $IT]( 43 | ${ST.tpe.typeSymbol.fullName}, 44 | ${m.name.toString}, 45 | _root_.scala.List(..$argumentNames), 46 | as => ${withCodedArguments(q""" 47 | $fmonad.flatMap( 48 | $returnKindCodec.apply(${ 49 | groupedArguments.foldLeft[Tree](q"$$service.${m.name.toTermName}") { (acc, pl) => 50 | pl.lastOption match { 51 | case Some((s, i)) if s == definitions.RepeatedParamClass => 52 | q"$acc(..${pl.init.map(_._2)}, $i: _*)" 53 | case _ => q"$acc(..${pl.map(_._2)})" 54 | } 55 | } 56 | }) 57 | )( 58 | _root_.scala.Predef.implicitly[_root_.poppet.core.Codec[$returnType,${IT.tpe}]] 59 | .apply(_).fold($$fh.apply, $fmonad.pure) 60 | ) 61 | """)} 62 | )""" 63 | } 64 | c.Expr(q"(($$service, $$fh) => $methodProcessors)") 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/src-3/poppet/consumer/core/ConsumerProcessorObjectBinCompat.scala: -------------------------------------------------------------------------------- 1 | package poppet.consumer.core 2 | 3 | import cats.Monad 4 | import cats.Traverse 5 | import java.util.UUID 6 | import poppet.consumer.all.* 7 | import poppet.core.ProcessorMacro.* 8 | import poppet.core.Request 9 | import poppet.core.Response 10 | import scala.annotation.experimental 11 | import scala.quoted.* 12 | import scala.compiletime.* 13 | 14 | trait ConsumerProcessorObjectBinCompat { 15 | implicit inline def generate[F[_], I, S](implicit inline MF: Monad[F]): ConsumerProcessor[F, I, S] = 16 | ${ ConsumerProcessorObjectBinCompat.processorExpr('MF) } 17 | } 18 | 19 | @experimental 20 | object ConsumerProcessorObjectBinCompat { 21 | import scala.language.experimental.* 22 | 23 | def processorExpr[F[_]: Type, I: Type, S: Type]( 24 | using q: Quotes)(MF: Expr[Monad[F]] 25 | ): Expr[ConsumerProcessor[F, I, S]] = '{ 26 | new ConsumerProcessor[F, I, S] { 27 | override def apply(client: Request[I] => F[Response[I]], fh: FailureHandler[F]): S = 28 | ${ ConsumerProcessorObjectBinCompat.serviceImplExpr[F, I, S]('client, 'fh, MF) } 29 | } 30 | } 31 | 32 | private def serviceImplExpr[F[_]: Type, I: Type, S: Type]( 33 | using q: Quotes)(client: Expr[Request[I] => F[Response[I]]], fh: Expr[FailureHandler[F]], MF: Expr[Monad[F]] 34 | ): Expr[S] = { 35 | import q.reflect._ 36 | def methodSymbols(classSymbol: Symbol) = getAbstractMethods[S].map(m => 37 | Symbol.newMethod(classSymbol, m.name, TypeRepr.of[S].memberType(m.symbol)) 38 | ) 39 | def methodImpls(classSymbol: Symbol) = classSymbol.declaredMethods.map(m => 40 | DefDef(m, argss => Option(methodBodyTerm[F, I, S](m, argss, client, fh, MF).changeOwner(m))) 41 | .changeOwner(classSymbol) 42 | ) 43 | val className = s"$$PoppetConsumer_${TypeRepr.of[S].show}_${UUID.randomUUID()}" 44 | val parents = List(TypeTree.of[Object], TypeTree.of[S]) 45 | val classSymbol = Symbol.newClass( 46 | Symbol.spliceOwner, className, parents.map(_.tpe), methodSymbols, None 47 | ) 48 | val classDef = ClassDef(classSymbol, parents, methodImpls(classSymbol)) 49 | Block( 50 | List(classDef), 51 | Typed(Apply(Select(New(TypeIdent(classDef.symbol)), classSymbol.primaryConstructor), Nil), TypeTree.of[S]) 52 | ).asExprOf 53 | } 54 | 55 | private def methodBodyTerm[F[_]: Type, I: Type, S: Type]( 56 | using q: Quotes 57 | )( 58 | methodSymbol: q.reflect.Symbol, 59 | argss: List[List[q.reflect.Tree]], 60 | client: Expr[Request[I] => F[Response[I]]], 61 | fh: Expr[FailureHandler[F]], 62 | MF: Expr[Monad[F]] 63 | ): q.reflect.Term = { 64 | import q.reflect._ 65 | val methodReturnTpe = methodSymbol.tree.asInstanceOf[DefDef].returnTpt.tpe 66 | def codedArgument(a: Tree): Expr[F[I]] = unwrapVararg(resolveTypeMember( 67 | TypeRepr.of[S], Ref(a.symbol).tpe.widen 68 | )).asType match { case '[at] => 69 | '{ summonInline[Codec[at,I]].apply(${Ref.term(a.symbol.termRef).asExprOf[at]}).fold($fh.apply, $MF.pure) } 70 | } 71 | def codedArguments(terms: List[Tree]): Expr[F[Map[String, I]]] = { 72 | '{$MF.map(Traverse[List].sequence(${Expr.ofList(terms.map(t => '{ 73 | $MF.map(${codedArgument(t)})(${Literal(StringConstant(t.symbol.name)).asExprOf[String]} -> _) 74 | }))})(using $MF))(_.toMap)} 75 | } 76 | val (returnKind, returnType) = separateReturnType(TypeRepr.of[F], methodReturnTpe, false) 77 | val (returnKindCodec, returnTypeCodec) = inferReturnCodecs( 78 | TypeRepr.of[F], TypeRepr.of[I], TypeRepr.of[F[I]], 79 | returnKind, returnType, methodReturnTpe, 80 | ) 81 | ( 82 | methodReturnTpe.asType, 83 | returnType.asType, 84 | ) match { case ('[rtt], '[rt]) => 85 | Apply(TypeApply(Select.unique(returnKindCodec, "apply"), List(TypeTree.of[rt])), List('{ 86 | $MF.flatMap( 87 | $MF.flatMap($MF.map( 88 | ${codedArguments(argss.flatten)})(args => 89 | Request[I]( 90 | ${Literal(StringConstant(TypeRepr.of[S].show)).asExprOf[String]}, 91 | ${Literal(StringConstant(methodSymbol.name)).asExprOf[String]}, 92 | args 93 | ) 94 | ))(a => $client.apply(a)) 95 | )(a => {${returnTypeCodec.asExprOf[Codec[I, rt]]}.apply((a: Response[I]).value)}.fold($fh.apply, $MF.pure)) 96 | }.asTerm)) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /core/src-3/poppet/core/ProcessorMacro.scala: -------------------------------------------------------------------------------- 1 | package poppet.core 2 | 3 | import scala.quoted.* 4 | import poppet.* 5 | 6 | object ProcessorMacro { 7 | def getAbstractMethods[S: Type](using q: Quotes): List[q.reflect.DefDef] = { 8 | import q.reflect._ 9 | TypeRepr.of[S].typeSymbol.typeMembers.view.filter(_.flags.is(Flags.Deferred)).foreach { t => 10 | report.throwError(s"Abstract types are not supported: ${TypeRepr.of[S].show}.${t.name}") 11 | } 12 | val methods = TypeRepr.of[S].typeSymbol.memberMethods.filter(s => { 13 | s.flags.is(Flags.Method) && (s.flags.is(Flags.Deferred) || s.flags.is(Flags.Abstract)) 14 | }).sortBy(_.fullName).map(_.tree).collect { 15 | case m : DefDef if m.paramss.size == m.termParamss.size => m 16 | case m : DefDef => report.throwError( 17 | s"Generic methods are not supported: ${TypeRepr.of[S].show}.${m.name}" 18 | ) 19 | } 20 | if (methods.isEmpty) report.throwError( 21 | s"${TypeRepr.of[S].show} has no abstract methods. Make sure that service method is parametrized with a trait." 22 | ) 23 | if (methods.map(m => (m.name, m.paramss.flatMap(_.params).map(_.name.toString))).toSet.size != methods.size) 24 | report.throwError("Use unique argument name lists for overloaded methods.") 25 | methods 26 | } 27 | 28 | def unwrapVararg(using q: Quotes)(tpe: q.reflect.TypeRepr) = { 29 | import q.reflect._ 30 | tpe match { 31 | case AnnotatedType(tpeP, t) if t.tpe.typeSymbol == defn.RepeatedAnnot => 32 | TypeRepr.of[Seq].appliedTo(tpeP.typeArgs) 33 | case tpe if tpe.typeSymbol == defn.RepeatedParamClass => 34 | TypeRepr.of[Seq].appliedTo(tpe.typeArgs) 35 | case _ => tpe 36 | } 37 | } 38 | 39 | def resolveTypeMember( 40 | using q: Quotes)( 41 | owner: q.reflect.TypeRepr, 42 | member: q.reflect.TypeRepr, 43 | ): q.reflect.TypeRepr = { 44 | val declarationOwner = owner.baseType(member.typeSymbol.maybeOwner) 45 | member.substituteTypes(declarationOwner.typeSymbol.memberTypes, declarationOwner.typeArgs) 46 | } 47 | 48 | def separateReturnType( 49 | using q: Quotes)(fType: q.reflect.TypeRepr, returnType: q.reflect.TypeRepr, fromReturn: Boolean 50 | ): (q.reflect.TypeRepr, q.reflect.TypeRepr) = { 51 | import q.reflect._ 52 | (returnType match { 53 | case AppliedType(tycon, List(arg)) => Option(tycon -> arg) 54 | case _ => None 55 | }).flatMap { case (tycon, arg) => 56 | TypeRepr.of[CodecK].appliedTo( 57 | if (fromReturn) List(tycon, fType) else List(fType, tycon) 58 | ).asType match { case '[ct] => 59 | Expr.summon[ct].map(_ => tycon -> arg) 60 | } 61 | }.getOrElse(TypeRepr.of[cats.Id] -> returnType) 62 | } 63 | 64 | def summonImplicit[A: Type](using q: Quotes): Option[Expr[A]] = { 65 | import q.reflect._ 66 | Implicits.search(TypeRepr.of[A]) match { 67 | case iss: ImplicitSearchSuccess => Option(iss.tree.asExpr.asInstanceOf[Expr[A]]) 68 | case isf: AmbiguousImplicits => report.throwError(isf.explanation) 69 | case isf: ImplicitSearchFailure => None 70 | } 71 | } 72 | 73 | def inferReturnCodecs( 74 | using q: Quotes)( 75 | fType: q.reflect.TypeRepr, faType: q.reflect.TypeRepr, ffaType: q.reflect.TypeRepr, 76 | tType: q.reflect.TypeRepr, taType: q.reflect.TypeRepr, ttaType: q.reflect.TypeRepr, 77 | ): (q.reflect.Term, q.reflect.Term) = { 78 | import q.reflect._ 79 | ( 80 | TypeRepr.of[CodecK].appliedTo(List(fType, tType)).asType, 81 | TypeRepr.of[Codec].appliedTo(List(faType, taType)).asType, 82 | ) match { case ('[ckt], '[ct]) => 83 | val codecK = summonImplicit[ckt] 84 | val codec = summonImplicit[ct] 85 | if (codecK.nonEmpty && codec.nonEmpty) (codecK.get.asTerm, codec.get.asTerm) 86 | else { 87 | def typeConstructorAndArgs(t: q.reflect.TypeRepr) = t match { 88 | case AppliedType(tycon, args) => Option(tycon -> args) 89 | case _ => None 90 | } 91 | def showType(t: q.reflect.TypeRepr) = t match { 92 | case t if t =:= TypeRepr.of[cats.Id] => "cats.Id" 93 | case TypeLambda(_, _, AppliedType(hkt, _)) => hkt.show 94 | case _ => t.show 95 | } 96 | val taTypeConstructorAndArgs = typeConstructorAndArgs(taType) 97 | val faTypeConstructorAndArgs = typeConstructorAndArgs(faType) 98 | val ttaTypeConstructorAndArgs = typeConstructorAndArgs(ttaType) 99 | val ffaTypeConstructorAndArgs = typeConstructorAndArgs(ffaType) 100 | report.throwError( 101 | s"Unable to convert ${showType(ffaType)} to ${showType(ttaType)}. Try to provide " + 102 | (if (codecK.isEmpty) s"poppet.CodecK[${showType(fType)},${showType(tType)}]" else "") + 103 | (if (codecK.isEmpty && codec.isEmpty) " with " else "") + 104 | (if (codec.isEmpty) s"poppet.Codec[${showType(faType)},${showType(taType)}]" else "") + 105 | (if ( 106 | !ttaTypeConstructorAndArgs.exists(_._1 =:= TypeRepr.of[cats.Id]) && 107 | tType =:= TypeRepr.of[cats.Id] && 108 | taTypeConstructorAndArgs.exists(_._2.size == 1) 109 | ) { 110 | val stType = taType match { 111 | case AppliedType(tycon, _) => Option(tycon) 112 | case _ => None 113 | } 114 | val staType = taTypeConstructorAndArgs.get._2.head 115 | val scodec = TypeRepr.of[Codec].appliedTo(List(faType, staType)).asType match { 116 | case ('[t]) => Expr.summon[t] 117 | } 118 | s" or poppet.CodecK[${showType(fType)},${showType(stType.get)}]" + 119 | (if (scodec.isEmpty) s" with poppet.Codec[${showType(faType)},${showType(staType)}]" else "") 120 | } else "") + 121 | (if ( 122 | !ffaTypeConstructorAndArgs.exists(_._1 =:= TypeRepr.of[cats.Id]) && 123 | fType =:= TypeRepr.of[cats.Id] && 124 | faTypeConstructorAndArgs.exists(_._2.size == 1) 125 | ) { 126 | val stType = faType match { 127 | case AppliedType(tycon, _) => Option(tycon) 128 | case _ => None 129 | } 130 | val staType = faTypeConstructorAndArgs.get._2.head 131 | val scodec = TypeRepr.of[Codec].appliedTo(List(staType, taType)).asType match { 132 | case ('[t]) => Expr.summon[t] 133 | } 134 | s" or poppet.CodecK[${showType(stType.get)},${showType(tType)}]" + 135 | (if (scodec.isEmpty) s" with poppet.Codec[${showType(staType)},${showType(taType)}]" else "") 136 | } else "") + 137 | "." 138 | ) 139 | } 140 | } 141 | 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /core/src-3/poppet/provider/core/ProviderProcessorObjectBinCompat.scala: -------------------------------------------------------------------------------- 1 | package poppet.provider.core 2 | 3 | import cats.Monad 4 | import cats.Traverse 5 | import poppet.provider.all._ 6 | import poppet.core.ProcessorMacro.* 7 | import scala.quoted.* 8 | import scala.compiletime.* 9 | 10 | trait ProviderProcessorObjectBinCompat { 11 | implicit inline def generate[F[_], I, S](implicit inline MF: Monad[F]): ProviderProcessor[F, I, S] = 12 | ${ ProviderProcessorObjectBinCompat.processorExpr('MF) } 13 | } 14 | 15 | object ProviderProcessorObjectBinCompat { 16 | def processorExpr[F[_]: Type, I: Type, S: Type]( 17 | using q: Quotes)(MF: Expr[Monad[F]] 18 | ): Expr[ProviderProcessor[F, I, S]] = '{ 19 | new ProviderProcessor[F, I, S] { 20 | override def apply(service: S, fh: FailureHandler[F]): List[MethodProcessor[F, I]] = 21 | ${ ProviderProcessorObjectBinCompat.methodProcessorsImpl[F, I, S]('service, 'fh, MF) } 22 | } 23 | } 24 | 25 | def methodProcessorsImpl[F[_] : Type, I : Type, S : Type]( 26 | using q: Quotes)(service: Expr[S], fh: Expr[FailureHandler[F]], MF: Expr[Monad[F]] 27 | ): Expr[List[poppet.provider.core.MethodProcessor[F, I]]] = { 28 | import q.reflect._ 29 | val serviceName = TypeRepr.of[S].show 30 | val methodProcessors = getAbstractMethods[S].map { m => 31 | def decodeArg(arg: ValDef): Expr[Map[String, I] => F[Any]] = { 32 | unwrapVararg(resolveTypeMember(TypeRepr.of[S], arg.tpt.tpe)).asType match { case '[at] => '{ input => 33 | summonInline[Codec[I, at]] 34 | .apply(input(${Literal(StringConstant(arg.name)).asExprOf[String]})) 35 | .fold($fh.apply, $MF.pure) 36 | }} 37 | } 38 | val decodeArgs: Expr[Map[String, I] => F[List[Any]]] = '{ input => 39 | Traverse[List].sequence( 40 | ${Expr.ofList(m.termParamss.flatMap(_.params).map(decodeArg))}.map(_(input)) 41 | )(using $MF) 42 | } 43 | val (returnKind, returnType) = separateReturnType( 44 | TypeRepr.of[F], resolveTypeMember(TypeRepr.of[S], m.returnTpt.tpe), true 45 | ) 46 | val (returnKindCodec, returnTypeCodec) = inferReturnCodecs( 47 | returnKind, returnType, m.returnTpt.tpe, 48 | TypeRepr.of[F], TypeRepr.of[I], TypeRepr.of[F[I]], 49 | ) 50 | val callService: Expr[Map[String, I] => F[I]] = returnType.asType match { case '[rt] => 51 | val paramTypes = m.termParamss.map(_.params.map(arg => 52 | arg -> resolveTypeMember(TypeRepr.of[S], arg.tpt.tpe) 53 | )) 54 | '{ input => 55 | $MF.flatMap( 56 | $decodeArgs(input))(ast => $MF.flatMap( 57 | ${Apply(TypeApply( 58 | Select.unique(returnKindCodec, "apply"), 59 | List(TypeTree.of[rt])), 60 | List(paramTypes.foldLeft[(Term, Int)]( 61 | Select(service.asTerm, m.symbol) -> 0)((acc, item) => ( 62 | Apply(acc._1, item.zipWithIndex.map{ case ((arg, t), i) => 63 | t.asType match { case '[at] => 64 | val term = '{ast.apply(${Literal(IntConstant(i + acc._2)).asExprOf[Int]}).asInstanceOf[at]}.asTerm 65 | arg.tpt.tpe match { 66 | case AnnotatedType(tpeP, t) if t.tpe.typeSymbol == defn.RepeatedAnnot => 67 | Typed(term, Inferred(defn.RepeatedParamClass.typeRef.appliedTo(tpeP.typeArgs))) 68 | case _ => term 69 | } 70 | } 71 | }), 72 | (item.size + acc._2) 73 | ) 74 | )._1) 75 | ).asExprOf[F[rt]]})( 76 | ${returnTypeCodec.asExprOf[Codec[rt, I]]}.apply(_).fold($fh.apply, $MF.pure) 77 | ) 78 | ) 79 | } 80 | } 81 | '{MethodProcessor[F, I]( 82 | ${Literal(StringConstant(serviceName)).asExprOf[String]}, 83 | ${Literal(StringConstant(m.name)).asExprOf[String]}, 84 | ${Expr.ofList(m.paramss.flatMap(_.params).map(n => Literal(StringConstant(n.name)).asExprOf[String]))}, 85 | input => $callService(input) 86 | )} 87 | }.toList 88 | Expr.ofList(methodProcessors) 89 | } 90 | } -------------------------------------------------------------------------------- /core/src/poppet/CoreDsl.scala: -------------------------------------------------------------------------------- 1 | package poppet 2 | 3 | trait CoreDsl { 4 | type Codec[A, B] = core.Codec[A, B] 5 | type CodecK[F[_], G[_]] = core.CodecK[F, G] 6 | type Failure = core.Failure 7 | type CodecFailure[I] = core.CodecFailure[I] 8 | type FailureHandler[F[_]] = core.FailureHandler[F] 9 | type Request[I] = core.Request[I] 10 | type Response[I] = core.Response[I] 11 | 12 | val FailureHandler = core.FailureHandler 13 | val Request = core.Request 14 | val Response = core.Response 15 | } 16 | -------------------------------------------------------------------------------- /core/src/poppet/consumer/ConsumerDsl.scala: -------------------------------------------------------------------------------- 1 | package poppet.consumer 2 | 3 | trait ConsumerDsl { 4 | type Consumer[F[_], I, S] = core.Consumer[F, I, S] 5 | type Transport[F[_], I] = Request[I] => F[Response[I]] 6 | 7 | val Consumer = core.Consumer 8 | } 9 | -------------------------------------------------------------------------------- /core/src/poppet/consumer/all/all.scala: -------------------------------------------------------------------------------- 1 | package poppet.consumer 2 | 3 | import poppet.CoreDsl 4 | 5 | package object all extends CoreDsl with ConsumerDsl 6 | -------------------------------------------------------------------------------- /core/src/poppet/consumer/core/Consumer.scala: -------------------------------------------------------------------------------- 1 | package poppet.consumer.core 2 | 3 | import cats.implicits._ 4 | import cats.Monad 5 | import poppet.consumer.all._ 6 | 7 | /** 8 | * @param transport function that transfers data to the provider 9 | * 10 | * @tparam F consumer data kind, for example Future[_] 11 | * @tparam I intermediate data type, for example Json 12 | * @tparam S service type, for example HelloService 13 | */ 14 | class Consumer[F[_]: Monad, I, S]( 15 | transport: Transport[F, I], 16 | fh: FailureHandler[F], 17 | processor: ConsumerProcessor[F, I, S] 18 | ) { 19 | def service: S = processor(transport, fh) 20 | } 21 | 22 | object Consumer { 23 | 24 | def apply[F[_], I]( 25 | client: Transport[F, I], 26 | fh: FailureHandler[F] = FailureHandler.throwing[F] 27 | )(implicit 28 | F: Monad[F], 29 | ): Builder[F, I] = new Builder[F, I](client, fh) 30 | 31 | class Builder[F[_], I]( 32 | client: Transport[F, I], 33 | fh: FailureHandler[F] 34 | )(implicit 35 | F: Monad[F], 36 | ) { 37 | def service[S](implicit processor: ConsumerProcessor[F, I, S]): S = 38 | new Consumer(client, fh, processor).service 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /core/src/poppet/consumer/core/ConsumerProcessor.scala: -------------------------------------------------------------------------------- 1 | package poppet.consumer.core 2 | 3 | import poppet.consumer.all._ 4 | import poppet.core.Request 5 | import poppet.core.Response 6 | 7 | trait ConsumerProcessor[F[_], I, S] { 8 | def apply(client: Request[I] => F[Response[I]], fh: FailureHandler[F]): S 9 | } 10 | 11 | object ConsumerProcessor extends ConsumerProcessorObjectBinCompat { 12 | def apply[F[_], I, S](implicit instance: ConsumerProcessor[F, I, S]): ConsumerProcessor[F, I, S] = instance 13 | } 14 | -------------------------------------------------------------------------------- /core/src/poppet/consumer/package.scala: -------------------------------------------------------------------------------- 1 | package poppet 2 | 3 | package object consumer extends CoreDsl with ConsumerDsl 4 | -------------------------------------------------------------------------------- /core/src/poppet/core/Codec.scala: -------------------------------------------------------------------------------- 1 | package poppet.core 2 | 3 | trait Codec[A, B] { 4 | def apply(a: A): Either[CodecFailure[A], B] 5 | } 6 | 7 | object Codec { 8 | implicit def codecIdentityInstance[A]: Codec[A, A] = new Codec[A, A] { 9 | override def apply(a: A): Either[CodecFailure[A], A] = Right(a) 10 | } 11 | } -------------------------------------------------------------------------------- /core/src/poppet/core/CodecK.scala: -------------------------------------------------------------------------------- 1 | package poppet.core 2 | 3 | trait CodecK[F[_], G[_]] { 4 | def apply[A](a: F[A]): G[A] 5 | } 6 | 7 | object CodecK { 8 | implicit def codecKIdentityInstance[F[_]]: CodecK[F, F] = new CodecK[F, F] { 9 | override def apply[A](a: F[A]): F[A] = a 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/src/poppet/core/Failure.scala: -------------------------------------------------------------------------------- 1 | package poppet.core 2 | 3 | class Failure(message: String, e: Throwable) extends Exception(message, e) { 4 | def this(message: String) = this(message, null) 5 | } 6 | 7 | class CodecFailure[I](message: String, val data: I, e: Throwable) extends Failure(message, e) { 8 | def this(message: String, data: I) = this(message, data, null) 9 | 10 | def withData[II](data: II): CodecFailure[II] = new CodecFailure(message, data, e) 11 | } 12 | -------------------------------------------------------------------------------- /core/src/poppet/core/FailureHandler.scala: -------------------------------------------------------------------------------- 1 | package poppet.core 2 | 3 | trait FailureHandler[F[_]] { 4 | def apply[A](f: Failure): F[A] 5 | } 6 | 7 | object FailureHandler { 8 | def throwing[F[_]]: FailureHandler[F] = new FailureHandler[F] { 9 | override def apply[A](f: Failure): F[A] = throw f 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/src/poppet/core/Request.scala: -------------------------------------------------------------------------------- 1 | package poppet.core 2 | 3 | case class Request[I]( 4 | service: String, method: String, arguments: Map[String, I] 5 | ) 6 | -------------------------------------------------------------------------------- /core/src/poppet/core/Response.scala: -------------------------------------------------------------------------------- 1 | package poppet.core 2 | 3 | case class Response[I](value: I) 4 | -------------------------------------------------------------------------------- /core/src/poppet/package.scala: -------------------------------------------------------------------------------- 1 | package object poppet extends CoreDsl 2 | -------------------------------------------------------------------------------- /core/src/poppet/provider/ProviderDsl.scala: -------------------------------------------------------------------------------- 1 | package poppet.provider 2 | 3 | trait ProviderDsl { 4 | type Provider[F[_], I] = core.Provider[F, I] 5 | type Server[F[_], I] = Request[I] => F[Response[I]] 6 | 7 | val Provider = core.Provider 8 | } 9 | -------------------------------------------------------------------------------- /core/src/poppet/provider/all/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.provider 2 | 3 | import poppet.CoreDsl 4 | 5 | package object all extends CoreDsl with ProviderDsl 6 | -------------------------------------------------------------------------------- /core/src/poppet/provider/core/Provider.scala: -------------------------------------------------------------------------------- 1 | package poppet.provider.core 2 | 3 | import cats.data.OptionT 4 | import cats.implicits._ 5 | import cats.Monad 6 | import poppet.core.Request 7 | import poppet.core.Response 8 | import poppet.provider.all._ 9 | import poppet.provider.core.Provider._ 10 | 11 | /** 12 | * @tparam F service data kind, for example Future 13 | * @tparam I intermediate data type, for example Json 14 | */ 15 | class Provider[F[_]: Monad, I]( 16 | fh: FailureHandler[F], 17 | processors: List[MethodProcessor[F, I]] 18 | ) extends Server[F, I] { 19 | 20 | private val indexedProcessors: Map[String, Map[String, Map[String, Map[String, I] => F[I]]]] = 21 | processors.groupBy(_.service).mapValues( 22 | _.groupBy(_.name).mapValues( 23 | _.map(m => m.arguments.sorted.mkString(",") -> m.f).toMap 24 | ).toMap 25 | ).toMap 26 | 27 | private def processorNotFoundFailure(processor: String, in: String): Failure = new Failure( 28 | s"Requested processor $processor is not in $in. Make sure that desired service is provided and up to date." 29 | ) 30 | 31 | def apply(request: Request[I]): F[Response[I]] = for { 32 | serviceProcessors <- OptionT.fromOption[F](indexedProcessors.get(request.service)) 33 | .getOrElseF(fh(processorNotFoundFailure( 34 | request.service, 35 | s"[${indexedProcessors.keySet.mkString(",")}]" 36 | ))) 37 | methodProcessors <- OptionT.fromOption[F](serviceProcessors.get(request.method)) 38 | .getOrElseF(fh(processorNotFoundFailure( 39 | s"${request.service}.${request.method}", 40 | s"${request.service}.[${serviceProcessors.keySet.mkString(",")}]" 41 | ))) 42 | processor <- OptionT.fromOption[F](methodProcessors.get(request.arguments.keys.toList.sorted.mkString(","))) 43 | .getOrElseF(fh(processorNotFoundFailure( 44 | s"${request.service}.${request.method}(${request.arguments.keys.toList.sorted.mkString(",")})", 45 | s"${request.service}.${request.method}[${methodProcessors.keySet.map(p => s"($p)").mkString(",")}]" 46 | ))) 47 | value <- processor(request.arguments) 48 | } yield Response(value) 49 | 50 | def service[S](s: S)(implicit processor: ProviderProcessor[F, I, S]) = 51 | new Provider[F, I](fh, processors ::: processor(s, fh)) 52 | } 53 | 54 | object Provider { 55 | 56 | def apply[F[_]: Monad, I]( 57 | fh: FailureHandler[F] = FailureHandler.throwing[F] 58 | ): Builder[F, I] = new Builder[F, I](fh) 59 | 60 | class Builder[F[_]: Monad, I](fh: FailureHandler[F]) { 61 | def service[S](s: S)(implicit processor: ProviderProcessor[F, I, S]): Provider[F, I] = 62 | new Provider[F, I](fh, Nil).service(s) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /core/src/poppet/provider/core/ProviderProcessor.scala: -------------------------------------------------------------------------------- 1 | package poppet.provider.core 2 | 3 | import poppet.core.FailureHandler 4 | 5 | trait ProviderProcessor[F[_], I, S] { 6 | def apply(service: S, fh: FailureHandler[F]): List[MethodProcessor[F, I]] 7 | } 8 | 9 | class MethodProcessor[F[_], I]( 10 | val service: String, val name: String, val arguments: List[String], val f: Map[String, I] => F[I] 11 | ) 12 | 13 | object ProviderProcessor extends ProviderProcessorObjectBinCompat { 14 | def apply[F[_], I, S](implicit instance: ProviderProcessor[F, I, S]): ProviderProcessor[F, I, S] = instance 15 | } 16 | -------------------------------------------------------------------------------- /core/src/poppet/provider/package.scala: -------------------------------------------------------------------------------- 1 | package poppet 2 | 3 | package object provider extends CoreDsl with ProviderDsl 4 | -------------------------------------------------------------------------------- /core/test/src/poppet/PoppetSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet 2 | 3 | import org.scalatest.Assertions 4 | import scala.concurrent.ExecutionContextExecutor 5 | 6 | trait PoppetSpec extends Assertions { 7 | implicit val runNowEc: ExecutionContextExecutor = new ExecutionContextExecutor { 8 | def execute(runnable: Runnable): Unit = { 9 | try runnable.run() 10 | catch { case t: Throwable => reportFailure(t) } 11 | } 12 | def reportFailure(t: Throwable): Unit = t.printStackTrace() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/test/src/poppet/codec/CodecSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec 2 | 3 | import poppet.codec.CodecSpec.A 4 | import poppet.core._ 5 | 6 | trait CodecSpec { 7 | val stringExample = "a" 8 | val intExample = 1 9 | val caseClassExample = A(stringExample, intExample) 10 | 11 | def assertCustomCodec[I, A](value: A)(implicit 12 | fc: Codec[A, I], 13 | bc: Codec[I, A], 14 | ): Unit = { 15 | val actual = fc(value).flatMap(bc(_)) 16 | assert(actual == Right(value), s"$actual != ${Right(value)}") 17 | } 18 | 19 | } 20 | 21 | object CodecSpec { 22 | case class A(a: String, b: Int) 23 | } 24 | -------------------------------------------------------------------------------- /core/test/src/poppet/consumer/ConsumerProcessorSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet.consumer 2 | 3 | import cats._ 4 | import cats.data.EitherT 5 | import org.scalatest.freespec.AsyncFreeSpec 6 | import poppet.consumer.core.ConsumerProcessor 7 | import poppet.core.ProcessorSpec 8 | import poppet.core.ProcessorSpec._ 9 | import poppet.core.Request 10 | import poppet.core.Response 11 | import scala.concurrent.Future 12 | 13 | class ConsumerProcessorSpec extends AsyncFreeSpec with ProcessorSpec { 14 | "Consumer processor" - { 15 | "should generate instance" - { 16 | implicit val c0: Codec[String, Int] = a => Right(a.toInt) 17 | implicit val c1: Codec[String, List[Int]] = a => Right(List(a.toInt)) 18 | implicit val c2: Codec[String, SimpleDto] = a => Right(SimpleDto(a.toInt)) 19 | implicit val c3: Codec[String, List[String]] = a => Right(List(a)) 20 | implicit val cp0: Codec[Boolean, String] = a => Right(a.toString) 21 | implicit val cp1: Codec[Option[Boolean], String] = a => Right(a.getOrElse(false).toString) 22 | implicit val cp2: Codec[Seq[Boolean], String] = a => Right(a.mkString(",")) 23 | var request: Request[String] = null 24 | 25 | "when has id data kind" - { 26 | val client: Request[String] => Response[String] = r => { 27 | request = r 28 | Response("0") 29 | } 30 | "for methods with different arguments number" in { 31 | val a = ConsumerProcessor.generate[Id, String, Simple].apply(client, FailureHandler.throwing) 32 | 33 | assert(a.a0 == 0 && request == Request[String]( 34 | "poppet.core.ProcessorSpec.Simple", 35 | "a0", 36 | Map.empty 37 | )) 38 | assert(a.a00() == List(0) && request == Request[String]( 39 | "poppet.core.ProcessorSpec.Simple", 40 | "a00", 41 | Map.empty 42 | )) 43 | assert(a.a1(true) == SimpleDto(0) && request == Request( 44 | "poppet.core.ProcessorSpec.Simple", 45 | "a1", 46 | Map("b" -> "true") 47 | )) 48 | assert(a.a2(true, None) == List("0") && request == Request( 49 | "poppet.core.ProcessorSpec.Simple", 50 | "a2", 51 | Map("b0" -> "true", "b1" -> "false") 52 | )) 53 | } 54 | "for methods with future kind" in { 55 | implicit val ck: CodecK[Id, Future] = new CodecK[Id, Future] { 56 | override def apply[A](a: Id[A]): Future[A] = Future.successful(a) 57 | } 58 | val a = ConsumerProcessor[Id, String, WithFutureKind].apply(client, FailureHandler.throwing) 59 | 60 | assert(a.a0.value.get.get == 0 && request == Request[String]( 61 | "poppet.core.ProcessorSpec.WithFutureKind", 62 | "a0", 63 | Map.empty 64 | )) 65 | assert(a.a1.value.get.get == List(0) && request == Request[String]( 66 | "poppet.core.ProcessorSpec.WithFutureKind", 67 | "a1", 68 | Map.empty 69 | )) 70 | } 71 | "for methods with multiple argument lists" in { 72 | val a = 73 | ConsumerProcessor[Id, String, WithMultipleArgumentLists].apply(client, FailureHandler.throwing) 74 | 75 | assert(a.a0(true)(false) == 0 && request == Request( 76 | "poppet.core.ProcessorSpec.WithMultipleArgumentLists", 77 | "a0", 78 | Map( 79 | "b0" -> "true", 80 | "b1" -> "false" 81 | ) 82 | )) 83 | assert(a.a1(true)()(false) == List(0) && request == Request( 84 | "poppet.core.ProcessorSpec.WithMultipleArgumentLists", 85 | "a1", 86 | Map( 87 | "b0" -> "true", 88 | "b1" -> "false" 89 | ) 90 | )) 91 | assert(a.a2(true)(false, true) == SimpleDto(0) && request == Request( 92 | "poppet.core.ProcessorSpec.WithMultipleArgumentLists", 93 | "a2", 94 | Map( 95 | "b0" -> "true", 96 | "b10" -> "false", 97 | "b11" -> "true" 98 | ) 99 | )) 100 | } 101 | "for methods with default arguments" in { 102 | val a: WithDefaultArguments = 103 | ConsumerProcessor[Id, String, WithDefaultArguments].apply(client, FailureHandler.throwing) 104 | 105 | assert(a.a0(false) == 0 && request == Request( 106 | "poppet.core.ProcessorSpec.WithDefaultArguments", 107 | "a0", 108 | Map("b" -> "false") 109 | )) 110 | assert(a.a0() == 0 && request == Request( 111 | "poppet.core.ProcessorSpec.WithDefaultArguments", 112 | "a0", 113 | Map("b" -> "true") 114 | )) 115 | assert(a.a1(false) == List(0) && request == Request( 116 | "poppet.core.ProcessorSpec.WithDefaultArguments", 117 | "a1", 118 | Map("b0" -> "false", "b1" -> "true") 119 | )) 120 | assert(a.a1(true, false) == List(0) && request == Request( 121 | "poppet.core.ProcessorSpec.WithDefaultArguments", 122 | "a1", 123 | Map("b0" -> "true", "b1" -> "false") 124 | )) 125 | assert(a.a2(false, false) == SimpleDto(0) && request == Request( 126 | "poppet.core.ProcessorSpec.WithDefaultArguments", 127 | "a2", 128 | Map( 129 | "b0" -> "false", 130 | "b1" -> "false", 131 | "b2" -> "true", 132 | "b3" -> "true" 133 | ) 134 | )) 135 | assert(a.a2(true, true, false) == SimpleDto(0) && request == Request( 136 | "poppet.core.ProcessorSpec.WithDefaultArguments", 137 | "a2", 138 | Map( 139 | "b0" -> "true", 140 | "b1" -> "true", 141 | "b2" -> "false", 142 | "b3" -> "true" 143 | ) 144 | )) 145 | assert(a.a2(true, true, false, false) == SimpleDto(0) && request == Request( 146 | "poppet.core.ProcessorSpec.WithDefaultArguments", 147 | "a2", 148 | Map( 149 | "b0" -> "true", 150 | "b1" -> "true", 151 | "b2" -> "false", 152 | "b3" -> "false" 153 | ) 154 | )) 155 | } 156 | "for traits with generic hierarchy" in { 157 | val a = ConsumerProcessor[Id, String, WithParentWithParameters] 158 | .apply(client, FailureHandler.throwing) 159 | 160 | assert(a.a0(false) == 0 && request == Request( 161 | "poppet.core.ProcessorSpec.WithParentWithParameters", 162 | "a0", 163 | Map("b0" -> "false") 164 | )) 165 | assert(a.a1 == 0 && request == Request[String]( 166 | "poppet.core.ProcessorSpec.WithParentWithParameters", 167 | "a1", 168 | Map.empty 169 | )) 170 | } 171 | "for methods with varargs" in { 172 | val a = ConsumerProcessor[Id, String, WithVarargs] 173 | .apply(client, FailureHandler.throwing) 174 | 175 | assert(a.a0(false, true) == 0 && request == Request( 176 | "poppet.core.ProcessorSpec.WithVarargs", 177 | "a0", 178 | Map("b" -> "false,true") 179 | )) 180 | } 181 | } 182 | "when has future data kind" in { 183 | import cats.implicits._ 184 | import scala.concurrent.Future 185 | 186 | implicit val ck0: CodecK[Future, cats.Id] = new CodecK[Future, cats.Id] { 187 | override def apply[A](a: Future[A]): Id[A] = a.value.get.get 188 | } 189 | 190 | val a = ConsumerProcessor[Future, String, Simple].apply( 191 | r => { 192 | request = r 193 | Future.successful(Response("0")) 194 | }, 195 | FailureHandler.throwing 196 | ) 197 | 198 | assert(a.a0 == 0 && request == Request[String]( 199 | "poppet.core.ProcessorSpec.Simple", 200 | "a0", 201 | Map.empty 202 | )) 203 | assert(a.a00() == List(0) && request == Request[String]( 204 | "poppet.core.ProcessorSpec.Simple", 205 | "a00", 206 | Map.empty 207 | )) 208 | assert(a.a1(true) == SimpleDto(0) && request == Request( 209 | "poppet.core.ProcessorSpec.Simple", 210 | "a1", 211 | Map("b" -> "true") 212 | )) 213 | assert(a.a2(true, Option(false)) == List("0") && request == Request( 214 | "poppet.core.ProcessorSpec.Simple", 215 | "a2", 216 | Map("b0" -> "true", "b1" -> "false") 217 | )) 218 | } 219 | "when has complex data kind" in { 220 | import cats.implicits._ 221 | 222 | implicit val ck: CodecK[WithComplexReturnTypes.ReturnType, cats.Id] = 223 | new CodecK[WithComplexReturnTypes.ReturnType, cats.Id] { 224 | override def apply[A](a: WithComplexReturnTypes.ReturnType[A]): Id[A] = 225 | a.value.value.get.get.toOption.get 226 | } 227 | 228 | val p = ConsumerProcessor[WithComplexReturnTypes.ReturnType, String, WithComplexReturnTypes].apply( 229 | r => { 230 | request = r 231 | EitherT.pure(Response("0")) 232 | }, 233 | FailureHandler.throwing 234 | ) 235 | 236 | def result[A](value: WithComplexReturnTypes.ReturnType[A]): A = 237 | value.value.value.get.get.toOption.get 238 | 239 | assert(result(p.a0(true)) == 0 && request == Request( 240 | "poppet.core.ProcessorSpec.WithComplexReturnTypes", 241 | "a0", 242 | Map("b" -> "true") 243 | )) 244 | assert(result(p.a1(true, false)) == 0 && request == Request( 245 | "poppet.core.ProcessorSpec.WithComplexReturnTypes", 246 | "a1", 247 | Map("b0" -> "true", "b1" -> "false") 248 | )) 249 | } 250 | "when has A data kind and service has B data kind" in { 251 | import cats.implicits._ 252 | 253 | type F[A] = Option[A] 254 | type G[A] = WithComplexReturnTypes.ReturnType[A] 255 | 256 | implicit val ck0: CodecK[F, G] = new CodecK[F, G] { 257 | override def apply[A](a: F[A]): G[A] = EitherT.fromEither(a.toRight("not found")) 258 | } 259 | implicit val ck1: CodecK[G, F] = new CodecK[G, F] { 260 | override def apply[A](a: G[A]): F[A] = a.value.value.get.get.toOption 261 | } 262 | 263 | val p = ConsumerProcessor[F, String, WithComplexReturnTypes].apply( 264 | r => { 265 | request = r 266 | Option(Response("0")) 267 | }, 268 | FailureHandler.throwing 269 | ) 270 | 271 | def result[A](value: WithComplexReturnTypes.ReturnType[A]): A = 272 | value.value.value.get.get.toOption.get 273 | 274 | assert(result(p.a0(true)) == 0 && request == Request( 275 | "poppet.core.ProcessorSpec.WithComplexReturnTypes", 276 | "a0", 277 | Map("b" -> "true") 278 | )) 279 | assert(result(p.a1(true, true)) == 0 && request == Request( 280 | "poppet.core.ProcessorSpec.WithComplexReturnTypes", 281 | "a1", 282 | Map("b0" -> "true", "b1" -> "true") 283 | )) 284 | } 285 | } 286 | 287 | "shouldn't generate instance" - { 288 | "when has id data kind" - { 289 | "for trait with generic methods" in { 290 | assertCompilationErrorMessage( 291 | assertCompiles("""ConsumerProcessor[Id, String, WithMethodWithParameters]"""), 292 | "Generic methods are not supported: " + 293 | "poppet.core.ProcessorSpec.WithMethodWithParameters.a" 294 | ) 295 | } 296 | "for trait without abstract methods" in { 297 | assertCompilationErrorMessage( 298 | assertCompiles("""ConsumerProcessor[Id, String, WithNoAbstractMethods]"""), 299 | "poppet.core.ProcessorSpec.WithNoAbstractMethods has no abstract methods. " + 300 | "Make sure that service method is parametrized with a trait." 301 | ) 302 | } 303 | "for trait with conflicting methods" in { 304 | assertCompilationErrorMessage( 305 | assertCompiles("""ConsumerProcessor[Id, String, WithConflictedMethods]""".stripMargin), 306 | "Use unique argument name lists for overloaded methods." 307 | ) 308 | } 309 | "for trait with abstract type" in { 310 | assertCompilationErrorMessage( 311 | assertCompiles("""ConsumerProcessor[Id, String, WithAbstractType]"""), 312 | "Abstract types are not supported: " + 313 | "poppet.core.ProcessorSpec.WithAbstractType.A" 314 | ) 315 | } 316 | "for valid trait with ambiguous simple codec" in { 317 | implicit val c0: Codec[String, Int] = a => Right(a.toInt) 318 | implicit val c1: Codec[String, Int] = c0 319 | assertCompilationErrorMessage( 320 | assertCompiles("""ConsumerProcessor[Id, String, Simple]"""), 321 | """ambiguous implicit values: 322 | | both value c1 of type poppet.consumer.Codec[String,Int] 323 | | and value c0 of type poppet.consumer.Codec[String,Int] 324 | | match expected type poppet.Codec[String,Int]""".stripMargin, 325 | "both value c1 and value c0 match type poppet.core.Codec[String, Int]" 326 | ) 327 | } 328 | "for valid trait without simple codec" in { 329 | assertCompilationErrorMessagePattern( 330 | assertCompiles("""ConsumerProcessor[Id, String, Simple]"""), 331 | ("Unable to convert " + 332 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String) to " + 333 | "(scala.)?Int. Try to provide poppet.Codec\\[(scala.Predef.|java.lang.)?String,(scala.)?Int].").r 334 | ) 335 | } 336 | "for valid trait without codec for type with argument" in { 337 | implicit val c0: Codec[String, Int] = a => Right(a.toInt) 338 | assertCompilationErrorMessagePattern( 339 | assertCompiles("""ConsumerProcessor[Id, String, Simple]"""), 340 | ("Unable to convert " + 341 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String) to " + 342 | "(scala.collection.immutable.)?List\\[(scala.)?Int]. " + 343 | "Try to provide " + 344 | "poppet.Codec\\[(scala.Predef.|java.lang.)?String,(scala.collection.immutable.)?List\\[(scala.)?Int]] " + 345 | "or " + 346 | "poppet.CodecK\\[(\\[A\\])?(cats.)?Id(\\[A\\])?,(scala.collection.immutable.)?List].").r 347 | ) 348 | } 349 | "for valid trait without codec for simple type with explicit Id kind" in { 350 | implicit val c0: Codec[String, Int] = a => Right(a.toInt) 351 | implicit val c1: Codec[String, List[Int]] = a => Right(List(a.toInt)) 352 | implicit val c2: Codec[String, SimpleDto] = a => Right(SimpleDto(a.toInt)) 353 | assertCompilationErrorMessagePattern( 354 | assertCompiles("""ConsumerProcessor[Id, String, Simple]"""), 355 | ("Unable to convert " + 356 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String) to " + 357 | "(cats.)?Id\\[(scala.collection.immutable.)?List\\[(scala.Predef.|java.lang.)?String]]. " + 358 | "Try to provide " + 359 | "poppet.Codec\\[(scala.Predef.|java.lang.)?String,(scala.collection.immutable.)?List\\[(scala.Predef.|java.lang.)?String]].").r 360 | ) 361 | } 362 | "for valid trait without codec for simple type with Future kind" in { 363 | assertCompilationErrorMessagePattern( 364 | assertCompiles("""ConsumerProcessor[Id, String, WithFutureKind]"""), 365 | ("Unable to convert " + 366 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String) to " + 367 | "scala.concurrent.Future\\[(scala.)?Int]. " + 368 | "Try to provide " + 369 | "poppet.Codec\\[(scala.Predef.|java.lang.)?String,scala.concurrent.Future\\[(scala.)?Int]] " + 370 | "or " + 371 | "poppet.CodecK\\[(\\[A\\])?(cats.)?Id(\\[A\\])?,(\\[\\+T\\])?scala.concurrent.Future(\\[T\\])?] " + 372 | "with poppet.Codec\\[(scala.Predef.|java.lang.)?String,(scala.)?Int].").r 373 | ) 374 | } 375 | "for valid trait without codec for simple type with Future kind, but with codecK" in { 376 | implicit val ck = new CodecK[Id, Future] { 377 | override def apply[A](a: Id[A]): Future[A] = Future.successful(a) 378 | } 379 | assertCompilationErrorMessagePattern( 380 | assertCompiles("""ConsumerProcessor[Id, String, WithFutureKind]"""), 381 | ("Unable to convert " + 382 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String) to " + 383 | "scala.concurrent.Future\\[(scala.)?Int]. " + 384 | "Try to provide poppet.Codec\\[(scala.Predef.|java.lang.)?String,(scala.)?Int].").r 385 | ) 386 | } 387 | "for valid trait without codecK for simple type with Future kind, but with codec" in { 388 | implicit val c0: Codec[String, Int] = a => Right(a.toInt) 389 | assertCompilationErrorMessagePattern( 390 | assertCompiles("""ConsumerProcessor[Id, String, WithFutureKind]"""), 391 | ("Unable to convert " + 392 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String) to " + 393 | "scala.concurrent.Future\\[(scala.)?Int]. " + 394 | "Try to " + 395 | "provide poppet.Codec\\[(scala.Predef.|java.lang.)?String,scala.concurrent.Future\\[(scala.)?Int]] " + 396 | "or " + 397 | "poppet.CodecK\\[(\\[A\\])?(cats.)?Id(\\[A\\])?,(\\[\\+T\\])?scala.concurrent.Future(\\[T\\])?].").r 398 | ) 399 | } 400 | } 401 | "when has Future data kind" - { 402 | "for valid trait without simple codec" in { 403 | assertCompilationErrorMessagePattern( 404 | assertCompiles("""ConsumerProcessor[Future, String, Simple]"""), 405 | ("Unable to convert scala.concurrent.Future\\[(scala.Predef.|java.lang.)?String] to (scala.)?Int. " + 406 | "Try to provide " + 407 | "poppet.CodecK\\[(\\[\\+T\\])?scala.concurrent.Future(\\[T\\])?,(cats.)?Id] with " + 408 | "poppet.Codec\\[(scala.Predef.|java.lang.)?String,(scala.)?Int].").r 409 | ) 410 | } 411 | "for valid trait without simple codec, but with codecK" in { 412 | implicit val ck = new CodecK[Future, Id] { 413 | override def apply[A](a: Future[A]): Id[A] = a.value.get.get 414 | } 415 | assertCompilationErrorMessagePattern( 416 | assertCompiles("""ConsumerProcessor[Future, String, Simple]"""), 417 | ("Unable to convert scala.concurrent.Future\\[(scala.Predef.|java.lang.)?String] to (scala.)?Int. " + 418 | "Try to provide poppet.Codec\\[(scala.Predef.|java.lang.)?String,(scala.)?Int].").r 419 | ) 420 | } 421 | "for valid trait without simple codecK, but with codec" in { 422 | implicit val c0: Codec[String, Int] = a => Right(a.toInt) 423 | assertCompilationErrorMessagePattern( 424 | assertCompiles("""ConsumerProcessor[Future, String, Simple]"""), 425 | ("Unable to convert scala.concurrent.Future\\[(scala.Predef.|java.lang.)?String] to (scala.)?Int. " + 426 | "Try to provide poppet.CodecK\\[(\\[\\+T\\])?scala.concurrent.Future(\\[T\\])?,(cats.)?Id].").r 427 | ) 428 | } 429 | "for valid trait without codec for type with argument" in { 430 | implicit val ck = new CodecK[Future, Id] { 431 | override def apply[A](a: Future[A]): Id[A] = a.value.get.get 432 | } 433 | implicit val c0: Codec[String, Int] = a => Right(a.toInt) 434 | assertCompilationErrorMessagePattern( 435 | assertCompiles("""ConsumerProcessor[Future, String, Simple]"""), 436 | ("Unable to convert " + 437 | "scala.concurrent.Future\\[(scala.Predef.|java.lang.)?String] to " + 438 | "(scala.collection.immutable.)?List\\[(scala.)?Int]. " + 439 | "Try to provide " + 440 | "poppet.Codec\\[(scala.Predef.|java.lang.)?String,(scala.collection.immutable.)?List\\[(scala.)?Int]] " + 441 | "or " + 442 | "poppet.CodecK\\[(\\[\\+T\\])?scala.concurrent.Future(\\[T\\])?,(scala.collection.immutable.)?List].").r 443 | ) 444 | } 445 | "for valid trait without codec for simple type with explicit Id kind" in { 446 | implicit val ck = new CodecK[Future, Id] { 447 | override def apply[A](a: Future[A]): Id[A] = a.value.get.get 448 | } 449 | implicit val c0: Codec[String, Int] = a => Right(a.toInt) 450 | implicit val c1: Codec[String, List[Int]] = a => Right(List(a.toInt)) 451 | implicit val c2: Codec[String, SimpleDto] = a => Right(SimpleDto(a.toInt)) 452 | assertCompilationErrorMessagePattern( 453 | assertCompiles("""ConsumerProcessor[Future, String, Simple]"""), 454 | ("Unable to convert " + 455 | "scala.concurrent.Future\\[(scala.Predef.|java.lang.)?String] to " + 456 | "(cats.)?Id\\[(scala.collection.immutable.)?List\\[(scala.Predef.|java.lang.)?String]]. " + 457 | "Try to provide " + 458 | "poppet.Codec\\[(scala.Predef.|java.lang.)?String,(scala.collection.immutable.)?List\\[(scala.Predef.|java.lang.)?String]].").r 459 | ) 460 | } 461 | } 462 | } 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /core/test/src/poppet/consumer/ConsumerSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet.consumer 2 | 3 | import cats._ 4 | import org.scalatest.freespec.AnyFreeSpec 5 | import poppet.codec.upickle.json.all._ 6 | import poppet.consumer.core.ConsumerProcessor 7 | import poppet.core.Request 8 | import poppet.core.Response 9 | import ujson.Value 10 | import upickle.default._ 11 | 12 | class ConsumerSpec extends AnyFreeSpec { 13 | trait A { 14 | def a(p0: String): String 15 | } 16 | "Consumer" - { 17 | val consumerProcessor = new ConsumerProcessor[Id, Value, A] { 18 | override def apply( 19 | client: Request[Value] => Id[Response[Value]], fh: FailureHandler[Id] 20 | ): A = new A { 21 | override def a(p0: String): String = client(Request("A", "a", Map("p0" -> writeJs(p0)))).value.str 22 | } 23 | } 24 | "should delegate calls correctly" in { 25 | val c = new Consumer[Id, Value, A]( 26 | request => { 27 | val result = read[String](request.arguments("p0")) + " response" 28 | Response(writeJs(result)) 29 | }, 30 | FailureHandler.throwing, 31 | consumerProcessor 32 | ).service 33 | assert(c.a("request") == "request response") 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/test/src/poppet/core/ProcessorSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet.core 2 | 3 | import cats.Id 4 | import cats.data.EitherT 5 | import org.scalatest.Assertion 6 | import org.scalatest.exceptions.TestFailedException 7 | import org.scalactic.source.Position 8 | import poppet.PoppetSpec 9 | import scala.concurrent.Future 10 | import scala.util.matching.Regex 11 | 12 | object ProcessorSpec { 13 | case class SimpleDto(value: Int) 14 | trait Simple { 15 | def a0: Int 16 | def a00(): List[Int] 17 | def a1(b: Boolean): SimpleDto 18 | def a2(b0: Boolean, b1: Option[Boolean]): Id[List[String]] 19 | } 20 | trait WithFutureKind { 21 | def a0: Future[Int] 22 | def a1: Future[List[Int]] 23 | } 24 | trait WithMultipleArgumentLists { 25 | def a0(b0: Boolean)(b1: Boolean): Int 26 | def a1(b0: Boolean)()(b1: Boolean): List[Int] 27 | def a2(b0: Boolean)(b10: Boolean, b11: Boolean): SimpleDto 28 | } 29 | trait WithDefaultArguments { 30 | def a0(b: Boolean = true): Int 31 | def a1(b0: Boolean, b1: Boolean = true): List[Int] 32 | def a2(b0: Boolean, b1: Boolean, b2: Boolean = true, b3: Boolean = true): SimpleDto 33 | } 34 | trait WithParameters[A, B] { 35 | def a0(b0: A): Int 36 | def a1: B 37 | } 38 | trait WithVarargs { 39 | def a0(b: Boolean*): Int 40 | } 41 | trait WithParentWithParameters extends WithParameters[Boolean, Int] 42 | trait WithComplexReturnTypes { 43 | def a0(b: Boolean): WithComplexReturnTypes.ReturnType[Int] 44 | def a1(b0: Boolean, b1: Boolean): WithComplexReturnTypes.ReturnType[Int] 45 | // def b: Either[String, Int] 46 | } 47 | object WithComplexReturnTypes { 48 | type ReturnType[A] = EitherT[Future, String, A] 49 | } 50 | 51 | trait WithMethodWithParameters { 52 | def a[A](a: A): A 53 | } 54 | trait WithNoAbstractMethods { 55 | def a: Int = 1 56 | } 57 | trait WithConflictedMethods { 58 | def a(b: Int): Int 59 | def a(b: String): String 60 | } 61 | trait WithAbstractType { 62 | type A 63 | def a: String 64 | } 65 | } 66 | 67 | trait ProcessorSpec extends PoppetSpec { 68 | def assertCompilationErrorMessage( 69 | compilesAssert: => Assertion, 70 | message: String, 71 | alternativeMessages: String* 72 | )(implicit 73 | pos: Position 74 | ): Assertion = { 75 | try { 76 | compilesAssert 77 | fail("Compilation was successful") 78 | } catch { 79 | case e: TestFailedException => 80 | val messages = (message :: alternativeMessages.toList).map(m => s""""$m"""") 81 | val candidateMessage = messages.find(e.getMessage.contains(_)).getOrElse(messages.head) 82 | assert(e.getMessage().contains(candidateMessage)) 83 | } 84 | } 85 | 86 | def assertCompilationErrorMessagePattern( 87 | compilesAssert: => Assertion, 88 | pattern: Regex 89 | )(implicit 90 | pos: Position 91 | ): Assertion = { 92 | try { 93 | compilesAssert 94 | fail("Compilation was successful") 95 | } catch { 96 | case e: TestFailedException => 97 | val finalPattern = s"""[^"]*"${pattern.regex}"[^"]*""" 98 | assert(e.getMessage().matches(finalPattern), s"; `${e.getMessage()}` doesn't match `$finalPattern`") 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /core/test/src/poppet/provider/ProviderProcessorSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet.provider 2 | 3 | import cats._ 4 | import cats.data.EitherT 5 | import org.scalatest.freespec.AsyncFreeSpec 6 | import poppet.core.ProcessorSpec 7 | import poppet.core.ProcessorSpec._ 8 | import poppet.provider.core.ProviderProcessor 9 | import scala.concurrent.Future 10 | 11 | class ProviderProcessorSpec extends AsyncFreeSpec with ProcessorSpec { 12 | "Provider processor" - { 13 | implicit class BooleanOps(value: Boolean) { 14 | def toInt = if (value) 1 else 0 15 | } 16 | val simpleImpl: Simple = new Simple { 17 | override def a0: Int = 0 18 | override def a00(): List[Int] = List(1) 19 | override def a1(b: Boolean): SimpleDto = SimpleDto(2) 20 | override def a2(b0: Boolean, b1: Option[Boolean]): Id[List[String]] = 21 | List((b0.toInt + b1.getOrElse(false).toInt).toString) 22 | } 23 | val withComplexReturnTypesImpl = new WithComplexReturnTypes { 24 | override def a0(b: Boolean): WithComplexReturnTypes.ReturnType[Int] = 25 | EitherT.fromEither(Right(b.toInt)) 26 | override def a1(b0: Boolean, b1: Boolean): WithComplexReturnTypes.ReturnType[Int] = 27 | EitherT.fromEither(Right(b0.toInt + b1.toInt)) 28 | } 29 | 30 | "should generate instance" - { 31 | implicit val c0: Codec[Int, String] = a => Right(a.toString) 32 | implicit val c1: Codec[List[Int], String] = a => Right(a.toString) 33 | implicit val c2: Codec[SimpleDto, String] = a => Right(a.toString) 34 | implicit val c3: Codec[List[String], String] = a => Right(a.toString) 35 | implicit val cp0: Codec[String, Boolean] = a => Right(a.toBoolean) 36 | implicit val cp1: Codec[String, Option[Boolean]] = a => Right(Option(a.toBoolean)) 37 | implicit val cp2: Codec[String, Seq[Boolean]] = a => Right(a.split(",").map(_.toBoolean)) 38 | 39 | "when has id data kind" - { 40 | "for methods with different arguments number" in { 41 | val p = ProviderProcessor.generate[Id, String, Simple].apply(simpleImpl, FailureHandler.throwing) 42 | 43 | assert(p(0).service == "poppet.core.ProcessorSpec.Simple" 44 | && p(0).name == "a0" && p(0).arguments == List.empty 45 | && p(0).f(Map.empty) == "0") 46 | assert(p(1).service == "poppet.core.ProcessorSpec.Simple" 47 | && p(1).name == "a00" && p(1).arguments == List.empty 48 | && p(1).f(Map.empty) == "List(1)") 49 | assert(p(2).service == "poppet.core.ProcessorSpec.Simple" 50 | && p(2).name == "a1" && p(2).arguments == List("b") 51 | && p(2).f(Map("b" -> "true")) == "SimpleDto(2)") 52 | assert(p(3).service == "poppet.core.ProcessorSpec.Simple" 53 | && p(3).name == "a2" && p(3).arguments == List("b0", "b1") 54 | && p(3).f(Map("b0" -> "false", "b1" -> "true")) == "List(1)") 55 | } 56 | "for methods with future kind" in { 57 | implicit val ck: CodecK[Future, Id] = new CodecK[Future, Id] { 58 | override def apply[A](a: Future[A]): Id[A] = a.value.get.get 59 | } 60 | val t = new WithFutureKind { 61 | override def a0: Future[Int] = Future.successful(1) 62 | override def a1: Future[List[Int]] = Future.successful(List(1)) 63 | } 64 | 65 | val p = ProviderProcessor[Id, String, WithFutureKind].apply(t, FailureHandler.throwing) 66 | 67 | assert(p(0).service == "poppet.core.ProcessorSpec.WithFutureKind" 68 | && p(0).name == "a0" && p(0).arguments == List.empty 69 | && p(0).f(Map.empty) == "1") 70 | assert(p(1).service == "poppet.core.ProcessorSpec.WithFutureKind" 71 | && p(1).name == "a1" && p(1).arguments == List.empty 72 | && p(1).f(Map.empty) == "List(1)") 73 | } 74 | "for methods with multiple argument lists" in { 75 | val t: WithMultipleArgumentLists = new WithMultipleArgumentLists { 76 | override def a0(b0: Boolean)(b1: Boolean): Int = b0.toInt + b1.toInt 77 | override def a1(b0: Boolean)()(b1: Boolean): List[Int] = List(b0.toInt, b1.toInt) 78 | override def a2(b0: Boolean)(b10: Boolean, b11: Boolean): SimpleDto = 79 | SimpleDto(b0.toInt + b10.toInt + b11.toInt) 80 | } 81 | 82 | val p = ProviderProcessor[Id, String, WithMultipleArgumentLists].apply(t, FailureHandler.throwing) 83 | 84 | assert(p(0).service == "poppet.core.ProcessorSpec.WithMultipleArgumentLists" 85 | && p(0).name == "a0" && p(0).arguments == List("b0", "b1") 86 | && p(0).f(Map("b0" -> "true", "b1" -> "false")) == "1") 87 | assert(p(1).service == "poppet.core.ProcessorSpec.WithMultipleArgumentLists" 88 | && p(1).name == "a1" && p(1).arguments == List("b0", "b1") 89 | && p(1).f(Map("b0" -> "false", "b1" -> "false")) == "List(0, 0)") 90 | assert(p(2).service == "poppet.core.ProcessorSpec.WithMultipleArgumentLists" 91 | && p(2).name == "a2" && p(2).arguments == List("b0", "b10", "b11") 92 | && p(2).f(Map("b0" -> "true", "b10" -> "true", "b11" -> "true")) == "SimpleDto(3)") 93 | } 94 | "for methods with default arguments" in { 95 | val t: WithDefaultArguments = new WithDefaultArguments { 96 | override def a0(b: Boolean): Int = b.toInt 97 | override def a1(b0: Boolean, b1: Boolean): List[Int] = List(b0.toInt, b1.toInt) 98 | override def a2(b0: Boolean, b1: Boolean, b2: Boolean, b3: Boolean): SimpleDto = 99 | SimpleDto(b0.toInt + b1.toInt + b2.toInt + b3.toInt) 100 | } 101 | 102 | val p = ProviderProcessor[Id, String, WithDefaultArguments].apply(t, FailureHandler.throwing) 103 | 104 | assert(p(0).service == "poppet.core.ProcessorSpec.WithDefaultArguments" 105 | && p(0).name == "a0" && p(0).arguments == List("b") 106 | && p(0).f(Map("b" -> "false")) == "0") 107 | assert(p(1).service == "poppet.core.ProcessorSpec.WithDefaultArguments" 108 | && p(1).name == "a1" && p(1).arguments == List("b0", "b1") 109 | && p(1).f(Map("b0" -> "true", "b1" -> "false")) == "List(1, 0)") 110 | assert( 111 | p(2).service == "poppet.core.ProcessorSpec.WithDefaultArguments" 112 | && p(2).name == "a2" && p(2).arguments == List("b0", "b1", "b2", "b3") 113 | && p(2).f(Map("b0" -> "true", "b1" -> "true", "b2" -> "true", "b3" -> "true")) == "SimpleDto(4)" 114 | ) 115 | } 116 | "for methods with varargs" in { 117 | val t = new WithVarargs { 118 | override def a0(a: Boolean*): Int = a.map(_.toInt).sum 119 | } 120 | 121 | val p = ProviderProcessor[Id, String, WithVarargs].apply(t, FailureHandler.throwing) 122 | 123 | assert(p(0).service == "poppet.core.ProcessorSpec.WithVarargs" 124 | && p(0).name == "a0" && p(0).arguments == List("b") 125 | && p(0).f(Map("b" -> "false,true")) == "1") 126 | } 127 | "for traits with generic hierarchy" in { 128 | val t: WithParentWithParameters = new WithParentWithParameters { 129 | override def a0(b0: Boolean): Int = b0.toInt 130 | override def a1: Int = 1 131 | } 132 | val p = ProviderProcessor[Id, String, WithParentWithParameters].apply(t, FailureHandler.throwing) 133 | assert(p(0).service == "poppet.core.ProcessorSpec.WithParentWithParameters" 134 | && p(0).name == "a0" && p(0).arguments == List("b0") 135 | && p(0).f(Map("b0" -> "true")) == "1") 136 | assert(p(1).service == "poppet.core.ProcessorSpec.WithParentWithParameters" 137 | && p(1).name == "a1" && p(1).arguments == List.empty 138 | && p(1).f(Map.empty) == "1") 139 | } 140 | } 141 | "when has future data kind" in { 142 | import cats.implicits._ 143 | import scala.concurrent.Future 144 | 145 | implicit val ck0: CodecK[cats.Id, Future] = new CodecK[cats.Id, Future] { 146 | override def apply[A](a: Id[A]): Future[A] = Future.successful(a) 147 | } 148 | 149 | val p = ProviderProcessor[Future, String, Simple].apply(simpleImpl, FailureHandler.throwing) 150 | 151 | p(0).f(Map.empty).map(result => 152 | assert(p(0).service == "poppet.core.ProcessorSpec.Simple" 153 | && p(0).name == "a0" && p(0).arguments == List.empty 154 | && result == "0") 155 | ) 156 | p(1).f(Map.empty).map(result => 157 | assert(p(1).service == "poppet.core.ProcessorSpec.Simple" 158 | && p(1).name == "a00" && p(1).arguments == List.empty 159 | && result == "List(0)") 160 | ) 161 | p(2).f(Map("b" -> "true")).map(result => 162 | assert(p(2).service == "poppet.core.ProcessorSpec.Simple" 163 | && p(2).name == "a1" && p(2).arguments == List("b") 164 | && result == "1") 165 | ) 166 | p(3).f(Map("b0" -> "true", "b1" -> "true")).map(result => 167 | assert(p(3).service == "poppet.core.ProcessorSpec.Simple" 168 | && p(3).name == "a2" && p(3).arguments == List("b0", "b1") 169 | && result == "List(2)") 170 | ) 171 | } 172 | "when has complex data kind" in { 173 | import cats.implicits._ 174 | 175 | val p = ProviderProcessor[WithComplexReturnTypes.ReturnType, String, WithComplexReturnTypes] 176 | .apply(withComplexReturnTypesImpl, FailureHandler.throwing) 177 | 178 | def result[A](value: WithComplexReturnTypes.ReturnType[A]): A = 179 | value.value.value.get.get.toOption.get 180 | 181 | assert(p(0).service == "poppet.core.ProcessorSpec.WithComplexReturnTypes" 182 | && p(0).name == "a0" && p(0).arguments == List("b") 183 | && result(p(0).f(Map("b" -> "true"))) == "1") 184 | assert(p(1).service == "poppet.core.ProcessorSpec.WithComplexReturnTypes" 185 | && p(1).name == "a1" && p(1).arguments == List("b0", "b1") 186 | && result(p(1).f(Map("b0" -> "true", "b1" -> "true"))) == "2") 187 | } 188 | "when has A data kind and service has B data kind" in { 189 | import scala.util.Try 190 | import cats.implicits._ 191 | 192 | type F[A] = Option[A] 193 | type G[A] = WithComplexReturnTypes.ReturnType[A] 194 | 195 | implicit val ck0: CodecK[F, G] = new CodecK[F, G] { 196 | override def apply[A](a: F[A]): G[A] = EitherT.fromEither(a.toRight("not found")) 197 | } 198 | implicit val ck1: CodecK[G, F] = new CodecK[G, F] { 199 | override def apply[A](a: G[A]): F[A] = a.value.value.get.get.toOption 200 | } 201 | 202 | val p = ProviderProcessor[F, String, WithComplexReturnTypes] 203 | .apply(withComplexReturnTypesImpl, FailureHandler.throwing) 204 | 205 | def result[A](value: F[A]): A = value.get 206 | 207 | assert(p(0).service == "poppet.core.ProcessorSpec.WithComplexReturnTypes" 208 | && p(0).name == "a0" && p(0).arguments == List("b") 209 | && result(p(0).f(Map("b" -> "true"))) == "1") 210 | assert(p(1).service == "poppet.core.ProcessorSpec.WithComplexReturnTypes" 211 | && p(1).name == "a1" && p(1).arguments == List("b0", "b1") 212 | && result(p(1).f(Map("b0" -> "true", "b1" -> "true"))) == "2") 213 | } 214 | } 215 | "shouldn't generate instance" - { 216 | "when has id data kind" - { 217 | "for trait with generic methods" in { 218 | assertCompilationErrorMessage( 219 | assertCompiles("""ProviderProcessor[Id, String, WithMethodWithParameters]"""), 220 | "Generic methods are not supported: " + 221 | "poppet.core.ProcessorSpec.WithMethodWithParameters.a" 222 | ) 223 | } 224 | "for trait without abstract methods" in { 225 | assertCompilationErrorMessage( 226 | assertCompiles("""ProviderProcessor[Id, String, WithNoAbstractMethods]"""), 227 | "poppet.core.ProcessorSpec.WithNoAbstractMethods has no abstract methods. " + 228 | "Make sure that service method is parametrized with a trait." 229 | ) 230 | } 231 | "for trait with conflicting methods" in { 232 | assertCompilationErrorMessage( 233 | assertCompiles("""ProviderProcessor[Id, String, WithConflictedMethods]"""), 234 | "Use unique argument name lists for overloaded methods." 235 | ) 236 | } 237 | "for trait with abstract type" in { 238 | assertCompilationErrorMessage( 239 | assertCompiles("""ProviderProcessor[Id, String, WithAbstractType]"""), 240 | "Abstract types are not supported: " + 241 | "poppet.core.ProcessorSpec.WithAbstractType.A" 242 | ) 243 | } 244 | "for valid trait without simple codec" in { 245 | assertCompilationErrorMessagePattern( 246 | assertCompiles("""ProviderProcessor[Id, String, Simple]"""), 247 | ("Unable to convert (scala.)?Int to " + 248 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String). " + 249 | "Try to provide poppet.Codec\\[(scala.)?Int,(scala.Predef.|java.lang.)?String].").r 250 | ) 251 | } 252 | "for valid trait without codec for type with argument" in { 253 | implicit val c0: Codec[Int, String] = a => Right(a.toString) 254 | assertCompilationErrorMessagePattern( 255 | assertCompiles("""ProviderProcessor[Id, String, Simple]"""), 256 | ("Unable to convert (scala.collection.immutable.)?List\\[(scala.)?Int] to " + 257 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String). " + 258 | "Try to provide poppet.Codec\\[(scala.collection.immutable.)?List\\[(scala.)?Int],(scala.Predef.|java.lang.)?String] " + 259 | "or poppet.CodecK\\[(scala.collection.immutable.)?List,(\\[A\\])?(cats.)?Id(\\[A\\])?].").r 260 | ) 261 | } 262 | "for valid trait without codec for simple type with explicit Id kind" in { 263 | implicit val c0: Codec[Int, String] = a => Right(a.toString) 264 | implicit val c1: Codec[List[Int], String] = a => Right(a.toString) 265 | implicit val c2: Codec[SimpleDto, String] = a => Right(a.toString) 266 | assertCompilationErrorMessagePattern( 267 | assertCompiles("""ProviderProcessor[Id, String, Simple]"""), 268 | ("Unable to convert " + 269 | "(cats.)?Id\\[(scala.collection.immutable.)?List\\[(scala.Predef.|java.lang.)?String]] to " + 270 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String). " + 271 | "Try to provide " + 272 | "poppet.Codec\\[(scala.collection.immutable.)?List\\[(scala.Predef.|java.lang.)?String],(scala.Predef.|java.lang.)?String].").r 273 | ) 274 | } 275 | "for valid trait without codec for simple type with Future kind" in { 276 | assertCompilationErrorMessagePattern( 277 | assertCompiles("""ProviderProcessor[Id, String, WithFutureKind]"""), 278 | ("Unable to convert scala.concurrent.Future\\[(scala.)?Int] to " + 279 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String). " + 280 | "Try to provide poppet.Codec\\[scala.concurrent.Future\\[(scala.)?Int],(scala.Predef.|java.lang.)?String] or " + 281 | "poppet.CodecK\\[(\\[\\+T\\])?scala.concurrent.Future(\\[T\\])?,(\\[A\\])?(cats.)?Id(\\[A\\])?] " + 282 | "with poppet.Codec\\[(scala.)?Int,(scala.Predef.|java.lang.)?String].").r 283 | ) 284 | } 285 | "for valid trait without codec for simple type with Future kind, but with codecK" in { 286 | implicit val ck = new CodecK[Future, Id] { 287 | override def apply[A](a: Future[A]): Id[A] = a.value.get.get 288 | } 289 | assertCompilationErrorMessagePattern( 290 | assertCompiles("""ProviderProcessor[Id, String, WithFutureKind]"""), 291 | ("Unable to convert scala.concurrent.Future\\[(scala.)?Int] to " + 292 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String). " + 293 | "Try to provide poppet.Codec\\[(scala.)?Int,(scala.Predef.|java.lang.)?String].").r 294 | ) 295 | } 296 | "for valid trait without codecK for simple type with Future kind, but with codec" in { 297 | implicit val c0: Codec[Int, String] = a => Right(a.toString) 298 | assertCompilationErrorMessagePattern( 299 | assertCompiles("""ProviderProcessor[Id, String, WithFutureKind]"""), 300 | ("Unable to convert scala.concurrent.Future\\[(scala.)?Int] to " + 301 | "((cats.)?Id\\[(scala.Predef.|java.lang.)?String]|(scala.Predef.|java.lang.)?String). " + 302 | "Try to provide poppet.Codec\\[scala.concurrent.Future\\[(scala.)?Int],(scala.Predef.|java.lang.)?String] or " + 303 | "poppet.CodecK\\[(\\[\\+T\\])?scala.concurrent.Future(\\[T\\])?,(\\[A\\])?(cats.)?Id(\\[A\\])?].").r 304 | ) 305 | } 306 | } 307 | "when has Future data kind" - { 308 | "for valid trait without simple codec" in { 309 | assertCompilationErrorMessagePattern( 310 | assertCompiles("""ProviderProcessor[Future, String, Simple]"""), 311 | ("Unable to convert (scala.)?Int to scala.concurrent.Future\\[(scala.Predef.|java.lang.)?String]. " + 312 | "Try to provide " + 313 | "poppet.CodecK\\[(\\[A\\])?(cats.)?Id(\\[A\\])?,(\\[\\+T\\])?scala.concurrent.Future(\\[T\\])?] " + 314 | "with poppet.Codec\\[(scala.)?Int,(scala.Predef.|java.lang.)?String].").r 315 | ) 316 | } 317 | "for valid trait without simple codec, but with codecK" in { 318 | implicit val ck = new CodecK[Id, Future] { 319 | override def apply[A](a: Id[A]): Future[A] = Future.successful(a) 320 | } 321 | assertCompilationErrorMessagePattern( 322 | assertCompiles("""ProviderProcessor[Future, String, Simple]"""), 323 | ("Unable to convert (scala.)?Int to scala.concurrent.Future\\[(scala.Predef.|java.lang.)?String]. " + 324 | "Try to provide poppet.Codec\\[(scala.)?Int,(scala.Predef.|java.lang.)?String].").r 325 | ) 326 | } 327 | "for valid trait without simple codecK, but with codec" in { 328 | implicit val c0: Codec[Int, String] = a => Right(a.toString) 329 | assertCompilationErrorMessagePattern( 330 | assertCompiles("""ProviderProcessor[Future, String, Simple]"""), 331 | ("Unable to convert (scala.)?Int to scala.concurrent.Future\\[(scala.Predef.|java.lang.)?String]. " + 332 | "Try to provide " + 333 | "poppet.CodecK\\[(\\[A\\])?(cats.)?Id(\\[A\\])?,(\\[\\+T\\])?scala.concurrent.Future(\\[T\\])?].").r 334 | ) 335 | } 336 | "for valid trait without codec for type with argument" in { 337 | implicit val ck = new CodecK[Id, Future] { 338 | override def apply[A](a: Id[A]): Future[A] = Future.successful(a) 339 | } 340 | implicit val c0: Codec[Int, String] = a => Right(a.toString) 341 | assertCompilationErrorMessagePattern( 342 | assertCompiles("""ProviderProcessor[Future, String, Simple]"""), 343 | ("Unable to convert (scala.collection.immutable.)?List\\[(scala.)?Int] to " + 344 | "scala.concurrent.Future\\[(scala.Predef.|java.lang.)?String]. " + 345 | "Try to provide " + 346 | "poppet.Codec\\[(scala.collection.immutable.)?List\\[(scala.)?Int],(scala.Predef.|java.lang.)?String] " + 347 | "or " + 348 | "poppet.CodecK\\[(scala.collection.immutable.)?List,(\\[\\+T\\])?scala.concurrent.Future(\\[T\\])?].").r 349 | ) 350 | } 351 | "for valid trait without codec for simple type with explicit Id kind" in { 352 | implicit val ck = new CodecK[Id, Future] { 353 | override def apply[A](a: Id[A]): Future[A] = Future.successful(a) 354 | } 355 | implicit val c0: Codec[Int, String] = a => Right(a.toString) 356 | implicit val c1: Codec[List[Int], String] = a => Right(a.toString) 357 | implicit val c2: Codec[SimpleDto, String] = a => Right(a.toString) 358 | assertCompilationErrorMessagePattern( 359 | assertCompiles("""ProviderProcessor[Future, String, Simple]"""), 360 | ("Unable to convert " + 361 | "(cats.)?Id\\[(scala.collection.immutable.)?List\\[(scala.Predef.|java.lang.)?String]] to " + 362 | "scala.concurrent.Future\\[(scala.Predef.|java.lang.)?String]. " + 363 | "Try to provide " + 364 | "poppet.Codec\\[(scala.collection.immutable.)?List\\[(scala.Predef.|java.lang.)?String],(scala.Predef.|java.lang.)?String].").r 365 | ) 366 | } 367 | } 368 | } 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /core/test/src/poppet/provider/ProviderSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet.provider 2 | 3 | import cats._ 4 | import org.scalatest.freespec.AnyFreeSpec 5 | import poppet.codec.upickle.json.all._ 6 | import poppet.core.Request 7 | import poppet.core.Response 8 | import poppet.provider.core.MethodProcessor 9 | import ujson.Value 10 | import upickle.default._ 11 | 12 | class ProviderSpec extends AnyFreeSpec { 13 | "Provider" - { 14 | def providerProcessors[F[_]: Applicative] = List(new MethodProcessor[F, Value]( 15 | "A", "a", List("p0"), 16 | request => Applicative[F].pure(writeJs(read[String](request("p0")) + " response")) 17 | )) 18 | val validRequest = Request("A", "a", Map("p0" -> writeJs("request"))) 19 | val validResponse = Response(writeJs("request response")) 20 | "should delegates calls correctly" in { 21 | val p = new Provider[Id, Value]( 22 | FailureHandler.throwing, 23 | providerProcessors, 24 | ) 25 | assert(p.apply(validRequest) == validResponse) 26 | } 27 | "should raise a failure if processor is not found" in { 28 | type Response[A] = Either[Failure, A] 29 | val p = new Provider[Response, Value]( 30 | new FailureHandler[Response] { 31 | override def apply[A](f: Failure): Response[A] = Left(f) 32 | }, 33 | providerProcessors, 34 | ) 35 | assert(p.apply(Request( 36 | "B", "a", Map("p0" -> writeJs("request")) 37 | )).left.map(_.getMessage) == Left( 38 | "Requested processor B is not in [A]. Make sure that desired service is provided and up to date." 39 | )) 40 | assert(p.apply(Request( 41 | "A", "b", Map("p0" -> writeJs("request")) 42 | )).left.map(_.getMessage) == Left( 43 | "Requested processor A.b is not in A.[a]. Make sure that desired service is provided and up to date." 44 | )) 45 | assert(p.apply(Request( 46 | "A", "a", Map("p1" -> writeJs("request")) 47 | )).left.map(_.getMessage) == Left( 48 | "Requested processor A.a(p1) is not in A.a[(p0)]. Make sure that desired service is provided and up to date." 49 | )) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /example/http4s-circe/api/src/poppet/example/http4s/model/User.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.http4s.model 2 | 3 | case class User(email: String, firstName: String) -------------------------------------------------------------------------------- /example/http4s-circe/api/src/poppet/example/http4s/model/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.http4s 2 | 3 | import cats.data.EitherT 4 | import cats.effect.IO 5 | 6 | package object model { 7 | /** 8 | * Service response type that based on IO effect and EitherT monad transformer 9 | * where left is a string error and right is an actual result 10 | */ 11 | type SR[A] = EitherT[IO, String, A] 12 | } 13 | -------------------------------------------------------------------------------- /example/http4s-circe/api/src/poppet/example/http4s/poppet/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.http4s 2 | 3 | import _root_.poppet._ 4 | import _root_.poppet.example.http4s.model.SR 5 | import cats.data.EitherT 6 | 7 | package object poppet { 8 | val SRFailureHandler = new FailureHandler[SR] { 9 | override def apply[A](f: Failure): SR[A] = EitherT.leftT(f.getMessage) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/http4s-circe/api/src/poppet/example/http4s/service/UserService.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.http4s.service 2 | 3 | import poppet.example.http4s.model.SR 4 | import poppet.example.http4s.model.User 5 | 6 | trait UserService { 7 | def findById(id: String): SR[User] 8 | } 9 | -------------------------------------------------------------------------------- /example/http4s-circe/consumer/src/poppet/example/http4s/consumer/Application.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.http4s.consumer 2 | 3 | import cats.effect.ExitCode 4 | import cats.effect.IO 5 | import cats.effect.IOApp 6 | import cats.effect.Resource 7 | import cats.implicits._ 8 | import org.http4s.blaze.client._ 9 | import org.http4s.blaze.server._ 10 | import org.http4s.implicits._ 11 | import poppet.example.http4s.consumer.api.UserApi 12 | import poppet.example.http4s.consumer.service.UserServiceProvider 13 | 14 | object Application extends IOApp { 15 | 16 | override def run(args: List[String]): IO[ExitCode] = (for { 17 | config <- Resource.pure[IO, Config](Config(uri"http://localhost:9001/api/service")) 18 | client <- BlazeClientBuilder[IO].resource 19 | userService = new UserServiceProvider(config, client).get 20 | userApi = new UserApi(userService) 21 | _ <- BlazeServerBuilder[IO] 22 | .bindHttp(9002, "0.0.0.0") 23 | .withHttpApp(userApi.routes.orNotFound) 24 | .resource 25 | } yield ExitCode.Success).useForever 26 | 27 | } 28 | -------------------------------------------------------------------------------- /example/http4s-circe/consumer/src/poppet/example/http4s/consumer/Config.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.http4s.consumer 2 | 3 | import org.http4s.Uri 4 | 5 | case class Config(consumerUrl: Uri) 6 | -------------------------------------------------------------------------------- /example/http4s-circe/consumer/src/poppet/example/http4s/consumer/api/UserApi.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.http4s.consumer.api 2 | 3 | import cats.effect.IO 4 | import io.circe.generic.auto._ 5 | import org.http4s._ 6 | import org.http4s.circe.CirceEntityCodec._ 7 | import org.http4s.dsl.io._ 8 | import poppet.example.http4s.service.UserService 9 | 10 | class UserApi(userService: UserService) { 11 | val routes = HttpRoutes.of[IO] { 12 | case GET -> Root / "api" / "user" / id => 13 | userService.findById(id).foldF(InternalServerError(_), Ok(_)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/http4s-circe/consumer/src/poppet/example/http4s/consumer/service/UserServiceProvider.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.http4s.consumer.service 2 | 3 | import cats.data.EitherT 4 | import cats.effect.IO 5 | import io.circe.Json 6 | import io.circe.generic.auto._ 7 | import org.http4s.Status.Successful 8 | import org.http4s._ 9 | import org.http4s.circe.CirceEntityDecoder._ 10 | import org.http4s.circe.CirceEntityEncoder._ 11 | import org.http4s.client.Client 12 | import org.http4s.client.dsl.io._ 13 | import poppet.codec.circe.all._ 14 | import poppet.consumer.all._ 15 | import poppet.consumer.all.Response 16 | import poppet.example.http4s.consumer.Config 17 | import poppet.example.http4s.model.SR 18 | import poppet.example.http4s.poppet.SRFailureHandler 19 | import poppet.example.http4s.service.UserService 20 | 21 | class UserServiceProvider(config: Config, client: Client[IO]) { 22 | private val transport: Transport[SR, Json] = request => EitherT( 23 | client.run(Method.POST.apply(request, config.consumerUrl)).use { 24 | case Successful(response) => response.as[Response[Json]].map(Right(_)) 25 | case failedResponse => failedResponse.as[String].map(Left(_)) 26 | } 27 | ) 28 | 29 | def get: UserService = Consumer[SR, Json](transport, fh = SRFailureHandler).service[UserService] 30 | } 31 | -------------------------------------------------------------------------------- /example/http4s-circe/provider/src/poppet/example/http4s/provider/Application.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.http4s.provider 2 | 3 | import cats.effect.ExitCode 4 | import cats.effect.IO 5 | import cats.effect.IOApp 6 | import cats.effect.Resource 7 | import org.http4s.blaze.server._ 8 | import org.http4s.implicits._ 9 | import poppet.example.http4s.provider.api.ProviderApi 10 | 11 | object Application extends IOApp { 12 | 13 | override def run(args: List[String]): IO[ExitCode] = (for { 14 | providerApi <- Resource.pure[IO, ProviderApi](new ProviderApi()) 15 | server <- BlazeServerBuilder[IO] 16 | .bindHttp(9001, "0.0.0.0") 17 | .withHttpApp(providerApi.routes.orNotFound) 18 | .resource 19 | } yield ExitCode.Success).useForever 20 | 21 | } 22 | -------------------------------------------------------------------------------- /example/http4s-circe/provider/src/poppet/example/http4s/provider/api/ProviderApi.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.http4s.provider.api 2 | 3 | import cats.data.EitherT 4 | import cats.effect.IO 5 | import io.circe.Json 6 | import io.circe.generic.auto._ 7 | import org.http4s.HttpRoutes 8 | import org.http4s.circe.CirceEntityDecoder._ 9 | import org.http4s.circe.CirceEntityEncoder._ 10 | import org.http4s.dsl.io._ 11 | import poppet.codec.circe.all._ 12 | import poppet.example.http4s.model.SR 13 | import poppet.example.http4s.poppet.SRFailureHandler 14 | import poppet.example.http4s.provider.service.UserInternalService 15 | import poppet.example.http4s.service.UserService 16 | import poppet.provider.all._ 17 | 18 | class ProviderApi() { 19 | val provider = Provider[SR, Json](fh = SRFailureHandler).service[UserService](new UserInternalService) 20 | 21 | val routes = HttpRoutes.of[IO] { 22 | case request@POST -> Root / "api" / "service" => (for { 23 | byteBody <- EitherT.right[String](request.as[Request[Json]]) 24 | response <- provider(byteBody) 25 | } yield response).foldF(InternalServerError(_), Ok(_)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/http4s-circe/provider/src/poppet/example/http4s/provider/service/UserInternalService.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.http4s.provider.service 2 | 3 | import cats.data.EitherT 4 | import poppet.example.http4s.model.SR 5 | import poppet.example.http4s.model.User 6 | import poppet.example.http4s.service.UserService 7 | 8 | class UserInternalService extends UserService { 9 | override def findById(id: String): SR[User] = { 10 | //emulation of business logic 11 | if (id == "1") EitherT.rightT(User(id, "Antony")) 12 | else EitherT.leftT("User is not found") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/play/api/src/poppet/example/play/model/User.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.play.model 2 | 3 | import play.api.libs.json.Json 4 | 5 | case class User(email: String, firstName: String) 6 | 7 | object User { 8 | implicit val F = Json.format[User] 9 | } -------------------------------------------------------------------------------- /example/play/api/src/poppet/example/play/service/UserService.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.play.service 2 | 3 | import poppet.example.play.model.User 4 | import scala.concurrent.Future 5 | 6 | trait UserService { 7 | def findById(id: String): Future[User] 8 | } 9 | -------------------------------------------------------------------------------- /example/play/consumer/app/poppet/example/play/controller/UserController.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.play.controller 2 | 3 | import javax.inject.Inject 4 | import javax.inject.Singleton 5 | import play.api.libs.json.Json 6 | import play.api.mvc._ 7 | import poppet.example.play.service.UserService 8 | import scala.concurrent.ExecutionContext 9 | 10 | @Singleton 11 | class UserController @Inject()( 12 | userService: UserService, cc: ControllerComponents)( 13 | implicit ec: ExecutionContext 14 | ) extends AbstractController(cc) { 15 | def findById(id: String) = Action.async { 16 | userService.findById(id).map(user => Ok(Json.toJson(user))) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/play/consumer/app/poppet/example/play/module/CustomModule.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.play.module 2 | 3 | import play.api.inject.SimpleModule 4 | import play.api.inject._ 5 | import poppet.example.play.service.UserService 6 | import poppet.example.play.service.UserServiceProvider 7 | 8 | class CustomModule extends SimpleModule( 9 | bind[UserService].toProvider[UserServiceProvider] 10 | ) -------------------------------------------------------------------------------- /example/play/consumer/app/poppet/example/play/service/UserServiceProvider.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.play.service 2 | 3 | import cats.implicits._ 4 | import javax.inject.Inject 5 | import javax.inject.Provider 6 | import javax.inject.Singleton 7 | import play.api.libs.json.JsValue 8 | import play.api.libs.json.Writes 9 | import play.api.libs.ws.WSClient 10 | import play.api.Configuration 11 | import poppet.codec.play.all._ 12 | import poppet.consumer.all._ 13 | import scala.concurrent.ExecutionContext 14 | import scala.concurrent.Future 15 | 16 | @Singleton 17 | class UserServiceProvider @Inject() ( 18 | wsClient: WSClient, 19 | config: Configuration 20 | )(implicit ec: ExecutionContext) extends Provider[UserService] { 21 | private val url = config.get[String]("consumer.url") 22 | 23 | private val client: Transport[Future, JsValue] = request => 24 | wsClient.url(url).post(Writes.of[Request[JsValue]].writes(request)).map(_.body[JsValue].as[Response[JsValue]]) 25 | 26 | override def get: UserService = Consumer[Future, JsValue](client).service[UserService] 27 | } 28 | -------------------------------------------------------------------------------- /example/play/consumer/conf/application.conf: -------------------------------------------------------------------------------- 1 | play { 2 | server.http.port = 9002 3 | http.secret.key = "some-dummy-secret" 4 | modules.enabled += "poppet.example.play.module.CustomModule" 5 | } 6 | 7 | consumer.url = "http://localhost:9001/api/service" -------------------------------------------------------------------------------- /example/play/consumer/conf/routes: -------------------------------------------------------------------------------- 1 | GET /api/user/:id poppet.example.play.controller.UserController.findById(id) 2 | -------------------------------------------------------------------------------- /example/play/provider/app/poppet/example/play/controller/ProviderController.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.play.controller 2 | 3 | import cats.implicits._ 4 | import javax.inject.Inject 5 | import javax.inject.Singleton 6 | import play.api.libs.json.JsValue 7 | import play.api.libs.json.Writes 8 | import play.api.mvc._ 9 | import poppet.codec.play.all._ 10 | import poppet.example.play.service.UserService 11 | import poppet.provider.all._ 12 | import poppet.provider.all.Request 13 | import scala.concurrent.ExecutionContext 14 | import scala.concurrent.Future 15 | 16 | @Singleton 17 | class ProviderController @Inject() ( 18 | userService: UserService, 19 | cc: ControllerComponents 20 | )( 21 | implicit ec: ExecutionContext 22 | ) extends AbstractController(cc) { 23 | private val provider = Provider[Future, JsValue]().service(userService) 24 | 25 | def apply(): Action[AnyContent] = Action.async(request => 26 | provider(request.body.asJson.flatMap(_.asOpt[Request[JsValue]]).get) 27 | .map(r => Ok(Writes.of[Response[JsValue]].writes(r))) 28 | ) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /example/play/provider/app/poppet/example/play/module/CustomModule.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.play.module 2 | 3 | import play.api.inject.SimpleModule 4 | import play.api.inject._ 5 | import poppet.example.play.service.UserService 6 | import poppet.example.play.service.UserInternalService 7 | 8 | class CustomModule extends SimpleModule( 9 | bind[UserService].to[UserInternalService] 10 | ) 11 | -------------------------------------------------------------------------------- /example/play/provider/app/poppet/example/play/service/UserInternalService.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.play.service 2 | 3 | import poppet.example.play.model.User 4 | import poppet.example.play.service.UserService 5 | import scala.concurrent.Future 6 | 7 | class UserInternalService extends UserService { 8 | override def findById(id: String): Future[User] = { 9 | //emulation of business logic 10 | if (id == "1") Future.successful(User(id, "Antony")) 11 | else Future.failed(new IllegalArgumentException("User is not found")) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/play/provider/conf/application.conf: -------------------------------------------------------------------------------- 1 | play { 2 | server.http.port = 9001 3 | http.secret.key = "some-dummy-secret" 4 | modules.enabled += "poppet.example.play.module.CustomModule" 5 | } -------------------------------------------------------------------------------- /example/play/provider/conf/routes: -------------------------------------------------------------------------------- 1 | POST /api/service poppet.example.play.controller.ProviderController.apply() 2 | -------------------------------------------------------------------------------- /example/spring-jackson/api/src/poppet/example/spring/model/User.java: -------------------------------------------------------------------------------- 1 | package poppet.example.spring.model; 2 | 3 | public class User { 4 | private String email; 5 | private String firstName; 6 | public User() {} 7 | public User(String email, String firstName) { 8 | this.email = email; 9 | this.firstName = firstName; 10 | } 11 | public String getEmail() { 12 | return email; 13 | } 14 | public void setEmail(String email) { 15 | this.email = email; 16 | } 17 | public String getFirstName() { 18 | return firstName; 19 | } 20 | public void setFirstName(String firstName) { 21 | this.firstName = firstName; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/spring-jackson/api/src/poppet/example/spring/service/UserService.java: -------------------------------------------------------------------------------- 1 | package poppet.example.spring.service; 2 | 3 | import poppet.example.spring.model.User; 4 | 5 | public interface UserService { 6 | public User findById(String id); 7 | } 8 | -------------------------------------------------------------------------------- /example/spring-jackson/consumer/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9002 3 | 4 | consumer: 5 | url: "http://localhost:9001/api/service" -------------------------------------------------------------------------------- /example/spring-jackson/consumer/src/poppet/example/spring/consumer/Application.java: -------------------------------------------------------------------------------- 1 | package poppet.example.spring.consumer; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.module.scala.ClassTagExtensions; 5 | import com.fasterxml.jackson.module.scala.DefaultScalaModule$; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Primary; 11 | import org.springframework.web.client.RestTemplate; 12 | import poppet.example.spring.service.UserService; 13 | import poppet.example.spring.consumer.service.UserServiceProvider; 14 | 15 | @SpringBootApplication 16 | public class Application { 17 | public static void main(String[] args) { 18 | SpringApplication.run(Application.class, args); 19 | } 20 | 21 | private static class MyObjectMapper extends ObjectMapper implements ClassTagExtensions {} 22 | 23 | @Bean 24 | @Primary 25 | public ObjectMapper objectMapper() { 26 | ObjectMapper objectMapper = new MyObjectMapper(); 27 | objectMapper.registerModule(DefaultScalaModule$.MODULE$); 28 | return objectMapper; 29 | } 30 | 31 | @Bean 32 | public RestTemplate restTemplate() { 33 | return new RestTemplate(); 34 | } 35 | 36 | @Bean 37 | public UserService userService( 38 | RestTemplate restTemplate, 39 | @Value("${consumer.url}") String url, 40 | ObjectMapper objectMapper 41 | ) { 42 | return new UserServiceProvider(restTemplate, url, objectMapper).get(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /example/spring-jackson/consumer/src/poppet/example/spring/consumer/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package poppet.example.spring.consumer.controller; 2 | 3 | import org.springframework.web.bind.annotation.PathVariable; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | import poppet.example.spring.model.User; 7 | import poppet.example.spring.service.UserService; 8 | 9 | @RestController 10 | public class UserController { 11 | private final UserService userService; 12 | 13 | public UserController(UserService userService) { 14 | this.userService = userService; 15 | } 16 | 17 | @RequestMapping("/api/user/{id}") 18 | public User findById(@PathVariable("id") String id) { 19 | return userService.findById(id); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/spring-jackson/consumer/src/poppet/example/spring/consumer/service/UserServiceProvider.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.spring.consumer.service 2 | 3 | import cats.Id 4 | import com.fasterxml.jackson.core.`type`.TypeReference 5 | import com.fasterxml.jackson.databind.JsonNode 6 | import com.fasterxml.jackson.databind.ObjectMapper 7 | import java.net.URI 8 | import org.springframework.http.HttpMethod 9 | import org.springframework.http.RequestEntity 10 | import org.springframework.web.client.RestTemplate 11 | import poppet.codec.jackson.all._ 12 | import poppet.consumer._ 13 | import poppet.example.spring.service.UserService 14 | 15 | class UserServiceProvider(restTemplate: RestTemplate)(url: String)(implicit objectMapper: ObjectMapper) { 16 | 17 | private def client(restTemplate: RestTemplate)(url: String): Transport[Id, JsonNode] = request => { 18 | objectMapper.readValue( 19 | objectMapper.treeAsTokens(restTemplate.exchange( 20 | new RequestEntity[JsonNode]( 21 | objectMapper.valueToTree[JsonNode](request), 22 | HttpMethod.POST, 23 | URI.create(url) 24 | ), 25 | classOf[JsonNode] 26 | ).getBody), 27 | new TypeReference[Response[JsonNode]]() {} 28 | ) 29 | } 30 | 31 | def get: UserService = Consumer[Id, JsonNode](client(restTemplate)(url)).service[UserService] 32 | } 33 | -------------------------------------------------------------------------------- /example/spring-jackson/provider/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9001 -------------------------------------------------------------------------------- /example/spring-jackson/provider/src/poppet/example/spring/provider/Application.java: -------------------------------------------------------------------------------- 1 | package poppet.example.spring.provider; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.module.scala.ClassTagExtensions; 5 | import com.fasterxml.jackson.module.scala.DefaultScalaModule$; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Primary; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | @SpringBootApplication 13 | public class Application { 14 | public static void main(String[] args) { 15 | SpringApplication.run(Application.class, args); 16 | } 17 | 18 | @Bean 19 | public RestTemplate restTemplate() { 20 | return new RestTemplate(); 21 | } 22 | 23 | private static class MyObjectMapper extends ObjectMapper implements ClassTagExtensions {} 24 | 25 | @Bean 26 | @Primary 27 | public ObjectMapper objectMapper() { 28 | ObjectMapper objectMapper = new MyObjectMapper(); 29 | objectMapper.registerModule(DefaultScalaModule$.MODULE$); 30 | return objectMapper; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/spring-jackson/provider/src/poppet/example/spring/provider/controller/ProviderController.java: -------------------------------------------------------------------------------- 1 | package poppet.example.spring.provider.controller; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.RequestEntity; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import poppet.core.Request; 10 | import poppet.core.Response; 11 | import poppet.example.spring.provider.service.ProviderGenerator; 12 | import scala.Function1; 13 | 14 | @Controller 15 | public class ProviderController { 16 | private final Function1, Response> provider; 17 | 18 | public ProviderController(ProviderGenerator providerGenerator) { 19 | provider = providerGenerator.apply(); 20 | } 21 | 22 | @RequestMapping("/api/service") 23 | public ResponseEntity> apply(RequestEntity> request) { 24 | return new ResponseEntity<>(provider.apply(request.getBody()), HttpStatus.OK); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/spring-jackson/provider/src/poppet/example/spring/provider/service/ProviderGenerator.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.spring.provider.service 2 | 3 | import cats.Id 4 | import com.fasterxml.jackson.databind.JsonNode 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import org.springframework.stereotype.Service 7 | import poppet.codec.jackson.all._ 8 | import poppet.example.spring.service.UserService 9 | import poppet.provider.all._ 10 | 11 | @Service 12 | class ProviderGenerator(userService: UserService)(implicit objectMapper: ObjectMapper) { 13 | private val provider = Provider[Id, JsonNode]().service(userService) 14 | 15 | def apply: Request[JsonNode] => Response[JsonNode] = request => provider.apply(request) 16 | } 17 | -------------------------------------------------------------------------------- /example/spring-jackson/provider/src/poppet/example/spring/provider/service/UserInternalService.java: -------------------------------------------------------------------------------- 1 | package poppet.example.spring.provider.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | import poppet.example.spring.model.User; 5 | import poppet.example.spring.service.UserService; 6 | 7 | @Service 8 | public class UserInternalService implements UserService { 9 | @Override 10 | public User findById(String id) { 11 | //emulation of business logic 12 | if ("1".equals(id)) return new User(id, "Antony"); 13 | else throw new IllegalArgumentException("User is not found"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/api/src/poppet/example/tapir/Util.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir 2 | 3 | import cats.effect.Concurrent 4 | import fs2._ 5 | 6 | object Util { 7 | 8 | def uncons[F[_]: Concurrent, A](stream: Stream[F, A]): F[Option[(A, Stream[F, A])]] = stream.pull.uncons1.flatMap { 9 | case Some((h, t)) => Pull.extendScopeTo(t).flatMap(et => Pull.output1(h -> et)) 10 | case None => Pull.done 11 | }.stream.compile.last 12 | 13 | } 14 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/api/src/poppet/example/tapir/model/CustomCodecs.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.model 2 | 3 | import cats.effect.IO 4 | import cats.implicits._ 5 | import fs2.Stream 6 | import io.circe.syntax._ 7 | import io.circe.Encoder 8 | import io.circe.Json 9 | import poppet.codec.circe.all._ 10 | import poppet.core.CodecFailure 11 | import poppet.Codec 12 | 13 | object CustomCodecs { 14 | implicit def fromSingleCodec[A](implicit subc: Codec[A, Json]): Codec[A, Either[Json, Stream[IO, Json]]] = a => 15 | subc(a).map(_.asLeft[Stream[IO, Json]]) 16 | 17 | implicit def fromStreamCodec[A: Encoder](implicit 18 | subc: Codec[A, Json] 19 | ): Codec[Stream[IO, A], Either[Json, Stream[IO, Json]]] = s => 20 | s.evalMap(subc(_).fold[IO[Json]](IO.raiseError, IO.pure)).asRight[Json].asRight[CodecFailure[Stream[IO, A]]] 21 | 22 | implicit def toSingleCodec[A](implicit subc: Codec[Json, A]): Codec[Either[Json, Stream[IO, Json]], A] = { 23 | case Left(json) => subc(json).left.map(f => f.withData(f.data.asLeft[Stream[IO, Json]])) 24 | case Right(value) => new CodecFailure("Cannot convert stream to single", value.asRight[Json]).asLeft[A] 25 | } 26 | 27 | implicit def toStreamCodec[A](implicit 28 | subc: Codec[Json, A] 29 | ): Codec[Either[Json, Stream[IO, Json]], Stream[IO, A]] = { 30 | case Left(json) => 31 | subc(json).bimap(f => f.withData(json.asLeft[Stream[IO, Json]]), Stream.emit(_).covary[IO]) 32 | case Right(stream) => 33 | stream 34 | .evalMap(a => subc(a).fold[IO[A]](IO.raiseError, IO.pure)) 35 | .asRight[CodecFailure[Either[Json, Stream[IO, Json]]]] 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/api/src/poppet/example/tapir/model/PoppetArgumentEvent.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.model 2 | 3 | import io.circe.Json 4 | 5 | case class PoppetArgumentEvent(argumentName: String, values: List[Json]) 6 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/api/src/poppet/example/tapir/model/PoppetRequestInitEvent.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.model 2 | 3 | import io.circe.Json 4 | 5 | case class PoppetRequestInitEvent( 6 | service: String, 7 | method: String, 8 | eagerArguments: Map[String, Json], 9 | streamArguments: Set[String], 10 | ) 11 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/api/src/poppet/example/tapir/model/PoppetResponseInitEvent.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.model 2 | 3 | import io.circe.Json 4 | 5 | case class PoppetResponseInitEvent( 6 | eagerResponse: Option[Json], 7 | ) 8 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/api/src/poppet/example/tapir/model/PoppetResultEvent.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.model 2 | 3 | import io.circe.Json 4 | 5 | case class PoppetResultEvent(values: List[Json]) 6 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/api/src/poppet/example/tapir/model/User.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.model 2 | 3 | case class User(email: String, firstName: String) 4 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/api/src/poppet/example/tapir/service/UserService.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.service 2 | 3 | import cats.effect.IO 4 | import fs2._ 5 | import poppet.example.tapir.model.User 6 | 7 | trait UserService { 8 | def findById(id: String): IO[User] 9 | def findByIds(ids: Stream[IO, String]): IO[Stream[IO, User]] 10 | } 11 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/consumer/src/poppet/example/tapir/consumer/Application.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.consumer 2 | 3 | import cats.effect.ExitCode 4 | import cats.effect.IO 5 | import cats.effect.IOApp 6 | import cats.effect.Resource 7 | import org.http4s.blaze.client._ 8 | import org.http4s.blaze.server._ 9 | import org.http4s.implicits._ 10 | import poppet.example.tapir.consumer.api.UserApi 11 | import poppet.example.tapir.consumer.service.UserServiceProvider 12 | import sttp.client3.http4s.Http4sBackend 13 | import sttp.tapir.server.http4s.Http4sServerInterpreter 14 | 15 | object Application extends IOApp { 16 | 17 | override def run(args: List[String]): IO[ExitCode] = (for { 18 | config <- Resource.pure[IO, Config](Config(uri"http://localhost:9001/api/service")) 19 | client <- Http4sBackend.usingDefaultBlazeClientBuilder[IO]() 20 | userService = new UserServiceProvider(config, client).get 21 | userApi = new UserApi(userService) 22 | _ <- BlazeServerBuilder[IO] 23 | .bindHttp(9002, "0.0.0.0") 24 | .withHttpApp(Http4sServerInterpreter[IO]().toRoutes(userApi.routes).orNotFound) 25 | .resource 26 | } yield ExitCode.Success).useForever 27 | 28 | } 29 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/consumer/src/poppet/example/tapir/consumer/Config.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.consumer 2 | 3 | import org.http4s.Uri 4 | 5 | case class Config(consumerUrl: Uri) 6 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/consumer/src/poppet/example/tapir/consumer/api/UserApi.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.consumer.api 2 | 3 | import cats.effect.IO 4 | import cats.implicits._ 5 | import fs2.Stream 6 | import io.circe.generic.auto._ 7 | import io.circe.syntax._ 8 | import org.http4s.ServerSentEvent 9 | import poppet.example.tapir.model.User 10 | import poppet.example.tapir.service.UserService 11 | import scala.concurrent.duration._ 12 | import sttp.capabilities.fs2.Fs2Streams 13 | import sttp.tapir._ 14 | import sttp.tapir.generic.auto._ 15 | import sttp.tapir.json.circe._ 16 | 17 | class UserApi(userService: UserService) { 18 | 19 | val routes = List( 20 | endpoint 21 | .get 22 | .in("api" / "user" / path[String]("userId")) 23 | .out(jsonBody[User]) 24 | .serverLogicSuccess[IO] { userId => 25 | userService.findById(userId) 26 | }, 27 | endpoint 28 | .get 29 | .in("api" / "user") 30 | .out(streamBody(Fs2Streams[IO])(implicitly[Schema[User]], CodecFormat.TextEventStream())) 31 | .serverLogicSuccess[IO](_ => 32 | for { 33 | userStream <- userService.findByIds( 34 | Stream.emits(List("1", "2", "3")).zipLeft(Stream.awakeEvery[IO](1.second)) 35 | ) 36 | } yield userStream 37 | .map(user => ServerSentEvent(user.asJson.noSpaces.some)) 38 | .through(ServerSentEvent.encoder[IO]) 39 | ) 40 | ) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/consumer/src/poppet/example/tapir/consumer/service/UserServiceProvider.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.consumer.service 2 | 3 | import cats.data.OptionT 4 | import cats.effect.IO 5 | import cats.implicits._ 6 | import fs2.Stream 7 | import io.circe.generic.auto._ 8 | import io.circe.parser._ 9 | import io.circe.syntax._ 10 | import io.circe.Json 11 | import org.http4s._ 12 | import org.http4s.client.dsl.io._ 13 | import poppet.codec.circe.all._ 14 | import poppet.consumer.all._ 15 | import poppet.consumer.all.Response 16 | import poppet.example.tapir.consumer.Config 17 | import poppet.example.tapir.model.CustomCodecs._ 18 | import poppet.example.tapir.model.PoppetArgumentEvent 19 | import poppet.example.tapir.model.PoppetRequestInitEvent 20 | import poppet.example.tapir.model.PoppetResponseInitEvent 21 | import poppet.example.tapir.model.PoppetResultEvent 22 | import poppet.example.tapir.service.UserService 23 | import poppet.example.tapir.Util 24 | import sttp.capabilities.fs2.Fs2Streams 25 | import sttp.client3._ 26 | import sttp.model.HeaderNames 27 | 28 | class UserServiceProvider(config: Config, client: SttpBackend[IO, Fs2Streams[IO]]) { 29 | 30 | private val transport: Transport[IO, Either[Json, Stream[IO, Json]]] = request => 31 | for { 32 | initEvent <- IO.pure(PoppetRequestInitEvent( 33 | request.service, 34 | request.method, 35 | request.arguments.collect { case (key, Left(json)) => key -> json }, 36 | request.arguments.collect { case (key, Right(_)) => key }.toSet, 37 | )) 38 | streamBody = ( 39 | Stream.emit(initEvent.asJson) ++ 40 | request.arguments.collect { case (key, Right(stream)) => 41 | stream.chunks.map(chunk => PoppetArgumentEvent(key, chunk.toList).asJson) 42 | }.toList.parJoinUnbounded 43 | ).map(data => ServerSentEvent(data.noSpaces.some)) 44 | clientResp <- quickRequest 45 | .post(uri"${config.consumerUrl}") 46 | .header(HeaderNames.TransferEncoding, TransferCoding.chunked.coding) 47 | .streamBody(Fs2Streams[IO])(streamBody.through(ServerSentEvent.encoder)) 48 | .response(asStreamAlwaysUnsafe(Fs2Streams[IO]).map(_.through(ServerSentEvent.decoder))) 49 | .send(client) 50 | x <- OptionT(Util.uncons(clientResp.body)) 51 | .subflatMap { case (initRespEvent, restRespEvents) => 52 | initRespEvent.data 53 | .flatMap(parse(_).flatMap(_.as[PoppetResponseInitEvent]).toOption) 54 | .map(_ -> restRespEvents) 55 | } 56 | .getOrElseF(IO.raiseError(new RuntimeException("Init event is not found"))) 57 | (initRespEvent, restRespEvents) = x 58 | respValue = initRespEvent.eagerResponse match { 59 | case Some(eagerResponse) => eagerResponse.asLeft[Stream[IO, Json]] 60 | case _ => 61 | restRespEvents 62 | .flatMap(e => 63 | Stream.fromOption(e.data.flatMap(d => parse(d).flatMap(_.as[PoppetResultEvent]).toOption)) 64 | ) 65 | .flatMap(e => Stream.emits(e.values)) 66 | .asRight[Json] 67 | } 68 | } yield Response(respValue) 69 | 70 | def get: UserService = Consumer[IO, Either[Json, Stream[IO, Json]]](transport).service[UserService] 71 | } 72 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/provider/src/poppet/example/tapir/provider/Application.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.provider 2 | 3 | import cats.effect.kernel.Resource 4 | import cats.effect.ExitCode 5 | import cats.effect.IO 6 | import cats.effect.IOApp 7 | import org.http4s.blaze.server._ 8 | import org.http4s.implicits._ 9 | import poppet.example.tapir.provider.api.ProviderApi 10 | import poppet.example.tapir.provider.service.UserInternalService 11 | import sttp.tapir.server.http4s.Http4sServerInterpreter 12 | 13 | object Application extends IOApp { 14 | 15 | override def run(args: List[String]): IO[ExitCode] = (for { 16 | providerApi <- Resource.pure[IO, ProviderApi](new ProviderApi(new UserInternalService())) 17 | routes = Http4sServerInterpreter[IO]().toRoutes(providerApi.routes) 18 | server <- BlazeServerBuilder[IO] 19 | .bindHttp(9001, "0.0.0.0") 20 | .withHttpApp(routes.orNotFound) 21 | .resource 22 | } yield ExitCode.Success).useForever 23 | 24 | } 25 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/provider/src/poppet/example/tapir/provider/api/ProviderApi.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.provider.api 2 | 3 | import cats.data.OptionT 4 | import cats.effect.std.Queue 5 | import cats.effect.IO 6 | import cats.implicits._ 7 | import fs2.concurrent.Channel 8 | import fs2.Stream 9 | import io.circe.generic.auto._ 10 | import io.circe.parser._ 11 | import io.circe.syntax._ 12 | import io.circe.Json 13 | import java.time.Instant 14 | import org.http4s.ServerSentEvent 15 | import poppet.codec.circe.all._ 16 | import poppet.example.tapir.model.CustomCodecs._ 17 | import poppet.example.tapir.model.PoppetArgumentEvent 18 | import poppet.example.tapir.model.PoppetRequestInitEvent 19 | import poppet.example.tapir.model.PoppetResponseInitEvent 20 | import poppet.example.tapir.model.PoppetResultEvent 21 | import poppet.example.tapir.model.User 22 | import poppet.example.tapir.service.UserService 23 | import poppet.example.tapir.Util 24 | import poppet.provider.all._ 25 | import poppet.Request 26 | import sttp.capabilities.fs2.Fs2Streams 27 | import sttp.tapir._ 28 | import sttp.tapir.json.circe._ 29 | 30 | class ProviderApi(userService: UserService) { 31 | private val provider = Provider[IO, Either[Json, Stream[IO, Json]]]().service(userService) 32 | 33 | val routes = List( 34 | endpoint 35 | .post 36 | .in("api" / "service") 37 | .in(streamBody(Fs2Streams[IO])(implicitly[Schema[Json]], CodecFormat.OctetStream())) 38 | .out(streamBody(Fs2Streams[IO])(implicitly[Schema[Json]], CodecFormat.TextEventStream())) 39 | .serverLogicSuccess[IO] { case (inputStream) => 40 | for { 41 | eventsStream <- IO.pure( 42 | inputStream 43 | .through(ServerSentEvent.decoder) 44 | .flatMap(e => Stream.fromOption(e.data.flatMap(parse(_).toOption))) 45 | ) 46 | x <- OptionT 47 | .apply(Util.uncons(eventsStream)) 48 | .subflatMap { case (initReqEvent, restReqEvents) => 49 | initReqEvent.as[PoppetRequestInitEvent].toOption.map(_ -> restReqEvents) 50 | } 51 | .getOrElseF(IO.raiseError(new RuntimeException("Init event is not found"))) 52 | (initReqEvent, restReqEvents) = x 53 | resultStream = for { 54 | channels <- Stream.eval(initReqEvent.streamArguments.toList.traverse(arg => 55 | Channel.synchronous[IO, Json].map(arg -> _) 56 | ).map(_.toMap)) 57 | _ <- Stream.resource((for { 58 | _ <- restReqEvents 59 | .flatMap(e => Stream.fromOption(e.as[PoppetArgumentEvent].toOption)) 60 | .evalMap(e => e.values.traverse(channels(e.argumentName).send)) 61 | .compile 62 | .drain 63 | _ <- channels.values.toList.traverse(_.close) 64 | } yield ()).background.void) 65 | providerResult <- Stream.eval(provider(Request( 66 | initReqEvent.service, 67 | initReqEvent.method, 68 | initReqEvent.eagerArguments.view.mapValues(_.asLeft[Stream[IO, Json]]).toMap ++ 69 | initReqEvent.streamArguments.map(arg => arg -> channels(arg).stream.asRight[Json]) 70 | ))) 71 | resultEvent <- providerResult.value.fold( 72 | json => Stream.emit(PoppetResponseInitEvent(json.some).asJson), 73 | stream => 74 | Stream.emit(PoppetResponseInitEvent(None).asJson) ++ 75 | stream.chunks.map(chunk => PoppetResultEvent(chunk.toList).asJson), 76 | ) 77 | } yield resultEvent 78 | } yield resultStream 79 | .map(c => ServerSentEvent(data = c.noSpaces.some)) 80 | .through(ServerSentEvent.encoder) 81 | } 82 | ) 83 | 84 | } 85 | -------------------------------------------------------------------------------- /example/tapir-sttp-fs2-circe/provider/src/poppet/example/tapir/provider/service/UserInternalService.scala: -------------------------------------------------------------------------------- 1 | package poppet.example.tapir.provider.service 2 | 3 | import cats.effect.IO 4 | import fs2._ 5 | import poppet.example.tapir.model.User 6 | import poppet.example.tapir.service.UserService 7 | import scala.concurrent.duration._ 8 | 9 | class UserInternalService extends UserService { 10 | private val users = List("Antony", "John", "Alice") 11 | 12 | override def findByIds(ids: Stream[IO, String]): IO[Stream[IO, User]] = { 13 | IO.pure( 14 | ids.zip(Stream.emits(users)) 15 | .map { case (id, name) => User(id, name) } 16 | ) 17 | } 18 | 19 | override def findById(id: String): IO[User] = IO.delay { 20 | id.toIntOption.filter(users.isDefinedAt) match { 21 | case Some(i) => User(i.toString, users(i)) 22 | case _ => throw new IllegalArgumentException("User is not found") 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /jackson/src-2/poppet/codec/jackson/instances/JacksonCodecInstancesBinCompat.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.jackson.instances 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import poppet._ 6 | import poppet.codec.jackson.instances.JacksonCodecInstancesBinCompat._ 7 | import scala.language.experimental.macros 8 | import scala.reflect.macros.blackbox 9 | 10 | trait JacksonCodecInstancesBinCompat { 11 | implicit def jacksonFromJsonCodec[A](implicit om: ObjectMapper): Codec[JsonNode, A] = 12 | macro jacksonFromJsonCodecImpl[A] 13 | } 14 | 15 | object JacksonCodecInstancesBinCompat { 16 | def jacksonFromJsonCodecImpl[A](c: blackbox.Context)(om: c.Expr[ObjectMapper])( 17 | implicit AT: c.WeakTypeTag[A] 18 | ): c.universe.Tree = { 19 | import c.universe._ 20 | q"""new _root_.poppet.Codec[_root_.com.fasterxml.jackson.databind.JsonNode, $AT] { 21 | def apply( 22 | a: _root_.com.fasterxml.jackson.databind.JsonNode 23 | ): _root_.scala.Either[_root_.poppet.CodecFailure[_root_.com.fasterxml.jackson.databind.JsonNode], $AT] = { 24 | try _root_.scala.Right($om.readValue( 25 | $om.treeAsTokens(a), 26 | new _root_.com.fasterxml.jackson.core.`type`.TypeReference[$AT] {} 27 | )) 28 | catch { case e: _root_.scala.Exception => _root_.scala.Left( 29 | new _root_.poppet.CodecFailure(e.getMessage, a, e) 30 | ) } 31 | } 32 | }""" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /jackson/src-3/poppet/codec/jackson/instances/JacksonCodecInstancesBinCompat.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.jackson.instances 2 | 3 | import cats.Applicative 4 | import cats.Functor 5 | import cats.implicits.* 6 | import com.fasterxml.jackson.core.`type`.TypeReference 7 | import com.fasterxml.jackson.databind.JsonNode 8 | import com.fasterxml.jackson.databind.ObjectMapper 9 | import poppet.* 10 | 11 | trait JacksonCodecInstancesBinCompat { 12 | protected class MacroTypeReference[A] extends TypeReference[A] 13 | 14 | implicit inline def jacksonFromJsonCodec[A](implicit inline om: ObjectMapper): Codec[JsonNode, A] = 15 | new Codec[JsonNode, A] { 16 | override def apply(a: JsonNode): Either[CodecFailure[JsonNode], A] = { 17 | try Right(om.readValue(om.treeAsTokens(a), new MacroTypeReference[A] {})) 18 | catch { case e: Exception => Left(new CodecFailure(e.getMessage, a, e)) } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /jackson/src/poppet/codec/jackson/all/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.jackson 2 | 3 | import poppet.codec.jackson.instances.JacksonCodecInstances 4 | 5 | package object all extends JacksonCodecInstances 6 | -------------------------------------------------------------------------------- /jackson/src/poppet/codec/jackson/instances/JacksonCodecInstances.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.jackson.instances 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import poppet._ 6 | 7 | trait JacksonCodenInstancesLp0 extends JacksonCodecInstancesBinCompat { 8 | implicit def jacksonToJsonCodec[A](implicit om: ObjectMapper): Codec[A, JsonNode] = a => { 9 | try Right(om.valueToTree(a)) 10 | catch { case e: Exception => Left(new CodecFailure(e.getMessage, a, e)) } 11 | } 12 | } 13 | 14 | trait JacksonCodecInstances extends JacksonCodenInstancesLp0 { 15 | implicit def jacksonUnitToJsonCodec(implicit om: ObjectMapper): Codec[Unit, JsonNode] = _ => 16 | Right(om.createObjectNode()) 17 | implicit def jacksonJsonToUnitCodec(implicit om: ObjectMapper): Codec[JsonNode, Unit] = _ => 18 | Right(()) 19 | } 20 | -------------------------------------------------------------------------------- /jackson/src/poppet/codec/jackson/instances/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.jackson 2 | 3 | package object instances extends JacksonCodecInstances 4 | -------------------------------------------------------------------------------- /jackson/test/src/poppet/codec/jackson/JacksonCodecSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.jackson 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.module.scala.ClassTagExtensions 6 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 7 | import org.scalatest.freespec.AnyFreeSpec 8 | import poppet.codec.jackson.all._ 9 | import poppet.codec.CodecSpec 10 | import poppet.codec.CodecSpec.A 11 | 12 | class JacksonCodecSpec extends AnyFreeSpec with CodecSpec { 13 | 14 | implicit val objectMapper: ObjectMapper = { 15 | val objectMapper = new ObjectMapper() with ClassTagExtensions 16 | objectMapper.registerModule(DefaultScalaModule) 17 | objectMapper 18 | } 19 | 20 | "Jackson codec should parse" - { 21 | "custom data structures" in { 22 | assertCustomCodec[JsonNode, Unit](()) 23 | assertCustomCodec[JsonNode, Int](intExample) 24 | assertCustomCodec[JsonNode, Long](1L) 25 | assertCustomCodec[JsonNode, String](stringExample) 26 | assertCustomCodec[JsonNode, A](caseClassExample) 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /mill: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # This is a wrapper script, that automatically download mill from GitHub release pages 4 | # You can give the required mill version with MILL_VERSION env variable 5 | # If no version is given, it falls back to the value of DEFAULT_MILL_VERSION 6 | DEFAULT_MILL_VERSION=0.11.1 7 | 8 | set -e 9 | 10 | if [ -z "$MILL_VERSION" ] ; then 11 | if [ -f ".mill-version" ] ; then 12 | MILL_VERSION="$(head -n 1 .mill-version 2> /dev/null)" 13 | elif [ -f "mill" ] && [ "$BASH_SOURCE" != "mill" ] ; then 14 | MILL_VERSION=$(grep -F "DEFAULT_MILL_VERSION=" "mill" | head -n 1 | cut -d= -f2) 15 | else 16 | MILL_VERSION=$DEFAULT_MILL_VERSION 17 | fi 18 | fi 19 | 20 | if [ "x${XDG_CACHE_HOME}" != "x" ] ; then 21 | MILL_DOWNLOAD_PATH="${XDG_CACHE_HOME}/mill/download" 22 | else 23 | MILL_DOWNLOAD_PATH="${HOME}/.cache/mill/download" 24 | fi 25 | MILL_EXEC_PATH="${MILL_DOWNLOAD_PATH}/${MILL_VERSION}" 26 | 27 | version_remainder="$MILL_VERSION" 28 | MILL_MAJOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}" 29 | MILL_MINOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}" 30 | 31 | if [ ! -s "$MILL_EXEC_PATH" ] ; then 32 | mkdir -p $MILL_DOWNLOAD_PATH 33 | if [ "$MILL_MAJOR_VERSION" -gt 0 ] || [ "$MILL_MINOR_VERSION" -ge 5 ] ; then 34 | ASSEMBLY="-assembly" 35 | fi 36 | DOWNLOAD_FILE=$MILL_EXEC_PATH-tmp-download 37 | MILL_VERSION_TAG=$(echo $MILL_VERSION | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') 38 | MILL_DOWNLOAD_URL="https://github.com/com-lihaoyi/mill/releases/download/${MILL_VERSION_TAG}/$MILL_VERSION${ASSEMBLY}" 39 | curl --fail -L -o "$DOWNLOAD_FILE" "$MILL_DOWNLOAD_URL" 40 | chmod +x "$DOWNLOAD_FILE" 41 | mv "$DOWNLOAD_FILE" "$MILL_EXEC_PATH" 42 | unset DOWNLOAD_FILE 43 | unset MILL_DOWNLOAD_URL 44 | fi 45 | 46 | unset MILL_DOWNLOAD_PATH 47 | unset MILL_VERSION 48 | 49 | exec $MILL_EXEC_PATH "$@" 50 | -------------------------------------------------------------------------------- /play-json/src/poppet/codec/play/all/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.play 2 | 3 | import poppet.codec.play.instances.PlayJsonCodecInstances 4 | 5 | package object all extends PlayJsonCodecInstances 6 | -------------------------------------------------------------------------------- /play-json/src/poppet/codec/play/instances/PlayJsonCodecInstances.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.play.instances 2 | 3 | import play.api.libs.json._ 4 | import poppet._ 5 | import poppet.core.Request 6 | import poppet.core.Response 7 | import scala.collection.Seq 8 | 9 | trait PlayJsonCodecInstancesLp0 { 10 | implicit def playJsonReadsToCodec[A: Reads]: Codec[JsValue, A] = a => implicitly[Reads[A]].reads(a).asEither 11 | .left.map(f => new CodecFailure(f.headOption.map(e => s"${e._1} ${e._2}").getOrElse("Codec failure"), a)) 12 | } 13 | 14 | trait PlayJsonCodecInstances extends PlayJsonCodecInstancesLp0 { 15 | implicit val unitFormat: Format[Unit] = Format[Unit](_ => JsSuccess(()), _ => JsObject(Seq.empty)) 16 | implicit val rqFormat: Format[Request[JsValue]] = Json.format[Request[JsValue]] 17 | implicit val rsFormat: Format[Response[JsValue]] = Json.format[Response[JsValue]] 18 | 19 | implicit def playJsonWritesToCodec[A: Writes]: Codec[A, JsValue] = a => Right(implicitly[Writes[A]].writes(a)) 20 | } 21 | -------------------------------------------------------------------------------- /play-json/src/poppet/codec/play/instances/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.play 2 | 3 | package object instances extends PlayJsonCodecInstances 4 | -------------------------------------------------------------------------------- /play-json/test/src/poppet/codec/play/PlayJsonCodecSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.play 2 | 3 | import org.scalatest.freespec.AnyFreeSpec 4 | import play.api.libs.json.JsValue 5 | import play.api.libs.json.Json 6 | import poppet.codec.CodecSpec 7 | import poppet.codec.CodecSpec.A 8 | import poppet.codec.play.all._ 9 | 10 | class PlayJsonCodecSpec extends AnyFreeSpec with CodecSpec { 11 | "Play codec should parse" - { 12 | "custom data structures" in { 13 | implicit val F = Json.format[A] 14 | assertCustomCodec[JsValue, Unit](()) 15 | assertCustomCodec[JsValue, Int](intExample) 16 | assertCustomCodec[JsValue, String](stringExample) 17 | assertCustomCodec[JsValue, A](caseClassExample) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /upickle/src/poppet/codec/upickle/binary/all/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.upickle.binary 2 | 3 | import poppet.codec.upickle.binary.instances.UpickleBinaryCodecInstances 4 | 5 | package object all extends UpickleBinaryCodecInstances 6 | -------------------------------------------------------------------------------- /upickle/src/poppet/codec/upickle/binary/instances/UpickleBinaryCodecInstances.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.upickle.binary.instances 2 | 3 | import poppet._ 4 | import poppet.core.Request 5 | import poppet.core.Response 6 | import upack.Msg 7 | import upickle.default._ 8 | 9 | trait UpickleBinaryCodecInstancesLp0 { 10 | implicit def upickleReaderToByteCodec[A: Reader]: Codec[Msg, A] = a => 11 | try Right(readBinary[A](a)) 12 | catch { case e: Msg.InvalidData => Left(new CodecFailure(e.getMessage, a, e)) } 13 | } 14 | 15 | trait UpickleBinaryCodecInstances extends UpickleBinaryCodecInstancesLp0 { 16 | implicit val upickleRequestBinaryRW: ReadWriter[Request[Msg]] = macroRW[Request[Msg]] 17 | implicit val upickleResponseBinaryRW: ReadWriter[Response[Msg]] = macroRW[Response[Msg]] 18 | 19 | implicit def upickleWriterToByteCodec[A: Writer]: Codec[A, Msg] = a => Right(writeMsg(a)) 20 | } 21 | -------------------------------------------------------------------------------- /upickle/src/poppet/codec/upickle/binary/instances/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.upickle.binary 2 | 3 | package object instances extends UpickleBinaryCodecInstances 4 | -------------------------------------------------------------------------------- /upickle/src/poppet/codec/upickle/json/all/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.upickle.json 2 | 3 | import poppet.codec.upickle.json.instances.UpickleJsonCodecInstances 4 | 5 | package object all extends UpickleJsonCodecInstances 6 | -------------------------------------------------------------------------------- /upickle/src/poppet/codec/upickle/json/instances/UpickleJsonCodecInstances.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.upickle.json.instances 2 | 3 | import poppet._ 4 | import poppet.core.Request 5 | import poppet.core.Response 6 | import ujson.ParsingFailedException 7 | import ujson.Value 8 | import upickle.core.Abort 9 | import upickle.default._ 10 | 11 | trait UpickleJsonCodecInstancesLp0 { 12 | implicit def upickleReaderToJsonCodec[A: Reader]: Codec[Value, A] = a => 13 | try Right(read[A](a)) 14 | catch {case e: Abort => Left(new CodecFailure(e.getMessage, a, e))} 15 | } 16 | 17 | trait UpickleJsonCodecInstances extends UpickleJsonCodecInstancesLp0 { 18 | implicit val upickleRequestJsonRW: ReadWriter[Request[Value]] = macroRW[Request[Value]] 19 | implicit val upickleResponseJsonRW: ReadWriter[Response[Value]] = macroRW[Response[Value]] 20 | 21 | implicit def upickleWriterToJsonCodec[A: Writer]: Codec[A, Value] = a => Right(writeJs(a)) 22 | } 23 | -------------------------------------------------------------------------------- /upickle/src/poppet/codec/upickle/json/instances/package.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.upickle.json 2 | 3 | package object instances extends UpickleJsonCodecInstances 4 | -------------------------------------------------------------------------------- /upickle/test/src/poppet/codec/upickle/binary/UpickleBinaryCodecSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.upickle.binary 2 | 3 | import org.scalatest.freespec.AnyFreeSpec 4 | import poppet.codec.CodecSpec 5 | import poppet.codec.CodecSpec.A 6 | import poppet.codec.upickle.binary.all._ 7 | import upack.Msg 8 | import upickle.default._ 9 | 10 | class UpickleBinaryCodecSpec extends AnyFreeSpec with CodecSpec { 11 | "Upickle binary codec should parse" - { 12 | "custom data structures" in { 13 | implicit val RW: ReadWriter[A] = macroRW[A] 14 | assertCustomCodec[Msg, Unit](()) 15 | assertCustomCodec[Msg, Int](intExample) 16 | assertCustomCodec[Msg, String](stringExample) 17 | assertCustomCodec[Msg, A](caseClassExample) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /upickle/test/src/poppet/codec/upickle/json/UpickleJsonCodecSpec.scala: -------------------------------------------------------------------------------- 1 | package poppet.codec.upickle.json 2 | 3 | import org.scalatest.freespec.AnyFreeSpec 4 | import poppet.codec.CodecSpec 5 | import poppet.codec.CodecSpec.A 6 | import poppet.codec.upickle.json.all._ 7 | import ujson.Value 8 | import upickle.default._ 9 | 10 | class UpickleJsonCodecSpec extends AnyFreeSpec with CodecSpec { 11 | "Upickle binary codec should parse" - { 12 | "custom data structures" in { 13 | implicit val RW: ReadWriter[A] = macroRW[A] 14 | assertCustomCodec[Value, Unit](()) 15 | assertCustomCodec[Value, Int](intExample) 16 | assertCustomCodec[Value, String](stringExample) 17 | assertCustomCodec[Value, A](caseClassExample) 18 | } 19 | } 20 | } 21 | --------------------------------------------------------------------------------