From a0e4bffd0edf66948d313978a8dfe3ece4a0a0cb Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Fri, 24 Oct 2025 09:14:26 -0700 Subject: [PATCH 01/43] Add FFI bindings for datadog-ffe Creates C-compatible FFI layer for Feature Flagging & Experimentation: - Add datadog-ffe-ffi crate following ddsketch-ffi patterns - Implement Handle wrappers for Configuration, EvaluationContext, Assignment - Export get_assignment, configuration_new, evaluation_context_new functions - Add memory management with drop functions - Generate C headers via cbindgen for Ruby integration - Update Cargo.toml to include new FFI crate in workspace --- Cargo.lock | 9 +++ Cargo.toml | 1 + datadog-ffe-ffi/Cargo.toml | 26 +++++++ datadog-ffe-ffi/build.rs | 8 +++ datadog-ffe-ffi/cbindgen.toml | 36 ++++++++++ datadog-ffe-ffi/src/assignment.rs | 77 ++++++++++++++++++++ datadog-ffe-ffi/src/configuration.rs | 44 ++++++++++++ datadog-ffe-ffi/src/error.rs | 10 +++ datadog-ffe-ffi/src/evaluation_context.rs | 85 +++++++++++++++++++++++ datadog-ffe-ffi/src/lib.rs | 12 ++++ 10 files changed, 308 insertions(+) create mode 100644 datadog-ffe-ffi/Cargo.toml create mode 100644 datadog-ffe-ffi/build.rs create mode 100644 datadog-ffe-ffi/cbindgen.toml create mode 100644 datadog-ffe-ffi/src/assignment.rs create mode 100644 datadog-ffe-ffi/src/configuration.rs create mode 100644 datadog-ffe-ffi/src/error.rs create mode 100644 datadog-ffe-ffi/src/evaluation_context.rs create mode 100644 datadog-ffe-ffi/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ca19bceb8d..44eecab590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1435,6 +1435,15 @@ dependencies = [ "url", ] +[[package]] +name = "datadog-ffe-ffi" +version = "22.1.0" +dependencies = [ + "build_common", + "datadog-ffe", + "ddcommon-ffi", +] + [[package]] name = "datadog-ipc" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7db0650c6f..eb00d087d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "datadog-crashtracker", "datadog-crashtracker-ffi", "datadog-ffe", + "datadog-ffe-ffi", "datadog-ipc", "datadog-ipc-macros", "datadog-library-config", diff --git a/datadog-ffe-ffi/Cargo.toml b/datadog-ffe-ffi/Cargo.toml new file mode 100644 index 0000000000..968b5838bd --- /dev/null +++ b/datadog-ffe-ffi/Cargo.toml @@ -0,0 +1,26 @@ +# Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "datadog-ffe-ffi" +edition.workspace = true +version.workspace = true +rust-version.workspace = true +license.workspace = true + +[lib] +crate-type = ["lib", "staticlib", "cdylib"] +bench = false + +[features] +default = ["cbindgen"] +cbindgen = ["build_common/cbindgen", "ddcommon-ffi/cbindgen"] + +[build-dependencies] +build_common = { path = "../build-common" } + +[dependencies] +datadog-ffe = { path = "../datadog-ffe" } +ddcommon-ffi = { path = "../ddcommon-ffi", default-features = false } + +[dev-dependencies] \ No newline at end of file diff --git a/datadog-ffe-ffi/build.rs b/datadog-ffe-ffi/build.rs new file mode 100644 index 0000000000..c7ecf3d90d --- /dev/null +++ b/datadog-ffe-ffi/build.rs @@ -0,0 +1,8 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + build_common::generate_and_configure_header("datadog_ffe.h"); +} \ No newline at end of file diff --git a/datadog-ffe-ffi/cbindgen.toml b/datadog-ffe-ffi/cbindgen.toml new file mode 100644 index 0000000000..f65615962a --- /dev/null +++ b/datadog-ffe-ffi/cbindgen.toml @@ -0,0 +1,36 @@ +# Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +language = "C" +cpp_compat = true +tab_width = 2 +header = """// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 +""" +include_guard = "DDOG_FFE_H" +style = "both" +pragma_once = true +no_includes = true +sys_includes = ["stdbool.h", "stddef.h", "stdint.h"] +includes = ["common.h"] + +[parse] +parse_deps = true +include = ["ddcommon-ffi", "datadog-ffe"] + +[export] +include = ["datadog-ffe-ffi"] +prefix = "ddog_ffe_" +renaming_overrides_prefixing = true + +[export.rename] +"VoidResult" = "ddog_VoidResult" +"Error" = "ddog_Error" +"Vec_u8" = "ddog_Vec_U8" + +[fn] +must_use = "DDOG_CHECK_RETURN" + +[enum] +prefix_with_name = true +rename_variants = "ScreamingSnakeCase" \ No newline at end of file diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs new file mode 100644 index 0000000000..853c09894f --- /dev/null +++ b/datadog-ffe-ffi/src/assignment.rs @@ -0,0 +1,77 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use std::ffi::{c_char, CStr}; +use std::mem::MaybeUninit; + +use datadog_ffe::rules_based::{get_assignment, now, Assignment, Configuration, EvaluationContext}; +use ddcommon_ffi::{Handle, ToInner, VoidResult}; + +use crate::error::ffe_error; + +/// Evaluates a feature flag and returns success/failure via VoidResult +/// If successful, writes the assignment to the output parameter +/// +/// # Safety +/// - `config` must be a valid Configuration handle +/// - `context` must be a valid EvaluationContext handle +/// - `flag_key` must be a valid null-terminated C string +/// - `assignment_out` must point to valid uninitialized memory for a Handle +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_get_assignment( + mut config: *mut Handle, + flag_key: *const c_char, + mut context: *mut Handle, + assignment_out: *mut MaybeUninit>, +) -> VoidResult { + if flag_key.is_null() { + return VoidResult::Err(ffe_error("flag_key cannot be null")); + } + if assignment_out.is_null() { + return VoidResult::Err(ffe_error("assignment_out cannot be null")); + } + + let config_ref = match config.to_inner_mut() { + Ok(c) => c, + Err(e) => return VoidResult::Err(ffe_error(&e.to_string())), + }; + + let context_ref = match context.to_inner_mut() { + Ok(c) => c, + Err(e) => return VoidResult::Err(ffe_error(&e.to_string())), + }; + + let flag_key_str = match CStr::from_ptr(flag_key).to_str() { + Ok(s) => s, + Err(_) => return VoidResult::Err(ffe_error("flag_key must be valid UTF-8")), + }; + + let assignment_result = get_assignment( + Some(config_ref), + flag_key_str, + context_ref, + None, + now(), + ); + + match assignment_result { + Ok(Some(assignment)) => { + assignment_out.write(MaybeUninit::new(Handle::from(assignment))); + VoidResult::Ok + } + Ok(None) => { + assignment_out.write(MaybeUninit::new(Handle::empty())); + VoidResult::Ok + } + Err(_) => VoidResult::Err(ffe_error("assignment evaluation failed")), + } +} + +/// Frees an Assignment handle +/// +/// # Safety +/// `assignment` must be a valid Assignment handle +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_assignment_drop(mut assignment: *mut Handle) { + drop(assignment.take()); +} \ No newline at end of file diff --git a/datadog-ffe-ffi/src/configuration.rs b/datadog-ffe-ffi/src/configuration.rs new file mode 100644 index 0000000000..3fc85c8f80 --- /dev/null +++ b/datadog-ffe-ffi/src/configuration.rs @@ -0,0 +1,44 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use std::ffi::{c_char, CStr}; + +use datadog_ffe::rules_based::{Configuration, UniversalFlagConfig}; +use ddcommon_ffi::{Handle, ToInner}; + +/// Creates a new Configuration from JSON bytes +/// +/// # Safety +/// `json_str` must be a valid null-terminated C string containing valid JSON +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_configuration_new( + json_str: *const c_char, +) -> Handle { + if json_str.is_null() { + return Handle::empty(); + } + + let json_cstr = match CStr::from_ptr(json_str).to_str() { + Ok(s) => s, + Err(_) => return Handle::empty(), + }; + + let json_bytes = json_cstr.as_bytes().to_vec(); + + match UniversalFlagConfig::from_json(json_bytes) { + Ok(universal_config) => { + let config = Configuration::from_server_response(universal_config); + Handle::from(config) + } + Err(_) => Handle::empty(), + } +} + +/// Frees a Configuration +/// +/// # Safety +/// `config` must be a valid Configuration handle created by `ddog_ffe_configuration_new` +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_configuration_drop(mut config: *mut Handle) { + drop(config.take()); +} \ No newline at end of file diff --git a/datadog-ffe-ffi/src/error.rs b/datadog-ffe-ffi/src/error.rs new file mode 100644 index 0000000000..3493a98727 --- /dev/null +++ b/datadog-ffe-ffi/src/error.rs @@ -0,0 +1,10 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use ddcommon_ffi::Error; + +const NULL_POINTER_ERROR: &str = "null pointer provided"; + +pub fn ffe_error(msg: &str) -> Error { + Error::from(msg) +} \ No newline at end of file diff --git a/datadog-ffe-ffi/src/evaluation_context.rs b/datadog-ffe-ffi/src/evaluation_context.rs new file mode 100644 index 0000000000..d5620903ec --- /dev/null +++ b/datadog-ffe-ffi/src/evaluation_context.rs @@ -0,0 +1,85 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::ffi::{c_char, CStr}; +use std::sync::Arc; + +use datadog_ffe::rules_based::{Attribute, EvaluationContext, Str}; +use ddcommon_ffi::{Handle, ToInner}; + +/// Creates a new EvaluationContext with the given targeting key +/// +/// # Safety +/// `targeting_key` must be a valid null-terminated C string +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( + targeting_key: *const c_char, +) -> Handle { + if targeting_key.is_null() { + return Handle::empty(); + } + + let key_cstr = match CStr::from_ptr(targeting_key).to_str() { + Ok(s) => s, + Err(_) => return Handle::empty(), + }; + + let key = Str::from(key_cstr.to_string()); + let attributes = Arc::new(HashMap::::new()); + let context = EvaluationContext::new(key, attributes); + + Handle::from(context) +} + +/// Creates a new EvaluationContext with the given targeting key and a single string attribute +/// +/// # Safety +/// `targeting_key`, `attr_name`, and `attr_value` must be valid null-terminated C strings +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_evaluation_context_new_with_attribute( + targeting_key: *const c_char, + attr_name: *const c_char, + attr_value: *const c_char, +) -> Handle { + if targeting_key.is_null() || attr_name.is_null() || attr_value.is_null() { + return Handle::empty(); + } + + let key_str = match CStr::from_ptr(targeting_key).to_str() { + Ok(s) => s, + Err(_) => return Handle::empty(), + }; + + let name_str = match CStr::from_ptr(attr_name).to_str() { + Ok(s) => s, + Err(_) => return Handle::empty(), + }; + + let value_str = match CStr::from_ptr(attr_value).to_str() { + Ok(s) => s, + Err(_) => return Handle::empty(), + }; + + let key = Str::from(key_str.to_string()); + let mut attributes = HashMap::::new(); + attributes.insert( + Str::from(name_str.to_string()), + Attribute::from(value_str.to_string()), + ); + let attributes = Arc::new(attributes); + let context = EvaluationContext::new(key, attributes); + + Handle::from(context) +} + +/// Frees an EvaluationContext +/// +/// # Safety +/// `context` must be a valid EvaluationContext handle created by `ddog_ffe_evaluation_context_new` +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_evaluation_context_drop( + mut context: *mut Handle, +) { + drop(context.take()); +} \ No newline at end of file diff --git a/datadog-ffe-ffi/src/lib.rs b/datadog-ffe-ffi/src/lib.rs new file mode 100644 index 0000000000..8623a344f4 --- /dev/null +++ b/datadog-ffe-ffi/src/lib.rs @@ -0,0 +1,12 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +mod assignment; +mod configuration; +mod error; +mod evaluation_context; + +pub use assignment::*; +pub use configuration::*; +pub use error::*; +pub use evaluation_context::*; \ No newline at end of file From 96c3e15d97acc4e7f6c8e84ac442efd33c97a180 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Fri, 24 Oct 2025 09:15:52 -0700 Subject: [PATCH 02/43] Fix missing newlines at ends of files --- datadog-ffe-ffi/Cargo.toml | 2 +- datadog-ffe-ffi/build.rs | 2 +- datadog-ffe-ffi/cbindgen.toml | 2 +- datadog-ffe-ffi/src/assignment.rs | 2 +- datadog-ffe-ffi/src/configuration.rs | 2 +- datadog-ffe-ffi/src/error.rs | 2 +- datadog-ffe-ffi/src/evaluation_context.rs | 2 +- datadog-ffe-ffi/src/lib.rs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/datadog-ffe-ffi/Cargo.toml b/datadog-ffe-ffi/Cargo.toml index 968b5838bd..8d68e711f4 100644 --- a/datadog-ffe-ffi/Cargo.toml +++ b/datadog-ffe-ffi/Cargo.toml @@ -23,4 +23,4 @@ build_common = { path = "../build-common" } datadog-ffe = { path = "../datadog-ffe" } ddcommon-ffi = { path = "../ddcommon-ffi", default-features = false } -[dev-dependencies] \ No newline at end of file +[dev-dependencies] diff --git a/datadog-ffe-ffi/build.rs b/datadog-ffe-ffi/build.rs index c7ecf3d90d..1eb343fa9b 100644 --- a/datadog-ffe-ffi/build.rs +++ b/datadog-ffe-ffi/build.rs @@ -5,4 +5,4 @@ fn main() { println!("cargo:rerun-if-changed=build.rs"); build_common::generate_and_configure_header("datadog_ffe.h"); -} \ No newline at end of file +} diff --git a/datadog-ffe-ffi/cbindgen.toml b/datadog-ffe-ffi/cbindgen.toml index f65615962a..24f8c7bc52 100644 --- a/datadog-ffe-ffi/cbindgen.toml +++ b/datadog-ffe-ffi/cbindgen.toml @@ -33,4 +33,4 @@ must_use = "DDOG_CHECK_RETURN" [enum] prefix_with_name = true -rename_variants = "ScreamingSnakeCase" \ No newline at end of file +rename_variants = "ScreamingSnakeCase" diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 853c09894f..ef63b6f6f7 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -74,4 +74,4 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( #[no_mangle] pub unsafe extern "C" fn ddog_ffe_assignment_drop(mut assignment: *mut Handle) { drop(assignment.take()); -} \ No newline at end of file +} diff --git a/datadog-ffe-ffi/src/configuration.rs b/datadog-ffe-ffi/src/configuration.rs index 3fc85c8f80..c229b84a4d 100644 --- a/datadog-ffe-ffi/src/configuration.rs +++ b/datadog-ffe-ffi/src/configuration.rs @@ -41,4 +41,4 @@ pub unsafe extern "C" fn ddog_ffe_configuration_new( #[no_mangle] pub unsafe extern "C" fn ddog_ffe_configuration_drop(mut config: *mut Handle) { drop(config.take()); -} \ No newline at end of file +} diff --git a/datadog-ffe-ffi/src/error.rs b/datadog-ffe-ffi/src/error.rs index 3493a98727..dd7f01a3ac 100644 --- a/datadog-ffe-ffi/src/error.rs +++ b/datadog-ffe-ffi/src/error.rs @@ -7,4 +7,4 @@ const NULL_POINTER_ERROR: &str = "null pointer provided"; pub fn ffe_error(msg: &str) -> Error { Error::from(msg) -} \ No newline at end of file +} diff --git a/datadog-ffe-ffi/src/evaluation_context.rs b/datadog-ffe-ffi/src/evaluation_context.rs index d5620903ec..3339016022 100644 --- a/datadog-ffe-ffi/src/evaluation_context.rs +++ b/datadog-ffe-ffi/src/evaluation_context.rs @@ -82,4 +82,4 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_drop( mut context: *mut Handle, ) { drop(context.take()); -} \ No newline at end of file +} diff --git a/datadog-ffe-ffi/src/lib.rs b/datadog-ffe-ffi/src/lib.rs index 8623a344f4..a4017a60f3 100644 --- a/datadog-ffe-ffi/src/lib.rs +++ b/datadog-ffe-ffi/src/lib.rs @@ -9,4 +9,4 @@ mod evaluation_context; pub use assignment::*; pub use configuration::*; pub use error::*; -pub use evaluation_context::*; \ No newline at end of file +pub use evaluation_context::*; From 37764b041726f772b4b115971d58016ccccf3ef5 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Fri, 24 Oct 2025 11:01:20 -0700 Subject: [PATCH 03/43] Fix Rust formatting issues in datadog-ffe-ffi - Remove trailing whitespace from doc comments - Reformat function call in assignment.rs to fit line length constraints --- datadog-ffe-ffi/src/assignment.rs | 15 +++++---------- datadog-ffe-ffi/src/configuration.rs | 4 ++-- datadog-ffe-ffi/src/evaluation_context.rs | 6 +++--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index ef63b6f6f7..ab5ece3fea 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -10,8 +10,8 @@ use ddcommon_ffi::{Handle, ToInner, VoidResult}; use crate::error::ffe_error; /// Evaluates a feature flag and returns success/failure via VoidResult -/// If successful, writes the assignment to the output parameter -/// +/// If successful, writes the assignment to the output parameter +/// /// # Safety /// - `config` must be a valid Configuration handle /// - `context` must be a valid EvaluationContext handle @@ -46,13 +46,8 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( Err(_) => return VoidResult::Err(ffe_error("flag_key must be valid UTF-8")), }; - let assignment_result = get_assignment( - Some(config_ref), - flag_key_str, - context_ref, - None, - now(), - ); + let assignment_result = + get_assignment(Some(config_ref), flag_key_str, context_ref, None, now()); match assignment_result { Ok(Some(assignment)) => { @@ -68,7 +63,7 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( } /// Frees an Assignment handle -/// +/// /// # Safety /// `assignment` must be a valid Assignment handle #[no_mangle] diff --git a/datadog-ffe-ffi/src/configuration.rs b/datadog-ffe-ffi/src/configuration.rs index c229b84a4d..d8126c3589 100644 --- a/datadog-ffe-ffi/src/configuration.rs +++ b/datadog-ffe-ffi/src/configuration.rs @@ -7,7 +7,7 @@ use datadog_ffe::rules_based::{Configuration, UniversalFlagConfig}; use ddcommon_ffi::{Handle, ToInner}; /// Creates a new Configuration from JSON bytes -/// +/// /// # Safety /// `json_str` must be a valid null-terminated C string containing valid JSON #[no_mangle] @@ -35,7 +35,7 @@ pub unsafe extern "C" fn ddog_ffe_configuration_new( } /// Frees a Configuration -/// +/// /// # Safety /// `config` must be a valid Configuration handle created by `ddog_ffe_configuration_new` #[no_mangle] diff --git a/datadog-ffe-ffi/src/evaluation_context.rs b/datadog-ffe-ffi/src/evaluation_context.rs index 3339016022..c219e0f85d 100644 --- a/datadog-ffe-ffi/src/evaluation_context.rs +++ b/datadog-ffe-ffi/src/evaluation_context.rs @@ -9,7 +9,7 @@ use datadog_ffe::rules_based::{Attribute, EvaluationContext, Str}; use ddcommon_ffi::{Handle, ToInner}; /// Creates a new EvaluationContext with the given targeting key -/// +/// /// # Safety /// `targeting_key` must be a valid null-terminated C string #[no_mangle] @@ -33,7 +33,7 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( } /// Creates a new EvaluationContext with the given targeting key and a single string attribute -/// +/// /// # Safety /// `targeting_key`, `attr_name`, and `attr_value` must be valid null-terminated C strings #[no_mangle] @@ -74,7 +74,7 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_new_with_attribute( } /// Frees an EvaluationContext -/// +/// /// # Safety /// `context` must be a valid EvaluationContext handle created by `ddog_ffe_evaluation_context_new` #[no_mangle] From c9272697574516cb5d29a111bb63a23fd86f2f26 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Fri, 24 Oct 2025 11:07:27 -0700 Subject: [PATCH 04/43] Remove unused NULL_POINTER_ERROR constant from error.rs --- datadog-ffe-ffi/src/error.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/datadog-ffe-ffi/src/error.rs b/datadog-ffe-ffi/src/error.rs index dd7f01a3ac..9749cd14b6 100644 --- a/datadog-ffe-ffi/src/error.rs +++ b/datadog-ffe-ffi/src/error.rs @@ -3,8 +3,6 @@ use ddcommon_ffi::Error; -const NULL_POINTER_ERROR: &str = "null pointer provided"; - pub fn ffe_error(msg: &str) -> Error { Error::from(msg) } From dfc60e3303bea125c252caf1b91e0054759550a1 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Fri, 24 Oct 2025 11:09:45 -0700 Subject: [PATCH 05/43] Fix formatting in assignment.rs by removing trailing whitespace from doc comments --- datadog-ffe-ffi/src/assignment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index ab5ece3fea..510c3f27a9 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -14,7 +14,7 @@ use crate::error::ffe_error; /// /// # Safety /// - `config` must be a valid Configuration handle -/// - `context` must be a valid EvaluationContext handle +/// - `context` must be a valid EvaluationContext handle /// - `flag_key` must be a valid null-terminated C string /// - `assignment_out` must point to valid uninitialized memory for a Handle #[no_mangle] From 5dfbd4664588e46ea2f7d63e553030b2f401f76c Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Fri, 24 Oct 2025 12:12:34 -0700 Subject: [PATCH 06/43] Update LICENSE-3rdparty.yml to include datadog-ffe-ffi --- LICENSE-3rdparty.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE-3rdparty.yml b/LICENSE-3rdparty.yml index 1c5457de32..3cc072e0aa 100644 --- a/LICENSE-3rdparty.yml +++ b/LICENSE-3rdparty.yml @@ -1,4 +1,4 @@ -root_name: builder, build_common, tools, datadog-alloc, datadog-crashtracker, ddcommon, ddtelemetry, datadog-ddsketch, cc_utils, datadog-crashtracker-ffi, ddcommon-ffi, datadog-ffe, datadog-ipc, datadog-ipc-macros, tarpc, tarpc-plugins, tinybytes, spawn_worker, datadog-library-config, datadog-library-config-ffi, datadog-live-debugger, datadog-live-debugger-ffi, datadog-profiling, datadog-profiling-protobuf, datadog-profiling-ffi, data-pipeline-ffi, data-pipeline, datadog-trace-protobuf, datadog-trace-stats, datadog-trace-utils, datadog-trace-normalization, dogstatsd-client, datadog-log, datadog-log-ffi, ddsketch-ffi, ddtelemetry-ffi, symbolizer-ffi, datadog-profiling-replayer, datadog-remote-config, datadog-sidecar, datadog-sidecar-macros, datadog-sidecar-ffi, datadog-trace-obfuscation, datadog-tracer-flare, sidecar_mockgen, test_spawn_from_lib +root_name: builder, build_common, tools, datadog-alloc, datadog-crashtracker, ddcommon, ddtelemetry, datadog-ddsketch, cc_utils, datadog-crashtracker-ffi, ddcommon-ffi, datadog-ffe, datadog-ffe-ffi, datadog-ipc, datadog-ipc-macros, tarpc, tarpc-plugins, tinybytes, spawn_worker, datadog-library-config, datadog-library-config-ffi, datadog-live-debugger, datadog-live-debugger-ffi, datadog-profiling, datadog-profiling-protobuf, datadog-profiling-ffi, data-pipeline-ffi, data-pipeline, datadog-trace-protobuf, datadog-trace-stats, datadog-trace-utils, datadog-trace-normalization, dogstatsd-client, datadog-log, datadog-log-ffi, ddsketch-ffi, ddtelemetry-ffi, symbolizer-ffi, datadog-profiling-replayer, datadog-remote-config, datadog-sidecar, datadog-sidecar-macros, datadog-sidecar-ffi, datadog-trace-obfuscation, datadog-tracer-flare, sidecar_mockgen, test_spawn_from_lib third_party_libraries: - package_name: addr2line package_version: 0.24.2 From 911cf1f94f2b183d29734d9e1d3e316f95ce4325 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Mon, 27 Oct 2025 13:21:35 -0700 Subject: [PATCH 07/43] Add datadog-ffe-ffi Cargo.toml to Dockerfile.build --- tools/docker/Dockerfile.build | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/docker/Dockerfile.build b/tools/docker/Dockerfile.build index cb5153c6fa..c824bfac0d 100644 --- a/tools/docker/Dockerfile.build +++ b/tools/docker/Dockerfile.build @@ -115,6 +115,7 @@ COPY "bin_tests/Cargo.toml" "bin_tests/" COPY "tinybytes/Cargo.toml" "tinybytes/" COPY "builder/Cargo.toml" "builder/" COPY "datadog-ffe/Cargo.toml" "datadog-ffe/" +COPY "datadog-ffe-ffi/Cargo.toml" "datadog-ffe-ffi/" RUN find -name "Cargo.toml" | sed -e s#Cargo.toml#src/lib.rs#g | xargs -n 1 sh -c 'mkdir -p $(dirname $1); touch $1; echo $1' create_stubs RUN echo \ bin_tests/src/bin/crashtracker_bin_test.rs \ From 47e0a5c605d2154a890a060cfb144ebddf2eecdc Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Mon, 27 Oct 2025 14:01:04 -0700 Subject: [PATCH 08/43] Set datadog-ffe-ffi version to 0.1.0 to match pre-release status --- datadog-ffe-ffi/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datadog-ffe-ffi/Cargo.toml b/datadog-ffe-ffi/Cargo.toml index 8d68e711f4..88bbec6624 100644 --- a/datadog-ffe-ffi/Cargo.toml +++ b/datadog-ffe-ffi/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "datadog-ffe-ffi" edition.workspace = true -version.workspace = true +version = "0.1.0" rust-version.workspace = true license.workspace = true From 168718ad6c9d13a239137e2514e44ca94329f0f2 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Mon, 27 Oct 2025 14:02:33 -0700 Subject: [PATCH 09/43] Update Cargo.lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 44eecab590..683dfc86ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1437,7 +1437,7 @@ dependencies = [ [[package]] name = "datadog-ffe-ffi" -version = "22.1.0" +version = "0.1.0" dependencies = [ "build_common", "datadog-ffe", From 43ec366082c700ebdf413509c18d0fc6d8d207d7 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Mon, 27 Oct 2025 14:20:53 -0700 Subject: [PATCH 10/43] Refactor build script and cbindgen configuration - Updated build.rs to use a variable for the header name and improved readability. - Modified cbindgen.toml to reorganize export settings and enforce naming conventions for types and functions. - Added Clippy lints in lib.rs to enhance code quality and prevent common pitfalls. --- datadog-ffe-ffi/build.rs | 8 +++++--- datadog-ffe-ffi/cbindgen.toml | 16 ++++++++++------ datadog-ffe-ffi/src/lib.rs | 6 ++++++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/datadog-ffe-ffi/build.rs b/datadog-ffe-ffi/build.rs index 1eb343fa9b..b80ec6a1c3 100644 --- a/datadog-ffe-ffi/build.rs +++ b/datadog-ffe-ffi/build.rs @@ -1,8 +1,10 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +extern crate build_common; -fn main() { - println!("cargo:rerun-if-changed=build.rs"); +use build_common::generate_and_configure_header; - build_common::generate_and_configure_header("datadog_ffe.h"); +fn main() { + let header_name = "datadog_ffe.h"; + generate_and_configure_header(header_name); } diff --git a/datadog-ffe-ffi/cbindgen.toml b/datadog-ffe-ffi/cbindgen.toml index 24f8c7bc52..0bb3f5f2fc 100644 --- a/datadog-ffe-ffi/cbindgen.toml +++ b/datadog-ffe-ffi/cbindgen.toml @@ -10,14 +10,11 @@ header = """// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ include_guard = "DDOG_FFE_H" style = "both" pragma_once = true + no_includes = true sys_includes = ["stdbool.h", "stddef.h", "stdint.h"] includes = ["common.h"] -[parse] -parse_deps = true -include = ["ddcommon-ffi", "datadog-ffe"] - [export] include = ["datadog-ffe-ffi"] prefix = "ddog_ffe_" @@ -28,9 +25,16 @@ renaming_overrides_prefixing = true "Error" = "ddog_Error" "Vec_u8" = "ddog_Vec_U8" -[fn] -must_use = "DDOG_CHECK_RETURN" +[export.mangle] +rename_types = "PascalCase" [enum] prefix_with_name = true rename_variants = "ScreamingSnakeCase" + +[fn] +must_use = "DDOG_CHECK_RETURN" + +[parse] +parse_deps = true +include = ["ddcommon-ffi", "datadog-ffe"] diff --git a/datadog-ffe-ffi/src/lib.rs b/datadog-ffe-ffi/src/lib.rs index a4017a60f3..40858896ae 100644 --- a/datadog-ffe-ffi/src/lib.rs +++ b/datadog-ffe-ffi/src/lib.rs @@ -1,6 +1,12 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +#![cfg_attr(not(test), deny(clippy::panic))] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![cfg_attr(not(test), deny(clippy::expect_used))] +#![cfg_attr(not(test), deny(clippy::todo))] +#![cfg_attr(not(test), deny(clippy::unimplemented))] + mod assignment; mod configuration; mod error; From a3c6612c4b32634da410cb671c819f0c6daa9742 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 28 Oct 2025 08:09:01 -0700 Subject: [PATCH 11/43] Update CODEOWNERS to include datadog-ffe-ffi and team --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 959da59757..df4f733b51 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -23,6 +23,7 @@ data-pipeline*/ @Datadog/libdatadog-apm datadog-tracer-flare @Datadog/libdatadog-apm ddsketch @Datadog/libdatadog-apm @Datadog/libdatadog-telemetry datadog-ffe @Datadog/feature-flagging-and-experimentation-sdk +datadog-ffe-ffi @Datadog/feature-flagging-and-experimentation-sdk # Most of the bin_tests are owned by the profiling team, but some are owned by the core team bin_tests/ @Datadog/libdatadog-profiling From 10298cb606af623d7b0a191e544c6ae133f00e1d Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 28 Oct 2025 11:12:00 -0700 Subject: [PATCH 12/43] Update datadog-ffe-ffi/Cargo.toml Pin datadog-ffe to exact version Co-authored-by: Oleksii Shmalko --- datadog-ffe-ffi/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datadog-ffe-ffi/Cargo.toml b/datadog-ffe-ffi/Cargo.toml index 88bbec6624..ec8304cb1d 100644 --- a/datadog-ffe-ffi/Cargo.toml +++ b/datadog-ffe-ffi/Cargo.toml @@ -20,7 +20,7 @@ cbindgen = ["build_common/cbindgen", "ddcommon-ffi/cbindgen"] build_common = { path = "../build-common" } [dependencies] -datadog-ffe = { path = "../datadog-ffe" } +datadog-ffe = { path = "../datadog-ffe", version = "=0.1.0" } ddcommon-ffi = { path = "../ddcommon-ffi", default-features = false } [dev-dependencies] From c453cb960c9e27584a9e5c5dba347dbcc14fe7da Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Tue, 28 Oct 2025 23:11:00 +0200 Subject: [PATCH 13/43] refactor(ffe): simplify error handling (#1285) refactor(ffe): simplify error handling Merge branch 'sameerank/FFL-1284-Create-datadog-ffe-ffi-crate' into ffe-simplify-error-handling Co-authored-by: sameerank --- Cargo.lock | 2 + datadog-ffe-ffi/Cargo.toml | 2 + datadog-ffe-ffi/src/assignment.rs | 65 ++++++++++------------------ datadog-ffe-ffi/src/configuration.rs | 38 ++++++++-------- datadog-ffe-ffi/src/error.rs | 8 ---- datadog-ffe-ffi/src/lib.rs | 2 - 6 files changed, 43 insertions(+), 74 deletions(-) delete mode 100644 datadog-ffe-ffi/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 683dfc86ec..d5393c8f19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1439,9 +1439,11 @@ dependencies = [ name = "datadog-ffe-ffi" version = "0.1.0" dependencies = [ + "anyhow", "build_common", "datadog-ffe", "ddcommon-ffi", + "function_name", ] [[package]] diff --git a/datadog-ffe-ffi/Cargo.toml b/datadog-ffe-ffi/Cargo.toml index ec8304cb1d..c2c3ba2bca 100644 --- a/datadog-ffe-ffi/Cargo.toml +++ b/datadog-ffe-ffi/Cargo.toml @@ -20,7 +20,9 @@ cbindgen = ["build_common/cbindgen", "ddcommon-ffi/cbindgen"] build_common = { path = "../build-common" } [dependencies] +anyhow = "1.0.93" datadog-ffe = { path = "../datadog-ffe", version = "=0.1.0" } ddcommon-ffi = { path = "../ddcommon-ffi", default-features = false } +function_name = "0.3.0" [dev-dependencies] diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 510c3f27a9..52e28157ad 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -2,64 +2,43 @@ // SPDX-License-Identifier: Apache-2.0 use std::ffi::{c_char, CStr}; -use std::mem::MaybeUninit; -use datadog_ffe::rules_based::{get_assignment, now, Assignment, Configuration, EvaluationContext}; -use ddcommon_ffi::{Handle, ToInner, VoidResult}; +use anyhow::ensure; +use function_name::named; -use crate::error::ffe_error; +use datadog_ffe::rules_based::{get_assignment, now, Assignment, Configuration, EvaluationContext}; +use ddcommon_ffi::{wrap_with_ffi_result, Handle, Result, ToInner}; -/// Evaluates a feature flag and returns success/failure via VoidResult -/// If successful, writes the assignment to the output parameter +/// Evaluates a feature flag. /// /// # Safety /// - `config` must be a valid Configuration handle /// - `context` must be a valid EvaluationContext handle /// - `flag_key` must be a valid null-terminated C string -/// - `assignment_out` must point to valid uninitialized memory for a Handle #[no_mangle] +#[named] pub unsafe extern "C" fn ddog_ffe_get_assignment( - mut config: *mut Handle, + mut config: Handle, flag_key: *const c_char, - mut context: *mut Handle, - assignment_out: *mut MaybeUninit>, -) -> VoidResult { - if flag_key.is_null() { - return VoidResult::Err(ffe_error("flag_key cannot be null")); - } - if assignment_out.is_null() { - return VoidResult::Err(ffe_error("assignment_out cannot be null")); - } - - let config_ref = match config.to_inner_mut() { - Ok(c) => c, - Err(e) => return VoidResult::Err(ffe_error(&e.to_string())), - }; + mut context: Handle, +) -> Result> { + wrap_with_ffi_result!({ + ensure!(!flag_key.is_null(), "flag_key must not be NULL"); - let context_ref = match context.to_inner_mut() { - Ok(c) => c, - Err(e) => return VoidResult::Err(ffe_error(&e.to_string())), - }; + let config = config.to_inner_mut()?; + let context = context.to_inner_mut()?; + let flag_key = CStr::from_ptr(flag_key).to_str()?; - let flag_key_str = match CStr::from_ptr(flag_key).to_str() { - Ok(s) => s, - Err(_) => return VoidResult::Err(ffe_error("flag_key must be valid UTF-8")), - }; + let assignment_result = get_assignment(Some(config), flag_key, context, None, now())?; - let assignment_result = - get_assignment(Some(config_ref), flag_key_str, context_ref, None, now()); + let handle = if let Some(assignment) = assignment_result { + Handle::from(assignment) + } else { + Handle::empty() + }; - match assignment_result { - Ok(Some(assignment)) => { - assignment_out.write(MaybeUninit::new(Handle::from(assignment))); - VoidResult::Ok - } - Ok(None) => { - assignment_out.write(MaybeUninit::new(Handle::empty())); - VoidResult::Ok - } - Err(_) => VoidResult::Err(ffe_error("assignment evaluation failed")), - } + Ok(handle) + }) } /// Frees an Assignment handle diff --git a/datadog-ffe-ffi/src/configuration.rs b/datadog-ffe-ffi/src/configuration.rs index d8126c3589..9cc28ec345 100644 --- a/datadog-ffe-ffi/src/configuration.rs +++ b/datadog-ffe-ffi/src/configuration.rs @@ -3,35 +3,31 @@ use std::ffi::{c_char, CStr}; +use anyhow::ensure; +use function_name::named; + use datadog_ffe::rules_based::{Configuration, UniversalFlagConfig}; -use ddcommon_ffi::{Handle, ToInner}; +use ddcommon_ffi::{wrap_with_ffi_result, Handle, Result, ToInner}; /// Creates a new Configuration from JSON bytes /// /// # Safety -/// `json_str` must be a valid null-terminated C string containing valid JSON +/// `json_str` must be a valid C string. #[no_mangle] +#[named] pub unsafe extern "C" fn ddog_ffe_configuration_new( json_str: *const c_char, -) -> Handle { - if json_str.is_null() { - return Handle::empty(); - } - - let json_cstr = match CStr::from_ptr(json_str).to_str() { - Ok(s) => s, - Err(_) => return Handle::empty(), - }; - - let json_bytes = json_cstr.as_bytes().to_vec(); - - match UniversalFlagConfig::from_json(json_bytes) { - Ok(universal_config) => { - let config = Configuration::from_server_response(universal_config); - Handle::from(config) - } - Err(_) => Handle::empty(), - } +) -> Result> { + wrap_with_ffi_result!({ + ensure!(!json_str.is_null(), "json_str must not be NULL"); + + let json_bytes = CStr::from_ptr(json_str).to_bytes().to_vec(); + + let configuration = + Configuration::from_server_response(UniversalFlagConfig::from_json(json_bytes)?); + + Ok(Handle::from(configuration)) + }) } /// Frees a Configuration diff --git a/datadog-ffe-ffi/src/error.rs b/datadog-ffe-ffi/src/error.rs deleted file mode 100644 index 9749cd14b6..0000000000 --- a/datadog-ffe-ffi/src/error.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ -// SPDX-License-Identifier: Apache-2.0 - -use ddcommon_ffi::Error; - -pub fn ffe_error(msg: &str) -> Error { - Error::from(msg) -} diff --git a/datadog-ffe-ffi/src/lib.rs b/datadog-ffe-ffi/src/lib.rs index 40858896ae..094a8ab080 100644 --- a/datadog-ffe-ffi/src/lib.rs +++ b/datadog-ffe-ffi/src/lib.rs @@ -9,10 +9,8 @@ mod assignment; mod configuration; -mod error; mod evaluation_context; pub use assignment::*; pub use configuration::*; -pub use error::*; pub use evaluation_context::*; From 1e0586a6280360ca17c347481a07a5c12dd52a9b Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 28 Oct 2025 23:45:26 -0700 Subject: [PATCH 14/43] Replace single-attribute constructor with multi-attribute API Replace ddog_ffe_evaluation_context_new_with_attribute with ddog_ffe_evaluation_context_new_with_attributes that accepts an array of AttributePair structs. --- datadog-ffe-ffi/src/evaluation_context.rs | 65 +++++++++++++++-------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/datadog-ffe-ffi/src/evaluation_context.rs b/datadog-ffe-ffi/src/evaluation_context.rs index c219e0f85d..5bf9d0d228 100644 --- a/datadog-ffe-ffi/src/evaluation_context.rs +++ b/datadog-ffe-ffi/src/evaluation_context.rs @@ -32,17 +32,27 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( Handle::from(context) } -/// Creates a new EvaluationContext with the given targeting key and a single string attribute +/// Represents a key-value pair for attributes +#[repr(C)] +pub struct AttributePair { + pub name: *const c_char, + pub value: *const c_char, +} + +/// Creates a new EvaluationContext with the given targeting key and multiple attributes /// /// # Safety -/// `targeting_key`, `attr_name`, and `attr_value` must be valid null-terminated C strings +/// - `targeting_key` must be a valid null-terminated C string +/// - `attributes` must point to a valid array of `AttributePair` structs (can be null if attributes_count is 0) +/// - Each `AttributePair.name` and `AttributePair.value` must be valid null-terminated C strings +/// - `attributes_count` must accurately represent the length of the `attributes` array #[no_mangle] -pub unsafe extern "C" fn ddog_ffe_evaluation_context_new_with_attribute( +pub unsafe extern "C" fn ddog_ffe_evaluation_context_new_with_attributes( targeting_key: *const c_char, - attr_name: *const c_char, - attr_value: *const c_char, + attributes: *const AttributePair, + attributes_count: usize, ) -> Handle { - if targeting_key.is_null() || attr_name.is_null() || attr_value.is_null() { + if targeting_key.is_null() || (attributes_count > 0 && attributes.is_null()) { return Handle::empty(); } @@ -51,24 +61,35 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_new_with_attribute( Err(_) => return Handle::empty(), }; - let name_str = match CStr::from_ptr(attr_name).to_str() { - Ok(s) => s, - Err(_) => return Handle::empty(), - }; + let key = Str::from(key_str.to_string()); + let mut attr_map = HashMap::::new(); - let value_str = match CStr::from_ptr(attr_value).to_str() { - Ok(s) => s, - Err(_) => return Handle::empty(), - }; + // Process attributes array + for i in 0..attributes_count { + let attr_pair = &*attributes.add(i); + + if attr_pair.name.is_null() || attr_pair.value.is_null() { + continue; // Skip invalid pairs + } - let key = Str::from(key_str.to_string()); - let mut attributes = HashMap::::new(); - attributes.insert( - Str::from(name_str.to_string()), - Attribute::from(value_str.to_string()), - ); - let attributes = Arc::new(attributes); - let context = EvaluationContext::new(key, attributes); + let name_str = match CStr::from_ptr(attr_pair.name).to_str() { + Ok(s) => s, + Err(_) => continue, // Skip invalid UTF-8 + }; + + let value_str = match CStr::from_ptr(attr_pair.value).to_str() { + Ok(s) => s, + Err(_) => continue, // Skip invalid UTF-8 + }; + + attr_map.insert( + Str::from(name_str.to_string()), + Attribute::from(value_str.to_string()), + ); + } + + let attributes_arc = Arc::new(attr_map); + let context = EvaluationContext::new(key, attributes_arc); Handle::from(context) } From 2fcf32bfb8a9c045f07f8d9d72f4f77079a4d9f3 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Tue, 28 Oct 2025 23:47:01 -0700 Subject: [PATCH 15/43] Remove unnecessary whitespace in evaluation_context.rs --- datadog-ffe-ffi/src/evaluation_context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datadog-ffe-ffi/src/evaluation_context.rs b/datadog-ffe-ffi/src/evaluation_context.rs index 5bf9d0d228..7cd941b0ab 100644 --- a/datadog-ffe-ffi/src/evaluation_context.rs +++ b/datadog-ffe-ffi/src/evaluation_context.rs @@ -67,7 +67,7 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_new_with_attributes( // Process attributes array for i in 0..attributes_count { let attr_pair = &*attributes.add(i); - + if attr_pair.name.is_null() || attr_pair.value.is_null() { continue; // Skip invalid pairs } From 7b7af45c3f493b3c91c152bf5303f71d53eb918b Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Wed, 29 Oct 2025 07:31:39 -0700 Subject: [PATCH 16/43] Fix documentation formatting in evaluation_context.rs --- Cargo.toml | 1 + datadog-ffe-ffi/src/evaluation_context.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index eb00d087d9..b8e0953f4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,3 +106,4 @@ codegen-units = 1 # so benchmarks are not measuring the same thing as the release build. This patch removes # the default dependency on libm. A PR will be opened to proptest to make this optional. proptest = { git = 'https://github.com/bantonsson/proptest.git', branch = "ban/avoid-libm-in-std" } +C diff --git a/datadog-ffe-ffi/src/evaluation_context.rs b/datadog-ffe-ffi/src/evaluation_context.rs index 7cd941b0ab..29f2d02962 100644 --- a/datadog-ffe-ffi/src/evaluation_context.rs +++ b/datadog-ffe-ffi/src/evaluation_context.rs @@ -43,7 +43,8 @@ pub struct AttributePair { /// /// # Safety /// - `targeting_key` must be a valid null-terminated C string -/// - `attributes` must point to a valid array of `AttributePair` structs (can be null if attributes_count is 0) +/// - `attributes` must point to a valid array of `AttributePair` structs (can be null if +/// attributes_count is 0) /// - Each `AttributePair.name` and `AttributePair.value` must be valid null-terminated C strings /// - `attributes_count` must accurately represent the length of the `attributes` array #[no_mangle] From 2d11e90b5b16f2997264eb9a0ee79d06addcc931 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Wed, 29 Oct 2025 07:40:55 -0700 Subject: [PATCH 17/43] Fix Cargo.toml --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b8e0953f4e..eb00d087d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,4 +106,3 @@ codegen-units = 1 # so benchmarks are not measuring the same thing as the release build. This patch removes # the default dependency on libm. A PR will be opened to proptest to make this optional. proptest = { git = 'https://github.com/bantonsson/proptest.git', branch = "ban/avoid-libm-in-std" } -C From 7a7d500636aa88ec7e533c36f49a8f3a6aa856dc Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Wed, 29 Oct 2025 11:51:43 -0700 Subject: [PATCH 18/43] Prevent malloc error in drop functions Handle take() errors gracefully in ddog_ffe_configuration_drop and ddog_ffe_assignment_drop to prevent malloc corruption from double-free or use-after-free scenarios. Previously, take() failures would panic and cause "pointer being freed was not allocated" errors. --- datadog-ffe-ffi/src/assignment.rs | 6 +++++- datadog-ffe-ffi/src/configuration.rs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 52e28157ad..2705ad1b20 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -47,5 +47,9 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( /// `assignment` must be a valid Assignment handle #[no_mangle] pub unsafe extern "C" fn ddog_ffe_assignment_drop(mut assignment: *mut Handle) { - drop(assignment.take()); + // Handle take() errors gracefully to prevent malloc corruption from double-free + // or use-after-free scenarios. If take() fails, silently ignore to avoid panics. + if let Ok(inner) = assignment.take() { + drop(inner); + } } diff --git a/datadog-ffe-ffi/src/configuration.rs b/datadog-ffe-ffi/src/configuration.rs index 9cc28ec345..33c8164ce4 100644 --- a/datadog-ffe-ffi/src/configuration.rs +++ b/datadog-ffe-ffi/src/configuration.rs @@ -36,5 +36,9 @@ pub unsafe extern "C" fn ddog_ffe_configuration_new( /// `config` must be a valid Configuration handle created by `ddog_ffe_configuration_new` #[no_mangle] pub unsafe extern "C" fn ddog_ffe_configuration_drop(mut config: *mut Handle) { - drop(config.take()); + // Handle take() errors gracefully to prevent malloc corruption from double-free + // or use-after-free scenarios. If take() fails, silently ignore to avoid panics. + if let Ok(inner) = config.take() { + drop(inner); + } } From 006b7097b57cc6dea78b674503016b9c7ddb02c5 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Wed, 29 Oct 2025 12:28:53 -0700 Subject: [PATCH 19/43] Update drop functions to simplify error handling --- datadog-ffe-ffi/src/assignment.rs | 10 +++------- datadog-ffe-ffi/src/configuration.rs | 6 +----- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 2705ad1b20..690bae15e6 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -12,13 +12,13 @@ use ddcommon_ffi::{wrap_with_ffi_result, Handle, Result, ToInner}; /// Evaluates a feature flag. /// /// # Safety -/// - `config` must be a valid Configuration handle +/// - `config` must be a valid Configuration handle pointer /// - `context` must be a valid EvaluationContext handle /// - `flag_key` must be a valid null-terminated C string #[no_mangle] #[named] pub unsafe extern "C" fn ddog_ffe_get_assignment( - mut config: Handle, + mut config: *mut Handle, flag_key: *const c_char, mut context: Handle, ) -> Result> { @@ -47,9 +47,5 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( /// `assignment` must be a valid Assignment handle #[no_mangle] pub unsafe extern "C" fn ddog_ffe_assignment_drop(mut assignment: *mut Handle) { - // Handle take() errors gracefully to prevent malloc corruption from double-free - // or use-after-free scenarios. If take() fails, silently ignore to avoid panics. - if let Ok(inner) = assignment.take() { - drop(inner); - } + drop(assignment.take()); } diff --git a/datadog-ffe-ffi/src/configuration.rs b/datadog-ffe-ffi/src/configuration.rs index 33c8164ce4..9cc28ec345 100644 --- a/datadog-ffe-ffi/src/configuration.rs +++ b/datadog-ffe-ffi/src/configuration.rs @@ -36,9 +36,5 @@ pub unsafe extern "C" fn ddog_ffe_configuration_new( /// `config` must be a valid Configuration handle created by `ddog_ffe_configuration_new` #[no_mangle] pub unsafe extern "C" fn ddog_ffe_configuration_drop(mut config: *mut Handle) { - // Handle take() errors gracefully to prevent malloc corruption from double-free - // or use-after-free scenarios. If take() fails, silently ignore to avoid panics. - if let Ok(inner) = config.take() { - drop(inner); - } + drop(config.take()); } From 2f1c2ff73acebbc336eaf6723ea05186acc903d6 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Wed, 29 Oct 2025 13:24:23 -0700 Subject: [PATCH 20/43] Refactor EvaluationContext API - Changed the `context` parameter in `ddog_ffe_get_assignment` to be a pointer for consistency. - Removed the single-attribute constructor `ddog_ffe_evaluation_context_new` and updated the multi-attribute constructor's documentation for clarity. --- datadog-ffe-ffi/src/assignment.rs | 4 ++-- datadog-ffe-ffi/src/evaluation_context.rs | 28 ++--------------------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 690bae15e6..c7386d7c0e 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -13,14 +13,14 @@ use ddcommon_ffi::{wrap_with_ffi_result, Handle, Result, ToInner}; /// /// # Safety /// - `config` must be a valid Configuration handle pointer -/// - `context` must be a valid EvaluationContext handle +/// - `context` must be a valid EvaluationContext handle pointer /// - `flag_key` must be a valid null-terminated C string #[no_mangle] #[named] pub unsafe extern "C" fn ddog_ffe_get_assignment( mut config: *mut Handle, flag_key: *const c_char, - mut context: Handle, + mut context: *mut Handle, ) -> Result> { wrap_with_ffi_result!({ ensure!(!flag_key.is_null(), "flag_key must not be NULL"); diff --git a/datadog-ffe-ffi/src/evaluation_context.rs b/datadog-ffe-ffi/src/evaluation_context.rs index 29f2d02962..fe1f784717 100644 --- a/datadog-ffe-ffi/src/evaluation_context.rs +++ b/datadog-ffe-ffi/src/evaluation_context.rs @@ -8,30 +8,6 @@ use std::sync::Arc; use datadog_ffe::rules_based::{Attribute, EvaluationContext, Str}; use ddcommon_ffi::{Handle, ToInner}; -/// Creates a new EvaluationContext with the given targeting key -/// -/// # Safety -/// `targeting_key` must be a valid null-terminated C string -#[no_mangle] -pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( - targeting_key: *const c_char, -) -> Handle { - if targeting_key.is_null() { - return Handle::empty(); - } - - let key_cstr = match CStr::from_ptr(targeting_key).to_str() { - Ok(s) => s, - Err(_) => return Handle::empty(), - }; - - let key = Str::from(key_cstr.to_string()); - let attributes = Arc::new(HashMap::::new()); - let context = EvaluationContext::new(key, attributes); - - Handle::from(context) -} - /// Represents a key-value pair for attributes #[repr(C)] pub struct AttributePair { @@ -39,7 +15,7 @@ pub struct AttributePair { pub value: *const c_char, } -/// Creates a new EvaluationContext with the given targeting key and multiple attributes +/// Creates a new EvaluationContext with the given targeting key and attributes /// /// # Safety /// - `targeting_key` must be a valid null-terminated C string @@ -48,7 +24,7 @@ pub struct AttributePair { /// - Each `AttributePair.name` and `AttributePair.value` must be valid null-terminated C strings /// - `attributes_count` must accurately represent the length of the `attributes` array #[no_mangle] -pub unsafe extern "C" fn ddog_ffe_evaluation_context_new_with_attributes( +pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( targeting_key: *const c_char, attributes: *const AttributePair, attributes_count: usize, From 8a462e1c07e0a5d288f7e9a374c1b3e800d9d394 Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Thu, 30 Oct 2025 04:01:01 +0200 Subject: [PATCH 21/43] [ffe] add pyo3 conversion methods --- Cargo.lock | 84 +++++++++++++ datadog-ffe/Cargo.toml | 8 +- datadog-ffe/src/lib.rs | 3 + datadog-ffe/src/pyo3.rs | 14 +++ datadog-ffe/src/rules_based/attributes.rs | 66 ++++++++-- .../rules_based/eval/evaluation_context.rs | 57 +++++++++ datadog-ffe/src/rules_based/str.rs | 28 +++++ datadog-ffe/src/rules_based/ufc/assignment.rs | 114 +++++++++++++++++- datadog-ffe/src/rules_based/ufc/models.rs | 45 +++++++ 9 files changed, 404 insertions(+), 15 deletions(-) create mode 100644 datadog-ffe/src/pyo3.rs diff --git a/Cargo.lock b/Cargo.lock index 51ef142f78..789b988c9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1417,6 +1417,7 @@ dependencies = [ "faststr", "log", "md5", + "pyo3", "regex", "serde", "serde-bool", @@ -2995,6 +2996,15 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + [[package]] name = "inout" version = "0.1.4" @@ -3985,6 +3995,68 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc55d7dec32ecaf61e0bd90b3d2392d721a28b95cfd23c3e176eccefbeab2f2" +[[package]] +name = "pyo3" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +dependencies = [ + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.87", +] + [[package]] name = "quote" version = "1.0.37" @@ -4946,6 +5018,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + [[package]] name = "target-triple" version = "0.1.4" @@ -5581,6 +5659,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + [[package]] name = "unsafe-libyaml" version = "0.2.11" diff --git a/datadog-ffe/Cargo.toml b/datadog-ffe/Cargo.toml index dad975e63f..c0c32c61ff 100644 --- a/datadog-ffe/Cargo.toml +++ b/datadog-ffe/Cargo.toml @@ -20,8 +20,9 @@ md5 = { version = "0.7.0", default-features = false } regex = "1.10.4" serde-bool = { version = "0.1.3", default-features = false } serde_with = { version = "3.11.0", default-features = false, features = ["base64", "hex", "macros"] } -thiserror = "2.0.3" -url = "2.5.0" +thiserror = { version = "2.0.3", default-features = false } +url = { version = "2.5.0", default-features = false, features = ["std"] } +pyo3 = { version = "0.25", optional = true, default-features = false, features = ["macros"] } [dev-dependencies] env_logger = "0.10" @@ -31,3 +32,6 @@ criterion = { version = "0.5", features = ["html_reports"] } name = "ffe-eval" harness = false path = "benches/eval.rs" + +[features] +pyo3 = ["dep:pyo3"] diff --git a/datadog-ffe/src/lib.rs b/datadog-ffe/src/lib.rs index 1937810d61..c1cf9a7fc7 100644 --- a/datadog-ffe/src/lib.rs +++ b/datadog-ffe/src/lib.rs @@ -2,3 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 pub mod rules_based; + +#[cfg(feature = "pyo3")] +pub mod pyo3; diff --git a/datadog-ffe/src/pyo3.rs b/datadog-ffe/src/pyo3.rs new file mode 100644 index 0000000000..4add9ce772 --- /dev/null +++ b/datadog-ffe/src/pyo3.rs @@ -0,0 +1,14 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use pyo3::prelude::*; + +use crate::rules_based::{Assignment, AssignmentReason}; + +/// Initialize FFE Python classes under the given module. +pub fn init(m: &Bound) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + + Ok(()) +} diff --git a/datadog-ffe/src/rules_based/attributes.rs b/datadog-ffe/src/rules_based/attributes.rs index 5581d58d1e..e2b99f534f 100644 --- a/datadog-ffe/src/rules_based/attributes.rs +++ b/datadog-ffe/src/rules_based/attributes.rs @@ -10,10 +10,10 @@ use crate::rules_based::Str; /// Attribute for evaluation context. See `From` implementations for initialization. #[derive(Debug, Clone, PartialEq, PartialOrd, derive_more::From, Serialize, Deserialize)] #[from(f64, bool, Str, String, &str, Arc, Arc, Cow<'_, str>)] -pub struct Attribute(AttributeValueImpl); +pub struct Attribute(AttributeImpl); #[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize, derive_more::From)] #[serde(untagged)] -enum AttributeValueImpl { +enum AttributeImpl { #[from] Number(f64), #[from(forward)] @@ -26,7 +26,7 @@ enum AttributeValueImpl { impl Attribute { pub(crate) fn is_null(&self) -> bool { - self == &Attribute(AttributeValueImpl::Null) + self == &Attribute(AttributeImpl::Null) } /// Try coercing attribute to a number. @@ -35,8 +35,8 @@ impl Attribute { /// number. pub(crate) fn coerce_to_number(&self) -> Option { match &self.0 { - AttributeValueImpl::Number(v) => Some(*v), - AttributeValueImpl::String(s) => s.parse().ok(), + AttributeImpl::Number(v) => Some(*v), + AttributeImpl::String(s) => s.parse().ok(), _ => None, } } @@ -46,19 +46,61 @@ impl Attribute { /// String attributes are returned as is. Number and boolean attributes are converted to string. pub(crate) fn coerce_to_string(&self) -> Option> { match &self.0 { - AttributeValueImpl::String(s) => Some(Cow::Borrowed(s)), - AttributeValueImpl::Number(v) => Some(Cow::Owned(v.to_string())), - AttributeValueImpl::Boolean(v) => { - Some(Cow::Borrowed(if *v { "true" } else { "false" })) - } - AttributeValueImpl::Null => None, + AttributeImpl::String(s) => Some(Cow::Borrowed(s)), + AttributeImpl::Number(v) => Some(Cow::Owned(v.to_string())), + AttributeImpl::Boolean(v) => Some(Cow::Borrowed(if *v { "true" } else { "false" })), + AttributeImpl::Null => None, } } pub(crate) fn as_str(&self) -> Option<&Str> { match self { - Attribute(AttributeValueImpl::String(s)) => Some(s), + Attribute(AttributeImpl::String(s)) => Some(s), _ => None, } } } + +#[cfg(feature = "pyo3")] +mod pyo3_impl { + use super::*; + + use pyo3::{ + exceptions::PyTypeError, + prelude::*, + types::{PyBool, PyFloat, PyInt, PyString}, + }; + + /// Convert Python value to Attribute. + /// + /// The following types are currently supported: + /// - `str` + /// - `int` + /// - `float` + /// - `bool` + /// - `NoneType` + /// + /// Note that nesting is not currently supported and will throw an error. + impl<'py> FromPyObject<'py> for Attribute { + #[inline] + fn extract_bound(value: &Bound<'py, PyAny>) -> PyResult { + if let Ok(s) = value.downcast::() { + return Ok(Attribute(AttributeImpl::String(s.to_cow()?.into()))); + } + // In Python, Bool inherits from Int, so it must be checked first here. + if let Ok(s) = value.downcast::() { + return Ok(Attribute(AttributeImpl::Boolean(s.is_true()))); + } + if let Ok(s) = value.downcast::() { + return Ok(Attribute(AttributeImpl::Number(s.value()))); + } + if let Ok(s) = value.downcast::() { + return Ok(Attribute(AttributeImpl::Number(s.extract::()?))); + } + if value.is_none() { + return Ok(Attribute(AttributeImpl::Null)); + } + Err(PyTypeError::new_err("invalid type for attribute")) + } + } +} diff --git a/datadog-ffe/src/rules_based/eval/evaluation_context.rs b/datadog-ffe/src/rules_based/eval/evaluation_context.rs index 5e8b7bd83e..d9e3be63f1 100644 --- a/datadog-ffe/src/rules_based/eval/evaluation_context.rs +++ b/datadog-ffe/src/rules_based/eval/evaluation_context.rs @@ -49,3 +49,60 @@ impl EvaluationContext { None } } + +#[cfg(feature = "pyo3")] +mod pyo3_impl { + use super::*; + + use pyo3::{intern, prelude::*, types::PyDict}; + + /// Accepts either a dict with `"targeting_key"` and `"attributes"` items, or any object with + /// `targeting_key` and `attributes` attributes. + /// + /// # Examples + /// + /// ```python + /// {"targeting_key": "user1", "attributes": {"attr1": 42}} + /// ``` + /// + /// ```python + /// @dataclass + /// class EvaluationContext: + /// targeting_key: Optional[str] + /// attributes: dict[str, Any] + /// + /// EvaluationContext(targeting_key="user1", attributes={"attr1": 42}) + /// ``` + impl<'py> FromPyObject<'py> for EvaluationContext { + #[inline] + fn extract_bound(value: &Bound<'py, PyAny>) -> PyResult { + let py = value.py(); + + let (targeting_key, attributes) = if let Ok(dict) = value.downcast::() { + ( + dict.get_item(intern!(py, "targeting_key"))?, + dict.get_item(intern!(py, "attributes"))?, + ) + } else { + ( + value.getattr_opt(intern!(py, "targeting_key"))?, + value.getattr_opt(intern!(py, "attributes"))?, + ) + }; + + let context = EvaluationContext { + targeting_key: targeting_key + .map(|it| it.extract()) + .transpose()? + .unwrap_or_else(|| Str::from_static_str("").into()), + attributes: attributes + .map(|it| it.extract()) + .transpose()? + .map(Arc::new) + .unwrap_or_default(), + }; + + Ok(context) + } + } +} diff --git a/datadog-ffe/src/rules_based/str.rs b/datadog-ffe/src/rules_based/str.rs index ae9a1b6404..6afb96569c 100644 --- a/datadog-ffe/src/rules_based/str.rs +++ b/datadog-ffe/src/rules_based/str.rs @@ -111,3 +111,31 @@ impl log::kv::ToValue for Str { log::kv::Value::from_display(self) } } + +#[cfg(feature = "pyo3")] +mod pyo3_impl { + use std::convert::Infallible; + + use super::*; + + use pyo3::{prelude::*, types::PyString}; + + impl<'py> FromPyObject<'py> for Str { + #[inline] + fn extract_bound(value: &Bound<'py, PyAny>) -> PyResult { + let s = value.downcast::()?; + Ok(Str::from(s.to_cow()?)) + } + } + + impl<'py> IntoPyObject<'py> for &Str { + type Target = PyString; + type Output = Bound<'py, PyString>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyString::new(py, self.as_str())) + } + } +} diff --git a/datadog-ffe/src/rules_based/ufc/assignment.rs b/datadog-ffe/src/rules_based/ufc/assignment.rs index 18cefb96a3..33e3e7bdb7 100644 --- a/datadog-ffe/src/rules_based/ufc/assignment.rs +++ b/datadog-ffe/src/rules_based/ufc/assignment.rs @@ -10,9 +10,13 @@ use crate::rules_based::Str; use super::VariationType; +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; + /// Reason for assignment evaluation result. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[cfg_attr(feature = "pyo3", pyo3::pyclass(eq, eq_int))] pub enum AssignmentReason { /// Assignment was made based on targeting rules or time bounds. TargetingMatch, @@ -24,6 +28,7 @@ pub enum AssignmentReason { /// Result of assignment evaluation. #[derive(Debug, Serialize, Clone)] +#[cfg_attr(feature = "pyo3", pyo3::pyclass(frozen))] #[serde(rename_all = "camelCase")] pub struct Assignment { /// Assignment value that should be returned to the user. @@ -299,3 +304,110 @@ impl AssignmentValue { } } } + +#[cfg(feature = "pyo3")] +mod pyo3_impl { + + use super::*; + + use pyo3::{ + exceptions::PyValueError, + types::{PyDict, PyList}, + }; + + impl<'py> IntoPyObject<'py> for &AssignmentValue { + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + let obj = match self { + AssignmentValue::String(v) => v.into_pyobject(py)?.into_any(), + AssignmentValue::Integer(v) => v.into_pyobject(py)?.into_any(), + AssignmentValue::Float(v) => v.into_pyobject(py)?.into_any(), + AssignmentValue::Boolean(v) => v.into_pyobject(py)?.to_owned().into_any(), + AssignmentValue::Json(v) => json_to_pyobject(v, py)?, + }; + Ok(obj) + } + } + + fn json_to_pyobject<'py>( + value: &serde_json::Value, + py: Python<'py>, + ) -> Result, PyErr> { + use serde_json::Value; + let v = match value { + Value::Null => py.None().into_bound(py), + Value::Bool(v) => v.into_pyobject(py)?.to_owned().into_any(), + Value::Number(number) => { + if let Some(v) = number.as_u128() { + v.into_pyobject(py)?.into_any() + } else if let Some(v) = number.as_i128() { + v.into_pyobject(py)?.into_any() + } else if let Some(v) = number.as_f64() { + v.into_pyobject(py)?.into_any() + } else { + // NOTE: this can only happen if serde_json is compiled with arbitrary-precision + // and it failed to parse the number / the number is larger than f64::MAX. + return Err(PyValueError::new_err("unable to convert number to python")); + } + } + Value::String(s) => s.into_pyobject(py)?.into_any(), + Value::Array(values) => { + let vals = values + .iter() + .map(|it| json_to_pyobject(it, py)) + .collect::, _>>()?; + PyList::new(py, vals)?.into_any() + } + Value::Object(map) => { + let dict = PyDict::new(py); + for (key, value) in map { + dict.set_item(key, json_to_pyobject(value, py)?)?; + } + dict.into_any() + } + }; + Ok(v) + } + + #[pymethods] + impl Assignment { + // pyo3 refuses to implement IntoPyObject for Arc, so we need to dereference it here in the + // getter. + // + // And because of https://github.com/PyO3/pyo3/issues/1003 we now need to re-implement all + // getter here. + #[getter] + fn extra_logging(&self) -> &HashMap { + &self.extra_logging + } + + #[getter] + fn value(&self) -> &AssignmentValue { + &self.value + } + + #[getter] + fn variation_key(&self) -> &Str { + &self.variation_key + } + + #[getter] + fn allocation_key(&self) -> &Str { + &self.allocation_key + } + + #[getter] + fn reason(&self) -> AssignmentReason { + self.reason + } + + #[getter] + fn do_log(&self) -> bool { + self.do_log + } + } +} diff --git a/datadog-ffe/src/rules_based/ufc/models.rs b/datadog-ffe/src/rules_based/ufc/models.rs index a884e0862d..6e238a3c73 100644 --- a/datadog-ffe/src/rules_based/ufc/models.rs +++ b/datadog-ffe/src/rules_based/ufc/models.rs @@ -503,6 +503,51 @@ impl ShardRange { } } +#[cfg(feature = "pyo3")] +mod pyo3_impl { + use super::*; + + use pyo3::{exceptions::PyValueError, prelude::*, types::PyString}; + + /// Convert `VariationType` from Python string. + /// + /// The value must be one of: + /// ```python + /// # Preferred: + /// "string" + /// "integer" + /// "float" + /// "boolean" + /// "object" + /// + /// # Legacy (for compatibility): + /// "STRING" + /// "INTEGER" + /// "NUMERIC" + /// "BOOLEAN" + /// "JSON" + /// ``` + impl<'py> FromPyObject<'py> for VariationType { + #[inline] + fn extract_bound(value: &Bound<'py, PyAny>) -> PyResult { + let s = value.downcast::()?.to_cow()?; + let ty = match s.as_ref() { + "string" | "STRING" => VariationType::String, + "integer" | "INTEGER" => VariationType::Integer, + "float" | "NUMERIC" => VariationType::Numeric, + "boolean" | "BOOLEAN" => VariationType::Boolean, + "object" | "JSON" => VariationType::Json, + _ => { + return Err(PyValueError::new_err(format!( + "unexpected value for VariationType: {s:?}" + ))) + } + }; + Ok(ty) + } + } +} + #[cfg(test)] mod tests { use super::{TryParse, UniversalFlagConfigWire}; From b4f03b96f1d5f4e4bd7829c869189d5ce403fe99 Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Thu, 30 Oct 2025 21:56:20 +0200 Subject: [PATCH 22/43] [ffe] Remove unused errors --- datadog-ffe/src/rules_based/error.rs | 49 ------------------- datadog-ffe/src/rules_based/mod.rs | 2 +- .../rules_based/ufc/compiled_flag_config.rs | 9 ++-- datadog-ffe/src/rules_based/ufc/models.rs | 24 +++------ 4 files changed, 11 insertions(+), 73 deletions(-) diff --git a/datadog-ffe/src/rules_based/error.rs b/datadog-ffe/src/rules_based/error.rs index 53ed1cd80c..3aad78c6b1 100644 --- a/datadog-ffe/src/rules_based/error.rs +++ b/datadog-ffe/src/rules_based/error.rs @@ -1,53 +1,10 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; - use serde::{Deserialize, Serialize}; use crate::rules_based::ufc::VariationType; -/// Represents a result type for operations in the feature flagging SDK. -/// -/// This type alias is used throughout the SDK to indicate the result of operations that may return -/// errors specific to the feature flagging SDK. -/// -/// This `Result` type is a standard Rust `Result` type where the error variant is defined by the -/// ffe-specific [`Error`] enum. -pub type Result = std::result::Result; - -/// Enum representing possible errors that can occur in the feature flagging SDK. -#[derive(thiserror::Error, Debug, Clone)] -#[non_exhaustive] -pub enum Error { - /// Error evaluating a flag. - #[error(transparent)] - EvaluationError(EvaluationError), - - /// Invalid base URL configuration. - #[error("invalid base_url configuration")] - InvalidBaseUrl(#[source] url::ParseError), - - /// The request was unauthorized, possibly due to an invalid API key. - #[error("unauthorized, api_key is likely invalid")] - Unauthorized, - - /// Indicates that the poller thread panicked. This should normally never happen. - #[error("poller thread panicked")] - PollerThreadPanicked, - - /// An I/O error. - #[error(transparent)] - // std::io::Error is not clonable, so we're wrapping it in an Arc. - Io(Arc), -} - -impl From for Error { - fn from(value: std::io::Error) -> Self { - Self::Io(Arc::new(value)) - } -} - /// Enum representing possible errors that can occur during evaluation. #[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[non_exhaustive] @@ -93,12 +50,6 @@ pub(crate) enum EvaluationFailure { /// being assigned. #[error("default allocation is matched and is serving NULL")] DefaultAllocationNull, - - #[error("flag resolved to a non-bandit variation")] - NonBanditVariation, - - #[error("no actions were supplied to bandit evaluation")] - NoActionsSuppliedForBandit, } impl From for EvaluationFailure { diff --git a/datadog-ffe/src/rules_based/mod.rs b/datadog-ffe/src/rules_based/mod.rs index 1c2747f6a7..f3197c36d2 100644 --- a/datadog-ffe/src/rules_based/mod.rs +++ b/datadog-ffe/src/rules_based/mod.rs @@ -12,7 +12,7 @@ mod ufc; pub use attributes::Attribute; pub use configuration::Configuration; -pub use error::{Error, EvaluationError, Result}; +pub use error::EvaluationError; pub use eval::{get_assignment, EvaluationContext}; pub use str::Str; pub use timestamp::{now, Timestamp}; diff --git a/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs b/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs index 93b906b79f..0ac5c382ea 100644 --- a/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs +++ b/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs @@ -6,7 +6,7 @@ use std::{collections::HashMap, sync::Arc}; use serde::Deserialize; use crate::rules_based::{ - error::EvaluationFailure, sharder::PreSaltedSharder, Error, EvaluationError, Str, + error::EvaluationFailure, sharder::PreSaltedSharder, EvaluationError, Str, }; use super::{ @@ -66,11 +66,8 @@ pub(crate) struct Shard { } impl UniversalFlagConfig { - pub fn from_json(json: Vec) -> Result { - let config: CompiledFlagsConfig = serde_json::from_slice(&json).map_err(|err| { - log::warn!("failed to compile flag configuration: {err:?}"); - Error::EvaluationError(EvaluationError::UnexpectedConfigurationError) - })?; + pub fn from_json(json: Vec) -> Result { + let config: CompiledFlagsConfig = serde_json::from_slice(&json)?; Ok(UniversalFlagConfig { wire_json: json, compiled: config, diff --git a/datadog-ffe/src/rules_based/ufc/models.rs b/datadog-ffe/src/rules_based/ufc/models.rs index 6e238a3c73..0e512595c9 100644 --- a/datadog-ffe/src/rules_based/ufc/models.rs +++ b/datadog-ffe/src/rules_based/ufc/models.rs @@ -6,7 +6,7 @@ use std::{collections::HashMap, sync::Arc}; use regex::Regex; use serde::{Deserialize, Serialize}; -use crate::rules_based::{Error, EvaluationError, Str}; +use crate::rules_based::{EvaluationError, Str}; #[allow(missing_docs)] pub type Timestamp = crate::rules_based::timestamp::Timestamp; @@ -281,7 +281,7 @@ impl From for Option { } impl TryFrom for Condition { - type Error = Error; + type Error = EvaluationError; fn try_from(condition: ConditionWire) -> Result { let attribute = condition.attribute; @@ -296,9 +296,7 @@ impl TryFrom for Condition { "failed to parse condition: {:?} condition with non-string condition value", condition.operator ); - return Err(Error::EvaluationError( - EvaluationError::UnexpectedConfigurationError, - )); + return Err(EvaluationError::UnexpectedConfigurationError); } }; let regex = match Regex::new(®ex_string) { @@ -307,9 +305,7 @@ impl TryFrom for Condition { log::warn!( "failed to parse condition: failed to compile regex {regex_string:?}: {err:?}" ); - return Err(Error::EvaluationError( - EvaluationError::UnexpectedConfigurationError, - )); + return Err(EvaluationError::UnexpectedConfigurationError); } }; @@ -337,9 +333,7 @@ impl TryFrom for Condition { "failed to parse condition: comparison value is not a number: {:?}", condition.value ); - return Err(Error::EvaluationError( - EvaluationError::UnexpectedConfigurationError, - )); + return Err(EvaluationError::UnexpectedConfigurationError); }; ConditionCheck::Comparison { operator, @@ -355,9 +349,7 @@ impl TryFrom for Condition { "failed to parse condition: membership condition with non-array value: {:?}", condition.value ); - return Err(Error::EvaluationError( - EvaluationError::UnexpectedConfigurationError, - )); + return Err(EvaluationError::UnexpectedConfigurationError); } }; ConditionCheck::Membership { @@ -371,9 +363,7 @@ impl TryFrom for Condition { log::warn!( "failed to parse condition: IS_NULL condition with non-boolean condition value" ); - return Err(Error::EvaluationError( - EvaluationError::UnexpectedConfigurationError, - )); + return Err(EvaluationError::UnexpectedConfigurationError); }; ConditionCheck::Null { expected_null } } From 44f5f49db8ef1f238c69fb4ad0f21d304c89fa27 Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Thu, 30 Oct 2025 22:04:37 +0200 Subject: [PATCH 23/43] [ffe] step back and remove IntoPyObject types These will be defined in dd-trace-py to better match what Python/OF need. --- datadog-ffe/src/lib.rs | 3 -- datadog-ffe/src/pyo3.rs | 14 ------ datadog-ffe/src/rules_based/ufc/assignment.rs | 44 +----------------- datadog-ffe/src/rules_based/ufc/models.rs | 45 ------------------- 4 files changed, 1 insertion(+), 105 deletions(-) delete mode 100644 datadog-ffe/src/pyo3.rs diff --git a/datadog-ffe/src/lib.rs b/datadog-ffe/src/lib.rs index c1cf9a7fc7..1937810d61 100644 --- a/datadog-ffe/src/lib.rs +++ b/datadog-ffe/src/lib.rs @@ -2,6 +2,3 @@ // SPDX-License-Identifier: Apache-2.0 pub mod rules_based; - -#[cfg(feature = "pyo3")] -pub mod pyo3; diff --git a/datadog-ffe/src/pyo3.rs b/datadog-ffe/src/pyo3.rs deleted file mode 100644 index 4add9ce772..0000000000 --- a/datadog-ffe/src/pyo3.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ -// SPDX-License-Identifier: Apache-2.0 - -use pyo3::prelude::*; - -use crate::rules_based::{Assignment, AssignmentReason}; - -/// Initialize FFE Python classes under the given module. -pub fn init(m: &Bound) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; - - Ok(()) -} diff --git a/datadog-ffe/src/rules_based/ufc/assignment.rs b/datadog-ffe/src/rules_based/ufc/assignment.rs index 33e3e7bdb7..d60ab534c7 100644 --- a/datadog-ffe/src/rules_based/ufc/assignment.rs +++ b/datadog-ffe/src/rules_based/ufc/assignment.rs @@ -10,13 +10,9 @@ use crate::rules_based::Str; use super::VariationType; -#[cfg(feature = "pyo3")] -use pyo3::prelude::*; - /// Reason for assignment evaluation result. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -#[cfg_attr(feature = "pyo3", pyo3::pyclass(eq, eq_int))] pub enum AssignmentReason { /// Assignment was made based on targeting rules or time bounds. TargetingMatch, @@ -28,7 +24,6 @@ pub enum AssignmentReason { /// Result of assignment evaluation. #[derive(Debug, Serialize, Clone)] -#[cfg_attr(feature = "pyo3", pyo3::pyclass(frozen))] #[serde(rename_all = "camelCase")] pub struct Assignment { /// Assignment value that should be returned to the user. @@ -312,6 +307,7 @@ mod pyo3_impl { use pyo3::{ exceptions::PyValueError, + prelude::*, types::{PyDict, PyList}, }; @@ -372,42 +368,4 @@ mod pyo3_impl { }; Ok(v) } - - #[pymethods] - impl Assignment { - // pyo3 refuses to implement IntoPyObject for Arc, so we need to dereference it here in the - // getter. - // - // And because of https://github.com/PyO3/pyo3/issues/1003 we now need to re-implement all - // getter here. - #[getter] - fn extra_logging(&self) -> &HashMap { - &self.extra_logging - } - - #[getter] - fn value(&self) -> &AssignmentValue { - &self.value - } - - #[getter] - fn variation_key(&self) -> &Str { - &self.variation_key - } - - #[getter] - fn allocation_key(&self) -> &Str { - &self.allocation_key - } - - #[getter] - fn reason(&self) -> AssignmentReason { - self.reason - } - - #[getter] - fn do_log(&self) -> bool { - self.do_log - } - } } diff --git a/datadog-ffe/src/rules_based/ufc/models.rs b/datadog-ffe/src/rules_based/ufc/models.rs index 0e512595c9..0e421b0043 100644 --- a/datadog-ffe/src/rules_based/ufc/models.rs +++ b/datadog-ffe/src/rules_based/ufc/models.rs @@ -493,51 +493,6 @@ impl ShardRange { } } -#[cfg(feature = "pyo3")] -mod pyo3_impl { - use super::*; - - use pyo3::{exceptions::PyValueError, prelude::*, types::PyString}; - - /// Convert `VariationType` from Python string. - /// - /// The value must be one of: - /// ```python - /// # Preferred: - /// "string" - /// "integer" - /// "float" - /// "boolean" - /// "object" - /// - /// # Legacy (for compatibility): - /// "STRING" - /// "INTEGER" - /// "NUMERIC" - /// "BOOLEAN" - /// "JSON" - /// ``` - impl<'py> FromPyObject<'py> for VariationType { - #[inline] - fn extract_bound(value: &Bound<'py, PyAny>) -> PyResult { - let s = value.downcast::()?.to_cow()?; - let ty = match s.as_ref() { - "string" | "STRING" => VariationType::String, - "integer" | "INTEGER" => VariationType::Integer, - "float" | "NUMERIC" => VariationType::Numeric, - "boolean" | "BOOLEAN" => VariationType::Boolean, - "object" | "JSON" => VariationType::Json, - _ => { - return Err(PyValueError::new_err(format!( - "unexpected value for VariationType: {s:?}" - ))) - } - }; - Ok(ty) - } - } -} - #[cfg(test)] mod tests { use super::{TryParse, UniversalFlagConfigWire}; From 466bafad8c25d447f9047a4cd993d3bdb1b7c1ba Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Thu, 30 Oct 2025 23:19:09 +0200 Subject: [PATCH 24/43] Revert "test(ffe): run sdk tests individually (#1273)" This reverts commit b9c68ca619ad21f34049c4b662959abf3e67d354. Test generation in build.rs made tests hard to edit. --- datadog-ffe/build.rs | 156 ------------------ .../src/rules_based/eval/eval_assignment.rs | 56 ++++++- 2 files changed, 51 insertions(+), 161 deletions(-) delete mode 100644 datadog-ffe/build.rs diff --git a/datadog-ffe/build.rs b/datadog-ffe/build.rs deleted file mode 100644 index b3c0a280c0..0000000000 --- a/datadog-ffe/build.rs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ -// SPDX-License-Identifier: Apache-2.0 - -use std::fs; -use std::io::Write; -use std::path::Path; - -fn main() { - // Tell Cargo to rerun this build script if the test data directory changes - println!("cargo:rerun-if-changed=tests/data/tests"); - - let test_dir = Path::new("tests/data/tests"); - let out_dir = std::env::var("OUT_DIR").unwrap(); - let dest_path = Path::new(&out_dir).join("sdk_tests.rs"); - - let mut file = fs::File::create(dest_path).unwrap(); - - // Read all JSON files in the test directory - let mut test_files = Vec::new(); - if let Ok(entries) = fs::read_dir(test_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().and_then(|s| s.to_str()) == Some("json") { - if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { - test_files.push(stem.to_string()); - } - } - } - } - - // Sort for consistent output - test_files.sort(); - - // Generate the entire test code - writeln!( - file, - "// This file is automatically generated by build.rs\n" - ) - .unwrap(); - - // Helper function - writeln!(file, "// Helper function to run a single test file").unwrap(); - writeln!(file, "#[allow(dead_code)]").unwrap(); - writeln!( - file, - "fn run_test_file(config: &Configuration, test_file_path: &str, now: chrono::DateTime) {{" - ) - .unwrap(); - writeln!(file, " let f = File::open(test_file_path)").unwrap(); - writeln!( - file, - " .unwrap_or_else(|e| panic!(\"Failed to open test file {{}}: {{}}\", test_file_path, e));" - ) - .unwrap(); - writeln!( - file, - " let test_cases: Vec = serde_json::from_reader(f)" - ) - .unwrap(); - writeln!( - file, - " .unwrap_or_else(|e| panic!(\"Failed to parse test file {{}}: {{}}\", test_file_path, e));" - ) - .unwrap(); - writeln!(file).unwrap(); - writeln!(file, " for test_case in test_cases {{").unwrap(); - writeln!(file, " let default_assignment =").unwrap(); - writeln!( - file, - " AssignmentValue::from_wire(test_case.variation_type, test_case.default_value)" - ) - .unwrap(); - writeln!(file, " .unwrap();").unwrap(); - writeln!(file).unwrap(); - writeln!( - file, - " let targeting_key = test_case.targeting_key.clone();" - ) - .unwrap(); - writeln!( - file, - " let subject = EvaluationContext::new(test_case.targeting_key, test_case.attributes);" - ) - .unwrap(); - writeln!(file, " let result = get_assignment(").unwrap(); - writeln!(file, " Some(config),").unwrap(); - writeln!(file, " &test_case.flag,").unwrap(); - writeln!(file, " &subject,").unwrap(); - writeln!(file, " Some(test_case.variation_type),").unwrap(); - writeln!(file, " now,").unwrap(); - writeln!(file, " )").unwrap(); - writeln!(file, " .unwrap_or(None);").unwrap(); - writeln!(file).unwrap(); - writeln!(file, " let result_assignment = result").unwrap(); - writeln!(file, " .as_ref()").unwrap(); - writeln!(file, " .map(|assignment| &assignment.value)").unwrap(); - writeln!(file, " .unwrap_or(&default_assignment);").unwrap(); - writeln!(file, " let expected_assignment =").unwrap(); - writeln!( - file, - " AssignmentValue::from_wire(test_case.variation_type, test_case.result.value)" - ) - .unwrap(); - writeln!(file, " .unwrap();").unwrap(); - writeln!(file).unwrap(); - writeln!(file, " assert_eq!(").unwrap(); - writeln!(file, " result_assignment, &expected_assignment,").unwrap(); - writeln!( - file, - " \"Test case failed for subject {{:?}} in file {{}}\"," - ) - .unwrap(); - writeln!(file, " targeting_key, test_file_path").unwrap(); - writeln!(file, " );").unwrap(); - writeln!(file, " }}").unwrap(); - writeln!(file, "}}").unwrap(); - writeln!(file).unwrap(); - - // Generate individual test functions - for test_file in &test_files { - let test_name = format!("evaluation_sdk_{}", test_file.replace('-', "_")); - - writeln!(file, "#[test]").unwrap(); - writeln!(file, "fn {}() {{", test_name).unwrap(); - writeln!( - file, - " let _ = env_logger::builder().is_test(true).try_init();" - ) - .unwrap(); - writeln!(file).unwrap(); - writeln!(file, " let config = UniversalFlagConfig::from_json(").unwrap(); - writeln!( - file, - " std::fs::read(\"tests/data/flags-v1.json\").unwrap()" - ) - .unwrap(); - writeln!(file, " )").unwrap(); - writeln!(file, " .unwrap();").unwrap(); - writeln!( - file, - " let config = Configuration::from_server_response(config);" - ) - .unwrap(); - writeln!(file, " let now = Utc::now();").unwrap(); - writeln!(file).unwrap(); - writeln!( - file, - " let test_file_path = \"tests/data/tests/{}.json\";", - test_file - ) - .unwrap(); - writeln!(file, " run_test_file(&config, test_file_path, now);").unwrap(); - writeln!(file, "}}").unwrap(); - writeln!(file).unwrap(); - } -} diff --git a/datadog-ffe/src/rules_based/eval/eval_assignment.rs b/datadog-ffe/src/rules_based/eval/eval_assignment.rs index 65523054ee..aec03e0ab0 100644 --- a/datadog-ffe/src/rules_based/eval/eval_assignment.rs +++ b/datadog-ffe/src/rules_based/eval/eval_assignment.rs @@ -220,7 +220,11 @@ impl Shard { #[cfg(test)] mod tests { - use std::{collections::HashMap, fs::File, sync::Arc}; + use std::{ + collections::HashMap, + fs::{self, File}, + sync::Arc, + }; use chrono::Utc; use serde::{Deserialize, Serialize}; @@ -247,8 +251,50 @@ mod tests { value: serde_json::Value, } - // Include the SDK tests generated by build.rs at compile time - // The build script automatically discovers all test files in tests/data/tests/ - // and generates a separate test function for each one - include!(concat!(env!("OUT_DIR"), "/sdk_tests.rs")); + #[test] + fn evaluation_sdk_test_data() { + let _ = env_logger::builder().is_test(true).try_init(); + + let config = + UniversalFlagConfig::from_json(std::fs::read("tests/data/flags-v1.json").unwrap()) + .unwrap(); + let config = Configuration::from_server_response(config); + let now = Utc::now(); + + for entry in fs::read_dir("tests/data/tests/").unwrap() { + let entry = entry.unwrap(); + println!("Processing test file: {:?}", entry.path()); + + let f = File::open(entry.path()).unwrap(); + let test_cases: Vec = serde_json::from_reader(f).unwrap(); + + for test_case in test_cases { + let default_assignment = + AssignmentValue::from_wire(test_case.variation_type, test_case.default_value) + .unwrap(); + + print!("test subject {:?} ... ", test_case.targeting_key); + let subject = EvaluationContext::new(test_case.targeting_key, test_case.attributes); + let result = get_assignment( + Some(&config), + &test_case.flag, + &subject, + Some(test_case.variation_type), + now, + ) + .unwrap_or(None); + + let result_assingment = result + .as_ref() + .map(|assignment| &assignment.value) + .unwrap_or(&default_assignment); + let expected_assignment = + AssignmentValue::from_wire(test_case.variation_type, test_case.result.value) + .unwrap(); + + assert_eq!(result_assingment, &expected_assignment); + println!("ok"); + } + } + } } From 572804b5878d2c1d2e185dab19989101cbc2e390 Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Thu, 30 Oct 2025 23:22:47 +0200 Subject: [PATCH 25/43] [ffe] Expose EvaluationFailure This is required to allow tracers return more precise error codes. --- datadog-ffe/src/rules_based/error.rs | 2 +- .../src/rules_based/eval/eval_assignment.rs | 34 ++++++++----------- datadog-ffe/src/rules_based/mod.rs | 2 +- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/datadog-ffe/src/rules_based/error.rs b/datadog-ffe/src/rules_based/error.rs index 3aad78c6b1..0d228e52f5 100644 --- a/datadog-ffe/src/rules_based/error.rs +++ b/datadog-ffe/src/rules_based/error.rs @@ -29,7 +29,7 @@ pub enum EvaluationError { /// default assignment. #[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub(crate) enum EvaluationFailure { +pub enum EvaluationFailure { /// True evaluation error that should be returned to the user. #[error(transparent)] Error(EvaluationError), diff --git a/datadog-ffe/src/rules_based/eval/eval_assignment.rs b/datadog-ffe/src/rules_based/eval/eval_assignment.rs index aec03e0ab0..d23d4ba760 100644 --- a/datadog-ffe/src/rules_based/eval/eval_assignment.rs +++ b/datadog-ffe/src/rules_based/eval/eval_assignment.rs @@ -20,13 +20,13 @@ pub fn get_assignment( subject: &EvaluationContext, expected_type: Option, now: DateTime, -) -> Result, EvaluationError> { +) -> Result { let Some(config) = configuration else { log::trace!( flag_key, targeting_key = subject.targeting_key(); "returning default assignment because of: {}", EvaluationFailure::ConfigurationMissing); - return Ok(None); + return Err(EvaluationFailure::ConfigurationMissing); }; config.eval_flag(flag_key, subject, expected_type, now) @@ -39,28 +39,26 @@ impl Configuration { context: &EvaluationContext, expected_type: Option, now: DateTime, - ) -> Result, EvaluationError> { + ) -> Result { let result = self .flags .compiled .eval_flag(flag_key, context, expected_type, now); - match result { + match &result { Ok(assignment) => { log::trace!( - flag_key, - targeting_key = context.targeting_key(), - assignment:serde = assignment.value; - "evaluated a flag"); - Ok(Some(assignment)) + flag_key, + targeting_key = context.targeting_key(), + assignment:serde = assignment.value; + "evaluated a flag"); } Err(EvaluationFailure::ConfigurationMissing) => { log::warn!( - flag_key, - targeting_key = context.targeting_key(); - "evaluating a flag before flags configuration has been fetched"); - Ok(None) + flag_key, + targeting_key = context.targeting_key(); + "evaluating a flag before flags configuration has been fetched"); } Err(EvaluationFailure::Error(err)) => { @@ -69,19 +67,16 @@ impl Configuration { targeting_key = context.targeting_key(); "error occurred while evaluating a flag: {err}", ); - Err(err) } - - // Non-Error failures are considered normal conditions and usually don't need extra - // attention, so we remap them to Ok(None) before returning to the user. Err(err) => { log::trace!( flag_key, targeting_key = context.targeting_key(); "returning default assignment because of: {err}"); - Ok(None) } } + + result } } @@ -281,8 +276,7 @@ mod tests { &subject, Some(test_case.variation_type), now, - ) - .unwrap_or(None); + ); let result_assingment = result .as_ref() diff --git a/datadog-ffe/src/rules_based/mod.rs b/datadog-ffe/src/rules_based/mod.rs index f3197c36d2..4bde4875d2 100644 --- a/datadog-ffe/src/rules_based/mod.rs +++ b/datadog-ffe/src/rules_based/mod.rs @@ -12,7 +12,7 @@ mod ufc; pub use attributes::Attribute; pub use configuration::Configuration; -pub use error::EvaluationError; +pub use error::{EvaluationError, EvaluationFailure}; pub use eval::{get_assignment, EvaluationContext}; pub use str::Str; pub use timestamp::{now, Timestamp}; From 750dcec31112589e6c8d1980bfb570c51f4d68a6 Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Sat, 1 Nov 2025 00:27:15 +0200 Subject: [PATCH 26/43] [ffe] remove support for JSON:API --- .../src/rules_based/eval/eval_assignment.rs | 4 +- .../rules_based/ufc/compiled_flag_config.rs | 11 +- datadog-ffe/src/rules_based/ufc/models.rs | 120 +- datadog-ffe/tests/data/flags-v1.json | 5297 +++++++++-------- 4 files changed, 2692 insertions(+), 2740 deletions(-) diff --git a/datadog-ffe/src/rules_based/eval/eval_assignment.rs b/datadog-ffe/src/rules_based/eval/eval_assignment.rs index d23d4ba760..af5c97a65e 100644 --- a/datadog-ffe/src/rules_based/eval/eval_assignment.rs +++ b/datadog-ffe/src/rules_based/eval/eval_assignment.rs @@ -7,9 +7,9 @@ use crate::rules_based::{ error::{EvaluationError, EvaluationFailure}, ufc::{ Allocation, Assignment, AssignmentReason, CompiledFlagsConfig, Flag, Shard, Split, - Timestamp, VariationType, + VariationType, }, - Configuration, EvaluationContext, + Configuration, EvaluationContext, Timestamp, }; /// Evaluate the specified feature flag for the given subject and return assigned variation and diff --git a/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs b/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs index 0ac5c382ea..efe7493244 100644 --- a/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs +++ b/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs @@ -6,12 +6,12 @@ use std::{collections::HashMap, sync::Arc}; use serde::Deserialize; use crate::rules_based::{ - error::EvaluationFailure, sharder::PreSaltedSharder, EvaluationError, Str, + error::EvaluationFailure, sharder::PreSaltedSharder, EvaluationError, Str, Timestamp, }; use super::{ AllocationWire, AssignmentValue, Environment, FlagWire, RuleWire, ShardRange, ShardWire, - SplitWire, Timestamp, UniversalFlagConfigWire, VariationType, + SplitWire, UniversalFlagConfigWire, VariationType, }; #[derive(Debug)] @@ -25,7 +25,6 @@ pub struct UniversalFlagConfig { #[serde(from = "UniversalFlagConfigWire")] pub(crate) struct CompiledFlagsConfig { /// When configuration was last updated. - #[allow(dead_code)] pub created_at: Timestamp, /// Environment this configuration belongs to. pub environment: Environment, @@ -82,8 +81,6 @@ impl UniversalFlagConfig { impl From for CompiledFlagsConfig { fn from(config: UniversalFlagConfigWire) -> Self { let flags = config - .data - .attributes .flags .into_iter() .map(|(key, flag)| { @@ -99,8 +96,8 @@ impl From for CompiledFlagsConfig { .collect(); CompiledFlagsConfig { - created_at: config.data.attributes.created_at.into(), - environment: config.data.attributes.environment, + created_at: config.created_at.into(), + environment: config.environment, flags, } } diff --git a/datadog-ffe/src/rules_based/ufc/models.rs b/datadog-ffe/src/rules_based/ufc/models.rs index 0e421b0043..cf539fee2a 100644 --- a/datadog-ffe/src/rules_based/ufc/models.rs +++ b/datadog-ffe/src/rules_based/ufc/models.rs @@ -6,57 +6,14 @@ use std::{collections::HashMap, sync::Arc}; use regex::Regex; use serde::{Deserialize, Serialize}; -use crate::rules_based::{EvaluationError, Str}; - -#[allow(missing_docs)] -pub type Timestamp = crate::rules_based::timestamp::Timestamp; - -// Temporary workaround till we figure out one proper format -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(untagged)] -pub enum WireTimestamp { - Iso8601(Timestamp), - UnixMs(i64), -} - -impl From for Timestamp { - fn from(value: WireTimestamp) -> Self { - match value { - WireTimestamp::Iso8601(ts) => ts, - WireTimestamp::UnixMs(unix) => { - Timestamp::from_timestamp_millis(unix).expect("timestamp should be in range") - } - } - } -} - -/// JSON API wrapper for Universal Flag Configuration. -#[derive(Debug, Serialize, Deserialize, Clone)] -pub(crate) struct UniversalFlagConfigWire { - /// JSON API data envelope. - pub data: UniversalFlagConfigData, -} - -/// JSON API data structure for Universal Flag Configuration. -#[derive(Debug, Serialize, Deserialize, Clone)] -pub(crate) struct UniversalFlagConfigData { - /// JSON API type field. - #[serde(rename = "type")] - pub data_type: String, - /// JSON API id field. - pub id: String, - /// JSON API attributes containing the actual UFC data. - pub attributes: UniversalFlagConfigAttributes, -} +use crate::rules_based::{EvaluationError, Str, Timestamp}; /// Universal Flag Configuration attributes. This contains the actual flag configuration data. #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] -pub(crate) struct UniversalFlagConfigAttributes { +pub(crate) struct UniversalFlagConfigWire { /// When configuration was last updated. - pub created_at: WireTimestamp, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub format: Option, + pub created_at: Timestamp, /// Environment this configuration belongs to. pub environment: Environment, /// Flags configuration. @@ -66,14 +23,6 @@ pub(crate) struct UniversalFlagConfigAttributes { pub flags: HashMap>, } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum ConfigurationFormat { - Client, - Server, - Precomputed, -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Environment { @@ -499,14 +448,7 @@ mod tests { #[test] fn parse_flags_v1() { - let json_content = { - let path = if std::path::Path::new("tests/data/flags-v1.json").exists() { - "tests/data/flags-v1.json" - } else { - "domains/ffe/libs/flagging/rust/evaluation/tests/data/flags-v1.json" - }; - std::fs::read_to_string(path).unwrap() - }; + let json_content = std::fs::read_to_string("tests/data/flags-v1.json").unwrap(); let _ufc: UniversalFlagConfigWire = serde_json::from_str(&json_content).unwrap(); } @@ -515,29 +457,26 @@ mod tests { let ufc: UniversalFlagConfigWire = serde_json::from_str( r#" { - "data": { - "type": "universal-flag-configuration", - "id": "1", - "attributes": { - "createdAt": "2024-07-18T00:00:00Z", - "format": "SERVER", - "environment": {"name": "test"}, - "flags": { - "success": { - "key": "success", - "enabled": true, - "variationType": "BOOLEAN", - "variations": {}, - "allocations": [] - }, - "fail_parsing": { - "key": "fail_parsing", - "enabled": true, - "variationType": "NEW_TYPE", - "variations": {}, - "allocations": [] - } - } + "id": "1", + "createdAt": "2024-07-18T00:00:00Z", + "format": "SERVER", + "environment": { + "name": "test" + }, + "flags": { + "success": { + "key": "success", + "enabled": true, + "variationType": "BOOLEAN", + "variations": {}, + "allocations": [] + }, + "fail_parsing": { + "key": "fail_parsing", + "enabled": true, + "variationType": "NEW_TYPE", + "variations": {}, + "allocations": [] } } } @@ -545,20 +484,17 @@ mod tests { ) .unwrap(); assert!( - matches!( - ufc.data.attributes.flags.get("success").unwrap(), - TryParse::Parsed(_) - ), + matches!(ufc.flags.get("success").unwrap(), TryParse::Parsed(_)), "{:?} should match TryParse::Parsed(_)", - ufc.data.attributes.flags.get("success").unwrap() + ufc.flags.get("success").unwrap() ); assert!( matches!( - ufc.data.attributes.flags.get("fail_parsing").unwrap(), + ufc.flags.get("fail_parsing").unwrap(), TryParse::ParseFailed(_) ), "{:?} should match TryParse::ParseFailed(_)", - ufc.data.attributes.flags.get("fail_parsing").unwrap() + ufc.flags.get("fail_parsing").unwrap() ); } } diff --git a/datadog-ffe/tests/data/flags-v1.json b/datadog-ffe/tests/data/flags-v1.json index e8e285b90b..670715638e 100644 --- a/datadog-ffe/tests/data/flags-v1.json +++ b/datadog-ffe/tests/data/flags-v1.json @@ -1,3060 +1,3079 @@ { - "data": { - "type": "universal-flag-configuration", - "id": "1", - "attributes": { - "createdAt": "2024-04-17T19:40:53.716Z", - "format": "SERVER", - "environment": { - "name": "Test" + "id": "1", + "createdAt": "2024-04-17T19:40:53.716Z", + "format": "SERVER", + "environment": { + "name": "Test" + }, + "flags": { + "empty_flag": { + "key": "empty_flag", + "enabled": true, + "variationType": "STRING", + "variations": {}, + "allocations": [] + }, + "disabled_flag": { + "key": "disabled_flag", + "enabled": false, + "variationType": "INTEGER", + "variations": {}, + "allocations": [] + }, + "no_allocations_flag": { + "key": "no_allocations_flag", + "enabled": true, + "variationType": "JSON", + "variations": { + "control": { + "key": "control", + "value": { + "variant": "control" + } + }, + "treatment": { + "key": "treatment", + "value": { + "variant": "treatment" + } + } }, - "flags": { - "empty_flag": { - "key": "empty_flag", - "enabled": true, - "variationType": "STRING", - "variations": {}, - "allocations": [] - }, - "disabled_flag": { - "key": "disabled_flag", - "enabled": false, - "variationType": "INTEGER", - "variations": {}, - "allocations": [] - }, - "no_allocations_flag": { - "key": "no_allocations_flag", - "enabled": true, - "variationType": "JSON", - "variations": { - "control": { - "key": "control", - "value": {"variant": "control"} - }, - "treatment": { - "key": "treatment", - "value": {"variant": "treatment"} - } - }, - "allocations": [] - }, - "numeric_flag": { - "key": "numeric_flag", - "enabled": true, - "variationType": "NUMERIC", - "variations": { - "e": { - "key": "e", - "value": 2.7182818 - }, - "pi": { - "key": "pi", - "value": 3.1415926 + "allocations": [] + }, + "numeric_flag": { + "key": "numeric_flag", + "enabled": true, + "variationType": "NUMERIC", + "variations": { + "e": { + "key": "e", + "value": 2.7182818 + }, + "pi": { + "key": "pi", + "value": 3.1415926 + } + }, + "allocations": [ + { + "key": "rollout", + "splits": [ + { + "variationKey": "pi", + "shards": [] } - }, - "allocations": [ + ], + "doLog": true + } + ] + }, + "regex-flag": { + "key": "regex-flag", + "enabled": true, + "variationType": "STRING", + "variations": { + "partial-example": { + "key": "partial-example", + "value": "partial-example" + }, + "test": { + "key": "test", + "value": "test" + } + }, + "allocations": [ + { + "key": "partial-example", + "rules": [ { - "key": "rollout", - "splits": [ + "conditions": [ { - "variationKey": "pi", - "shards": [] + "attribute": "email", + "operator": "MATCHES", + "value": "@example\\.com" } - ], - "doLog": true - } - ] - }, - "regex-flag": { - "key": "regex-flag", - "enabled": true, - "variationType": "STRING", - "variations": { - "partial-example": { - "key": "partial-example", - "value": "partial-example" - }, - "test": { - "key": "test", - "value": "test" + ] + } + ], + "splits": [ + { + "variationKey": "partial-example", + "shards": [] } - }, - "allocations": [ + ], + "doLog": true + }, + { + "key": "test", + "rules": [ { - "key": "partial-example", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "email", - "operator": "MATCHES", - "value": "@example\\.com" - } - ] + "attribute": "email", + "operator": "MATCHES", + "value": ".*@test\\.com" } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "test", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "numeric-one-of": { + "key": "numeric-one-of", + "enabled": true, + "variationType": "INTEGER", + "variations": { + "1": { + "key": "1", + "value": 1 + }, + "2": { + "key": "2", + "value": 2 + }, + "3": { + "key": "3", + "value": 3 + } + }, + "allocations": [ + { + "key": "1-for-1", + "rules": [ + { + "conditions": [ { - "variationKey": "partial-example", - "shards": [] + "attribute": "number", + "operator": "ONE_OF", + "value": [ + "1" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "1", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "2-for-123456789", + "rules": [ { - "key": "test", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "email", - "operator": "MATCHES", - "value": ".*@test\\.com" - } + "attribute": "number", + "operator": "ONE_OF", + "value": [ + "123456789" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "2", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "3-for-not-2", + "rules": [ + { + "conditions": [ { - "variationKey": "test", - "shards": [] + "attribute": "number", + "operator": "NOT_ONE_OF", + "value": [ + "2" + ] } - ], - "doLog": true - } - ] - }, - "numeric-one-of": { - "key": "numeric-one-of", - "enabled": true, - "variationType": "INTEGER", - "variations": { - "1": { - "key": "1", - "value": 1 - }, - "2": { - "key": "2", - "value": 2 - }, - "3": { - "key": "3", - "value": 3 + ] + } + ], + "splits": [ + { + "variationKey": "3", + "shards": [] } - }, - "allocations": [ + ], + "doLog": true + } + ] + }, + "boolean-one-of-matches": { + "key": "boolean-one-of-matches", + "enabled": true, + "variationType": "INTEGER", + "variations": { + "1": { + "key": "1", + "value": 1 + }, + "2": { + "key": "2", + "value": 2 + }, + "3": { + "key": "3", + "value": 3 + }, + "4": { + "key": "4", + "value": 4 + }, + "5": { + "key": "5", + "value": 5 + } + }, + "allocations": [ + { + "key": "1-for-one-of", + "rules": [ { - "key": "1-for-1", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "number", - "operator": "ONE_OF", - "value": [ - "1" - ] - } + "attribute": "one_of_flag", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "1", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "2-for-matches", + "rules": [ + { + "conditions": [ { - "variationKey": "1", - "shards": [] + "attribute": "matches_flag", + "operator": "MATCHES", + "value": "true" } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "2", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "3-for-not-one-of", + "rules": [ { - "key": "2-for-123456789", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "number", - "operator": "ONE_OF", - "value": [ - "123456789" - ] - } + "attribute": "not_one_of_flag", + "operator": "NOT_ONE_OF", + "value": [ + "false" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "3", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "4-for-not-matches", + "rules": [ + { + "conditions": [ { - "variationKey": "2", - "shards": [] + "attribute": "not_matches_flag", + "operator": "NOT_MATCHES", + "value": "false" } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "4", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "5-for-matches-null", + "rules": [ { - "key": "3-for-not-2", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "number", - "operator": "NOT_ONE_OF", - "value": [ - "2" - ] - } + "attribute": "null_flag", + "operator": "ONE_OF", + "value": [ + "null" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "5", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "empty_string_flag": { + "key": "empty_string_flag", + "enabled": true, + "comment": "Testing the empty string as a variation value", + "variationType": "STRING", + "variations": { + "empty_string": { + "key": "empty_string", + "value": "" + }, + "non_empty": { + "key": "non_empty", + "value": "non_empty" + } + }, + "allocations": [ + { + "key": "allocation-empty", + "rules": [ + { + "conditions": [ { - "variationKey": "3", - "shards": [] + "attribute": "country", + "operator": "MATCHES", + "value": "US" } - ], - "doLog": true - } - ] - }, - "boolean-one-of-matches": { - "key": "boolean-one-of-matches", - "enabled": true, - "variationType": "INTEGER", - "variations": { - "1": { - "key": "1", - "value": 1 - }, - "2": { - "key": "2", - "value": 2 - }, - "3": { - "key": "3", - "value": 3 - }, - "4": { - "key": "4", - "value": 4 - }, - "5": { - "key": "5", - "value": 5 + ] } - }, - "allocations": [ + ], + "splits": [ { - "key": "1-for-one-of", - "rules": [ + "variationKey": "empty_string", + "shards": [ { - "conditions": [ + "salt": "allocation-empty-shards", + "totalShards": 10000, + "ranges": [ { - "attribute": "one_of_flag", - "operator": "ONE_OF", - "value": [ - "true" - ] + "start": 0, + "end": 10000 } ] } - ], - "splits": [ - { - "variationKey": "1", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "doLog": true + }, + { + "key": "allocation-test", + "rules": [], + "splits": [ { - "key": "2-for-matches", - "rules": [ + "variationKey": "non_empty", + "shards": [ { - "conditions": [ + "salt": "allocation-empty-shards", + "totalShards": 10000, + "ranges": [ { - "attribute": "matches_flag", - "operator": "MATCHES", - "value": "true" + "start": 0, + "end": 10000 } ] } - ], - "splits": [ + ] + } + ], + "doLog": true + } + ] + }, + "kill-switch": { + "key": "kill-switch", + "enabled": true, + "variationType": "BOOLEAN", + "variations": { + "on": { + "key": "on", + "value": true + }, + "off": { + "key": "off", + "value": false + } + }, + "allocations": [ + { + "key": "on-for-NA", + "rules": [ + { + "conditions": [ { - "variationKey": "2", - "shards": [] + "attribute": "country", + "operator": "ONE_OF", + "value": [ + "US", + "Canada", + "Mexico" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "3-for-not-one-of", - "rules": [ + "variationKey": "on", + "shards": [ { - "conditions": [ + "salt": "some-salt", + "totalShards": 10000, + "ranges": [ { - "attribute": "not_one_of_flag", - "operator": "NOT_ONE_OF", - "value": [ - "false" - ] + "start": 0, + "end": 10000 } ] } - ], - "splits": [ + ] + } + ], + "doLog": true + }, + { + "key": "on-for-age-50+", + "rules": [ + { + "conditions": [ { - "variationKey": "3", - "shards": [] + "attribute": "age", + "operator": "GTE", + "value": 50 } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "4-for-not-matches", - "rules": [ + "variationKey": "on", + "shards": [ { - "conditions": [ + "salt": "some-salt", + "totalShards": 10000, + "ranges": [ { - "attribute": "not_matches_flag", - "operator": "NOT_MATCHES", - "value": "false" + "start": 0, + "end": 10000 } ] } - ], - "splits": [ + ] + } + ], + "doLog": true + }, + { + "key": "off-for-all", + "rules": [], + "splits": [ + { + "variationKey": "off", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "comparator-operator-test": { + "key": "comparator-operator-test", + "enabled": true, + "variationType": "STRING", + "variations": { + "small": { + "key": "small", + "value": "small" + }, + "medium": { + "key": "medium", + "value": "medium" + }, + "large": { + "key": "large", + "value": "large" + } + }, + "allocations": [ + { + "key": "small-size", + "rules": [ + { + "conditions": [ { - "variationKey": "4", - "shards": [] + "attribute": "size", + "operator": "LT", + "value": 10 } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "small", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "medum-size", + "rules": [ { - "key": "5-for-matches-null", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "null_flag", - "operator": "ONE_OF", - "value": [ - "null" - ] - } - ] + "attribute": "size", + "operator": "GTE", + "value": 10 + }, + { + "attribute": "size", + "operator": "LTE", + "value": 20 } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "medium", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "large-size", + "rules": [ + { + "conditions": [ { - "variationKey": "5", - "shards": [] + "attribute": "size", + "operator": "GT", + "value": 25 } - ], - "doLog": true - } - ] - }, - "empty_string_flag": { - "key": "empty_string_flag", - "enabled": true, - "comment": "Testing the empty string as a variation value", - "variationType": "STRING", - "variations": { - "empty_string": { - "key": "empty_string", - "value": "" + ] + } + ], + "splits": [ + { + "variationKey": "large", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "start-and-end-date-test": { + "key": "start-and-end-date-test", + "enabled": true, + "variationType": "STRING", + "variations": { + "old": { + "key": "old", + "value": "old" + }, + "current": { + "key": "current", + "value": "current" + }, + "new": { + "key": "new", + "value": "new" + } + }, + "allocations": [ + { + "key": "old-versions", + "splits": [ + { + "variationKey": "old", + "shards": [] + } + ], + "endAt": "2002-10-31T09:00:00.594Z", + "doLog": true + }, + { + "key": "future-versions", + "splits": [ + { + "variationKey": "new", + "shards": [] + } + ], + "startAt": "2052-10-31T09:00:00.594Z", + "doLog": true + }, + { + "key": "current-versions", + "splits": [ + { + "variationKey": "current", + "shards": [] + } + ], + "startAt": "2022-10-31T09:00:00.594Z", + "endAt": "2050-10-31T09:00:00.594Z", + "doLog": true + } + ] + }, + "null-operator-test": { + "key": "null-operator-test", + "enabled": true, + "variationType": "STRING", + "variations": { + "old": { + "key": "old", + "value": "old" + }, + "new": { + "key": "new", + "value": "new" + } + }, + "allocations": [ + { + "key": "null-operator", + "rules": [ + { + "conditions": [ + { + "attribute": "size", + "operator": "IS_NULL", + "value": true + } + ] }, - "non_empty": { - "key": "non_empty", - "value": "non_empty" + { + "conditions": [ + { + "attribute": "size", + "operator": "LT", + "value": 10 + } + ] + } + ], + "splits": [ + { + "variationKey": "old", + "shards": [] } - }, - "allocations": [ + ], + "doLog": true + }, + { + "key": "not-null-operator", + "rules": [ { - "key": "allocation-empty", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "country", - "operator": "MATCHES", - "value": "US" - } + "attribute": "size", + "operator": "IS_NULL", + "value": false + } + ] + } + ], + "splits": [ + { + "variationKey": "new", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "new-user-onboarding": { + "key": "new-user-onboarding", + "enabled": true, + "variationType": "STRING", + "variations": { + "control": { + "key": "control", + "value": "control" + }, + "red": { + "key": "red", + "value": "red" + }, + "blue": { + "key": "blue", + "value": "blue" + }, + "green": { + "key": "green", + "value": "green" + }, + "yellow": { + "key": "yellow", + "value": "yellow" + }, + "purple": { + "key": "purple", + "value": "purple" + } + }, + "allocations": [ + { + "key": "id rule", + "rules": [ + { + "conditions": [ + { + "attribute": "id", + "operator": "MATCHES", + "value": "zach" + } + ] + } + ], + "splits": [ + { + "variationKey": "purple", + "shards": [] + } + ], + "doLog": false + }, + { + "key": "internal users", + "rules": [ + { + "conditions": [ + { + "attribute": "email", + "operator": "MATCHES", + "value": "@mycompany.com" + } + ] + } + ], + "splits": [ + { + "variationKey": "green", + "shards": [] + } + ], + "doLog": false + }, + { + "key": "experiment", + "rules": [ + { + "conditions": [ + { + "attribute": "country", + "operator": "NOT_ONE_OF", + "value": [ + "US", + "Canada", + "Mexico" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "control", + "shards": [ { - "variationKey": "empty_string", - "shards": [ + "salt": "traffic-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ { - "salt": "allocation-empty-shards", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 10000 - } - ] + "start": 0, + "end": 6000 } ] - } - ], - "doLog": true - }, - { - "key": "allocation-test", - "rules": [], - "splits": [ + }, { - "variationKey": "non_empty", - "shards": [ + "salt": "split-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ { - "salt": "allocation-empty-shards", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 10000 - } - ] + "start": 0, + "end": 5000 } ] } - ], - "doLog": true - } - ] - }, - "kill-switch": { - "key": "kill-switch", - "enabled": true, - "variationType": "BOOLEAN", - "variations": { - "on": { - "key": "on", - "value": true + ] }, - "off": { - "key": "off", - "value": false - } - }, - "allocations": [ { - "key": "on-for-NA", - "rules": [ + "variationKey": "red", + "shards": [ { - "conditions": [ + "salt": "traffic-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ { - "attribute": "country", - "operator": "ONE_OF", - "value": [ - "US", - "Canada", - "Mexico" - ] + "start": 0, + "end": 6000 } ] - } - ], - "splits": [ + }, { - "variationKey": "on", - "shards": [ + "salt": "split-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ { - "salt": "some-salt", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 10000 - } - ] + "start": 5000, + "end": 8000 } ] } - ], - "doLog": true + ] }, { - "key": "on-for-age-50+", - "rules": [ + "variationKey": "yellow", + "shards": [ { - "conditions": [ + "salt": "traffic-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ { - "attribute": "age", - "operator": "GTE", - "value": 50 + "start": 0, + "end": 6000 } ] - } - ], - "splits": [ + }, { - "variationKey": "on", - "shards": [ + "salt": "split-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ { - "salt": "some-salt", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 10000 - } - ] + "start": 8000, + "end": 10000 } ] } - ], - "doLog": true - }, + ] + } + ], + "doLog": true + }, + { + "key": "rollout", + "rules": [ { - "key": "off-for-all", - "rules": [], - "splits": [ + "conditions": [ { - "variationKey": "off", - "shards": [] + "attribute": "country", + "operator": "ONE_OF", + "value": [ + "US", + "Canada", + "Mexico" + ] } - ], - "doLog": true - } - ] - }, - "comparator-operator-test": { - "key": "comparator-operator-test", - "enabled": true, - "variationType": "STRING", - "variations": { - "small": { - "key": "small", - "value": "small" - }, - "medium": { - "key": "medium", - "value": "medium" - }, - "large": { - "key": "large", - "value": "large" + ] } - }, - "allocations": [ + ], + "splits": [ { - "key": "small-size", - "rules": [ + "variationKey": "blue", + "shards": [ { - "conditions": [ + "salt": "split-new-user-onboarding-rollout", + "totalShards": 10000, + "ranges": [ { - "attribute": "size", - "operator": "LT", - "value": 10 + "start": 0, + "end": 8000 } ] } ], - "splits": [ + "extraLogging": { + "allocationvalue_type": "rollout", + "owner": "hippo" + } + } + ], + "doLog": true + } + ] + }, + "integer-flag": { + "key": "integer-flag", + "enabled": true, + "variationType": "INTEGER", + "variations": { + "one": { + "key": "one", + "value": 1 + }, + "two": { + "key": "two", + "value": 2 + }, + "three": { + "key": "three", + "value": 3 + } + }, + "allocations": [ + { + "key": "targeted allocation", + "rules": [ + { + "conditions": [ { - "variationKey": "small", - "shards": [] + "attribute": "country", + "operator": "ONE_OF", + "value": [ + "US", + "Canada", + "Mexico" + ] } - ], - "doLog": true + ] }, { - "key": "medum-size", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "size", - "operator": "GTE", - "value": 10 - }, + "attribute": "email", + "operator": "MATCHES", + "value": ".*@example.com" + } + ] + } + ], + "splits": [ + { + "variationKey": "three", + "shards": [ + { + "salt": "full-range-salt", + "totalShards": 10000, + "ranges": [ { - "attribute": "size", - "operator": "LTE", - "value": 20 + "start": 0, + "end": 10000 } ] } - ], - "splits": [ - { - "variationKey": "medium", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "doLog": true + }, + { + "key": "50/50 split", + "rules": [], + "splits": [ { - "key": "large-size", - "rules": [ + "variationKey": "one", + "shards": [ { - "conditions": [ + "salt": "split-numeric-flag-some-allocation", + "totalShards": 10000, + "ranges": [ { - "attribute": "size", - "operator": "GT", - "value": 25 + "start": 0, + "end": 5000 } ] } - ], - "splits": [ - { - "variationKey": "large", - "shards": [] - } - ], - "doLog": true - } - ] - }, - "start-and-end-date-test": { - "key": "start-and-end-date-test", - "enabled": true, - "variationType": "STRING", - "variations": { - "old": { - "key": "old", - "value": "old" - }, - "current": { - "key": "current", - "value": "current" + ] }, - "new": { - "key": "new", - "value": "new" - } - }, - "allocations": [ { - "key": "old-versions", - "splits": [ + "variationKey": "two", + "shards": [ { - "variationKey": "old", - "shards": [] + "salt": "split-numeric-flag-some-allocation", + "totalShards": 10000, + "ranges": [ + { + "start": 5000, + "end": 10000 + } + ] } - ], - "endAt": "2002-10-31T09:00:00.594Z", - "doLog": true - }, + ] + } + ], + "doLog": true + } + ] + }, + "json-config-flag": { + "key": "json-config-flag", + "enabled": true, + "variationType": "JSON", + "variations": { + "one": { + "key": "one", + "value": { + "integer": 1, + "string": "one", + "float": 1.0 + } + }, + "two": { + "key": "two", + "value": { + "integer": 2, + "string": "two", + "float": 2.0 + } + }, + "empty": { + "key": "empty", + "value": {} + } + }, + "allocations": [ + { + "key": "Optionally Force Empty", + "rules": [ { - "key": "future-versions", - "splits": [ + "conditions": [ { - "variationKey": "new", - "shards": [] + "attribute": "Force Empty", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "startAt": "2052-10-31T09:00:00.594Z", - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "current-versions", - "splits": [ + "variationKey": "empty", + "shards": [ { - "variationKey": "current", - "shards": [] + "salt": "full-range-salt", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 10000 + } + ] } - ], - "startAt": "2022-10-31T09:00:00.594Z", - "endAt": "2050-10-31T09:00:00.594Z", - "doLog": true - } - ] - }, - "null-operator-test": { - "key": "null-operator-test", - "enabled": true, - "variationType": "STRING", - "variations": { - "old": { - "key": "old", - "value": "old" - }, - "new": { - "key": "new", - "value": "new" + ] } - }, - "allocations": [ + ], + "doLog": true + }, + { + "key": "50/50 split", + "rules": [], + "splits": [ { - "key": "null-operator", - "rules": [ + "variationKey": "one", + "shards": [ { - "conditions": [ + "salt": "traffic-json-flag", + "totalShards": 10000, + "ranges": [ { - "attribute": "size", - "operator": "IS_NULL", - "value": true + "start": 0, + "end": 10000 } ] }, { - "conditions": [ + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ { - "attribute": "size", - "operator": "LT", - "value": 10 + "start": 0, + "end": 5000 } ] } - ], - "splits": [ - { - "variationKey": "old", - "shards": [] - } - ], - "doLog": true + ] }, { - "key": "not-null-operator", - "rules": [ + "variationKey": "two", + "shards": [ { - "conditions": [ + "salt": "traffic-json-flag", + "totalShards": 10000, + "ranges": [ { - "attribute": "size", - "operator": "IS_NULL", - "value": false + "start": 0, + "end": 10000 } ] - } - ], - "splits": [ + }, { - "variationKey": "new", - "shards": [] + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ + { + "start": 5000, + "end": 10000 + } + ] } - ], - "doLog": true - } - ] - }, - "new-user-onboarding": { - "key": "new-user-onboarding", - "enabled": true, - "variationType": "STRING", - "variations": { - "control": { - "key": "control", - "value": "control" - }, - "red": { - "key": "red", - "value": "red" - }, - "blue": { - "key": "blue", - "value": "blue" - }, - "green": { - "key": "green", - "value": "green" - }, - "yellow": { - "key": "yellow", - "value": "yellow" - }, - "purple": { - "key": "purple", - "value": "purple" + ] } - }, - "allocations": [ + ], + "doLog": true + } + ] + }, + "special-characters": { + "key": "special-characters", + "enabled": true, + "variationType": "JSON", + "variations": { + "de": { + "key": "de", + "value": { + "a": "kümmert", + "b": "schön" + } + }, + "ua": { + "key": "ua", + "value": { + "a": "піклуватися", + "b": "любов" + } + }, + "zh": { + "key": "zh", + "value": { + "a": "照顾", + "b": "漂亮" + } + }, + "emoji": { + "key": "emoji", + "value": { + "a": "🤗", + "b": "🌸" + } + } + }, + "allocations": [ + { + "key": "allocation-test", + "splits": [ { - "key": "id rule", - "rules": [ + "variationKey": "de", + "shards": [ { - "conditions": [ + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ { - "attribute": "id", - "operator": "MATCHES", - "value": "zach" + "start": 0, + "end": 2500 } ] } - ], - "splits": [ - { - "variationKey": "purple", - "shards": [] - } - ], - "doLog": false + ] }, { - "key": "internal users", - "rules": [ + "variationKey": "ua", + "shards": [ { - "conditions": [ + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ { - "attribute": "email", - "operator": "MATCHES", - "value": "@mycompany.com" + "start": 2500, + "end": 5000 } ] } - ], - "splits": [ - { - "variationKey": "green", - "shards": [] - } - ], - "doLog": false + ] }, { - "key": "experiment", - "rules": [ + "variationKey": "zh", + "shards": [ { - "conditions": [ + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ { - "attribute": "country", - "operator": "NOT_ONE_OF", - "value": [ - "US", - "Canada", - "Mexico" - ] + "start": 5000, + "end": 7500 } ] } - ], - "splits": [ + ] + }, + { + "variationKey": "emoji", + "shards": [ { - "variationKey": "control", - "shards": [ - { - "salt": "traffic-new-user-onboarding-experiment", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 6000 - } - ] - }, + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ { - "salt": "split-new-user-onboarding-experiment", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 5000 - } - ] + "start": 7500, + "end": 10000 } ] - }, + } + ] + } + ], + "doLog": true + }, + { + "key": "allocation-default", + "splits": [ + { + "variationKey": "de", + "shards": [] + } + ], + "doLog": false + } + ] + }, + "string_flag_with_special_characters": { + "key": "string_flag_with_special_characters", + "enabled": true, + "comment": "Testing the string with special characters and spaces", + "variationType": "STRING", + "variations": { + "string_with_spaces": { + "key": "string_with_spaces", + "value": " a b c d e f " + }, + "string_with_only_one_space": { + "key": "string_with_only_one_space", + "value": " " + }, + "string_with_only_multiple_spaces": { + "key": "string_with_only_multiple_spaces", + "value": " " + }, + "string_with_dots": { + "key": "string_with_dots", + "value": ".a.b.c.d.e.f." + }, + "string_with_only_one_dot": { + "key": "string_with_only_one_dot", + "value": "." + }, + "string_with_only_multiple_dots": { + "key": "string_with_only_multiple_dots", + "value": "......." + }, + "string_with_comas": { + "key": "string_with_comas", + "value": ",a,b,c,d,e,f," + }, + "string_with_only_one_coma": { + "key": "string_with_only_one_coma", + "value": "," + }, + "string_with_only_multiple_comas": { + "key": "string_with_only_multiple_comas", + "value": ",,,,,,," + }, + "string_with_colons": { + "key": "string_with_colons", + "value": ":a:b:c:d:e:f:" + }, + "string_with_only_one_colon": { + "key": "string_with_only_one_colon", + "value": ":" + }, + "string_with_only_multiple_colons": { + "key": "string_with_only_multiple_colons", + "value": ":::::::" + }, + "string_with_semicolons": { + "key": "string_with_semicolons", + "value": ";a;b;c;d;e;f;" + }, + "string_with_only_one_semicolon": { + "key": "string_with_only_one_semicolon", + "value": ";" + }, + "string_with_only_multiple_semicolons": { + "key": "string_with_only_multiple_semicolons", + "value": ";;;;;;;" + }, + "string_with_slashes": { + "key": "string_with_slashes", + "value": "/a/b/c/d/e/f/" + }, + "string_with_only_one_slash": { + "key": "string_with_only_one_slash", + "value": "/" + }, + "string_with_only_multiple_slashes": { + "key": "string_with_only_multiple_slashes", + "value": "///////" + }, + "string_with_dashes": { + "key": "string_with_dashes", + "value": "-a-b-c-d-e-f-" + }, + "string_with_only_one_dash": { + "key": "string_with_only_one_dash", + "value": "-" + }, + "string_with_only_multiple_dashes": { + "key": "string_with_only_multiple_dashes", + "value": "-------" + }, + "string_with_underscores": { + "key": "string_with_underscores", + "value": "_a_b_c_d_e_f_" + }, + "string_with_only_one_underscore": { + "key": "string_with_only_one_underscore", + "value": "_" + }, + "string_with_only_multiple_underscores": { + "key": "string_with_only_multiple_underscores", + "value": "_______" + }, + "string_with_plus_signs": { + "key": "string_with_plus_signs", + "value": "+a+b+c+d+e+f+" + }, + "string_with_only_one_plus_sign": { + "key": "string_with_only_one_plus_sign", + "value": "+" + }, + "string_with_only_multiple_plus_signs": { + "key": "string_with_only_multiple_plus_signs", + "value": "+++++++" + }, + "string_with_equal_signs": { + "key": "string_with_equal_signs", + "value": "=a=b=c=d=e=f=" + }, + "string_with_only_one_equal_sign": { + "key": "string_with_only_one_equal_sign", + "value": "=" + }, + "string_with_only_multiple_equal_signs": { + "key": "string_with_only_multiple_equal_signs", + "value": "=======" + }, + "string_with_dollar_signs": { + "key": "string_with_dollar_signs", + "value": "$a$b$c$d$e$f$" + }, + "string_with_only_one_dollar_sign": { + "key": "string_with_only_one_dollar_sign", + "value": "$" + }, + "string_with_only_multiple_dollar_signs": { + "key": "string_with_only_multiple_dollar_signs", + "value": "$$$$$$$" + }, + "string_with_at_signs": { + "key": "string_with_at_signs", + "value": "@a@b@c@d@e@f@" + }, + "string_with_only_one_at_sign": { + "key": "string_with_only_one_at_sign", + "value": "@" + }, + "string_with_only_multiple_at_signs": { + "key": "string_with_only_multiple_at_signs", + "value": "@@@@@@@" + }, + "string_with_amp_signs": { + "key": "string_with_amp_signs", + "value": "&a&b&c&d&e&f&" + }, + "string_with_only_one_amp_sign": { + "key": "string_with_only_one_amp_sign", + "value": "&" + }, + "string_with_only_multiple_amp_signs": { + "key": "string_with_only_multiple_amp_signs", + "value": "&&&&&&&" + }, + "string_with_hash_signs": { + "key": "string_with_hash_signs", + "value": "#a#b#c#d#e#f#" + }, + "string_with_only_one_hash_sign": { + "key": "string_with_only_one_hash_sign", + "value": "#" + }, + "string_with_only_multiple_hash_signs": { + "key": "string_with_only_multiple_hash_signs", + "value": "#######" + }, + "string_with_percentage_signs": { + "key": "string_with_percentage_signs", + "value": "%a%b%c%d%e%f%" + }, + "string_with_only_one_percentage_sign": { + "key": "string_with_only_one_percentage_sign", + "value": "%" + }, + "string_with_only_multiple_percentage_signs": { + "key": "string_with_only_multiple_percentage_signs", + "value": "%%%%%%%" + }, + "string_with_tilde_signs": { + "key": "string_with_tilde_signs", + "value": "~a~b~c~d~e~f~" + }, + "string_with_only_one_tilde_sign": { + "key": "string_with_only_one_tilde_sign", + "value": "~" + }, + "string_with_only_multiple_tilde_signs": { + "key": "string_with_only_multiple_tilde_signs", + "value": "~~~~~~~" + }, + "string_with_asterix_signs": { + "key": "string_with_asterix_signs", + "value": "*a*b*c*d*e*f*" + }, + "string_with_only_one_asterix_sign": { + "key": "string_with_only_one_asterix_sign", + "value": "*" + }, + "string_with_only_multiple_asterix_signs": { + "key": "string_with_only_multiple_asterix_signs", + "value": "*******" + }, + "string_with_single_quotes": { + "key": "string_with_single_quotes", + "value": "'a'b'c'd'e'f'" + }, + "string_with_only_one_single_quote": { + "key": "string_with_only_one_single_quote", + "value": "'" + }, + "string_with_only_multiple_single_quotes": { + "key": "string_with_only_multiple_single_quotes", + "value": "'''''''" + }, + "string_with_question_marks": { + "key": "string_with_question_marks", + "value": "?a?b?c?d?e?f?" + }, + "string_with_only_one_question_mark": { + "key": "string_with_only_one_question_mark", + "value": "?" + }, + "string_with_only_multiple_question_marks": { + "key": "string_with_only_multiple_question_marks", + "value": "???????" + }, + "string_with_exclamation_marks": { + "key": "string_with_exclamation_marks", + "value": "!a!b!c!d!e!f!" + }, + "string_with_only_one_exclamation_mark": { + "key": "string_with_only_one_exclamation_mark", + "value": "!" + }, + "string_with_only_multiple_exclamation_marks": { + "key": "string_with_only_multiple_exclamation_marks", + "value": "!!!!!!!" + }, + "string_with_opening_parentheses": { + "key": "string_with_opening_parentheses", + "value": "(a(b(c(d(e(f(" + }, + "string_with_only_one_opening_parenthese": { + "key": "string_with_only_one_opening_parenthese", + "value": "(" + }, + "string_with_only_multiple_opening_parentheses": { + "key": "string_with_only_multiple_opening_parentheses", + "value": "(((((((" + }, + "string_with_closing_parentheses": { + "key": "string_with_closing_parentheses", + "value": ")a)b)c)d)e)f)" + }, + "string_with_only_one_closing_parenthese": { + "key": "string_with_only_one_closing_parenthese", + "value": ")" + }, + "string_with_only_multiple_closing_parentheses": { + "key": "string_with_only_multiple_closing_parentheses", + "value": ")))))))" + } + }, + "allocations": [ + { + "key": "allocation-test-string_with_spaces", + "rules": [ + { + "conditions": [ { - "variationKey": "red", - "shards": [ - { - "salt": "traffic-new-user-onboarding-experiment", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 6000 - } - ] - }, - { - "salt": "split-new-user-onboarding-experiment", - "totalShards": 10000, - "ranges": [ - { - "start": 5000, - "end": 8000 - } - ] - } + "attribute": "string_with_spaces", + "operator": "ONE_OF", + "value": [ + "true" ] - }, + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_spaces", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_space", + "rules": [ + { + "conditions": [ { - "variationKey": "yellow", - "shards": [ - { - "salt": "traffic-new-user-onboarding-experiment", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 6000 - } - ] - }, - { - "salt": "split-new-user-onboarding-experiment", - "totalShards": 10000, - "ranges": [ - { - "start": 8000, - "end": 10000 - } - ] - } + "attribute": "string_with_only_one_space", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_space", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_spaces", + "rules": [ { - "key": "rollout", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "country", - "operator": "ONE_OF", - "value": [ - "US", - "Canada", - "Mexico" - ] - } + "attribute": "string_with_only_multiple_spaces", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_spaces", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_dots", + "rules": [ + { + "conditions": [ { - "variationKey": "blue", - "shards": [ - { - "salt": "split-new-user-onboarding-rollout", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 8000 - } - ] - } - ], - "extraLogging": { - "allocationvalue_type": "rollout", - "owner": "hippo" - } + "attribute": "string_with_dots", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - } - ] - }, - "integer-flag": { - "key": "integer-flag", - "enabled": true, - "variationType": "INTEGER", - "variations": { - "one": { - "key": "one", - "value": 1 - }, - "two": { - "key": "two", - "value": 2 - }, - "three": { - "key": "three", - "value": 3 + ] + } + ], + "splits": [ + { + "variationKey": "string_with_dots", + "shards": [] } - }, - "allocations": [ + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_dot", + "rules": [ { - "key": "targeted allocation", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "country", - "operator": "ONE_OF", - "value": [ - "US", - "Canada", - "Mexico" - ] - } + "attribute": "string_with_only_one_dot", + "operator": "ONE_OF", + "value": [ + "true" ] - }, + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_dot", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_dots", + "rules": [ + { + "conditions": [ { - "conditions": [ - { - "attribute": "email", - "operator": "MATCHES", - "value": ".*@example.com" - } + "attribute": "string_with_only_multiple_dots", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_dots", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_comas", + "rules": [ + { + "conditions": [ { - "variationKey": "three", - "shards": [ - { - "salt": "full-range-salt", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 10000 - } - ] - } + "attribute": "string_with_comas", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_comas", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_coma", + "rules": [ { - "key": "50/50 split", - "rules": [], - "splits": [ + "conditions": [ { - "variationKey": "one", - "shards": [ - { - "salt": "split-numeric-flag-some-allocation", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 5000 - } - ] - } + "attribute": "string_with_only_one_coma", + "operator": "ONE_OF", + "value": [ + "true" ] - }, + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_coma", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_comas", + "rules": [ + { + "conditions": [ { - "variationKey": "two", - "shards": [ - { - "salt": "split-numeric-flag-some-allocation", - "totalShards": 10000, - "ranges": [ - { - "start": 5000, - "end": 10000 - } - ] - } + "attribute": "string_with_only_multiple_comas", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "doLog": true - } - ] - }, - "json-config-flag": { - "key": "json-config-flag", - "enabled": true, - "variationType": "JSON", - "variations": { - "one": { - "key": "one", - "value": { "integer": 1, "string": "one", "float": 1.0 } - }, - "two": { - "key": "two", - "value": { "integer": 2, "string": "two", "float": 2.0 } - }, - "empty": { - "key": "empty", - "value": {} + ] } - }, - "allocations": [ + ], + "splits": [ { - "key": "Optionally Force Empty", - "rules": [ + "variationKey": "string_with_only_multiple_comas", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_colons", + "rules": [ + { + "conditions": [ { - "conditions": [ - { - "attribute": "Force Empty", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_colons", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_colons", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_colon", + "rules": [ + { + "conditions": [ { - "variationKey": "empty", - "shards": [ - { - "salt": "full-range-salt", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 10000 - } - ] - } + "attribute": "string_with_only_one_colon", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "50/50 split", - "rules": [], - "splits": [ + "variationKey": "string_with_only_one_colon", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_colons", + "rules": [ + { + "conditions": [ { - "variationKey": "one", - "shards": [ - { - "salt": "traffic-json-flag", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 10000 - } - ] - }, - { - "salt": "split-json-flag", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 5000 - } - ] - } + "attribute": "string_with_only_multiple_colons", + "operator": "ONE_OF", + "value": [ + "true" ] - }, + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_colons", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_semicolons", + "rules": [ + { + "conditions": [ { - "variationKey": "two", - "shards": [ - { - "salt": "traffic-json-flag", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 10000 - } - ] - }, - { - "salt": "split-json-flag", - "totalShards": 10000, - "ranges": [ - { - "start": 5000, - "end": 10000 - } - ] - } + "attribute": "string_with_semicolons", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "doLog": true - } - ] - }, - "special-characters": { - "key": "special-characters", - "enabled": true, - "variationType": "JSON", - "variations": { - "de": { - "key": "de", - "value": {"a": "kümmert", "b": "schön"} - }, - "ua": { - "key": "ua", - "value": {"a": "піклуватися", "b": "любов"} - }, - "zh": { - "key": "zh", - "value": {"a": "照顾", "b": "漂亮"} - }, - "emoji": { - "key": "emoji", - "value": {"a": "🤗", "b": "🌸"} + ] } - }, - "allocations": [ + ], + "splits": [ { - "key": "allocation-test", - "splits": [ + "variationKey": "string_with_semicolons", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_semicolon", + "rules": [ + { + "conditions": [ { - "variationKey": "de", - "shards": [ - { - "salt": "split-json-flag", - "totalShards": 10000, - "ranges": [ - { - "start": 0, - "end": 2500 - } - ] - } - ] - }, - { - "variationKey": "ua", - "shards": [ - { - "salt": "split-json-flag", - "totalShards": 10000, - "ranges": [ - { - "start": 2500, - "end": 5000 - } - ] - } - ] - }, - { - "variationKey": "zh", - "shards": [ - { - "salt": "split-json-flag", - "totalShards": 10000, - "ranges": [ - { - "start": 5000, - "end": 7500 - } - ] - } - ] - }, - { - "variationKey": "emoji", - "shards": [ - { - "salt": "split-json-flag", - "totalShards": 10000, - "ranges": [ - { - "start": 7500, - "end": 10000 - } - ] - } - ] - } - ], - "doLog": true - }, - { - "key": "allocation-default", - "splits": [ - { - "variationKey": "de", - "shards": [] - } - ], - "doLog": false - } - ] - }, - "string_flag_with_special_characters": { - "key": "string_flag_with_special_characters", - "enabled": true, - "comment": "Testing the string with special characters and spaces", - "variationType": "STRING", - "variations": { - "string_with_spaces": { - "key": "string_with_spaces", - "value": " a b c d e f " - }, - "string_with_only_one_space": { - "key": "string_with_only_one_space", - "value": " " - }, - "string_with_only_multiple_spaces": { - "key": "string_with_only_multiple_spaces", - "value": " " - }, - "string_with_dots": { - "key": "string_with_dots", - "value": ".a.b.c.d.e.f." - }, - "string_with_only_one_dot": { - "key": "string_with_only_one_dot", - "value": "." - }, - "string_with_only_multiple_dots": { - "key": "string_with_only_multiple_dots", - "value": "......." - }, - "string_with_comas": { - "key": "string_with_comas", - "value": ",a,b,c,d,e,f," - }, - "string_with_only_one_coma": { - "key": "string_with_only_one_coma", - "value": "," - }, - "string_with_only_multiple_comas": { - "key": "string_with_only_multiple_comas", - "value": ",,,,,,," - }, - "string_with_colons": { - "key": "string_with_colons", - "value": ":a:b:c:d:e:f:" - }, - "string_with_only_one_colon": { - "key": "string_with_only_one_colon", - "value": ":" - }, - "string_with_only_multiple_colons": { - "key": "string_with_only_multiple_colons", - "value": ":::::::" - }, - "string_with_semicolons": { - "key": "string_with_semicolons", - "value": ";a;b;c;d;e;f;" - }, - "string_with_only_one_semicolon": { - "key": "string_with_only_one_semicolon", - "value": ";" - }, - "string_with_only_multiple_semicolons": { - "key": "string_with_only_multiple_semicolons", - "value": ";;;;;;;" - }, - "string_with_slashes": { - "key": "string_with_slashes", - "value": "/a/b/c/d/e/f/" - }, - "string_with_only_one_slash": { - "key": "string_with_only_one_slash", - "value": "/" - }, - "string_with_only_multiple_slashes": { - "key": "string_with_only_multiple_slashes", - "value": "///////" - }, - "string_with_dashes": { - "key": "string_with_dashes", - "value": "-a-b-c-d-e-f-" - }, - "string_with_only_one_dash": { - "key": "string_with_only_one_dash", - "value": "-" - }, - "string_with_only_multiple_dashes": { - "key": "string_with_only_multiple_dashes", - "value": "-------" - }, - "string_with_underscores": { - "key": "string_with_underscores", - "value": "_a_b_c_d_e_f_" - }, - "string_with_only_one_underscore": { - "key": "string_with_only_one_underscore", - "value": "_" - }, - "string_with_only_multiple_underscores": { - "key": "string_with_only_multiple_underscores", - "value": "_______" - }, - "string_with_plus_signs": { - "key": "string_with_plus_signs", - "value": "+a+b+c+d+e+f+" - }, - "string_with_only_one_plus_sign": { - "key": "string_with_only_one_plus_sign", - "value": "+" - }, - "string_with_only_multiple_plus_signs": { - "key": "string_with_only_multiple_plus_signs", - "value": "+++++++" - }, - "string_with_equal_signs": { - "key": "string_with_equal_signs", - "value": "=a=b=c=d=e=f=" - }, - "string_with_only_one_equal_sign": { - "key": "string_with_only_one_equal_sign", - "value": "=" - }, - "string_with_only_multiple_equal_signs": { - "key": "string_with_only_multiple_equal_signs", - "value": "=======" - }, - "string_with_dollar_signs": { - "key": "string_with_dollar_signs", - "value": "$a$b$c$d$e$f$" - }, - "string_with_only_one_dollar_sign": { - "key": "string_with_only_one_dollar_sign", - "value": "$" - }, - "string_with_only_multiple_dollar_signs": { - "key": "string_with_only_multiple_dollar_signs", - "value": "$$$$$$$" - }, - "string_with_at_signs": { - "key": "string_with_at_signs", - "value": "@a@b@c@d@e@f@" - }, - "string_with_only_one_at_sign": { - "key": "string_with_only_one_at_sign", - "value": "@" - }, - "string_with_only_multiple_at_signs": { - "key": "string_with_only_multiple_at_signs", - "value": "@@@@@@@" - }, - "string_with_amp_signs": { - "key": "string_with_amp_signs", - "value": "&a&b&c&d&e&f&" - }, - "string_with_only_one_amp_sign": { - "key": "string_with_only_one_amp_sign", - "value": "&" - }, - "string_with_only_multiple_amp_signs": { - "key": "string_with_only_multiple_amp_signs", - "value": "&&&&&&&" - }, - "string_with_hash_signs": { - "key": "string_with_hash_signs", - "value": "#a#b#c#d#e#f#" - }, - "string_with_only_one_hash_sign": { - "key": "string_with_only_one_hash_sign", - "value": "#" - }, - "string_with_only_multiple_hash_signs": { - "key": "string_with_only_multiple_hash_signs", - "value": "#######" - }, - "string_with_percentage_signs": { - "key": "string_with_percentage_signs", - "value": "%a%b%c%d%e%f%" - }, - "string_with_only_one_percentage_sign": { - "key": "string_with_only_one_percentage_sign", - "value": "%" - }, - "string_with_only_multiple_percentage_signs": { - "key": "string_with_only_multiple_percentage_signs", - "value": "%%%%%%%" - }, - "string_with_tilde_signs": { - "key": "string_with_tilde_signs", - "value": "~a~b~c~d~e~f~" - }, - "string_with_only_one_tilde_sign": { - "key": "string_with_only_one_tilde_sign", - "value": "~" - }, - "string_with_only_multiple_tilde_signs": { - "key": "string_with_only_multiple_tilde_signs", - "value": "~~~~~~~" - }, - "string_with_asterix_signs": { - "key": "string_with_asterix_signs", - "value": "*a*b*c*d*e*f*" - }, - "string_with_only_one_asterix_sign": { - "key": "string_with_only_one_asterix_sign", - "value": "*" - }, - "string_with_only_multiple_asterix_signs": { - "key": "string_with_only_multiple_asterix_signs", - "value": "*******" - }, - "string_with_single_quotes": { - "key": "string_with_single_quotes", - "value": "'a'b'c'd'e'f'" - }, - "string_with_only_one_single_quote": { - "key": "string_with_only_one_single_quote", - "value": "'" - }, - "string_with_only_multiple_single_quotes": { - "key": "string_with_only_multiple_single_quotes", - "value": "'''''''" - }, - "string_with_question_marks": { - "key": "string_with_question_marks", - "value": "?a?b?c?d?e?f?" - }, - "string_with_only_one_question_mark": { - "key": "string_with_only_one_question_mark", - "value": "?" - }, - "string_with_only_multiple_question_marks": { - "key": "string_with_only_multiple_question_marks", - "value": "???????" - }, - "string_with_exclamation_marks": { - "key": "string_with_exclamation_marks", - "value": "!a!b!c!d!e!f!" - }, - "string_with_only_one_exclamation_mark": { - "key": "string_with_only_one_exclamation_mark", - "value": "!" - }, - "string_with_only_multiple_exclamation_marks": { - "key": "string_with_only_multiple_exclamation_marks", - "value": "!!!!!!!" - }, - "string_with_opening_parentheses": { - "key": "string_with_opening_parentheses", - "value": "(a(b(c(d(e(f(" - }, - "string_with_only_one_opening_parenthese": { - "key": "string_with_only_one_opening_parenthese", - "value": "(" - }, - "string_with_only_multiple_opening_parentheses": { - "key": "string_with_only_multiple_opening_parentheses", - "value": "(((((((" - }, - "string_with_closing_parentheses": { - "key": "string_with_closing_parentheses", - "value": ")a)b)c)d)e)f)" - }, - "string_with_only_one_closing_parenthese": { - "key": "string_with_only_one_closing_parenthese", - "value": ")" - }, - "string_with_only_multiple_closing_parentheses": { - "key": "string_with_only_multiple_closing_parentheses", - "value": ")))))))" - } - }, - "allocations": [ - { - "key": "allocation-test-string_with_spaces", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_spaces", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_spaces", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_one_space", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_one_space", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_one_space", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_multiple_spaces", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_multiple_spaces", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_spaces", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_dots", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_dots", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_dots", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_one_dot", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_one_dot", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_one_dot", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_multiple_dots", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_multiple_dots", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_dots", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_comas", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_comas", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_comas", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_one_coma", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_one_coma", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_one_coma", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_multiple_comas", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_multiple_comas", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_comas", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_colons", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_colons", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_colons", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_one_colon", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_one_colon", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_one_colon", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_multiple_colons", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_multiple_colons", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_colons", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_semicolons", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_semicolons", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_semicolons", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_one_semicolon", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_one_semicolon", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_one_semicolon", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_multiple_semicolons", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_multiple_semicolons", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_semicolons", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_slashes", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_slashes", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_slashes", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_one_slash", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_one_slash", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_one_slash", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_multiple_slashes", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_multiple_slashes", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_slashes", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_dashes", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_dashes", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_dashes", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_one_dash", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_one_dash", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_one_dash", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_multiple_dashes", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_multiple_dashes", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_dashes", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_underscores", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_underscores", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_underscores", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_one_underscore", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_one_underscore", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_one_underscore", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_multiple_underscores", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_multiple_underscores", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_underscores", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_plus_signs", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_plus_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_plus_signs", - "shards": [] - } - ], - "doLog": true - }, - { - "key": "allocation-test-string_with_only_one_plus_sign", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_one_plus_sign", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_semicolon", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_one_plus_sign", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_only_multiple_plus_signs", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_multiple_plus_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_plus_signs", - "shards": [] - } - ], - "doLog": true - }, + "variationKey": "string_with_only_one_semicolon", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_semicolons", + "rules": [ { - "key": "allocation-test-string_with_equal_signs", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_equal_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_semicolons", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_equal_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_only_one_equal_sign", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_one_equal_sign", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_one_equal_sign", - "shards": [] - } - ], - "doLog": true - }, + "variationKey": "string_with_only_multiple_semicolons", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_slashes", + "rules": [ { - "key": "allocation-test-string_with_only_multiple_equal_signs", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_equal_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_slashes", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_equal_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_dollar_signs", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_dollar_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_dollar_signs", - "shards": [] - } - ], - "doLog": true - }, + "variationKey": "string_with_slashes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_slash", + "rules": [ { - "key": "allocation-test-string_with_only_one_dollar_sign", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_one_dollar_sign", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_slash", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_one_dollar_sign", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_only_multiple_dollar_signs", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_multiple_dollar_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_dollar_signs", - "shards": [] - } - ], - "doLog": true - }, + "variationKey": "string_with_only_one_slash", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_slashes", + "rules": [ { - "key": "allocation-test-string_with_at_signs", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_at_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_slashes", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_at_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_only_one_at_sign", - "rules": [ - { - "conditions": [ - { - "attribute": "string_with_only_one_at_sign", - "operator": "ONE_OF", - "value": [ - "true" - ] - } - ] - } - ], - "splits": [ - { - "variationKey": "string_with_only_one_at_sign", - "shards": [] - } - ], - "doLog": true - }, + "variationKey": "string_with_only_multiple_slashes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_dashes", + "rules": [ { - "key": "allocation-test-string_with_only_multiple_at_signs", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_at_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_dashes", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_at_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_dashes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_dash", + "rules": [ { - "key": "allocation-test-string_with_amp_signs", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_amp_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_dash", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_amp_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_dash", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_dashes", + "rules": [ { - "key": "allocation-test-string_with_only_one_amp_sign", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_one_amp_sign", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_dashes", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_one_amp_sign", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_only_multiple_amp_signs", - "rules": [ + "variationKey": "string_with_only_multiple_dashes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_underscores", + "rules": [ + { + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_amp_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_underscores", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_amp_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_underscores", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_underscore", + "rules": [ { - "key": "allocation-test-string_with_hash_signs", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_hash_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_underscore", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_hash_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_underscore", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_underscores", + "rules": [ { - "key": "allocation-test-string_with_only_one_hash_sign", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_one_hash_sign", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_underscores", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_one_hash_sign", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_underscores", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_plus_signs", + "rules": [ { - "key": "allocation-test-string_with_only_multiple_hash_signs", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_hash_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_plus_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_hash_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_percentage_signs", - "rules": [ + "variationKey": "string_with_plus_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_plus_sign", + "rules": [ + { + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_percentage_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_plus_sign", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_percentage_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_plus_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_plus_signs", + "rules": [ { - "key": "allocation-test-string_with_only_one_percentage_sign", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_one_percentage_sign", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_plus_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_one_percentage_sign", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_plus_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_equal_signs", + "rules": [ { - "key": "allocation-test-string_with_only_multiple_percentage_signs", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_percentage_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_equal_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_percentage_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_equal_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_equal_sign", + "rules": [ { - "key": "allocation-test-string_with_tilde_signs", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_tilde_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_equal_sign", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_tilde_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_only_one_tilde_sign", - "rules": [ + "variationKey": "string_with_only_one_equal_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_equal_signs", + "rules": [ + { + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_one_tilde_sign", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_equal_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_one_tilde_sign", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_equal_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_dollar_signs", + "rules": [ { - "key": "allocation-test-string_with_only_multiple_tilde_signs", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_tilde_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_dollar_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_only_multiple_tilde_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_dollar_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_dollar_sign", + "rules": [ { - "key": "allocation-test-string_with_asterix_signs", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_asterix_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_dollar_sign", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ - { - "variationKey": "string_with_asterix_signs", - "shards": [] - } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_dollar_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_dollar_signs", + "rules": [ { - "key": "allocation-test-string_with_only_one_asterix_sign", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_one_asterix_sign", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_dollar_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_dollar_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_at_signs", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_one_asterix_sign", - "shards": [] + "attribute": "string_with_at_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_only_multiple_asterix_signs", - "rules": [ + "variationKey": "string_with_at_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_at_sign", + "rules": [ + { + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_asterix_signs", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_at_sign", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_at_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_at_signs", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_multiple_asterix_signs", - "shards": [] + "attribute": "string_with_only_multiple_at_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_at_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_amp_signs", + "rules": [ { - "key": "allocation-test-string_with_single_quotes", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_single_quotes", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_amp_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_amp_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_amp_sign", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_single_quotes", - "shards": [] + "attribute": "string_with_only_one_amp_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_amp_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_amp_signs", + "rules": [ { - "key": "allocation-test-string_with_only_one_single_quote", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_one_single_quote", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_amp_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_amp_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_hash_signs", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_one_single_quote", - "shards": [] + "attribute": "string_with_hash_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_hash_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_hash_sign", + "rules": [ { - "key": "allocation-test-string_with_only_multiple_single_quotes", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_single_quotes", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_hash_sign", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_hash_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_hash_signs", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_multiple_single_quotes", - "shards": [] + "attribute": "string_with_only_multiple_hash_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_question_marks", - "rules": [ + "variationKey": "string_with_only_multiple_hash_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_percentage_signs", + "rules": [ + { + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_question_marks", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_percentage_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_percentage_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_percentage_sign", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_question_marks", - "shards": [] + "attribute": "string_with_only_one_percentage_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_percentage_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_percentage_signs", + "rules": [ { - "key": "allocation-test-string_with_only_one_question_mark", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_one_question_mark", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_percentage_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_percentage_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_tilde_signs", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_one_question_mark", - "shards": [] + "attribute": "string_with_tilde_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_tilde_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_tilde_sign", + "rules": [ { - "key": "allocation-test-string_with_only_multiple_question_marks", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_question_marks", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_tilde_sign", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_tilde_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_tilde_signs", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_multiple_question_marks", - "shards": [] + "attribute": "string_with_only_multiple_tilde_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_tilde_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_asterix_signs", + "rules": [ { - "key": "allocation-test-string_with_exclamation_marks", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_exclamation_marks", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_asterix_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_asterix_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_asterix_sign", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_exclamation_marks", - "shards": [] + "attribute": "string_with_only_one_asterix_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_only_one_exclamation_mark", - "rules": [ + "variationKey": "string_with_only_one_asterix_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_asterix_signs", + "rules": [ + { + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_one_exclamation_mark", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_asterix_signs", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_asterix_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_single_quotes", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_one_exclamation_mark", - "shards": [] + "attribute": "string_with_single_quotes", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_single_quotes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_single_quote", + "rules": [ { - "key": "allocation-test-string_with_only_multiple_exclamation_marks", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_exclamation_marks", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_single_quote", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_single_quote", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_single_quotes", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_multiple_exclamation_marks", - "shards": [] + "attribute": "string_with_only_multiple_single_quotes", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_single_quotes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_question_marks", + "rules": [ { - "key": "allocation-test-string_with_opening_parentheses", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_opening_parentheses", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_question_marks", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_question_marks", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_question_mark", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_opening_parentheses", - "shards": [] + "attribute": "string_with_only_one_question_mark", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_question_mark", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_question_marks", + "rules": [ { - "key": "allocation-test-string_with_only_one_opening_parenthese", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_one_opening_parenthese", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_question_marks", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_question_marks", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_exclamation_marks", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_one_opening_parenthese", - "shards": [] + "attribute": "string_with_exclamation_marks", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ { - "key": "allocation-test-string_with_only_multiple_opening_parentheses", - "rules": [ + "variationKey": "string_with_exclamation_marks", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_exclamation_mark", + "rules": [ + { + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_opening_parentheses", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_exclamation_mark", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_exclamation_mark", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_exclamation_marks", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_multiple_opening_parentheses", - "shards": [] + "attribute": "string_with_only_multiple_exclamation_marks", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_exclamation_marks", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_opening_parentheses", + "rules": [ { - "key": "allocation-test-string_with_closing_parentheses", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_closing_parentheses", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_opening_parentheses", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_opening_parentheses", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_opening_parenthese", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_closing_parentheses", - "shards": [] + "attribute": "string_with_only_one_opening_parenthese", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_opening_parenthese", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_opening_parentheses", + "rules": [ { - "key": "allocation-test-string_with_only_one_closing_parenthese", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_one_closing_parenthese", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_multiple_opening_parentheses", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_opening_parentheses", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_closing_parentheses", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_one_closing_parenthese", - "shards": [] + "attribute": "string_with_closing_parentheses", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true - }, + ] + } + ], + "splits": [ + { + "variationKey": "string_with_closing_parentheses", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_closing_parenthese", + "rules": [ { - "key": "allocation-test-string_with_only_multiple_closing_parentheses", - "rules": [ + "conditions": [ { - "conditions": [ - { - "attribute": "string_with_only_multiple_closing_parentheses", - "operator": "ONE_OF", - "value": [ - "true" - ] - } + "attribute": "string_with_only_one_closing_parenthese", + "operator": "ONE_OF", + "value": [ + "true" ] } - ], - "splits": [ + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_closing_parenthese", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_closing_parentheses", + "rules": [ + { + "conditions": [ { - "variationKey": "string_with_only_multiple_closing_parentheses", - "shards": [] + "attribute": "string_with_only_multiple_closing_parentheses", + "operator": "ONE_OF", + "value": [ + "true" + ] } - ], - "doLog": true + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_closing_parentheses", + "shards": [] } - ] + ], + "doLog": true } - } + ] } } } From dad6a08ccb6e8cdba5e6bf64bd291aa55139ab80 Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Sat, 1 Nov 2025 00:34:44 +0200 Subject: [PATCH 27/43] [ffe] fix clippy --- datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs b/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs index efe7493244..108a47a3b2 100644 --- a/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs +++ b/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs @@ -96,7 +96,7 @@ impl From for CompiledFlagsConfig { .collect(); CompiledFlagsConfig { - created_at: config.created_at.into(), + created_at: config.created_at, environment: config.environment, flags, } From b2481f0441a091a51a2d8751877d815cc1860d5f Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Mon, 3 Nov 2025 14:17:36 -0800 Subject: [PATCH 28/43] Update datadog-ffe-ffi/src/evaluation_context.rs Co-authored-by: Oleksii Shmalko --- datadog-ffe-ffi/src/evaluation_context.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datadog-ffe-ffi/src/evaluation_context.rs b/datadog-ffe-ffi/src/evaluation_context.rs index fe1f784717..4b525f4b67 100644 --- a/datadog-ffe-ffi/src/evaluation_context.rs +++ b/datadog-ffe-ffi/src/evaluation_context.rs @@ -60,8 +60,8 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( }; attr_map.insert( - Str::from(name_str.to_string()), - Attribute::from(value_str.to_string()), + Str::from(name_str), + Attribute::from(value_str), ); } From f49b07ff15d436aa91bca90b06bb60ae9d22d2d0 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Mon, 3 Nov 2025 15:29:10 -0800 Subject: [PATCH 29/43] Lint fix --- datadog-ffe-ffi/src/evaluation_context.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/datadog-ffe-ffi/src/evaluation_context.rs b/datadog-ffe-ffi/src/evaluation_context.rs index 4b525f4b67..da87f1f1b2 100644 --- a/datadog-ffe-ffi/src/evaluation_context.rs +++ b/datadog-ffe-ffi/src/evaluation_context.rs @@ -59,10 +59,7 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( Err(_) => continue, // Skip invalid UTF-8 }; - attr_map.insert( - Str::from(name_str), - Attribute::from(value_str), - ); + attr_map.insert(Str::from(name_str), Attribute::from(value_str)); } let attributes_arc = Arc::new(attr_map); From 3ee6f4b6841247cdf0741e121d6a400b857ea562 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Mon, 3 Nov 2025 16:26:38 -0800 Subject: [PATCH 30/43] Replace Assignment with ResolutionDetails structure --- datadog-ffe-ffi/src/assignment.rs | 84 +++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index c7386d7c0e..59c0a0c667 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -1,14 +1,70 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use std::ffi::{c_char, CStr}; +use std::ffi::{c_char, CStr, CString}; use anyhow::ensure; use function_name::named; -use datadog_ffe::rules_based::{get_assignment, now, Assignment, Configuration, EvaluationContext}; +use datadog_ffe::rules_based::{get_assignment, now, Assignment, AssignmentValue, AssignmentReason, Configuration, EvaluationContext}; use ddcommon_ffi::{wrap_with_ffi_result, Handle, Result, ToInner}; +#[repr(C)] +pub struct ResolutionDetails { + pub value: Option, + pub error_code: Option, + pub error_message: *const c_char, // C-compatible string + pub reason: Option, + pub variant: *const c_char, + pub allocation_key: *const c_char, + pub do_log: bool, +} + +#[repr(C)] +pub enum ErrorCode { + TypeMismatch, + ParseError, + FlagNotFound, + TargetingKeyMissing, + InvalidContext, + ProviderNotReady, + General, +} + +#[repr(C)] +pub enum Reason { + Static, + Default, + TargetingMatch, + Split, + Disabled, + Error, +} + +impl ResolutionDetails { + fn empty(reason: Reason) -> Self { + Self { + value: None, + error_code: None, + error_message: std::ptr::null(), + reason: Some(reason), + variant: std::ptr::null(), + allocation_key: std::ptr::null(), + do_log: false, + } + } +} + +impl From for Reason { + fn from(reason: AssignmentReason) -> Self { + match reason { + AssignmentReason::Static => Reason::Static, + AssignmentReason::TargetingMatch => Reason::TargetingMatch, + AssignmentReason::Split => Reason::Split, + } + } +} + /// Evaluates a feature flag. /// /// # Safety @@ -21,7 +77,7 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( mut config: *mut Handle, flag_key: *const c_char, mut context: *mut Handle, -) -> Result> { +) -> Result> { wrap_with_ffi_result!({ ensure!(!flag_key.is_null(), "flag_key must not be NULL"); @@ -29,15 +85,23 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( let context = context.to_inner_mut()?; let flag_key = CStr::from_ptr(flag_key).to_str()?; - let assignment_result = get_assignment(Some(config), flag_key, context, None, now())?; + let assignment_result = get_assignment(Some(config), flag_key, context, None, now()); - let handle = if let Some(assignment) = assignment_result { - Handle::from(assignment) - } else { - Handle::empty() + let resolution_details = match assignment_result { + Ok(Some(assignment)) => ResolutionDetails { + value: Some(assignment.value), + error_code: None, + error_message: std::ptr::null(), + reason: Some(assignment.reason.into()), + variant: CString::new(assignment.variation_key.as_str()).unwrap().into_raw(), + allocation_key: CString::new(assignment.allocation_key.as_str()).unwrap().into_raw(), + do_log: assignment.do_log, + }, + Ok(None) => ResolutionDetails::empty(Reason::Default), + Err(_evaluation_error) => ResolutionDetails::empty(Reason::Error), }; - - Ok(handle) + + Ok(Handle::from(resolution_details)) }) } From 61582eb401b23d61cb983e878e2203fe2c3baf6e Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Mon, 3 Nov 2025 16:37:19 -0800 Subject: [PATCH 31/43] Replace AssignmentValue with C-compatible value representation --- datadog-ffe-ffi/src/assignment.rs | 48 ++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 59c0a0c667..03e068645b 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -11,7 +11,8 @@ use ddcommon_ffi::{wrap_with_ffi_result, Handle, Result, ToInner}; #[repr(C)] pub struct ResolutionDetails { - pub value: Option, + pub value_type: *const c_char, // "STRING", "INTEGER", "FLOAT", "BOOLEAN", "JSON", or NULL + pub value_string: *const c_char, // String representation of the value, or NULL pub error_code: Option, pub error_message: *const c_char, // C-compatible string pub reason: Option, @@ -44,7 +45,8 @@ pub enum Reason { impl ResolutionDetails { fn empty(reason: Reason) -> Self { Self { - value: None, + value_type: std::ptr::null(), + value_string: std::ptr::null(), error_code: None, error_message: std::ptr::null(), reason: Some(reason), @@ -55,6 +57,23 @@ impl ResolutionDetails { } } +fn convert_assignment_value(value: &AssignmentValue) -> (*const c_char, *const c_char) { + use std::ffi::CString; + use datadog_ffe::rules_based::AssignmentValue; + + let (type_name, value_string) = match value { + AssignmentValue::String(s) => ("STRING", s.as_str().to_owned()), + AssignmentValue::Integer(i) => ("INTEGER", i.to_string()), + AssignmentValue::Float(f) => ("FLOAT", f.to_string()), + AssignmentValue::Boolean(b) => ("BOOLEAN", b.to_string()), + AssignmentValue::Json(j) => ("JSON", j.to_string()), + }; + + let type_str = CString::new(type_name).unwrap().into_raw(); + let value_str = CString::new(value_string).unwrap().into_raw(); + (type_str, value_str) +} + impl From for Reason { fn from(reason: AssignmentReason) -> Self { match reason { @@ -88,16 +107,23 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( let assignment_result = get_assignment(Some(config), flag_key, context, None, now()); let resolution_details = match assignment_result { - Ok(Some(assignment)) => ResolutionDetails { - value: Some(assignment.value), - error_code: None, - error_message: std::ptr::null(), - reason: Some(assignment.reason.into()), - variant: CString::new(assignment.variation_key.as_str()).unwrap().into_raw(), - allocation_key: CString::new(assignment.allocation_key.as_str()).unwrap().into_raw(), - do_log: assignment.do_log, + Ok(Some(assignment)) => { + let (value_type, value_string) = convert_assignment_value(&assignment.value); + ResolutionDetails { + value_type, + value_string, + error_code: None, + error_message: std::ptr::null(), + reason: Some(assignment.reason.into()), + variant: CString::new(assignment.variation_key.as_str()).unwrap().into_raw(), + allocation_key: CString::new(assignment.allocation_key.as_str()).unwrap().into_raw(), + do_log: assignment.do_log, + } + }, + Ok(None) => { + // Return empty handle to signal no assignment found + return Ok(Handle::empty()); }, - Ok(None) => ResolutionDetails::empty(Reason::Default), Err(_evaluation_error) => ResolutionDetails::empty(Reason::Error), }; From 8314d124efe33c8d652b61de71a66dff454044c4 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Mon, 3 Nov 2025 18:01:02 -0800 Subject: [PATCH 32/43] Add error mapping for ResolutionDetails error handling --- datadog-ffe-ffi/src/assignment.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 03e068645b..88d1dda0b5 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -6,7 +6,7 @@ use std::ffi::{c_char, CStr, CString}; use anyhow::ensure; use function_name::named; -use datadog_ffe::rules_based::{get_assignment, now, Assignment, AssignmentValue, AssignmentReason, Configuration, EvaluationContext}; +use datadog_ffe::rules_based::{get_assignment, now, Assignment, AssignmentValue, AssignmentReason, Configuration, EvaluationContext, EvaluationError}; use ddcommon_ffi::{wrap_with_ffi_result, Handle, Result, ToInner}; #[repr(C)] @@ -57,6 +57,17 @@ impl ResolutionDetails { } } +fn convert_evaluation_error(error: &EvaluationError) -> ErrorCode { + use datadog_ffe::rules_based::EvaluationError; + + match error { + EvaluationError::TypeMismatch { .. } => ErrorCode::TypeMismatch, + EvaluationError::UnexpectedConfigurationError => ErrorCode::General, + // Handle any future variants that might be added + _ => ErrorCode::General, + } +} + fn convert_assignment_value(value: &AssignmentValue) -> (*const c_char, *const c_char) { use std::ffi::CString; use datadog_ffe::rules_based::AssignmentValue; @@ -124,7 +135,16 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( // Return empty handle to signal no assignment found return Ok(Handle::empty()); }, - Err(_evaluation_error) => ResolutionDetails::empty(Reason::Error), + Err(evaluation_error) => ResolutionDetails { + value_type: std::ptr::null(), + value_string: std::ptr::null(), + error_code: Some(convert_evaluation_error(&evaluation_error)), + error_message: CString::new(evaluation_error.to_string()).unwrap().into_raw(), + reason: Some(Reason::Error), + variant: std::ptr::null(), + allocation_key: std::ptr::null(), + do_log: false, + }, }; Ok(Handle::from(resolution_details)) From b3321aaf9877b6c8d0d5b251cf759f398375a1aa Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Mon, 3 Nov 2025 18:37:38 -0800 Subject: [PATCH 33/43] Lint fixes --- datadog-ffe-ffi/src/assignment.rs | 35 +++++++++++++++++++------------ 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 88d1dda0b5..2a4cb8fcda 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -6,13 +6,16 @@ use std::ffi::{c_char, CStr, CString}; use anyhow::ensure; use function_name::named; -use datadog_ffe::rules_based::{get_assignment, now, Assignment, AssignmentValue, AssignmentReason, Configuration, EvaluationContext, EvaluationError}; +use datadog_ffe::rules_based::{ + get_assignment, now, Assignment, AssignmentReason, AssignmentValue, Configuration, + EvaluationContext, EvaluationError, +}; use ddcommon_ffi::{wrap_with_ffi_result, Handle, Result, ToInner}; #[repr(C)] pub struct ResolutionDetails { - pub value_type: *const c_char, // "STRING", "INTEGER", "FLOAT", "BOOLEAN", "JSON", or NULL - pub value_string: *const c_char, // String representation of the value, or NULL + pub value_type: *const c_char, // "STRING", "INTEGER", "FLOAT", "BOOLEAN", "JSON", or NULL + pub value_string: *const c_char, // String representation of the value, or NULL pub error_code: Option, pub error_message: *const c_char, // C-compatible string pub reason: Option, @@ -59,7 +62,7 @@ impl ResolutionDetails { fn convert_evaluation_error(error: &EvaluationError) -> ErrorCode { use datadog_ffe::rules_based::EvaluationError; - + match error { EvaluationError::TypeMismatch { .. } => ErrorCode::TypeMismatch, EvaluationError::UnexpectedConfigurationError => ErrorCode::General, @@ -69,9 +72,9 @@ fn convert_evaluation_error(error: &EvaluationError) -> ErrorCode { } fn convert_assignment_value(value: &AssignmentValue) -> (*const c_char, *const c_char) { - use std::ffi::CString; use datadog_ffe::rules_based::AssignmentValue; - + use std::ffi::CString; + let (type_name, value_string) = match value { AssignmentValue::String(s) => ("STRING", s.as_str().to_owned()), AssignmentValue::Integer(i) => ("INTEGER", i.to_string()), @@ -79,7 +82,7 @@ fn convert_assignment_value(value: &AssignmentValue) -> (*const c_char, *const c AssignmentValue::Boolean(b) => ("BOOLEAN", b.to_string()), AssignmentValue::Json(j) => ("JSON", j.to_string()), }; - + let type_str = CString::new(type_name).unwrap().into_raw(); let value_str = CString::new(value_string).unwrap().into_raw(); (type_str, value_str) @@ -126,27 +129,33 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( error_code: None, error_message: std::ptr::null(), reason: Some(assignment.reason.into()), - variant: CString::new(assignment.variation_key.as_str()).unwrap().into_raw(), - allocation_key: CString::new(assignment.allocation_key.as_str()).unwrap().into_raw(), + variant: CString::new(assignment.variation_key.as_str()) + .unwrap() + .into_raw(), + allocation_key: CString::new(assignment.allocation_key.as_str()) + .unwrap() + .into_raw(), do_log: assignment.do_log, } - }, + } Ok(None) => { // Return empty handle to signal no assignment found return Ok(Handle::empty()); - }, + } Err(evaluation_error) => ResolutionDetails { value_type: std::ptr::null(), value_string: std::ptr::null(), error_code: Some(convert_evaluation_error(&evaluation_error)), - error_message: CString::new(evaluation_error.to_string()).unwrap().into_raw(), + error_message: CString::new(evaluation_error.to_string()) + .unwrap() + .into_raw(), reason: Some(Reason::Error), variant: std::ptr::null(), allocation_key: std::ptr::null(), do_log: false, }, }; - + Ok(Handle::from(resolution_details)) }) } From 95a04fe21694470e1ea63a24cb256a65dcc3b052 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Mon, 3 Nov 2025 18:46:27 -0800 Subject: [PATCH 34/43] Clippy fixes --- datadog-ffe-ffi/src/assignment.rs | 45 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 2a4cb8fcda..08468fcbfd 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -1,7 +1,7 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use std::ffi::{c_char, CStr, CString}; +use std::ffi::{c_char, CStr}; use anyhow::ensure; use function_name::named; @@ -45,17 +45,21 @@ pub enum Reason { Error, } -impl ResolutionDetails { - fn empty(reason: Reason) -> Self { - Self { - value_type: std::ptr::null(), - value_string: std::ptr::null(), - error_code: None, - error_message: std::ptr::null(), - reason: Some(reason), - variant: std::ptr::null(), - allocation_key: std::ptr::null(), - do_log: false, + +/// Helper function to safely create a CString, replacing null bytes with a placeholder +fn safe_cstring(input: &str) -> *const c_char { + use std::ffi::CString; + + // Replace null bytes with a placeholder to avoid CString::new() panics + let sanitized = input.replace('\0', "\\0"); + + match CString::new(sanitized) { + Ok(cstring) => cstring.into_raw(), + Err(_) => { + // Fallback to a static error message if somehow still fails + // This should never happen since we sanitized the input, but safety first + static FALLBACK: &[u8] = b"invalid_string\0"; + FALLBACK.as_ptr() as *const c_char } } } @@ -73,7 +77,6 @@ fn convert_evaluation_error(error: &EvaluationError) -> ErrorCode { fn convert_assignment_value(value: &AssignmentValue) -> (*const c_char, *const c_char) { use datadog_ffe::rules_based::AssignmentValue; - use std::ffi::CString; let (type_name, value_string) = match value { AssignmentValue::String(s) => ("STRING", s.as_str().to_owned()), @@ -83,8 +86,8 @@ fn convert_assignment_value(value: &AssignmentValue) -> (*const c_char, *const c AssignmentValue::Json(j) => ("JSON", j.to_string()), }; - let type_str = CString::new(type_name).unwrap().into_raw(); - let value_str = CString::new(value_string).unwrap().into_raw(); + let type_str = safe_cstring(type_name); + let value_str = safe_cstring(&value_string); (type_str, value_str) } @@ -129,12 +132,8 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( error_code: None, error_message: std::ptr::null(), reason: Some(assignment.reason.into()), - variant: CString::new(assignment.variation_key.as_str()) - .unwrap() - .into_raw(), - allocation_key: CString::new(assignment.allocation_key.as_str()) - .unwrap() - .into_raw(), + variant: safe_cstring(assignment.variation_key.as_str()), + allocation_key: safe_cstring(assignment.allocation_key.as_str()), do_log: assignment.do_log, } } @@ -146,9 +145,7 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( value_type: std::ptr::null(), value_string: std::ptr::null(), error_code: Some(convert_evaluation_error(&evaluation_error)), - error_message: CString::new(evaluation_error.to_string()) - .unwrap() - .into_raw(), + error_message: safe_cstring(&evaluation_error.to_string()), reason: Some(Reason::Error), variant: std::ptr::null(), allocation_key: std::ptr::null(), From 6b94369f924b60ddad831a14cb3f8dd2065c08e4 Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Tue, 4 Nov 2025 06:03:57 +0200 Subject: [PATCH 35/43] refactor(ffe): merge EvaluationError and EvalutionFailure into one type The distinction served us well in Eppo but it's less useful as we're migrating to OpenFeature and it handles these various errors rather similarly. --- datadog-ffe/src/rules_based/error.rs | 36 ++++++----------- .../src/rules_based/eval/eval_assignment.rs | 40 ++++++------------- datadog-ffe/src/rules_based/mod.rs | 2 +- .../rules_based/ufc/compiled_flag_config.rs | 18 ++++----- datadog-ffe/src/rules_based/ufc/models.rs | 10 ++--- 5 files changed, 38 insertions(+), 68 deletions(-) diff --git a/datadog-ffe/src/rules_based/error.rs b/datadog-ffe/src/rules_based/error.rs index 0d228e52f5..bb95b5484f 100644 --- a/datadog-ffe/src/rules_based/error.rs +++ b/datadog-ffe/src/rules_based/error.rs @@ -5,12 +5,16 @@ use serde::{Deserialize, Serialize}; use crate::rules_based::ufc::VariationType; -/// Enum representing possible errors that can occur during evaluation. +/// Enum representing all possible reasons that could result in evaluation returning an error or +/// default assignment. +/// +/// Not all of these are technically "errors"—some can be expected to occur frequently (e.g., +/// `FlagDisabled` or `DefaultAllocation`). #[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -#[non_exhaustive] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[non_exhaustive] pub enum EvaluationError { - /// Requested flag has invalid type. + /// Requested flag has unexpected type. #[error("invalid flag type (expected: {expected:?}, found: {found:?})")] TypeMismatch { /// Expected type of the flag. @@ -19,23 +23,13 @@ pub enum EvaluationError { found: VariationType, }, - /// Configuration received from the server is invalid for the SDK. This should normally never - /// happen and is likely a signal that you should update SDK. - #[error("unexpected configuration received from the server")] - UnexpectedConfigurationError, -} - -/// Enum representing all possible reasons that could result in evaluation returning an error or -/// default assignment. -#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum EvaluationFailure { - /// True evaluation error that should be returned to the user. - #[error(transparent)] - Error(EvaluationError), + /// Failed to parse configuration. This should normally never happen and is likely a signal + /// that you should update SDK. + #[error("failed to parse configuration")] + ConfigurationParseError, /// Configuration has not been fetched yet. - #[error("configuration has not been fetched yet")] + #[error("flags configuration is missing")] ConfigurationMissing, /// The requested flag configuration was not found. It either does not exist or is disabled. @@ -51,9 +45,3 @@ pub enum EvaluationFailure { #[error("default allocation is matched and is serving NULL")] DefaultAllocationNull, } - -impl From for EvaluationFailure { - fn from(value: EvaluationError) -> EvaluationFailure { - EvaluationFailure::Error(value) - } -} diff --git a/datadog-ffe/src/rules_based/eval/eval_assignment.rs b/datadog-ffe/src/rules_based/eval/eval_assignment.rs index 51938725e5..5f157f5efd 100644 --- a/datadog-ffe/src/rules_based/eval/eval_assignment.rs +++ b/datadog-ffe/src/rules_based/eval/eval_assignment.rs @@ -4,7 +4,7 @@ use chrono::{DateTime, Utc}; use crate::rules_based::{ - error::{EvaluationError, EvaluationFailure}, + error::EvaluationError, ufc::{ Allocation, Assignment, AssignmentReason, CompiledFlagsConfig, Flag, Shard, Split, VariationType, @@ -20,13 +20,13 @@ pub fn get_assignment( subject: &EvaluationContext, expected_type: Option, now: DateTime, -) -> Result { +) -> Result { let Some(config) = configuration else { log::trace!( flag_key, targeting_key = subject.targeting_key(); - "returning default assignment because of: {}", EvaluationFailure::ConfigurationMissing); - return Err(EvaluationFailure::ConfigurationMissing); + "returning default assignment because of: {}", EvaluationError::ConfigurationMissing); + return Err(EvaluationError::ConfigurationMissing); }; config.eval_flag(flag_key, subject, expected_type, now) @@ -39,7 +39,7 @@ impl Configuration { context: &EvaluationContext, expected_type: Option, now: DateTime, - ) -> Result { + ) -> Result { let result = self .flags .compiled @@ -54,20 +54,6 @@ impl Configuration { "evaluated a flag"); } - Err(EvaluationFailure::ConfigurationMissing) => { - log::warn!( - flag_key, - targeting_key = context.targeting_key(); - "evaluating a flag before flags configuration has been fetched"); - } - - Err(EvaluationFailure::Error(err)) => { - log::warn!( - flag_key, - targeting_key = context.targeting_key(); - "error occurred while evaluating a flag: {err}", - ); - } Err(err) => { log::trace!( flag_key, @@ -88,7 +74,7 @@ impl CompiledFlagsConfig { subject: &EvaluationContext, expected_type: Option, now: DateTime, - ) -> Result { + ) -> Result { let flag = self.get_flag(flag_key)?; if let Some(ty) = expected_type { @@ -98,24 +84,24 @@ impl CompiledFlagsConfig { flag.eval(subject, now) } - fn get_flag(&self, flag_key: &str) -> Result<&Flag, EvaluationFailure> { + fn get_flag(&self, flag_key: &str) -> Result<&Flag, EvaluationError> { self.flags .get(flag_key) - .ok_or(EvaluationFailure::FlagUnrecognizedOrDisabled)? + .ok_or(EvaluationError::FlagUnrecognizedOrDisabled)? .as_ref() .map_err(Clone::clone) } } impl Flag { - fn verify_type(&self, ty: VariationType) -> Result<(), EvaluationFailure> { + fn verify_type(&self, ty: VariationType) -> Result<(), EvaluationError> { if self.variation_type == ty { Ok(()) } else { - Err(EvaluationFailure::Error(EvaluationError::TypeMismatch { + Err(EvaluationError::TypeMismatch { expected: ty, found: self.variation_type, - })) + }) } } @@ -123,14 +109,14 @@ impl Flag { &self, subject: &EvaluationContext, now: DateTime, - ) -> Result { + ) -> Result { let Some((allocation, (split, reason))) = self.allocations.iter().find_map(|allocation| { let result = allocation.get_matching_split(subject, now); result .ok() .map(|(split, reason)| (allocation, (split, reason))) }) else { - return Err(EvaluationFailure::DefaultAllocationNull); + return Err(EvaluationError::DefaultAllocationNull); }; let value = split.value.clone(); diff --git a/datadog-ffe/src/rules_based/mod.rs b/datadog-ffe/src/rules_based/mod.rs index 4bde4875d2..f3197c36d2 100644 --- a/datadog-ffe/src/rules_based/mod.rs +++ b/datadog-ffe/src/rules_based/mod.rs @@ -12,7 +12,7 @@ mod ufc; pub use attributes::Attribute; pub use configuration::Configuration; -pub use error::{EvaluationError, EvaluationFailure}; +pub use error::EvaluationError; pub use eval::{get_assignment, EvaluationContext}; pub use str::Str; pub use timestamp::{now, Timestamp}; diff --git a/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs b/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs index 108a47a3b2..4d71abf8c0 100644 --- a/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs +++ b/datadog-ffe/src/rules_based/ufc/compiled_flag_config.rs @@ -5,9 +5,7 @@ use std::{collections::HashMap, sync::Arc}; use serde::Deserialize; -use crate::rules_based::{ - error::EvaluationFailure, sharder::PreSaltedSharder, EvaluationError, Str, Timestamp, -}; +use crate::rules_based::{error::EvaluationError, sharder::PreSaltedSharder, Str, Timestamp}; use super::{ AllocationWire, AssignmentValue, Environment, FlagWire, RuleWire, ShardRange, ShardWire, @@ -31,7 +29,7 @@ pub(crate) struct CompiledFlagsConfig { /// Flags configuration. /// /// For flags that failed to parse or are disabled, we store the evaluation failure directly. - pub flags: HashMap>, + pub flags: HashMap>, } #[derive(Debug)] @@ -87,9 +85,7 @@ impl From for CompiledFlagsConfig { ( key, Option::from(flag) - .ok_or(EvaluationFailure::Error( - EvaluationError::UnexpectedConfigurationError, - )) + .ok_or(EvaluationError::ConfigurationParseError) .and_then(compile_flag), ) }) @@ -103,9 +99,9 @@ impl From for CompiledFlagsConfig { } } -fn compile_flag(flag: FlagWire) -> Result { +fn compile_flag(flag: FlagWire) -> Result { if !flag.enabled { - return Err(EvaluationFailure::FlagDisabled); + return Err(EvaluationError::FlagDisabled); } let variation_values = flag @@ -113,7 +109,7 @@ fn compile_flag(flag: FlagWire) -> Result { .into_values() .map(|variation| { let assignment_value = AssignmentValue::from_wire(flag.variation_type, variation.value) - .ok_or(EvaluationError::UnexpectedConfigurationError)?; + .ok_or(EvaluationError::ConfigurationParseError)?; Ok((variation.key, assignment_value)) }) @@ -169,7 +165,7 @@ fn compile_split( let result = variation_values .get(&split.variation_key) .cloned() - .ok_or(EvaluationError::UnexpectedConfigurationError)?; + .ok_or(EvaluationError::ConfigurationParseError)?; Ok(Split { shards, diff --git a/datadog-ffe/src/rules_based/ufc/models.rs b/datadog-ffe/src/rules_based/ufc/models.rs index 62291ea117..2188abce3e 100644 --- a/datadog-ffe/src/rules_based/ufc/models.rs +++ b/datadog-ffe/src/rules_based/ufc/models.rs @@ -245,7 +245,7 @@ impl TryFrom for Condition { "failed to parse condition: {:?} condition with non-string condition value", condition.operator ); - return Err(EvaluationError::UnexpectedConfigurationError); + return Err(EvaluationError::ConfigurationParseError); } }; let regex = match Regex::new(®ex_string) { @@ -254,7 +254,7 @@ impl TryFrom for Condition { log::warn!( "failed to parse condition: failed to compile regex {regex_string:?}: {err:?}" ); - return Err(EvaluationError::UnexpectedConfigurationError); + return Err(EvaluationError::ConfigurationParseError); } }; @@ -282,7 +282,7 @@ impl TryFrom for Condition { "failed to parse condition: comparison value is not a number: {:?}", condition.value ); - return Err(EvaluationError::UnexpectedConfigurationError); + return Err(EvaluationError::ConfigurationParseError); }; ConditionCheck::Comparison { operator, @@ -298,7 +298,7 @@ impl TryFrom for Condition { "failed to parse condition: membership condition with non-array value: {:?}", condition.value ); - return Err(EvaluationError::UnexpectedConfigurationError); + return Err(EvaluationError::ConfigurationParseError); } }; ConditionCheck::Membership { @@ -312,7 +312,7 @@ impl TryFrom for Condition { log::warn!( "failed to parse condition: IS_NULL condition with non-boolean condition value" ); - return Err(EvaluationError::UnexpectedConfigurationError); + return Err(EvaluationError::ConfigurationParseError); }; ConditionCheck::Null { expected_null } } From b360449f64c94862b1173ff52f3cb96e2997ff8f Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Tue, 4 Nov 2025 10:47:04 +0200 Subject: [PATCH 36/43] feat: handle non-string attributes --- datadog-ffe-ffi/build.rs | 1 + datadog-ffe-ffi/cbindgen.toml | 3 +- datadog-ffe-ffi/src/assignment.rs | 169 +++++++--------------- datadog-ffe-ffi/src/configuration.rs | 25 +++- datadog-ffe-ffi/src/evaluation_context.rs | 97 +++++++------ datadog-ffe-ffi/src/handle.rs | 68 +++++++++ datadog-ffe-ffi/src/lib.rs | 3 + datadog-ffe/src/rules_based/error.rs | 7 +- 8 files changed, 199 insertions(+), 174 deletions(-) create mode 100644 datadog-ffe-ffi/src/handle.rs diff --git a/datadog-ffe-ffi/build.rs b/datadog-ffe-ffi/build.rs index b80ec6a1c3..4b6fe1d421 100644 --- a/datadog-ffe-ffi/build.rs +++ b/datadog-ffe-ffi/build.rs @@ -5,6 +5,7 @@ extern crate build_common; use build_common::generate_and_configure_header; fn main() { + println!("cargo:rerun-if-changed=src/*"); let header_name = "datadog_ffe.h"; generate_and_configure_header(header_name); } diff --git a/datadog-ffe-ffi/cbindgen.toml b/datadog-ffe-ffi/cbindgen.toml index 0bb3f5f2fc..47381a00c0 100644 --- a/datadog-ffe-ffi/cbindgen.toml +++ b/datadog-ffe-ffi/cbindgen.toml @@ -8,7 +8,8 @@ header = """// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 """ include_guard = "DDOG_FFE_H" -style = "both" +style = "tag" +usize_is_size_t = true pragma_once = true no_includes = true diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 08468fcbfd..8713e79c00 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -3,29 +3,29 @@ use std::ffi::{c_char, CStr}; -use anyhow::ensure; -use function_name::named; - use datadog_ffe::rules_based::{ - get_assignment, now, Assignment, AssignmentReason, AssignmentValue, Configuration, - EvaluationContext, EvaluationError, + now, Assignment, Configuration, EvaluationContext, EvaluationError, Str, }; -use ddcommon_ffi::{wrap_with_ffi_result, Handle, Result, ToInner}; + +use crate::Handle; + +/// Opaque type representing a result of evaluation. +#[allow(unused)] +pub struct ResolutionDetails(Result); #[repr(C)] -pub struct ResolutionDetails { - pub value_type: *const c_char, // "STRING", "INTEGER", "FLOAT", "BOOLEAN", "JSON", or NULL - pub value_string: *const c_char, // String representation of the value, or NULL - pub error_code: Option, - pub error_message: *const c_char, // C-compatible string - pub reason: Option, - pub variant: *const c_char, - pub allocation_key: *const c_char, - pub do_log: bool, +pub enum FlagType { + Unknown, + String, + Integer, + Float, + Boolean, + Object, } #[repr(C)] pub enum ErrorCode { + Ok, TypeMismatch, ParseError, FlagNotFound, @@ -45,123 +45,54 @@ pub enum Reason { Error, } - -/// Helper function to safely create a CString, replacing null bytes with a placeholder -fn safe_cstring(input: &str) -> *const c_char { - use std::ffi::CString; - - // Replace null bytes with a placeholder to avoid CString::new() panics - let sanitized = input.replace('\0', "\\0"); - - match CString::new(sanitized) { - Ok(cstring) => cstring.into_raw(), - Err(_) => { - // Fallback to a static error message if somehow still fails - // This should never happen since we sanitized the input, but safety first - static FALLBACK: &[u8] = b"invalid_string\0"; - FALLBACK.as_ptr() as *const c_char - } - } -} - -fn convert_evaluation_error(error: &EvaluationError) -> ErrorCode { - use datadog_ffe::rules_based::EvaluationError; - - match error { - EvaluationError::TypeMismatch { .. } => ErrorCode::TypeMismatch, - EvaluationError::UnexpectedConfigurationError => ErrorCode::General, - // Handle any future variants that might be added - _ => ErrorCode::General, - } -} - -fn convert_assignment_value(value: &AssignmentValue) -> (*const c_char, *const c_char) { - use datadog_ffe::rules_based::AssignmentValue; - - let (type_name, value_string) = match value { - AssignmentValue::String(s) => ("STRING", s.as_str().to_owned()), - AssignmentValue::Integer(i) => ("INTEGER", i.to_string()), - AssignmentValue::Float(f) => ("FLOAT", f.to_string()), - AssignmentValue::Boolean(b) => ("BOOLEAN", b.to_string()), - AssignmentValue::Json(j) => ("JSON", j.to_string()), - }; - - let type_str = safe_cstring(type_name); - let value_str = safe_cstring(&value_string); - (type_str, value_str) -} - -impl From for Reason { - fn from(reason: AssignmentReason) -> Self { - match reason { - AssignmentReason::Static => Reason::Static, - AssignmentReason::TargetingMatch => Reason::TargetingMatch, - AssignmentReason::Split => Reason::Split, - } - } -} - /// Evaluates a feature flag. /// +/// # Ownership +/// +/// The caller must call `ddog_ffe_assignment_drop` on the returned value to free resources. +/// /// # Safety -/// - `config` must be a valid Configuration handle pointer -/// - `context` must be a valid EvaluationContext handle pointer -/// - `flag_key` must be a valid null-terminated C string +/// - `config` must be a valid `Configuration` handle +/// - `flag_key` must be a valid C string +/// - `context` must be a valid `EvaluationContext` handle #[no_mangle] -#[named] pub unsafe extern "C" fn ddog_ffe_get_assignment( - mut config: *mut Handle, + config: Handle, flag_key: *const c_char, - mut context: *mut Handle, -) -> Result> { - wrap_with_ffi_result!({ - ensure!(!flag_key.is_null(), "flag_key must not be NULL"); + _expected_type: FlagType, + context: Handle, +) -> Handle { + if flag_key.is_null() { + return Handle::from(ResolutionDetails(Err(EvaluationError::Internal( + Str::from_static_str("ddog_ffe_get_assignment: flag_key must not be NULL"), + )))); + } - let config = config.to_inner_mut()?; - let context = context.to_inner_mut()?; - let flag_key = CStr::from_ptr(flag_key).to_str()?; + let config = unsafe { config.as_ref() }; + let context = unsafe { context.as_ref() }; - let assignment_result = get_assignment(Some(config), flag_key, context, None, now()); + let Ok(flag_key) = unsafe { + // SAFETY: we checked that flag_key is not NULL + CStr::from_ptr(flag_key) + } + .to_str() else { + return Handle::from(ResolutionDetails(Err(EvaluationError::Internal( + Str::from_static_str("ddog_ffe_get_assignment: flag_key is not a valid UTF-8 string"), + )))); + }; - let resolution_details = match assignment_result { - Ok(Some(assignment)) => { - let (value_type, value_string) = convert_assignment_value(&assignment.value); - ResolutionDetails { - value_type, - value_string, - error_code: None, - error_message: std::ptr::null(), - reason: Some(assignment.reason.into()), - variant: safe_cstring(assignment.variation_key.as_str()), - allocation_key: safe_cstring(assignment.allocation_key.as_str()), - do_log: assignment.do_log, - } - } - Ok(None) => { - // Return empty handle to signal no assignment found - return Ok(Handle::empty()); - } - Err(evaluation_error) => ResolutionDetails { - value_type: std::ptr::null(), - value_string: std::ptr::null(), - error_code: Some(convert_evaluation_error(&evaluation_error)), - error_message: safe_cstring(&evaluation_error.to_string()), - reason: Some(Reason::Error), - variant: std::ptr::null(), - allocation_key: std::ptr::null(), - do_log: false, - }, - }; + let assignment_result = config.eval_flag(flag_key, context, None, now()); - Ok(Handle::from(resolution_details)) - }) + Handle::from(ResolutionDetails(assignment_result)) } -/// Frees an Assignment handle +// TODO: accessors for various data inside ResolutionDetails. + +/// Frees an Assignment handle. /// /// # Safety -/// `assignment` must be a valid Assignment handle +/// - `assignment` must be a valid Assignment handle #[no_mangle] -pub unsafe extern "C" fn ddog_ffe_assignment_drop(mut assignment: *mut Handle) { - drop(assignment.take()); +pub unsafe extern "C" fn ddog_ffe_assignment_drop(assignment: *mut Handle) { + unsafe { Handle::free(assignment) } } diff --git a/datadog-ffe-ffi/src/configuration.rs b/datadog-ffe-ffi/src/configuration.rs index 9cc28ec345..a778183966 100644 --- a/datadog-ffe-ffi/src/configuration.rs +++ b/datadog-ffe-ffi/src/configuration.rs @@ -7,12 +7,20 @@ use anyhow::ensure; use function_name::named; use datadog_ffe::rules_based::{Configuration, UniversalFlagConfig}; -use ddcommon_ffi::{wrap_with_ffi_result, Handle, Result, ToInner}; +use ddcommon_ffi::{wrap_with_ffi_result, Result}; -/// Creates a new Configuration from JSON bytes +use crate::Handle; + +/// Creates a new Configuration from JSON bytes. +/// +/// # Ownership +/// +/// The caller must call `ddog_ffe_configuration_drop` to release resources allocated for +/// configuration. /// /// # Safety -/// `json_str` must be a valid C string. +/// +/// - `json_str` must be a valid C string. #[no_mangle] #[named] pub unsafe extern "C" fn ddog_ffe_configuration_new( @@ -21,7 +29,7 @@ pub unsafe extern "C" fn ddog_ffe_configuration_new( wrap_with_ffi_result!({ ensure!(!json_str.is_null(), "json_str must not be NULL"); - let json_bytes = CStr::from_ptr(json_str).to_bytes().to_vec(); + let json_bytes = unsafe { CStr::from_ptr(json_str) }.to_bytes().to_vec(); let configuration = Configuration::from_server_response(UniversalFlagConfig::from_json(json_bytes)?); @@ -30,11 +38,12 @@ pub unsafe extern "C" fn ddog_ffe_configuration_new( }) } -/// Frees a Configuration +/// Frees a Configuration. /// /// # Safety -/// `config` must be a valid Configuration handle created by `ddog_ffe_configuration_new` +/// +/// `config` must be a valid Configuration handle created by `ddog_ffe_configuration_new`. #[no_mangle] -pub unsafe extern "C" fn ddog_ffe_configuration_drop(mut config: *mut Handle) { - drop(config.take()); +pub unsafe extern "C" fn ddog_ffe_configuration_drop(config: *mut Handle) { + unsafe { Handle::free(config) }; } diff --git a/datadog-ffe-ffi/src/evaluation_context.rs b/datadog-ffe-ffi/src/evaluation_context.rs index da87f1f1b2..445b892acc 100644 --- a/datadog-ffe-ffi/src/evaluation_context.rs +++ b/datadog-ffe-ffi/src/evaluation_context.rs @@ -6,66 +6,77 @@ use std::ffi::{c_char, CStr}; use std::sync::Arc; use datadog_ffe::rules_based::{Attribute, EvaluationContext, Str}; -use ddcommon_ffi::{Handle, ToInner}; -/// Represents a key-value pair for attributes +use crate::Handle; + +/// Represents a key-value pair for attributes. +/// +/// # Safety +/// - `name` must be a valid C string. #[repr(C)] pub struct AttributePair { pub name: *const c_char, - pub value: *const c_char, + pub value: AttributeValue, +} + +/// # Safety +/// - `string` must be a valid C string. +#[repr(C)] +pub enum AttributeValue { + String(*const c_char), + Number(f64), + Boolean(bool), } -/// Creates a new EvaluationContext with the given targeting key and attributes +/// Creates a new EvaluationContext with the given targeting key and attributes. +/// +/// # Ownership +/// +/// `ddog_ffe_evaluation_context_drop` must be called on the result value to free resources. /// /// # Safety -/// - `targeting_key` must be a valid null-terminated C string -/// - `attributes` must point to a valid array of `AttributePair` structs (can be null if -/// attributes_count is 0) -/// - Each `AttributePair.name` and `AttributePair.value` must be valid null-terminated C strings -/// - `attributes_count` must accurately represent the length of the `attributes` array +/// - `targeting_key` must be a valid C string. +/// - `attributes` must point to a valid array of valid `AttributePair` structs (can be null if +/// `attributes_count` is 0) #[no_mangle] pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( targeting_key: *const c_char, attributes: *const AttributePair, attributes_count: usize, ) -> Handle { - if targeting_key.is_null() || (attributes_count > 0 && attributes.is_null()) { - return Handle::empty(); - } - - let key_str = match CStr::from_ptr(targeting_key).to_str() { - Ok(s) => s, - Err(_) => return Handle::empty(), - }; - - let key = Str::from(key_str.to_string()); - let mut attr_map = HashMap::::new(); - - // Process attributes array - for i in 0..attributes_count { - let attr_pair = &*attributes.add(i); - - if attr_pair.name.is_null() || attr_pair.value.is_null() { - continue; // Skip invalid pairs + let targeting_key = if targeting_key.is_null() { + Str::from_static_str("") + } else { + match unsafe { CStr::from_ptr(targeting_key).to_str() } { + Ok(s) => Str::from(s), + Err(_) => Str::from_static_str(""), } + }; - let name_str = match CStr::from_ptr(attr_pair.name).to_str() { - Ok(s) => s, - Err(_) => continue, // Skip invalid UTF-8 - }; + let attributes = if attributes.is_null() { + HashMap::new() + } else { + unsafe { std::slice::from_raw_parts(attributes, attributes_count) } + .into_iter() + .filter_map(|attr_pair| { + if attr_pair.name.is_null() { + return None; // Skip invalid pairs + } - let value_str = match CStr::from_ptr(attr_pair.value).to_str() { - Ok(s) => s, - Err(_) => continue, // Skip invalid UTF-8 - }; + let name_str = unsafe { CStr::from_ptr(attr_pair.name) }.to_str().ok()?; - attr_map.insert(Str::from(name_str), Attribute::from(value_str)); - } + let attribute: Attribute = match attr_pair.value { + AttributeValue::String(s) => unsafe { CStr::from_ptr(s) }.to_str().ok()?.into(), + AttributeValue::Number(v) => v.into(), + AttributeValue::Boolean(v) => v.into(), + }; - let attributes_arc = Arc::new(attr_map); - let context = EvaluationContext::new(key, attributes_arc); + Some((Str::from(name_str), attribute)) + }) + .collect() + }; - Handle::from(context) + Handle::from(EvaluationContext::new(targeting_key, Arc::new(attributes))) } /// Frees an EvaluationContext @@ -73,8 +84,6 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( /// # Safety /// `context` must be a valid EvaluationContext handle created by `ddog_ffe_evaluation_context_new` #[no_mangle] -pub unsafe extern "C" fn ddog_ffe_evaluation_context_drop( - mut context: *mut Handle, -) { - drop(context.take()); +pub unsafe extern "C" fn ddog_ffe_evaluation_context_drop(context: *mut Handle) { + unsafe { Handle::free(context) }; } diff --git a/datadog-ffe-ffi/src/handle.rs b/datadog-ffe-ffi/src/handle.rs new file mode 100644 index 0000000000..d2b5063a55 --- /dev/null +++ b/datadog-ffe-ffi/src/handle.rs @@ -0,0 +1,68 @@ +/// An opaque handle for a resource. The inner fields must not be dereferenced. +/// +/// This is similar to `ddcommon_ffi::Handle` but only allows shared access to internal resource, so +/// it's safe to share between thread or access concurrently (if the underlying type is). +/// +/// # Ownership +/// +/// `Handle::free()` must be called exactly once on any created Handle. Failure to do that will +/// result in a memory leak. +#[repr(transparent)] +pub struct Handle { + inner: *mut T, +} + +unsafe impl Send for Handle {} +unsafe impl Sync for Handle {} + +impl Handle { + /// Create a new handle to `T`. + /// + /// # Ownership + /// + /// This moves `value` to heap. + /// + /// `Handle::free()` must be called exactly once on any created Handle. Failure to do that will + /// result in a memory leak. + pub(crate) fn new(value: T) -> Handle { + Handle { + inner: Box::into_raw(Box::new(value)), + } + } + + /// Get a reference to inner value. + /// + /// # Safety + /// - `self` must be a valid handle for `T`. + pub(crate) unsafe fn as_ref(&self) -> &T { + unsafe { self.inner.as_ref().expect("detected use after free") } + } + + /// Free this handle. This and all other copies of the handle become invalid after freeing. + /// + /// # Safety + /// - `this` must be a valid pointer to valid handle for `T`. + pub(crate) unsafe fn free(this: *mut Self) { + if this.is_null() { + return; + } + + let ptr = std::mem::take(&mut (unsafe { &mut *this }).inner); + if ptr.is_null() { + // We try to detect double-free but it's not fool-proof. The C side might have copied + // the handle. + debug_assert!(false, "detected double-free"); + return; + } + + let value = unsafe { Box::from_raw(ptr) }; + + drop(value); + } +} + +impl From for Handle { + fn from(value: T) -> Self { + Handle::new(value) + } +} diff --git a/datadog-ffe-ffi/src/lib.rs b/datadog-ffe-ffi/src/lib.rs index 094a8ab080..9c422400f8 100644 --- a/datadog-ffe-ffi/src/lib.rs +++ b/datadog-ffe-ffi/src/lib.rs @@ -6,11 +6,14 @@ #![cfg_attr(not(test), deny(clippy::expect_used))] #![cfg_attr(not(test), deny(clippy::todo))] #![cfg_attr(not(test), deny(clippy::unimplemented))] +#![deny(unsafe_op_in_unsafe_fn)] mod assignment; mod configuration; mod evaluation_context; +mod handle; pub use assignment::*; pub use configuration::*; pub use evaluation_context::*; +pub use handle::*; diff --git a/datadog-ffe/src/rules_based/error.rs b/datadog-ffe/src/rules_based/error.rs index bb95b5484f..13cfd66fa6 100644 --- a/datadog-ffe/src/rules_based/error.rs +++ b/datadog-ffe/src/rules_based/error.rs @@ -3,14 +3,14 @@ use serde::{Deserialize, Serialize}; -use crate::rules_based::ufc::VariationType; +use crate::rules_based::{ufc::VariationType, Str}; /// Enum representing all possible reasons that could result in evaluation returning an error or /// default assignment. /// /// Not all of these are technically "errors"—some can be expected to occur frequently (e.g., /// `FlagDisabled` or `DefaultAllocation`). -#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(thiserror::Error, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[non_exhaustive] pub enum EvaluationError { @@ -44,4 +44,7 @@ pub enum EvaluationError { /// being assigned. #[error("default allocation is matched and is serving NULL")] DefaultAllocationNull, + + #[error("internal error: {0}")] + Internal(Str), } From 7afa96ba626adaa2dd7529b6830c319316f6d33d Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Tue, 4 Nov 2025 11:12:43 +0200 Subject: [PATCH 37/43] feat(ffe): adding accessors for ResolutionDetails --- datadog-ffe-ffi/src/assignment.rs | 85 +++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 8713e79c00..3c9e9deb85 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -1,10 +1,11 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use std::ffi::{c_char, CStr}; +use std::ffi::{c_char, c_uchar, CStr}; use datadog_ffe::rules_based::{ - now, Assignment, Configuration, EvaluationContext, EvaluationError, Str, + now, Assignment, AssignmentValue, Configuration, EvaluationContext, EvaluationError, Str, + VariationType, }; use crate::Handle; @@ -23,6 +24,18 @@ pub enum FlagType { Object, } +impl From for FlagType { + fn from(value: VariationType) -> Self { + match value { + VariationType::String => FlagType::String, + VariationType::Integer => FlagType::Integer, + VariationType::Numeric => FlagType::Float, + VariationType::Boolean => FlagType::Boolean, + VariationType::Json => FlagType::Object, + } + } +} + #[repr(C)] pub enum ErrorCode { Ok, @@ -86,7 +99,73 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( Handle::from(ResolutionDetails(assignment_result)) } -// TODO: accessors for various data inside ResolutionDetails. +#[repr(C)] +pub enum VariantValue { + /// Evaluation did not produce any value. + None, + String(*const c_uchar), + Integer(i64), + Float(f64), + Boolean(bool), + Object(*const c_char), +} + +/// Get value produced by evaluation. +/// +/// # Ownership +/// +/// The returned `VariantValue` borrows from `assignment`. It must not be used after `assignment` is +/// freed. +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_assignment_get_value( + assignment: Handle, +) -> VariantValue { + match unsafe { assignment.as_ref() } { + ResolutionDetails(Ok(assignment)) => match &assignment.value { + AssignmentValue::String(s) => VariantValue::String(s.as_ptr()), + AssignmentValue::Integer(v) => VariantValue::Integer(*v), + AssignmentValue::Float(v) => VariantValue::Float(*v), + AssignmentValue::Boolean(v) => VariantValue::Boolean(*v), + AssignmentValue::Json(_value) => todo!("make AssignmentValue hold onto raw json value"), + }, + _ => VariantValue::None, + } +} + +/// Get variant key produced by evaluation. Returns `NULL` if evaluation did not produce any value. +/// +/// # Ownership +/// +/// The returned string borrows from `assignment`. It must not be used after `assignment` is +/// freed. +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_assignment_get_variant( + assignment: Handle, +) -> *const c_uchar { + match unsafe { assignment.as_ref() } { + ResolutionDetails(Ok(assignment)) => assignment.variation_key.as_ptr(), + _ => std::ptr::null(), + } +} + +/// Get allocation key produced by evaluation. Returns `NULL` if evaluation did not produce any +/// value. +/// +/// # Ownership +/// +/// The returned string borrows from `assignment`. It must not be used after `assignment` is +/// freed. +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_assignment_get_allocation_key( + assignment: Handle, +) -> *const c_uchar { + match unsafe { assignment.as_ref() } { + ResolutionDetails(Ok(assignment)) => assignment.allocation_key.as_ptr(), + _ => std::ptr::null(), + } +} + +// TODO: add accessors for various data inside ResolutionDetails. /// Frees an Assignment handle. /// From bccc2838602ec47b837b26d61bd3050f3eb7f9d9 Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Wed, 5 Nov 2025 06:23:24 +0200 Subject: [PATCH 38/43] feat(ffe): preserve json raw value --- datadog-ffe-ffi/src/assignment.rs | 91 ++++++++++++++++--- datadog-ffe/Cargo.toml | 2 +- .../src/rules_based/eval/eval_assignment.rs | 2 +- datadog-ffe/src/rules_based/ufc/assignment.rs | 53 ++++++----- datadog-ffe/src/rules_based/ufc/models.rs | 2 +- 5 files changed, 114 insertions(+), 36 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 3c9e9deb85..2b18183d93 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -1,12 +1,16 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use std::ffi::{c_char, c_uchar, CStr}; +use std::{ + ffi::{c_char, c_uchar, CStr}, + marker::PhantomData, +}; use datadog_ffe::rules_based::{ now, Assignment, AssignmentValue, Configuration, EvaluationContext, EvaluationError, Str, VariationType, }; +use ddcommon_ffi::CharSlice; use crate::Handle; @@ -103,11 +107,54 @@ pub unsafe extern "C" fn ddog_ffe_get_assignment( pub enum VariantValue { /// Evaluation did not produce any value. None, - String(*const c_uchar), + String(BorrowedStr), Integer(i64), Float(f64), Boolean(bool), - Object(*const c_char), + Object(BorrowedStr), +} + +/// A string that has been borrowed. Beware that it is NOT nul-terminated! +/// +/// # Ownership +/// +/// This string is non-owning. You must not free `ptr`. +/// +/// # Safety +/// +/// - The string is not NUL-terminated, it can only be used with API that accept the len as an +/// additional parameter. +/// - The value must not be used after the value it borrowed from has been moved, modified, or +/// freed. +#[repr(C)] +pub struct BorrowedStr { + /// May be NULL if `len` is `0`. + pub ptr: *const u8, + pub len: usize, +} + +impl<'a> BorrowedStr { + /// Borrow string from `s`. + /// + /// # Safety + /// + /// - The returned value must non outlive `s`. + /// - `s` must not be modified while `BorrowedStr` is alive. + #[inline] + unsafe fn new(s: &str) -> BorrowedStr { + BorrowedStr { + ptr: s.as_ptr(), + len: s.len(), + } + } + + #[inline] + const fn empty() -> BorrowedStr { + BorrowedStr { + ptr: std::ptr::null(), + len: 0, + } + } } /// Get value produced by evaluation. @@ -117,16 +164,28 @@ pub enum VariantValue { /// The returned `VariantValue` borrows from `assignment`. It must not be used after `assignment` is /// freed. #[no_mangle] -pub unsafe extern "C" fn ddog_ffe_assignment_get_value( +pub unsafe extern "C" fn ddog_ffe_assignment_get_value<'a>( assignment: Handle, ) -> VariantValue { match unsafe { assignment.as_ref() } { ResolutionDetails(Ok(assignment)) => match &assignment.value { - AssignmentValue::String(s) => VariantValue::String(s.as_ptr()), + AssignmentValue::String(s) => { + VariantValue::String(unsafe { + // SAFETY: caller is required to not use return value after freeing + // `assignment`. + BorrowedStr::new(s.as_str()) + }) + } AssignmentValue::Integer(v) => VariantValue::Integer(*v), AssignmentValue::Float(v) => VariantValue::Float(*v), AssignmentValue::Boolean(v) => VariantValue::Boolean(*v), - AssignmentValue::Json(_value) => todo!("make AssignmentValue hold onto raw json value"), + AssignmentValue::Json { value: _, raw } => { + VariantValue::Object(unsafe { + // SAFETY: caller is required to not use return value after freeing + // `assignment`. + BorrowedStr::new(raw.get()) + }) + } }, _ => VariantValue::None, } @@ -141,10 +200,14 @@ pub unsafe extern "C" fn ddog_ffe_assignment_get_value( #[no_mangle] pub unsafe extern "C" fn ddog_ffe_assignment_get_variant( assignment: Handle, -) -> *const c_uchar { +) -> BorrowedStr { match unsafe { assignment.as_ref() } { - ResolutionDetails(Ok(assignment)) => assignment.variation_key.as_ptr(), - _ => std::ptr::null(), + ResolutionDetails(Ok(assignment)) => unsafe { + // SAFETY: caller is required to not use return value after freeing + // `assignment`. + BorrowedStr::new(&assignment.variation_key) + }, + _ => BorrowedStr::empty(), } } @@ -158,10 +221,14 @@ pub unsafe extern "C" fn ddog_ffe_assignment_get_variant( #[no_mangle] pub unsafe extern "C" fn ddog_ffe_assignment_get_allocation_key( assignment: Handle, -) -> *const c_uchar { +) -> BorrowedStr { match unsafe { assignment.as_ref() } { - ResolutionDetails(Ok(assignment)) => assignment.allocation_key.as_ptr(), - _ => std::ptr::null(), + ResolutionDetails(Ok(assignment)) => unsafe { + // SAFETY: caller is required to not use return value after freeing + // `assignment`. + BorrowedStr::new(assignment.allocation_key.as_str()) + }, + _ => BorrowedStr::empty(), } } diff --git a/datadog-ffe/Cargo.toml b/datadog-ffe/Cargo.toml index c0c32c61ff..4507190590 100644 --- a/datadog-ffe/Cargo.toml +++ b/datadog-ffe/Cargo.toml @@ -12,7 +12,7 @@ bench = false [dependencies] faststr = { version = "0.2.23", default-features = false, features = ["serde"] } serde = { version = "1.0", default-features = false, features = ["derive", "rc"] } -serde_json = { version = "1.0", default-features = false, features = ["std"] } +serde_json = { version = "1.0", default-features = false, features = ["std", "raw_value"] } chrono = { version = "0.4.38", default-features = false, features = ["now", "serde"] } derive_more = { version = "2.0.0", default-features = false, features = ["from", "into"] } log = { version = "0.4.21", default-features = false, features = ["kv", "kv_serde"] } diff --git a/datadog-ffe/src/rules_based/eval/eval_assignment.rs b/datadog-ffe/src/rules_based/eval/eval_assignment.rs index 5f157f5efd..1682fcdc77 100644 --- a/datadog-ffe/src/rules_based/eval/eval_assignment.rs +++ b/datadog-ffe/src/rules_based/eval/eval_assignment.rs @@ -50,7 +50,7 @@ impl Configuration { log::trace!( flag_key, targeting_key = context.targeting_key(), - assignment:serde = assignment.value; + assignment:? = assignment.value; "evaluated a flag"); } diff --git a/datadog-ffe/src/rules_based/ufc/assignment.rs b/datadog-ffe/src/rules_based/ufc/assignment.rs index d60ab534c7..c82abd7af6 100644 --- a/datadog-ffe/src/rules_based/ufc/assignment.rs +++ b/datadog-ffe/src/rules_based/ufc/assignment.rs @@ -23,8 +23,7 @@ pub enum AssignmentReason { } /// Result of assignment evaluation. -#[derive(Debug, Serialize, Clone)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone)] pub struct Assignment { /// Assignment value that should be returned to the user. pub value: AssignmentValue, @@ -53,8 +52,7 @@ pub struct Assignment { /// ```json /// {"type":"JSON","value":{"hello":"world"}} /// ``` -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type", content = "value")] +#[derive(Debug, Clone)] pub enum AssignmentValue { /// A string value. String(Str), @@ -65,24 +63,37 @@ pub enum AssignmentValue { /// A boolean value. Boolean(bool), /// Arbitrary JSON value. - Json(Arc), + Json { + value: Arc, + raw: Arc, + }, } impl AssignmentValue { pub(crate) fn from_wire( ty: VariationType, - value: serde_json::Value, + value: Arc, ) -> Option { - use serde_json::Value; - Some(match (ty, value) { - (VariationType::String, Value::String(s)) => AssignmentValue::String(s.into()), - (VariationType::Integer, Value::Number(n)) => AssignmentValue::Integer(n.as_i64()?), - (VariationType::Numeric, Value::Number(n)) => AssignmentValue::Float(n.as_f64()?), - (VariationType::Boolean, Value::Bool(v)) => AssignmentValue::Boolean(v), - (VariationType::Json, v) => AssignmentValue::Json(Arc::new(v)), - // Type mismatch - _ => return None, - }) + let result = match ty { + VariationType::String => { + AssignmentValue::String(serde_json::from_str(value.get()).ok()?) + } + VariationType::Integer => { + AssignmentValue::Integer(serde_json::from_str(value.get()).ok()?) + } + VariationType::Numeric => { + AssignmentValue::Float(serde_json::from_str(value.get()).ok()?) + } + VariationType::Boolean => { + AssignmentValue::Boolean(serde_json::from_str(value.get()).ok()?) + } + VariationType::Json => AssignmentValue::Json { + value: serde_json::from_str(value.get()).ok()?, + raw: value, + }, + }; + + Some(result) } /// Checks if the assignment value is of type String. @@ -244,7 +255,7 @@ impl AssignmentValue { /// - The JSON value if the assignment value is of type Json, otherwise `None`. pub fn as_json(&self) -> Option<&serde_json::Value> { match self { - Self::Json(value) => Some(value), + Self::Json { value, raw: _ } => Some(value), _ => None, } } @@ -254,7 +265,7 @@ impl AssignmentValue { /// - The JSON value if the assignment value is of type Json, otherwise `None`. pub fn to_json(self) -> Option> { match self { - Self::Json(value) => Some(value), + Self::Json { value, raw: _ } => Some(value), _ => None, } } @@ -278,7 +289,7 @@ impl AssignmentValue { AssignmentValue::Integer(_) => VariationType::Integer, AssignmentValue::Float(_) => VariationType::Numeric, AssignmentValue::Boolean(_) => VariationType::Boolean, - AssignmentValue::Json(_) => VariationType::Json, + AssignmentValue::Json { .. } => VariationType::Json, } } @@ -295,7 +306,7 @@ impl AssignmentValue { Value::Number(Number::from_f64(*n).expect("value should not be infinite/NaN")) } AssignmentValue::Boolean(b) => Value::Bool(*b), - AssignmentValue::Json(value) => value.as_ref().clone(), + AssignmentValue::Json { value, raw: _ } => value.as_ref().clone(), } } } @@ -323,7 +334,7 @@ mod pyo3_impl { AssignmentValue::Integer(v) => v.into_pyobject(py)?.into_any(), AssignmentValue::Float(v) => v.into_pyobject(py)?.into_any(), AssignmentValue::Boolean(v) => v.into_pyobject(py)?.to_owned().into_any(), - AssignmentValue::Json(v) => json_to_pyobject(v, py)?, + AssignmentValue::Json { value, raw: _ } => json_to_pyobject(value, py)?, }; Ok(obj) } diff --git a/datadog-ffe/src/rules_based/ufc/models.rs b/datadog-ffe/src/rules_based/ufc/models.rs index 2188abce3e..bcc693fe19 100644 --- a/datadog-ffe/src/rules_based/ufc/models.rs +++ b/datadog-ffe/src/rules_based/ufc/models.rs @@ -93,7 +93,7 @@ pub enum VariationType { #[allow(missing_docs)] pub(crate) struct VariationWire { pub key: Str, - pub value: serde_json::Value, + pub value: Arc, } #[derive(Debug, Serialize, Deserialize, Clone)] From 99e02e5c09f2bb8fd9d42f8a18767af9bc9c313f Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Wed, 5 Nov 2025 09:01:01 +0200 Subject: [PATCH 39/43] datadog-ffe-ffi: finish assignment getters --- datadog-ffe-ffi/src/assignment.rs | 328 ++++++++++++++++++---- datadog-ffe-ffi/src/configuration.rs | 14 +- datadog-ffe-ffi/src/evaluation_context.rs | 10 +- datadog-ffe-ffi/src/handle.rs | 10 +- datadog-ffe-ffi/src/lib.rs | 6 +- 5 files changed, 306 insertions(+), 62 deletions(-) diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 2b18183d93..9fa7afe56d 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -1,22 +1,85 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use std::{ - ffi::{c_char, c_uchar, CStr}, - marker::PhantomData, -}; +use std::ffi::{c_char, CStr}; use datadog_ffe::rules_based::{ - now, Assignment, AssignmentValue, Configuration, EvaluationContext, EvaluationError, Str, - VariationType, + now, Assignment, AssignmentReason, AssignmentValue, Configuration, EvaluationContext, + EvaluationError, Str, VariationType, }; -use ddcommon_ffi::CharSlice; use crate::Handle; /// Opaque type representing a result of evaluation. -#[allow(unused)] -pub struct ResolutionDetails(Result); +pub struct ResolutionDetails { + inner: Result, + // memoizing some fields, so we can hand off references to them: + error_message: Option, + extra_logging: Vec>, + flag_metadata: Vec>, +} +impl ResolutionDetails { + fn new(value: Result) -> ResolutionDetails { + let error_message = value.as_ref().err().map(|err| err.to_string()); + + let extra_logging = value + .as_ref() + .iter() + .flat_map(|it| it.extra_logging.iter()) + .map(|(k, v)| { + KeyValue { + // SAFETY: the borrow is valid as long as string allocation is + // alive. ResolutionDetails will get moved into heap but this does not + // invalidate the string. + key: unsafe { BorrowedStr::borrow_from_str(k.as_str()) }, + // SAFETY: the borrow is valid as long as string allocation is + // alive. ResolutionDetails will get moved into heap but this does not + // innvalidate the string. + value: unsafe { BorrowedStr::borrow_from_str(v.as_str()) }, + } + }) + .collect(); + + let flag_metadata = match value.as_ref() { + Ok(a) => { + vec![KeyValue { + // SAFETY: borrowing from static is safe as it lives long enough. + key: unsafe { BorrowedStr::borrow_from_str("allocation_key") }, + // SAFETY: allocation_key is alive until ResolutionDetails is dropped. + value: unsafe { BorrowedStr::borrow_from_str(a.allocation_key.as_str()) }, + }] + } + Err(_) => Vec::new(), + }; + + ResolutionDetails { + inner: value, + error_message, + extra_logging, + flag_metadata, + } + } +} +impl From for ResolutionDetails { + fn from(value: Assignment) -> Self { + ResolutionDetails::new(Ok(value)) + } +} +impl From for ResolutionDetails { + fn from(value: EvaluationError) -> Self { + ResolutionDetails::new(Err(value)) + } +} +impl From> for ResolutionDetails { + fn from(value: Result) -> Self { + ResolutionDetails::new(value) + } +} +impl AsRef> for ResolutionDetails { + fn as_ref(&self) -> &Result { + &self.inner + } +} #[repr(C)] pub enum FlagType { @@ -27,6 +90,18 @@ pub enum FlagType { Boolean, Object, } +impl From for Option { + fn from(value: FlagType) -> Self { + match value { + FlagType::Unknown => None, + FlagType::String => Some(VariationType::String), + FlagType::Integer => Some(VariationType::Integer), + FlagType::Float => Some(VariationType::Numeric), + FlagType::Boolean => Some(VariationType::Boolean), + FlagType::Object => Some(VariationType::Json), + } + } +} impl From for FlagType { fn from(value: VariationType) -> Self { @@ -40,6 +115,7 @@ impl From for FlagType { } } +#[derive(Debug, PartialEq)] #[repr(C)] pub enum ErrorCode { Ok, @@ -69,6 +145,7 @@ pub enum Reason { /// The caller must call `ddog_ffe_assignment_drop` on the returned value to free resources. /// /// # Safety +/// /// - `config` must be a valid `Configuration` handle /// - `flag_key` must be a valid C string /// - `context` must be a valid `EvaluationContext` handle @@ -76,31 +153,36 @@ pub enum Reason { pub unsafe extern "C" fn ddog_ffe_get_assignment( config: Handle, flag_key: *const c_char, - _expected_type: FlagType, + expected_type: FlagType, context: Handle, ) -> Handle { if flag_key.is_null() { - return Handle::from(ResolutionDetails(Err(EvaluationError::Internal( - Str::from_static_str("ddog_ffe_get_assignment: flag_key must not be NULL"), - )))); + return Handle::new( + EvaluationError::Internal(Str::from_static_str( + "ddog_ffe_get_assignment: flag_key must not be NULL", + )) + .into(), + ); } + // SAFETY: the caller must ensure that configuration handle is valid let config = unsafe { config.as_ref() }; + // SAFETY: the caller must ensure that context handle is valid let context = unsafe { context.as_ref() }; - let Ok(flag_key) = unsafe { - // SAFETY: we checked that flag_key is not NULL - CStr::from_ptr(flag_key) - } - .to_str() else { - return Handle::from(ResolutionDetails(Err(EvaluationError::Internal( - Str::from_static_str("ddog_ffe_get_assignment: flag_key is not a valid UTF-8 string"), - )))); + // SAFETY: we checked that flag_key is not NULL. + let Ok(flag_key) = unsafe { CStr::from_ptr(flag_key) }.to_str() else { + return Handle::new( + EvaluationError::Internal(Str::from_static_str( + "ddog_ffe_get_assignment: flag_key is not a valid UTF-8 string", + )) + .into(), + ); }; - let assignment_result = config.eval_flag(flag_key, context, None, now()); + let assignment_result = config.eval_flag(flag_key, context, expected_type.into(), now()); - Handle::from(ResolutionDetails(assignment_result)) + Handle::new(assignment_result.into()) } #[repr(C)] @@ -132,8 +214,36 @@ pub struct BorrowedStr { pub ptr: *const u8, pub len: usize, } +impl BorrowedStr { + #[inline] + pub(crate) unsafe fn as_bytes(&self) -> &[u8] { + // SAFETY: the caller must ensure that ptr and len are valid. + unsafe { std::slice::from_raw_parts(self.ptr, self.len) } + } +} + +#[repr(C)] +pub struct KeyValue { + pub key: K, + pub value: V, +} +#[repr(C)] +pub struct ArrayMap { + pub elements: *const KeyValue, + pub count: usize, +} +impl ArrayMap { + /// # Safety + /// - The returned value must not outlive `slice`. + unsafe fn borrow_from_slice(slice: &[KeyValue]) -> ArrayMap { + ArrayMap { + elements: slice.as_ptr(), + count: slice.len(), + } + } +} -impl<'a> BorrowedStr { +impl BorrowedStr { /// Borrow string from `s`. /// /// # Safety @@ -141,7 +251,7 @@ impl<'a> BorrowedStr { /// - The returned value must non outlive `s`. /// - `s` must not be modified while `BorrowedStr` is alive. #[inline] - unsafe fn new(s: &str) -> BorrowedStr { + unsafe fn borrow_from_str(s: &str) -> BorrowedStr { BorrowedStr { ptr: s.as_ptr(), len: s.len(), @@ -163,28 +273,28 @@ impl<'a> BorrowedStr { /// /// The returned `VariantValue` borrows from `assignment`. It must not be used after `assignment` is /// freed. +/// +/// # Safety +/// `assignment` must be a valid handle. #[no_mangle] -pub unsafe extern "C" fn ddog_ffe_assignment_get_value<'a>( +pub unsafe extern "C" fn ddog_ffe_assignment_get_value( assignment: Handle, ) -> VariantValue { - match unsafe { assignment.as_ref() } { - ResolutionDetails(Ok(assignment)) => match &assignment.value { + // SAFETY: the caller must ensure that assignment is valid. + match unsafe { assignment.as_ref() }.as_ref() { + Ok(assignment) => match &assignment.value { AssignmentValue::String(s) => { - VariantValue::String(unsafe { - // SAFETY: caller is required to not use return value after freeing - // `assignment`. - BorrowedStr::new(s.as_str()) - }) + // SAFETY: caller is required to not use return value after freeing + // `assignment`. + VariantValue::String(unsafe { BorrowedStr::borrow_from_str(s.as_str()) }) } AssignmentValue::Integer(v) => VariantValue::Integer(*v), AssignmentValue::Float(v) => VariantValue::Float(*v), AssignmentValue::Boolean(v) => VariantValue::Boolean(*v), AssignmentValue::Json { value: _, raw } => { - VariantValue::Object(unsafe { - // SAFETY: caller is required to not use return value after freeing - // `assignment`. - BorrowedStr::new(raw.get()) - }) + // SAFETY: caller is required to not use return value after freeing + // `assignment`. + VariantValue::Object(unsafe { BorrowedStr::borrow_from_str(raw.get()) }) } }, _ => VariantValue::None, @@ -197,16 +307,18 @@ pub unsafe extern "C" fn ddog_ffe_assignment_get_value<'a>( /// /// The returned string borrows from `assignment`. It must not be used after `assignment` is /// freed. +/// +/// # Safety +/// `assignment` must be a valid handle. #[no_mangle] pub unsafe extern "C" fn ddog_ffe_assignment_get_variant( assignment: Handle, ) -> BorrowedStr { - match unsafe { assignment.as_ref() } { - ResolutionDetails(Ok(assignment)) => unsafe { - // SAFETY: caller is required to not use return value after freeing - // `assignment`. - BorrowedStr::new(&assignment.variation_key) - }, + // SAFETY: the caller must ensure that assignment is valid. + match unsafe { assignment.as_ref() }.as_ref() { + Ok(assignment) => + // SAFETY: caller is required to not use return value after freeing `assignment`. + unsafe { BorrowedStr::borrow_from_str(&assignment.variation_key) }, _ => BorrowedStr::empty(), } } @@ -218,21 +330,136 @@ pub unsafe extern "C" fn ddog_ffe_assignment_get_variant( /// /// The returned string borrows from `assignment`. It must not be used after `assignment` is /// freed. +/// +/// # Safety +/// `assignment` must be a valid handle. #[no_mangle] pub unsafe extern "C" fn ddog_ffe_assignment_get_allocation_key( assignment: Handle, ) -> BorrowedStr { - match unsafe { assignment.as_ref() } { - ResolutionDetails(Ok(assignment)) => unsafe { - // SAFETY: caller is required to not use return value after freeing - // `assignment`. - BorrowedStr::new(assignment.allocation_key.as_str()) + // SAFETY: the caller must ensure that assignment is valid. + match unsafe { assignment.as_ref() }.as_ref() { + // SAFETY: caller is required to not use return value after freeing + // `assignment`. + Ok(assignment) => unsafe { + BorrowedStr::borrow_from_str(assignment.allocation_key.as_str()) }, _ => BorrowedStr::empty(), } } -// TODO: add accessors for various data inside ResolutionDetails. +/// # Safety +/// `assignment` must be a valid handle. +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_assignment_get_reason( + assignment: Handle, +) -> Reason { + // SAFETY: the caller must ensure that assignment is valid + Reason::from(unsafe { assignment.as_ref() }) +} +impl From<&ResolutionDetails> for Reason { + fn from(value: &ResolutionDetails) -> Self { + match value.as_ref() { + Ok(assignment) => assignment.reason.into(), + Err(EvaluationError::FlagDisabled) => Reason::Disabled, + Err(EvaluationError::DefaultAllocationNull) => Reason::Default, + Err(_) => Reason::Error, + } + } +} +impl From for Reason { + fn from(value: AssignmentReason) -> Self { + match value { + AssignmentReason::TargetingMatch => Reason::TargetingMatch, + AssignmentReason::Split => Reason::Split, + AssignmentReason::Static => Reason::Static, + } + } +} + +/// # Safety +/// `assignment` must be a valid handle. +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_assignment_get_error_code( + assignment: Handle, +) -> ErrorCode { + // SAFETY: the caller must ensure that assignment is valid + ErrorCode::from(unsafe { assignment.as_ref() }) +} +impl From<&ResolutionDetails> for ErrorCode { + fn from(value: &ResolutionDetails) -> Self { + match value.as_ref() { + Ok(_) => ErrorCode::Ok, + Err(err) => ErrorCode::from(err), + } + } +} +impl From<&EvaluationError> for ErrorCode { + fn from(value: &EvaluationError) -> Self { + match value { + EvaluationError::TypeMismatch { .. } => ErrorCode::TypeMismatch, + EvaluationError::ConfigurationParseError => ErrorCode::ParseError, + EvaluationError::ConfigurationMissing => ErrorCode::ProviderNotReady, + EvaluationError::FlagUnrecognizedOrDisabled => ErrorCode::FlagNotFound, + EvaluationError::FlagDisabled => ErrorCode::Ok, + EvaluationError::DefaultAllocationNull => ErrorCode::Ok, + EvaluationError::Internal(_) => ErrorCode::General, + _ => ErrorCode::General, + } + } +} + +/// # Safety +/// `assignment` must be a valid handle. +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_assignment_get_error_message( + assignment: Handle, +) -> BorrowedStr { + // SAFETY: the caller must ensure that assignment is valid + let assignment = unsafe { assignment.as_ref() }; + match assignment.error_message.as_ref() { + // SAFETY: the caller must not use returned value after assignment is freed. + Some(s) => unsafe { BorrowedStr::borrow_from_str(s.as_str()) }, + None => BorrowedStr::empty(), + } +} + +/// # Safety +/// `assignment` must be a valid handle. +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_assignment_get_do_log( + assignment: Handle, +) -> bool { + // SAFETY: the caller must ensure that assignment handle is valid. + match unsafe { assignment.as_ref() }.as_ref() { + Ok(a) => a.do_log, + Err(_) => false, + } +} + +/// # Safety +/// `assignment` must be a valid handle. +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_assignnment_get_flag_metadata( + assignment: Handle, +) -> ArrayMap { + // SAFETY: the caller must ensure that assignment is valid + let a = unsafe { assignment.as_ref() }; + // SAFETY: the caller must ensure that returned value is not used after `assignment` is freed. + unsafe { ArrayMap::borrow_from_slice(&a.flag_metadata) } +} + +/// # Safety +/// `assignment` must be a valid handle. +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_assignnment_get_extra_logging( + assignment: Handle, +) -> ArrayMap { + // SAFETY: the caller must ensure that assignment is valid + let a = unsafe { assignment.as_ref() }; + // SAFETY: the caller must ensure that returned value is not used after `assignment` is freed. + unsafe { ArrayMap::borrow_from_slice(&a.extra_logging) } +} /// Frees an Assignment handle. /// @@ -240,5 +467,6 @@ pub unsafe extern "C" fn ddog_ffe_assignment_get_allocation_key( /// - `assignment` must be a valid Assignment handle #[no_mangle] pub unsafe extern "C" fn ddog_ffe_assignment_drop(assignment: *mut Handle) { + // SAFETY: the caller must ensure that assignment is valid unsafe { Handle::free(assignment) } } diff --git a/datadog-ffe-ffi/src/configuration.rs b/datadog-ffe-ffi/src/configuration.rs index a778183966..563be7ffb0 100644 --- a/datadog-ffe-ffi/src/configuration.rs +++ b/datadog-ffe-ffi/src/configuration.rs @@ -1,15 +1,13 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -use std::ffi::{c_char, CStr}; - use anyhow::ensure; use function_name::named; use datadog_ffe::rules_based::{Configuration, UniversalFlagConfig}; use ddcommon_ffi::{wrap_with_ffi_result, Result}; -use crate::Handle; +use crate::{BorrowedStr, Handle}; /// Creates a new Configuration from JSON bytes. /// @@ -20,16 +18,17 @@ use crate::Handle; /// /// # Safety /// -/// - `json_str` must be a valid C string. +/// - `json_bytes` must point to valid memory. #[no_mangle] #[named] pub unsafe extern "C" fn ddog_ffe_configuration_new( - json_str: *const c_char, + json_bytes: BorrowedStr, ) -> Result> { wrap_with_ffi_result!({ - ensure!(!json_str.is_null(), "json_str must not be NULL"); + ensure!(!json_bytes.ptr.is_null(), "json_str must not be NULL"); - let json_bytes = unsafe { CStr::from_ptr(json_str) }.to_bytes().to_vec(); + // SAFETY: the caller must ensure that it's a valid pointer, we also checked for null + let json_bytes = unsafe { json_bytes.as_bytes() }.to_vec(); let configuration = Configuration::from_server_response(UniversalFlagConfig::from_json(json_bytes)?); @@ -45,5 +44,6 @@ pub unsafe extern "C" fn ddog_ffe_configuration_new( /// `config` must be a valid Configuration handle created by `ddog_ffe_configuration_new`. #[no_mangle] pub unsafe extern "C" fn ddog_ffe_configuration_drop(config: *mut Handle) { + // SAFETY: the caller must ensure that config is a valid handle unsafe { Handle::free(config) }; } diff --git a/datadog-ffe-ffi/src/evaluation_context.rs b/datadog-ffe-ffi/src/evaluation_context.rs index 445b892acc..ebab8f9355 100644 --- a/datadog-ffe-ffi/src/evaluation_context.rs +++ b/datadog-ffe-ffi/src/evaluation_context.rs @@ -47,7 +47,8 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( let targeting_key = if targeting_key.is_null() { Str::from_static_str("") } else { - match unsafe { CStr::from_ptr(targeting_key).to_str() } { + // SAFETY: the caller must ensure that it's a valid C string + match unsafe { CStr::from_ptr(targeting_key) }.to_str() { Ok(s) => Str::from(s), Err(_) => Str::from_static_str(""), } @@ -56,16 +57,20 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( let attributes = if attributes.is_null() { HashMap::new() } else { + // SAFETY: the caller must ensure that `attributes` is a valid pointer and + // `attributes_count` accurately represent the number of elements. unsafe { std::slice::from_raw_parts(attributes, attributes_count) } - .into_iter() + .iter() .filter_map(|attr_pair| { if attr_pair.name.is_null() { return None; // Skip invalid pairs } + // SAFETY: the caller must ensure that it's a valid C string let name_str = unsafe { CStr::from_ptr(attr_pair.name) }.to_str().ok()?; let attribute: Attribute = match attr_pair.value { + // SAFETY: the caller must ensure that it's a valid C string. AttributeValue::String(s) => unsafe { CStr::from_ptr(s) }.to_str().ok()?.into(), AttributeValue::Number(v) => v.into(), AttributeValue::Boolean(v) => v.into(), @@ -85,5 +90,6 @@ pub unsafe extern "C" fn ddog_ffe_evaluation_context_new( /// `context` must be a valid EvaluationContext handle created by `ddog_ffe_evaluation_context_new` #[no_mangle] pub unsafe extern "C" fn ddog_ffe_evaluation_context_drop(context: *mut Handle) { + // SAFETY: the caller must ensure that context is a valid handle. unsafe { Handle::free(context) }; } diff --git a/datadog-ffe-ffi/src/handle.rs b/datadog-ffe-ffi/src/handle.rs index d2b5063a55..04570dd10a 100644 --- a/datadog-ffe-ffi/src/handle.rs +++ b/datadog-ffe-ffi/src/handle.rs @@ -12,7 +12,9 @@ pub struct Handle { inner: *mut T, } +// SAFETY: the box pointer is safe to move across threads as long as the underlying type is Send. unsafe impl Send for Handle {} +// SAFETY: we only hand off shared refences, so it's Sync as long as underlying type is Sync. unsafe impl Sync for Handle {} impl Handle { @@ -34,8 +36,10 @@ impl Handle { /// /// # Safety /// - `self` must be a valid handle for `T`. + #[allow(clippy::expect_used)] pub(crate) unsafe fn as_ref(&self) -> &T { - unsafe { self.inner.as_ref().expect("detected use after free") } + // SAFETY: the caller must ensure that self is valid + unsafe { self.inner.as_ref() }.expect("detected use after free") } /// Free this handle. This and all other copies of the handle become invalid after freeing. @@ -47,7 +51,8 @@ impl Handle { return; } - let ptr = std::mem::take(&mut (unsafe { &mut *this }).inner); + // SAFETY: the caller must ensure that the pointer is valid. + let ptr = std::mem::replace(&mut (unsafe { &mut *this }).inner, std::ptr::null_mut()); if ptr.is_null() { // We try to detect double-free but it's not fool-proof. The C side might have copied // the handle. @@ -55,6 +60,7 @@ impl Handle { return; } + // SAFETY: the original value was created by Box::into_raw(). let value = unsafe { Box::from_raw(ptr) }; drop(value); diff --git a/datadog-ffe-ffi/src/lib.rs b/datadog-ffe-ffi/src/lib.rs index 9c422400f8..0725976028 100644 --- a/datadog-ffe-ffi/src/lib.rs +++ b/datadog-ffe-ffi/src/lib.rs @@ -6,7 +6,11 @@ #![cfg_attr(not(test), deny(clippy::expect_used))] #![cfg_attr(not(test), deny(clippy::todo))] #![cfg_attr(not(test), deny(clippy::unimplemented))] -#![deny(unsafe_op_in_unsafe_fn)] +#![deny( + unsafe_op_in_unsafe_fn, + clippy::undocumented_unsafe_blocks, + clippy::multiple_unsafe_ops_per_block +)] mod assignment; mod configuration; From f98114ee0369795c0f8534255139dcc1bdafd182 Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Wed, 5 Nov 2025 09:03:01 +0200 Subject: [PATCH 40/43] add missing license header --- datadog-ffe-ffi/src/handle.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/datadog-ffe-ffi/src/handle.rs b/datadog-ffe-ffi/src/handle.rs index 04570dd10a..ed2fd91df0 100644 --- a/datadog-ffe-ffi/src/handle.rs +++ b/datadog-ffe-ffi/src/handle.rs @@ -1,3 +1,6 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + /// An opaque handle for a resource. The inner fields must not be dereferenced. /// /// This is similar to `ddcommon_ffi::Handle` but only allows shared access to internal resource, so From 0be43f189cffbe9735887f5045fe42347571d12d Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Wed, 5 Nov 2025 09:19:29 +0200 Subject: [PATCH 41/43] fix tests --- .../src/rules_based/eval/eval_assignment.rs | 4 ++-- datadog-ffe/src/rules_based/ufc/assignment.rs | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/datadog-ffe/src/rules_based/eval/eval_assignment.rs b/datadog-ffe/src/rules_based/eval/eval_assignment.rs index 1682fcdc77..5bfddc5811 100644 --- a/datadog-ffe/src/rules_based/eval/eval_assignment.rs +++ b/datadog-ffe/src/rules_based/eval/eval_assignment.rs @@ -221,7 +221,7 @@ mod tests { struct TestCase { flag: String, variation_type: VariationType, - default_value: serde_json::Value, + default_value: Arc, targeting_key: Str, attributes: Arc>, result: TestResult, @@ -229,7 +229,7 @@ mod tests { #[derive(Debug, Serialize, Deserialize)] struct TestResult { - value: serde_json::Value, + value: Arc, } #[test] diff --git a/datadog-ffe/src/rules_based/ufc/assignment.rs b/datadog-ffe/src/rules_based/ufc/assignment.rs index c82abd7af6..cf9ad53326 100644 --- a/datadog-ffe/src/rules_based/ufc/assignment.rs +++ b/datadog-ffe/src/rules_based/ufc/assignment.rs @@ -311,6 +311,28 @@ impl AssignmentValue { } } +impl PartialEq for AssignmentValue { + fn eq(&self, other: &AssignmentValue) -> bool { + match (self, other) { + (Self::String(l0), Self::String(r0)) => l0 == r0, + (Self::Integer(l0), Self::Integer(r0)) => l0 == r0, + (Self::Float(l0), Self::Float(r0)) => l0 == r0, + (Self::Boolean(l0), Self::Boolean(r0)) => l0 == r0, + ( + Self::Json { + value: l_value, + raw: _, + }, + Self::Json { + value: r_value, + raw: _, + }, + ) => l_value == r_value, + _ => false, + } + } +} + #[cfg(feature = "pyo3")] mod pyo3_impl { From 080057f31bee793cfdf60c08e54c8246360adf7a Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Thu, 6 Nov 2025 16:05:41 +0200 Subject: [PATCH 42/43] fix parsing RawValue through TryParse (I think) the way serde_json implements untagged enum parsing is by first parsing into serde_json::Value and then parsing that to one of the variants. This breaks RawValue deserialization because Value does not hold onto original string. This PR makes TryParse work by quickly scanning into RawValue first, and then trying to parse it into target type. If that parsing fails, the RawValue is copied into Box. --- .../src/rules_based/eval/eval_rules.rs | 21 ++++++-------- datadog-ffe/src/rules_based/ufc/models.rs | 28 ++++++++++++++----- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/datadog-ffe/src/rules_based/eval/eval_rules.rs b/datadog-ffe/src/rules_based/eval/eval_rules.rs index 6b7b68efbe..dff2cf2f32 100644 --- a/datadog-ffe/src/rules_based/eval/eval_rules.rs +++ b/datadog-ffe/src/rules_based/eval/eval_rules.rs @@ -2,16 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use crate::rules_based::{ - ufc::{ComparisonOperator, Condition, ConditionCheck, RuleWire, TryParse}, + ufc::{ComparisonOperator, Condition, ConditionCheck, RuleWire}, Attribute, EvaluationContext, }; impl RuleWire { pub(super) fn eval(&self, subject: &EvaluationContext) -> bool { - self.conditions.iter().all(|condition| match condition { - TryParse::Parsed(condition) => condition.eval(subject), - TryParse::ParseFailed(_) => false, - }) + self.conditions + .iter() + .all(|condition| condition.eval(subject)) } } @@ -227,8 +226,7 @@ mod tests { operator: ComparisonOperator::Gt, comparand: 10.0, }, - } - .into()], + }], }; assert!(rule.eval(&EvaluationContext::new( "key".into(), @@ -246,16 +244,14 @@ mod tests { operator: ComparisonOperator::Gt, comparand: 18.0, }, - } - .into(), + }, Condition { attribute: "age".into(), check: ConditionCheck::Comparison { operator: ComparisonOperator::Lt, comparand: 100.0, }, - } - .into(), + }, ], }; assert!(rule.eval(&EvaluationContext::new( @@ -281,8 +277,7 @@ mod tests { operator: ComparisonOperator::Gt, comparand: 10.0, }, - } - .into()], + }], }; assert!(!rule.eval(&EvaluationContext::new( "key".into(), diff --git a/datadog-ffe/src/rules_based/ufc/models.rs b/datadog-ffe/src/rules_based/ufc/models.rs index bcc693fe19..40049fcb50 100644 --- a/datadog-ffe/src/rules_based/ufc/models.rs +++ b/datadog-ffe/src/rules_based/ufc/models.rs @@ -36,16 +36,19 @@ pub struct Environment { /// This can be helpful to isolate errors in a subtree. e.g., if configuration for one flag parses, /// the rest of the flags are still usable. #[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(untagged)] +#[serde(untagged, from = "&serde_json::value::RawValue")] pub enum TryParse { /// Successfully parsed. Parsed(T), /// Parsing failed. - ParseFailed(serde_json::Value), + ParseFailed(Box), } -impl From for TryParse { - fn from(value: T) -> TryParse { - TryParse::Parsed(value) +impl<'de, T: Deserialize<'de>> From<&'de serde_json::value::RawValue> for TryParse { + fn from(raw_value: &'de serde_json::value::RawValue) -> Self { + match serde_json::from_str(raw_value.get()) { + Ok(value) => TryParse::Parsed(value), + Err(_) => TryParse::ParseFailed(raw_value.to_owned()), + } } } impl From> for Option { @@ -120,7 +123,7 @@ fn default_do_log() -> bool { #[serde(rename_all = "camelCase")] #[allow(missing_docs)] pub(crate) struct RuleWire { - pub conditions: Vec>, + pub conditions: Vec, } /// `Condition` is a check that given user `attribute` matches the condition `value` under the given @@ -450,7 +453,18 @@ mod tests { #[cfg_attr(miri, ignore)] // this test is way too slow on miri fn parse_flags_v1() { let json_content = std::fs::read_to_string("tests/data/flags-v1.json").unwrap(); - let _ufc: UniversalFlagConfigWire = serde_json::from_str(&json_content).unwrap(); + let ufc: UniversalFlagConfigWire = serde_json::from_str(&json_content).unwrap(); + + let failures = ufc + .flags + .values() + .filter(|it| matches!(it, TryParse::ParseFailed(_))) + .count(); + assert!( + failures == 0, + "failed to parse {failures}/{} flags", + ufc.flags.len() + ); } #[test] From b0323721f4ab69d4bce5c7597c533759e82d54d3 Mon Sep 17 00:00:00 2001 From: Oleksii Shmalko Date: Thu, 6 Nov 2025 18:46:56 +0200 Subject: [PATCH 43/43] feat: support multiple/number expected flag type --- datadog-ffe-ffi/src/assignment.rs | 51 +++++++++------ datadog-ffe/benches/eval.rs | 15 +++-- datadog-ffe/src/rules_based/error.rs | 6 +- .../src/rules_based/eval/eval_assignment.rs | 64 ++++++++----------- datadog-ffe/src/rules_based/flag_type.rs | 49 ++++++++++++++ datadog-ffe/src/rules_based/mod.rs | 4 +- datadog-ffe/src/rules_based/ufc/assignment.rs | 21 +++--- datadog-ffe/src/rules_based/ufc/models.rs | 26 +++++++- 8 files changed, 160 insertions(+), 76 deletions(-) create mode 100644 datadog-ffe/src/rules_based/flag_type.rs diff --git a/datadog-ffe-ffi/src/assignment.rs b/datadog-ffe-ffi/src/assignment.rs index 9fa7afe56d..afe1469926 100644 --- a/datadog-ffe-ffi/src/assignment.rs +++ b/datadog-ffe-ffi/src/assignment.rs @@ -3,9 +3,10 @@ use std::ffi::{c_char, CStr}; +use datadog_ffe::rules_based as ffe; use datadog_ffe::rules_based::{ now, Assignment, AssignmentReason, AssignmentValue, Configuration, EvaluationContext, - EvaluationError, Str, VariationType, + EvaluationError, Str, }; use crate::Handle; @@ -82,35 +83,47 @@ impl AsRef> for ResolutionDetails { } #[repr(C)] -pub enum FlagType { - Unknown, +pub enum ExpectedFlagType { String, Integer, Float, Boolean, Object, + Number, + Any, } -impl From for Option { - fn from(value: FlagType) -> Self { +impl From for ffe::ExpectedFlagType { + fn from(value: ExpectedFlagType) -> ffe::ExpectedFlagType { match value { - FlagType::Unknown => None, - FlagType::String => Some(VariationType::String), - FlagType::Integer => Some(VariationType::Integer), - FlagType::Float => Some(VariationType::Numeric), - FlagType::Boolean => Some(VariationType::Boolean), - FlagType::Object => Some(VariationType::Json), + ExpectedFlagType::String => ffe::ExpectedFlagType::String, + ExpectedFlagType::Integer => ffe::ExpectedFlagType::Integer, + ExpectedFlagType::Float => ffe::ExpectedFlagType::Float, + ExpectedFlagType::Boolean => ffe::ExpectedFlagType::Boolean, + ExpectedFlagType::Object => ffe::ExpectedFlagType::Object, + ExpectedFlagType::Number => ffe::ExpectedFlagType::Number, + ExpectedFlagType::Any => ffe::ExpectedFlagType::Any, } } } -impl From for FlagType { - fn from(value: VariationType) -> Self { +#[repr(C)] +pub enum FlagType { + Unknown, + String, + Integer, + Float, + Boolean, + Object, +} + +impl From for FlagType { + fn from(value: ffe::FlagType) -> Self { match value { - VariationType::String => FlagType::String, - VariationType::Integer => FlagType::Integer, - VariationType::Numeric => FlagType::Float, - VariationType::Boolean => FlagType::Boolean, - VariationType::Json => FlagType::Object, + ffe::FlagType::String => FlagType::String, + ffe::FlagType::Integer => FlagType::Integer, + ffe::FlagType::Float => FlagType::Float, + ffe::FlagType::Boolean => FlagType::Boolean, + ffe::FlagType::Object => FlagType::Object, } } } @@ -153,7 +166,7 @@ pub enum Reason { pub unsafe extern "C" fn ddog_ffe_get_assignment( config: Handle, flag_key: *const c_char, - expected_type: FlagType, + expected_type: ExpectedFlagType, context: Handle, ) -> Handle { if flag_key.is_null() { diff --git a/datadog-ffe/benches/eval.rs b/datadog-ffe/benches/eval.rs index ab6da4733d..737c937223 100644 --- a/datadog-ffe/benches/eval.rs +++ b/datadog-ffe/benches/eval.rs @@ -6,7 +6,8 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs, sync::Arc}; use datadog_ffe::rules_based::{ - get_assignment, Attribute, Configuration, EvaluationContext, Str, UniversalFlagConfig, + get_assignment, Attribute, Configuration, EvaluationContext, ExpectedFlagType, FlagType, Str, + UniversalFlagConfig, }; fn load_configuration_bytes() -> Vec { @@ -17,7 +18,7 @@ fn load_configuration_bytes() -> Vec { #[serde(rename_all = "camelCase")] struct TestCase { flag: String, - variation_type: String, + variation_type: FlagType, default_value: serde_json::Value, targeting_key: Str, attributes: HashMap, @@ -72,7 +73,13 @@ fn bench_sdk_test_data_rules_based(b: &mut Bencher) { b.iter(|| { for (flag_key, context) in black_box(&test_cases) { // Evaluate assignment - let _assignment = get_assignment(Some(&configuration), flag_key, context, None, now); + let _assignment = get_assignment( + Some(&configuration), + flag_key, + context, + ExpectedFlagType::Any, + now, + ); let _ = black_box(_assignment); } @@ -105,7 +112,7 @@ fn bench_single_flag_rules_based(b: &mut Bencher) { black_box(Some(&configuration)), black_box("kill-switch"), black_box(&context), - None, + ExpectedFlagType::Any, now, ); let _ = black_box(_assignment); diff --git a/datadog-ffe/src/rules_based/error.rs b/datadog-ffe/src/rules_based/error.rs index 13cfd66fa6..3869b24bed 100644 --- a/datadog-ffe/src/rules_based/error.rs +++ b/datadog-ffe/src/rules_based/error.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; -use crate::rules_based::{ufc::VariationType, Str}; +use crate::rules_based::{ExpectedFlagType, FlagType, Str}; /// Enum representing all possible reasons that could result in evaluation returning an error or /// default assignment. @@ -18,9 +18,9 @@ pub enum EvaluationError { #[error("invalid flag type (expected: {expected:?}, found: {found:?})")] TypeMismatch { /// Expected type of the flag. - expected: VariationType, + expected: ExpectedFlagType, /// Actual type of the flag. - found: VariationType, + found: FlagType, }, /// Failed to parse configuration. This should normally never happen and is likely a signal diff --git a/datadog-ffe/src/rules_based/eval/eval_assignment.rs b/datadog-ffe/src/rules_based/eval/eval_assignment.rs index 5bfddc5811..df3ca70ec6 100644 --- a/datadog-ffe/src/rules_based/eval/eval_assignment.rs +++ b/datadog-ffe/src/rules_based/eval/eval_assignment.rs @@ -5,11 +5,8 @@ use chrono::{DateTime, Utc}; use crate::rules_based::{ error::EvaluationError, - ufc::{ - Allocation, Assignment, AssignmentReason, CompiledFlagsConfig, Flag, Shard, Split, - VariationType, - }, - Configuration, EvaluationContext, Timestamp, + ufc::{Allocation, Assignment, AssignmentReason, CompiledFlagsConfig, Flag, Shard, Split}, + Configuration, EvaluationContext, ExpectedFlagType, Timestamp, }; /// Evaluate the specified feature flag for the given subject and return assigned variation and @@ -18,7 +15,7 @@ pub fn get_assignment( configuration: Option<&Configuration>, flag_key: &str, subject: &EvaluationContext, - expected_type: Option, + expected_type: ExpectedFlagType, now: DateTime, ) -> Result { let Some(config) = configuration else { @@ -37,7 +34,7 @@ impl Configuration { &self, flag_key: &str, context: &EvaluationContext, - expected_type: Option, + expected_type: ExpectedFlagType, now: DateTime, ) -> Result { let result = self @@ -72,16 +69,10 @@ impl CompiledFlagsConfig { &self, flag_key: &str, subject: &EvaluationContext, - expected_type: Option, + expected_type: ExpectedFlagType, now: DateTime, ) -> Result { - let flag = self.get_flag(flag_key)?; - - if let Some(ty) = expected_type { - flag.verify_type(ty)?; - } - - flag.eval(subject, now) + self.get_flag(flag_key)?.eval(subject, expected_type, now) } fn get_flag(&self, flag_key: &str) -> Result<&Flag, EvaluationError> { @@ -94,22 +85,19 @@ impl CompiledFlagsConfig { } impl Flag { - fn verify_type(&self, ty: VariationType) -> Result<(), EvaluationError> { - if self.variation_type == ty { - Ok(()) - } else { - Err(EvaluationError::TypeMismatch { - expected: ty, - found: self.variation_type, - }) - } - } - fn eval( &self, subject: &EvaluationContext, + expected_type: ExpectedFlagType, now: DateTime, ) -> Result { + if !expected_type.is_compatible(self.variation_type.into()) { + return Err(EvaluationError::TypeMismatch { + expected: expected_type, + found: self.variation_type.into(), + }); + } + let Some((allocation, (split, reason))) = self.allocations.iter().find_map(|allocation| { let result = allocation.get_matching_split(subject, now); result @@ -212,15 +200,15 @@ mod tests { use crate::rules_based::{ eval::get_assignment, - ufc::{AssignmentValue, UniversalFlagConfig, VariationType}, - Attribute, Configuration, EvaluationContext, Str, + ufc::{AssignmentValue, UniversalFlagConfig}, + Attribute, Configuration, EvaluationContext, FlagType, Str, }; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct TestCase { flag: String, - variation_type: VariationType, + variation_type: FlagType, default_value: Arc, targeting_key: Str, attributes: Arc>, @@ -251,9 +239,11 @@ mod tests { let test_cases: Vec = serde_json::from_reader(f).unwrap(); for test_case in test_cases { - let default_assignment = - AssignmentValue::from_wire(test_case.variation_type, test_case.default_value) - .unwrap(); + let default_assignment = AssignmentValue::from_wire( + test_case.variation_type.into(), + test_case.default_value, + ) + .unwrap(); print!("test subject {:?} ... ", test_case.targeting_key); let subject = EvaluationContext::new(test_case.targeting_key, test_case.attributes); @@ -261,7 +251,7 @@ mod tests { Some(&config), &test_case.flag, &subject, - Some(test_case.variation_type), + test_case.variation_type.into(), now, ); @@ -269,9 +259,11 @@ mod tests { .as_ref() .map(|assignment| &assignment.value) .unwrap_or(&default_assignment); - let expected_assignment = - AssignmentValue::from_wire(test_case.variation_type, test_case.result.value) - .unwrap(); + let expected_assignment = AssignmentValue::from_wire( + test_case.variation_type.into(), + test_case.result.value, + ) + .unwrap(); assert_eq!(result_assingment, &expected_assignment); println!("ok"); diff --git a/datadog-ffe/src/rules_based/flag_type.rs b/datadog-ffe/src/rules_based/flag_type.rs new file mode 100644 index 0000000000..83f0cd6581 --- /dev/null +++ b/datadog-ffe/src/rules_based/flag_type.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[repr(u8)] +pub enum FlagType { + #[serde(alias = "BOOLEAN")] + Boolean = 1, + #[serde(alias = "STRING")] + String = 1 << 1, + #[serde(alias = "NUMERIC")] + Float = 1 << 2, + #[serde(alias = "INTEGER")] + Integer = 1 << 3, + #[serde(alias = "JSON")] + Object = 1 << 4, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[repr(u8)] +#[non_exhaustive] +pub enum ExpectedFlagType { + Boolean = FlagType::Boolean as u8, + String = FlagType::String as u8, + Float = FlagType::Float as u8, + Integer = FlagType::Integer as u8, + Object = FlagType::Object as u8, + Number = (FlagType::Integer as u8) | (FlagType::Float as u8), + Any = 0xff, +} + +impl From for ExpectedFlagType { + fn from(value: FlagType) -> Self { + match value { + FlagType::String => ExpectedFlagType::String, + FlagType::Integer => ExpectedFlagType::Integer, + FlagType::Float => ExpectedFlagType::Float, + FlagType::Boolean => ExpectedFlagType::Boolean, + FlagType::Object => ExpectedFlagType::Object, + } + } +} + +impl ExpectedFlagType { + pub(crate) fn is_compatible(self, ty: FlagType) -> bool { + (self as u8) & (ty as u8) != 0 + } +} diff --git a/datadog-ffe/src/rules_based/mod.rs b/datadog-ffe/src/rules_based/mod.rs index f3197c36d2..6353e1b6f3 100644 --- a/datadog-ffe/src/rules_based/mod.rs +++ b/datadog-ffe/src/rules_based/mod.rs @@ -5,6 +5,7 @@ mod attributes; mod configuration; mod error; mod eval; +mod flag_type; mod sharder; mod str; mod timestamp; @@ -14,6 +15,7 @@ pub use attributes::Attribute; pub use configuration::Configuration; pub use error::EvaluationError; pub use eval::{get_assignment, EvaluationContext}; +pub use flag_type::{ExpectedFlagType, FlagType}; pub use str::Str; pub use timestamp::{now, Timestamp}; -pub use ufc::{Assignment, AssignmentReason, AssignmentValue, UniversalFlagConfig, VariationType}; +pub use ufc::{Assignment, AssignmentReason, AssignmentValue, UniversalFlagConfig}; diff --git a/datadog-ffe/src/rules_based/ufc/assignment.rs b/datadog-ffe/src/rules_based/ufc/assignment.rs index cf9ad53326..ec7596dbc3 100644 --- a/datadog-ffe/src/rules_based/ufc/assignment.rs +++ b/datadog-ffe/src/rules_based/ufc/assignment.rs @@ -6,9 +6,7 @@ use std::sync::Arc; use serde::{Deserialize, Serialize}; -use crate::rules_based::Str; - -use super::VariationType; +use crate::rules_based::{ufc::VariationType, FlagType, Str}; /// Reason for assignment evaluation result. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] @@ -278,18 +276,17 @@ impl AssignmentValue { /// /// # Examples /// ``` - /// # use datadog_ffe::rules_based::AssignmentValue; - /// # use datadog_ffe::rules_based::VariationType; + /// # use datadog_ffe::rules_based::{AssignmentValue, FlagType}; /// let value = AssignmentValue::String("example".into()); - /// assert_eq!(value.variation_type(), VariationType::String); + /// assert_eq!(value.variation_type(), FlagType::String); /// ``` - pub fn variation_type(&self) -> VariationType { + pub fn variation_type(&self) -> FlagType { match self { - AssignmentValue::String(_) => VariationType::String, - AssignmentValue::Integer(_) => VariationType::Integer, - AssignmentValue::Float(_) => VariationType::Numeric, - AssignmentValue::Boolean(_) => VariationType::Boolean, - AssignmentValue::Json { .. } => VariationType::Json, + AssignmentValue::String(_) => FlagType::String, + AssignmentValue::Integer(_) => FlagType::Integer, + AssignmentValue::Float(_) => FlagType::Float, + AssignmentValue::Boolean(_) => FlagType::Boolean, + AssignmentValue::Json { .. } => FlagType::Object, } } diff --git a/datadog-ffe/src/rules_based/ufc/models.rs b/datadog-ffe/src/rules_based/ufc/models.rs index 40049fcb50..583a1c3818 100644 --- a/datadog-ffe/src/rules_based/ufc/models.rs +++ b/datadog-ffe/src/rules_based/ufc/models.rs @@ -6,7 +6,7 @@ use std::{collections::HashMap, sync::Arc}; use regex::Regex; use serde::{Deserialize, Serialize}; -use crate::rules_based::{EvaluationError, Str, Timestamp}; +use crate::rules_based::{EvaluationError, FlagType, Str, Timestamp}; /// Universal Flag Configuration attributes. This contains the actual flag configuration data. #[derive(Debug, Serialize, Deserialize, Clone)] @@ -91,6 +91,30 @@ pub enum VariationType { Json, } +impl From for FlagType { + fn from(value: VariationType) -> FlagType { + match value { + VariationType::String => FlagType::String, + VariationType::Integer => FlagType::Integer, + VariationType::Numeric => FlagType::Float, + VariationType::Boolean => FlagType::Boolean, + VariationType::Json => FlagType::Object, + } + } +} + +impl From for VariationType { + fn from(value: FlagType) -> VariationType { + match value { + FlagType::String => VariationType::String, + FlagType::Integer => VariationType::Integer, + FlagType::Float => VariationType::Numeric, + FlagType::Boolean => VariationType::Boolean, + FlagType::Object => VariationType::Json, + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] #[allow(missing_docs)]