All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.cardell.openfeature.provider.flipt.FliptProvider.scala Maven / Gradle / Ivy

There is a newer version: 0.4.0
Show newest version
/*
 * 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.flipt

import cats.MonadThrow
import cats.syntax.all._
import io.circe.parser.parse
import scala.util.Success
import scala.util.Try

import io.cardell.flipt.EvaluationRequest
import io.cardell.flipt.FliptApi
import io.cardell.flipt.model.{EvaluationReason => FliptReason}
import io.cardell.openfeature.ErrorCode
import io.cardell.openfeature.EvaluationContext
import io.cardell.openfeature.EvaluationReason
import io.cardell.openfeature.StructureCodec
import io.cardell.openfeature.StructureDecoder
import io.cardell.openfeature.StructureDecoderError
import io.cardell.openfeature.circe.JsonStructureConverters
import io.cardell.openfeature.provider.EvaluationProvider
import io.cardell.openfeature.provider.FlagMetadataValue
import io.cardell.openfeature.provider.ProviderMetadata
import io.cardell.openfeature.provider.ResolutionDetails

final class FliptProvider[F[_]: MonadThrow](
    flipt: FliptApi[F],
    namespace: String
) extends EvaluationProvider[F] {

  override def metadata: ProviderMetadata = ProviderMetadata(name = "flipt")

  override def resolveBooleanValue(
      flagKey: String,
      defaultValue: Boolean,
      context: EvaluationContext
  ): F[ResolutionDetails[Boolean]] = {
    val evalContext = mapContext(context)

    val req: EvaluationRequest = EvaluationRequest(
      namespaceKey = namespace,
      flagKey = flagKey,
      entityId = context.targetingKey,
      context = evalContext,
      reference = None
    )

    val resolution = flipt.evaluateBoolean(req).map { evaluation =>
      ResolutionDetails[Boolean](
        value = evaluation.enabled,
        errorCode = None,
        errorMessage = None,
        reason = mapReason(evaluation.reason).some,
        variant = None,
        metadata = None
      )
    }

    resolution.attempt.map {
      case Right(value) => value
      case Left(error)  => default(error, defaultValue)
    }
  }

  override def resolveStringValue(
      flagKey: String,
      defaultValue: String,
      context: EvaluationContext
  ): F[ResolutionDetails[String]] = resolve[String](
    flagKey,
    defaultValue,
    context
  )

  override def resolveIntValue(
      flagKey: String,
      defaultValue: Int,
      context: EvaluationContext
  ): F[ResolutionDetails[Int]] = resolve[Int](
    flagKey,
    defaultValue,
    context
  )

  override def resolveDoubleValue(
      flagKey: String,
      defaultValue: Double,
      context: EvaluationContext
  ): F[ResolutionDetails[Double]] = resolve[Double](
    flagKey,
    defaultValue,
    context
  )

  override def resolveStructureValue[A: StructureCodec](
      flagKey: String,
      defaultValue: A,
      context: EvaluationContext
  ): F[ResolutionDetails[A]] = {
    val evalContext = mapContext(context)

    val req: EvaluationRequest = EvaluationRequest(
      namespaceKey = namespace,
      flagKey = flagKey,
      entityId = context.targetingKey,
      context = evalContext,
      reference = None
    )

    val resolution = flipt.evaluateVariant(req).map { evaluation =>
      val jsonAttachment = parse(evaluation.variantAttachment).map(_.asObject)

      jsonAttachment match {
        case Left(parseError) =>
          ResolutionDetails.fromThrowable(defaultValue, parseError)
        case Right(None) =>
          ResolutionDetails.error(defaultValue, "did not receive json object")
        case Right(Some(jsonObject)) =>
          val structure = JsonStructureConverters.jsonToStructure(jsonObject)
          val decodedStructure = StructureDecoder[A].decodeStructure(structure)
          decodedStructure match {
            case Left(error) =>
              ResolutionDetails.fromThrowable[A](defaultValue, error.cause)
            case Right(value) => ResolutionDetails[A](value)
          }
      }

    }

    resolution.attempt.map {
      case Right(value) => value
      case Left(error)  => default(error, defaultValue)
    }
  }

  private def mapReason(evalReason: FliptReason): EvaluationReason =
    evalReason match {
      case FliptReason.Default      => EvaluationReason.Default
      case FliptReason.FlagDisabled => EvaluationReason.Disabled
      case FliptReason.Match        => EvaluationReason.TargetingMatch
      case FliptReason.Unknown      => EvaluationReason.Unknown
    }

  private def mapContext(context: EvaluationContext): Map[String, String] =
    context.values.map { case (k, v) => (k, v.stringValue) }

  def default[A](t: Throwable, defaultValue: A) = ResolutionDetails[A](
    value = defaultValue,
    errorCode = Some(ErrorCode.General),
    errorMessage = Some(t.getMessage()),
    reason = Some(EvaluationReason.Error),
    variant = None,
    metadata = None
  )

  def decodeDefault[A](
      e: StructureDecoderError,
      defaultValue: A
  ) = ResolutionDetails[A](
    value = defaultValue,
    errorCode = Some(ErrorCode.ParseError),
    errorMessage = Some(e.message),
    reason = Some(EvaluationReason.Error),
    variant = None,
    metadata = None
  )

  private def resolve[A: ResolveDecoder](
      flagKey: String,
      defaultValue: A,
      context: EvaluationContext
  ): F[ResolutionDetails[A]] = {
    val evalContext = mapContext(context)

    val req: EvaluationRequest = EvaluationRequest(
      namespaceKey = namespace,
      flagKey = flagKey,
      entityId = context.targetingKey,
      context = evalContext,
      reference = None
    )

    val stringResolution = flipt.evaluateVariant(req).map { evaluation =>
      ResolutionDetails[String](
        value = evaluation.variantKey,
        errorCode = None,
        errorMessage = None,
        reason = mapReason(evaluation.reason).some,
        variant = Some(evaluation.variantKey),
        metadata = Some(
          Map(
            "variant-attachment" -> FlagMetadataValue
              .StringValue(evaluation.variantAttachment)
          )
        )
      )
    }

    val resolution =
      for {
        res <- stringResolution
        casted <- MonadThrow[F].fromTry(
          ResolveDecoder[A].decode(res.value)
        )
      } yield ResolutionDetails[A](
        value = casted,
        errorCode = None,
        errorMessage = None,
        reason = res.reason,
        variant = res.variant,
        metadata = res.metadata
      )

    resolution.attempt.map {
      case Right(value) => value
      case Left(error)  => default(error, defaultValue)
    }
  }

}

protected sealed trait ResolveDecoder[A] {
  def decode(s: String): Try[A]
}

protected object ResolveDecoder {

  def apply[A](implicit r: ResolveDecoder[A]): ResolveDecoder[A] = implicitly

  implicit val string: ResolveDecoder[String] =
    new ResolveDecoder[String] {
      def decode(s: String): Try[String] = Success(s)
    }

  implicit val int: ResolveDecoder[Int] =
    new ResolveDecoder[Int] {
      def decode(s: String): Try[Int] = Try(s.toInt)
    }

  implicit val double: ResolveDecoder[Double] =
    new ResolveDecoder[Double] {
      def decode(s: String): Try[Double] = Try(s.toDouble)
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy