Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ object CirceFeatureKey {
key: String,
default: Json,
): LDCodecResult[FeatureKey.Aux[Json]] =
FeatureKey.instanceOrFailure(key, default)
FeatureKey.instance(key, default)

/** Define a feature key that is expected to return a JSON value.
*
Expand All @@ -55,6 +55,6 @@ object CirceFeatureKey {
default: A,
): LDCodecResult[FeatureKey.Aux[A]] = {
implicit val ldCodec: LDCodec[A] = JsonLDCodec.ldCodecFromCirceCodec[A]
FeatureKey.instanceOrFailure(key, default)
FeatureKey.instance(key, default)
}
}
31 changes: 16 additions & 15 deletions circe/src/main/scala/org/typelevel/catapult/circe/JsonLDCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ private object JsonLDCodecImplementation {
_encode = (json, history) =>
json.fold[LDCodecResult[LDValue]](
jsonNull = LDValue.ofNull().validNec,
jsonBoolean = _.asLDValue.valid,
jsonNumber = _.asLDValueOrFailure(history),
jsonString = _.asLDValue.valid,
jsonArray = _.asLDValueOrFailure(history),
jsonObject = _.toIterable.asLDValueOrFailure(history),
jsonBoolean = _.asLDValue(history),
jsonNumber = _.asLDValue(history),
jsonString = _.asLDValue(history),
jsonArray = _.asLDValue(history),
jsonObject = _.toIterable.asLDValue(history),
),
_decode = cursor =>
cursor.valueType match {
Expand Down Expand Up @@ -117,15 +117,16 @@ private object JsonLDCodecImplementation {
LDCodecFailure(LDReason.unencodableValue(LDValueType.NUMBER, "JNumber"), history)
}
.toValidatedNec,
_.checkType(LDValueType.NUMBER).andThen { c =>
// Less of a hack on the encoding side, because circe can handle (most) doubles.
// The JVM double doesn't map cleanly to the JSON number, so this can fail as well.
LDCodec[Double]
.decode(c)
.map(Json.fromDouble(_).flatMap(_.asNumber))
.andThen(_.toValidNec {
c.fail(LDReason.undecodableValue(LDValueType.NUMBER, "JNumber"))
})
},
cursor =>
cursor.checkType(LDValueType.NUMBER).value.andThen { value =>
// Less of a hack on the encoding side, because circe can handle (most) doubles.
// The JVM double doesn't map cleanly to the JSON number, so this can fail as well.
LDCodec[Double]
.decode(value)
.map(Json.fromDouble(_).flatMap(_.asNumber))
.andThen(_.toValidNec {
cursor.fail(LDReason.undecodableValue(LDValueType.NUMBER, "JNumber"))
})
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ object client {
implicit F: MonadThrow[F]
): F[Json] =
defaultValue
.asLDValueOrFailure(LDCursorHistory.root)
.asLDValue(LDCursorHistory.root)
.asEncodingFailure
.liftTo[F]
.flatMap(client.jsonValueVariation(featureKey, ctx, _))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ object mtlClient {
) extends AnyVal {
def circeVariation(featureKey: String, defaultValue: Json)(implicit F: MonadThrow[F]): F[Json] =
defaultValue
.asLDValueOrFailure(LDCursorHistory.root)
.asLDValue(LDCursorHistory.root)
.asEncodingFailure
.liftTo[F]
.flatMap(client.jsonValueVariation(featureKey, _))
Expand Down
60 changes: 51 additions & 9 deletions core/src/main/scala/org/typelevel/catapult/FeatureKey.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ package org.typelevel.catapult

import cats.syntax.all.*
import cats.Show
import cats.data.NonEmptyChain
import cats.kernel.Hash
import com.launchdarkly.sdk.LDValue
import org.typelevel.catapult.codec.LDCodec.LDCodecResult
import org.typelevel.catapult.codec.{LDCodec, LDCodecWithInfallibleEncode, LDCursorHistory}
import org.typelevel.catapult.codec.{LDCodec, LDCodecFailure, LDCursorHistory}
import org.typelevel.catapult.instances.*

/** Defines a Launch Darkly key, it's expected type, and a default value
Expand All @@ -38,39 +39,67 @@ object FeatureKey {
type Type = T
}

final case class InvalidDefault[A](
key: String,
default: A,
encodingErrors: NonEmptyChain[LDCodecFailure],
) extends IllegalArgumentException(
s"FeatureKey $key defined with default that cannot be encoded in an LDValue"
Comment thread
morgen-peschke marked this conversation as resolved.
)

/** Define a feature key that is expected to return a value of type `A`
*
* @param key
* the key of the flag
* @param default
* a value to return if the retrieval fails or the value cannot be decoded to an `A`
* @return
* `LDCodecResult[FeatureKey.Aux[A]]` instead of `FeatureKey.Aux[A]` because many unremarkable
* values cannot be encoded in an `LDValue`
*/
def instance[A: LDCodecWithInfallibleEncode](key: String, default: A): FeatureKey.Aux[A] =
new Impl[A](key, default, LDCodecWithInfallibleEncode[A].safeEncode(default))
def instance[A: LDCodec](key: String, default: A): LDCodecResult[FeatureKey.Aux[A]] =
LDCodec[A].encode(default, LDCursorHistory.root).map(new Impl[A](key, default, _))

