Skip to content

Commit

Permalink
#8 ♻️ Restructure JSON deserialisation to enable other implementation…
Browse files Browse the repository at this point in the history
…s 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.
  • Loading branch information
jwojnowski committed Feb 28, 2023
1 parent 19099b9 commit 76f7aa2
Show file tree
Hide file tree
Showing 54 changed files with 1,493 additions and 881 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
scala: [2.12.17, 2.13.10, 3.1.3]
scala: [2.12.17, 2.13.10, 3.2.2]
java: [[email protected]]
runs-on: ${{ matrix.os }}
steps:
Expand Down Expand Up @@ -59,7 +59,7 @@ jobs:
- run: sbt ++${{ matrix.scala }} test docs/mdoc mimaReportBinaryIssues

- name: Compress target directories
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
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

- name: Upload target directories
uses: actions/upload-artifact@v2
Expand Down Expand Up @@ -120,12 +120,12 @@ jobs:
tar xf targets.tar
rm targets.tar
- name: Download target directories (3.1.3)
- name: Download target directories (3.2.2)
uses: actions/download-artifact@v2
with:
name: target-${{ matrix.os }}-3.1.3-${{ matrix.java }}
name: target-${{ matrix.os }}-3.2.2-${{ matrix.java }}

- name: Inflate target directories (3.1.3)
- name: Inflate target directories (3.2.2)
run: |
tar xf targets.tar
rm targets.tar
Expand Down
51 changes: 44 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def crossPlugin(x: sbt.librarymanagement.ModuleID) = compilerPlugin(x.cross(Cros

val Scala212 = "2.12.17"
val Scala213 = "2.13.10"
val Scala3 = "3.1.3"
val Scala3 = "3.2.2"

val GraalVM11 = "[email protected]"

Expand Down Expand Up @@ -62,6 +62,7 @@ val Versions = new {
val catsEffect = "3.3.14"
val catsEffect2 = "2.5.5"
val circe = "0.14.3"
val jsoniter = "2.21.1"
val monix = "3.4.1"
val scalaTest = "3.2.15"
val sttp = "3.3.18"
Expand Down Expand Up @@ -97,12 +98,8 @@ lazy val oauth2 = crossProject(JSPlatform, JVMPlatform)
.settings(
name := "sttp-oauth2",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % Versions.catsCore,
"io.circe" %%% "circe-parser" % Versions.circe,
"io.circe" %%% "circe-core" % Versions.circe,
"io.circe" %%% "circe-refined" % Versions.circe,
"com.softwaremill.sttp.client3" %%% "core" % Versions.sttp,
"com.softwaremill.sttp.client3" %%% "circe" % Versions.sttp,
"org.typelevel" %%% "cats-core" % Versions.catsCore,
"eu.timepit" %%% "refined" % Versions.refined,
"org.scalatest" %%% "scalatest" % Versions.scalaTest % Test
),
Expand All @@ -114,6 +111,42 @@ lazy val oauth2 = crossProject(JSPlatform, JVMPlatform)
jsSettings
)

lazy val `oauth2-circe` = crossProject(JSPlatform, JVMPlatform)
.withoutSuffixFor(JVMPlatform)
.in(file("oauth2-circe"))
.settings(
name := "sttp-oauth2-circe",
libraryDependencies ++= Seq(
"io.circe" %%% "circe-parser" % Versions.circe,
"io.circe" %%% "circe-core" % Versions.circe,
"io.circe" %%% "circe-refined" % Versions.circe
),
mimaSettings,
compilerPlugins
)
.jsSettings(
jsSettings
)
.dependsOn(oauth2 % "compile->compile;test->test")

lazy val `oauth2-jsoniter` = crossProject(JSPlatform, JVMPlatform)
.withoutSuffixFor(JVMPlatform)
.in(file("oauth2-jsoniter"))
.settings(
name := "sttp-oauth2-jsoniter",
libraryDependencies ++= Seq(
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % Versions.jsoniter,
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % Versions.jsoniter % "compile-internal"
),
mimaSettings,
compilerPlugins,
scalacOptions ++= Seq("-Wconf:cat=deprecation:info") // jsoniter-scala macro-generated code uses deprecated methods
)
.jsSettings(
jsSettings
)
.dependsOn(oauth2 % "compile->compile;test->test")

lazy val docs = project
.in(file("mdoc")) // important: it must not be docs/
.settings(
Expand Down Expand Up @@ -212,5 +245,9 @@ val root = project
`oauth2-cache-ce2`,
`oauth2-cache-future`.jvm,
`oauth2-cache-future`.js,
`oauth2-cache-scalacache`
`oauth2-cache-scalacache`,
`oauth2-circe`.jvm,
`oauth2-circe`.js,
`oauth2-jsoniter`.jvm,
`oauth2-jsoniter`.js,
)
4 changes: 3 additions & 1 deletion docs/client-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ description: Client credentials grant documentation

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

```scala
import com.ocadotechnology.sttp.oauth2.json.circe.instances._ // Or your favorite JSON implementation

val accessTokenProvider = AccessTokenProvider[IO](tokenUrl, clientId, clientSecret)(backend)
val tokenIntrospection = TokenIntrospection[IO](tokenIntrospectionUrl, clientId, clientSecret)(backend)
val scope: Option[Scope] = Some("scope")
Expand Down
7 changes: 7 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,10 @@ The raw documentation goes through a few steps process before the final website
- 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.
- When the build finishes, the compiled documentation ends up in `./mdoc/target/mdoc/`
- 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.

## Adding JSON implementations
When adding a JSON implementation please follow the subsequent guidelines:
1. Each JSON implementation should exist in a separate module, not to introduce unwanted dependencies.
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.
3. It should make use of `com.ocadotechnology.sttp.oauth2.json.JsonSpec` test suite to ensure correctness.
4. It should be included in the documentation ([JSON Deserialisation](json-deserialisation.md)).
10 changes: 9 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,24 @@ description: Getting started with sttp-oauth2
## About

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.
It uses [circe](https://github.com/circe/circe) for JSON serialization/deserialization.
There are multiple JSON implementations, see [JSON deserialisation](json-deserialisation.md) for details.

## Installation

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

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

Each grant is implemented in an object with explicit return and error types on methods and additionally, Tagless Final friendly `*Provider` interface.

All grant implementations require a set of implicit `JsonDecoder`s, e.g.:
```scala
import com.ocadotechnology.sttp.oauth2.json.circe.instances._
```

See [JSON deserialisation](json-deserialisation.md) for details.
36 changes: 36 additions & 0 deletions docs/json-deserialisation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
sidebar_position: 7
description: Choosing JSON deserialisation module
---

# Choosing JSON deserialisation module
JSON deserialisation has been decoupled from the core modules.
There are now a couple of options to choose from:

## circe
To use [circe](https://github.com/circe/circe) implementation
add the following module to your dependencies:

```scala
"com.ocadotechnology" %% "sttp-oauth2-circe" % "@VERSION@"
```

Then import appropriate set of implicit instances:

```scala
import com.ocadotechnology.sttp.oauth2.json.circe.instances._
```

## jsoniter-scala
To use [jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala) implementation
add the following module to your dependencies:

```scala
"com.ocadotechnology" %% "sttp-oauth2-jsoniter" % "@VERSION@"
```

Then import appropriate set of implicit instances:

```scala
import com.ocadotechnology.sttp.oauth2.json.jsoniter.instances._
```
19 changes: 18 additions & 1 deletion docs/migrating.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
---
sidebar_position: 6
sidebar_position: 8
description: Migrations
---

# Migrating to newer versions

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

## [v0.17.0-RC-1](https://github.com/ocadotechnology/sttp-oauth2/releases/tag/v0.17.0)

Significant changes were introduced due to separation of JSON deserialisation from the core. Adding a module
with chosen JSON implementation is now required, as is importing an associated set of `JsonDecoder`s.

For backwards compatibility just add `circe` module:

```scala
"com.ocadotechnology" %% "sttp-oauth2-circe" % "0.16.0"
```

and a following import where you were using `sttp-oauth2`:

```scala
import com.ocadotechnology.sttp.oauth2.json.circe.instances._
```

## [v0.16.0](https://github.com/ocadotechnology/sttp-oauth2/releases/tag/v0.16.0)

Minor change [#336](https://github.com/ocadotechnology/sttp-oauth2/pull/336) removed implicit parameter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.ocadotechnology.sttp.oauth2.json.circe

import cats.syntax.all._
import com.ocadotechnology.sttp.oauth2.ClientCredentialsToken.AccessTokenResponse
import com.ocadotechnology.sttp.oauth2.UserInfo
import com.ocadotechnology.sttp.oauth2.common.Error.OAuth2Error
import com.ocadotechnology.sttp.oauth2.ExtendedOAuth2TokenResponse
import com.ocadotechnology.sttp.oauth2.Introspection.Audience
import com.ocadotechnology.sttp.oauth2.Introspection.SeqAudience
import com.ocadotechnology.sttp.oauth2.Introspection.StringAudience
import com.ocadotechnology.sttp.oauth2.Introspection.TokenIntrospectionResponse
import com.ocadotechnology.sttp.oauth2.OAuth2TokenResponse
import com.ocadotechnology.sttp.oauth2.RefreshTokenResponse
import com.ocadotechnology.sttp.oauth2.Secret
import com.ocadotechnology.sttp.oauth2.TokenUserDetails
import com.ocadotechnology.sttp.oauth2.json.JsonDecoder
import io.circe.Decoder
import io.circe.refined._

import java.time.Instant
import scala.concurrent.duration.DurationLong
import scala.concurrent.duration.FiniteDuration

trait CirceJsonDecoders {

implicit def jsonDecoder[A](implicit decoder: Decoder[A]): JsonDecoder[A] =
(data: String) => io.circe.parser.decode[A](data).leftMap(error => JsonDecoder.Error(error.getMessage, cause = Some(error)))

implicit val userInfoDecoder: Decoder[UserInfo] = (
Decoder[Option[String]].at("sub"),
Decoder[Option[String]].at("name"),
Decoder[Option[String]].at("given_name"),
Decoder[Option[String]].at("family_name"),
Decoder[Option[String]].at("job_title"),
Decoder[Option[String]].at("domain"),
Decoder[Option[String]].at("preferred_username"),
Decoder[Option[String]].at("email"),
Decoder[Option[Boolean]].at("email_verified"),
Decoder[Option[String]].at("locale"),
Decoder[List[String]].at("sites").or(Decoder.const(List.empty[String])),
Decoder[List[String]].at("banners").or(Decoder.const(List.empty[String])),
Decoder[List[String]].at("regions").or(Decoder.const(List.empty[String])),
Decoder[List[String]].at("fulfillment_contexts").or(Decoder.const(List.empty[String]))
).mapN(UserInfo.apply)

implicit val secondsDecoder: Decoder[FiniteDuration] = Decoder.decodeLong.map(_.seconds)

implicit val instantDecoder: Decoder[Instant] = Decoder.decodeLong.map(Instant.ofEpochSecond)

implicit val tokenDecoder: Decoder[AccessTokenResponse] =
Decoder
.forProduct4(
"access_token",
"domain",
"expires_in",
"scope"
)(AccessTokenResponse.apply)
.validate {
_.downField("token_type").as[String] match {
case Right(value) if value.equalsIgnoreCase("Bearer") => List.empty
case Right(string) => List(s"Error while decoding '.token_type': value '$string' is not equal to 'Bearer'")
case Left(s) => List(s"Error while decoding '.token_type': ${s.getMessage}")
}
}

implicit val errorDecoder: Decoder[OAuth2Error] =
Decoder.forProduct2[OAuth2Error, String, Option[String]]("error", "error_description")(OAuth2Error.fromErrorTypeAndDescription)

implicit val tokenResponseDecoder: Decoder[OAuth2TokenResponse] =
Decoder.forProduct5(
"access_token",
"scope",
"token_type",
"expires_in",
"refresh_token"
)(OAuth2TokenResponse.apply)

implicit val tokenUserDetailsDecoder: Decoder[TokenUserDetails] =
Decoder.forProduct7(
"username",
"name",
"forename",
"surname",
"mail",
"cn",
"sn"
)(TokenUserDetails.apply)

implicit val extendedTokenResponseDecoder: Decoder[ExtendedOAuth2TokenResponse] =
Decoder.forProduct11(
"access_token",
"refresh_token",
"expires_in",
"user_name",
"domain",
"user_details",
"roles",
"scope",
"security_level",
"user_id",
"token_type"
)(ExtendedOAuth2TokenResponse.apply)

implicit val audienceDecoder: Decoder[Audience] =
Decoder.decodeString.map(StringAudience.apply).or(Decoder.decodeSeq[String].map(SeqAudience.apply))

implicit val tokenIntrospectionResponseDecoder: Decoder[TokenIntrospectionResponse] =
Decoder.forProduct13(
"active",
"client_id",
"domain",
"exp",
"iat",
"nbf",
"authorities",
"scope",
"token_type",
"sub",
"iss",
"jti",
"aud"
)(TokenIntrospectionResponse.apply)

implicit val refreshTokenResponseDecoder: Decoder[RefreshTokenResponse] =
Decoder.forProduct11(
"access_token",
"refresh_token",
"expires_in",
"user_name",
"domain",
"user_details",
"roles",
"scope",
"security_level",
"user_id",
"token_type"
)(RefreshTokenResponse.apply)

implicit def secretDecoder[A: Decoder]: Decoder[Secret[A]] = Decoder[A].map(Secret(_))

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.ocadotechnology.sttp.oauth2.json.circe

object instances extends CirceJsonDecoders
Loading

0 comments on commit 76f7aa2

Please sign in to comment.