From 1b9551615d234e9bb2b1baec5085f6c71e2cdc59 Mon Sep 17 00:00:00 2001 From: Subhash Chandra Date: Tue, 16 Jul 2024 21:07:14 +0530 Subject: [PATCH] fix: added clent for experimentation system --- .../test/dart_cac_client_test.dart | 2 - clients/dart/dart_exp_client/.gitignore | 29 +++ clients/dart/dart_exp_client/.metadata | 10 + clients/dart/dart_exp_client/CHANGELOG.md | 3 + clients/dart/dart_exp_client/LICENSE | 1 + clients/dart/dart_exp_client/README.md | 68 ++++++ .../dart_exp_client/analysis_options.yaml | 4 + clients/dart/dart_exp_client/lib/client.dart | 223 ++++++++++++++++++ .../dart_exp_client/lib/dart_exp_client.dart | 125 ++++++++++ .../dart/dart_exp_client/lib/types/types.dart | 65 +++++ clients/dart/dart_exp_client/pubspec.yaml | 20 ++ .../test/dart_exp_client_test.dart | 36 +++ 12 files changed, 584 insertions(+), 2 deletions(-) create mode 100644 clients/dart/dart_exp_client/.gitignore create mode 100644 clients/dart/dart_exp_client/.metadata create mode 100644 clients/dart/dart_exp_client/CHANGELOG.md create mode 100644 clients/dart/dart_exp_client/LICENSE create mode 100644 clients/dart/dart_exp_client/README.md create mode 100644 clients/dart/dart_exp_client/analysis_options.yaml create mode 100644 clients/dart/dart_exp_client/lib/client.dart create mode 100644 clients/dart/dart_exp_client/lib/dart_exp_client.dart create mode 100644 clients/dart/dart_exp_client/lib/types/types.dart create mode 100644 clients/dart/dart_exp_client/pubspec.yaml create mode 100644 clients/dart/dart_exp_client/test/dart_exp_client_test.dart diff --git a/clients/dart/dart_cac_client/test/dart_cac_client_test.dart b/clients/dart/dart_cac_client/test/dart_cac_client_test.dart index a846fdb1a..c8822fd27 100644 --- a/clients/dart/dart_cac_client/test/dart_cac_client_test.dart +++ b/clients/dart/dart_cac_client/test/dart_cac_client_test.dart @@ -1,5 +1,3 @@ -import 'dart:isolate'; - import 'package:flutter_test/flutter_test.dart'; import 'package:dart_cac_client/dart_cac_client.dart'; diff --git a/clients/dart/dart_exp_client/.gitignore b/clients/dart/dart_exp_client/.gitignore new file mode 100644 index 000000000..ac5aa9893 --- /dev/null +++ b/clients/dart/dart_exp_client/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/clients/dart/dart_exp_client/.metadata b/clients/dart/dart_exp_client/.metadata new file mode 100644 index 000000000..b10e4879b --- /dev/null +++ b/clients/dart/dart_exp_client/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ba393198430278b6595976de84fe170f553cc728" + channel: "stable" + +project_type: package diff --git a/clients/dart/dart_exp_client/CHANGELOG.md b/clients/dart/dart_exp_client/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/clients/dart/dart_exp_client/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/clients/dart/dart_exp_client/LICENSE b/clients/dart/dart_exp_client/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/clients/dart/dart_exp_client/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/clients/dart/dart_exp_client/README.md b/clients/dart/dart_exp_client/README.md new file mode 100644 index 000000000..104182bc3 --- /dev/null +++ b/clients/dart/dart_exp_client/README.md @@ -0,0 +1,68 @@ +# Dart Experimentation Client + +This package provides a Dart interface for the Experimentation client, allowing you to interact with the experimentation server to retrieve and manage experiments. + +## Table of Contents +- [Dart Experimentation Client](#dart-experimentation-client) + - [Table of Contents](#table-of-contents) + - [Usage](#usage) + - [Creating a Client](#creating-a-client) + - [Starting Polling for Updates](#starting-polling-for-updates) + - [Retrieving Configurations](#retrieving-configurations) + - [Getting Default Configurations](#getting-default-configurations) + - [Getting Resolved Configurations](#getting-resolved-configurations) + - [Getting Last Modified Timestamp](#getting-last-modified-timestamp) + - [Disposing the Client](#disposing-the-client) + +## Usage + +### Creating a Client +To create a new DartExptClient client, instantiate the DartCacClient class with the tenant name, update frequency (in seconds), and host URL + +```dart +final client = To create a new DartExptClient client, instantiate the DartCacClient class with the tenant name, update frequency (in seconds), and host URL +('dev', 60, 'http://localhost:8080'); +``` + +### Starting Polling for Updates +Use the exptStartPolling method to start polling for configuration updates for the specified tenant: + +```dart +client.exptStartPolling(""); +``` + +### Retrieving Configurations +Use the getConfigs method to retrieve configurations based on a filter query and filter prefix: +```dart +String configs = client.getConfigs('{"country": "India"}', 'country'); +``` + +### Getting Default Configurations +Use the getDefaultConfig method to retrieve the default configurations for the specified keys: + +```dart +String defaultConfigs = client.getDefaultConfig("india"); +``` + +### Getting Resolved Configurations +Use the getResolvedConfig method to retrieve resolved configurations based on the provided query, keys, and merge strategy: + + +```dart +String resolvedConfigs = client.getResolvedConfig( + '{"query": "example"}', "key1, key2", MergeStrategy.MERGE/MergeStrategy.REPLACE); +``` + + +### Getting Last Modified Timestamp +Use the getCacLastModified method to get the last modified timestamp of the configurations: + +```dart +String lastModified = client.getCacLastModified(); +``` + +### Disposing the Client +Ensure that you dispose of the client properly to free up resources: +```dart +client.dispose(); +``` diff --git a/clients/dart/dart_exp_client/analysis_options.yaml b/clients/dart/dart_exp_client/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/clients/dart/dart_exp_client/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/clients/dart/dart_exp_client/lib/client.dart b/clients/dart/dart_exp_client/lib/client.dart new file mode 100644 index 000000000..4626217fe --- /dev/null +++ b/clients/dart/dart_exp_client/lib/client.dart @@ -0,0 +1,223 @@ +import 'dart:ffi' as ffi; +import 'package:dart_cac_client/types/types.dart'; +import 'package:ffi/ffi.dart'; +import 'dart:io' show Platform; +import 'package:path/path.dart' as path; + +// Define the library +final ffi.DynamicLibrary _lib = ffi.DynamicLibrary.open(_libName); + +// Helper to get the correct library name based on the platform +String get _libName { + if (Platform.isWindows) { + return path.join( + '/Users/subhash/working-repos/github/superposition/target/debug/', + 'libexperimentation_client.dll'); + } + if (Platform.isMacOS) { + return path.join( + '/Users/subhash/working-repos/github/superposition/target/debug/', + 'libexperimentation_client.dylib'); + } + return path.join( + '/Users/subhash/working-repos/github/superposition/target/debug/', + 'libexperimentation_client.so'); +} + +// Bind the C functions +final ExptNewClientDart _exptNewClient = _lib + .lookup>('expt_new_client') + .asFunction(); + +final ExptGetClientDart _exptGetClient = _lib + .lookup>('expt_get_client') + .asFunction(); + +final ExptLastErrorLengthDart _exptLastErrorLength = _lib + .lookup>( + 'expt_last_error_length') + .asFunction(); + +final ExptLastErrorMessageDart _exptLastErrorMessage = _lib + .lookup>( + 'expt_last_error_message') + .asFunction(); + +final ExptFreeStringDart _exptFreeString = _lib + .lookup>('expt_free_string') + .asFunction(); + +final ExpStartPollingUpdateDart _exptStartPollingUpdate = _lib + .lookup>( + 'expt_start_polling_update') + .asFunction(); + +final ExptFreeClientDart _exptFreeClient = _lib + .lookup>('expt_free_client') + .asFunction(); + +final ExptGetApplicableVariantDart _exptGetApplicableVariant = _lib + .lookup>( + 'expt_get_applicable_variant') + .asFunction(); + +final ExptGetSatisfiedExperimentsDart _exptGetSatisfiedExperiments = _lib + .lookup>( + 'expt_get_satisfied_experiments') + .asFunction(); + +final ExptGetFilteredSatisfiedExperimentsDart + _exptGetFilteredSatisfiedExperiments = _lib + .lookup>( + 'expt_get_filtered_satisfied_experiments') + .asFunction(); + +final ExptGetRunningExperimentsDart _exptGetRunningExperiments = _lib + .lookup>( + 'expt_get_running_experiments') + .asFunction(); + +// Dart wrapper class +class ExptClient { + late ffi.Pointer _clientPtr; + + ExptClient(String tenant, int updateFrequency, String hostname) { + final tenantPtr = tenant.toNativeUtf8(); + final hostnamePtr = hostname.toNativeUtf8(); + + final result = _exptNewClient(tenantPtr, updateFrequency, hostnamePtr); + print("Expt Client result: $result"); + + _exptFreeString(tenantPtr); + _exptFreeString(hostnamePtr); + + if (result != 0) { + final errorPtr = _exptLastErrorMessage(); + final errorMessage = errorPtr.toDartString(); + print("Error message: $errorMessage"); + _exptFreeString(errorPtr); + throw Exception("Failed to create Experimentation client: $errorMessage"); + } + + _clientPtr = _exptGetClient(tenant.toNativeUtf8()); + if (_clientPtr == ffi.nullptr) { + final errorPtr = _exptLastErrorMessage(); + final errorMessage = errorPtr.toDartString(); + print("Error getting client pointer: $errorMessage"); + _exptFreeString(errorPtr); + throw Exception("Failed to get Experimentation client: $errorMessage"); + } + print("Client pointer obtained successfully"); + } + + String getApplicableVariants(String context, int toss) { + final clientPtr = context.toNativeUtf8(); + final tossPtr = malloc.allocate(ffi.sizeOf()); + tossPtr.value = toss; + final applicableVariantPtr = + _exptGetApplicableVariant(_clientPtr, clientPtr, tossPtr); + + _exptFreeString(clientPtr); + malloc.free(tossPtr); + + if (applicableVariantPtr == ffi.nullptr) { + final errorPtr = _exptLastErrorMessage(); + final errorMessage = errorPtr.toDartString(); + print("Error getting Applicable Variant: $errorMessage"); + _exptFreeString(errorPtr); + throw Exception("Failed to get config: $errorMessage"); + } + final applicableVariant = applicableVariantPtr.toDartString(); + print("Received Applicable Variant: $applicableVariant"); + _exptFreeString(applicableVariantPtr); + + return applicableVariant; + } + + String getSatisfiedExperiments(String context, String filterPrefix) { + final contextPtr = context.toNativeUtf8(); + final filterPrefixPtr = filterPrefix.toNativeUtf8(); + + final satisfiedExperimentsPtr = + _exptGetSatisfiedExperiments(_clientPtr, contextPtr, filterPrefixPtr); + + _exptFreeString(contextPtr); + _exptFreeString(filterPrefixPtr); + + if (satisfiedExperimentsPtr == ffi.nullptr) { + final errorPtr = _exptLastErrorMessage(); + final errorMessage = errorPtr.toDartString(); + print("Error getting satisfied experiments: $errorMessage"); + _exptFreeString(errorPtr); + throw Exception("Failed to get satisfied experiments: $errorMessage"); + } + + final satisfiedExperiments = satisfiedExperimentsPtr.toDartString(); + print("Received satisfied experiments: $satisfiedExperiments"); + _exptFreeString(satisfiedExperimentsPtr); + + return satisfiedExperiments; + } + + String getFilteredSatisfiedExperiments(String context, String filterPrefix) { + if (_clientPtr == ffi.nullptr) { + throw Exception("Failed to get Experimentation client!"); + } + final contextPtr = context.toNativeUtf8(); + final filterPrefixPtr = filterPrefix.toNativeUtf8(); + + final fltSatisfiedExperimentsPtr = _exptGetFilteredSatisfiedExperiments( + _clientPtr, contextPtr, filterPrefixPtr); + + _exptFreeString(contextPtr); + _exptFreeString(filterPrefixPtr); + + if (fltSatisfiedExperimentsPtr == ffi.nullptr) { + final errorPtr = _exptLastErrorMessage(); + final errorMessage = errorPtr.toDartString(); + print("Error getting filtered satisfied experiments : $errorMessage"); + _exptFreeString(errorPtr); + throw Exception( + "Failed to get filtered satisfied experiments: $errorMessage"); + } + var fltSatisfiedExperiments = fltSatisfiedExperimentsPtr.toDartString(); + _exptFreeString(fltSatisfiedExperimentsPtr); + + return fltSatisfiedExperiments; + } + + String getRunningExperiments() { + if (_clientPtr == ffi.nullptr) { + throw Exception("Failed to get Experimentation client!"); + } + final runningExprsPtr = _exptGetRunningExperiments(_clientPtr); + if (runningExprsPtr == ffi.nullptr) { + final errorPtr = _exptLastErrorMessage(); + final errorMessage = errorPtr.toDartString(); + print("Error getting filtered running experiments : $errorMessage"); + _exptFreeString(errorPtr); + throw Exception( + "Failed to get filtered running experiments: $errorMessage"); + } + var runningExprs = runningExprsPtr.toDartString(); + _exptFreeString(runningExprsPtr); + return runningExprs; + } + + void startPollingUpdate(String tenant) { + if (_clientPtr == ffi.nullptr) { + throw Exception("Failed to get Experimentation client!"); + } + final tenantPtr = tenant.toNativeUtf8(); + _exptStartPollingUpdate(tenantPtr); + _exptFreeString(tenantPtr); + print("finish polling"); + } + + void dispose() { + if (_clientPtr != ffi.nullptr) { + _exptFreeClient(_clientPtr); + _clientPtr = ffi.nullptr; + } + } +} diff --git a/clients/dart/dart_exp_client/lib/dart_exp_client.dart b/clients/dart/dart_exp_client/lib/dart_exp_client.dart new file mode 100644 index 000000000..f3e778255 --- /dev/null +++ b/clients/dart/dart_exp_client/lib/dart_exp_client.dart @@ -0,0 +1,125 @@ +library dart_cac_client; + +import 'package:dart_cac_client/client.dart'; + +enum MergeStrategy { REPLACE, MERGE } + +class DartExptClient { + late ExptClient client; + + /// Creates a new CacClient instance. + /// + /// [tenant] is the name of the tenant. + /// [updateFrequency] is the frequency of updates in seconds. + /// [hostUrl] is the URL of the CAC server. + /// + /// Example: + /// ```dart + /// final dartClient = DartExptClient('dev', 60, 'http://localhost:8080'); + /// ``` + + DartExptClient(String tenant, int updateFrequency, String hostUrl) { + client = ExptClient(tenant, updateFrequency, hostUrl); + } + + /// Start polling the superposition server for updates for the given tenant. + /// + /// [tenant] is the name of the tenant to poll updates for. + /// + /// Example: + /// ```dart + /// client.exptStartPolling("dev"); + /// ``` + void exptStartPolling(String tenant) async { + try { + client.startPollingUpdate(tenant); + print("Started polling update for tenant: $tenant"); + } catch (e) { + print("Something went wrong while starting polling update: $e"); + } + } + + /// get the experiments that apply to a given context. + /// + /// [context] is a JSON string representing the query parameters. + /// [toss] a number toss between 0 - 100 that is used to assign a variant IDs. + /// + /// Returns a string formatted array of variant IDs that match the parameters passed. + /// + /// Throws an exception if the retrieval fails. + /// + /// Example: + /// ```dart + /// var configs = dartClient.getApplicableVariants("{\"os\": \"android\", \"client\": \"1mg\"}", 10) + /// ``` + String getApplicableVariants(String context, int toss) { + try { + var applicableVariant = client.getApplicableVariants(context, toss); + return applicableVariant; + } catch (e) { + print("Something went wrong ${e}"); + } + return "[]"; + } + + /// get the experiments that apply to a given context c_context. It also filters on config key prefix. + /// + /// [context] string value representing the context. + /// [filterPrefix] key prefix. + /// + /// returns a string formatted array of experiments that match the parameters passed. + /// Example: + /// ```dart + /// var satisfiedExperiments = + /// dartClient.getSatisfiedExperiments("{\"os\": \"android\", \"client\": \"1mg\"}", "os"); + /// ``` + String getSatisfiedExperiments(String context, String filterPrefix) { + try { + var satisfiedExperiments = + client.getSatisfiedExperiments(context, filterPrefix); + return satisfiedExperiments; + } catch (e) { + print("Something went wrong while getting satisfied experiments $e"); + } + return "[]"; + } + + /// gets experiments that apply to a given context c_context. It also filters on config key prefix + /// + /// [context] is a string representing the experiment context. + /// [filterPrefix] is a comma-separated string of configuration keys to retrieve. + /// + /// returns a string formatted array of experiments that match the parameters passed. + /// + /// Example: + /// ```dart + /// var fltrSatisfiedExpt = + /// client.getFilteredSatisfiedExperiments("{\"os\": \"android\", \"client\": \"1mg\"}", "client"); + /// ``` + String getFilteredSatisfiedExperiments(String context, String filterPrefix) { + try { + var fltrSatisfiedExpt = + client.getFilteredSatisfiedExperiments(context, filterPrefix); + return fltrSatisfiedExpt; + } catch (e) { + print( + "Something went wrong while getting filtered satisfied experiments $e"); + } + return "[]"; + } + + /// get all currently running experiments + /// returns a string formatted array of experiments that match the parameters passed + String getRunningExperiments() { + try { + return client.getRunningExperiments(); + } catch (e) { + print("Something went wrong while fetching running experiments $e"); + } + return "[]"; + } + + void dispose() { + client.dispose(); + } +} diff --git a/clients/dart/dart_exp_client/lib/types/types.dart b/clients/dart/dart_exp_client/lib/types/types.dart new file mode 100644 index 000000000..3c2f1bf00 --- /dev/null +++ b/clients/dart/dart_exp_client/lib/types/types.dart @@ -0,0 +1,65 @@ +import 'dart:ffi' as ffi; +import 'package:ffi/ffi.dart'; + +// Define the C functions +typedef ExptNewClientNative = ffi.Int32 Function( + ffi.Pointer, ffi.Uint64, ffi.Pointer); +typedef ExptNewClientDart = int Function( + ffi.Pointer, int, ffi.Pointer); + +typedef ExptGetClientNative = ffi.Pointer Function(ffi.Pointer); +typedef ExptGetClientDart = ffi.Pointer Function(ffi.Pointer); + +typedef ExptFreeClientNative = ffi.Void Function(ffi.Pointer); +typedef ExptFreeClientDart = void Function(ffi.Pointer); + +typedef ExptGetApplicableVariantNative = ffi.Pointer Function( + ffi.Pointer, ffi.Pointer, ffi.Pointer); +typedef ExptGetApplicableVariantDart = ffi.Pointer Function( + ffi.Pointer, ffi.Pointer, ffi.Pointer); + +typedef ExptGetSatisfiedExperimentsNative = ffi.Pointer Function( + ffi.Pointer, ffi.Pointer, ffi.Pointer); +typedef ExptGetSatisfiedExperimentsDart = ffi.Pointer Function( + ffi.Pointer, ffi.Pointer, ffi.Pointer); + +typedef ExptGetFilteredSatisfiedExperimentsNative = ffi.Pointer Function( + ffi.Pointer, ffi.Pointer, ffi.Pointer); +typedef ExptGetFilteredSatisfiedExperimentsDart = ffi.Pointer Function( + ffi.Pointer, ffi.Pointer, ffi.Pointer); + +typedef ExptGetRunningExperimentsNative = ffi.Pointer Function( + ffi.Pointer); +typedef ExptGetRunningExperimentsDart = ffi.Pointer Function( + ffi.Pointer); + +typedef ExptGetLastModifiedNative = ffi.Pointer Function( + ffi.Pointer); + +typedef ExptFreeStringNative = ffi.Void Function(ffi.Pointer); +typedef ExptFreeStringDart = void Function(ffi.Pointer); + +typedef ExptLastErrorMessageNative = ffi.Pointer Function(); +typedef ExptLastErrorMessageDart = ffi.Pointer Function(); + +typedef ExptLastErrorLengthNative = ffi.Pointer Function(); +typedef ExptLastErrorLengthDart = ffi.Pointer Function(); + +typedef ExpGetResolvedConfigsNative = ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer); +typedef ExpGetResolvedConfigsDart = ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer); + +typedef ExpStartPollingUpdateNative = ffi.Pointer Function( + ffi.Pointer); +typedef ExpStartPollingUpdateDart = ffi.Pointer Function( + ffi.Pointer); + +typedef ExpGetLastModifiedDart = ffi.Pointer Function( + ffi.Pointer); diff --git a/clients/dart/dart_exp_client/pubspec.yaml b/clients/dart/dart_exp_client/pubspec.yaml new file mode 100644 index 000000000..88d0d8e4f --- /dev/null +++ b/clients/dart/dart_exp_client/pubspec.yaml @@ -0,0 +1,20 @@ +name: dart_cac_client +description: "Package for Experimentation system." +version: 0.0.1 + +environment: + sdk: '>=3.3.1 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + ffi: ^2.0.0 + flutter: + sdk: flutter + path: ^1.9.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +flutter: diff --git a/clients/dart/dart_exp_client/test/dart_exp_client_test.dart b/clients/dart/dart_exp_client/test/dart_exp_client_test.dart new file mode 100644 index 000000000..c19174de4 --- /dev/null +++ b/clients/dart/dart_exp_client/test/dart_exp_client_test.dart @@ -0,0 +1,36 @@ +import 'package:dart_cac_client/dart_exp_client.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('adds one to input values', () async { + final client = DartExptClient('dev', 1, 'http://localhost:8080'); + + print("Client created successfully"); + try { + client.exptStartPolling("dev"); + // Perform various operations + try { + var applicableVariants = client.getApplicableVariants( + "{\"os\": \"android\", \"client\": \"1mg\"}", 10); + print('Full Applicable variants: $applicableVariants'); + + var satisfiedExperiments = + client.getSatisfiedExperiments("juspay", "key1"); + print("Satisfied Expirments: $satisfiedExperiments"); + + var filteredsatisfiedExperiments = + client.getFilteredSatisfiedExperiments("juspay", "key1"); + print("filtered satisfied experiments: $filteredsatisfiedExperiments"); + + var runningExperiments = client.getRunningExperiments(); + print("running experiments: $runningExperiments"); + } catch (e) { + print("Error during operations: $e"); + } finally { + client.dispose(); + } + } catch (e) { + print("Error creating client: $e"); + } + }); +}