diff --git a/openfeature/provider-memory/src/main/scala/io/cardell/openfeature/provider/memory/MemoryProvider.scala b/openfeature/provider-memory/src/main/scala/io/cardell/openfeature/provider/memory/MemoryProvider.scala index 40aa299..9a5cb4b 100644 --- a/openfeature/provider-memory/src/main/scala/io/cardell/openfeature/provider/memory/MemoryProvider.scala +++ b/openfeature/provider-memory/src/main/scala/io/cardell/openfeature/provider/memory/MemoryProvider.scala @@ -24,26 +24,18 @@ import cats.syntax.all._ import io.cardell.openfeature.ErrorCode import io.cardell.openfeature.EvaluationContext import io.cardell.openfeature.EvaluationReason +import io.cardell.openfeature.FlagValue import io.cardell.openfeature.StructureDecoder import io.cardell.openfeature.provider.EvaluationProvider import io.cardell.openfeature.provider.ProviderMetadata import io.cardell.openfeature.provider.ResolutionDetails -sealed trait MemoryFlagState - -object MemoryFlagState { - case class BooleanFlagState(value: Boolean) extends MemoryFlagState - case class StringFlagState(value: String) extends MemoryFlagState - case class IntFlagState(value: Int) extends MemoryFlagState - case class DoubleFlagState(value: Double) extends MemoryFlagState -} - +/** Probably don't use in production, see `resolveStructureValue` for why + */ final class MemoryProvider[F[_]: MonadThrow]( - ref: Ref[F, Map[String, MemoryFlagState]] + ref: Ref[F, Map[String, FlagValue]] ) extends EvaluationProvider[F] { - import MemoryFlagState._ - override def metadata: ProviderMetadata = ProviderMetadata("memory") private def missing[A]( @@ -86,8 +78,8 @@ final class MemoryProvider[F[_]: MonadThrow]( ): F[ResolutionDetails[Boolean]] = ref.get.map { state => state.get(flagKey) match { case None => missing[Boolean](flagKey, defaultValue) - case Some(BooleanFlagState(value)) => resolution[Boolean](value) - case Some(_) => typeMismatch(flagKey, defaultValue) + case Some(FlagValue.BooleanValue(value)) => resolution[Boolean](value) + case Some(_) => typeMismatch(flagKey, defaultValue) } } @@ -98,8 +90,8 @@ final class MemoryProvider[F[_]: MonadThrow]( ): F[ResolutionDetails[String]] = ref.get.map { state => state.get(flagKey) match { case None => missing[String](flagKey, defaultValue) - case Some(StringFlagState(value)) => resolution[String](value) - case Some(_) => typeMismatch(flagKey, defaultValue) + case Some(FlagValue.StringValue(value)) => resolution[String](value) + case Some(_) => typeMismatch(flagKey, defaultValue) } } @@ -109,9 +101,9 @@ final class MemoryProvider[F[_]: MonadThrow]( context: EvaluationContext ): F[ResolutionDetails[Int]] = ref.get.map { state => state.get(flagKey) match { - case None => missing[Int](flagKey, defaultValue) - case Some(IntFlagState(value)) => resolution[Int](value) - case Some(_) => typeMismatch(flagKey, defaultValue) + case None => missing[Int](flagKey, defaultValue) + case Some(FlagValue.IntValue(value)) => resolution[Int](value) + case Some(_) => typeMismatch(flagKey, defaultValue) } } @@ -122,44 +114,43 @@ final class MemoryProvider[F[_]: MonadThrow]( ): F[ResolutionDetails[Double]] = ref.get.map { state => state.get(flagKey) match { case None => missing[Double](flagKey, defaultValue) - case Some(DoubleFlagState(value)) => resolution[Double](value) - case Some(_) => typeMismatch(flagKey, defaultValue) + case Some(FlagValue.DoubleValue(value)) => resolution[Double](value) + case Some(_) => typeMismatch(flagKey, defaultValue) } } + /** NOTE: StructureValue can contain anything, and therefore may throw + * classcast exceptions + * + * Can't get around type erasure to do the check + */ override def resolveStructureValue[A: StructureDecoder]( flagKey: String, defaultValue: A, context: EvaluationContext - ): F[ResolutionDetails[A]] = MonadThrow[F].raiseError( - new NotImplementedError( - "Structure values not implemented in in-memory provider" - ) - ) - // { - // val resolved = ref.get.map { state => - // val x = state.get(flagKey) match { - // case None => missing[A](flagKey, defaultValue) - // case Some(StructureFlagState(value)) => resolution[A](value.asInstanceOf[A]) - // case Some(_) => typeMismatch(flagKey, defaultValue) - // } - // @nowarn - // val value: A = x.value - // x - // } - // - // resolved.handleError { case _ => missing[A](flagKey, defaultValue) } - // } + ): F[ResolutionDetails[A]] = { + val resolved = ref.get.map { state => + state.get(flagKey) match { + case None => missing[A](flagKey, defaultValue) + case Some(FlagValue.StructureValue(value)) => + val v = value.asInstanceOf[A] + resolution[A](v) + case Some(_) => typeMismatch(flagKey, defaultValue) + } + } + + resolved.handleError { case _ => missing[A](flagKey, defaultValue) } + } } object MemoryProvider { def apply[F[_]: Sync]( - state: Map[String, MemoryFlagState] + state: Map[String, FlagValue] ): F[MemoryProvider[F]] = for { - ref <- Ref.of[F, Map[String, MemoryFlagState]](state) + ref <- Ref.of[F, Map[String, FlagValue]](state) } yield new MemoryProvider[F](ref) } diff --git a/openfeature/provider-memory/src/test/scala/io/cardell/openfeature/provider/memory/MemoryProviderTest.scala b/openfeature/provider-memory/src/test/scala/io/cardell/openfeature/provider/memory/MemoryProviderTest.scala index fe5bdeb..172b702 100644 --- a/openfeature/provider-memory/src/test/scala/io/cardell/openfeature/provider/memory/MemoryProviderTest.scala +++ b/openfeature/provider-memory/src/test/scala/io/cardell/openfeature/provider/memory/MemoryProviderTest.scala @@ -21,6 +21,7 @@ import munit.CatsEffectSuite import io.cardell.openfeature.ErrorCode import io.cardell.openfeature.EvaluationContext +import io.cardell.openfeature.FlagValue import io.cardell.openfeature.StructureDecoder import io.cardell.openfeature.StructureDecoderError @@ -41,7 +42,7 @@ class MemoryProviderTest extends CatsEffectSuite { test("can return boolean values") { val expected = true - val flag = MemoryFlagState.BooleanFlagState(expected) + val flag = FlagValue.BooleanValue(expected) val key = "boolean-flag-key" val state = Map(key -> flag) @@ -63,7 +64,7 @@ class MemoryProviderTest extends CatsEffectSuite { test("can return string values") { val expected = "string" - val flag = MemoryFlagState.StringFlagState(expected) + val flag = FlagValue.StringValue(expected) val key = "string-flag-key" val state = Map(key -> flag) @@ -85,7 +86,7 @@ class MemoryProviderTest extends CatsEffectSuite { test("can return int values when type is as expected") { val expected = 33 - val flag = MemoryFlagState.IntFlagState(expected) + val flag = FlagValue.IntValue(expected) val key = "int-flag-key" val state = Map(key -> flag) @@ -107,7 +108,7 @@ class MemoryProviderTest extends CatsEffectSuite { test("can return double values when type is as expected") { val expected = 40.0 - val flag = MemoryFlagState.DoubleFlagState(expected) + val flag = FlagValue.DoubleValue(expected) val key = "double-flag-key" val state = Map(key -> flag) @@ -126,11 +127,33 @@ class MemoryProviderTest extends CatsEffectSuite { } } + test("can return structure values") { + val expected = TestStructure("a", 0) + + val flag = FlagValue.StructureValue(expected) + val key = "structure-flag-key" + val state = Map(key -> flag) + + val default = TestStructure("a", 0) + + MemoryProvider[IO](state).flatMap { provider => + val resolution = provider.resolveStructureValue[TestStructure]( + key, + default, + EvaluationContext.empty + ) + + for { + result <- resolution.map(_.value) + } yield assertEquals(result, expected) + } + } + test("receives type mismatch error when boolean not received") { val expectedValue = false val expectedErrorCode = Some(ErrorCode.TypeMismatch) - val flag = MemoryFlagState.DoubleFlagState(0.0) + val flag = FlagValue.DoubleValue(0.0) val key = "boolean-flag-key" val state = Map(key -> flag) @@ -158,7 +181,7 @@ class MemoryProviderTest extends CatsEffectSuite { val expectedValue = "default" val expectedErrorCode = Some(ErrorCode.TypeMismatch) - val flag = MemoryFlagState.DoubleFlagState(0.0) + val flag = FlagValue.DoubleValue(0.0) val key = "string-flag-key" val state = Map(key -> flag) @@ -186,7 +209,7 @@ class MemoryProviderTest extends CatsEffectSuite { val expectedValue = 33 val expectedErrorCode = Some(ErrorCode.TypeMismatch) - val flag = MemoryFlagState.DoubleFlagState(0.0) + val flag = FlagValue.DoubleValue(0.0) val key = "int-flag-key" val state = Map(key -> flag) @@ -214,7 +237,7 @@ class MemoryProviderTest extends CatsEffectSuite { val expectedValue = 40.0 val expectedErrorCode = Some(ErrorCode.TypeMismatch) - val flag = MemoryFlagState.IntFlagState(0) + val flag = FlagValue.IntValue(0) val key = "double-flag-key" val state = Map(key -> flag) @@ -238,4 +261,29 @@ class MemoryProviderTest extends CatsEffectSuite { } } + // test( + // "receives type mismatch error when expected structure type not received" + // ) { + // val expected = TestStructure("a", 0) + // + // val flag = FlagValue.StructureValue(OtherTestStructure(40.0)) + // val key = "structure-flag-key" + // val state = Map(key -> flag) + // + // val default = expected + // + // MemoryProvider[IO](state).flatMap { provider => + // val resolution = provider.resolveStructureValue( + // key, + // default, + // EvaluationContext.empty + // ) + // + // for { + // result <- resolution + // _ <- IO.println("alexxxxxx") + // } yield assertEquals(result.value, expected) + // } + // } + } diff --git a/openfeature/sdk/src/main/scala/io/cardell/openfeature/FlagValue.scala b/openfeature/sdk/src/main/scala/io/cardell/openfeature/FlagValue.scala new file mode 100644 index 0000000..f96a8ec --- /dev/null +++ b/openfeature/sdk/src/main/scala/io/cardell/openfeature/FlagValue.scala @@ -0,0 +1,50 @@ +/* + * 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 + +import io.cardell.openfeature.FlagValue.BooleanValue +import io.cardell.openfeature.FlagValue.DoubleValue +import io.cardell.openfeature.FlagValue.IntValue +import io.cardell.openfeature.FlagValue.StringValue +import io.cardell.openfeature.FlagValue.StructureValue + +sealed trait FlagValue { + + def valueType: FlagValueType = + this match { + case _: BooleanValue => FlagValueType.BooleanValueType + case _: StringValue => FlagValueType.StringValueType + case _: IntValue => FlagValueType.IntValueType + case _: DoubleValue => FlagValueType.DoubleValueType + case _: StructureValue[_] => FlagValueType.StructureValueType + } + +} + +object FlagValue { + case class BooleanValue(value: Boolean) extends FlagValue + case class StringValue(value: String) extends FlagValue + case class IntValue(value: Int) extends FlagValue + case class DoubleValue(value: Double) extends FlagValue + case class StructureValue[A](value: A) extends FlagValue + + def apply(b: Boolean): FlagValue = BooleanValue(b) + def apply(s: String): FlagValue = StringValue(s) + def apply(i: Int): FlagValue = IntValue(i) + def apply(d: Double): FlagValue = DoubleValue(d) + def apply[A](s: A): FlagValue = StructureValue(s) +} diff --git a/openfeature/sdk/src/main/scala/io/cardell/openfeature/FlagValueType.scala b/openfeature/sdk/src/main/scala/io/cardell/openfeature/FlagValueType.scala new file mode 100644 index 0000000..fd4db34 --- /dev/null +++ b/openfeature/sdk/src/main/scala/io/cardell/openfeature/FlagValueType.scala @@ -0,0 +1,27 @@ +/* + * 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 + +sealed trait FlagValueType + +object FlagValueType { + case object BooleanValueType extends FlagValueType + case object StringValueType extends FlagValueType + case object IntValueType extends FlagValueType + case object DoubleValueType extends FlagValueType + case object StructureValueType extends FlagValueType +} diff --git a/openfeature/sdk/src/main/scala/io/cardell/openfeature/Hooks.scala b/openfeature/sdk/src/main/scala/io/cardell/openfeature/Hooks.scala index 664af3a..4005ebf 100644 --- a/openfeature/sdk/src/main/scala/io/cardell/openfeature/Hooks.scala +++ b/openfeature/sdk/src/main/scala/io/cardell/openfeature/Hooks.scala @@ -20,59 +20,11 @@ import cats.Applicative import cats.Monad import cats.syntax.all._ -import io.cardell.openfeature.FlagValue.BooleanValue -import io.cardell.openfeature.FlagValue.DoubleValue -import io.cardell.openfeature.FlagValue.IntValue -import io.cardell.openfeature.FlagValue.StringValue -import io.cardell.openfeature.FlagValue.StructureValue - -sealed trait FlagValueType - -object FlagValueType { - case object BooleanValueType extends FlagValueType - case object StringValueType extends FlagValueType - case object IntValueType extends FlagValueType - case object DoubleValueType extends FlagValueType - case object StructureValueType extends FlagValueType -} - -sealed trait FlagValue - -object FlagValue { - case class BooleanValue(value: Boolean) extends FlagValue - case class StringValue(value: String) extends FlagValue - case class IntValue(value: Int) extends FlagValue - case class DoubleValue(value: Double) extends FlagValue - case class StructureValue[A](value: A) extends FlagValue - - def apply(b: Boolean): FlagValue = BooleanValue(b) - def apply(s: String): FlagValue = StringValue(s) - def apply(i: Int): FlagValue = IntValue(i) - def apply(d: Double): FlagValue = DoubleValue(d) - - def apply[A](s: A): FlagValue = StructureValue(s) -} - case class HookContext( flagKey: String, evaluationContext: EvaluationContext, defaultValue: FlagValue -) { - - def valueType: FlagValueType = - defaultValue match { - case _: BooleanValue => FlagValueType.BooleanValueType - case _: StringValue => FlagValueType.StringValueType - case _: IntValue => FlagValueType.IntValueType - case _: DoubleValue => FlagValueType.DoubleValueType - case _: StructureValue[_] => FlagValueType.StructureValueType - } - -} - -object HookHints { - def empty: HookHints = Map.empty -} +) object Hooks { diff --git a/openfeature/sdk/src/main/scala/io/cardell/openfeature/package.scala b/openfeature/sdk/src/main/scala/io/cardell/openfeature/package.scala index e9673d3..17740c9 100644 --- a/openfeature/sdk/src/main/scala/io/cardell/openfeature/package.scala +++ b/openfeature/sdk/src/main/scala/io/cardell/openfeature/package.scala @@ -18,4 +18,9 @@ package io.cardell package object openfeature { type HookHints = Map[String, ContextValue] + + object HookHints { + def empty: HookHints = Map.empty + } + }