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

cloud.prefab.client.internal.ConfigRuleEvaluator Maven / Gradle / Ivy

Go to download

API Client for https://prefab.cloud: rate limits, feature flags and semaphores as a service

There is a newer version: 0.3.23
Show newest version
package cloud.prefab.client.internal;

import cloud.prefab.client.ConfigStore;
import cloud.prefab.client.config.ConfigElement;
import cloud.prefab.client.config.ConfigValueUtils;
import cloud.prefab.client.config.EvaluatedCriterion;
import cloud.prefab.client.config.Match;
import cloud.prefab.client.config.logging.AbstractLoggingListener;
import cloud.prefab.domain.Prefab;
import com.google.common.collect.Streams;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ConfigRuleEvaluator {

  public static final String CURRENT_TIME_KEY = "prefab.current-time";
  private static final Logger LOG = LoggerFactory.getLogger(ConfigRuleEvaluator.class);

  private final ConfigStore configStore;
  private final WeightedValueEvaluator weightedValueEvaluator;

  public ConfigRuleEvaluator(
    ConfigStore configStoreImpl,
    WeightedValueEvaluator weightedValueEvaluator
  ) {
    this.weightedValueEvaluator = weightedValueEvaluator;
    this.configStore = configStoreImpl;
  }

  public Optional getMatch(String key, LookupContext lookupContext) {
    final ConfigElement configElement = configStore.getElement(key);
    if (configElement == null) {
      // logging lookups generate a lot of misses so skip those
      if (!key.startsWith(AbstractLoggingListener.LOG_LEVEL_PREFIX)) {
        LOG.trace("No config value found for key {}", key);
      }
      return Optional.empty();
    }

    return getMatch(configElement, lookupContext);
  }

  /**
   * find if we have a match for the given properties
   *
   * @param configElement
   * @param lookupContext
   * @return
   */
  public Optional getMatch(
    ConfigElement configElement,
    LookupContext lookupContext
  ) {
    return getMatch(configElement, lookupContext, new LinkedList<>());
  }

  private Optional getMatch(
    String key,
    LookupContext lookupContext,
    Deque> rowPropertiesStack
  ) {
    if (!configStore.containsKey(key)) {
      // logging lookups generate a lot of misses so skip those
      if (!key.startsWith(AbstractLoggingListener.LOG_LEVEL_PREFIX)) {
        LOG.trace("No config value found for key {}", key);
      }
      return Optional.empty();
    }
    final ConfigElement configElement = configStore.getElement(key);

    return getMatch(configElement, lookupContext, rowPropertiesStack);
  }

  private Optional getMatch(
    ConfigElement configElement,
    LookupContext lookupContext,
    Deque> rowPropertiesStack
  ) {
    // Prefer rows that have a projEnvId to ones that don't
    // There will be 0-1 rows with projenv and 0-1 rows without (the default row)

    return Streams
      .mapWithIndex(
        configElement.getRowsProjEnvFirst(configStore.getProjectEnvironmentId()),
        (configRow, rowIndex) -> {
          if (!configRow.getPropertiesMap().isEmpty()) {
            rowPropertiesStack.push(configRow.getPropertiesMap());
          }
          // Return the value of the first matching set of criteria
          int conditionalValueIndex = 0;
          for (Prefab.ConditionalValue conditionalValue : configRow.getValuesList()) {
            Optional optionalMatch = evaluateConditionalValue(
              rowIndex,
              conditionalValue,
              conditionalValueIndex,
              lookupContext,
              rowPropertiesStack,
              configElement
            );

            if (optionalMatch.isPresent()) {
              return optionalMatch.get();
            }
            conditionalValueIndex++;
          }
          if (!configRow.getPropertiesMap().isEmpty()) {
            rowPropertiesStack.pop();
          }
          return null;
        }
      )
      .filter(Objects::nonNull)
      .findFirst();
  }

  /**
   * If all of the conditions match, return a true match
   *
   * @param conditionalValue
   * @param rowProperties
   * @param configElement
   * @return
   */
  private Optional evaluateConditionalValue(
    long rowIndex,
    Prefab.ConditionalValue conditionalValue,
    int conditionalValueIndex,
    LookupContext lookupContext,
    Deque> rowProperties,
    ConfigElement configElement
  ) {
    List evaluatedCriteria = new ArrayList<>();
    for (Prefab.Criterion criterion : conditionalValue.getCriteriaList()) {
      for (EvaluatedCriterion evaluateCriterion : evaluateCriterionMatch(
        criterion,
        lookupContext,
        rowProperties
      )) {
        if (!evaluateCriterion.isMatch()) {
          return Optional.empty();
        }
        evaluatedCriteria.add(evaluateCriterion);
      }
    }
    return Optional.of(
      simplifyToMatch(
        rowIndex,
        conditionalValue,
        conditionalValueIndex,
        configElement,
        lookupContext,
        evaluatedCriteria
      )
    );
  }

  /**
   * A ConfigValue may be a WeightedValue. If so break it down so we can return a simpler form.
   */
  private Match simplifyToMatch(
    long rowIndex,
    Prefab.ConditionalValue selectedConditionalValue,
    int conditionalValueIndex,
    ConfigElement configElement,
    LookupContext lookupContext,
    List evaluatedCriteria
  ) {
    if (selectedConditionalValue.getValue().hasWeightedValues()) {
      WeightedValueEvaluator.Result result = weightedValueEvaluator.toResult(
        selectedConditionalValue.getValue().getWeightedValues(),
        configElement.getConfig().getKey(),
        lookupContext
      );
      return new Match(
        result.getValue(),
        configElement,
        evaluatedCriteria,
        (int) rowIndex,
        conditionalValueIndex,
        Optional.of(result.getIndex())
      );
    } else {
      return new Match(
        selectedConditionalValue.getValue(),
        configElement,
        evaluatedCriteria,
        (int) rowIndex,
        conditionalValueIndex,
        Optional.empty()
      );
    }
  }

  private List keyAndLowerCasedKey(String key) {
    String lowerCased = key.toLowerCase();
    if (lowerCased.equals(key)) {
      return Collections.singletonList(key);
    }
    return List.of(key, lowerCased);
  }

  private Optional prop(
    String key,
    LookupContext lookupContext,
    Deque> rowPropertiesStack
  ) {
    List keysToLookup = keyAndLowerCasedKey(key);

    for (Map rowProperties : rowPropertiesStack) {
      for (String keyToLookup : keysToLookup) {
        Prefab.ConfigValue rowPropValue = rowProperties.get(keyToLookup);
        if (rowPropValue != null) {
          return Optional.of(rowPropValue);
        }
      }
    }

    for (String keyToLookup : keysToLookup) {
      Prefab.ConfigValue valueFromLookupContext = lookupContext
        .getExpandedProperties()
        .get(keyToLookup);
      if (valueFromLookupContext != null) {
        return Optional.of(valueFromLookupContext);
      }
    }
    //TODO: move this current time injection into a ContextResolver class?
    if (CURRENT_TIME_KEY.equals(key)) {
      return Optional.of(
        Prefab.ConfigValue.newBuilder().setInt(System.currentTimeMillis()).build()
      );
    }
    return Optional.empty();
  }

  private Optional getPropFromContextWrapper(
    List keysToLookup,
    ContextWrapper contextWrapper
  ) {
    for (String keyToLookup : keysToLookup) {
      Prefab.ConfigValue configValue = contextWrapper
        .getConfigValueMap()
        .get(keyToLookup);
      if (configValue != null) {
        return Optional.of(configValue);
      }
    }
    return Optional.empty();
  }

  List evaluateCriterionMatch(
    Prefab.Criterion criterion,
    LookupContext lookupContext
  ) {
    return evaluateCriterionMatch(criterion, lookupContext, new LinkedList<>());
  }

  /**
   * Does this criterion match?
   *
   * @param criterion
   * @return
   */
  List evaluateCriterionMatch(
    Prefab.Criterion criterion,
    LookupContext lookupContext,
    Deque> rowPropertiesStack
  ) {
    final Optional prop = prop(
      criterion.getPropertyName(),
      lookupContext,
      rowPropertiesStack
    );
    Optional propStringValue = prop.flatMap(ConfigValueUtils::coerceToString);

    switch (criterion.getOperator()) {
      case ALWAYS_TRUE:
        return List.of(new EvaluatedCriterion(criterion, true));
      case HIERARCHICAL_MATCH:
        if (prop.isPresent()) {
          if (prop.get().hasString() && criterion.getValueToMatch().hasString()) {
            final String propertyString = prop.get().getString();
            return List.of(
              new EvaluatedCriterion(
                criterion,
                criterion.getValueToMatch(),
                hierarchicalMatch(propertyString, criterion.getValueToMatch().getString())
              )
            );
          }
        }
        return List.of(
          new EvaluatedCriterion(criterion, criterion.getValueToMatch(), false)
        );
      // The string here is the key of the Segment
      case IN_SEG:
        final Optional evaluatedSegment = getMatch(
          criterion.getValueToMatch().getString(),
          lookupContext,
          rowPropertiesStack
        );

        if (
          evaluatedSegment.isPresent() &&
          evaluatedSegment.get().getConfigValue().hasBool() &&
          evaluatedSegment.get().getConfigValue().getBool()
        ) {
          return evaluatedSegment.get().getEvaluatedCriterion();
        } else {
          return List.of(
            new EvaluatedCriterion(
              criterion,
              "Missing Segment " + criterion.getValueToMatch().getString(),
              false
            )
          );
        }
      case NOT_IN_SEG:
        final Optional evaluatedNotSegment = getMatch(
          criterion.getValueToMatch().getString(),
          lookupContext
        )
          .map(Match::getConfigValue);

        if (evaluatedNotSegment.isPresent() && evaluatedNotSegment.get().hasBool()) {
          return List.of(
            new EvaluatedCriterion(
              criterion,
              criterion.getValueToMatch(),
              !evaluatedNotSegment.get().getBool()
            )
          );
        } else {
          return List.of(
            new EvaluatedCriterion(
              criterion,
              "Missing Segment " + criterion.getValueToMatch().getString(),
              true
            )
          );
        }
      case PROP_IS_ONE_OF:
        if (propStringValue.isEmpty()) {
          return List.of(new EvaluatedCriterion(criterion, false));
        }
        // assumption that property is a String
        return List.of(
          new EvaluatedCriterion(
            criterion,
            propStringValue.get(),
            criterion
              .getValueToMatch()
              .getStringList()
              .getValuesList()
              .contains(propStringValue.get())
          )
        );
      case PROP_IS_NOT_ONE_OF:
        if (propStringValue.isEmpty()) {
          return List.of(new EvaluatedCriterion(criterion, false));
        }

        return List.of(
          new EvaluatedCriterion(
            criterion,
            propStringValue.get(),
            !criterion
              .getValueToMatch()
              .getStringList()
              .getValuesList()
              .contains(propStringValue.get())
          )
        );
      case PROP_ENDS_WITH_ONE_OF:
        if (prop.isPresent() && prop.get().hasString()) {
          final boolean matched = criterion
            .getValueToMatch()
            .getStringList()
            .getValuesList()
            .stream()
            .anyMatch(value -> prop.get().getString().endsWith(value));

          return List.of(new EvaluatedCriterion(criterion, prop.get(), matched));
        } else {
          return List.of(new EvaluatedCriterion(criterion, false));
        }
      case PROP_DOES_NOT_END_WITH_ONE_OF:
        if (prop.isPresent() && prop.get().hasString()) {
          final boolean matched = criterion
            .getValueToMatch()
            .getStringList()
            .getValuesList()
            .stream()
            .anyMatch(value -> prop.get().getString().endsWith(value));

          return List.of(new EvaluatedCriterion(criterion, prop.get(), !matched));
        } else {
          return List.of(new EvaluatedCriterion(criterion, true));
        }
      case IN_INT_RANGE:
        if (
          prop.isPresent() &&
          prop.get().hasInt() &&
          criterion.getValueToMatch().hasIntRange()
        ) {
          return List.of(
            new EvaluatedCriterion(
              criterion,
              IntRangeWrapper
                .of(criterion.getValueToMatch().getIntRange())
                .contains(prop.get().getInt())
            )
          );
        }
      default:
        LOG.debug(
          "Unexpected operator {} found in criterion {}",
          criterion.getOperator(),
          criterion
        );
    }
    // Unknown Operator
    return List.of(new EvaluatedCriterion(criterion, false));
  }

  /**
   * a.b.c match a.b -> true
   * a.b match a.b.c -> false
   *
   * @param valueToMatch
   * @param propertyString
   * @return
   */
  boolean hierarchicalMatch(String propertyString, String valueToMatch) {
    return propertyString.startsWith(valueToMatch);
  }

  public Collection getKeys() {
    return configStore.getKeys();
  }

  public Collection getKeysOfConfigType(Prefab.ConfigType configType) {
    return configStore
      .getElements()
      .stream()
      .map(ConfigElement::getConfig)
      .filter(config -> config.getConfigType() == configType)
      .map(Prefab.Config::getKey)
      .collect(Collectors.toList());
  }

  public ConfigElement getRaw(String key) {
    return configStore.getElement(key);
  }

  public boolean containsKey(String key) {
    return configStore.containsKey(key);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy