scalaflagr

A generic flagr evaluation client inspired by sksamuel/elastic4s

Unsupported Features

  • Authentication
  • Export API

Installation

val scalaflagrVersion = "1.3.0"
libraryDependencies += "io.github.crystailx" %% "scalaflagr-core" % scalaflagrVersion

Quick Start

Evaluation Client

// Provide a http client and cacher

import crystailx.scalaflagr.FlagrService
import crystailx.scalaflagr.cache.simpleCacheKey
import crystailx.scalaflagr.data.EntityContext

val service = FlagrService(client, cacher)
val flagrContext = EntityContext("flag-key", entityContext)
val result = service.isEnabled(flagrContext)

Management Client (for manipulating feature flags via API)

import crystailx.scalaflagr.client.sttp.SttpHttpClient

val flagrConfig = FlagrConfig("http://localhost:18000", "/api/v1")
val manager = new SttpHttpClient(flagrConfig)
manager.createTag(1L, createTagRequest value "new tag")

Supported Http Clients

sttp v1

libraryDependencies += "io.github.crystailx" %% "scalaflagr-client-sttp1" % scalaflagrVersion
import com.softwaremill.sttp.akkahttp.AkkaHttpBackend
import crystailx.scalaflagr.client.sttp.SttpHttpClient

implicit val backend = AkkaHttpBackend()
val client = FlagrClient(new SttpHttpClient(FlagrConfig()))

Supported Cache

No Cache

import crystailx.scalaflagr.FlagrService
import crystailx.scalaflagr.cache.nocache._

val service1 = FlagrService(client) // uses NoCache by default
val service2 = FlagrService(client, NoCache())

InMemory Cache

import crystailx.scalaflagr.FlagrService
import crystailx.scalaflagr.cache.inmemory._

val service = FlagrService(client, InMemoryCache())

Scredis Cache

libraryDependencies += "io.github.crystailx" %% "scalaflagr-cache-scredis" % scalaflagrVersion
import crystailx.scalaflagr.cache.scredis._
import scredis.RedisCluster
import scala.concurrent.ExecutionContext.Implicits.global

val cluster = RedisCluster()
val service = FlagrService(client, RedisCache(cluster))

Scaffeine Cache

libraryDependencies += "io.github.crystailx" %% "scalaflagr-cache-scaffeine" % scalaflagrVersion
import crystailx.scalaflagr.cache.scaffeine._
import com.github.blemale.scaffeine.Scaffeine

val cacher = ScaffeineAsyncLoadingCache.builder(Scaffeine()).buildAsyncFuture
val service = FlagrService(client, cacher)

Supported JSON Libraries

Circe

libraryDependencies += "io.github.crystailx" %% "scalaflagr-json-circe" % scalaflagrVersion
import crystailx.scalaflagr.json.circe._

Play JSON

libraryDependencies += "io.github.crystailx" %% "scalaflagr-json-play" % scalaflagrVersion
import crystailx.scalaflagr.json.play._

Jackson

libraryDependencies += "io.github.crystailx" %% "scalaflagr-json-jackson" % scalaflagrVersion
import crystailx.scalaflagr.json.jackson._

Supported Effects

Scala Future (default)

import crystailx.scalaflagr.effect._

Twitter Future

libraryDependencies += "io.github.crystailx" %% "scalaflagr-effect-twitter" % scalaflagrVersion
import crystailx.scalaflagr.effect.twitter.syntax._

Cats

libraryDependencies += "io.github.crystailx" %% "scalaflagr-effect-cats" % scalaflagrVersion
import crystailx.scalaflagr.effect.cats.syntax._

Example Application

Evaluation Service

import akka.stream.scaladsl.Source
import akka.util.ByteString
import com.softwaremill.sttp.SttpBackend
import com.softwaremill.sttp.akkahttp.AkkaHttpBackend
import crystailx.scalaflagr.auth.BasicAuthConfig
import crystailx.scalaflagr.cache.scredis.{ RedisCache, _ }
import crystailx.scalaflagr.cache.simpleCacheKey
import crystailx.scalaflagr.client.sttp.SttpHttpClient
import crystailx.scalaflagr.data._
import crystailx.scalaflagr.effect._
import crystailx.scalaflagr.json.circe._
import crystailx.scalaflagr.{ FlagrClient, FlagrConfig, FlagrService }
import scredis.protocol.AuthConfig

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{ Await, Future }

