diff --git a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java index cb869e69a..cd9f8f9a9 100644 --- a/src/main/java/com/hubspot/jinjava/JinjavaConfig.java +++ b/src/main/java/com/hubspot/jinjava/JinjavaConfig.java @@ -23,6 +23,8 @@ import com.hubspot.jinjava.el.JinjavaObjectUnwrapper; import com.hubspot.jinjava.el.JinjavaProcessors; import com.hubspot.jinjava.el.ObjectUnwrapper; +import com.hubspot.jinjava.features.FeatureConfig; +import com.hubspot.jinjava.features.Features; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.Context.Library; import com.hubspot.jinjava.interpret.InterpreterFactory; @@ -80,6 +82,8 @@ public class JinjavaConfig { private final boolean enablePreciseDivideFilter; private final ObjectMapper objectMapper; + private final Features features; + private final ObjectUnwrapper objectUnwrapper; private final JinjavaProcessors processors; @@ -140,6 +144,7 @@ private JinjavaConfig(Builder builder) { objectMapper = setupObjectMapper(builder.objectMapper); objectUnwrapper = builder.objectUnwrapper; processors = builder.processors; + features = new Features(builder.featureConfig); } private ObjectMapper setupObjectMapper(@Nullable ObjectMapper objectMapper) { @@ -288,6 +293,10 @@ public DateTimeProvider getDateTimeProvider() { return dateTimeProvider; } + public Features getFeatures() { + return features; + } + public static class Builder { private Charset charset = StandardCharsets.UTF_8; private Locale locale = Locale.ENGLISH; @@ -322,6 +331,7 @@ public static class Builder { private ObjectUnwrapper objectUnwrapper = new JinjavaObjectUnwrapper(); private JinjavaProcessors processors = JinjavaProcessors.newBuilder().build(); + private FeatureConfig featureConfig = FeatureConfig.newBuilder().build(); private Builder() {} @@ -508,6 +518,11 @@ public Builder withProcessors(JinjavaProcessors jinjavaProcessors) { return this; } + public Builder withFeatureConfig(FeatureConfig featureConfig) { + this.featureConfig = featureConfig; + return this; + } + public JinjavaConfig build() { return new JinjavaConfig(this); } diff --git a/src/main/java/com/hubspot/jinjava/features/DateTimeFeatureActivationStrategy.java b/src/main/java/com/hubspot/jinjava/features/DateTimeFeatureActivationStrategy.java new file mode 100644 index 000000000..364f84c32 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/features/DateTimeFeatureActivationStrategy.java @@ -0,0 +1,25 @@ +package com.hubspot.jinjava.features; + +import com.hubspot.jinjava.interpret.Context; +import java.time.LocalDateTime; + +public class DateTimeFeatureActivationStrategy implements FeatureActivationStrategy { + private final LocalDateTime activateAt; + + public static DateTimeFeatureActivationStrategy of(LocalDateTime activateAt) { + return new DateTimeFeatureActivationStrategy(activateAt); + } + + private DateTimeFeatureActivationStrategy(LocalDateTime activateAt) { + this.activateAt = activateAt; + } + + @Override + public boolean isActive(Context context) { + return LocalDateTime.now().isAfter(activateAt); + } + + public LocalDateTime getActivateAt() { + return activateAt; + } +} diff --git a/src/main/java/com/hubspot/jinjava/features/FeatureActivationStrategy.java b/src/main/java/com/hubspot/jinjava/features/FeatureActivationStrategy.java new file mode 100644 index 000000000..2abdd59de --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/features/FeatureActivationStrategy.java @@ -0,0 +1,7 @@ +package com.hubspot.jinjava.features; + +import com.hubspot.jinjava.interpret.Context; + +public interface FeatureActivationStrategy { + boolean isActive(Context context); +} diff --git a/src/main/java/com/hubspot/jinjava/features/FeatureConfig.java b/src/main/java/com/hubspot/jinjava/features/FeatureConfig.java new file mode 100644 index 000000000..1374745a7 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/features/FeatureConfig.java @@ -0,0 +1,34 @@ +package com.hubspot.jinjava.features; + +import com.google.common.collect.ImmutableMap; +import java.util.HashMap; +import java.util.Map; + +public class FeatureConfig { + Map features; + + private FeatureConfig(Map features) { + this.features = ImmutableMap.copyOf(features); + } + + public FeatureActivationStrategy getFeature(String name) { + return features.getOrDefault(name, FeatureStrategies.INACTIVE); + } + + public static FeatureConfig.Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + private final Map features = new HashMap<>(); + + public Builder add(String name, FeatureActivationStrategy strategy) { + features.put(name, strategy); + return this; + } + + public FeatureConfig build() { + return new FeatureConfig(features); + } + } +} diff --git a/src/main/java/com/hubspot/jinjava/features/FeatureStrategies.java b/src/main/java/com/hubspot/jinjava/features/FeatureStrategies.java new file mode 100644 index 000000000..5c4b679d9 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/features/FeatureStrategies.java @@ -0,0 +1,6 @@ +package com.hubspot.jinjava.features; + +public class FeatureStrategies { + public static final FeatureActivationStrategy INACTIVE = c -> false; + public static final FeatureActivationStrategy ACTIVE = c -> true; +} diff --git a/src/main/java/com/hubspot/jinjava/features/Features.java b/src/main/java/com/hubspot/jinjava/features/Features.java new file mode 100644 index 000000000..9920b9d0d --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/features/Features.java @@ -0,0 +1,19 @@ +package com.hubspot.jinjava.features; + +import com.hubspot.jinjava.interpret.Context; + +public class Features { + private final FeatureConfig featureConfig; + + public Features(FeatureConfig featureConfig) { + this.featureConfig = featureConfig; + } + + public boolean isActive(String featureName, Context context) { + return getActivationStrategy(featureName).isActive(context); + } + + public FeatureActivationStrategy getActivationStrategy(String featureName) { + return featureConfig.getFeature(featureName); + } +} diff --git a/src/test/java/com/hubspot/jinjava/FeaturesTest.java b/src/test/java/com/hubspot/jinjava/FeaturesTest.java new file mode 100644 index 000000000..8448c820b --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/FeaturesTest.java @@ -0,0 +1,74 @@ +package com.hubspot.jinjava; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.features.DateTimeFeatureActivationStrategy; +import com.hubspot.jinjava.features.FeatureConfig; +import com.hubspot.jinjava.features.FeatureStrategies; +import com.hubspot.jinjava.features.Features; +import com.hubspot.jinjava.interpret.Context; +import java.time.LocalDateTime; +import org.junit.Before; +import org.junit.Test; + +public class FeaturesTest { + private static final String ALWAYS_OFF = "alwaysOff"; + private static final String ALWAYS_ON = "alwaysOn"; + private static final String DATE_PAST = "datePast"; + private static final String DATE_FUTURE = "dateFuture"; + private static final String DELEGATING = "delegating"; + + private Features features; + + private boolean delegateActive = false; + + private Context context = new Context(); + + @Before + public void setUp() throws Exception { + features = + new Features( + FeatureConfig + .newBuilder() + .add(ALWAYS_OFF, FeatureStrategies.INACTIVE) + .add(ALWAYS_ON, FeatureStrategies.ACTIVE) + .add(DATE_PAST, DateTimeFeatureActivationStrategy.of(LocalDateTime.MIN)) + .add(DATE_FUTURE, DateTimeFeatureActivationStrategy.of(LocalDateTime.MAX)) + .add(DELEGATING, d -> delegateActive) + .build() + ); + } + + @Test + public void itHasEnabledFeature() { + assertThat(features.isActive(ALWAYS_ON, context)).isTrue(); + } + + @Test + public void itDoesNotHaveDisabledFeature() { + assertThat(features.isActive(ALWAYS_OFF, context)).isFalse(); + } + + @Test + public void itHasPastEnabledFeature() { + assertThat(features.isActive(DATE_PAST, context)).isTrue(); + } + + @Test + public void itDoesNotHaveFutureEnabledFeature() { + assertThat(features.isActive(DATE_FUTURE, context)).isFalse(); + } + + @Test + public void itUsesDelegate() { + delegateActive = false; + assertThat(features.isActive(DELEGATING, context)).isEqualTo(delegateActive); + delegateActive = true; + assertThat(features.isActive(DELEGATING, context)).isEqualTo(delegateActive); + } + + @Test + public void itDefaultsToFalse() { + assertThat(features.isActive("unknown", context)).isFalse(); + } +}