Skip to content

Commit 8de2776

Browse files
committed
feat: add zio-json support
1 parent 94b7bc2 commit 8de2776

File tree

5 files changed

+359
-2
lines changed

5 files changed

+359
-2
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
- run: sbt '++ ${{ matrix.scala }}' test docs/mdoc mimaReportBinaryIssues
5454

5555
- name: Compress target directories
56-
run: tar cf targets.tar oauth2-jsoniter/jvm/target oauth2/js/target oauth2-cache/js/target oauth2-cache-ce2/target oauth2-cache-zio/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
56+
run: tar cf targets.tar oauth2-jsoniter/jvm/target oauth2/js/target oauth2-cache/js/target oauth2-cache-ce2/target oauth2-cache-zio/target oauth2-jsoniter/js/target target oauth2-cache-scalacache/target mdoc/target oauth2-circe/jvm/target oauth2-cache-cats/target oauth2-zio-json/js/target oauth2-cache-future/jvm/target oauth2-circe/js/target oauth2-cache/jvm/target oauth2-cache-future/js/target oauth2-zio-json/jvm/target oauth2/jvm/target project/target
5757

5858
- name: Upload target directories
5959
uses: actions/upload-artifact@v4

build.sbt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import sbtghactions.UseRef
22

3+
Global / onChangedBuildSource := ReloadOnSourceChanges
4+
35
inThisBuild(
46
List(
57
organization := "org.polyvariant",
@@ -51,6 +53,7 @@ val Versions = new {
5153
val catsEffect2 = "2.5.5"
5254
val circe = "0.14.9"
5355
val jsoniter = "2.30.7"
56+
val zioJson = "0.7.45"
5457
val monix = "3.4.1"
5558
val scalaTest = "3.2.19"
5659
val sttp = "4.0.3"
@@ -130,6 +133,27 @@ lazy val `oauth2-jsoniter` = crossProject(JSPlatform, JVMPlatform)
130133
)
131134
.dependsOn(oauth2 % "compile->compile;test->test")
132135

136+
lazy val `oauth2-zio-json` = crossProject(JSPlatform, JVMPlatform)
137+
.withoutSuffixFor(JVMPlatform)
138+
.in(file("oauth2-zio-json"))
139+
.settings(
140+
name := "sttp-oauth2-zio-json",
141+
libraryDependencies ++= Seq(
142+
"dev.zio" %%% "zio-json" % Versions.zioJson
143+
),
144+
// zio-json-macros only available for Scala 2.x (provides @jsonField for Scala 2)
145+
// For Scala 3, @jsonField is in zio-json core
146+
libraryDependencies ++= (
147+
if (scalaVersion.value.startsWith("3")) Seq.empty
148+
else Seq("dev.zio" %%% "zio-json-macros" % Versions.zioJson)
149+
),
150+
mimaSettings,
151+
compilerPlugins,
152+
// zio-json 0.7.45 pulls in scala-library 2.13.17, allow upgrade for Scala 2.13
153+
allowUnsafeScalaLibUpgrade := true
154+
)
155+
.dependsOn(oauth2 % "compile->compile;test->test")
156+
133157
lazy val docs = project
134158
.in(file("mdoc")) // important: it must not be docs/
135159
.settings(
@@ -259,5 +283,7 @@ val root = project
259283
`oauth2-circe`.jvm,
260284
`oauth2-circe`.js,
261285
`oauth2-jsoniter`.jvm,
262-
`oauth2-jsoniter`.js
286+
`oauth2-jsoniter`.js,
287+
`oauth2-zio-json`.jvm,
288+
`oauth2-zio-json`.js
263289
)
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
package org.polyvariant.sttp.oauth2.json.ziojson
2+
3+
import org.polyvariant.sttp.oauth2.ClientCredentialsToken.AccessTokenResponse
4+
import org.polyvariant.sttp.oauth2.ExtendedOAuth2TokenResponse
5+
import org.polyvariant.sttp.oauth2.Introspection.Audience
6+
import org.polyvariant.sttp.oauth2.Introspection.SeqAudience
7+
import org.polyvariant.sttp.oauth2.Introspection.StringAudience
8+
import org.polyvariant.sttp.oauth2.Introspection.TokenIntrospectionResponse
9+
import org.polyvariant.sttp.oauth2.OAuth2TokenResponse
10+
import org.polyvariant.sttp.oauth2.RefreshTokenResponse
11+
import org.polyvariant.sttp.oauth2.Secret
12+
import org.polyvariant.sttp.oauth2.TokenUserDetails
13+
import org.polyvariant.sttp.oauth2.UserInfo
14+
import org.polyvariant.sttp.oauth2.common.Error.OAuth2Error
15+
import org.polyvariant.sttp.oauth2.common.Scope
16+
import org.polyvariant.sttp.oauth2.json.{JsonDecoder => OAuth2JsonDecoder}
17+
import zio.json._
18+
import zio.json.jsonMemberNames
19+
import zio.json.SnakeCase
20+
21+
import java.time.Instant
22+
import scala.concurrent.duration.DurationLong
23+
import scala.concurrent.duration.FiniteDuration
24+
25+
trait ZioJsonDecoders {
26+
import ZioJsonDecoders._
27+
28+
implicit def jsonDecoder[A](
29+
implicit decoder: JsonDecoder[A]
30+
): OAuth2JsonDecoder[A] =
31+
(data: String) => decoder.decodeJson(data).left.map(msg => OAuth2JsonDecoder.Error(msg))
32+
33+
implicit val secretStringDecoder: JsonDecoder[Secret[String]] =
34+
JsonDecoder.string.map(Secret(_))
35+
36+
implicit val secondsDecoder: JsonDecoder[FiniteDuration] =
37+
JsonDecoder.long.map(_.seconds)
38+
39+
implicit val instantDecoder: JsonDecoder[Instant] =
40+
JsonDecoder.long.map(Instant.ofEpochSecond)
41+
42+
implicit val scopeDecoder: JsonDecoder[Scope] =
43+
JsonDecoder.string.mapOrFail { value =>
44+
Scope.from(value).left.map(identity)
45+
}
46+
47+
implicit val optionScopeDecoder: JsonDecoder[Option[Scope]] =
48+
JsonDecoder.option[String].mapOrFail {
49+
case None | Some("") => Right(None)
50+
case Some(value) => Scope.from(value).map(Some(_)).left.map(identity)
51+
}
52+
53+
implicit val tokenUserDetailsDecoder: JsonDecoder[TokenUserDetails] =
54+
DeriveJsonDecoder.gen[TokenUserDetails]
55+
56+
implicit val userInfoDecoder: JsonDecoder[UserInfo] =
57+
userInfoRawDecoder.map { raw =>
58+
UserInfo(
59+
raw.sub,
60+
raw.name,
61+
raw.givenName,
62+
raw.familyName,
63+
raw.jobTitle,
64+
raw.domain,
65+
raw.preferredUsername,
66+
raw.email,
67+
raw.emailVerified,
68+
raw.locale,
69+
raw.sites.getOrElse(Nil),
70+
raw.banners.getOrElse(Nil),
71+
raw.regions.getOrElse(Nil),
72+
raw.fulfillmentContexts.getOrElse(Nil)
73+
)
74+
}
75+
76+
implicit val accessTokenResponseDecoder: JsonDecoder[AccessTokenResponse] =
77+
accessTokenResponseRawDecoder.mapOrFail { raw =>
78+
if (raw.tokenType.equalsIgnoreCase("Bearer"))
79+
Right(AccessTokenResponse(raw.accessToken, raw.domain, raw.expiresIn, raw.scope))
80+
else
81+
Left(s"Error while decoding '.token_type': value '${raw.tokenType}' is not equal to 'Bearer'")
82+
}
83+
84+
implicit val oAuth2ErrorDecoder: JsonDecoder[OAuth2Error] =
85+
oAuth2ErrorRawDecoder.map { raw =>
86+
OAuth2Error.fromErrorTypeAndDescription(raw.error, raw.errorDescription)
87+
}
88+
89+
implicit val oAuth2TokenResponseDecoder: JsonDecoder[OAuth2TokenResponse] =
90+
oAuth2TokenResponseRawDecoder.map { raw =>
91+
OAuth2TokenResponse(raw.accessToken, raw.scope, raw.tokenType, raw.expiresIn, raw.refreshToken)
92+
}
93+
94+
implicit val extendedOAuth2TokenResponseDecoder: JsonDecoder[ExtendedOAuth2TokenResponse] =
95+
extendedOAuth2TokenResponseRawDecoder.map { raw =>
96+
ExtendedOAuth2TokenResponse(
97+
raw.accessToken,
98+
raw.refreshToken,
99+
raw.expiresIn,
100+
raw.userName,
101+
raw.domain,
102+
raw.userDetails,
103+
raw.roles,
104+
raw.scope,
105+
raw.securityLevel,
106+
raw.userId,
107+
raw.tokenType
108+
)
109+
}
110+
111+
implicit val audienceDecoder: JsonDecoder[Audience] =
112+
JsonDecoder
113+
.string
114+
.map(StringAudience(_))
115+
.orElse(
116+
JsonDecoder.list[String].map(seq => SeqAudience(seq))
117+
)
118+
119+
implicit val tokenIntrospectionResponseDecoder: JsonDecoder[TokenIntrospectionResponse] =
120+
tokenIntrospectionResponseRawDecoder.map { raw =>
121+
TokenIntrospectionResponse(
122+
raw.active,
123+
raw.clientId,
124+
raw.domain,
125+
raw.exp,
126+
raw.iat,
127+
raw.nbf,
128+
raw.authorities,
129+
raw.scope,
130+
raw.tokenType,
131+
raw.sub,
132+
raw.iss,
133+
raw.jti,
134+
raw.aud
135+
)
136+
}
137+
138+
implicit val refreshTokenResponseDecoder: JsonDecoder[RefreshTokenResponse] =
139+
refreshTokenResponseRawDecoder.map { raw =>
140+
RefreshTokenResponse(
141+
raw.accessToken,
142+
raw.refreshToken,
143+
raw.expiresIn,
144+
raw.userName,
145+
raw.domain,
146+
raw.userDetails,
147+
raw.roles,
148+
raw.scope,
149+
raw.securityLevel,
150+
raw.userId,
151+
raw.tokenType
152+
)
153+
}
154+
155+
}
156+
157+
object ZioJsonDecoders {
158+
159+
// Base decoders needed for derivation
160+
private[ziojson] implicit val secretStringDecoder: JsonDecoder[Secret[String]] =
161+
JsonDecoder.string.map(Secret(_))
162+
163+
private[ziojson] implicit val secondsDecoder: JsonDecoder[FiniteDuration] =
164+
JsonDecoder.long.map(_.seconds)
165+
166+
private[ziojson] implicit val instantDecoder: JsonDecoder[Instant] =
167+
JsonDecoder.long.map(Instant.ofEpochSecond)
168+
169+
private[ziojson] implicit val scopeDecoder: JsonDecoder[Scope] =
170+
JsonDecoder.string.mapOrFail { value =>
171+
Scope.from(value).left.map(identity)
172+
}
173+
174+
private[ziojson] implicit val optionScopeDecoder: JsonDecoder[Option[Scope]] =
175+
JsonDecoder.option[String].mapOrFail {
176+
case None | Some("") => Right(None)
177+
case Some(value) => Scope.from(value).map(Some(_)).left.map(identity)
178+
}
179+
180+
private[ziojson] implicit val tokenUserDetailsDecoder: JsonDecoder[TokenUserDetails] =
181+
DeriveJsonDecoder.gen[TokenUserDetails]
182+
183+
private[ziojson] implicit val audienceDecoder: JsonDecoder[Audience] =
184+
JsonDecoder
185+
.string
186+
.map(StringAudience(_))
187+
.orElse(
188+
JsonDecoder.list[String].map(seq => SeqAudience(seq))
189+
)
190+
191+
@jsonMemberNames(SnakeCase)
192+
private[ziojson] final case class UserInfoRaw(
193+
sub: Option[String],
194+
name: Option[String],
195+
givenName: Option[String],
196+
familyName: Option[String],
197+
jobTitle: Option[String],
198+
domain: Option[String],
199+
preferredUsername: Option[String],
200+
email: Option[String],
201+
emailVerified: Option[Boolean],
202+
locale: Option[String],
203+
sites: Option[List[String]],
204+
banners: Option[List[String]],
205+
regions: Option[List[String]],
206+
fulfillmentContexts: Option[List[String]]
207+
)
208+
209+
private[ziojson] val userInfoRawDecoder: JsonDecoder[UserInfoRaw] =
210+
DeriveJsonDecoder.gen[UserInfoRaw]
211+
212+
@jsonMemberNames(SnakeCase)
213+
private[ziojson] final case class AccessTokenResponseRaw(
214+
accessToken: Secret[String],
215+
domain: Option[String],
216+
expiresIn: FiniteDuration,
217+
scope: Option[Scope],
218+
tokenType: String
219+
)
220+
221+
private[ziojson] val accessTokenResponseRawDecoder: JsonDecoder[AccessTokenResponseRaw] =
222+
DeriveJsonDecoder.gen[AccessTokenResponseRaw]
223+
224+
@jsonMemberNames(SnakeCase)
225+
private[ziojson] final case class OAuth2ErrorRaw(
226+
error: String,
227+
errorDescription: Option[String]
228+
)
229+
230+
private[ziojson] val oAuth2ErrorRawDecoder: JsonDecoder[OAuth2ErrorRaw] =
231+
DeriveJsonDecoder.gen[OAuth2ErrorRaw]
232+
233+
@jsonMemberNames(SnakeCase)
234+
private[ziojson] final case class OAuth2TokenResponseRaw(
235+
accessToken: Secret[String],
236+
scope: String,
237+
tokenType: String,
238+
expiresIn: Option[FiniteDuration],
239+
refreshToken: Option[String]
240+
)
241+
242+
private[ziojson] val oAuth2TokenResponseRawDecoder: JsonDecoder[OAuth2TokenResponseRaw] =
243+
DeriveJsonDecoder.gen[OAuth2TokenResponseRaw]
244+
245+
@jsonMemberNames(SnakeCase)
246+
private[ziojson] final case class ExtendedOAuth2TokenResponseRaw(
247+
accessToken: Secret[String],
248+
refreshToken: String,
249+
expiresIn: FiniteDuration,
250+
userName: String,
251+
domain: String,
252+
userDetails: TokenUserDetails,
253+
roles: Set[String],
254+
scope: String,
255+
securityLevel: Long,
256+
userId: String,
257+
tokenType: String
258+
)
259+
260+
private[ziojson] val extendedOAuth2TokenResponseRawDecoder: JsonDecoder[ExtendedOAuth2TokenResponseRaw] =
261+
DeriveJsonDecoder.gen[ExtendedOAuth2TokenResponseRaw]
262+
263+
@jsonMemberNames(SnakeCase)
264+
private[ziojson] final case class TokenIntrospectionResponseRaw(
265+
active: Boolean,
266+
clientId: Option[String],
267+
domain: Option[String],
268+
exp: Option[Instant],
269+
iat: Option[Instant],
270+
nbf: Option[Instant],
271+
authorities: Option[List[String]],
272+
scope: Option[Scope],
273+
tokenType: Option[String],
274+
sub: Option[String],
275+
iss: Option[String],
276+
jti: Option[String],
277+
aud: Option[Audience]
278+
)
279+
280+
private[ziojson] val tokenIntrospectionResponseRawDecoder: JsonDecoder[TokenIntrospectionResponseRaw] =
281+
DeriveJsonDecoder.gen[TokenIntrospectionResponseRaw]
282+
283+
@jsonMemberNames(SnakeCase)
284+
private[ziojson] final case class RefreshTokenResponseRaw(
285+
accessToken: Secret[String],
286+
refreshToken: Option[String],
287+
expiresIn: FiniteDuration,
288+
userName: String,
289+
domain: String,
290+
userDetails: TokenUserDetails,
291+
roles: Set[String],
292+
scope: String,
293+
securityLevel: Long,
294+
userId: String,
295+
tokenType: String
296+
)
297+
298+
private[ziojson] val refreshTokenResponseRawDecoder: JsonDecoder[RefreshTokenResponseRaw] =
299+
DeriveJsonDecoder.gen[RefreshTokenResponseRaw]
300+
301+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package org.polyvariant.sttp.oauth2.json.ziojson
2+
3+
object instances extends ZioJsonDecoders
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.polyvariant.sttp.oauth2.json.ziojson
2+
3+
import org.polyvariant.sttp.oauth2.json.JsonSpec
4+
import org.polyvariant.sttp.oauth2.json.ziojson.instances._
5+
import org.polyvariant.sttp.oauth2.json.JsonDecoder
6+
import org.polyvariant.sttp.oauth2.Introspection.TokenIntrospectionResponse
7+
import org.polyvariant.sttp.oauth2.common._
8+
import org.polyvariant.sttp.oauth2.ClientCredentialsToken
9+
import org.polyvariant.sttp.oauth2.ExtendedOAuth2TokenResponse
10+
import org.polyvariant.sttp.oauth2.RefreshTokenResponse
11+
import org.polyvariant.sttp.oauth2.UserInfo
12+
13+
class ZioJsonSpec extends JsonSpec {
14+
15+
protected implicit def tokenIntrospectionResponseJsonDecoder: JsonDecoder[TokenIntrospectionResponse] = jsonDecoder
16+
17+
protected implicit def oAuth2ErrorJsonDecoder: JsonDecoder[Error.OAuth2Error] = jsonDecoder
18+
19+
protected implicit def extendedOAuth2TokenResponseJsonDecoder: JsonDecoder[ExtendedOAuth2TokenResponse] = jsonDecoder
20+
21+
protected implicit def refreshTokenResponseJsonDecoder: JsonDecoder[RefreshTokenResponse] = jsonDecoder
22+
23+
protected implicit def userInfoJsonDecoder: JsonDecoder[UserInfo] = jsonDecoder
24+
25+
protected implicit def accessTokenResponseJsonDecoder: JsonDecoder[ClientCredentialsToken.AccessTokenResponse] = jsonDecoder
26+
27+
}

0 commit comments

Comments
 (0)