Skip to content

Commit 76f7aa2

Browse files
authored
#8 ♻️ Restructure JSON deserialisation to enable other implementations than Circe (#351)
* #8 ♻️ Restructure JSON (de)serialisation to enable other implementations than Circe. * #8 💚 Add new modules to the CI. * #8 ♻️ Revert the core module and group JSON-related tests in a single trait for simplified implementation per JSON module. * #8 ♻️ Remove unused EntityDecoder. Replace codec in names with json. * #8 ♻️ Remove JsonInput.sanitize from JsonSupport and move it to the main package. * #8 🎨 Optimise imports and reformat a few files. * #8 📝 Modify docs to reflect changes in JSON deserialisation support. * #8 📝 Add backwards compatibility TL;DR regarding circe module to the changelog 👌. * ♻️ Make `EncodedJson` final. * #8 ✨ Add jsoniter-scala module. * 💚 Fix CI. * ⬆️ Bump sbt-scalajs. * ⬆️ Bump scala 3 version. * 🔧 Tweak deprecation warning configuration for jsoniter modules. Macro-generated code uses deprecated methods: ``` .../com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMaker.scala: method isEmpty in class IterableOnceExtensionMethods is deprecated since 2.13.0: Use .iterator.isEmpty instead ``` * ♻️ Move OAuth2Error creation to common to avoid duplication in JSON implementations.
1 parent 19099b9 commit 76f7aa2

File tree

54 files changed

+1493
-881
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1493
-881
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
strategy:
2828
matrix:
2929
os: [ubuntu-latest]
30-
scala: [2.12.17, 2.13.10, 3.1.3]
30+
scala: [2.12.17, 2.13.10, 3.2.2]
3131
3232
runs-on: ${{ matrix.os }}
3333
steps:
@@ -59,7 +59,7 @@ jobs:
5959
- run: sbt ++${{ matrix.scala }} test docs/mdoc mimaReportBinaryIssues
6060

6161
- name: Compress target directories
62-
run: tar cf targets.tar oauth2/js/target oauth2-cache/js/target oauth2-cache-ce2/target target oauth2-cache-scalacache/target mdoc/target oauth2-cache-cats/target oauth2-cache-future/jvm/target oauth2-cache/jvm/target oauth2-cache-future/js/target oauth2/jvm/target project/target
62+
run: tar cf targets.tar oauth2-jsoniter/jvm/target oauth2/js/target oauth2-cache/js/target oauth2-cache-ce2/target oauth2-jsoniter/js/target target oauth2-cache-scalacache/target mdoc/target oauth2-circe/jvm/target oauth2-cache-cats/target oauth2-cache-future/jvm/target oauth2-circe/js/target oauth2-cache/jvm/target oauth2-cache-future/js/target oauth2/jvm/target project/target
6363

6464
- name: Upload target directories
6565
uses: actions/upload-artifact@v2
@@ -120,12 +120,12 @@ jobs:
120120
tar xf targets.tar
121121
rm targets.tar
122122
123-
- name: Download target directories (3.1.3)
123+
- name: Download target directories (3.2.2)
124124
uses: actions/download-artifact@v2
125125
with:
126-
name: target-${{ matrix.os }}-3.1.3-${{ matrix.java }}
126+
name: target-${{ matrix.os }}-3.2.2-${{ matrix.java }}
127127

128-
- name: Inflate target directories (3.1.3)
128+
- name: Inflate target directories (3.2.2)
129129
run: |
130130
tar xf targets.tar
131131
rm targets.tar