object Service {

  import io.circe.generic.auto._

  case class Attachment(viewType: String)

  case class ExtraContext(age: Int)

  def main(args: Array[String]): Unit = {
    implicit val backend: SttpBackend[Future, Source[ByteString, Any]] = AkkaHttpBackend()
    val client = FlagrClient(
      new SttpHttpClient(
        FlagrConfig("http://localhost:18000", basicAuth = Some(BasicAuthConfig("test", "testtest")))
      )
    )
    lazy val cacher = RedisCache(
      scredis.Redis.apply("localhost", 36379, Some(AuthConfig(None, "test")))
    )
    lazy val service = new FlagrService(client, cacher)

    val evaluation = for {
      isEnabled  <- service.isEnabled(BasicContext("test-flag-key"))
      variant    <- service.getUnsafeVariant(EntityContext("test-flag-key", ExtraContext(29)))
      attachment <- Future(variant.attachment[Attachment])
    } yield (isEnabled, variant, attachment)
    val result = Await.result(evaluation, Duration.Inf)
    println(result)

    System.exit(0)
  }

}

Management Client

import akka.stream.scaladsl.Source
import akka.util.ByteString
import com.softwaremill.sttp.SttpBackend
import com.softwaremill.sttp.akkahttp.AkkaHttpBackend
import crystailx.scalaflagr.api._
import crystailx.scalaflagr.api.syntax._
import crystailx.scalaflagr.auth.BasicAuthConfig
import crystailx.scalaflagr.client.sttp.SttpHttpClient
import crystailx.scalaflagr.data._
import crystailx.scalaflagr.effect._
import crystailx.scalaflagr.json.circe._
import crystailx.scalaflagr.{ FlagrClient, FlagrConfig }

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{ Await, Future }

object Manager {

  import io.circe.generic.auto._

  case class Attachment(viewType: String)

  case class ExtraContext(age: Int)

  def main(args: Array[String]): Unit = {
    implicit val backend: SttpBackend[Future, Source[ByteString, Any]] = AkkaHttpBackend()
    implicit val client: FlagrClient[Future] = FlagrClient(
      new SttpHttpClient(
        FlagrConfig("http://localhost:18000", basicAuth = Some(BasicAuthConfig("test", "testtest")))
      )
    )

    val setupFlag = for {
      flag        <- createNewFlag
      variant     <- addVariant(flag.id)
      segment     <- addSegment(flag.id)
      _           <- setDistribution(flag.id, segment.id, variant.key, variant.id)
      enabledFlag <- client.execute(enableFlag(flag.id, enable))
    } yield enabledFlag
    val flag = Await.result(setupFlag, Duration.Inf)
    println(s"flag enabled:${flag.enabled}")

    val finding = client.execute {
      findFlags(findFlagsParam key "test-flag-key")
    }
    val found = Await.result(finding, Duration.Inf)
    println(found)

    System.exit(0)
  }

  def createNewFlag(implicit client: FlagrClient[Future]): Future[Flag] =
    client.execute(
      createFlag(createFlagRequest description "test flag" key "test-flag-key")
    )

  def addVariant(flagID: Long)(implicit client: FlagrClient[Future]): Future[Variant] =
    client.execute(
      createVariant(
        flagID,
        createVariantRequest key "var1" attachment Attachment("homepage")
      )
    )

  def addSegment(flagID: Long)(implicit client: FlagrClient[Future]): Future[Segment] =
    client.execute(
      createSegment(
        flagID,
        createSegmentRequest description "all user" rolloutPercent 100
      )
    )

  def setDistribution(
    flagID: Long,
    segmentID: Long,
    variantKey: String,
    variantID: Long
  )(implicit client: FlagrClient[Future]): Future[List[Distribution]] =
    client.execute(
      updateDistributions(
        flagID,
        segmentID,
        updateDistributionsRequest distributions List(
          Distribution(None, 100, variantKey, variantID)
        )
      )
    )

}