diff --git a/.changeset/odd-windows-brake.md b/.changeset/odd-windows-brake.md new file mode 100644 index 00000000..6cbad64a --- /dev/null +++ b/.changeset/odd-windows-brake.md @@ -0,0 +1,6 @@ +--- +"eppo_core": minor +"ruby-sdk": minor +--- + +[ruby] support ruby-4.0 diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 0c64cbb8..44f2abae 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -41,6 +41,7 @@ jobs: - "3.2" - "3.3" - "3.4" + - "4.0.0-preview3" steps: - uses: actions/checkout@v5 diff --git a/eppo_core/Cargo.toml b/eppo_core/Cargo.toml index 9dc9263e..f1772b9d 100644 --- a/eppo_core/Cargo.toml +++ b/eppo_core/Cargo.toml @@ -50,8 +50,8 @@ pyo3 = { version = "0.23.0", optional = true, default-features = false } serde-pyobject = { version = "0.6.0", optional = true } # magnus dependencies -magnus = { version = "0.7.1", default-features = false, optional = true } -serde_magnus = { version = "0.9.0", default-features = false, optional = true } +magnus = { version = "0.8.0", default-features = false, optional = true } +serde_magnus = { version = "0.10.0", default-features = false, optional = true } # elixir dependencies rustler = { version = "0.36.1", optional = true } diff --git a/eppo_core/src/eval/eval_details.rs b/eppo_core/src/eval/eval_details.rs index 6bd367ec..a794632e 100644 --- a/eppo_core/src/eval/eval_details.rs +++ b/eppo_core/src/eval/eval_details.rs @@ -270,8 +270,8 @@ mod magnus_impl { use super::{EvaluationDetails, EvaluationResultWithDetails}; impl IntoValue for &EvaluationDetails { - fn into_value_with(self, _handle: &magnus::Ruby) -> magnus::Value { - serde_magnus::serialize(self) + fn into_value_with(self, handle: &magnus::Ruby) -> magnus::Value { + serde_magnus::serialize(handle, self) .expect("EvaluationDetails should always be serializable to Ruby") } } diff --git a/eppo_core/src/events.rs b/eppo_core/src/events.rs index 4342bcc8..77784084 100644 --- a/eppo_core/src/events.rs +++ b/eppo_core/src/events.rs @@ -131,15 +131,15 @@ mod magnus_impl { use super::{AssignmentEvent, BanditEvent}; impl IntoValue for AssignmentEvent { - fn into_value_with(self, _handle: &magnus::Ruby) -> magnus::Value { - serde_magnus::serialize(&self) + fn into_value_with(self, handle: &magnus::Ruby) -> magnus::Value { + serde_magnus::serialize(handle, &self) .expect("AssignmentEvent should always be serializable to Ruby") } } impl IntoValue for BanditEvent { - fn into_value_with(self, _handle: &magnus::Ruby) -> magnus::Value { - serde_magnus::serialize(&self) + fn into_value_with(self, handle: &magnus::Ruby) -> magnus::Value { + serde_magnus::serialize(handle, &self) .expect("BanditEvent should always be serializable to Ruby") } } diff --git a/eppo_core/src/ufc/assignment.rs b/eppo_core/src/ufc/assignment.rs index 92d203c9..96d05888 100644 --- a/eppo_core/src/ufc/assignment.rs +++ b/eppo_core/src/ufc/assignment.rs @@ -380,8 +380,10 @@ mod magnus_impl { AssignmentValue::Integer(i) => i.into_value_with(handle), AssignmentValue::Numeric(n) => n.into_value_with(handle), AssignmentValue::Boolean(b) => b.into_value_with(handle), - AssignmentValue::Json { raw: _, parsed } => serde_magnus::serialize(&parsed) - .expect("JSON value should always be serializable to Ruby"), + AssignmentValue::Json { raw: _, parsed } => { + serde_magnus::serialize(handle, &parsed) + .expect("JSON value should always be serializable to Ruby") + } } } } @@ -392,7 +394,7 @@ mod magnus_impl { let _ = hash.aset(handle.sym_new("value"), self.value); let _ = hash.aset( handle.sym_new("event"), - serde_magnus::serialize::<_, magnus::Value>(&self.event) + serde_magnus::serialize::<_, magnus::Value>(handle, &self.event) .expect("AssignmentEvent should always be serializable to Ruby"), ); hash.as_value() diff --git a/package-lock.json b/package-lock.json index 74f28a5b..6be2a0e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "devDependencies": {} }, "eppo_core": { - "version": "9.2.0", + "version": "9.3.0", "devDependencies": {} }, "mock-server": { @@ -3411,15 +3411,15 @@ } }, "python-sdk": { - "version": "4.3.1", + "version": "4.4.0", "devDependencies": {} }, "ruby-sdk": { - "version": "3.7.2", + "version": "3.7.4", "devDependencies": {} }, "rust-sdk": { - "version": "5.1.0", + "version": "5.2.0", "devDependencies": {} } } diff --git a/ruby-sdk/Cargo.lock b/ruby-sdk/Cargo.lock index c111ce19..eecd0db8 100644 --- a/ruby-sdk/Cargo.lock +++ b/ruby-sdk/Cargo.lock @@ -351,8 +351,6 @@ dependencies = [ [[package]] name = "eppo_core" version = "9.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7a2aa615d3a5848b8cd60921fe67290122054e3e51519cce8e024bd6107c8d" dependencies = [ "base64", "chrono", @@ -932,9 +930,9 @@ dependencies = [ [[package]] name = "magnus" -version = "0.7.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d87ae53030f3a22e83879e666cb94e58a7bdf31706878a0ba48752994146dab" +checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012" dependencies = [ "magnus-macros", "rb-sys", @@ -944,9 +942,9 @@ dependencies = [ [[package]] name = "magnus-macros" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3" +checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892" dependencies = [ "proc-macro2", "quote", @@ -1226,18 +1224,18 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.111" +version = "0.9.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "becea799ce051c16fb140be80f5e7cf781070f99ca099332383c2b17861249af" +checksum = "45fb1a185af97ee456f1c9e56dbe6e2e662bec4fdeaf83c4c28e0e6adfb18816" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.111" +version = "0.9.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64691175abc704862f60a9ca8ef06174080cc50615f2bf1d4759f46db18b4d29" +checksum = "a58ebd02d7a6033e6a5f6f8d150c1e9f16506039092b84a73e6bedce6d3adf41" dependencies = [ "bindgen", "lazy_static", @@ -1250,9 +1248,9 @@ dependencies = [ [[package]] name = "rb-sys-env" -version = "0.1.2" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb" +checksum = "08f8d2924cf136a1315e2b4c7460a39f62ef11ee5d522df9b2750fab55b868b6" [[package]] name = "redox_syscall" @@ -1489,9 +1487,9 @@ dependencies = [ [[package]] name = "serde_magnus" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b8b945a2dadb221f1c5490cfb411cab6c3821446b8eca50ee07e5a3893ec51" +checksum = "c4a5121544138e6a14036e48ecbe589fb63577f35620caaa32642257b412c317" dependencies = [ "magnus", "serde", diff --git a/ruby-sdk/Gemfile.lock b/ruby-sdk/Gemfile.lock index b8e6b6cd..7938ee1b 100644 --- a/ruby-sdk/Gemfile.lock +++ b/ruby-sdk/Gemfile.lock @@ -1,8 +1,9 @@ PATH remote: . specs: - eppo-server-sdk (3.7.2) - rb_sys (~> 0.9.105) + eppo-server-sdk (3.7.4) + logger (~> 1.6) + rb_sys (~> 0.9.120) GEM remote: https://rubygems.org/ @@ -12,6 +13,7 @@ GEM json (2.15.2) language_server-protocol (3.17.0.5) lint_roller (1.1.0) + logger (1.7.0) parallel (1.27.0) parser (3.3.10.0) ast (~> 2.4.1) diff --git a/ruby-sdk/eppo-server-sdk.gemspec b/ruby-sdk/eppo-server-sdk.gemspec index 8f144989..4f0bc59a 100644 --- a/ruby-sdk/eppo-server-sdk.gemspec +++ b/ruby-sdk/eppo-server-sdk.gemspec @@ -32,8 +32,10 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.extensions = ["ext/eppo_client/extconf.rb"] - # 0.9.105 added Ruby 3.4 to the list of supported version - spec.add_dependency "rb_sys", "~> 0.9.105" + # 0.9.120 added Ruby 4.0 to the list of supported version + spec.add_dependency "rb_sys", "~> 0.9.120" + + spec.add_dependency "logger", "~> 1.6" # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/ruby-sdk/ext/eppo_client/Cargo.toml b/ruby-sdk/ext/eppo_client/Cargo.toml index 3da19db0..82537838 100644 --- a/ruby-sdk/ext/eppo_client/Cargo.toml +++ b/ruby-sdk/ext/eppo_client/Cargo.toml @@ -14,9 +14,9 @@ crate-type = ["cdylib"] env_logger = { version = "0.11.3", features = ["unstable-kv"] } eppo_core = { version = "=9.3.0", features = ["magnus", "event_ingestion"] } log = { version = "0.4.21", features = ["kv_serde"] } -magnus = { version = "0.7.1" } +magnus = { version = "0.8.2" } serde = { version = "1.0.203", features = ["derive"] } -serde_magnus = "0.9.0" -rb-sys = "0.9.105" +serde_magnus = "0.10.0" +rb-sys = "0.9.120" serde_json = "1.0.128" tokio = { version = "1.44.1", default-features = false, features = ["time"] } diff --git a/ruby-sdk/ext/eppo_client/src/client.rs b/ruby-sdk/ext/eppo_client/src/client.rs index a524b979..19156420 100644 --- a/ruby-sdk/ext/eppo_client/src/client.rs +++ b/ruby-sdk/ext/eppo_client/src/client.rs @@ -13,7 +13,7 @@ use eppo_core::{ ufc::VariationType, Attributes, ContextAttributes, SdkKey, }; -use magnus::{error::Result, exception, prelude::*, Error, IntoValue, Ruby, TryConvert, Value}; +use magnus::{error::Result, prelude::*, Error, IntoValue, Ruby, TryConvert, Value}; #[derive(Debug)] #[magnus::wrap(class = "EppoClient::Core::Config", size, free_immediately)] @@ -29,6 +29,7 @@ pub struct Config { impl TryConvert for Config { // `val` is expected to be of type EppoClient::Config. fn try_convert(val: Value) -> Result { + let ruby = Ruby::get_with(val); let sdk_key = String::try_convert(val.funcall("api_key", ())?)?; let base_url = String::try_convert(val.funcall("base_url", ())?)?; let poll_interval_seconds = @@ -38,7 +39,7 @@ impl TryConvert for Config { let s = Option::::try_convert(val.funcall("log_level", ())?)?; s.map(|s| { log::LevelFilter::from_str(&s) - .map_err(|err| Error::new(exception::runtime_error(), err.to_string())) + .map_err(|err| Error::new(ruby.exception_runtime_error(), err.to_string())) }) .transpose()? }; @@ -139,8 +140,8 @@ impl Client { subject_attributes: Value, expected_type: Value, ) -> Result { - let expected_type: VariationType = serde_magnus::deserialize(expected_type)?; - let subject_attributes: Attributes = serde_magnus::deserialize(subject_attributes)?; + let expected_type: VariationType = serde_magnus::deserialize(ruby, expected_type)?; + let subject_attributes: Attributes = serde_magnus::deserialize(ruby, subject_attributes)?; let result = rb_self .evaluator @@ -151,24 +152,23 @@ impl Client { Some(expected_type), ) // TODO: maybe expose possible errors individually. - .map_err(|err| Error::new(exception::runtime_error(), err.to_string()))?; + .map_err(|err| Error::new(ruby.exception_runtime_error(), err.to_string()))?; Ok(result.into_value_with(&ruby)) } pub fn get_assignment_details( - &self, + ruby: &Ruby, + rb_self: &Self, flag_key: String, subject_key: String, subject_attributes: Value, expected_type: Value, ) -> Result { - let ruby = Ruby::get_with(subject_attributes); + let expected_type: VariationType = serde_magnus::deserialize(ruby, expected_type)?; + let subject_attributes: Attributes = serde_magnus::deserialize(ruby, subject_attributes)?; - let expected_type: VariationType = serde_magnus::deserialize(expected_type)?; - let subject_attributes: Attributes = serde_magnus::deserialize(subject_attributes)?; - - let result = self.evaluator.get_assignment_details( + let result = rb_self.evaluator.get_assignment_details( &flag_key, &subject_key.into(), &Arc::new(subject_attributes), @@ -179,25 +179,26 @@ impl Client { } pub fn get_bandit_action( - &self, + ruby: &Ruby, + rb_self: &Self, flag_key: String, subject_key: String, subject_attributes: Value, actions: Value, default_variation: String, ) -> Result { - let subject_attributes = serde_magnus::deserialize::<_, ContextAttributes>( - subject_attributes, - ) - .map_err(|err| { - Error::new( - exception::runtime_error(), - format!("Unexpected value for subject_attributes: {err}"), - ) - })?; - let actions = serde_magnus::deserialize(actions)?; + let subject_attributes = + serde_magnus::deserialize::<_, ContextAttributes>(ruby, subject_attributes).map_err( + |err| { + Error::new( + ruby.exception_runtime_error(), + format!("Unexpected value for subject_attributes: {err}"), + ) + }, + )?; + let actions = serde_magnus::deserialize(ruby, actions)?; - let result = self.evaluator.get_bandit_action( + let result = rb_self.evaluator.get_bandit_action( &flag_key, &subject_key.into(), &subject_attributes, @@ -205,29 +206,30 @@ impl Client { &default_variation.into(), ); - serde_magnus::serialize(&result) + serde_magnus::serialize(ruby, &result) } pub fn get_bandit_action_details( - &self, + ruby: &Ruby, + rb_self: &Self, flag_key: String, subject_key: String, subject_attributes: Value, actions: Value, default_variation: String, ) -> Result { - let subject_attributes = serde_magnus::deserialize::<_, ContextAttributes>( - subject_attributes, - ) - .map_err(|err| { - Error::new( - exception::runtime_error(), - format!("Unexpected value for subject_attributes: {err}"), - ) - })?; - let actions = serde_magnus::deserialize(actions)?; + let subject_attributes = + serde_magnus::deserialize::<_, ContextAttributes>(ruby, subject_attributes).map_err( + |err| { + Error::new( + ruby.exception_runtime_error(), + format!("Unexpected value for subject_attributes: {err}"), + ) + }, + )?; + let actions = serde_magnus::deserialize(ruby, actions)?; - let result = self.evaluator.get_bandit_action_details( + let result = rb_self.evaluator.get_bandit_action_details( &flag_key, &subject_key.into(), &subject_attributes, @@ -235,7 +237,7 @@ impl Client { &default_variation.into(), ); - serde_magnus::serialize(&result) + serde_magnus::serialize(ruby, &result) } pub fn wait_for_initialization(&self, timeout_secs: f64) { @@ -278,22 +280,23 @@ impl Client { pub fn shutdown(&self) { if let Some(thread) = self.background_thread.take() { - thread.graceful_shutdown(); + thread.shutdown(); } } - pub fn track(&self, event_type: String, payload: Value) -> Result<()> { - let Some(event_ingestion) = &self.event_ingestion else { + pub fn track(ruby: &Ruby, rb_self: &Self, event_type: String, payload: Value) -> Result<()> { + let Some(event_ingestion) = &rb_self.event_ingestion else { // Event ingestion is disabled, do nothing. return Ok(()); }; - let payload: serde_json::Value = serde_magnus::deserialize(payload).map_err(|err| { - Error::new( - exception::runtime_error(), - format!("Unexpected value for payload: {err}"), - ) - })?; + let payload: serde_json::Value = + serde_magnus::deserialize(ruby, payload).map_err(|err| { + Error::new( + ruby.exception_runtime_error(), + format!("Unexpected value for payload: {err}"), + ) + })?; event_ingestion.track(event_type, payload); diff --git a/ruby-sdk/ext/eppo_client/src/configuration.rs b/ruby-sdk/ext/eppo_client/src/configuration.rs index ae281061..6a4a8891 100644 --- a/ruby-sdk/ext/eppo_client/src/configuration.rs +++ b/ruby-sdk/ext/eppo_client/src/configuration.rs @@ -9,7 +9,7 @@ use crate::{gc_lock::GcLock, SDK_METADATA}; pub(crate) fn init(ruby: &Ruby) -> Result<(), Error> { let eppo_client = ruby.define_module("EppoClient")?; - let configuration = eppo_client.define_class("Configuration", magnus::class::object())?; + let configuration = eppo_client.define_class("Configuration", ruby.class_object())?; configuration.define_singleton_method("new", function!(Configuration::new, 1))?; configuration.define_method( "flags_configuration", diff --git a/ruby-sdk/ext/eppo_client/src/lib.rs b/ruby-sdk/ext/eppo_client/src/lib.rs index 97957b6f..84ce2e97 100644 --- a/ruby-sdk/ext/eppo_client/src/lib.rs +++ b/ruby-sdk/ext/eppo_client/src/lib.rs @@ -16,7 +16,7 @@ pub(crate) const SDK_METADATA: SdkMetadata = SdkMetadata { fn init(ruby: &Ruby) -> Result<(), Error> { let eppo_client = ruby.define_module("EppoClient")?; let core = eppo_client.define_module("Core")?; - let core_client = core.define_class("Client", magnus::class::object())?; + let core_client = core.define_class("Client", ruby.class_object())?; core_client.define_singleton_method("new", function!(Client::new, 1))?; core_client.define_method("get_assignment", method!(Client::get_assignment, 4))?; core_client.define_method(