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.
- checksums.yaml +4 -4
- data/.gitignore +22 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/Cargo.lock +2015 -0
- data/Cargo.toml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +22 -0
- data/Rakefile +53 -0
- data/Steepfile +27 -0
- data/ext/eppo_client/Cargo.toml +19 -0
- data/ext/eppo_client/build.rs +5 -0
- data/ext/eppo_client/extconf.rb +6 -0
- data/ext/eppo_client/src/client.rs +178 -0
- data/ext/eppo_client/src/lib.rs +34 -0
- data/lib/eppo_client/assignment_logger.rb +7 -3
- data/lib/eppo_client/client.rb +159 -205
- data/lib/eppo_client/config.rb +4 -4
- data/lib/eppo_client/custom_errors.rb +0 -17
- data/lib/eppo_client/validation.rb +2 -2
- data/lib/eppo_client/version.rb +2 -1
- data/lib/eppo_client.rb +7 -45
- data/sig/eppo_server_sdk.rbs +96 -0
- metadata +26 -172
- data/lib/eppo_client/configuration_requestor.rb +0 -108
- data/lib/eppo_client/configuration_store.rb +0 -35
- data/lib/eppo_client/constants.rb +0 -20
- data/lib/eppo_client/http_client.rb +0 -75
- data/lib/eppo_client/lru_cache.rb +0 -28
- data/lib/eppo_client/poller.rb +0 -48
- data/lib/eppo_client/rules.rb +0 -119
- data/lib/eppo_client/shard.rb +0 -30
- data/lib/eppo_client/variation_type.rb +0 -39
data/Cargo.toml
ADDED
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,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
|
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(
|
9
|
-
raise(EppoClient::AssignmentLoggerError,
|
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
|