build.sbt

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def crossPlugin(x: sbt.librarymanagement.ModuleID) = compilerPlugin(x.cross(Cros
3333

3434
val Scala212 = "2.12.17"
3535
val Scala213 = "2.13.10"
36-
val Scala3 = "3.1.3"
36+
val Scala3 = "3.2.2"
3737

3838
val GraalVM11 = "[email protected]"
3939

@@ -62,6 +62,7 @@ val Versions = new {
6262
val catsEffect = "3.3.14"
6363
val catsEffect2 = "2.5.5"
6464
val circe = "0.14.3"
65+
val jsoniter = "2.21.1"
6566
val monix = "3.4.1"
6667
val scalaTest = "3.2.15"
6768
val sttp = "3.3.18"
@@ -97,12 +98,8 @@ lazy val oauth2 = crossProject(JSPlatform, JVMPlatform)
9798
.settings(
9899
name := "sttp-oauth2",
99100
libraryDependencies ++= Seq(
100-
"org.typelevel" %%% "cats-core" % Versions.catsCore,
101-
"io.circe" %%% "circe-parser" % Versions.circe,
102-
"io.circe" %%% "circe-core" % Versions.circe,
103-
"io.circe" %%% "circe-refined" % Versions.circe,
104101
"com.softwaremill.sttp.client3" %%% "core" % Versions.sttp,
105-
"com.softwaremill.sttp.client3" %%% "circe" % Versions.sttp,
102+
"org.typelevel" %%% "cats-core" % Versions.catsCore,
106103
"eu.timepit" %%% "refined" % Versions.refined,
107104
"org.scalatest" %%% "scalatest" % Versions.scalaTest % Test
108105
),
@@ -114,6 +111,42 @@ lazy val oauth2 = crossProject(JSPlatform, JVMPlatform)
114111
jsSettings
115112
)
116113

114+
lazy val `oauth2-circe` = crossProject(JSPlatform, JVMPlatform)
115+
.withoutSuffixFor(JVMPlatform)
116+
.in(file("oauth2-circe"))
117+
.settings(
118+
name := "sttp-oauth2-circe",
119+
libraryDependencies ++= Seq(
120+
"io.circe" %%% "circe-parser" % Versions.circe,
121+
"io.circe" %%% "circe-core" % Versions.circe,
122+
"io.circe" %%% "circe-refined" % Versions.circe
123+
),
124+
mimaSettings,
125+
compilerPlugins
126+
)
127+
.jsSettings(
128+
jsSettings
129+
)
130+
.dependsOn(oauth2 % "compile->compile;test->test")
131+
132+
lazy val `oauth2-jsoniter` = crossProject(JSPlatform, JVMPlatform)
133+
.withoutSuffixFor(JVMPlatform)
134+
.in(file("oauth2-jsoniter"))
135+
.settings(
136+
name := "sttp-oauth2-jsoniter",
137+
libraryDependencies ++= Seq(
138+
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % Versions.jsoniter,
139+
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % Versions.jsoniter % "compile-internal"
140+
),
141+
mimaSettings,
142+
compilerPlugins,
143+
scalacOptions ++= Seq("-Wconf:cat=deprecation:info") // jsoniter-scala macro-generated code uses deprecated methods
144+
)
145+
.jsSettings(
146+
jsSettings
147+
)
148+
.dependsOn(oauth2 % "compile->compile;test->test")
149+
117150
lazy val docs = project
118151
.in(file("mdoc")) // important: it must not be docs/
119152
.settings(
@@ -212,5 +245,9 @@ val root = project
212245
`oauth2-cache-ce2`,
213246
`oauth2-cache-future`.jvm,
214247
`oauth2-cache-future`.js,
215-
`oauth2-cache-scalacache`
248+
`oauth2-cache-scalacache`,
249+
`oauth2-circe`.jvm,
250+
`oauth2-circe`.js,
251+
`oauth2-jsoniter`.jvm,
252+
`oauth2-jsoniter`.js,
216253
)

docs/client-credentials.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ description: Client credentials grant documentation
77

88
`ClientCredentials` and traits `AccessTokenProvider` and `TokenIntrospection` expose methods that:
99
- Obtain token via `requestToken`
10-
- `introspect` the token for it's details like `UserInfo`
10+
- `introspect` the token for its details like `UserInfo`
1111

1212
```scala
13+
import com.ocadotechnology.sttp.oauth2.json.circe.instances._ // Or your favorite JSON implementation
14+
1315
val accessTokenProvider = AccessTokenProvider[IO](tokenUrl, clientId, clientSecret)(backend)
1416
val tokenIntrospection = TokenIntrospection[IO](tokenIntrospectionUrl, clientId, clientSecret)(backend)
1517
val scope: Option[Scope] = Some("scope")

docs/contributing.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,10 @@ The raw documentation goes through a few steps process before the final website
3838
- The first step when building the documentation is to run `docs/mdoc/`. This step compiles the code examples, verifying if everything makes sense and is up to date.
3939
- When the build finishes, the compiled documentation ends up in `./mdoc/target/mdoc/`
4040
- The last step is to build docusaurus. Docusaurus is configured to read files from `./mdoc/target/mdoc/` and generate the website using regular docusaurus rules.
41+
42+
## Adding JSON implementations
43+
When adding a JSON implementation please follow the subsequent guidelines:
44+
1. Each JSON implementation should exist in a separate module, not to introduce unwanted dependencies.
45+
2. It should expose all necessary `JsonDecoder`s via a single import following the `import com.ocadotechnology.sttp.oauth2.json.<insert-json-library-name-here>.instances._` convention.
46+
3. It should make use of `com.ocadotechnology.sttp.oauth2.json.JsonSpec` test suite to ensure correctness.
47+
4. It should be included in the documentation ([JSON Deserialisation](json-deserialisation.md)).

docs/getting-started.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,24 @@ description: Getting started with sttp-oauth2
88
## About
99

1010
This library aims to provide easy integration with OAuth2 providers based on [OAuth2 RFC](https://tools.ietf.org/html/rfc6749) using [sttp](https://github.com/softwaremill/sttp) client.
11-
It uses [circe](https://github.com/circe/circe) for JSON serialization/deserialization.
11+
There are multiple JSON implementations, see [JSON deserialisation](json-deserialisation.md) for details.
1212

1313
## Installation
1414

1515
To use this library add following dependency to your `build.sbt` file
1616
```scala
1717
"com.ocadotechnology" %% "sttp-oauth2" % "@VERSION@"
18+
"com.ocadotechnology" %% "sttp-oauth2-circe" % "@VERSION@" // Or other, see JSON support
1819
```
1920
## Usage
2021

2122
Depending on your use case, please see documentation for the grant you want to support.
2223

2324
Each grant is implemented in an object with explicit return and error types on methods and additionally, Tagless Final friendly `*Provider` interface.
25+
26+
All grant implementations require a set of implicit `JsonDecoder`s, e.g.:
27+
```scala
28+
import com.ocadotechnology.sttp.oauth2.json.circe.instances._
29+
```
30+
31+
See [JSON deserialisation](json-deserialisation.md) for details.

docs/json-deserialisation.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
sidebar_position: 7
3+
description: Choosing JSON deserialisation module
4+
---
5+
6+
# Choosing JSON deserialisation module
7+
JSON deserialisation has been decoupled from the core modules.
8+
There are now a couple of options to choose from:
9+
10+
## circe
11+
To use [circe](https://github.com/circe/circe) implementation
12+
add the following module to your dependencies:
13+
14+
```scala
15+
"com.ocadotechnology" %% "sttp-oauth2-circe" % "@VERSION@"
16+
```
17+
18+
Then import appropriate set of implicit instances:
19+
20+
```scala
21+
import com.ocadotechnology.sttp.oauth2.json.circe.instances._
22+
```
23+
24+
## jsoniter-scala
25+
To use [jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala) implementation
26+
add the following module to your dependencies:
27+
28+
```scala
29+
"com.ocadotechnology" %% "sttp-oauth2-jsoniter" % "@VERSION@"
30+
```
31+
32+
Then import appropriate set of implicit instances:
33+
34+
```scala
35+
import com.ocadotechnology.sttp.oauth2.json.jsoniter.instances._
36+
```

docs/migrating.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
---
2-
sidebar_position: 6
2+
sidebar_position: 8
33
description: Migrations
44
---
55

66
# Migrating to newer versions
77

88
Some releases introduce breaking changes. This page aims to list those and provide migration guide.
99

10+
## [v0.17.0-RC-1](https://github.com/ocadotechnology/sttp-oauth2/releases/tag/v0.17.0)
11+
12+
Significant changes were introduced due to separation of JSON deserialisation from the core. Adding a module
13+
with chosen JSON implementation is now required, as is importing an associated set of `JsonDecoder`s.
14+
15+
For backwards compatibility just add `circe` module:
16+
17+
```scala
18+
"com.ocadotechnology" %% "sttp-oauth2-circe" % "0.16.0"
19+
```
20+
21+
and a following import where you were using `sttp-oauth2`:
22+
23+
```scala
24+
import com.ocadotechnology.sttp.oauth2.json.circe.instances._
25+
```
26+
1027
## [v0.16.0](https://github.com/ocadotechnology/sttp-oauth2/releases/tag/v0.16.0)
1128

1229
Minor change [#336](https://github.com/ocadotechnology/sttp-oauth2/pull/336) removed implicit parameter
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.ocadotechnology.sttp.oauth2.json.circe
2+
3+
import cats.syntax.all._
4+
import com.ocadotechnology.sttp.oauth2.ClientCredentialsToken.AccessTokenResponse
5+
import com.ocadotechnology.sttp.oauth2.UserInfo
6+
import com.ocadotechnology.sttp.oauth2.common.Error.OAuth2Error
7+
import com.ocadotechnology.sttp.oauth2.ExtendedOAuth2TokenResponse
8+
import com.ocadotechnology.sttp.oauth2.Introspection.Audience
9+
import com.ocadotechnology.sttp.oauth2.Introspection.SeqAudience
10+
import com.ocadotechnology.sttp.oauth2.Introspection.StringAudience
11+
import com.ocadotechnology.sttp.oauth2.Introspection.TokenIntrospectionResponse
12+
import com.ocadotechnology.sttp.oauth2.OAuth2TokenResponse
13+
import com.ocadotechnology.sttp.oauth2.RefreshTokenResponse
14+
import com.ocadotechnology.sttp.oauth2.Secret
15+
import com.ocadotechnology.sttp.oauth2.TokenUserDetails
16+
import com.ocadotechnology.sttp.oauth2.json.JsonDecoder
17+
import io.circe.Decoder
18+
import io.circe.refined._
19+
20+
import java.time.Instant
21+
import scala.concurrent.duration.DurationLong
22+
import scala.concurrent.duration.FiniteDuration
23+
24+
trait CirceJsonDecoders {
25+
26+
implicit def jsonDecoder[A](implicit decoder: Decoder[A]): JsonDecoder[A] =
27+
(data: String) => io.circe.parser.decode[A](data).leftMap(error => JsonDecoder.Error(error.getMessage, cause = Some(error)))
28+
29+
implicit val userInfoDecoder: Decoder[UserInfo] = (
30+
Decoder[Option[String]].at("sub"),
31+
Decoder[Option[String]].at("name"),
32+
Decoder[Option[String]].at("given_name"),
33+
Decoder[Option[String]].at("family_name"),
34+
Decoder[Option[String]].at("job_title"),
35+
Decoder[Option[String]].at("domain"),
36+
Decoder[Option[String]].at("preferred_username"),
37+
Decoder[Option[String]].at("email"),
38+
Decoder[Option[Boolean]].at("email_verified"),
39+
Decoder[Option[String]].at("locale"),
40+
Decoder[List[String]].at("sites").or(Decoder.const(List.empty[String])),
41+
Decoder[List[String]].at("banners").or(Decoder.const(List.empty[String])),
42+
Decoder[List[String]].at("regions").or(Decoder.const(List.empty[String])),
43+
Decoder[List[String]].at("fulfillment_contexts").or(Decoder.const(List.empty[String]))
44+
).mapN(UserInfo.apply)
45+
46+
implicit val secondsDecoder: Decoder[FiniteDuration] = Decoder.decodeLong.map(_.seconds)
47+
48+
implicit val instantDecoder: Decoder[Instant] = Decoder.decodeLong.map(Instant.ofEpochSecond)
49+
50+
implicit val tokenDecoder: Decoder[AccessTokenResponse] =
51+
Decoder
52+
.forProduct4(
53+
"access_token",
54+
"domain",
55+
"expires_in",
56+
"scope"
57+
)(AccessTokenResponse.apply)
58+
.validate {
59+
_.downField("token_type").as[String] match {
60+
case Right(value) if value.equalsIgnoreCase("Bearer") => List.empty
61+
case Right(string) => List(s"Error while decoding '.token_type': value '$string' is not equal to 'Bearer'")
62+
case Left(s) => List(s"Error while decoding '.token_type': ${s.getMessage}")
63+
}
64+
}
65+
66+
implicit val errorDecoder: Decoder[OAuth2Error] =
67+
Decoder.forProduct2[OAuth2Error, String, Option[String]]("error", "error_description")(OAuth2Error.fromErrorTypeAndDescription)
68+
69+
implicit val tokenResponseDecoder: Decoder[OAuth2TokenResponse] =
70+
Decoder.forProduct5(
71+
"access_token",
72+
"scope",
73+
"token_type",
74+
"expires_in",
75+
"refresh_token"
76+
)(OAuth2TokenResponse.apply)
77+
78+
implicit val tokenUserDetailsDecoder: Decoder[TokenUserDetails] =
79+
Decoder.forProduct7(
80+
"username",
81+
"name",
82+
"forename",
83+
"surname",
84+
"mail",
85+
"cn",
86+
"sn"
87+
)(TokenUserDetails.apply)
88+
89+
implicit val extendedTokenResponseDecoder: Decoder[ExtendedOAuth2TokenResponse] =
90+
Decoder.forProduct11(
91+
"access_token",
92+
"refresh_token",
93+
"expires_in",
94+
"user_name",
95+
"domain",
96+
"user_details",
97+
"roles",
98+
"scope",
99+
"security_level",
100+
"user_id",
101+
"token_type"
102+
)(ExtendedOAuth2TokenResponse.apply)
103+
104+
implicit val audienceDecoder: Decoder[Audience] =
105+
Decoder.decodeString.map(StringAudience.apply).or(Decoder.decodeSeq[String].map(SeqAudience.apply))
106+
107+
implicit val tokenIntrospectionResponseDecoder: Decoder[TokenIntrospectionResponse] =
108+
Decoder.forProduct13(
109+
"active",
110+
"client_id",
111+
"domain",
112+
"exp",
113+
"iat",
114+
"nbf",
115+
"authorities",
116+
"scope",
117+
"token_type",
118+
"sub",
119+
"iss",
120+
"jti",
121+
"aud"
122+
)(TokenIntrospectionResponse.apply)
123+
124+
implicit val refreshTokenResponseDecoder: Decoder[RefreshTokenResponse] =
125+
Decoder.forProduct11(
126+
"access_token",
127+
"refresh_token",
128+
"expires_in",
129+
"user_name",
130+
"domain",
131+
"user_details",
132+
"roles",
133+
"scope",
134+
"security_level",
135+
"user_id",
136+
"token_type"
137+
)(RefreshTokenResponse.apply)
138+
139+
implicit def secretDecoder[A: Decoder]: Decoder[Secret[A]] = Decoder[A].map(Secret(_))
140+
141+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.ocadotechnology.sttp.oauth2.json.circe
2+
3+
object instances extends CirceJsonDecoders

0 commit comments

Comments
 (0)