From f5325d9317d7b0e6d1fd3bcebb7f578d8debd3e3 Mon Sep 17 00:00:00 2001 From: Darren Gibson Date: Fri, 19 Jul 2024 16:22:01 -0500 Subject: [PATCH] Add MTL version of launch darkly client --- .github/workflows/ci.yml | 4 +- build.sbt | 13 +- .../catapult/LaunchDarklyMTLClient.scala | 156 ++++++++++++++++++ 3 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 mtl/src/main/scala/org.typelevel/catapult/LaunchDarklyMTLClient.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d97674..16d4868 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,11 +72,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 core/.jvm/target testkit/.jvm/target project/target + run: mkdir -p core/.jvm/target mtl/.jvm/target testkit/.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 core/.jvm/target testkit/.jvm/target project/target + run: tar cf targets.tar core/.jvm/target mtl/.jvm/target testkit/.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') diff --git a/build.sbt b/build.sbt index f29a3d3..5b95094 100644 --- a/build.sbt +++ b/build.sbt @@ -21,7 +21,7 @@ val Scala213 = "2.13.12" ThisBuild / crossScalaVersions := Seq(Scala213, "3.3.3") ThisBuild / scalaVersion := Scala213 // the default Scala -lazy val root = tlCrossRootProject.aggregate(core, testkit) +lazy val root = tlCrossRootProject.aggregate(core, mtl, testkit) lazy val testkit = crossProject(JVMPlatform) .crossType(CrossType.Pure) @@ -49,4 +49,15 @@ lazy val core = crossProject(JVMPlatform) ), ) +lazy val mtl = crossProject(JVMPlatform) + .crossType(CrossType.Pure) + .in(file("mtl")) + .settings( + name := "catapult-mtl", + libraryDependencies ++= Seq( + "org.typelevel" %% "cats-mtl" % "1.4.0" + ), + ) + .dependsOn(core) + lazy val docs = project.in(file("site")).enablePlugins(TypelevelSitePlugin) diff --git a/mtl/src/main/scala/org.typelevel/catapult/LaunchDarklyMTLClient.scala b/mtl/src/main/scala/org.typelevel/catapult/LaunchDarklyMTLClient.scala new file mode 100644 index 0000000..e88bf7d --- /dev/null +++ b/mtl/src/main/scala/org.typelevel/catapult/LaunchDarklyMTLClient.scala @@ -0,0 +1,156 @@ +/* + * Copyright 2022 Typelevel + * + * 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 org.typelevel.catapult + +import cats.effect.{Async, Resource} +import cats.~> +import com.launchdarkly.sdk.server.interfaces.{FlagValueChangeEvent, FlagValueChangeListener} +import com.launchdarkly.sdk.server.{LDClient, LDConfig} +import com.launchdarkly.sdk.LDContext +import com.launchdarkly.sdk.LDValue +import fs2._ +import cats._ +import cats.implicits._ +import cats.mtl._ +import cats.mtl.syntax._ + +trait LaunchDarklyMTLClient[F[_]] { + + /** @param featureKey the key of the flag to be evaluated + * @param defaultValue the value to use if evaluation fails for any reason + * @return the flag value, suspended in the `F` effect. If evaluation fails for any reason, or the evaluated value is not of type Boolean, returns the default value. + * @see [[https://javadoc.io/doc/com.launchdarkly/launchdarkly-java-server-sdk/latest/com/launchdarkly/sdk/server/interfaces/LDClientInterface.html#boolVariation(java.lang.String,com.launchdarkly.sdk.LDContext,boolean) LDClientInterface#boolVariation]] + */ + def boolVariation( + featureKey: String, + defaultValue: Boolean, + ): F[Boolean] + + /** @param featureKey the key of the flag to be evaluated + * @param defaultValue the value to use if evaluation fails for any reason + * @return the flag value, suspended in the `F` effect. If evaluation fails for any reason, or the evaluated value is not of type String, returns the default value. + * @see [[https://javadoc.io/doc/com.launchdarkly/launchdarkly-java-server-sdk/latest/com/launchdarkly/sdk/server/interfaces/LDClientInterface.html#stringVariation(java.lang.String,com.launchdarkly.sdk.LDContext,string) LDClientInterface#stringVariation]] + */ + def stringVariation( + featureKey: String, + defaultValue: String, + ): F[String] + + /** @param featureKey the key of the flag to be evaluated + * @param defaultValue the value to use if evaluation fails for any reason + * @return the flag value, suspended in the `F` effect. If evaluation fails for any reason, or the evaluated value cannot be represented as type Int, returns the default value. + * @see [[https://javadoc.io/doc/com.launchdarkly/launchdarkly-java-server-sdk/latest/com/launchdarkly/sdk/server/interfaces/LDClientInterface.html#intVariation(java.lang.String,com.launchdarkly.sdk.LDContext,int) LDClientInterface#intVariation]] + */ + + /** @param featureKey the key of the flag to be evaluated + * @param defaultValue the value to use if evaluation fails for any reason + * @return the flag value, suspended in the `F` effect. If evaluation fails for any reason, or the evaluated value cannot be represented as type Double, returns the default value. + * @see [[https://javadoc.io/doc/com.launchdarkly/launchdarkly-java-server-sdk/latest/com/launchdarkly/sdk/server/interfaces/LDClientInterface.html#doubleVariation(java.lang.String,com.launchdarkly.sdk.LDContext,double) LDClientInterface#doubleVariation]] + */ + def doubleVariation( + featureKey: String, + defaultValue: Double, + ): F[Double] + + /** @param featureKey the key of the flag to be evaluated + * @param defaultValue the value to use if evaluation fails for any reason + * @return the flag value, suspended in the `F` effect. If evaluation fails for any reason, returns the default value. + * @see [[https://javadoc.io/doc/com.launchdarkly/launchdarkly-java-server-sdk/latest/com/launchdarkly/sdk/server/interfaces/LDClientInterface.html#jsonValueVariation(java.lang.String,com.launchdarkly.sdk.LDContext,com.launchdarkly.sdk.LDValue) LDClientInterface#jsonValueVariation]] + */ + def jsonValueVariation( + featureKey: String, + defaultValue: LDValue, + ): F[LDValue] + + /** @param featureKey the key of the flag to be evaluated + * @return A `Stream` of [[https://javadoc.io/doc/com.launchdarkly/launchdarkly-java-server-sdk/latest/com/launchdarkly/sdk/server/interfaces/FlagValueChangeEvent.html FlagValueChangeEvent]] instances representing changes to the value of the flag in the provided context. Note: if the flag value changes multiple times in quick succession, some intermediate values may be missed; for example, a change from 1` to `2` to `3` may be represented only as a change from `1` to `3` + * @see [[https://javadoc.io/doc/com.launchdarkly/launchdarkly-java-server-sdk/latest/com/launchdarkly/sdk/server/interfaces/FlagTracker.html FlagTracker]] + */ + def trackFlagValueChanges( + featureKey: String + ): Stream[F, FlagValueChangeEvent] + + /** @see [[https://javadoc.io/doc/com.launchdarkly/launchdarkly-java-server-sdk/latest/com/launchdarkly/sdk/server/interfaces/LDClientInterface.html#flush() LDClientInterface#flush]] + */ + def flush: F[Unit] + + def mapK[G[_]](fk: F ~ G): LaunchDarklyMTLClient[G] = LaunchDarklyMTLClient.mapk(this)(fk) +} + +object LaunchDarklyMTLClient { + + /** @return a Catapult [[LaunchDarklyMTLClient]] wrapped in [[cats.effect.Resource]], created using the given SDK key and config + */ + def resource[F[_]: Async](sdkKey: String, config: LDConfig)(implicit + contextAsk: Ask[F, LDContext] + ): Resource[F, LaunchDarklyMTLClient[F]] = + LaunchDarklyClient.resource[F](sdkKey, config).map(fromLaunchDarklyClient[F](_)) + + /** @return a Catapult [[LaunchDarklyMTLClient]] wrapped in [[cats.effect.Resource]], created using the given SDK key and default config + */ + def resource[F[_]: Async](sdkKey: String)(implicit + contextAsk: Ask[F, LDContext] + ): Resource[F, LaunchDarklyMTLClient[F]] = + LaunchDarklyClient.resource[F](sdkKey).map(fromLaunchDarklyClient[F](_)) + + def fromLaunchDarklyClient[F[_]]( + launchDarklyClient: LaunchDarklyClient[F] + )(implicit F: Monad[F], contextAsk: Ask[F, LDContext]): LaunchDarklyMTLClient[F] = + new LaunchDarklyMTLClient[F] { + def flush: F[Unit] = launchDarklyClient.flush + + def boolVariation(featureKey: String, defaultValue: Boolean): F[Boolean] = + contextAsk.ask.flatMap(launchDarklyClient.boolVariation(featureKey, _, defaultValue)) + + def doubleVariation(featureKey: String, defaultValue: Double): F[Double] = + contextAsk.ask.flatMap(launchDarklyClient.doubleVariation(featureKey, _, defaultValue)) + + def jsonValueVariation( + featureKey: String, + defaultValue: com.launchdarkly.sdk.LDValue, + ): F[com.launchdarkly.sdk.LDValue] = + contextAsk.ask.flatMap(launchDarklyClient.jsonValueVariation(featureKey, _, defaultValue)) + + def stringVariation(featureKey: String, defaultValue: String): F[String] = + contextAsk.ask.flatMap(launchDarklyClient.stringVariation(featureKey, _, defaultValue)) + + def trackFlagValueChanges( + featureKey: String + ): fs2.Stream[F, com.launchdarkly.sdk.server.interfaces.FlagValueChangeEvent] = + Stream.eval(contextAsk.ask).flatMap(launchDarklyClient.trackFlagValueChanges(featureKey, _)) + } + + def mapK[F[_], G[_]](ldc: LaunchDarklyMTLClient[F])(fk: F ~> G): LaunchDarklyMTLClient[G] = + new LaunchDarklyMTLClient[G] { + def boolVariation(featureKey: String, defaultValue: Boolean): G[Boolean] = + fk(ldc.boolVariation(featureKey, defaultValue)) + + def stringVariation(featureKey: String, defaultValue: String): G[String] = + fk(ldc.stringVariation(featureKey, defaultValue)) + + def doubleVariation(featureKey: String, defaultValue: Double): G[Double] = + fk(ldc.doubleVariation(featureKey, defaultValue)) + + def jsonValueVariation(featureKey: String, defaultValue: LDValue): G[LDValue] = + fk(ldc.jsonValueVariation(featureKey, defaultValue)) + + def trackFlagValueChanges(featureKey: String): Stream[G, FlagValueChangeEvent] = + ldc.trackFlagValueChanges(featureKey).translate(fk) + + def flush: G[Unit] = fk(ldc.flush) + } +}