eppo-server-sdk 0.3.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Cargo.toml ADDED
@@ -0,0 +1,7 @@
1
+ # This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is
2
+ # a Rust project. Your extensions dependencies should be added to the Cargo.toml
3
+ # in the ext/ directory.
4
+
5
+ [workspace]
6
+ members = ["./ext/eppo_client"]
7
+ resolver = "2"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Eppo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # Eppo SDK for Ruby
2
+
3
+ ## Getting Started
4
+
5
+ Refer to our [SDK documentation](https://docs.geteppo.com/feature-flags/sdks/ruby) for how to install and use the SDK.
6
+
7
+ ## Supported Ruby Versions
8
+ This version of the SDK is compatible with Ruby 3.0.6 and above.
9
+
10
+ # Contributing
11
+
12
+ ## Testing with local version of `eppo_core`
13
+
14
+ To run build and tests against a local version of `eppo_core`, you should instruct Cargo to look for it at the local path.
15
+
16
+ Add the following to `.cargo/config.toml` file (relative to `ruby-sdk`):
17
+ ```toml
18
+ [patch.crates-io]
19
+ eppo_core = { path = '../eppo_core' }
20
+ ```
21
+
22
+ Make sure you remove the override before updating `Cargo.lock`. Otherwise, the lock file will be missing `eppo_core` checksum and will be unsuitable for release. (CI will warn you if you do this accidentally.)
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require_relative 'lib/eppo_client/version'
6
+
7
+ GEM_NAME = 'eppo-server-sdk'
8
+ GEM_VERSION = EppoClient::VERSION
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ require "rb_sys/extensiontask"
17
+
18
+ task default: :build
19
+
20
+ GEMSPEC = Gem::Specification.load("eppo-server-sdk.gemspec")
21
+
22
+ RbSys::ExtensionTask.new("eppo_client", GEMSPEC) do |ext|
23
+ ext.lib_dir = "lib/eppo_client"
24
+ end
25
+
26
+ task build: :compile do
27
+ system "gem build #{GEM_NAME}.gemspec"
28
+ end
29
+
30
+ task install: :build do
31
+ system "gem install #{GEM_NAME}-#{GEM_VERSION}.gem"
32
+ end
33
+
34
+ task devinstall: :build do
35
+ system "gem install #{GEM_NAME}-#{GEM_VERSION}.gem --dev"
36
+ end
37
+
38
+ task publish: :build do
39
+ system "gem push #{GEM_NAME}-#{GEM_VERSION}.gem"
40
+ end
41
+
42
+ task :clean do
43
+ system 'rm *.gem'
44
+ end
45
+
46
+ RSpec::Core::RakeTask.new(:test) do |task|
47
+ root_dir = Rake.application.original_dir
48
+ task.pattern = "#{root_dir}/spec/*_spec.rb"
49
+ task.verbose = false
50
+ end
51
+
52
+ task test: :devinstall
53
+ task test_refreshed_data: [:devinstall, 'test-data']
data/Steepfile ADDED
@@ -0,0 +1,27 @@
1
+ # D = Steep::Diagnostic
2
+ #
3
+ target :lib do
4
+ signature "sig"
5
+
6
+ check "lib" # Directory name
7
+
8
+ library "singleton"
9
+ library "logger"
10
+
11
+ # configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
12
+ # configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
13
+ # configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
14
+ # configure_code_diagnostics(D::Ruby.silent) # `silent` diagnostics setting
15
+ # configure_code_diagnostics do |hash| # You can setup everything yourself
16
+ # hash[D::Ruby::NoMethod] = :information
17
+ # end
18
+ end
19
+
20
+ target :test do
21
+ signature "sig", "sig-private"
22
+
23
+ check "test"
24
+
25
+ library "singleton"
26
+ library "logger"
27
+ end
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "eppo_client"
3
+ version = "3.1.0"
4
+ edition = "2021"
5
+ license = "MIT"
6
+ publish = false
7
+ rust-version = "1.71.1"
8
+
9
+ [lib]
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ env_logger = { version = "0.11.3", features = ["unstable-kv"] }
14
+ eppo_core = { version = "2.0.0" }
15
+ log = { version = "0.4.21", features = ["kv_serde"] }
16
+ magnus = { version = "0.6.2" }
17
+ serde = { version = "1.0.203", features = ["derive"] }
18
+ serde_magnus = "0.8.1"
19
+ rb-sys = "0.9"
@@ -0,0 +1,5 @@
1
+ fn main() {
2
+ // Without this flag, building via `cargo build` fails with undefined references to ruby
3
+ // library. This is fine as `eppo_client` is going to be loaded as an extension by the host Ruby.
4
+ println!("cargo:rustc-link-arg=-Wl,-undefined,dynamic_lookup");
5
+ }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("eppo_client")
@@ -0,0 +1,178 @@
1
+ use std::{cell::RefCell, sync::Arc};
2
+
3
+ use eppo_core::{
4
+ configuration_fetcher::ConfigurationFetcher,
5
+ configuration_store::ConfigurationStore,
6
+ eval::{get_assignment, get_assignment_details, get_bandit_action, get_bandit_action_details},
7
+ poller_thread::PollerThread,
8
+ ufc::VariationType,
9
+ Attributes, ContextAttributes,
10
+ };
11
+ use magnus::{error::Result, exception, prelude::*, Error, TryConvert, Value};
12
+
13
+ #[derive(Debug)]
14
+ #[magnus::wrap(class = "EppoClient::Core::Config", size, free_immediately)]
15
+ pub struct Config {
16
+ api_key: String,
17
+ base_url: String,
18
+ }
19
+
20
+ impl TryConvert for Config {
21
+ // `val` is expected to be of type EppoClient::Config.
22
+ fn try_convert(val: magnus::Value) -> Result<Self> {
23
+ let api_key = String::try_convert(val.funcall("api_key", ())?)?;
24
+ let base_url = String::try_convert(val.funcall("base_url", ())?)?;
25
+ Ok(Config { api_key, base_url })
26
+ }
27
+ }
28
+
29
+ #[magnus::wrap(class = "EppoClient::Core::Client")]
30
+ pub struct Client {
31
+ configuration_store: Arc<ConfigurationStore>,
32
+ // Magnus only allows sharing aliased references (&T) through the API, so we need to use RefCell
33
+ // to get interior mutability.
34
+ //
35
+ // This should be safe as Ruby only uses a single OS thread, and `Client` lives in the Ruby
36
+ // world.
37
+ poller_thread: RefCell<Option<PollerThread>>,
38
+ }
39
+
40
+ impl Client {
41
+ pub fn new(config: Config) -> Client {
42
+ let configuration_store = Arc::new(ConfigurationStore::new());
43
+
44
+ let poller_thread = PollerThread::start(
45
+ ConfigurationFetcher::new(
46
+ eppo_core::configuration_fetcher::ConfigurationFetcherConfig {
47
+ base_url: config.base_url,
48
+ api_key: config.api_key,
49
+ sdk_name: "ruby".to_owned(),
50
+ sdk_version: env!("CARGO_PKG_VERSION").to_owned(),
51
+ },
52
+ ),
53
+ configuration_store.clone(),
54
+ )
55
+ .expect("should be able to start poller thread");
56
+
57
+ Client {
58
+ configuration_store,
59
+ poller_thread: RefCell::new(Some(poller_thread)),
60
+ }
61
+ }
62
+
63
+ pub fn get_assignment(
64
+ &self,
65
+ flag_key: String,
66
+ subject_key: String,
67
+ subject_attributes: Value,
68
+ expected_type: Value,
69
+ ) -> Result<Value> {
70
+ let expected_type: VariationType = serde_magnus::deserialize(expected_type)?;
71
+ let subject_attributes: Attributes = serde_magnus::deserialize(subject_attributes)?;
72
+
73
+ let config = self.configuration_store.get_configuration();
74
+ let result = get_assignment(
75
+ config.as_ref().map(AsRef::as_ref),
76
+ &flag_key,
77
+ &subject_key,
78
+ &subject_attributes,
79
+ Some(expected_type),
80
+ )
81
+ // TODO: maybe expose possible errors individually.
82
+ .map_err(|err| Error::new(exception::runtime_error(), err.to_string()))?;
83
+
84
+ Ok(serde_magnus::serialize(&result).expect("assignment value should be serializable"))
85
+ }
86
+
87
+ pub fn get_assignment_details(
88
+ &self,
89
+ flag_key: String,
90
+ subject_key: String,
91
+ subject_attributes: Value,
92
+ expected_type: Value,
93
+ ) -> Result<Value> {
94
+ let expected_type: VariationType = serde_magnus::deserialize(expected_type)?;
95
+ let subject_attributes: Attributes = serde_magnus::deserialize(subject_attributes)?;
96
+
97
+ let config = self.configuration_store.get_configuration();
98
+ let result = get_assignment_details(
99
+ config.as_ref().map(AsRef::as_ref),
100
+ &flag_key,
101
+ &subject_key,
102
+ &subject_attributes,
103
+ Some(expected_type),
104
+ );
105
+
106
+ Ok(serde_magnus::serialize(&result).expect("assignment value should be serializable"))
107
+ }
108
+
109
+ pub fn get_bandit_action(
110
+ &self,
111
+ flag_key: String,
112
+ subject_key: String,
113
+ subject_attributes: Value,
114
+ actions: Value,
115
+ default_variation: String,
116
+ ) -> Result<Value> {
117
+ let subject_attributes = serde_magnus::deserialize::<_, ContextAttributes>(
118
+ subject_attributes,
119
+ )
120
+ .map_err(|err| {
121
+ Error::new(
122
+ exception::runtime_error(),
123
+ format!("enexpected value for subject_attributes: {err}"),
124
+ )
125
+ })?;
126
+ let actions = serde_magnus::deserialize(actions)?;
127
+
128
+ let config = self.configuration_store.get_configuration();
129
+ let result = get_bandit_action(
130
+ config.as_ref().map(AsRef::as_ref),
131
+ &flag_key,
132
+ &subject_key,
133
+ &subject_attributes,
134
+ &actions,
135
+ &default_variation,
136
+ );
137
+
138
+ serde_magnus::serialize(&result)
139
+ }
140
+
141
+ pub fn get_bandit_action_details(
142
+ &self,
143
+ flag_key: String,
144
+ subject_key: String,
145
+ subject_attributes: Value,
146
+ actions: Value,
147
+ default_variation: String,
148
+ ) -> Result<Value> {
149
+ let subject_attributes = serde_magnus::deserialize::<_, ContextAttributes>(
150
+ subject_attributes,
151
+ )
152
+ .map_err(|err| {
153
+ Error::new(
154
+ exception::runtime_error(),
155
+ format!("enexpected value for subject_attributes: {err}"),
156
+ )
157
+ })?;
158
+ let actions = serde_magnus::deserialize(actions)?;
159
+
160
+ let config = self.configuration_store.get_configuration();
161
+ let result = get_bandit_action_details(
162
+ config.as_ref().map(AsRef::as_ref),
163
+ &flag_key,
164
+ &subject_key,
165
+ &subject_attributes,
166
+ &actions,
167
+ &default_variation,
168
+ );
169
+
170
+ serde_magnus::serialize(&result)
171
+ }
172
+
173
+ pub fn shutdown(&self) {
174
+ if let Some(t) = self.poller_thread.take() {
175
+ let _ = t.shutdown();
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,34 @@
1
+ mod client;
2
+
3
+ use magnus::{function, method, prelude::*, Error, Object, Ruby};
4
+
5
+ use crate::client::Client;
6
+
7
+ #[magnus::init]
8
+ fn init(ruby: &Ruby) -> Result<(), Error> {
9
+ env_logger::Builder::from_env(env_logger::Env::new().default_filter_or("eppo")).init();
10
+
11
+ let eppo_client = ruby.define_module("EppoClient")?;
12
+ let core = eppo_client.define_module("Core")?;
13
+
14
+ let core_client = core.define_class("Client", magnus::class::object())?;
15
+ core_client.define_singleton_method("new", function!(Client::new, 1))?;
16
+ core_client.define_method("get_assignment", method!(Client::get_assignment, 4))?;
17
+ core_client.define_method(
18
+ "get_assignment_details",
19
+ method!(Client::get_assignment_details, 4),
20
+ )?;
21
+ core_client.define_method("get_bandit_action", method!(Client::get_bandit_action, 5))?;
22
+ core_client.define_method(
23
+ "get_bandit_action_details",
24
+ method!(Client::get_bandit_action_details, 5),
25
+ )?;
26
+ core_client.define_method("shutdown", method!(Client::shutdown, 0))?;
27
+
28
+ core.const_set(
29
+ "DEFAULT_BASE_URL",
30
+ eppo_core::configuration_fetcher::DEFAULT_BASE_URL,
31
+ )?;
32
+
33
+ Ok(())
34
+ }
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'custom_errors'
3
+ require_relative "custom_errors"
4
4
 
5
5
  module EppoClient
6
6
  # The base assignment logger class to override
7
7
  class AssignmentLogger
8
- def log_assignment(_assignment)
9
- raise(EppoClient::AssignmentLoggerError, 'log_assignment has not been set up')
8
+ def log_assignment(_assignment_event)
9
+ raise(EppoClient::AssignmentLoggerError, "log_assignment has not been set up")
10
+ end
11
+
12
+ def log_bandit_action(_assignment_event)
13
+ raise(EppoClient::AssignmentLoggerError, "log_bandit_action has not been set up")
10
14
  end
11
15
  end
12
16
  end