Skip to main content

Java & Scala

It's simple to build your client using the APIs. If you're application is built on jvm you can also use the built-in Izanami client.

This client offer nice strategies for better performances.

You need to add repository

    repositories {
jcenter()
maven {
url 'https://raw.githubusercontent.com/mathieuancelin/json-lib-javaslang/master/repository/releases/'
}
}

Add the following dependency to your project

libraryDependencies += "fr.maif" %% "izanami-client" % "1.11.0"

The client can be used in java or scala. There is two distinct dsl. Be sure to import the correct one :

import izanami.*;
import izanami.javadsl.*;

Izanami client is built with

  • Scala: As programming language
  • Akka: to handle global state, scheduler ...
  • Akka http: for http request, sse ...

The scaladsl rely on :

The javadsl rely on :

  • vavr: For functional structures like future, either, option ...
  • play json java: For json handling

Setup the Izanami client

The first thing to do is to create a client. The client own the shared http client between config client, feature client and the experiment client.

You need to create a single client for all your application.

izanamiClient = IzanamiClient.client(
system,
ClientConfig.create("http://localhost:8089")
.withClientId("xxxx")
.withClientIdHeaderName("Another-Client-Id-Header")
.withClientSecret("xxxx")
.withClientSecretHeaderName("Another-Client-Secret-Header")
.sseBackend()
.withDispatcher("izanami-example.blocking-io-dispatcher")
.withPageSize(50)
.withZoneId(ZoneId.of("Europe/Paris"))
);
FieldDescription
clientIdThe client id to access izanami APIs see Manage APIs keys
clientSecretIdNameA custom header for the client id
clientSecretThe client secret to access izanami APIs see Manage APIs keys
clientSecretHeaderNameA custom header for the client secret
sseBackendEnable sse to get events from the server
dispatcherReference a dispatcher to manage thread pool
pageSizeChange the size of the pages when fetching from the server
zoneIdZone Id to handle date

Configs client

The config client is used to access the shared config in Izanami. To understand how configs work, just visit this page

Setup the client

ConfigClient configClient = izanamiClient.configClient(
Strategies.fetchStrategy(),
Configs.configs(
Config.config("my:config", Json.obj(
Syntax.$("value", "Fallback value")
))
)
);

When you set up a client you have to choose a strategy :

StrategyDescription
FetchCall izanami for each request
Fetch with cacheKeep response in cache
Smart cache with pollingKeep data in memory and poll izanami to refresh the cache asynchronously.
Smart cache with sseKeep data in memory and refresh the cache with the events from the izanami server.

The fetch strategy

The fetch strategy will call izanami for each request.

ConfigClient configFetchStrategy = izanamiClient.configClient(
Strategies.fetchStrategy(),
Configs.configs(
Config.config("my:config", Json.obj(
Syntax.$("value", "Fallback value")
))
)
);

The fetch with cache strategy

The fetch with cache will do dumb cache by http call. You have to provide a the max elements in cache and a TTL.

Integer maxElementInCache = 100;
FiniteDuration ttl = FiniteDuration.create(20, TimeUnit.MINUTES);
ConfigClient fetchWithCacheStrategy = izanamiClient.configClient(
Strategies.fetchWithCacheStrategy(maxElementInCache, ttl),
Configs.configs(
Config.config("my:config", Json.obj(
Syntax.$("value", "Fallback value")
))
)
);

The smart cache strategy

When you choose the smart cache, you have to provide patterns :

ConfigClient configClient = izanamiClient.configClient(
Strategies.smartCacheWithPollingStrategy(
FiniteDuration.create(20, TimeUnit.SECONDS),
"my:configs:*", "other:pattern"
),
Configs.configs(
Config.config("my:config", Json.obj(
Syntax.$("value", "Fallback value")
))
)
);

The client will cache all the configs matching this patterns.

  • With a poll strategy, the client will request the server to get change so you have to set a delay.
  • With a SSE strategy, the client will listen events from the server to refresh the cache.

::: warning

There is no TTL using this strategy so you have to choose the right patterns to be sure that all datas fit in memory.

:::

Handling errors

An error handling strategy could be provided. You can choose between :

  • RecoverWithFallback: If the call crash, the fallback is used
  • Crash: The call will finish on error if an error occured while evaluating the feature, config or experiment.
ConfigClient configErrorStrategy = izanamiClient.configClient(
Strategies.fetchStrategy(ErrorStrategies.crash())
);

Client usage

Get configs for a pattern

Future<Configs> futureConfigs = configClient.configs("my:*");
futureConfigs.onSuccess((Configs configs) -> {
JsValue config = configs.config("my:config");
System.out.println(config.field("value").asString());
});

Get one config

Future<JsValue> futureConfig = configClient.config("my:config");
futureConfig.onSuccess((JsValue config) -> {
System.out.println(config.field("value").asString());
});

Create / Update / Delete configs

Create config using json

JsValue createdJson = configClient.createConfig("my:config", Json.obj(Syntax.$("value", "A configuration"))).get();

Create config using a config object

