From 5731cce84e6533f0b4267c76f7e057196478874a Mon Sep 17 00:00:00 2001 From: chandresh pancholi Date: Mon, 25 Apr 2022 19:11:46 +0530 Subject: [PATCH] Litmus client library refactor --- Dockerfile.core | 4 +- Dockerfile.proxy | 4 +- litmus-cache/pom.xml | 4 +- litmus-client/pom.xml | 6 +- .../navi/medici/client/ClickStreamClient.java | 32 +++++--- .../client/ExperimentBackupHandlerFile.java | 3 +- .../medici/client/HttpExperimentFetcher.java | 81 +++++++++++-------- .../medici/client/OkHttpClientContainer.java | 28 +++++++ .../navi/medici/event/EventDispatcher.java | 10 ++- .../com/navi/medici/litmus/DefaultLitmus.java | 12 ++- .../navi/medici/strategy/ConstraintUtil.java | 38 ++++++++- .../constraints/ConstraintOperator.java | 8 ++ .../constraints/DateConstraintOperator.java | 30 +++++++ .../strategy/constraints/DateParser.java | 49 +++++++++++ .../constraints/NumberConstraintOperator.java | 65 +++++++++++++++ .../constraints/StringConstraintOperator.java | 71 ++++++++++++++++ .../com/navi/medici/util/VariantUtil.java | 12 +-- ...usExperimentBootstrapFileProviderTest.java | 10 +++ .../medici/client/ClickStreamClientTest.java | 71 ++++++++++++++++ .../client/HttpExperimentFetcherTest.java | 62 ++++++++++++++ .../medici/event/EventDispatcherTest.java | 75 +++++++++++++++++ .../java/com/navi/medici/utils/TestUtils.java | 28 +++++++ litmus-core/pom.xml | 19 +++-- .../com/navi/medici/config/SwaggerConfig.java | 18 +++++ .../controller/v1/ExperimentController.java | 6 ++ .../service/experiment/ExperimentService.java | 2 + .../experiment/ExperimentServiceImpl.java | 11 ++- .../src/main/resources/application.properties | 4 +- litmus-db/pom.xml | 6 +- litmus-liquibase/pom.xml | 4 +- litmus-mock/pom.xml | 8 +- litmus-model/pom.xml | 4 +- .../navi/medici/constraint/Constraint.java | 4 + .../java/com/navi/medici/enums/Operator.java | 12 ++- .../medici/request/v1/LitmusExperiment.java | 3 +- litmus-proxy/pom.xml | 8 +- .../controller/LitmusProxyController.java | 4 +- litmus-util/pom.xml | 4 +- pom.xml | 7 +- 39 files changed, 724 insertions(+), 103 deletions(-) create mode 100644 litmus-client/src/main/java/com/navi/medici/client/OkHttpClientContainer.java create mode 100644 litmus-client/src/main/java/com/navi/medici/strategy/constraints/ConstraintOperator.java create mode 100644 litmus-client/src/main/java/com/navi/medici/strategy/constraints/DateConstraintOperator.java create mode 100644 litmus-client/src/main/java/com/navi/medici/strategy/constraints/DateParser.java create mode 100644 litmus-client/src/main/java/com/navi/medici/strategy/constraints/NumberConstraintOperator.java create mode 100644 litmus-client/src/main/java/com/navi/medici/strategy/constraints/StringConstraintOperator.java create mode 100644 litmus-client/src/test/java/com/navi/medici/bootstrap/LitmusExperimentBootstrapFileProviderTest.java create mode 100644 litmus-client/src/test/java/com/navi/medici/client/ClickStreamClientTest.java create mode 100644 litmus-client/src/test/java/com/navi/medici/client/HttpExperimentFetcherTest.java create mode 100644 litmus-client/src/test/java/com/navi/medici/event/EventDispatcherTest.java create mode 100644 litmus-client/src/test/java/com/navi/medici/utils/TestUtils.java create mode 100644 litmus-core/src/main/java/com/navi/medici/config/SwaggerConfig.java diff --git a/Dockerfile.core b/Dockerfile.core index b7e5cc8..0aa42d3 100644 --- a/Dockerfile.core +++ b/Dockerfile.core @@ -1,5 +1,5 @@ FROM 193044292705.dkr.ecr.ap-south-1.amazonaws.com/common/maven:3.8.3-openjdk-17-slim as builder -ARG ARTIFACT_VERSION=2.0.0-SNAPSHOT +ARG ARTIFACT_VERSION=2.0.1-RELEASE RUN mkdir -p /build WORKDIR /build COPY . /build @@ -7,7 +7,7 @@ RUN mvn clean install -DskipTests RUN mvn clean verify -DskipTests -Dartifact.version=${ARTIFACT_VERSION} FROM 193044292705.dkr.ecr.ap-south-1.amazonaws.com/common/openjdk:17-slim-bullseye -ARG ARTIFACT_VERSION=2.0.0-SNAPSHOT +ARG ARTIFACT_VERSION=2.0.1-RELEASE RUN mkdir -p /usr/local RUN apt-get update -y && apt-get -y install fontconfig libpng-dev WORKDIR /usr/local/ diff --git a/Dockerfile.proxy b/Dockerfile.proxy index bfdc858..3d6f5a6 100644 --- a/Dockerfile.proxy +++ b/Dockerfile.proxy @@ -1,5 +1,5 @@ FROM 193044292705.dkr.ecr.ap-south-1.amazonaws.com/common/maven:3.8.3-openjdk-17-slim as builder -ARG ARTIFACT_VERSION=2.0.0-SNAPSHOT +ARG ARTIFACT_VERSION=2.0.1-RELEASE RUN mkdir -p /build WORKDIR /build COPY . /build @@ -7,7 +7,7 @@ RUN mvn clean install RUN mvn clean verify -DskipTests -Dartifact.version=${ARTIFACT_VERSION} FROM 193044292705.dkr.ecr.ap-south-1.amazonaws.com/common/openjdk:17-slim-bullseye -ARG ARTIFACT_VERSION=2.0.0-SNAPSHOT +ARG ARTIFACT_VERSION=2.0.1-RELEASE RUN mkdir -p /usr/local RUN apt-get update -y && apt-get -y install fontconfig libpng-dev WORKDIR /usr/local/ diff --git a/litmus-cache/pom.xml b/litmus-cache/pom.xml index 580f940..878741f 100644 --- a/litmus-cache/pom.xml +++ b/litmus-cache/pom.xml @@ -4,11 +4,11 @@ litmus com.navi.medici - 2.0.0-SNAPSHOT + 2.0.1-RELEASE litmus-cache - 2.0.0-SNAPSHOT + 2.0.1-RELEASE jar litmus-cache diff --git a/litmus-client/pom.xml b/litmus-client/pom.xml index 3a18d02..7772cbd 100644 --- a/litmus-client/pom.xml +++ b/litmus-client/pom.xml @@ -5,11 +5,11 @@ litmus com.navi.medici - 2.0.0-SNAPSHOT + 2.0.1-RELEASE litmus-client - 2.0.0-SNAPSHOT + 2.0.1-RELEASE jar litmus-client @@ -43,7 +43,7 @@ com.navi.medici litmus-model - 2.0.0-SNAPSHOT + 2.0.1-RELEASE diff --git a/litmus-client/src/main/java/com/navi/medici/client/ClickStreamClient.java b/litmus-client/src/main/java/com/navi/medici/client/ClickStreamClient.java index ed14b31..857d6a6 100644 --- a/litmus-client/src/main/java/com/navi/medici/client/ClickStreamClient.java +++ b/litmus-client/src/main/java/com/navi/medici/client/ClickStreamClient.java @@ -6,6 +6,7 @@ import com.navi.medici.config.LitmusConfig; import com.navi.medici.response.LitmusExperimentResponse; import com.navi.medici.util.JacksonUtils; import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; import lombok.extern.log4j.Log4j2; import okhttp3.MediaType; import okhttp3.OkHttpClient; @@ -18,28 +19,33 @@ public class ClickStreamClient { public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - private final LitmusConfig litmusConfig; - private final OkHttpClient client; + private final String clickStreamAPI; + private final String appName; + private final String vertical; + private final MeterRegistry meterRegistry; - public ClickStreamClient(LitmusConfig litmusConfig) { - this.litmusConfig = litmusConfig; - this.client = new OkHttpClient(); + public ClickStreamClient(String clickStreamAPI, String appName, + String vertical, MeterRegistry meterRegistry) { + this.clickStreamAPI = clickStreamAPI; + this.appName = appName; + this.vertical = vertical; + this.meterRegistry = meterRegistry; } public void publish(String experimentName, ClickStreamPayload payload) { String requestBody = JacksonUtils.objectToString(payload); Request request = new Request.Builder() - .url(this.litmusConfig.getClickStreamAPI()) + .url(clickStreamAPI) .post(RequestBody.create(JSON, requestBody)) .build(); - try(Response response = client.newCall(request).execute()) { + try(Response response = OkHttpClientContainer.getInstance().newCall(request).execute()) { Counter.builder("litmus_client_click_stream_event_ingestion") - .tag("vertical", litmusConfig.getVertical()) + .tag("vertical", vertical) .tag("status", String.valueOf(response.code())) .tag("experiment_name", experimentName) - .tag("app_name", litmusConfig.getAppName()) - .register(this.litmusConfig.getMeterRegistry()) + .tag("app_name", appName) + .register(meterRegistry) .increment(); if (response.code() < 300) { var responseBody = response.body(); @@ -51,11 +57,11 @@ public class ClickStreamClient { } } catch (Exception e) { Counter.builder("litmus_client_click_stream_event_ingestion") - .tag("vertical", litmusConfig.getVertical()) + .tag("vertical", vertical) .tag("experiment_name", experimentName) - .tag("app_name", litmusConfig.getAppName()) + .tag("app_name", appName) .tag("exception", e.getMessage()) - .register(this.litmusConfig.getMeterRegistry()) + .register(meterRegistry) .increment(); log.error("clickstream event ingestion failed. ", e); diff --git a/litmus-client/src/main/java/com/navi/medici/client/ExperimentBackupHandlerFile.java b/litmus-client/src/main/java/com/navi/medici/client/ExperimentBackupHandlerFile.java index 7266408..0ed4247 100644 --- a/litmus-client/src/main/java/com/navi/medici/client/ExperimentBackupHandlerFile.java +++ b/litmus-client/src/main/java/com/navi/medici/client/ExperimentBackupHandlerFile.java @@ -45,9 +45,8 @@ public class ExperimentBackupHandlerFile implements ExperimentBackupHandler { } catch (IOException | IllegalStateException e) { log.error(""); } - List emptyList = Collections.emptyList(); return LitmusExperimentCollection.builder() - .litmusExperiments(emptyList) + .litmusExperiments(Collections.emptyList()) .build(); } diff --git a/litmus-client/src/main/java/com/navi/medici/client/HttpExperimentFetcher.java b/litmus-client/src/main/java/com/navi/medici/client/HttpExperimentFetcher.java index ff8f17b..b9394dc 100644 --- a/litmus-client/src/main/java/com/navi/medici/client/HttpExperimentFetcher.java +++ b/litmus-client/src/main/java/com/navi/medici/client/HttpExperimentFetcher.java @@ -6,7 +6,9 @@ import com.navi.medici.response.LitmusExperimentCollection; import com.navi.medici.response.LitmusExperimentResponse; import com.navi.medici.response.LitmusResponse; import com.navi.medici.util.JacksonUtils; +import com.navi.medici.util.LitmusURLs; import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; import java.net.URL; import lombok.extern.log4j.Log4j2; import okhttp3.OkHttpClient; @@ -15,31 +17,41 @@ import okhttp3.Response; @Log4j2 public class HttpExperimentFetcher implements ExperimentFetcher { - private final OkHttpClient client; - private final URL experimentUrl; - private final URL segmentIdUrl; - private final LitmusConfig litmusConfig; - public HttpExperimentFetcher(LitmusConfig litmusConfig) { - this.client = new OkHttpClient(); - this.experimentUrl = - litmusConfig.getLitmusURLs() - .getLitmusExperimentsURL(litmusConfig.getProjectName(), litmusConfig.getNamePrefix()); - this.segmentIdUrl = litmusConfig.getLitmusURLs().getSegmentIdURL(); - this.litmusConfig = litmusConfig; + private final LitmusURLs litmusURLs; + private final String appName; + private final String projectNamePrefix; + private final String projectName; + private final String vertical; + private final MeterRegistry meterRegistry; + + public HttpExperimentFetcher(LitmusURLs litmusURLs, + String appName, + String projectNamePrefix, + String projectName, + String vertical, + MeterRegistry meterRegistry) { + this.litmusURLs = litmusURLs; + this.appName = appName; + this.projectNamePrefix = projectNamePrefix; + this.projectName = projectName; + this.vertical = vertical; + this.meterRegistry = meterRegistry; } @Override public LitmusExperimentResponse fetchExperiments(String vertical, Long pollingTime) throws LitmusException { + URL experimentUrl = litmusURLs + .getLitmusExperimentsURL(projectName, projectNamePrefix); Request request = new Request.Builder() - .url(String.format("%s?vertical=%s&polling_time=%s", this.experimentUrl, vertical, pollingTime)) + .url(String.format("%s?vertical=%s&polling_time=%s", experimentUrl, vertical, pollingTime)) .build(); - try (Response response = client.newCall(request).execute()) { + try (Response response = OkHttpClientContainer.getInstance().newCall(request).execute()) { Counter.builder("litmus_client_fetch_experiment_polling") - .tag("vertical", vertical) - .tag("app_name", litmusConfig.getAppName()) - .register(this.litmusConfig.getMeterRegistry()) + .tag("vertical", this.vertical) + .tag("app_name", this.appName) + .register(this.meterRegistry) .increment(); if (response.code() < 300) { @@ -52,27 +64,27 @@ public class HttpExperimentFetcher implements ExperimentFetcher { .tag("vertical", vertical) .tag("status", String.valueOf(response.code())) .tag("experiment_state", LitmusExperimentResponse.Status.CHANGED.name()) - .tag("app_name", litmusConfig.getAppName()) - .register(this.litmusConfig.getMeterRegistry()) + .tag("app_name", this.appName) + .register(this.meterRegistry) .increment(); return new LitmusExperimentResponse(LitmusExperimentResponse.Status.CHANGED, experiments); } else if (response.code() == 304) { Counter.builder("litmus_client_fetch_experiment_polling_request_status") .tag("vertical", vertical) - .tag("app_name", litmusConfig.getAppName()) + .tag("app_name", this.appName) .tag("status", String.valueOf(response.code())) .tag("experiment_state", LitmusExperimentResponse.Status.NOT_CHANGED.name()) - .register(this.litmusConfig.getMeterRegistry()) + .register(this.meterRegistry) .increment(); return new LitmusExperimentResponse(LitmusExperimentResponse.Status.NOT_CHANGED, response.code()); } else { Counter.builder("litmus_client_fetch_experiment_polling_request_status") - .tag("vertical", vertical) - .tag("app_name", litmusConfig.getAppName()) + .tag("vertical", this.vertical) + .tag("app_name", this.appName) .tag("status", String.valueOf(response.code())) .tag("experiment_state", LitmusExperimentResponse.Status.NOT_CHANGED.name()) - .register(this.litmusConfig.getMeterRegistry()) + .register(this.meterRegistry) .increment(); return new LitmusExperimentResponse(LitmusExperimentResponse.Status.UNAVAILABLE, response.code()); @@ -80,37 +92,38 @@ public class HttpExperimentFetcher implements ExperimentFetcher { } catch (Exception e) { Counter.builder("litmus_client_fetch_experiment_polling_request_failed") - .tag("vertical", vertical) - .tag("app_name", litmusConfig.getAppName()) - .register(this.litmusConfig.getMeterRegistry()) + .tag("vertical", this.vertical) + .tag("app_name", this.appName) + .register(this.meterRegistry) .increment(); throw new LitmusException("fetch experiments failed.", e); } } public LitmusResponse segmentIdExists(String segmentName, String id) { + URL segmentIdUrl = this.litmusURLs.getSegmentIdURL(); Request request = new Request.Builder() - .url(String.format("%s?segment_name=%s&id=%s", this.segmentIdUrl, segmentName, id)) + .url(String.format("%s?segment_name=%s&id=%s", segmentIdUrl, segmentName, id)) .build(); - try (Response response = client.newCall(request).execute()) { + try (Response response = OkHttpClientContainer.getInstance().newCall(request).execute()) { var responseBody = response.body(); assert responseBody != null; Counter.builder("litmus_client_segment_id_exist_request") - .tag("vertical", litmusConfig.getVertical()) - .tag("app_name", litmusConfig.getAppName()) + .tag("vertical", this.vertical) + .tag("app_name", this.appName) .tag("status", String.valueOf(response.code())) - .register(this.litmusConfig.getMeterRegistry()) + .register(this.meterRegistry) .increment(); return JacksonUtils.stringToObject(responseBody.string(), LitmusResponse.class); } catch (Exception e) { Counter.builder("litmus_client_segment_id_exist_request_failed") - .tag("vertical", litmusConfig.getVertical()) + .tag("vertical", this.vertical) .tag("segment_name", segmentName) - .tag("app_name", litmusConfig.getAppName()) + .tag("app_name", this.appName) .tag("exception", e.getMessage()) - .register(this.litmusConfig.getMeterRegistry()) + .register(this.meterRegistry) .increment(); throw new LitmusException("segment name to id matching exists.", e); diff --git a/litmus-client/src/main/java/com/navi/medici/client/OkHttpClientContainer.java b/litmus-client/src/main/java/com/navi/medici/client/OkHttpClientContainer.java new file mode 100644 index 0000000..7f4ba94 --- /dev/null +++ b/litmus-client/src/main/java/com/navi/medici/client/OkHttpClientContainer.java @@ -0,0 +1,28 @@ +package com.navi.medici.client; + +import java.util.concurrent.TimeUnit; +import okhttp3.OkHttpClient; + +public class OkHttpClientContainer { + + private static OkHttpClient okHttpClient; + + private OkHttpClientContainer() { + } + + public static OkHttpClient getInstance() { + if (okHttpClient == null) { + synchronized (OkHttpClientContainer.class) { + if (okHttpClient == null) { + okHttpClient = new OkHttpClient.Builder() + .readTimeout(10, TimeUnit.MILLISECONDS) + .callTimeout(1, TimeUnit.SECONDS) + .connectTimeout(1, TimeUnit.SECONDS) + .build(); + } + } + } + return okHttpClient; + } + +} diff --git a/litmus-client/src/main/java/com/navi/medici/event/EventDispatcher.java b/litmus-client/src/main/java/com/navi/medici/event/EventDispatcher.java index 164ffe2..60f0f7e 100644 --- a/litmus-client/src/main/java/com/navi/medici/event/EventDispatcher.java +++ b/litmus-client/src/main/java/com/navi/medici/event/EventDispatcher.java @@ -12,12 +12,15 @@ import com.navi.medici.util.JacksonUtils; import com.navi.medici.variants.Variant; import java.util.List; import java.util.concurrent.CompletableFuture; +import lombok.extern.log4j.Log4j2; +@Log4j2 public class EventDispatcher { private final ClickStreamClient clickStreamClient; public EventDispatcher(LitmusConfig litmusConfig) { - this.clickStreamClient = new ClickStreamClient(litmusConfig); + this.clickStreamClient = new ClickStreamClient(litmusConfig.getClickStreamAPI(), litmusConfig.getAppName(), + litmusConfig.getVertical(), litmusConfig.getMeterRegistry()); } public void publish(LitmusContext litmusContext, LitmusExperiment litmusExperiment, Boolean result, Variant variant) { @@ -25,10 +28,10 @@ public class EventDispatcher { var clickStreamPayload = clickStreamPayloadOptional .map(payload -> JacksonUtils.stringToObject(payload, ClickStreamPayload.class)) - .orElse(null); + .orElse(ClickStreamPayload.builder().build()); String variantName = variant != null ? variant.getName() : null; - if (clickStreamPayload != null && litmusExperiment != null) { + if (litmusExperiment != null) { var litmusExperimentEvent = LitmusExperimentEvent.builder() .experimentId(litmusExperiment.getExperimentId()) .experimentName(litmusExperiment.getName()) @@ -52,7 +55,6 @@ public class EventDispatcher { clickStreamClient.publish(litmusExperiment.getName(), clickStreamPayload); return null; }); - } } } diff --git a/litmus-client/src/main/java/com/navi/medici/litmus/DefaultLitmus.java b/litmus-client/src/main/java/com/navi/medici/litmus/DefaultLitmus.java index c839409..6f61292 100644 --- a/litmus-client/src/main/java/com/navi/medici/litmus/DefaultLitmus.java +++ b/litmus-client/src/main/java/com/navi/medici/litmus/DefaultLitmus.java @@ -50,7 +50,8 @@ public class DefaultLitmus implements Litmus { private static ExperimentRepository defaultExperimentRepository(LitmusConfig litmusConfig) { return new LitmusExperimentRepository( litmusConfig, - new HttpExperimentFetcher(litmusConfig), + new HttpExperimentFetcher(litmusConfig.getLitmusURLs(), litmusConfig.getAppName(), litmusConfig.getNamePrefix(), + litmusConfig.getProjectName(), litmusConfig.getVertical(), litmusConfig.getMeterRegistry()), new ExperimentBackupHandlerFile(litmusConfig)) { }; } @@ -175,8 +176,13 @@ public class DefaultLitmus implements Litmus { List BUILTIN_STRATEGIES = Arrays.asList( new DefaultStrategy(), - new UserWithIdStrategy(new HttpExperimentFetcher(litmusConfig)), - new DeviceWithIdStrategy(new HttpExperimentFetcher(litmusConfig)), + + new UserWithIdStrategy(new HttpExperimentFetcher(litmusConfig.getLitmusURLs(), litmusConfig.getAppName(), litmusConfig.getNamePrefix(), + litmusConfig.getProjectName(), litmusConfig.getVertical(), litmusConfig.getMeterRegistry())), + + new DeviceWithIdStrategy(new HttpExperimentFetcher(litmusConfig.getLitmusURLs(), litmusConfig.getAppName(), litmusConfig.getNamePrefix(), + litmusConfig.getProjectName(), litmusConfig.getVertical(), litmusConfig.getMeterRegistry())), + new FlexibleRolloutStrategy()); BUILTIN_STRATEGIES.forEach(strategy -> map.put(strategy.getName(), strategy)); diff --git a/litmus-client/src/main/java/com/navi/medici/strategy/ConstraintUtil.java b/litmus-client/src/main/java/com/navi/medici/strategy/ConstraintUtil.java index ab5a15a..6e62ac9 100644 --- a/litmus-client/src/main/java/com/navi/medici/strategy/ConstraintUtil.java +++ b/litmus-client/src/main/java/com/navi/medici/strategy/ConstraintUtil.java @@ -1,12 +1,48 @@ package com.navi.medici.strategy; +import static com.navi.medici.enums.Operator.DATE_AFTER; +import static com.navi.medici.enums.Operator.DATE_BEFORE; +import static com.navi.medici.enums.Operator.IN; +import static com.navi.medici.enums.Operator.NOT_IN; +import static com.navi.medici.enums.Operator.NUM_EQ; +import static com.navi.medici.enums.Operator.NUM_GT; +import static com.navi.medici.enums.Operator.NUM_GTE; +import static com.navi.medici.enums.Operator.NUM_LT; +import static com.navi.medici.enums.Operator.NUM_LTE; +import static com.navi.medici.enums.Operator.STR_CONTAINS; +import static com.navi.medici.enums.Operator.STR_ENDS_WITH; +import static com.navi.medici.enums.Operator.STR_STARTS_WITH; + import com.navi.medici.constraint.Constraint; import com.navi.medici.context.LitmusContext; import com.navi.medici.enums.Operator; +import com.navi.medici.strategy.constraints.ConstraintOperator; +import com.navi.medici.strategy.constraints.DateConstraintOperator; +import com.navi.medici.strategy.constraints.NumberConstraintOperator; +import com.navi.medici.strategy.constraints.StringConstraintOperator; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.Optional; public class ConstraintUtil { + private static final Map operators = new HashMap<>(); + + static { + operators.put(STR_CONTAINS, new StringConstraintOperator(Locale.ROOT)); + operators.put(STR_ENDS_WITH, new StringConstraintOperator(Locale.ROOT)); + operators.put(STR_STARTS_WITH, new StringConstraintOperator(Locale.ROOT)); + operators.put(IN, new StringConstraintOperator(Locale.ROOT)); + operators.put(NOT_IN, new StringConstraintOperator(Locale.ROOT)); + operators.put(NUM_LT, new NumberConstraintOperator()); + operators.put(NUM_LTE, new NumberConstraintOperator()); + operators.put(NUM_EQ, new NumberConstraintOperator()); + operators.put(NUM_GTE, new NumberConstraintOperator()); + operators.put(NUM_GT, new NumberConstraintOperator()); + operators.put(DATE_BEFORE, new DateConstraintOperator()); + operators.put(DATE_AFTER, new DateConstraintOperator()); + } public static boolean validate(List constraints, LitmusContext context) { if (constraints != null && constraints.size() > 0) { @@ -21,6 +57,6 @@ public class ConstraintUtil { boolean isIn = contextValue.isPresent() && constraint.getValues().contains(contextValue.get().trim()); - return (constraint.getOperator() == Operator.IN) == isIn; + return (constraint.getOperator() == IN) == isIn; } } diff --git a/litmus-client/src/main/java/com/navi/medici/strategy/constraints/ConstraintOperator.java b/litmus-client/src/main/java/com/navi/medici/strategy/constraints/ConstraintOperator.java new file mode 100644 index 0000000..a8e60cc --- /dev/null +++ b/litmus-client/src/main/java/com/navi/medici/strategy/constraints/ConstraintOperator.java @@ -0,0 +1,8 @@ +package com.navi.medici.strategy.constraints; + +import com.navi.medici.constraint.Constraint; +import com.navi.medici.context.LitmusContext; + +public interface ConstraintOperator { + boolean evaluate(Constraint constraint, LitmusContext context); +} diff --git a/litmus-client/src/main/java/com/navi/medici/strategy/constraints/DateConstraintOperator.java b/litmus-client/src/main/java/com/navi/medici/strategy/constraints/DateConstraintOperator.java new file mode 100644 index 0000000..5815ac8 --- /dev/null +++ b/litmus-client/src/main/java/com/navi/medici/strategy/constraints/DateConstraintOperator.java @@ -0,0 +1,30 @@ +package com.navi.medici.strategy.constraints; + +import com.navi.medici.constraint.Constraint; +import com.navi.medici.context.LitmusContext; +import com.navi.medici.enums.Operator; +import java.time.ZonedDateTime; + +public class DateConstraintOperator implements ConstraintOperator { + + @Override + public boolean evaluate(Constraint constraint, LitmusContext context) { + try { + ZonedDateTime value = DateParser.parseDate(constraint.getValue()); + return eval(constraint.getOperator(), value, ZonedDateTime.now()); + } catch (Exception e) { + return false; + } + } + + private boolean eval(Operator op, ZonedDateTime value, ZonedDateTime toMatch) { + switch (op) { + case DATE_AFTER: + return toMatch.isAfter(value); + case DATE_BEFORE: + return toMatch.isBefore(value); + default: + return false; + } + } +} diff --git a/litmus-client/src/main/java/com/navi/medici/strategy/constraints/DateParser.java b/litmus-client/src/main/java/com/navi/medici/strategy/constraints/DateParser.java new file mode 100644 index 0000000..c46bfab --- /dev/null +++ b/litmus-client/src/main/java/com/navi/medici/strategy/constraints/DateParser.java @@ -0,0 +1,49 @@ +package com.navi.medici.strategy.constraints; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class DateParser { + private static final List formatters = new ArrayList<>(); + + static { + formatters.add(DateTimeFormatter.ISO_INSTANT); + formatters.add(DateTimeFormatter.ISO_DATE_TIME); + formatters.add(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + formatters.add(DateTimeFormatter.ISO_ZONED_DATE_TIME); + } + + public static ZonedDateTime parseDate(String date) { + if (date != null && date.length() > 0) { + return formatters.stream() + .map( + f -> { + try { + return ZonedDateTime.parse(date, f); + } catch (DateTimeParseException dateTimeParseException) { + return null; + } + }) + .filter(Objects::nonNull) + .findFirst() + .orElseGet( + () -> { + try { + return LocalDateTime.parse( + date, DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .atZone(ZoneOffset.UTC); + } catch (DateTimeParseException dateTimeParseException) { + return null; + } + }); + } else { + return null; + } + } +} diff --git a/litmus-client/src/main/java/com/navi/medici/strategy/constraints/NumberConstraintOperator.java b/litmus-client/src/main/java/com/navi/medici/strategy/constraints/NumberConstraintOperator.java new file mode 100644 index 0000000..168e777 --- /dev/null +++ b/litmus-client/src/main/java/com/navi/medici/strategy/constraints/NumberConstraintOperator.java @@ -0,0 +1,65 @@ +package com.navi.medici.strategy.constraints; + +import com.navi.medici.constraint.Constraint; +import com.navi.medici.context.LitmusContext; +import com.navi.medici.enums.Operator; +import java.util.Objects; + +public class NumberConstraintOperator implements ConstraintOperator { + + @Override + public boolean evaluate(Constraint constraint, LitmusContext context) { + return context.getByName(constraint.getContextName()) + .map( + cVal -> { + try { + return Double.parseDouble(cVal); + } catch (NumberFormatException nfe) { + return null; + } + }) + .map( + cVal -> { + try { + if (constraint.getValues().size() > 0) { + return constraint.getValues().stream() + .map( + v -> { + try { + return Double.parseDouble(v); + } catch (NumberFormatException nfe) { + return null; + } + }) + .filter(Objects::nonNull) + .anyMatch(v -> eval(constraint.getOperator(), v, cVal)); + } else if (constraint.getValue() != null + && constraint.getValue().length() > 0) { + Double value = Double.parseDouble(constraint.getValue()); + return eval(constraint.getOperator(), value, cVal); + } else { + return null; + } + } catch (NumberFormatException nfe) { + return null; + } + }) + .orElse(false); + } + private boolean eval(Operator operator, Double value, Double contextValue) { + switch (operator) { + case NUM_LT: + return contextValue < value; + case NUM_LTE: + return contextValue <= value; + case NUM_EQ: + return contextValue.equals(value); + case NUM_GTE: + return contextValue >= value; + case NUM_GT: + return contextValue > value; + default: + return false; + } + } +} diff --git a/litmus-client/src/main/java/com/navi/medici/strategy/constraints/StringConstraintOperator.java b/litmus-client/src/main/java/com/navi/medici/strategy/constraints/StringConstraintOperator.java new file mode 100644 index 0000000..aed3e9f --- /dev/null +++ b/litmus-client/src/main/java/com/navi/medici/strategy/constraints/StringConstraintOperator.java @@ -0,0 +1,71 @@ +package com.navi.medici.strategy.constraints; + +import com.navi.medici.constraint.Constraint; +import com.navi.medici.context.LitmusContext; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +public class StringConstraintOperator implements ConstraintOperator { + + private Locale comparisonLocale; + + public StringConstraintOperator(Locale comparisonLocale) { + this.comparisonLocale = comparisonLocale; + } + + @Override + public boolean evaluate(Constraint constraint, LitmusContext context) { + List values = constraint.getValues(); + Optional contextValue = context.getByName(constraint.getContextName()); + boolean caseInsensitive = constraint.isCaseInsensitive(); + switch (constraint.getOperator()) { + case IN: + return isIn(values, contextValue, caseInsensitive); + case NOT_IN: + return !isIn(values, contextValue, caseInsensitive); + case STR_CONTAINS: + return contains(values, contextValue, caseInsensitive); + case STR_STARTS_WITH: + return startsWith(values, contextValue, caseInsensitive); + case STR_ENDS_WITH: + return endsWith(values, contextValue, caseInsensitive); + default: + return false; + } + } + + private boolean endsWith( + List values, Optional contextValue, boolean caseInsensitive) { + return contextValue + .map(c -> values.stream() + .anyMatch(v -> caseInsensitive + ? c.toLowerCase(comparisonLocale).endsWith(v.toLowerCase(comparisonLocale)) : c.endsWith(v))) + .orElse(false); + } + + private boolean startsWith( + List values, Optional contextValue, boolean caseInsensitive) { + return contextValue + .map( + c -> values.stream() + .anyMatch(v -> caseInsensitive + ? v.toLowerCase(comparisonLocale).startsWith(c.toLowerCase(comparisonLocale)) : c.startsWith(v))) + .orElse(false); + } + + private boolean contains( + List values, Optional contextValue, boolean caseInsensitive) { + return contextValue + .map(c -> values.stream() + .anyMatch(v -> caseInsensitive + ? c.toLowerCase(comparisonLocale).contains(v.toLowerCase(comparisonLocale)) : c.contains(v))) + .orElse(false); + } + + private boolean isIn(List values, Optional value, boolean caseInsensitive) { + return value.map(v -> values.stream() + .anyMatch(c -> caseInsensitive ? c.equalsIgnoreCase(v) : c.equals(v))) + .orElse(false); + } +} diff --git a/litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java b/litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java index d37085a..2c05012 100644 --- a/litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java +++ b/litmus-client/src/main/java/com/navi/medici/util/VariantUtil.java @@ -80,16 +80,16 @@ public final class VariantUtil { } public static Variant selectVariant(LitmusExperiment litmusExperiment, LitmusContext context, Variant defaultVariant) { - if (litmusExperiment == null || StringUtils.isBlank(litmusExperiment.getVariants())) { + if (litmusExperiment.getVariants().isEmpty()) { return defaultVariant; } - List variants = JacksonUtils.stringToListObject(litmusExperiment.getVariants(), VariantDefinition.class); - int totalWeight = variants.stream().mapToInt(VariantDefinition::getWeight).sum(); +// List variants = JacksonUtils.stringToListObject(litmusExperiment.getVariants(), VariantDefinition.class); + int totalWeight = litmusExperiment.getVariants().stream().mapToInt(VariantDefinition::getWeight).sum(); if (totalWeight == 0) { return defaultVariant; } - Optional variantOverride = getOverride(variants, context); + Optional variantOverride = getOverride(litmusExperiment.getVariants(), context); if (variantOverride.isPresent()) { var variantDefinition = variantOverride.get(); return Variant.builder() @@ -100,7 +100,7 @@ public final class VariantUtil { .build(); } Optional customStickiness = - variants.stream() + litmusExperiment.getVariants().stream() .filter(f -> f.getStickiness() != null) .map(VariantDefinition::getStickiness) .findFirst(); @@ -109,7 +109,7 @@ public final class VariantUtil { getSeed(context, customStickiness), litmusExperiment.getName(), totalWeight); int counter = 0; - for (final VariantDefinition definition : variants) { + for (final VariantDefinition definition : litmusExperiment.getVariants()) { if (definition.getWeight() != 0) { counter += definition.getWeight(); if (counter >= target) { diff --git a/litmus-client/src/test/java/com/navi/medici/bootstrap/LitmusExperimentBootstrapFileProviderTest.java b/litmus-client/src/test/java/com/navi/medici/bootstrap/LitmusExperimentBootstrapFileProviderTest.java new file mode 100644 index 0000000..f350985 --- /dev/null +++ b/litmus-client/src/test/java/com/navi/medici/bootstrap/LitmusExperimentBootstrapFileProviderTest.java @@ -0,0 +1,10 @@ +package com.navi.medici.bootstrap; + +import junit.framework.TestCase; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class LitmusExperimentBootstrapFileProviderTest { + +} \ No newline at end of file diff --git a/litmus-client/src/test/java/com/navi/medici/client/ClickStreamClientTest.java b/litmus-client/src/test/java/com/navi/medici/client/ClickStreamClientTest.java new file mode 100644 index 0000000..049c4fd --- /dev/null +++ b/litmus-client/src/test/java/com/navi/medici/client/ClickStreamClientTest.java @@ -0,0 +1,71 @@ +package com.navi.medici.client; + +import static com.navi.medici.utils.TestUtils.buildLitmusConfig; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.navi.medici.clickstream.ClickStreamPayload; +import com.navi.medici.clickstream.ClickStreamPayload.App; +import com.navi.medici.config.LitmusConfig; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import java.util.List; +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ClickStreamClientTest { + + @Mock + LitmusConfig litmusConfig; + + @Mock + OkHttpClient okHttpClient; + + @Mock + MeterRegistry meterRegistry; + + ClickStreamClient client; + + + @Test + public void testEventIngestion() throws Exception { + var litmusConfig = buildLitmusConfig(); + + var clickStreamPayload = ClickStreamPayload.builder() + .clickStreamEvent(List.of()) + .app(App.builder().build()) + .build(); + client = new ClickStreamClient(litmusConfig.getClickStreamAPI(), litmusConfig.getAppName(), + litmusConfig.getVertical(), litmusConfig.getMeterRegistry()); + + final Response response = new Response.Builder() + .request(new Request.Builder().url("https://example.com").build()) + .protocol(Protocol.HTTP_1_1) + .code(200).message("").body( + ResponseBody.create( + MediaType.parse("application/json"), + "serializedBody" + )) + .build(); + + final Call remoteCall = mock(Call.class); + + when(remoteCall.execute()).thenReturn(response); + + client.publish("test-experiment", clickStreamPayload); + } + + +} \ No newline at end of file diff --git a/litmus-client/src/test/java/com/navi/medici/client/HttpExperimentFetcherTest.java b/litmus-client/src/test/java/com/navi/medici/client/HttpExperimentFetcherTest.java new file mode 100644 index 0000000..3cb8101 --- /dev/null +++ b/litmus-client/src/test/java/com/navi/medici/client/HttpExperimentFetcherTest.java @@ -0,0 +1,62 @@ +package com.navi.medici.client; + +import static com.navi.medici.utils.TestUtils.buildLitmusConfig; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.navi.medici.config.LitmusConfig; +import io.micrometer.core.instrument.MeterRegistry; +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class HttpExperimentFetcherTest { + @Mock + LitmusConfig litmusConfig; + + @Mock + OkHttpClient okHttpClient; + + @Mock + MeterRegistry meterRegistry; + + HttpExperimentFetcher httpExperimentFetcher; + + + @Test + public void fetchExperiments() throws Exception { + var litmusConfig = buildLitmusConfig(); + httpExperimentFetcher = new HttpExperimentFetcher(litmusConfig.getLitmusURLs(), litmusConfig.getAppName(), litmusConfig.getNamePrefix(), + litmusConfig.getProjectName(), litmusConfig.getVertical(), litmusConfig.getMeterRegistry()); + final Response response = new Response.Builder() + .request(new Request.Builder().url("https://example.com/litmus-core/v1/experiments?vertical=test-experiment&polling_time=20").build()) + .protocol(Protocol.HTTP_1_1) + .code(200).message("").body( + ResponseBody.create( + MediaType.parse("application/json"), + "serializedBody" + )) + .build(); + + final Call remoteCall = mock(Call.class); + when(okHttpClient.newCall(any())).thenReturn(remoteCall); + + when(okHttpClient.newCall(any()).execute()).thenReturn(response); + + var litmusResponse = httpExperimentFetcher.fetchExperiments("test-experiment", 20L ); + } + +} \ No newline at end of file diff --git a/litmus-client/src/test/java/com/navi/medici/event/EventDispatcherTest.java b/litmus-client/src/test/java/com/navi/medici/event/EventDispatcherTest.java new file mode 100644 index 0000000..b961199 --- /dev/null +++ b/litmus-client/src/test/java/com/navi/medici/event/EventDispatcherTest.java @@ -0,0 +1,75 @@ +package com.navi.medici.event; + +import static com.navi.medici.utils.TestUtils.buildLitmusContext; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.navi.medici.clickstream.ClickStreamPayload; +import com.navi.medici.client.ClickStreamClient; +import com.navi.medici.config.LitmusConfig; +import com.navi.medici.context.LitmusContext; +import com.navi.medici.enums.ExperimentType; +import com.navi.medici.request.v1.LitmusExperiment; +import java.util.concurrent.CompletableFuture; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EventDispatcherTest { + + @Mock + ClickStreamClient client; + + @Mock + LitmusConfig litmusConfig; + + @InjectMocks + EventDispatcher eventDispatcher; + + @Test + public void testEventDispatchWithoutClickStreamPayload() { + LitmusContext litmusContext = LitmusContext.builder() + .appName("test-app") + .userId("test-user-id") + .appVersionCode("200") + .osType("android") + .clickStreamPayload(null) + .build(); + + LitmusExperiment litmusExperiment = LitmusExperiment.builder() + .experimentId("test-experiment-id") + .vertical("PL") + .enabled(true) + .name("test-experiment-name") + .type(ExperimentType.EXPERIMENT) + .build(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ClickStreamPayload.class); + eventDispatcher.publish(litmusContext, litmusExperiment, false, null); + } + + @Test + public void testEventDispatchWithClickStreamPayload() { + + LitmusContext litmusContext = buildLitmusContext(); + + LitmusExperiment litmusExperiment = LitmusExperiment.builder() + .experimentId("test-experiment-id") + .vertical("PL") + .enabled(true) + .name("test-experiment-name") + .type(ExperimentType.EXPERIMENT) + .build(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ClickStreamPayload.class); + eventDispatcher.publish(litmusContext, litmusExperiment, false, null); + } +} \ No newline at end of file diff --git a/litmus-client/src/test/java/com/navi/medici/utils/TestUtils.java b/litmus-client/src/test/java/com/navi/medici/utils/TestUtils.java new file mode 100644 index 0000000..1fbebe9 --- /dev/null +++ b/litmus-client/src/test/java/com/navi/medici/utils/TestUtils.java @@ -0,0 +1,28 @@ +package com.navi.medici.utils; + +import com.navi.medici.config.LitmusConfig; +import com.navi.medici.context.LitmusContext; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +public class TestUtils { + public static LitmusConfig buildLitmusConfig() { + return LitmusConfig.builder() + .meterRegistry(new SimpleMeterRegistry()) + .clickStreamAPI("https://example.com") + .vertical("PL") + .appName("test-app") + .litmusAPI("https://example.com/litmus-core/v1") + .build(); + } + + public static LitmusContext buildLitmusContext() { + var metadata = "{\"app\":{\"name\":\"test-name\",\"version\":\"test-version\"},\"device\":null,\"os\":{\"name\":\"test-os\",\"version\":null,\"api_version\":\"test-api-version\"},\"location\":null,\"network\":null,\"session\":null,\"user\":null}"; + return LitmusContext.builder() + .appName("test-app") + .userId("test-user-id") + .appVersionCode("200") + .osType("android") + .clickStreamPayload(metadata) + .build(); + } +} diff --git a/litmus-core/pom.xml b/litmus-core/pom.xml index c256607..1e8209a 100644 --- a/litmus-core/pom.xml +++ b/litmus-core/pom.xml @@ -5,11 +5,11 @@ litmus com.navi.medici - 2.0.0-SNAPSHOT + 2.0.1-RELEASE litmus-core - 2.0.0-SNAPSHOT + 2.0.1-RELEASE jar litmus-core @@ -31,25 +31,25 @@ com.navi.medici litmus-model - 2.0.0-SNAPSHOT + 2.0.1-RELEASE com.navi.medici litmus-db - 2.0.0-SNAPSHOT + 2.0.1-RELEASE com.navi.medici litmus-cache - 2.0.0-SNAPSHOT + 2.0.1-RELEASE com.navi.medici litmus-util - 2.0.0-SNAPSHOT + 2.0.1-RELEASE @@ -86,6 +86,13 @@ 5.5.2 + + org.springdoc + springdoc-openapi-ui + 1.6.8 + + + diff --git a/litmus-core/src/main/java/com/navi/medici/config/SwaggerConfig.java b/litmus-core/src/main/java/com/navi/medici/config/SwaggerConfig.java new file mode 100644 index 0000000..05181d8 --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/config/SwaggerConfig.java @@ -0,0 +1,18 @@ +package com.navi.medici.config; + + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + +// @Bean +// public Docket api() { +// return new Docket(DocumentationType.SWAGGER_2) +// .select() +// .apis(RequestHandlerSelectors.any()) +// .paths(regex("/litmus-core/.*")) +// .build(); +// } + +} diff --git a/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java b/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java index 222177e..4cf602a 100644 --- a/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java +++ b/litmus-core/src/main/java/com/navi/medici/controller/v1/ExperimentController.java @@ -40,6 +40,12 @@ public class ExperimentController { return experimentService.fetchAllExperiments(); } + @GetMapping("/fetch") + @Timed(value = "litmus.core.fetch.experiment.by.name", percentiles = {0.95, 0.99}) + public LitmusExperiment fetchExperimentsByName(@RequestParam("experiment_name") String experimentName) { + return experimentService.fetchExperimentByName(experimentName); + } + @GetMapping(value = "/vertical") @Timed(value = "litmus.fetch.vertical.experiment", percentiles = {0.95, 0.99}) public ResponseEntity fetchExperimentsForAVertical(@RequestParam("vertical") String vertical, diff --git a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java index 2d35089..d21a72d 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java +++ b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java @@ -11,6 +11,8 @@ public interface ExperimentService { LitmusExperimentCollection fetchAllExperiments(); + LitmusExperiment fetchExperimentByName(String experimentName); + void attachVariants(String experimentId, List variantDefinitions); LitmusExperimentCollection fetchAllExperimentsForVerticals(String vertical, Long pollingTime); diff --git a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java index f0f9fb4..3033b33 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java +++ b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java @@ -12,6 +12,7 @@ import com.navi.medici.strategy.ActivationStrategy; import com.navi.medici.util.JacksonUtils; import com.navi.medici.variants.VariantDefinition; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; import lombok.extern.log4j.Log4j2; @@ -59,6 +60,12 @@ public record ExperimentServiceImpl(IExperimentQuery experimentQuery, .build(); } + @Override + public LitmusExperiment fetchExperimentByName(String experimentName) { + Optional litmusExperiment = experimentQuery.findByName(experimentName); + return litmusExperiment.map(this::build).orElse(null); + } + @Override public void attachVariants(String experimentId, List variantDefinitions) { var experimentOptional = experimentQuery.findByExperimentId(experimentId); @@ -75,7 +82,7 @@ public record ExperimentServiceImpl(IExperimentQuery experimentQuery, @Override public LitmusExperimentCollection fetchAllExperimentsForVerticals(String vertical, Long pollingTime) { - var experimentEntities = experimentQuery.findByVertical(vertical, pollingTime); + var experimentEntities = experimentQuery.findByVertical(vertical, pollingTime); List litmusExperiments = experimentEntities.stream() .map(this::build) @@ -94,7 +101,7 @@ public record ExperimentServiceImpl(IExperimentQuery experimentQuery, .enabled(experimentEntity.getEnabled()) .archived(experimentEntity.getArchived()) .strategies(jacksonUtils.stringToListObject(experimentEntity.getStrategies(), ActivationStrategy.class)) - .variants(experimentEntity.getVariants()) + .variants(jacksonUtils.stringToListObject(experimentEntity.getVariants(), VariantDefinition.class)) .type(experimentEntity.getType()) .startTime(experimentEntity.getStartTime()) .endTime(experimentEntity.getEndTime()) diff --git a/litmus-core/src/main/resources/application.properties b/litmus-core/src/main/resources/application.properties index c722155..a5d2d0e 100644 --- a/litmus-core/src/main/resources/application.properties +++ b/litmus-core/src/main/resources/application.properties @@ -35,4 +35,6 @@ redis.false.probability=0.001 segment.s3.bucket=${SEGMENT_S3_BUCKET:navi-test} -s3.enabled=${S3_ENABLED:false} \ No newline at end of file +s3.enabled=${S3_ENABLED:false} + +springdoc.swagger-ui.path=/swagger-ui.html \ No newline at end of file diff --git a/litmus-db/pom.xml b/litmus-db/pom.xml index 79f0777..06a042f 100644 --- a/litmus-db/pom.xml +++ b/litmus-db/pom.xml @@ -4,11 +4,11 @@ litmus com.navi.medici - 2.0.0-SNAPSHOT + 2.0.1-RELEASE litmus-db - 2.0.0-SNAPSHOT + 2.0.1-RELEASE jar litmus-db @@ -27,7 +27,7 @@ com.navi.medici litmus-model - 2.0.0-SNAPSHOT + 2.0.1-RELEASE diff --git a/litmus-liquibase/pom.xml b/litmus-liquibase/pom.xml index 818e531..2d919a6 100644 --- a/litmus-liquibase/pom.xml +++ b/litmus-liquibase/pom.xml @@ -4,11 +4,11 @@ litmus com.navi.medici - 2.0.0-SNAPSHOT + 2.0.1-RELEASE litmus-liquibase - 2.0.0-SNAPSHOT + 2.0.1-RELEASE jar litmus-liquibase diff --git a/litmus-mock/pom.xml b/litmus-mock/pom.xml index 252c326..50a1864 100644 --- a/litmus-mock/pom.xml +++ b/litmus-mock/pom.xml @@ -4,11 +4,11 @@ litmus com.navi.medici - 2.0.0-SNAPSHOT + 2.0.1-RELEASE litmus-mock - 2.0.0-SNAPSHOT + 2.0.1-RELEASE jar litmus-mock @@ -16,13 +16,13 @@ com.navi.medici litmus-model - 2.0.0-SNAPSHOT + 2.0.1-RELEASE com.navi.medici litmus-client - 2.0.0-SNAPSHOT + 2.0.1-RELEASE diff --git a/litmus-model/pom.xml b/litmus-model/pom.xml index 7e74623..c05a2cd 100644 --- a/litmus-model/pom.xml +++ b/litmus-model/pom.xml @@ -5,11 +5,11 @@ litmus com.navi.medici - 2.0.0-SNAPSHOT + 2.0.1-RELEASE litmus-model - 2.0.0-SNAPSHOT + 2.0.1-RELEASE jar litmus-model diff --git a/litmus-model/src/main/java/com/navi/medici/constraint/Constraint.java b/litmus-model/src/main/java/com/navi/medici/constraint/Constraint.java index 6de107f..1856c61 100644 --- a/litmus-model/src/main/java/com/navi/medici/constraint/Constraint.java +++ b/litmus-model/src/main/java/com/navi/medici/constraint/Constraint.java @@ -23,4 +23,8 @@ public class Constraint { String contextName; Operator operator; List values; + + String value; + boolean inverted; + boolean caseInsensitive; } diff --git a/litmus-model/src/main/java/com/navi/medici/enums/Operator.java b/litmus-model/src/main/java/com/navi/medici/enums/Operator.java index 9197db4..212ad4a 100644 --- a/litmus-model/src/main/java/com/navi/medici/enums/Operator.java +++ b/litmus-model/src/main/java/com/navi/medici/enums/Operator.java @@ -2,5 +2,15 @@ package com.navi.medici.enums; public enum Operator { IN, - NOT_IN + NOT_IN, + STR_ENDS_WITH, + STR_STARTS_WITH, + STR_CONTAINS, + NUM_EQ, + NUM_GT, + NUM_GTE, + NUM_LT, + NUM_LTE, + DATE_AFTER, + DATE_BEFORE, } diff --git a/litmus-model/src/main/java/com/navi/medici/request/v1/LitmusExperiment.java b/litmus-model/src/main/java/com/navi/medici/request/v1/LitmusExperiment.java index 8cf090a..a067879 100644 --- a/litmus-model/src/main/java/com/navi/medici/request/v1/LitmusExperiment.java +++ b/litmus-model/src/main/java/com/navi/medici/request/v1/LitmusExperiment.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.navi.medici.enums.ExperimentType; import com.navi.medici.strategy.ActivationStrategy; +import com.navi.medici.variants.VariantDefinition; import java.time.LocalDateTime; import java.util.List; import lombok.AccessLevel; @@ -41,7 +42,7 @@ public class LitmusExperiment { List strategies; @JsonProperty("variants") - String variants; + List variants; @JsonProperty("type") ExperimentType type; diff --git a/litmus-proxy/pom.xml b/litmus-proxy/pom.xml index 2a4b3b4..4de3d3b 100644 --- a/litmus-proxy/pom.xml +++ b/litmus-proxy/pom.xml @@ -4,11 +4,11 @@ litmus com.navi.medici - 2.0.0-SNAPSHOT + 2.0.1-RELEASE litmus-proxy - 2.0.0-SNAPSHOT + 2.0.1-RELEASE jar litmus-proxy @@ -17,13 +17,13 @@ com.navi.medici litmus-model - 2.0.0-SNAPSHOT + 2.0.1-RELEASE com.navi.medici litmus-client - 2.0.0-SNAPSHOT + 2.0.1-RELEASE diff --git a/litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java b/litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java index 8cd42f9..cfe048c 100644 --- a/litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java +++ b/litmus-proxy/src/main/java/com/navi/medici/controller/LitmusProxyController.java @@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.RestController; public record LitmusProxyController(Litmus litmus) { @GetMapping(value = "/experiment", produces = MediaType.APPLICATION_JSON_VALUE) - @Timed(value = "litmus.proxy.create.experiment", percentiles = {0.95, 0.99}) + @Timed(value = "litmus.proxy.fetch.experiment", percentiles = {0.95, 0.99}, extraTags = {"mt", "fetch"} ) public ResponseEntity fetch(@RequestParam("name") String experimentName) { var result = litmus.isEnabled(experimentName); @@ -25,7 +25,7 @@ public record LitmusProxyController(Litmus litmus) { } @GetMapping(value = "/variant", produces = MediaType.APPLICATION_JSON_VALUE) - @Timed(value = "litmus.proxy.fetch.experiment", percentiles = {0.95, 0.99}) + @Timed(value = "litmus.proxy.fetch.experiment", percentiles = {0.95, 0.99}, extraTags = {"mt", "fetchVariants"}) public ResponseEntity fetchVariants(@RequestParam("name") String variantName) { var variant = litmus.getVariant(variantName); diff --git a/litmus-util/pom.xml b/litmus-util/pom.xml index e3eb9c8..7903351 100644 --- a/litmus-util/pom.xml +++ b/litmus-util/pom.xml @@ -4,11 +4,11 @@ litmus com.navi.medici - 2.0.0-SNAPSHOT + 2.0.1-RELEASE litmus-util - 2.0.0-SNAPSHOT + 2.0.1-RELEASE litmus-util diff --git a/pom.xml b/pom.xml index a830b2d..ee16b76 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.navi.medici litmus - 2.0.0-SNAPSHOT + 2.0.1-RELEASE pom litmus @@ -81,6 +81,11 @@ 4.13.2 test + + org.mockito + mockito-core + test + org.apache.logging.log4j