/** Define a feature key that is expected to return a value of type `A`
* @param key
* the key of the flag
* @param default
* a value to return if the retrieval fails or the value cannot be decoded to an `A`
* @throws InvalidDefault
* when `default` cannot be encoded in an `LDValue`
*/
def instanceOrFailure[A: LDCodec](key: String, default: A): LDCodecResult[FeatureKey.Aux[A]] =
LDCodec[A].encode(default, LDCursorHistory.root).map(new Impl[A](key, default, _))
def instanceUnsafe[A: LDCodec](key: String, default: A): FeatureKey.Aux[A] =
instance(key, default).valueOr(errors => throw InvalidDefault(key, default, errors))

/** Define a feature key that is expected to return a boolean value.
* @param key
* the key of the flag
* @param default
* a value to return if the retrieval fails or the type is not expected
*/
def bool(key: String, default: Boolean): FeatureKey.Aux[Boolean] = instance[Boolean](key, default)
def bool(key: String, default: Boolean): LDCodecResult[FeatureKey.Aux[Boolean]] =
instance[Boolean](key, default)

/** @see [[FeatureKey.bool]]
* @see [[FeatureKey.instanceUnsafe]]
*/
def boolUnsafe(key: String, default: Boolean): FeatureKey.Aux[Boolean] =
instanceUnsafe[Boolean](key, default)

/** Define a feature key that is expected to return a string value.
* @param key
* the key of the flag
* @param default
* a value to return if the retrieval fails or the type is not expected
*/
def string(key: String, default: String): FeatureKey.Aux[String] = instance[String](key, default)
def string(key: String, default: String): LDCodecResult[FeatureKey.Aux[String]] =
instance[String](key, default)

/** @see [[FeatureKey.string]]
* @see [[FeatureKey.instanceUnsafe]]
*/
def stringUnsafe(key: String, default: String): FeatureKey.Aux[String] =
instanceUnsafe[String](key, default)

/** Define a feature key that is expected to return a integer value.
* @param key
Expand All @@ -79,15 +108,28 @@ object FeatureKey {
* a value to return if the retrieval fails or the type is not expected
*/
def int(key: String, default: Int): LDCodecResult[FeatureKey.Aux[Int]] =
instanceOrFailure[Int](key, default)
instance[Int](key, default)

/** @see [[FeatureKey.int]]
* @see [[FeatureKey.instanceUnsafe]]
*/
def intUnsafe(key: String, default: Int): FeatureKey.Aux[Int] =
instanceUnsafe[Int](key, default)

/** Define a feature key that is expected to return a double value.
* @param key
* the key of the flag
* @param default
* a value to return if the retrieval fails or the type is not expected
*/
def double(key: String, default: Double): FeatureKey.Aux[Double] = instance[Double](key, default)
def double(key: String, default: Double): LDCodecResult[FeatureKey.Aux[Double]] =
instance[Double](key, default)

/** @see [[FeatureKey.double]]
* @see [[FeatureKey.instanceUnsafe]]
*/
def doubleUnsafe(key: String, default: Double): FeatureKey.Aux[Double] =
instanceUnsafe[Double](key, default)

/** Define a feature key that is expected to return a JSON value.
*
Expand Down
Loading