Config created = configClient.createConfig(Config.config("my:config", Json.obj(Syntax.$("value", "A configuration")))).get();

Update config using json

JsValue updatedJson = configClient.updateConfig("my:previous:config", "my:config", Json.obj(Syntax.$("value", "A configuration"))).get();

Update config using a config object

Config updated = configClient.updateConfig("my:previous:config", Config.config("my:config", Json.obj(Syntax.$("value", "A configuration")))).get();

Delete a config

Done deleted = configClient.deleteConfig("my:config").get();

Autocreate configs

You can autocreate configs that are define as fallback. To enable this you need set the autocreate parameter when the client is created.

Boolean autocreate = true;
ConfigClient configClient = izanamiClient.configClient(
Strategies.fetchStrategy(),
Configs.configs(
Config.config("my:config", Json.obj(
Syntax.$("value", "Fallback value")
))
),
autocreate
);

Features client

To understand how features work, just visit this page

Setup the client

FeatureClient featureClient = izanamiClient.featureClient(
Strategies.fetchStrategy(),
Features.features(
Features.feature("my:feature", false)
)
);

When you set up a client you have to choose a strategy :

StrategyDescription
FetchCall izanami for each request
Fetch with cacheKeep response in cache
Smart cache with pollingKeep data in memory and poll izanami to refresh the cache asynchronously. The features that need a context are not cached because it can needs a huge amount of memory
Smart cache with sseKeep data in memory and refresh the cache with the events from the izanami server. The features that need a context are not cached because it can needs a huge amount of memory

The fetch strategy

The fetch strategy will call izanami for each request

FeatureClient featureClient = izanamiClient.featureClient(
Strategies.fetchStrategy(),
Features.features(
Features.feature("my:feature", false)
)
);

The fetch with cache strategy

The fetch with cache will do dumb cache by http call. You have to provide a the max elements in cache and a TTL.

Integer maxElementInCache = 100;
FiniteDuration ttl = FiniteDuration.create(20, TimeUnit.MINUTES);
FeatureClient featureClientWithCache = izanamiClient.featureClient(
Strategies.fetchWithCacheStrategy(maxElementInCache, ttl),
Features.features(
Features.feature("my:feature", false)
)
);

The smart cache strategy

When you choose the smart cache, you have to provide patterns to select the keys that will be in cache:

FeatureClient featureClient = izanamiClient.featureClient(
Strategies.smartCacheWithSseStrategy(
scala.Option.apply(FiniteDuration.create(1, TimeUnit.MINUTES)),
"my:features:*", "other:pattern"
),
Features.features(
Features.feature("my:feature", false)
)
);

The client will cache all the configs matching this patterns.

  • With a poll strategy, the client will request the server to get change so you have to set a delay.
  • With a SSE strategy, the client will listen events from the server to refresh the cache.

::: note

The feature that need a context to be evaluated are not cached. The cache is used only for simple features or feature with release date.

:::

::: warning

There is no TTL using this strategy so you have to choose the right patterns to be sure that all datas fit in memory.

:::

Handling errors

An error handling strategy could be provided. You can choose between :

  • RecoverWithFallback: If the call crash, the fallback is used
  • Crash: The call will finish on error if an error occured while evaluating the feature, config or experiment.
FeatureClient featureClientWithErrorHandling = izanamiClient.featureClient(
Strategies.fetchStrategy(ErrorStrategies.crash())
);

Client usage

List features

Future<Features> futureFeatures = featureClient.features("my:feature:*");

futureFeatures.onSuccess(features -> {
boolean active = features.isActive("my:feature:test");
if (active) {
System.out.println("Feature my:feature:test is active");
} else {
System.out.println("Feature my:feature:test is active");
}
JsObject tree = features.tree();
System.out.println("Tree is " + Json.prettyPrint(tree));
});

Check feature

Future<Boolean> futureCheck = featureClient.checkFeature("my:feature");

If the feature needs a context to be evaluated:

Future<Boolean> futureCheckContext = featureClient.checkFeature("my:feature", Json.obj(
Syntax.$("context", true)
));

Conditional code on feature

This execute a code and return a value if a feature is active:

Future<String> conditonal = featureClient.featureOrElse("my:feature",
() -> "Feature is active",
() -> "Feature is not active"
);

Or with a context

Future<String> conditonalContext = featureClient.featureOrElse(
"my:feature",
Json.obj(Syntax.$("context", true)),
() -> "Feature is active",
() -> "Feature is not active"
);

Create / update / delete

With the client you can mutate features.

Create with raw data:

Feature createdJson = featureClient.createJsonFeature(
"my:feature",
true,
Features.hourRangeType(),
Option.of(Json.obj(
Syntax.$("startAt", "05:25"),
Syntax.$("endAt", "16:30")
))
).get();

Or with a feature object:

Feature created = featureClient.createFeature(
Features.hourRange("my:feature", true, LocalTime.of(5, 25), LocalTime.of(16, 30))
).get();

Update a feature :

Feature updated = featureClient.updateFeature("my:previous:feature", Features.hourRange("my:feature:test", true, LocalTime.of(5, 25), LocalTime.of(16, 30))).get();

