Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenFeature: Add After hooks #26

Merged
merged 4 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ trait FeatureClient[F[_]] {
def evaluationContext: EvaluationContext
def withEvaluationContext(context: EvaluationContext): FeatureClient[F]

def beforeHooks: List[BeforeHook[F]]

def withHook(hook: Hook[F]): FeatureClient[F]
// def withHooks(hooks: List[Hook]): FeatureClient[F]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ protected[openfeature] final class FeatureClientImpl[F[_]](
provider: EvaluationProvider[F],
val clientEvaluationContext: EvaluationContext,
val beforeHooks: List[BeforeHook[F]],
val errorHooks: List[ErrorHook[F]]
val errorHooks: List[ErrorHook[F]],
val afterHooks: List[AfterHook[F]]
)(implicit M: MonadThrow[F])
extends FeatureClient[F] {

Expand All @@ -42,7 +43,8 @@ protected[openfeature] final class FeatureClientImpl[F[_]](
provider,
clientEvaluationContext ++ context,
beforeHooks,
errorHooks
errorHooks,
afterHooks
)

override def withHook(hook: Hook[F]): FeatureClient[F] =
Expand All @@ -52,14 +54,24 @@ protected[openfeature] final class FeatureClientImpl[F[_]](
provider,
clientEvaluationContext,
beforeHooks.appended(h),
errorHooks
errorHooks,
afterHooks
)
case h: ErrorHook[F] =>
new FeatureClientImpl[F](
provider,
clientEvaluationContext,
beforeHooks,
errorHooks.appended(h)
errorHooks.appended(h),
afterHooks
)
case h: AfterHook[F] =>
new FeatureClientImpl[F](
provider,
clientEvaluationContext,
beforeHooks,
errorHooks,
afterHooks.appended(h)
)
}

