Skip to content

Commit 70acaa7

Browse files
Allow encoding to LDValue to fail
Needed to support circe functionality
1 parent c7cfc05 commit 70acaa7

File tree

19 files changed

+1364
-869
lines changed

19 files changed

+1364
-869
lines changed

circe/src/main/scala/org.typelevel/catapult/circe/CirceFeatureKey.scala

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616

1717
package org.typelevel.catapult.circe
1818

19-
import com.launchdarkly.sdk.LDValue
20-
import io.circe.syntax.*
2119
import io.circe.{Decoder, Encoder, Json}
2220
import org.typelevel.catapult.FeatureKey
23-
import org.typelevel.catapult.circe.LDValueCodec.*
21+
import org.typelevel.catapult.circe.JsonLDCodec.circeLDCodecForJSON
22+
import org.typelevel.catapult.codec.LDCodec
23+
import org.typelevel.catapult.codec.LDCodec.LDCodecResult
2424

2525
object CirceFeatureKey {
2626

@@ -37,8 +37,8 @@ object CirceFeatureKey {
3737
def featureKey(
3838
key: String,
3939
default: Json,
40-
): Decoder.Result[FeatureKey.Aux[Json]] =
41-
default.as[LDValue].map(FeatureKey.ldValue(key, _))
40+
): LDCodecResult[FeatureKey.Aux[Json]] =
41+
FeatureKey.instanceOrFailure(key, default)
4242

4343
/** Define a feature key that is expected to return a JSON value.
4444
*
@@ -50,9 +50,11 @@ object CirceFeatureKey {
5050
* @param default
5151
* a value to return if the retrieval fails or the type is not expected
5252
*/
53-
def featureKeyEncoded[A: Encoder](
53+
def featureKeyEncoded[A: Encoder: Decoder](
5454
key: String,
5555
default: A,
56-
): Decoder.Result[FeatureKey.Aux[A]] =
57-
featureKey(key, default.asJson)
56+
): LDCodecResult[FeatureKey.Aux[A]] = {
57+
implicit val ldCodec: LDCodec[A] = JsonLDCodec.ldCodecFromCirceCodec[A]
58+
FeatureKey.instanceOrFailure(key, default)
59+
}
5860
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2022 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.catapult.circe
18+
19+
import cats.Defer
20+
import cats.data.{Chain, NonEmptyChain, Validated}
21+
import cats.syntax.all.*
22+
import com.launchdarkly.sdk.json.{JsonSerialization, SerializationException}
23+
import com.launchdarkly.sdk.{LDValue, LDValueType}
24+
import io.circe.*
25+
import org.typelevel.catapult.codec.*
26+
import org.typelevel.catapult.codec.LDCodec.LDCodecResult
27+
import org.typelevel.catapult.codec.LDCursorHistory.Move
28+
import org.typelevel.catapult.codec.syntax.*
29+
30+
object JsonLDCodec {
31+
implicit val circeLDCodecForJSON: LDCodec[Json] =
32+
JsonLDCodecImplementation.jsonLDCodecRetainingNumberErrors
33+
34+
def ldCodecFromCirceCodec[A: Encoder: Decoder]: LDCodec[A] =
35+
circeLDCodecForJSON.imapVFull[A](
36+
(a, _) => Encoder[A].apply(a).valid,
37+
(json, history) =>
38+
Decoder[A].decodeAccumulating(json.hcursor).leftMap { errors =>
39+
NonEmptyChain
40+
.fromNonEmptyList(errors)
41+
.map(JsonLDCodecImplementation.convertCirceFailureToLDFailure)
42+
.map(lcf => lcf.updateHistory(history.combine(_)))
43+
},
44+
)
45+
}
46+
47+
// This is separate to keep the implicits from colliding during construction
48+
private object JsonLDCodecImplementation {
49+
val jsonLDCodecRetainingNumberErrors: LDCodec[Json] = Defer[LDCodec].fix { implicit recurse =>
50+
LDCodec.instance(
51+
_encode = (json, history) =>
52+
json.fold[LDCodecResult[LDValue]](
53+
jsonNull = LDValue.ofNull().validNec,
54+
jsonBoolean = _.asLDValue.valid,
55+
jsonNumber = _.asLDValueOrFailure(history),
56+
jsonString = _.asLDValue.valid,
57+
jsonArray = _.asLDValueOrFailure(history),
58+
jsonObject = _.toIterable.asLDValueOrFailure(history),
59+
),
60+
_decode = cursor =>
61+
cursor.valueType match {
62+
case LDValueType.NULL => Json.Null.valid
63+
case LDValueType.BOOLEAN => cursor.as[Boolean].map(Json.fromBoolean)
64+
case LDValueType.NUMBER => cursor.as[JsonNumber].map(Json.fromJsonNumber)
65+
case LDValueType.STRING => cursor.as[String].map(Json.fromString)
66+
case LDValueType.ARRAY => cursor.as[Iterable[Json]].map(Json.fromValues)
67+
case LDValueType.OBJECT => cursor.as[Vector[(String, Json)]].map(Json.fromFields(_))
68+
},
69+
)
70+
}
71+
72+
def convertCirceFailureToLDFailure(decodingFailure: DecodingFailure): LDCodecFailure =
73+
LDCodecFailure(
74+
decodingFailure.reason match {
75+
case DecodingFailure.Reason.CustomReason(message) => LDReason.other(message)
76+
case DecodingFailure.Reason.MissingField => LDReason.missingField
77+
case DecodingFailure.Reason.WrongTypeExpectation(expectedJsonFieldType, jsonValue) =>
78+
LDReason.wrongType(
79+
expectedJsonFieldType,
80+
jsonValue.fold(
81+
jsonNull = LDValueType.NULL,
82+
jsonBoolean = _ => LDValueType.BOOLEAN,
83+
jsonNumber = _ => LDValueType.NUMBER,
84+
jsonString = _ => LDValueType.STRING,
85+
jsonArray = _ => LDValueType.ARRAY,
86+
jsonObject = _ => LDValueType.OBJECT,
87+
),
88+
)
89+
},
90+
LDCursorHistory.of(Chain.fromSeq(decodingFailure.history).mapFilter {
91+
case CursorOp.DownField(k) => Move(k).some
92+
case CursorOp.DownN(n) => Move(n).some
93+
case _ => none
94+
}),
95+
)
96+
97+
implicit private val jNumberCodec: LDCodec[JsonNumber] = LDCodec.instance(
98+
(jNumber, history) =>
99+
Validated
100+
.catchOnly[SerializationException] {
101+
// This nasty hack is because LDValue number support is lacking.
102+
//
103+
// LDValueNumber encodes everything as Double and that introduces encoding
104+
// issues like negative/positive/unsigned zero and rounding if we try to do
105+
// the conversion ourselves with the raw.
106+
//
107+
// Here, we're basically hoping they've got their house in order and can
108+
// parse a valid JSON number.
109+
LDValue.normalize(
110+
JsonSerialization.deserialize(
111+
Json.fromJsonNumber(jNumber).noSpaces,
112+
classOf[LDValue],
113+
)
114+
)
115+
}
116+
.leftMap { (_: SerializationException) =>
117+
LDCodecFailure(LDReason.unencodableValue(LDValueType.NUMBER, "JNumber"), history)
118+
}
119+
.toValidatedNec,
120+
_.checkType(LDValueType.NUMBER).andThen { c =>
121+
// Less of a hack on the encoding side, because circe can handle (most) doubles.
122+
// The JVM double doesn't map cleanly to the JSON number, so this can fail as well.
123+
LDCodec[Double]
124+
.decode(c)
125+
.map(Json.fromDouble(_).flatMap(_.asNumber))
126+
.andThen(_.toValidNec {
127+
c.fail(LDReason.undecodableValue(LDValueType.NUMBER, "JNumber"))
128+
})
129+
},
130+
)
131+
}

circe/src/main/scala/org.typelevel/catapult/circe/LDValueCodec.scala

Lines changed: 0 additions & 114 deletions
This file was deleted.

circe/src/main/scala/org/typelevel/catapult/circe/syntax/client.scala

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ package org.typelevel.catapult.circe.syntax
1818

1919
import cats.MonadThrow
2020
import cats.syntax.all.*
21-
import com.launchdarkly.sdk.LDValue
2221
import io.circe.syntax.*
23-
import io.circe.{Decoder, Json}
24-
import org.typelevel.catapult.{ContextEncoder, FeatureKey, LaunchDarklyClient}
25-
import org.typelevel.catapult.circe.LDValueCodec.*
22+
import io.circe.{Decoder, Encoder, Json}
23+
import org.typelevel.catapult.circe.JsonLDCodec.*
24+
import org.typelevel.catapult.codec.LDCursorHistory
25+
import org.typelevel.catapult.codec.syntax.*
26+
import org.typelevel.catapult.{ContextEncoder, LaunchDarklyClient}
2627

2728
object client {
2829
implicit final class CatapultLaunchDarklyClientCirceOps[F[_]](
@@ -32,22 +33,18 @@ object client {
3233
implicit F: MonadThrow[F]
3334
): F[Json] =
3435
defaultValue
35-
.as[LDValue]
36+
.asLDValueOrFailure(LDCursorHistory.root)
37+
.asEncodingFailure
3638
.liftTo[F]
3739
.flatMap(client.jsonValueVariation(featureKey, ctx, _))
38-
.map(_.asJson)
40+
.flatMap(_.decode[Json].asDecodingFailure.liftTo[F])
3941

40-
41-
def circeVariationAs[A: Decoder, Ctx: ContextEncoder](
42+
def circeVariationAs[A: Decoder: Encoder, Ctx: ContextEncoder](
4243
featureKey: String,
4344
ctx: Ctx,
44-
defaultValue: Json,
45+
defaultValue: A,
4546
)(implicit F: MonadThrow[F]): F[A] =
46-
defaultValue
47-
.as[LDValue]
48-
.liftTo[F]
49-
.flatMap(client.jsonValueVariation(featureKey, ctx, _))
50-
.flatMap(_.asJson.as[A].liftTo[F])
51-
47+
circeVariation[Ctx](featureKey, ctx, defaultValue.asJson)
48+
.flatMap(_.as[A].liftTo[F])
5249
}
5350
}

circe/src/main/scala/org/typelevel/catapult/circe/syntax/mtlClient.scala

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ package org.typelevel.catapult.circe.syntax
1818

1919
import cats.MonadThrow
2020
import cats.syntax.all.*
21-
import com.launchdarkly.sdk.LDValue
2221
import io.circe.syntax.*
23-
import io.circe.{Decoder, Json}
24-
import org.typelevel.catapult.FeatureKey
25-
import org.typelevel.catapult.circe.LDValueCodec.*
22+
import io.circe.{Decoder, Encoder, Json}
23+
import org.typelevel.catapult.circe.JsonLDCodec.*
24+
import org.typelevel.catapult.codec.LDCursorHistory
25+
import org.typelevel.catapult.codec.syntax.*
2626
import org.typelevel.catapult.mtl.LaunchDarklyMTLClient
2727

2828
object mtlClient {
@@ -31,20 +31,16 @@ object mtlClient {
3131
) extends AnyVal {
3232
def circeVariation(featureKey: String, defaultValue: Json)(implicit F: MonadThrow[F]): F[Json] =
3333
defaultValue
34-
.as[LDValue]
34+
.asLDValueOrFailure(LDCursorHistory.root)
35+
.asEncodingFailure
3536
.liftTo[F]
3637
.flatMap(client.jsonValueVariation(featureKey, _))
37-
.map(_.asJson)
38+
.flatMap(_.decode[Json].asDecodingFailure.liftTo[F])
3839

39-
40-
def circeVariationAs[A: Decoder](featureKey: String, defaultValue: Json)(implicit
40+
def circeVariationAs[A: Decoder: Encoder](featureKey: String, defaultValue: A)(implicit
4141
F: MonadThrow[F]
4242
): F[A] =
43-
defaultValue
44-
.as[LDValue]
45-
.liftTo[F]
46-
.flatMap(client.jsonValueVariation(featureKey, _))
47-
.flatMap(_.asJson.as[A].liftTo[F])
48-
43+
circeVariation(featureKey, defaultValue.asJson)
44+
.flatMap(_.as[A].liftTo[F])
4945
}
5046
}

0 commit comments

Comments
 (0)