Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding ruby client for CAC and experimentation system #186

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions clients/ruby/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Set directory path that contains superposition object files in <span style="color: red" > SUPERPOSITION_LIB_PATH </span> env variable;


## [<u> CAC Client </u>](./cacclient)

1. This exports a class that exposes functions that internally call rust functions.
2. For Different platform it read different superposition object files.
* <span style="color: #808080" >For Mac </span> -> libcac_client.dylib
* <span style="color: #357EC7" >For Windows </span> -> libcac_client.so
* <span style="color: orange" >For Linux </span> -> libcac_client.dll

## [<u> Experimentation Client </u>](./expclient)

1. This exports a class that exposes functions that internally call rust functions.
2. For Different platform it read different superposition object files.
* <span style="color: #808080" >For Mac </span> -> libexperimentation_client.dylib
* <span style="color: #357EC7" >For Windows </span> -> libexperimentation_client.so
* <span style="color: orange" >For Linux </span> -> 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.

## Uses
To use functions from clients, refer to <span style="color: #808080" >main.rb </span> files in the respective clients.
Copy link
Collaborator

@mahatoankitkumar mahatoankitkumar Sep 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add proper steps to add documentation on how to add this to a separate project?
Like importing and using it with sample codes

5 changes: 5 additions & 0 deletions clients/ruby/cac-client/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

source "https://rubygems.org"

# gem "rails"
12 changes: 12 additions & 0 deletions clients/ruby/cac-client/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
GEM
remote: https://rubygems.org/
specs:

PLATFORMS
arm64-darwin-23
ruby

DEPENDENCIES

BUNDLED WITH
2.5.18
91 changes: 91 additions & 0 deletions clients/ruby/cac-client/cacclient.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
require 'ffi'
require 'os'
require 'dotenv'

Dotenv.load

module MergeStrategy
REPLACE = "REPLACE"
MERGE = "MERGE"
end

module CACLib
extend FFI::Library

extension = "dylib"
if OS.windows?
extension = "dll"
elsif OS.mac?
extension = "dylib"
else
extension = "so"
end
libpath = File.expand_path(File.join(__dir__, "#{ENV["SUPERPOSITION_LIB_PATH"]}/libcac_client.#{extension}"))
ffi_lib libpath

class ArcClient < FFI::Struct
layout :dummy, :int
end

# Attach C functions
attach_function :cac_last_error_length, [], :int
attach_function :cac_last_error_message, [], :string
attach_function :cac_free_string, [:pointer], :void
attach_function :cac_new_client, [:string, :ulong, :string], :int
attach_function :cac_start_polling_update, [:string], :void
attach_function :cac_free_client, [ArcClient.by_ref], :void
attach_function :cac_get_client, [:string], ArcClient.by_ref
attach_function :cac_get_last_modified, [ArcClient.by_ref], :string
attach_function :cac_get_config, [ArcClient.by_ref, :string, :string], :string
attach_function :cac_get_resolved_config, [ArcClient.by_ref, :string, :string, :string], :string
attach_function :cac_get_default_config, [ArcClient.by_ref, :string], :string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we improve the return type for the functions like cac_get_config, cac_get_resolved_config and others to respective types like map .....
Refer haskell client.

end

class CACClient
def initialize(tenant, update_frequency, hostname)
result = CACLib.cac_new_client(tenant, update_frequency, hostname)
raise "Failed to create client: #{last_error_message}" if result != 0
@client = CACLib.cac_get_client(tenant)
raise "Failed to get client: #{last_error_message}" if @client.null?
@tenant = tenant
end

def start_polling_update
CACLib.cac_start_polling_update(@tenant)
end

def get_last_modified
result = CACLib.cac_get_last_modified(@client)
raise "Failed to get last modified time: #{last_error_message}" if result.nil?
result
end

def get_config(filter_query, filter_prefix)
result = CACLib.cac_get_config(@client, filter_query, filter_prefix)
puts "Filter query : #{result}"
raise "Failed to get config: #{last_error_message}" if result.nil?
result
ensure
# CACLib.cac_free_string(result) if result // No need to free as its handled internally
end

def get_resolved_config(query, filter_keys, merge_strategy)
result = CACLib.cac_get_resolved_config(@client, query, filter_keys, merge_strategy)
raise "Failed to get resolved config: #{last_error_message}" if result.nil?
result
end

def get_default_config(filter_keys)
result = CACLib.cac_get_default_config(@client, filter_keys)
raise "Failed to get default config: #{last_error_message}" if result.nil?
result
end

def last_error_message
CACLib.cac_last_error_message
end

def close
CACLib.cac_free_client(@client) unless @client.null?
end
end
25 changes: 25 additions & 0 deletions clients/ruby/cac-client/main.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require_relative 'cacclient'

# Example usage
begin
puts "Starting the client"
client = CACClient.new("dev", 60000, "http://localhost:8080")

client.start_polling_update

puts "Last Modified: #{client.get_last_modified}"

config = client.get_config('{"country": "India"}', 'country')
puts "Config: #{config}"