Delete a feature :

Done deleted = featureClient.deleteFeature("my:feature").get();

You can also activate or deactivate a feature

Feature activated = featureClient.switchFeature("my:feature", true).get();

Autocreate features

You can autocreate features that are define as fallback. To enable this you need set the autocreate parameter when the client is created.

Boolean autocreate = true;
FeatureClient featureClient = izanamiClient.featureClient(
Strategies.fetchStrategy(),
Features.features(
Features.feature("my:feature", false)
),
autocreate
);

Experiments client

To understand how experiments work, just visit this page

Setup the client

ExperimentsClient experimentsClient = izanamiClient.experimentClient(Strategies.fetchStrategy());

For experiments, there is only two strategies available : fetch or dev.

Variants

Get a variant for a client

Future<Option<Variant>> futureVariant = experimentsClient.getVariantFor("my:experiment", "clientId");
futureVariant.onSuccess(mayBeVariant ->
Match(mayBeVariant).of(
Case($Some($()), exist -> {
String phrase = "Variant is " + exist;
System.out.println(phrase);
return phrase;
}),
Case($None(), __ -> {
String phrase = "Variant not found";
System.out.println(phrase);
return phrase;
})
)
);

Mark variant displayed

Future<ExperimentVariantDisplayed> futureDisplayed = experimentsClient.markVariantDisplayed("my:experiment", "clientId");
futureDisplayed.onSuccess(event ->
System.out.println(event)
);

Mark variant won

Future<ExperimentVariantWon> futureWon = experimentsClient.markVariantWon("my:experiment", "clientId");
futureWon.onSuccess(event ->
System.out.println(event)
);

Work with experiment

Get the experiment

Future<Option<ExperimentClient>> futureExperiment = experimentsClient.experiment("my:experiment");
futureExperiment.onSuccess(mayBeExperiment ->
Match(mayBeExperiment).of(
Case($Some($()), exist -> {
String phrase = "Experiment is " + exist;
System.out.println(phrase);
return phrase;
}),
Case($None(), __ -> {
String phrase = "Experiment not found";
System.out.println(phrase);
return phrase;
})
)
);

Once you get the experiment, you can get a variant for a client, mark variant displayed or mark variant won :

ExperimentClient experiment = mayExperiment.get();
Future<Option<Variant>> clientId = experiment.getVariantFor("clientId");
Future<ExperimentVariantDisplayed> displayed = experiment.markVariantDisplayed("clientId");
Future<ExperimentVariantWon> won = experiment.markVariantWon("clientId");

Experiment tree

You can get the experiments tree with associated variant for a client :

Future<JsValue> futureTree = experimentsClient.tree("*", "clientId");
futureTree.onSuccess(tree -> {
assertThat(tree).isEqualTo(
Json.obj(
Syntax.$("my", Json.obj(
Syntax.$("experiment", Json.obj(
Syntax.$("variant", "A")
))
))
)
);
});

Exposing izanami with a Proxy

When you have to use Izanami from the client side, you can't call Izanami directly from the browser because it means the API keys are exposed to anyone.

The best solution is to use your backend as a proxy. You can do this with the jvm client.

ConfigClient configClient = izanamiClient.configClient(
Strategies.dev(),
Configs.configs(
Config.config("configs:test", Json.obj(
Syntax.$("value", 2)
))
)
);
FeatureClient featureClient = izanamiClient.featureClient(
Strategies.dev(),
Features.features(
Features.feature("features:test1", true)
)
);
ExperimentsClient experimentsClient = izanamiClient.experimentClient(
Strategies.dev(),
Experiments.create(
ExperimentFallback.create(
"experiments:id",
"Experiment",
"An Experiment",
true,
Variant.create("A", "Variant A", scala.Option.apply("Variant A"))
)));

Proxy proxy = izanamiClient.proxy()
.withConfigClient(configClient)
.withConfigPattern("configs:*")
.withFeatureClient(featureClient)
.withFeaturePattern("features:*")
.withExperimentsClient(experimentsClient)
.withExperimentPattern("experiments:*");

Future<Tuple2<Integer, JsValue>> fJsonResponse = proxy.statusAndJsonResponse();
fJsonResponse.onSuccess(t ->
System.out.println("Code = " + t._1 + ", json body = " + t._2)
);

//Or with string response and additional infos :
Future<Tuple2<Integer, String>> fStringResponse = proxy.statusAndStringResponse(Json.obj().with("id", "ragnard.lodbrock@gmail.com"), "ragnard.lodbrock@gmail.com");
fStringResponse.onSuccess(t ->
System.out.println("Code = " + t._1 + ", string body = " + t._2)
);
// Experiment proxy

Future<Tuple2<Integer, JsValue>> markVariantDisplayed = proxy.markVariantDisplayed("experiments:id", "ragnars.lodbrock@gmail.com");
Future<Tuple2<Integer, JsValue>> markVariantWon = proxy.markVariantWon("experiments:id", "ragnars.lodbrock@gmail.com");