├── .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 | [](https://mvnrepository.com/search?q=poppet)
3 | [](https://oss.sonatype.org/content/repositories/snapshots/com/github/yakivy/poppet-core_2.13/)
4 | [](https://opensource.org/licenses/MIT)
5 |
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 |
--------------------------------------------------------------------------------