|
| 1 | +use datadog_ffe::rules_based::{ |
| 2 | + self as ffe, AssignmentReason, AssignmentValue, Attribute, Configuration, EvaluationContext, |
| 3 | + EvaluationError, ExpectedFlagType, Str, UniversalFlagConfig, |
| 4 | +}; |
| 5 | +use std::collections::HashMap; |
| 6 | +use std::ffi::{c_char, CStr, CString}; |
| 7 | +use std::sync::{Arc, Mutex}; |
| 8 | + |
| 9 | +/// Holds both the FFE configuration and a "changed" flag atomically behind a |
| 10 | +/// single Mutex. This avoids the race where another thread could observe |
| 11 | +/// `config` updated but `changed` still false (or vice-versa). |
| 12 | +/// |
| 13 | +/// A `RwLock` would be more appropriate here (many readers via `ddog_ffe_evaluate`, |
| 14 | +/// rare writer via `store_config`), but PHP is single-threaded per process so |
| 15 | +/// contention is not a practical concern. Keeping a Mutex for simplicity. |
| 16 | +struct FfeState { |
| 17 | + config: Option<Configuration>, |
| 18 | + changed: bool, |
| 19 | +} |
| 20 | + |
| 21 | +lazy_static::lazy_static! { |
| 22 | + static ref FFE_STATE: Mutex<FfeState> = Mutex::new(FfeState { |
| 23 | + config: None, |
| 24 | + changed: false, |
| 25 | + }); |
| 26 | +} |
| 27 | + |
| 28 | +/// Called by remote_config when a new FFE configuration arrives via RC. |
| 29 | +pub fn store_config(config: Configuration) { |
| 30 | + if let Ok(mut state) = FFE_STATE.lock() { |
| 31 | + state.config = Some(config); |
| 32 | + state.changed = true; |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +/// Called by remote_config when an FFE configuration is removed. |
| 37 | +pub fn clear_config() { |
| 38 | + if let Ok(mut state) = FFE_STATE.lock() { |
| 39 | + state.config = None; |
| 40 | + state.changed = true; |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +/// Load a UFC JSON config string directly into the FFE engine. |
| 45 | +/// Used by tests to load config without Remote Config. |
| 46 | +#[no_mangle] |
| 47 | +pub extern "C" fn ddog_ffe_load_config(json: *const c_char) -> bool { |
| 48 | + if json.is_null() { |
| 49 | + return false; |
| 50 | + } |
| 51 | + let json_str = match unsafe { CStr::from_ptr(json) }.to_str() { |
| 52 | + Ok(s) => s, |
| 53 | + Err(_) => return false, |
| 54 | + }; |
| 55 | + match UniversalFlagConfig::from_json(json_str.as_bytes().to_vec()) { |
| 56 | + Ok(ufc) => { |
| 57 | + store_config(Configuration::from_server_response(ufc)); |
| 58 | + true |
| 59 | + } |
| 60 | + Err(_) => false, |
| 61 | + } |
| 62 | +} |
| 63 | + |
| 64 | +/// Check if FFE configuration is loaded. |
| 65 | +#[no_mangle] |
| 66 | +pub extern "C" fn ddog_ffe_has_config() -> bool { |
| 67 | + FFE_STATE.lock().map(|s| s.config.is_some()).unwrap_or(false) |
| 68 | +} |
| 69 | + |
| 70 | +/// Check if FFE config has changed since last check. |
| 71 | +/// Resets the changed flag after reading. |
| 72 | +#[no_mangle] |
| 73 | +pub extern "C" fn ddog_ffe_config_changed() -> bool { |
| 74 | + if let Ok(mut state) = FFE_STATE.lock() { |
| 75 | + let was_changed = state.changed; |
| 76 | + state.changed = false; |
| 77 | + was_changed |
| 78 | + } else { |
| 79 | + false |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +/// Opaque handle for FFE evaluation results returned to C/PHP. |
| 84 | +pub struct FfeResult { |
| 85 | + pub value_json: CString, |
| 86 | + pub variant: Option<CString>, |
| 87 | + pub allocation_key: Option<CString>, |
| 88 | + pub reason: i32, |
| 89 | + pub error_code: i32, |
| 90 | + pub do_log: bool, |
| 91 | +} |
| 92 | + |
| 93 | +/// A single attribute passed from C/PHP for building an EvaluationContext. |
| 94 | +#[repr(C)] |
| 95 | +pub struct FfeAttribute { |
| 96 | + pub key: *const c_char, |
| 97 | + /// 0 = string, 1 = number, 2 = bool |
| 98 | + pub value_type: i32, |
| 99 | + pub string_value: *const c_char, |
| 100 | + pub number_value: f64, |
| 101 | + pub bool_value: bool, |
| 102 | +} |
| 103 | + |
| 104 | +/// Evaluate a feature flag using the stored Configuration. |
| 105 | +/// |
| 106 | +/// Accepts structured attributes from C instead of a JSON blob. |
| 107 | +/// `targeting_key` may be null (no targeting key). |
| 108 | +/// `attributes` / `attributes_count` describe an array of `FfeAttribute`. |
| 109 | +/// Returns null if no config is loaded. |
| 110 | +#[no_mangle] |
| 111 | +pub extern "C" fn ddog_ffe_evaluate( |
| 112 | + flag_key: *const c_char, |
| 113 | + expected_type: i32, |
| 114 | + targeting_key: *const c_char, |
| 115 | + attributes: *const FfeAttribute, |
| 116 | + attributes_count: usize, |
| 117 | +) -> *mut FfeResult { |
| 118 | + let flag_key = match unsafe { CStr::from_ptr(flag_key) }.to_str() { |
| 119 | + Ok(s) => s, |
| 120 | + Err(_) => return std::ptr::null_mut(), |
| 121 | + }; |
| 122 | + |
| 123 | + let expected_type = match expected_type { |
| 124 | + 0 => ExpectedFlagType::String, |
| 125 | + 1 => ExpectedFlagType::Integer, |
| 126 | + 2 => ExpectedFlagType::Float, |
| 127 | + 3 => ExpectedFlagType::Boolean, |
| 128 | + 4 => ExpectedFlagType::Object, |
| 129 | + _ => return std::ptr::null_mut(), |
| 130 | + }; |
| 131 | + |
| 132 | + // Build targeting key |
| 133 | + let tk = if targeting_key.is_null() { |
| 134 | + None |
| 135 | + } else { |
| 136 | + match unsafe { CStr::from_ptr(targeting_key) }.to_str() { |
| 137 | + Ok(s) if !s.is_empty() => Some(Str::from(s)), |
| 138 | + _ => None, |
| 139 | + } |
| 140 | + }; |
| 141 | + |
| 142 | + // Build attributes map from the C array |
| 143 | + let mut attrs = HashMap::new(); |
| 144 | + if !attributes.is_null() && attributes_count > 0 { |
| 145 | + let slice = unsafe { std::slice::from_raw_parts(attributes, attributes_count) }; |
| 146 | + for attr in slice { |
| 147 | + if attr.key.is_null() { |
| 148 | + continue; |
| 149 | + } |
| 150 | + let key = match unsafe { CStr::from_ptr(attr.key) }.to_str() { |
| 151 | + Ok(s) => s, |
| 152 | + Err(_) => continue, |
| 153 | + }; |
| 154 | + let value = match attr.value_type { |
| 155 | + 0 => { |
| 156 | + // string |
| 157 | + if attr.string_value.is_null() { |
| 158 | + continue; |
| 159 | + } |
| 160 | + match unsafe { CStr::from_ptr(attr.string_value) }.to_str() { |
| 161 | + Ok(s) => Attribute::from(s), |
| 162 | + Err(_) => continue, |
| 163 | + } |
| 164 | + } |
| 165 | + 1 => { |
| 166 | + // number |
| 167 | + Attribute::from(attr.number_value) |
| 168 | + } |
| 169 | + 2 => { |
| 170 | + // bool |
| 171 | + Attribute::from(attr.bool_value) |
| 172 | + } |
| 173 | + _ => continue, |
| 174 | + }; |
| 175 | + attrs.insert(Str::from(key), value); |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + let context = EvaluationContext::new(tk, Arc::new(attrs)); |
| 180 | + |
| 181 | + let state = match FFE_STATE.lock() { |
| 182 | + Ok(s) => s, |
| 183 | + Err(_) => return std::ptr::null_mut(), |
| 184 | + }; |
| 185 | + |
| 186 | + let assignment = ffe::get_assignment( |
| 187 | + state.config.as_ref(), |
| 188 | + flag_key, |
| 189 | + &context, |
| 190 | + expected_type, |
| 191 | + ffe::now(), |
| 192 | + ); |
| 193 | + |
| 194 | + let result = match assignment { |
| 195 | + Ok(a) => FfeResult { |
| 196 | + value_json: CString::new(assignment_value_to_json(&a.value)).unwrap_or_default(), |
| 197 | + variant: Some(CString::new(a.variation_key.as_str()).unwrap_or_default()), |
| 198 | + allocation_key: Some(CString::new(a.allocation_key.as_str()).unwrap_or_default()), |
| 199 | + reason: match a.reason { |
| 200 | + AssignmentReason::Static => 0, |
| 201 | + AssignmentReason::TargetingMatch => 2, |
| 202 | + AssignmentReason::Split => 3, |
| 203 | + }, |
| 204 | + error_code: 0, |
| 205 | + do_log: a.do_log, |
| 206 | + }, |
| 207 | + Err(err) => { |
| 208 | + let (error_code, reason) = match &err { |
| 209 | + EvaluationError::TypeMismatch { .. } => (1, 5), |
| 210 | + EvaluationError::ConfigurationParseError => (2, 5), |
| 211 | + EvaluationError::ConfigurationMissing => (6, 5), |
| 212 | + EvaluationError::FlagUnrecognizedOrDisabled => (3, 1), |
| 213 | + EvaluationError::FlagDisabled => (0, 4), |
| 214 | + EvaluationError::DefaultAllocationNull => (0, 1), |
| 215 | + _ => (7, 5), |
| 216 | + }; |
| 217 | + FfeResult { |
| 218 | + value_json: CString::new("null").unwrap_or_default(), |
| 219 | + variant: None, |
| 220 | + allocation_key: None, |
| 221 | + reason, |
| 222 | + error_code, |
| 223 | + do_log: false, |
| 224 | + } |
| 225 | + } |
| 226 | + }; |
| 227 | + |
| 228 | + Box::into_raw(Box::new(result)) |
| 229 | +} |
| 230 | + |
| 231 | +#[no_mangle] |
| 232 | +pub extern "C" fn ddog_ffe_result_value(r: *const FfeResult) -> *const c_char { |
| 233 | + if r.is_null() { |
| 234 | + return std::ptr::null(); |
| 235 | + } |
| 236 | + unsafe { &*r }.value_json.as_ptr() |
| 237 | +} |
| 238 | + |
| 239 | +#[no_mangle] |
| 240 | +pub extern "C" fn ddog_ffe_result_variant(r: *const FfeResult) -> *const c_char { |
| 241 | + if r.is_null() { |
| 242 | + return std::ptr::null(); |
| 243 | + } |
| 244 | + unsafe { &*r } |
| 245 | + .variant |
| 246 | + .as_ref() |
| 247 | + .map(|s| s.as_ptr()) |
| 248 | + .unwrap_or(std::ptr::null()) |
| 249 | +} |
| 250 | + |
| 251 | +#[no_mangle] |
| 252 | +pub extern "C" fn ddog_ffe_result_allocation_key(r: *const FfeResult) -> *const c_char { |
| 253 | + if r.is_null() { |
| 254 | + return std::ptr::null(); |
| 255 | + } |
| 256 | + unsafe { &*r } |
| 257 | + .allocation_key |
| 258 | + .as_ref() |
| 259 | + .map(|s| s.as_ptr()) |
| 260 | + .unwrap_or(std::ptr::null()) |
| 261 | +} |
| 262 | + |
| 263 | +#[no_mangle] |
| 264 | +pub extern "C" fn ddog_ffe_result_reason(r: *const FfeResult) -> i32 { |
| 265 | + if r.is_null() { |
| 266 | + return -1; |
| 267 | + } |
| 268 | + unsafe { &*r }.reason |
| 269 | +} |
| 270 | + |
| 271 | +#[no_mangle] |
| 272 | +pub extern "C" fn ddog_ffe_result_error_code(r: *const FfeResult) -> i32 { |
| 273 | + if r.is_null() { |
| 274 | + return -1; |
| 275 | + } |
| 276 | + unsafe { &*r }.error_code |
| 277 | +} |
| 278 | + |
| 279 | +#[no_mangle] |
| 280 | +pub extern "C" fn ddog_ffe_result_do_log(r: *const FfeResult) -> bool { |
| 281 | + if r.is_null() { |
| 282 | + return false; |
| 283 | + } |
| 284 | + unsafe { &*r }.do_log |
| 285 | +} |
| 286 | + |
| 287 | +#[no_mangle] |
| 288 | +pub unsafe extern "C" fn ddog_ffe_free_result(r: *mut FfeResult) { |
| 289 | + if !r.is_null() { |
| 290 | + drop(Box::from_raw(r)); |
| 291 | + } |
| 292 | +} |
| 293 | + |
| 294 | +fn assignment_value_to_json(value: &AssignmentValue) -> String { |
| 295 | + match value { |
| 296 | + AssignmentValue::String(s) => serde_json::to_string(s.as_str()).unwrap_or_default(), |
| 297 | + AssignmentValue::Integer(i) => i.to_string(), |
| 298 | + AssignmentValue::Float(f) => serde_json::Number::from_f64(*f) |
| 299 | + .map(|n| n.to_string()) |
| 300 | + .unwrap_or_else(|| f.to_string()), |
| 301 | + AssignmentValue::Boolean(b) => b.to_string(), |
| 302 | + AssignmentValue::Json { raw, .. } => raw.get().to_string(), |
| 303 | + } |
| 304 | +} |
0 commit comments