From ed6bc42e80e751f54a0804719193c4c5063291bb Mon Sep 17 00:00:00 2001
From: namit goel <53664454+namitgoel@users.noreply.github.com>
Date: Thu, 1 Aug 2024 19:45:21 +0530
Subject: [PATCH] feat: adding lua cac and experimentation client wrapper
---
clients/lua/Readme.md | 33 ++++++
clients/lua/cacclient/CacClient.lua | 106 +++++++++++++++++
.../lua/expclient/ExperimentationClient.lua | 107 ++++++++++++++++++
clients/lua/main.lua | 14 +++
4 files changed, 260 insertions(+)
create mode 100644 clients/lua/Readme.md
create mode 100644 clients/lua/cacclient/CacClient.lua
create mode 100644 clients/lua/expclient/ExperimentationClient.lua
create mode 100644 clients/lua/main.lua
diff --git a/clients/lua/Readme.md b/clients/lua/Readme.md
new file mode 100644
index 00000000..b4c9c8a3
--- /dev/null
+++ b/clients/lua/Readme.md
@@ -0,0 +1,33 @@
+Set directory path that contains superposition object files in SUPERPOSITION_LIB_PATH env variable;
+
+## [ CAC Client ](./cacclient/CacClient.lua)
+
+1. This exports a class that exposes functions that internally call rust functions.
+2. For Different platform it read different superposition object files.
+ * For Mac -> libcac_client.dylib
+ * For Windows -> libcac_client.so
+ * For Linux -> libcac_client.dll
+3. This run CAC CLient in two thread one is main thread another is worker thread.
+4. Polling updates for config are done on different thread. ([ref](./cacclient/CacClient.lua#78)).
+
+
+## [ Experimentation Client ](./expclient/ExperimentationClient.lua)
+
+1. This exports a class that exposes functions that internally call rust functions.
+2. For Different platform it read different superposition object files.
+ * For Mac -> libexperimentation_client.dylib
+ * For Windows -> libexperimentation_client.so
+ * For Linux -> libexperimentation_client.dll
+3. This run Experimentation CLient in two thread one is main thread another is worker thread.
+4. Polling updates for experiments are done on different thread. ([ref](./expclient/ExperimentationClient.lua#80)).
+
+
+## [ Test ](./main.lua)
+
+1. To test this sample project follow below steps.
+ * Run superposition client.
+ * Run **luajit main.lua** .
+2. By Default this sample code uses [dev](./main.lua#4) tenant.
+3. By Default this sample code assumes superposition is running on [8080](./main.lua#6) port.
+3. By Default this sample code polls superposition every [1 second](./main.lua#L5) port.
+4. This sample code creates both [CAC CLient](./main.lua#12) and [Experimentation Client](./main.lua#8) with above default values.
\ No newline at end of file
diff --git a/clients/lua/cacclient/CacClient.lua b/clients/lua/cacclient/CacClient.lua
new file mode 100644
index 00000000..d61fbf0c
--- /dev/null
+++ b/clients/lua/cacclient/CacClient.lua
@@ -0,0 +1,106 @@
+local ffi = require("ffi")
+local platform = string.lower(jit.os)
+
+local lib_path = os.getenv("SUPERPOSITION_LIB_PATH")
+if not lib_path then
+ error("Environment variable SUPERPOSITION_LIB_PATH is not set")
+end
+
+if platform == "osx" then
+ lib_path = lib_path .. "/libcac_client.dylib"
+elseif platform == "linux" then
+ lib_path = lib_path .. "/libcac_client.so"
+elseif platform == "windows" then
+ lib_path = lib_path .. "/libcac_client.dll"
+else
+ error("Unsupported platform: " .. platform)
+end
+
+
+local lib_path = "/Users/namit.goel/Desktop/repos/namit_superposition/superposition/target/debug/libcac_client.dylib"
+
+ffi.cdef[[
+ int cac_new_client(const char* tenant_name, int polling_frequency, const char* cac_host_name);
+ const char* cac_get_client(const char* tenant_name);
+ void cac_start_polling_update(const char* tenant_name);
+ void cac_free_client(const char* client_ptr);
+ const char* cac_last_error_message();
+ int cac_last_error_length();
+ const char* cac_get_config(const char* client_ptr, const char* filter_query, const char* filter_prefix);
+ void cac_free_string(const char* string);
+ const char* cac_get_last_modified(const char* client_ptr);
+ const char* cac_get_resolved_config(const char* client_ptr, const char* query, const char* filter_keys, const char* merge_strategy);
+ const char* cac_get_default_config(const char* client_ptr, const char* filter_keys);
+]]
+
+local rust_lib = ffi.load(lib_path)
+
+local CacClient = {}
+CacClient.__index = CacClient
+
+function CacClient:new(tenant_name, polling_frequency, cac_host_name)
+ assert(tenant_name and #tenant_name > 0, "tenantName cannot be null or empty")
+ assert(cac_host_name and #cac_host_name > 0, "cacHostName cannot be null or empty")
+
+ local self = setmetatable({}, CacClient)
+ self.tenant = tenant_name
+ self.polling_frequency = polling_frequency
+ self.cac_host_name = cac_host_name
+ return self
+end
+
+function CacClient:get_cac_last_error_message()
+ local error_message = rust_lib.cac_last_error_message()
+ if error_message == nil then
+ return "No Error"
+ end
+ return ffi.string(error_message)
+end
+
+function CacClient:get_cac_last_error_length()
+ return rust_lib.cac_last_error_length()
+end
+
+function CacClient:get_cac_client()
+ return ffi.string(rust_lib.cac_get_client(self.tenant))
+end
+
+function CacClient:create_new_cac_client()
+ local resp = rust_lib.cac_new_client(self.tenant, self.polling_frequency, self.cac_host_name)
+ if resp == 1 then
+ local error_message = self:get_cac_last_error_message()
+ print("Some Error Occur while creating new client ", error_message)
+ end
+ return resp
+end
+
+function CacClient:start_cac_polling_update()
+ rust_lib.cac_start_polling_update(self.tenant)
+end
+
+function CacClient:get_cac_config(filter_query, filter_prefix)
+ local client_ptr = self:get_cac_client()
+ return ffi.string(rust_lib.cac_get_config(client_ptr, filter_query, filter_prefix))
+end
+
+function CacClient:free_cac_client(client_ptr)
+ rust_lib.cac_free_client(client_ptr)
+end
+
+function CacClient:free_cac_string(string)
+ rust_lib.cac_free_string(string)
+end
+
+function CacClient:get_last_modified()
+ return ffi.string(rust_lib.cac_get_last_modified(self:get_cac_client()))
+end
+
+function CacClient:get_resolved_config(query, filter_keys, merge_strategy)
+ return ffi.string(rust_lib.cac_get_resolved_config(self:get_cac_client(), query, filter_keys, merge_strategy))
+end
+
+function CacClient:get_default_config(filter_keys)
+ return ffi.string(rust_lib.cac_get_default_config(self:get_cac_client(), filter_keys))
+end
+
+return CacClient
\ No newline at end of file
diff --git a/clients/lua/expclient/ExperimentationClient.lua b/clients/lua/expclient/ExperimentationClient.lua
new file mode 100644
index 00000000..1ad43b0c
--- /dev/null
+++ b/clients/lua/expclient/ExperimentationClient.lua
@@ -0,0 +1,107 @@
+local ffi = require("ffi")
+local platform = string.lower(jit.os)
+
+local lib_path = os.getenv("SUPERPOSITION_LIB_PATH")
+if not lib_path then
+ error("Environment variable SUPERPOSITION_LIB_PATH is not set")
+end
+
+if platform == "osx" then
+ lib_path = lib_path .. "/libexperimentation_client.dylib"
+elseif platform == "linux" then
+ lib_path = lib_path .. "/libexperimentation_client.so"
+elseif platform == "windows" then
+ lib_path = lib_path .. "/libexperimentation_client.dll"
+else
+ error("Unsupported platform: " .. platform)
+end
+
+ffi.cdef[[
+ int expt_new_client(const char* tenant_name, int polling_frequency, const char* cac_host_name);
+ void expt_start_polling_update(const char* tenant_name);
+ const char* expt_get_client(const char* tenant_name);
+ const char* expt_get_applicable_variant(const char* client_ptr, const char* context, int toss);
+ const char* expt_get_satisfied_experiments(const char* client_ptr, const char* context, const char* filter_prefix);
+ const char* expt_get_filtered_satisfied_experiments(const char* client_ptr, const char* context, const char* filter_prefix);
+ const char* expt_get_running_experiments(const char* client_ptr);
+ void expt_free_string(const char* string);
+ const char* expt_last_error_message();
+ int expt_last_error_length();
+ void expt_free_client(const char* client_ptr);
+]]
+
+local rust_lib = ffi.load(lib_path)
+
+local ExperimentationClient = {}
+ExperimentationClient.__index = ExperimentationClient
+
+function ExperimentationClient:new(tenant_name, polling_frequency, cac_host_name)
+ assert(tenant_name and #tenant_name > 0, "tenantName cannot be null or empty")
+ assert(cac_host_name and #cac_host_name > 0, "cacHostName cannot be null or empty")
+
+ local self = setmetatable({}, ExperimentationClient)
+ self.tenant = tenant_name
+ self.polling_frequency = polling_frequency
+ self.cac_host_name = cac_host_name
+ return self
+end
+
+function ExperimentationClient:get_experimentation_last_error_message()
+ local error_message = rust_lib.expt_last_error_message();
+ if error_message == nil then
+ return "No Error"
+ end
+ return ffi.string(rust_lib.expt_last_error_message())
+end
+
+function ExperimentationClient:create_new_experimentation_client()
+ local resp_code = rust_lib.expt_new_client(self.tenant, self.polling_frequency, self.cac_host_name)
+ if resp_code == 1 then
+ local error_message = self:get_experimentation_last_error_message()
+ print("Some error occurred while creating new experimentation client:", error_message)
+ error("Client Creation Error")
+ end
+ return resp_code
+end
+
+function ExperimentationClient:get_experimentation_client()
+ return ffi.string(rust_lib.expt_get_client(self.tenant))
+end
+
+function ExperimentationClient:get_running_experiments()
+ return ffi.string(rust_lib.expt_get_running_experiments(self:get_experimentation_client()))
+end
+
+function ExperimentationClient:free_string(string)
+ rust_lib.expt_free_string(string)
+end
+
+function ExperimentationClient:start_experimentation_polling_update()
+ rust_lib.expt_start_polling_update(self.tenant)
+end
+
+function ExperimentationClient:get_experimentation_last_error_length()
+ return rust_lib.expt_last_error_length()
+end
+
+function ExperimentationClient:free_experimentation_client()
+ rust_lib.expt_free_client(self:get_experimentation_client())
+end
+
+function ExperimentationClient:get_filtered_satisfied_experiments(context, filter_prefix)
+ local resp = rust_lib.expt_get_filtered_satisfied_experiments(self:get_experimentation_client(), context, filter_prefix)
+ if resp == nil then
+ return ""
+ end
+ return ffi.string(resp)
+end
+
+function ExperimentationClient:get_applicable_variant(context, toss)
+ return ffi.string(rust_lib.expt_get_applicable_variant(self:get_experimentation_client(), context, toss))
+end
+
+function ExperimentationClient:get_satisfied_experiments(context, filter_prefix)
+ return ffi.string(rust_lib.expt_get_satisfied_experiments(self:get_experimentation_client(), context, filter_prefix))
+end
+
+return ExperimentationClient
\ No newline at end of file
diff --git a/clients/lua/main.lua b/clients/lua/main.lua
new file mode 100644
index 00000000..55c8af28
--- /dev/null
+++ b/clients/lua/main.lua
@@ -0,0 +1,14 @@
+local CacClient = require("cacclient.CacClient")
+local ExperimentationClient = require("expclient.ExperimentationClient")
+
+local tenant_name = "dev"
+local polling_frequency = 1
+local cac_host_name = "http://localhost:8080"
+
+local exp_client = ExperimentationClient:new(tenant_name, polling_frequency, cac_host_name)
+local response = exp_client:create_new_experimentation_client()
+print(exp_client:get_satisfied_experiments("{}",""))
+
+local cac_client = CacClient:new(tenant_name, polling_frequency, cac_host_name)
+local response = cac_client:create_new_cac_client()
+print(cac_client:get_last_modified())
\ No newline at end of file