Expand Down Expand Up @@ -360,11 +372,16 @@ protected[openfeature] final class FeatureClientImpl[F[_]](
for {
newContext <- Hooks.runBefore[F](beforeHooks)(hookContext, hookHints)
evaluation <- evaluate(newContext)
_ <-
Hooks.runAfter[F](afterHooks)(
hookContext.copy(evaluationContext = newContext),
hookHints
)
} yield evaluation

run
.onError { case e =>
Hooks.runErrors(errorHooks)(hookContext, hookHints, e)
.onError { case error =>
Hooks.runErrors(errorHooks)(hookContext, hookHints, error)
}
.handleError(error =>
EvaluationDetails(flagKey, ResolutionDetails.error(default, error))
Expand All @@ -383,7 +400,8 @@ object FeatureClientImpl {
provider = provider,
clientEvaluationContext = EvaluationContext.empty,
beforeHooks = List.empty[BeforeHook[F]],
errorHooks = List.empty[ErrorHook[F]]
errorHooks = List.empty[ErrorHook[F]],
afterHooks = List.empty[AfterHook[F]]
)

}
22 changes: 22 additions & 0 deletions openfeature/sdk/src/main/scala/io/cardell/openfeature/Hook.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,25 @@ object ErrorHook {
}

}

trait AfterHook[F[_]] extends Hook[F] {

def apply(context: HookContext, hints: HookHints): F[Unit]

}

object AfterHook {

def apply[F[_]](
f: (HookContext, HookHints) => F[Unit]
): AfterHook[F] =
new AfterHook[F] {

def apply(
context: HookContext,
hints: HookHints
): F[Unit] = f(context, hints)

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.cardell.openfeature

import cats.Applicative
import cats.Monad
import cats.syntax.all._

Expand Down Expand Up @@ -98,12 +99,14 @@ object Hooks {
aux(hooks, context).map(_.getOrElse(context.evaluationContext))
}

def runErrors[F[_]: Monad](
def runErrors[F[_]: Applicative](
hooks: List[ErrorHook[F]]
)(
context: HookContext,
hints: HookHints,
error: Throwable
): F[Unit] = hooks.traverse(_.apply(context, hints, error)).void
)(context: HookContext, hints: HookHints, error: Throwable): F[Unit] =
hooks.traverse(_.apply(context, hints, error)).void

def runAfter[F[_]: Applicative](
hooks: List[AfterHook[F]]
)(context: HookContext, hints: HookHints): F[Unit] =
hooks.traverse(_.apply(context, hints)).void

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,9 @@

package io.cardell.openfeature.provider

import io.cardell.openfeature.BeforeHook
import io.cardell.openfeature.ErrorHook
import io.cardell.openfeature.Hook

trait Provider[F[_]] extends EvaluationProvider[F] {
def beforeHooks: List[BeforeHook[F]]
def errorHooks: List[ErrorHook[F]]

def withHook(hook: Hook[F]): Provider[F]
// def withHooks(hooks: List[Hook[F]]): Provider[F]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package io.cardell.openfeature.provider
import cats.MonadThrow
import cats.syntax.all._

import io.cardell.openfeature.AfterHook
import io.cardell.openfeature.BeforeHook
import io.cardell.openfeature.ErrorHook
import io.cardell.openfeature.EvaluationContext
Expand All @@ -32,24 +33,34 @@ import io.cardell.openfeature.StructureDecoder
protected class ProviderImpl[F[_]: MonadThrow](
evaluationProvider: EvaluationProvider[F],
val beforeHooks: List[BeforeHook[F]],
val errorHooks: List[ErrorHook[F]]
val errorHooks: List[ErrorHook[F]],
val afterHooks: List[AfterHook[F]]
) extends Provider[F] {

override def metadata: ProviderMetadata = evaluationProvider.metadata

override def withHook(hook: Hook[F]): Provider[F] =
hook match {
case bh: BeforeHook[F] =>
case h: BeforeHook[F] =>
new ProviderImpl[F](
evaluationProvider = evaluationProvider,
beforeHooks = beforeHooks.appended(bh),
errorHooks = errorHooks
beforeHooks = beforeHooks.appended(h),
errorHooks = errorHooks,
afterHooks = afterHooks
)
case eh: ErrorHook[F] =>
case h: ErrorHook[F] =>
new ProviderImpl[F](
evaluationProvider = evaluationProvider,
beforeHooks = beforeHooks,
errorHooks = errorHooks.appended(eh)
errorHooks = errorHooks.appended(h),
afterHooks = afterHooks
)
case h: AfterHook[F] =>
new ProviderImpl[F](
evaluationProvider = evaluationProvider,
beforeHooks = beforeHooks,
errorHooks = errorHooks,
afterHooks = afterHooks.appended(h)
)

}
Expand Down Expand Up @@ -137,6 +148,11 @@ protected class ProviderImpl[F[_]: MonadThrow](
for {
context <- Hooks.runBefore(beforeHooks)(hc, hints)
res <- resolve(context)
_ <-
Hooks.runAfter(afterHooks)(
hc.copy(evaluationContext = context),
hints
)
} yield res

run.onError(error =>
Expand All @@ -154,7 +170,8 @@ object ProviderImpl {
new ProviderImpl[F](
evaluationProvider = evaluationProvider,
beforeHooks = List.empty[BeforeHook[F]],
errorHooks = List.empty[ErrorHook[F]]
errorHooks = List.empty[ErrorHook[F]],
afterHooks = List.empty[AfterHook[F]]
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ class FeatureClientImplTest extends CatsEffectSuite {

val client = FeatureClientImpl[IO](provider)

val result = client.withHook(beforeHook1).beforeHooks
val result =
client
.withHook(beforeHook1)
.asInstanceOf[FeatureClientImpl[IO]]
.beforeHooks

assertEquals(expected, result)
}
Expand Down Expand Up @@ -114,6 +118,21 @@ class FeatureClientImplTest extends CatsEffectSuite {
}
}

test("before hooks run on boolean evaluation") {
val ref = Ref.unsafe[IO, Int](0)

val afterHook = AfterHook[IO] { case _ => ref.update(_ + 2) }

val client = FeatureClientImpl[IO](provider).withHook(afterHook)

val expected = 2

for {
_ <- client.getBooleanValue("test-flag", false)
result <- ref.get
} yield assertEquals(result, expected)
}

}

object ThrowingEvaluationProvider extends EvaluationProvider[IO] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import cats.effect.IO
import cats.effect.kernel.Ref
import munit.CatsEffectSuite

import io.cardell.openfeature.AfterHook
import io.cardell.openfeature.BeforeHook
import io.cardell.openfeature.ErrorHook
import io.cardell.openfeature.EvaluationContext
Expand Down Expand Up @@ -47,7 +48,8 @@ class ProviderImplTest extends CatsEffectSuite {

val provider = ProviderImpl(evaluationProvider)

val result = provider.withHook(beforeHook1).beforeHooks
val result =
provider.withHook(beforeHook1).asInstanceOf[ProviderImpl[IO]].beforeHooks

assertEquals(result, expected)
}
Expand All @@ -62,7 +64,8 @@ class ProviderImplTest extends CatsEffectSuite {

val provider = ProviderImpl(evaluationProvider).withHook(beforeHook1)

val result = provider.withHook(beforeHook2).beforeHooks
val result =
provider.withHook(beforeHook2).asInstanceOf[ProviderImpl[IO]].beforeHooks

assertEquals(result, expected)
}
Expand Down Expand Up @@ -135,4 +138,27 @@ class ProviderImplTest extends CatsEffectSuite {
} yield assert(result.isLeft, "result was not a Left")
}

test("after hooks run after successful evaluation") {
val ref = Ref.unsafe[IO, Int](0)

val afterHook = AfterHook[IO] { case _ => ref.update(_ + 2) }

val provider = ProviderImpl(evaluationProvider)
.withHook(afterHook)

val expected = 2

for {
_ <-
provider
.resolveBooleanValue(
"test-flag",
false,
EvaluationContext.empty
)
.attempt
result <- ref.get
} yield assertEquals(result, expected)
}

}