openfeature-scala

Features

Installing

libraryDependencies += "io.cardell" %%% "flipt-sdk-server" % "0.4.0"
// or
libraryDependencies ++= Seq(
    "io.cardell" %%% "openfeature-sdk" % "0.4.0",
    // for circe json variant types
    "io.cardell" %%% "openfeature-sdk-circe" % "0.4.0",
    // to use flipt as a backend
    "io.cardell" %%% "openfeature-provider-flipt" % "0.4.0"
)

OpenFeature Compatibility

Features Status
Providers
Targeting
Logging 🚧
Domains 🚧
Eventing 🚧
Shutdown 🚧
Transaction Context Propagation 🚧

OpenFeature Usage

The OpenFeature SDK adds features like handling default values in case of errors. Eventually the SDK will cover the full range of the openfeature specification, like hooks, events, static vs dynamic context.

OpenFeature Java Compatibility

The openfeature-provider-java module wraps existing OpenFeature Java SDKs.

Installation

Using Flagd as an example:

libraryDependencies ++= Seq(
    "io.cardell" %%% "openfeature-sdk" % "0.4.0",
    "io.cardell" %%% "openfeature-provider-java" % "0.4.0",
    "dev.openfeature.contrib.providers" % "flagd" % "0.8.9",
)
import cats.effect.IO
import dev.openfeature.contrib.providers.flagd.FlagdOptions
import dev.openfeature.contrib.providers.flagd.FlagdProvider

import io.cardell.openfeature.OpenFeature
import io.cardell.openfeature.provider.java.JavaProvider

val provider =
    new FlagdProvider(
        FlagdOptions
            .builder()
            .host("host")
            .port(8013)
            .build()
    )

JavaProvider
    .resource[IO](provider)
    .map(OpenFeature[IO])
    .evalMap(_.client)

FeatureClient Evaluation

import cats.effect.IO

import io.cardell.openfeature.FeatureClient
import io.cardell.openfeature.StructureCodec

case class SomeVariant(field: String, field2: Int)

def program(features: FeatureClient[IO])(
    implicit codec: StructureCodec[SomeVariant]
) = {
    for {
        flagEnabled <- features.getBooleanValue("boolean-flag", false)
        _ <- IO.println(s"${flagEnabled}")
        variant <- features.getStructureValue[SomeVariant](
            "structure-flag",
            SomeVariant("a", 1)
        )
        _ <- IO.println(s"${variant}")
    } yield ()
}

Hooks

Hooks are work-in-progress. All four OpenFeature hook types are supported but only on the FeatureClient and Provider interfaces.

Hook types:

import cats.effect.IO
import io.cardell.openfeature.FeatureClient
import io.cardell.openfeature.BeforeHook
import io.cardell.openfeature.provider.Provider

val hook = BeforeHook[IO] { case (context, hints @ _) => 
    IO.println(s"I'm about to evaluate ${context.flagKey}").as(None)
}
// hook: BeforeHook[IO] = io.cardell.openfeature.BeforeHook$$anon$1@78feb6ed

def providerWithHook(provider: Provider[IO]) = 
    provider.withHook(hook)

// and similarly for `client`
def clientWithHook(client: FeatureClient[IO]) = 
    client.withHook(hook)

otel4s

otel4s trace integration is provided, offering a set of trace hooks

import cats.effect.IO
import org.typelevel.otel4s.trace.Tracer
import io.cardell.openfeature.provider.EvaluationProvider
import io.cardell.openfeature.otel4s.TracedProvider

def traceExample(
    provider: EvaluationProvider[IO]
)(implicit T: Tracer[IO]) =
    new TracedProvider[IO](provider)

// or

import io.cardell.openfeature.otel4s.syntax._

def tracedProviderSyntax(
    provider: EvaluationProvider[IO]
)(implicit T: Tracer[IO]) =
    provider.withTracing

Variants

Providers offer resolving a particular variant, using a Structure type. Typically this is JSON defined on the server side.

To provide arbitrary case classes for variant decoding, a StructureCodec[A] is required.

This could be done explicitly, but you can also derive them from JSON codecs. Currently only Circe is supported.

Circe Integration

Provider implicit Decoder[A] and Encoder.AsObject[A]. Import io.cardell.openfeature.circe._

import cats.effect.IO
import io.circe.Decoder
import io.circe.Encoder

import io.cardell.openfeature.FeatureClient
import io.cardell.openfeature.circe._

case class SomeVariant(field: String, field2: Int)

def circeProgram(features: FeatureClient[IO])(
    implicit d: Decoder[SomeVariant],
    e: Encoder.AsObject[SomeVariant]
) = {
    for {
        flagEnabled <- features.getBooleanValue("boolean-flag", false)
        _ <- IO.println(s"${flagEnabled}")
        variant <- features.getStructureValue[SomeVariant](
            "structure-flag",
            SomeVariant("a", 1)
        )
        _ <- IO.println(s"${variant}")
    } yield ()
}

Alternative, Codec.AsObject[A] would work.

Implementing A New EvaluationProvider

EvaluationProvider does not need to handle any errors that aren't deemed recoverable, or need to implement any hook logic. Running hooks, and handling default evaluations on error is handled in the library

Implement the call, response decoding, and handle any recoverable errors that make sense.

Flipt Usage

The Flipt client is bare-bones, using it is not recommended, unless as OpenFeature SDK Provider.

import cats.effect.IO
import org.http4s.ember.client.EmberClientBuilder
import org.http4s.Uri
import io.cardell.flipt.FliptApi
import io.cardell.flipt.EvaluationRequest
import io.cardell.flipt.auth.AuthenticationStrategy

val url: Uri = Uri.unsafeFromString("https://flipt.example.com")
// url: Uri = Uri(
//   scheme = Some(value = Scheme(https)),
//   authority = Some(
//     value = Authority(
//       userInfo = None,
//       host = RegName(host = flipt.example.com),
//       port = None
//     )
//   ),
//   path = ,
//   query = ,
//   fragment = None
// )
val token: String = "token"
// token: String = "token"

val resource = EmberClientBuilder
    .default[IO]
    .build
    .map(client =>
        FliptApi[IO](client, url, AuthenticationStrategy.ClientToken("token"))
    )
// resource: cats.effect.kernel.Resource[IO, FliptApi[IO]] = Bind(
//   source = Bind(
//     source = Eval(fa = Pure(value = ())),
//     fs = org.http4s.ember.client.EmberClientBuilder$$Lambda$14688/0x0000000803b3c840@6f8624b5
//   ),
//   fs = cats.effect.kernel.Resource$$Lambda$14689/0x0000000803b3b840@1da7a299
// )

resource.use { flipt => 
    for {
        res <- flipt.evaluateBoolean(
            EvaluationRequest(
                namespaceKey = "default",
                flagKey = "my-flag-1",
                entityId = None,
                context = Map.empty,
                reference = None
            )
        )
    } yield res.enabled
}
// res3: IO[Boolean] = FlatMap(
//   ioe = Pure(value = ()),
//   f = cats.effect.kernel.Resource$$Lambda$14691/0x0000000803b3a040@24effb5b,
//   event = cats.effect.tracing.TracingEvent$StackTrace
// )