resolved_config = client.get_resolved_config('{"country": "India"}',
"country_image_url,hyperpay_version", MergeStrategy::REPLACE)
puts "Resolved Config: #{resolved_config}"

default_config = client.get_default_config("your_filter_keys")
puts "Default Config: #{default_config}"
rescue StandardError => e
puts "Error: #{e.message}"
ensure
puts "Done with execution"
end
7 changes: 7 additions & 0 deletions clients/ruby/exp-client/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

source "https://rubygems.org"

gem 'dotenv'
gem 'ffi'
gem 'os'
37 changes: 37 additions & 0 deletions clients/ruby/exp-client/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
GEM
remote: https://rubygems.org/
specs:
dotenv (3.1.2)
ffi (1.17.0)
ffi (1.17.0-aarch64-linux-gnu)
ffi (1.17.0-aarch64-linux-musl)
ffi (1.17.0-arm-linux-gnu)
ffi (1.17.0-arm-linux-musl)
ffi (1.17.0-arm64-darwin)
ffi (1.17.0-x86-linux-gnu)
ffi (1.17.0-x86-linux-musl)
ffi (1.17.0-x86_64-darwin)
ffi (1.17.0-x86_64-linux-gnu)
ffi (1.17.0-x86_64-linux-musl)
os (1.1.4)

PLATFORMS
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
ruby
x86-linux-gnu
x86-linux-musl
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl

DEPENDENCIES
dotenv
ffi
os

BUNDLED WITH
2.5.18
98 changes: 98 additions & 0 deletions clients/ruby/exp-client/expclient.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
require 'ffi'
require 'os'
require 'thread'
require 'dotenv'
Dotenv.load

module ExpClientLib
extend FFI::Library
extension = if OS.windows?
"dll"
elsif OS.mac?
"dylib"
else
"so"
end
libpath = File.expand_path(File.join(__dir__, "#{ENV["SUPERPOSITION_LIB_PATH"]}/libexperimentation_client.#{extension}"))
ffi_lib libpath

attach_function :expt_new_client, [:string, :int, :string], :int
attach_function :expt_start_polling_update, [:string], :void
attach_function :expt_get_client, [:string], :string
attach_function :expt_get_applicable_variant, [:string, :string, :int], :string
attach_function :expt_get_satisfied_experiments, [:string, :string, :string], :string
attach_function :expt_get_filtered_satisfied_experiments, [:string, :string, :string], :string
attach_function :expt_get_running_experiments, [:string], :string
attach_function :expt_free_string, [:string], :void
attach_function :expt_last_error_message, [], :string
attach_function :expt_last_error_length, [], :int
attach_function :expt_free_client, [:string], :void

class ExperimentationClient
def initialize(tenant_name, polling_frequency, cac_host_name)
@tenant = tenant_name
@polling_frequency = polling_frequency
@cac_host_name = cac_host_name
@polling_thread = nil
end

def get_experimentation_last_error_message
ExpClientLib.expt_last_error_message
end

def create_new_experimentation_client
resp_code = ExpClientLib.expt_new_client(@tenant, @polling_frequency, @cac_host_name)
if resp_code == 1
error_message = get_experimentation_last_error_message
puts "Some Error Occurred while creating new experimentation client: #{error_message}"
raise "Client Creation Error"
end
resp_code
end

def get_experimentation_client
ExpClientLib.expt_get_client(@tenant)
end

def get_running_experiments
client_ptr = get_experimentation_client
ExpClientLib.expt_get_running_experiments(client_ptr)
end

def free_string(str)
ExpClientLib.expt_free_string(str)
end

def start_experimentation_polling_update
@polling_thread = Thread.new do
ExpClientLib.expt_start_polling_update(@tenant)
end
end

def get_experimentation_last_error_length
ExpClientLib.expt_last_error_length
end

def free_experimentation_client
ExpClientLib.expt_free_client(get_experimentation_client)
end

def get_filtered_satisfied_experiments(context, filter_prefix)
ExpClientLib.expt_get_filtered_satisfied_experiments(
get_experimentation_client, context, filter_prefix
)
end

def get_applicable_variant(context, toss)
ExpClientLib.expt_get_applicable_variant(
get_experimentation_client, context, toss
)
end

def get_satisfied_experiments(context, filter_prefix)
ExpClientLib.expt_get_satisfied_experiments(
get_experimentation_client, context, filter_prefix
)
end
end
end
15 changes: 15 additions & 0 deletions clients/ruby/exp-client/main.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require_relative 'expclient'

# Example usage
begin
client = ExpClientLib::ExperimentationClient.new('dev', 10, 'http://localhost:8080')
client.create_new_experimentation_client
puts "Running experiments :: #{client.get_running_experiments}"
client.start_experimentation_polling_update
puts "After starting the polling"
puts "Running get_filtered_satisfied_experiments :: #{client.get_filtered_satisfied_experiments("juspay", "key1")}"
rescue StandardError => e
puts "Error: #{e.message}"
ensure
puts "Done with the ExperimentationClient execution!"
end
Loading