Skip to content

Commit 8d87e01

Browse files
Copilotaepfli
andauthored
feat(storage): detect and report changed flags in update_state (#38)
## Description Per the provider spec, `update_state` now detects and reports which flags changed by comparing all flag fields between old and new configurations. **Changes:** - **New `UpdateStateResponse` struct** - Returns `success`, `error`, and `changedFlags` array - **`FeatureFlag::is_different_from()`** - Uses derived `PartialEq` to compare all flag fields (state, default variant, variants, targeting, and metadata) - **Change detection logic** - Identifies added, removed, and mutated flags with sorted output - **Updated API signature** - `update_flag_state()` returns `Result<UpdateStateResponse, String>` **Response format:** ```json { "success": true, "error": null, "changedFlags": ["flag1", "flag2"] } ``` **Detection criteria:** - Added: Flag exists in new config but not old - Removed: Flag exists in old config but not new - Mutated: Any flag field changed (state, default variant, variants, targeting rules, or metadata) ## Related Issue Closes # ## Type of Change - [x] `feat`: New feature (minor version bump) - [ ] `fix`: Bug fix (patch version bump) - [ ] `docs`: Documentation only changes - [ ] `chore`: Maintenance tasks, dependency updates - [ ] `refactor`: Code refactoring without functional changes - [ ] `test`: Adding or updating tests - [ ] `ci`: CI/CD changes - [ ] `perf`: Performance improvements - [ ] `build`: Build system changes - [ ] `style`: Code style/formatting changes ## PR Title Format Title follows Conventional Commits format for automated changelog generation. ## Testing - [x] Unit tests added/updated - [x] Integration tests added/updated - [x] Manual testing performed - [x] All tests pass (`cargo test`) - 338 tests passing - [x] Code is formatted (`cargo fmt`) - [x] Clippy checks pass (`cargo clippy -- -D warnings`) - [x] WASM builds successfully (if applicable) ## Breaking Changes - [x] This PR includes breaking changes - [x] Documentation has been updated to reflect breaking changes - [ ] Migration guide included (if needed) **Breaking change:** `storage::update_flag_state()` return type changed from `Result<(), String>` to `Result<UpdateStateResponse, String>`. WASM API remains backward compatible with added `changedFlags` field. ## Additional Notes Added 20 comprehensive tests covering all change scenarios including: - First update, additions, removals - Default variant, state, variants (value changes and additions), targeting rules, and metadata mutations - No changes scenario and mixed operations Implementation simplified based on PR feedback to use derived `PartialEq` for complete field comparison, making the code more maintainable and robust. Changed flags are returned sorted alphabetically for deterministic output. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Detect and report changed flags in update_state</issue_title> > <issue_description>Per the provider spec, when `update_state` is called, the evaluator should compare newly parsed flags with the stored flags to identify added, removed, or mutated flags (comparing at least default variant, targeting rules, and metadata). > > Return the list of changed keys in the update response for use in `PROVIDER_CONFIGURATION_CHANGED` events.</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #37 <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/open-feature-forking/flagd-evaluator/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aepfli <9987394+aepfli@users.noreply.github.com>
1 parent f2bb69d commit 8d87e01

5 files changed

Lines changed: 961 additions & 59 deletions

File tree

src/lib.rs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub use evaluation::{
5454
pub use memory::{
5555
pack_ptr_len, string_from_memory, string_to_memory, unpack_ptr_len, wasm_alloc, wasm_dealloc,
5656
};
57-
pub use model::{FeatureFlag, ParsingResult};
57+
pub use model::{FeatureFlag, ParsingResult, UpdateStateResponse};
5858
pub use operators::{create_evaluator, ends_with, fractional, sem_ver, starts_with};
5959
pub use storage::{
6060
clear_flag_state, get_flag_state, set_validation_mode, update_flag_state, ValidationMode,
@@ -252,24 +252,31 @@ pub extern "C" fn set_validation_mode_wasm(mode: u32) -> u64 {
252252
/// Updates the feature flag state with a new configuration.
253253
///
254254
/// This function parses the provided JSON configuration and stores it in
255-
/// thread-local storage for later evaluation.
255+
/// thread-local storage for later evaluation. It also detects which flags
256+
/// have changed by comparing the new configuration with the previous state.
256257
///
257258
/// # Arguments
258259
/// * `config_ptr` - Pointer to the JSON configuration string in WASM memory
259260
/// * `config_len` - Length of the JSON configuration string
260261
///
261262
/// # Returns
262263
/// A packed u64 containing the pointer (upper 32 bits) and length (lower 32 bits)
263-
/// of the response JSON string. The response indicates success or failure.
264+
/// of the response JSON string. The response includes a list of changed flag keys.
264265
///
265266
/// # Response Format
266267
/// ```json
267268
/// {
268269
/// "success": true|false,
269-
/// "error": null|"error message"
270+
/// "error": null|"error message",
271+
/// "changedFlags": ["flag1", "flag2", ...]
270272
/// }
271273
/// ```
272274
///
275+
/// The `changedFlags` array contains the keys of all flags that were:
276+
/// - Added (present in new config but not in old)
277+
/// - Removed (present in old config but not in new)
278+
/// - Mutated (default variant, targeting rules, or metadata changed)
279+
///
273280
/// # Safety
274281
/// The caller must ensure:
275282
/// - `config_ptr` points to valid memory
@@ -289,22 +296,30 @@ fn update_state_internal(config_ptr: *const u8, config_len: u32) -> String {
289296
Err(e) => {
290297
return serde_json::json!({
291298
"success": false,
292-
"error": format!("Failed to read configuration: {}", e)
299+
"error": format!("Failed to read configuration: {}", e),
300+
"changedFlags": null
293301
})
294302
.to_string()
295303
}
296304
};
297305

298306
// Parse and store the configuration using the storage module
299307
match update_flag_state(&config_str) {
300-
Ok(()) => serde_json::json!({
301-
"success": true,
302-
"error": null
303-
})
304-
.to_string(),
308+
Ok(response) => {
309+
// Convert UpdateStateResponse to JSON
310+
serde_json::to_string(&response).unwrap_or_else(|e| {
311+
serde_json::json!({
312+
"success": false,
313+
"error": format!("Failed to serialize response: {}", e),
314+
"changedFlags": null
315+
})
316+
.to_string()
317+
})
318+
}
305319
Err(e) => serde_json::json!({
306320
"success": false,
307-
"error": e
321+
"error": e,
322+
"changedFlags": null
308323
})
309324
.to_string(),
310325
}

src/model/feature_flag.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,40 @@ impl FeatureFlag {
8686
.map(|t| t.to_string())
8787
.unwrap_or_else(|| "{}".to_string())
8888
}
89+
90+
/// Checks if this flag is different from another flag.
91+
///
92+
/// Compares all fields of the flag using the derived PartialEq implementation.
93+
/// This includes state, default variant, variants, targeting rules, and metadata.
94+
///
95+
/// # Arguments
96+
///
97+
/// * `other` - The flag to compare against
98+
///
99+
/// # Example
100+
///
101+
/// ```
102+
/// use flagd_evaluator::model::FeatureFlag;
103+
/// use serde_json::json;
104+
/// use std::collections::HashMap;
105+
///
106+
/// let flag1 = FeatureFlag {
107+
/// key: Some("test".to_string()),
108+
/// state: "ENABLED".to_string(),
109+
/// default_variant: "on".to_string(),
110+
/// variants: HashMap::new(),
111+
/// targeting: Some(json!({"==": [1, 1]})),
112+
/// metadata: HashMap::new(),
113+
/// };
114+
///
115+
/// let mut flag2 = flag1.clone();
116+
/// flag2.default_variant = "off".to_string();
117+
///
118+
/// assert!(flag1.is_different_from(&flag2));
119+
/// ```
120+
pub fn is_different_from(&self, other: &FeatureFlag) -> bool {
121+
self != other
122+
}
89123
}
90124

91125
/// Result of parsing a flagd configuration file.

src/model/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,23 @@
66
mod feature_flag;
77

88
pub use feature_flag::{FeatureFlag, ParsingResult};
9+
10+
use serde::{Deserialize, Serialize};
11+
12+
/// Response from updating flag state indicating which flags have changed.
13+
///
14+
/// This is used for PROVIDER_CONFIGURATION_CHANGED events per the provider spec.
15+
#[derive(Debug, Clone, Serialize, Deserialize)]
16+
#[serde(rename_all = "camelCase")]
17+
pub struct UpdateStateResponse {
18+
/// Whether the update was successful
19+
pub success: bool,
20+
21+
/// Error message if the update failed
22+
#[serde(skip_serializing_if = "Option::is_none")]
23+
pub error: Option<String>,
24+
25+
/// List of flag keys that were changed (added, removed, or mutated)
26+
#[serde(skip_serializing_if = "Option::is_none")]
27+
pub changed_flags: Option<Vec<String>>,
28+
}

0 commit comments

Comments
 (0)