Skip to content

Commit

Permalink
OpenFeature: Add in-memory provider (#32)
Browse files Browse the repository at this point in the history
- **OpenFeature: Add in-memory provider**
- **Move dependency versions out of build.sbt**
  • Loading branch information
alexcardell authored Sep 15, 2024
1 parent e190805 commit c9f30bb
Show file tree
Hide file tree
Showing 5 changed files with 456 additions and 20 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p openfeature/provider-flipt/.jvm/target flipt/sdk-server/native/target flipt/sdk-server/js/target openfeature/sdk-circe/.native/target openfeature/sdk-circe/.jvm/target openfeature/provider-flipt/.native/target openfeature/sdk-circe/.js/target openfeature/sdk/.native/target openfeature/provider-flipt/.js/target openfeature/sdk/.jvm/target openfeature/sdk/.js/target flipt/sdk-server/jvm/target project/target
run: mkdir -p openfeature/provider-flipt/.jvm/target openfeature/provider-memory/.js/target flipt/sdk-server/native/target flipt/sdk-server/js/target openfeature/sdk-circe/.native/target openfeature/provider-memory/.native/target openfeature/sdk-circe/.jvm/target openfeature/provider-flipt/.native/target openfeature/sdk-circe/.js/target openfeature/sdk/.native/target openfeature/provider-memory/.jvm/target openfeature/provider-flipt/.js/target openfeature/sdk/.jvm/target openfeature/sdk/.js/target flipt/sdk-server/jvm/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar openfeature/provider-flipt/.jvm/target flipt/sdk-server/native/target flipt/sdk-server/js/target openfeature/sdk-circe/.native/target openfeature/sdk-circe/.jvm/target openfeature/provider-flipt/.native/target openfeature/sdk-circe/.js/target openfeature/sdk/.native/target openfeature/provider-flipt/.js/target openfeature/sdk/.jvm/target openfeature/sdk/.js/target flipt/sdk-server/jvm/target project/target
run: tar cf targets.tar openfeature/provider-flipt/.jvm/target openfeature/provider-memory/.js/target flipt/sdk-server/native/target flipt/sdk-server/js/target openfeature/sdk-circe/.native/target openfeature/provider-memory/.native/target openfeature/sdk-circe/.jvm/target openfeature/provider-flipt/.native/target openfeature/sdk-circe/.js/target openfeature/sdk/.native/target openfeature/provider-memory/.jvm/target openfeature/provider-flipt/.js/target openfeature/sdk/.jvm/target openfeature/sdk/.js/target flipt/sdk-server/jvm/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
54 changes: 36 additions & 18 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import build.V

Global / onChangedBuildSource := ReloadOnSourceChanges

// https://typelevel.org/sbt-typelevel/faq.html#what-is-a-base-version-anyway
ThisBuild / tlBaseVersion := "0.3" // your current series x.y

Expand Down Expand Up @@ -32,6 +36,7 @@ lazy val projects = Seq(
`flipt-sdk-server-it`,
`openfeature-sdk`,
`openfeature-sdk-circe`,
`openfeature-provider-memory`,
`openfeature-provider-flipt`,
`openfeature-provider-flipt-it`,
examples,
Expand All @@ -40,10 +45,10 @@ lazy val projects = Seq(

lazy val commonDependencies = Seq(
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % "2.10.0",
"org.typelevel" %%% "cats-effect" % "3.5.3",
"org.scalameta" %%% "munit" % "1.0.0-RC1" % Test,
"org.typelevel" %%% "munit-cats-effect" % "2.0.0-M5" % Test
"org.typelevel" %%% "cats-core" % V.cats,
"org.typelevel" %%% "cats-effect" % V.catsEffect,
"org.scalameta" %%% "munit" % V.munit % Test,
"org.typelevel" %%% "munit-cats-effect" % V.munitCatsEffect % Test
)
)

Expand All @@ -60,11 +65,11 @@ lazy val `flipt-sdk-server` = crossProject(
.settings(
name := "flipt-sdk-server",
libraryDependencies ++= Seq(
"org.http4s" %%% "http4s-client" % "0.23.26",
"org.http4s" %%% "http4s-circe" % "0.23.26",
"io.circe" %%% "circe-core" % "0.14.7",
"io.circe" %%% "circe-parser" % "0.14.7",
"io.circe" %%% "circe-generic" % "0.14.7"
"org.http4s" %%% "http4s-client" % V.http4s,
"org.http4s" %%% "http4s-circe" % V.http4s,
"io.circe" %%% "circe-core" % V.circe,
"io.circe" %%% "circe-parser" % V.circe,
"io.circe" %%% "circe-generic" % V.circe
)
)

Expand All @@ -75,8 +80,8 @@ lazy val `flipt-sdk-server-it` = crossProject(JVMPlatform)
.settings(commonDependencies)
.settings(
libraryDependencies ++= Seq(
"org.http4s" %%% "http4s-ember-client" % "0.23.26",
"com.dimafeng" %% "testcontainers-scala-munit" % "0.41.3" % Test
"org.http4s" %%% "http4s-ember-client" % V.http4s,
"com.dimafeng" %% "testcontainers-scala-munit" % V.testcontainers % Test
)
)
.dependsOn(`flipt-sdk-server`)
Expand Down Expand Up @@ -104,12 +109,25 @@ lazy val `openfeature-sdk-circe` = crossProject(
.settings(
name := "openfeature-sdk-circe",
libraryDependencies ++= Seq(
"io.circe" %%% "circe-core" % "0.14.7",
"io.circe" %%% "circe-parser" % "0.14.7"
"io.circe" %%% "circe-core" % V.circe,
"io.circe" %%% "circe-parser" % V.circe
)
)
.dependsOn(`openfeature-sdk`)

lazy val `openfeature-provider-memory` = crossProject(
JVMPlatform,
JSPlatform,
NativePlatform
)
.crossType(CrossType.Pure)
.in(file("openfeature/provider-memory"))
.settings(commonDependencies)
.settings(
name := "openfeature-provider-memory"
)
.dependsOn(`openfeature-sdk`)

lazy val `openfeature-provider-flipt` = crossProject(
JVMPlatform,
JSPlatform,
Expand All @@ -134,9 +152,9 @@ lazy val `openfeature-provider-flipt-it` = crossProject(JVMPlatform)
.settings(
name := "openfeature-provider-flipt-it",
libraryDependencies ++= Seq(
"org.http4s" %%% "http4s-ember-client" % "0.23.26" % Test,
"com.dimafeng" %% "testcontainers-scala-munit" % "0.41.3" % Test,
"io.circe" %%% "circe-generic" % "0.14.7" % Test
"org.http4s" %%% "http4s-ember-client" % V.http4s % Test,
"com.dimafeng" %% "testcontainers-scala-munit" % V.testcontainers % Test,
"io.circe" %%% "circe-generic" % V.circe % Test
)
)
.dependsOn(
Expand All @@ -152,7 +170,7 @@ lazy val examples = crossProject(JVMPlatform)

lazy val docs = project
.in(file("site"))
.enablePlugins(TypelevelSitePlugin)
.enablePlugins(NoPublishPlugin, TypelevelSitePlugin)
.settings(
tlSiteHelium := {
import laika.helium.config.IconLink
Expand All @@ -163,7 +181,7 @@ lazy val docs = project
)
},
libraryDependencies ++= Seq(
"org.http4s" %%% "http4s-ember-client" % "0.23.26"
"org.http4s" %%% "http4s-ember-client" % V.http4s
)
)
.dependsOn(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* 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.memory

import cats.MonadThrow
import cats.effect.kernel.Ref
import cats.effect.kernel.Sync
import cats.syntax.all._

import io.cardell.openfeature.ErrorCode
import io.cardell.openfeature.EvaluationContext
import io.cardell.openfeature.EvaluationReason
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
}

final class MemoryProvider[F[_]: MonadThrow](
ref: Ref[F, Map[String, MemoryFlagState]]
) extends EvaluationProvider[F] {

import MemoryFlagState._

override def metadata: ProviderMetadata = ProviderMetadata("memory")

private def missing[A](
flagKey: String,
defaultValue: A
): ResolutionDetails[A] = ResolutionDetails(
value = defaultValue,
errorCode = Some(ErrorCode.FlagNotFound),
errorMessage = Some(s"${flagKey} not found"),
reason = Some(EvaluationReason.Error),
variant = None,
metadata = None
)

private def typeMismatch[A](
flagKey: String,
defaultValue: A
): ResolutionDetails[A] = ResolutionDetails(
value = defaultValue,
errorCode = Some(ErrorCode.TypeMismatch),
errorMessage = Some(s"${flagKey} was unexpected type"),
reason = Some(EvaluationReason.Error),
variant = None,
metadata = None
)

private def resolution[A](value: A): ResolutionDetails[A] = ResolutionDetails(
value = value,
errorCode = None,
errorMessage = None,
reason = Some(EvaluationReason.Static),
variant = None,
metadata = None
)

override def resolveBooleanValue(
flagKey: String,
defaultValue: Boolean,
context: EvaluationContext
): 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)
}
}

override def resolveStringValue(
flagKey: String,
defaultValue: String,
context: EvaluationContext
): 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)
}
}

override def resolveIntValue(
flagKey: String,
defaultValue: Int,
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)
}
}

override def resolveDoubleValue(
flagKey: String,
defaultValue: Double,
context: EvaluationContext
): 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)
}
}

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) }
// }

}

object MemoryProvider {

def apply[F[_]: Sync](
state: Map[String, MemoryFlagState]
): F[MemoryProvider[F]] =
for {
ref <- Ref.of[F, Map[String, MemoryFlagState]](state)
} yield new MemoryProvider[F](ref)

}
Loading

0 comments on commit c9f30bb

Please sign in to comment.