From 3006381365271c9565696d4112c860ad353b0c03 Mon Sep 17 00:00:00 2001 From: alexcardell <29524087+alexcardell@users.noreply.github.com> Date: Fri, 20 Sep 2024 18:44:11 +0100 Subject: [PATCH] Add flagd java provider integration tests --- build.sbt | 1 + docker-compose.yaml | 8 + docker/flagd/flagd-features.json | 55 ++++++ .../provider/flipt/FliptProvider.scala | 9 +- .../java/provider/JavaProviderItTest.scala | 187 ++++++++++++++++++ .../java/provider/TestVariant.scala | 78 ++++++++ .../provider/java/JavaConverters.scala | 164 +++++++++++++++ .../provider/java/JavaProvider.scala | 185 +++++++++++++++++ .../openfeature/FeatureClientImpl.scala | 2 +- .../provider/ResolutionDetails.scala | 34 +++- 10 files changed, 707 insertions(+), 16 deletions(-) create mode 100644 docker/flagd/flagd-features.json create mode 100644 openfeature/provider-java-it/src/test/scala/io/cardell/openfeature/java/provider/JavaProviderItTest.scala create mode 100644 openfeature/provider-java-it/src/test/scala/io/cardell/openfeature/java/provider/TestVariant.scala create mode 100644 openfeature/provider-java/src/main/scala/io/cardell/openfeature/provider/java/JavaConverters.scala create mode 100644 openfeature/provider-java/src/main/scala/io/cardell/openfeature/provider/java/JavaProvider.scala diff --git a/build.sbt b/build.sbt index aef7733..f52676d 100644 --- a/build.sbt +++ b/build.sbt @@ -153,6 +153,7 @@ lazy val `openfeature-provider-java-it` = crossProject(JVMPlatform) ) ) .dependsOn( + `openfeature-sdk-circe`, `openfeature-provider-java` ) diff --git a/docker-compose.yaml b/docker-compose.yaml index 72d7775..18d9534 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,3 +8,11 @@ services: FLIPT_STORAGE_LOCAL_PATH: "/config" volumes: - "./docker/flipt/features.yaml:/config/features.yaml" + + flagd: + image: ghcr.io/open-feature/flagd:v0.11.2 + command: 'start --uri file:./etc/flagd/flagd-features.json' + ports: + - "8013:8013" + volumes: + - "./docker/flagd/flagd-features.json:/etc/flagd/flagd-features.json" diff --git a/docker/flagd/flagd-features.json b/docker/flagd/flagd-features.json new file mode 100644 index 0000000..e513bd0 --- /dev/null +++ b/docker/flagd/flagd-features.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://flagd.dev/schema/v0/flags.json", + "flags": { + "boolean-flag-1": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "on" + }, + "string-variant-flag-1": { + "state": "ENABLED", + "defaultVariant": "key-1", + "variants": { + "key-1": "string-value-1", + "key-2": "string-value-1" + }, + "targeting": {} + }, + "int-variant-flag-1": { + "state": "ENABLED", + "defaultVariant": "key-1", + "variants": { + "key-1": 13, + "key-2": 88 + }, + "targeting": {} + }, + "double-variant-flag-1": { + "state": "ENABLED", + "defaultVariant": "key-2", + "variants": { + "key-1": 88.0, + "key-2": 17.1 + }, + "targeting": {} + }, + "structure-variant-flag-1": { + "state": "ENABLED", + "defaultVariant": "structure-1", + "variants": { + "structure-1": { + "field": "string", + "intField": 33 + }, + "structure-2": { + "field": "other", + "intField": 10001 + } + }, + "targeting": {} + } + } +} diff --git a/openfeature/provider-flipt/src/main/scala/io/cardell/openfeature/provider/flipt/FliptProvider.scala b/openfeature/provider-flipt/src/main/scala/io/cardell/openfeature/provider/flipt/FliptProvider.scala index 86e1df6..9bfec1d 100644 --- a/openfeature/provider-flipt/src/main/scala/io/cardell/openfeature/provider/flipt/FliptProvider.scala +++ b/openfeature/provider-flipt/src/main/scala/io/cardell/openfeature/provider/flipt/FliptProvider.scala @@ -126,18 +126,15 @@ final class FliptProvider[F[_]: MonadThrow]( jsonAttachment match { case Left(parseError) => - ResolutionDetails.error(defaultValue, parseError) + ResolutionDetails.fromThrowable(defaultValue, parseError) case Right(None) => - ResolutionDetails.error( - defaultValue, - new Throwable("did not receive json object") - ) + ResolutionDetails.error(defaultValue, "did not receive json object") case Right(Some(jsonObject)) => val structure = JsonStructureConverters.jsonToStructure(jsonObject) val decodedStructure = StructureDecoder[A].decodeStructure(structure) decodedStructure match { case Left(error) => - ResolutionDetails.error[A](defaultValue, error.cause) + ResolutionDetails.fromThrowable[A](defaultValue, error.cause) case Right(value) => ResolutionDetails[A](value) } } diff --git a/openfeature/provider-java-it/src/test/scala/io/cardell/openfeature/java/provider/JavaProviderItTest.scala b/openfeature/provider-java-it/src/test/scala/io/cardell/openfeature/java/provider/JavaProviderItTest.scala new file mode 100644 index 0000000..febf20e --- /dev/null +++ b/openfeature/provider-java-it/src/test/scala/io/cardell/openfeature/java/provider/JavaProviderItTest.scala @@ -0,0 +1,187 @@ +/* + * Copyright 2023 Alex Cardell + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cardell.openfeature.provider.java + +import cats.effect.IO +import cats.effect.kernel.Resource +import cats.syntax.all._ +import com.dimafeng.testcontainers.ContainerDef +import com.dimafeng.testcontainers.DockerComposeContainer +import com.dimafeng.testcontainers.ExposedService +import com.dimafeng.testcontainers.munit.TestContainerForAll +import dev.openfeature.contrib.providers.flagd.FlagdOptions +import dev.openfeature.contrib.providers.flagd.FlagdProvider +import java.io.File +import munit.CatsEffectSuite +import org.testcontainers.containers.wait.strategy.Wait + +import io.cardell.openfeature.EvaluationContext +import io.cardell.openfeature.FeatureClient +import io.cardell.openfeature.OpenFeature + +class JavaProviderItTest extends CatsEffectSuite with TestContainerForAll { + + override val containerDef: ContainerDef = DockerComposeContainer.Def( + new File("docker-compose.yaml"), + exposedServices = Seq( + ExposedService( + "flagd", + 8013, + Wait.forLogMessage("^.*watching filepath.*", 1) + ) + ), + tailChildContainers = true + ) + + def flagd(containers: Containers): Resource[IO, FeatureClient[IO]] = { + val container = + containers + .asInstanceOf[DockerComposeContainer] + .getContainerByServiceName("flagd") + .get + + val flagdProvder = + new FlagdProvider( + FlagdOptions + .builder() + .host(container.getHost()) + .port(container.getMappedPort(8013).toInt) + .build() + ) + + JavaProvider + .resource[IO](flagdProvder) + .map(OpenFeature[IO]) + .evalMap(_.client) + } + + val evaluationContext = EvaluationContext.empty + + test("can fetch boolean flag") { + val expected = true + + withContainers { containers => + flagd(containers) + .use { provider => + for { + res <- provider.getBooleanDetails( + "boolean-flag-1", + false, + EvaluationContext.empty + ) + } yield assertEquals(res.value, expected) + } + } + } + + test("uses default when boolean flag missing") { + val expected = false + + withContainers { containers => + flagd(containers).use { provider => + for { + res <- provider.getBooleanDetails( + "no-flag", + false, + EvaluationContext.empty + ) + } yield assertEquals(res.value, expected) + } + } + } + + test("can resolve string value for provider variant flag") { + val expected = "string-value-1" + + withContainers { containers => + flagd(containers).use { provider => + for { + res <- provider.getStringDetails( + "string-variant-flag-1", + "default-string", + evaluationContext + ) + } yield assertEquals(res.value, expected) + } + } + } + + test("uses default when string flag missing") { + val expected = "some-string" + + withContainers { containers => + flagd(containers).use { provider => + for { + res <- provider.getStringDetails( + "no-flag", + expected, + EvaluationContext.empty + ) + } yield assertEquals(res.value, expected) + } + } + } + + test("can resolve int value for provider variant flag") { + val expected = 13 + + withContainers { containers => + flagd(containers).use { provider => + for { + res <- provider.getIntDetails( + "int-variant-flag-1", + 99, + evaluationContext + ) + } yield assertEquals(res.value, expected) + } + } + } + + test("can resolve double value for provider variant flag") { + val expected = 17.1 + + withContainers { containers => + flagd(containers).use { provider => + for { + res <- provider.getDoubleDetails( + "double-variant-flag-1", + 99.9, + evaluationContext + ) + } yield assertEquals(res.value, expected) + } + } + } + + test("can deserialise variant match") { + val expected = TestVariant("string", 33) + + withContainers { containers => + flagd(containers).use { provider => + for { + res <- provider.getStructureDetails[TestVariant]( + "structure-variant-flag-1", + TestVariant("a", 0), + evaluationContext + ) + } yield assertEquals(res.value, expected) + } + } + } + +} diff --git a/openfeature/provider-java-it/src/test/scala/io/cardell/openfeature/java/provider/TestVariant.scala b/openfeature/provider-java-it/src/test/scala/io/cardell/openfeature/java/provider/TestVariant.scala new file mode 100644 index 0000000..894b1eb --- /dev/null +++ b/openfeature/provider-java-it/src/test/scala/io/cardell/openfeature/java/provider/TestVariant.scala @@ -0,0 +1,78 @@ +/* + * Copyright 2023 Alex Cardell + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cardell.openfeature.provider.java + +import io.cardell.openfeature.FlagValue +import io.cardell.openfeature.FlagValue.IntValue +import io.cardell.openfeature.FlagValue.StringValue +import io.cardell.openfeature.Structure +import io.cardell.openfeature.StructureDecoder +import io.cardell.openfeature.StructureDecoderError +import io.cardell.openfeature.StructureEncoder + +case class TestVariant(field: String, intField: Int) + +object TestVariant { + + implicit val sd: StructureDecoder[TestVariant] = + new StructureDecoder[TestVariant] { + + def decodeStructure( + s: Structure + ): Either[StructureDecoderError, TestVariant] = { + val maybeStruct = + for { + field <- s.values.get("field") + intField <- s.values.get("intField") + variant <- + (field, intField) match { + case (StringValue(s), IntValue(i)) => Some(TestVariant(s, i)) + case _ => None + } + } yield variant + + maybeStruct match { + case None => + Left( + StructureDecoderError( + new Throwable( + "some fields missing converting TestVariant to Structure" + ) + ) + ) + case Some(value) => Right(value) + } + + } + + } + + implicit val se: StructureEncoder[TestVariant] = + new StructureEncoder[TestVariant] { + + def encodeStructure( + in: TestVariant + ): Structure = Structure( + Map( + "field" -> StringValue(in.field), + "intField" -> IntValue(in.intField) + ) + ) + + } + +} diff --git a/openfeature/provider-java/src/main/scala/io/cardell/openfeature/provider/java/JavaConverters.scala b/openfeature/provider-java/src/main/scala/io/cardell/openfeature/provider/java/JavaConverters.scala new file mode 100644 index 0000000..5a71564 --- /dev/null +++ b/openfeature/provider-java/src/main/scala/io/cardell/openfeature/provider/java/JavaConverters.scala @@ -0,0 +1,164 @@ +/* + * Copyright 2023 Alex Cardell + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cardell.openfeature.provider.java + +import dev.openfeature.sdk.ErrorCode.FLAG_NOT_FOUND +import dev.openfeature.sdk.ErrorCode.GENERAL +import dev.openfeature.sdk.ErrorCode.INVALID_CONTEXT +import dev.openfeature.sdk.ErrorCode.PARSE_ERROR +import dev.openfeature.sdk.ErrorCode.PROVIDER_NOT_READY +import dev.openfeature.sdk.ErrorCode.TARGETING_KEY_MISSING +import dev.openfeature.sdk.ErrorCode.TYPE_MISMATCH +import dev.openfeature.sdk.Reason.CACHED +import dev.openfeature.sdk.Reason.DEFAULT +import dev.openfeature.sdk.Reason.DISABLED +import dev.openfeature.sdk.Reason.ERROR +import dev.openfeature.sdk.Reason.SPLIT +import dev.openfeature.sdk.Reason.STATIC +import dev.openfeature.sdk.Reason.TARGETING_MATCH +import dev.openfeature.sdk.Reason.UNKNOWN +import dev.openfeature.sdk.{ErrorCode => JErrorCode} +import dev.openfeature.sdk.{ImmutableContext => JContext} +import dev.openfeature.sdk.{ImmutableStructure => JStructure} +import dev.openfeature.sdk.{ProviderEvaluation => JEvaluation} +import dev.openfeature.sdk.{Reason => JReason} +import dev.openfeature.sdk.{Value => JValue} +import scala.jdk.CollectionConverters._ +import scala.util.Failure +import scala.util.Success +import scala.util.Try + +import io.cardell.openfeature.ContextValue +import io.cardell.openfeature.ContextValue.BooleanValue +import io.cardell.openfeature.ContextValue.DoubleValue +import io.cardell.openfeature.ContextValue.IntValue +import io.cardell.openfeature.ContextValue.StringValue +import io.cardell.openfeature.ErrorCode +import io.cardell.openfeature.EvaluationContext +import io.cardell.openfeature.EvaluationReason +import io.cardell.openfeature.FlagValue +import io.cardell.openfeature.Structure +import io.cardell.openfeature.provider.ResolutionDetails + +object ToJavaConverters { + + def contextValue(value: ContextValue): JValue = + value match { + case BooleanValue(b) => new JValue(b) + case DoubleValue(d) => new JValue(d) + case IntValue(i) => new JValue(i) + case StringValue(s) => new JValue(s) + } + + def evaluationContext(ec: EvaluationContext): JContext = { + val values = ec.values.map { case (k, v) => (k, contextValue(v)) }.asJava + new JContext(values) + } + + def structure(structure: Structure): JStructure = { + val values = + structure.values.map { case (k, v) => (k, flagValue(v)) }.asJava + + new JStructure(values) + } + + def flagValue(value: FlagValue): JValue = + value match { + case FlagValue.BooleanValue(b) => new JValue(b) + case FlagValue.IntValue(i) => new JValue(i) + case FlagValue.DoubleValue(d) => new JValue(d) + case FlagValue.StringValue(s) => new JValue(s) + case FlagValue.StructureValue(s) => new JValue(structure(s)) + } + +} + +object FromJavaConverters { + + def errorCode(ec: JErrorCode): ErrorCode = + ec match { + case GENERAL => ErrorCode.General + case PARSE_ERROR => ErrorCode.ParseError + case TYPE_MISMATCH => ErrorCode.TypeMismatch + case TARGETING_KEY_MISSING => ErrorCode.TargetingKeyMissing + case PROVIDER_NOT_READY => ErrorCode.ProviderNotReady + case INVALID_CONTEXT => ErrorCode.InvalidContext + case FLAG_NOT_FOUND => ErrorCode.FlagNotFound + } + + def reason(reason: JReason): EvaluationReason = + reason match { + case DEFAULT => EvaluationReason.Default + case TARGETING_MATCH => EvaluationReason.TargetingMatch + case DISABLED => EvaluationReason.Disabled + case UNKNOWN => EvaluationReason.Unknown + case ERROR => EvaluationReason.Error + case SPLIT => EvaluationReason.Split + case CACHED => EvaluationReason.Cached + case STATIC => EvaluationReason.Static + } + + def evaluation[A, B](ev: JEvaluation[A])( + f: A => B + ): ResolutionDetails[B] = { + val value = ev.getValue() + val converted = f(value) + + ResolutionDetails[B]( + value = converted, + reason = Option(ev.getReason()) + .map(JReason.valueOf) + .map(FromJavaConverters.reason), + errorCode = Option(ev.getErrorCode()) + .map(FromJavaConverters.errorCode), + errorMessage = Option(ev.getErrorMessage()), + variant = Option(ev.getVariant()), + metadata = None + ) + } + + def evaluation[A](ev: JEvaluation[A]): ResolutionDetails[A] = + evaluation[A, A](ev)(identity[A] _) + + def structure(structure: JStructure): Structure = { + val values = Map.from( + structure + .asMap() + .asScala + .map { case (k, v) => (k, value(v)) } + ) + + Structure(values) + } + + def value(value: JValue): FlagValue = + value match { + case v if v.isBoolean() => FlagValue.BooleanValue(v.asBoolean()) + case v if v.isNumber() => + Try(v.asInteger()) match { + case Success(i) => FlagValue.IntValue(i) + case Failure(_) => FlagValue.DoubleValue(v.asDouble()) + } + case v if v.isString() => FlagValue.StringValue(v.asString()) + case v if v.isStructure() => + FlagValue.StructureValue( + structure(new JStructure(v.asStructure().asMap())) + ) + case _ => ??? // TODO handle + } + +} diff --git a/openfeature/provider-java/src/main/scala/io/cardell/openfeature/provider/java/JavaProvider.scala b/openfeature/provider-java/src/main/scala/io/cardell/openfeature/provider/java/JavaProvider.scala new file mode 100644 index 0000000..775418b --- /dev/null +++ b/openfeature/provider-java/src/main/scala/io/cardell/openfeature/provider/java/JavaProvider.scala @@ -0,0 +1,185 @@ +/* + * Copyright 2023 Alex Cardell + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cardell.openfeature.provider.java + +import cats.effect.kernel.Resource +import cats.effect.kernel.Sync +import cats.syntax.all._ +import dev.openfeature.sdk.{FeatureProvider => JProvider} +import dev.openfeature.sdk.{ImmutableContext => JContext} +import dev.openfeature.sdk.{Reason => JReason} +import dev.openfeature.sdk.{Value => JValue} +import java.lang.{Boolean => JBoolean} +import java.lang.{Double => JDouble} + +import io.cardell.openfeature.ErrorCode +import io.cardell.openfeature.EvaluationContext +import io.cardell.openfeature.FlagValue +import io.cardell.openfeature.StructureCodec +import io.cardell.openfeature.StructureDecoder +import io.cardell.openfeature.StructureEncoder +import io.cardell.openfeature.provider.EvaluationProvider +import io.cardell.openfeature.provider.ProviderMetadata +import io.cardell.openfeature.provider.ResolutionDetails + +final class JavaProvider[F[_]: Sync] private (provider: JProvider) + extends EvaluationProvider[F] { + + override def metadata: ProviderMetadata = { + val name = + for { + m <- Option(provider.getMetadata()) + n <- Option(m.getName()) + } yield n + + ProviderMetadata(name = name.getOrElse("unknown-java-provider")) + } + + override def resolveBooleanValue( + flagKey: String, + defaultValue: Boolean, + context: EvaluationContext + ): F[ResolutionDetails[Boolean]] = { + val jContext = ToJavaConverters.evaluationContext(context) + val providerResolution = Sync[F].blocking( + provider.getBooleanEvaluation(flagKey, defaultValue, jContext) + ) + providerResolution.map( + FromJavaConverters.evaluation[JBoolean, Boolean](_)(_.booleanValue()) + ) + } + + override def resolveStringValue( + flagKey: String, + defaultValue: String, + context: EvaluationContext + ): F[ResolutionDetails[String]] = { + val jContext = ToJavaConverters.evaluationContext(context) + + val providerResolution = Sync[F].blocking( + provider.getStringEvaluation(flagKey, defaultValue, jContext) + ) + + providerResolution.map(FromJavaConverters.evaluation[String](_)) + } + + override def resolveIntValue( + flagKey: String, + defaultValue: Int, + context: EvaluationContext + ): F[ResolutionDetails[Int]] = { + val jContext = ToJavaConverters.evaluationContext(context) + + val providerResolution = Sync[F].blocking( + provider.getIntegerEvaluation(flagKey, defaultValue, jContext) + ) + + providerResolution.map( + FromJavaConverters.evaluation[Integer, Int](_)(_.toInt) + ) + + } + + override def resolveDoubleValue( + flagKey: String, + defaultValue: Double, + context: EvaluationContext + ): F[ResolutionDetails[Double]] = { + val jContext = ToJavaConverters.evaluationContext(context) + + val providerResolution = Sync[F].blocking( + provider.getDoubleEvaluation(flagKey, defaultValue, jContext) + ) + + providerResolution.map( + FromJavaConverters.evaluation[JDouble, Double](_)(_.toDouble) + ) + } + + override def resolveStructureValue[A: StructureCodec]( + flagKey: String, + defaultValue: A, + context: EvaluationContext + ): F[ResolutionDetails[A]] = { + val jContext = ToJavaConverters.evaluationContext(context) + val defaultStructure = StructureEncoder[A].encodeStructure(defaultValue) + val jStructure = ToJavaConverters.structure(defaultStructure) + + val providerResolution = Sync[F].blocking( + provider.getObjectEvaluation(flagKey, new JValue(jStructure), jContext) + ) + + providerResolution.map { case jResolution => + val flagValue = FromJavaConverters.value(jResolution.getValue()) + + val maybeStructure = + flagValue match { + case FlagValue.StructureValue(structure) => Right(structure) + // StructureDecoder[A].decodeStructure(structure) + case otherType => Left(otherType.valueType) + + } + + maybeStructure match { + case Left(wrongType) => + ResolutionDetails.error[A]( + defaultValue, + s"Wrong type, received ${wrongType}", + ErrorCode.TypeMismatch + ) + case Right(struct) => + val maybeValue = StructureDecoder[A].decodeStructure(struct) + + maybeValue match { + case Left(error) => + ResolutionDetails + .fromThrowable[A]( + defaultValue, + error.cause, + ErrorCode.TypeMismatch + ) + case Right(value) => + ResolutionDetails[A]( + value = value, + reason = Option(jResolution.getReason()) + .map(JReason.valueOf) + .map(FromJavaConverters.reason), + errorCode = Option(jResolution.getErrorCode()) + .map(FromJavaConverters.errorCode), + errorMessage = Option(jResolution.getErrorMessage()), + variant = Option(jResolution.getVariant()), + metadata = None + ) + } + } + } + } + +} + +object JavaProvider { + + def resource[F[_]: Sync](provider: JProvider): Resource[F, JavaProvider[F]] = + Resource + .make[F, JProvider] { + Sync[F].blocking(provider.initialize(new JContext())).as(provider) + } { provider => + Sync[F].blocking(provider.shutdown()) + } + .map(new JavaProvider[F](_)) + +} diff --git a/openfeature/sdk/src/main/scala/io/cardell/openfeature/FeatureClientImpl.scala b/openfeature/sdk/src/main/scala/io/cardell/openfeature/FeatureClientImpl.scala index 0d3a314..e96115d 100644 --- a/openfeature/sdk/src/main/scala/io/cardell/openfeature/FeatureClientImpl.scala +++ b/openfeature/sdk/src/main/scala/io/cardell/openfeature/FeatureClientImpl.scala @@ -400,7 +400,7 @@ protected[openfeature] final class FeatureClientImpl[F[_]]( def errorEvaluation(e: Throwable) = EvaluationDetails( flagKey, - ResolutionDetails.error(default, e) + ResolutionDetails.fromThrowable(default, e) ) run.attempt diff --git a/openfeature/sdk/src/main/scala/io/cardell/openfeature/provider/ResolutionDetails.scala b/openfeature/sdk/src/main/scala/io/cardell/openfeature/provider/ResolutionDetails.scala index c44bd77..cefbe1b 100644 --- a/openfeature/sdk/src/main/scala/io/cardell/openfeature/provider/ResolutionDetails.scala +++ b/openfeature/sdk/src/main/scala/io/cardell/openfeature/provider/ResolutionDetails.scala @@ -53,14 +53,30 @@ object ResolutionDetails { metadata = None ) - def error[A](defaultValue: A, error: Throwable): ResolutionDetails[A] = - ResolutionDetails[A]( - value = defaultValue, - errorCode = Some(ErrorCode.General), - errorMessage = Some(error.getMessage()), - reason = Some(EvaluationReason.Error), - variant = None, - metadata = None - ) + def fromThrowable[A]( + defaultValue: A, + error: Throwable, + errorCode: ErrorCode = ErrorCode.General + ): ResolutionDetails[A] = ResolutionDetails[A]( + value = defaultValue, + errorCode = Some(errorCode), + errorMessage = Some(error.getMessage()), + reason = Some(EvaluationReason.Error), + variant = None, + metadata = None + ) + + def error[A]( + defaultValue: A, + errorMessage: String, + errorCode: ErrorCode = ErrorCode.General + ): ResolutionDetails[A] = ResolutionDetails[A]( + value = defaultValue, + errorCode = Some(errorCode), + errorMessage = Some(errorMessage), + reason = Some(EvaluationReason.Error), + variant = None, + metadata = None + ) }