Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions src/flag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,21 @@ impl Flag {
.using_mobile_key
}

/// Returns an iterator over the keys of flags this flag lists as
/// prerequisites.
pub fn prerequisite_keys(&self) -> impl Iterator<Item = &str> + '_ {
self.prerequisites.iter().map(|p| p.key.as_str())
}

/// Returns an iterator over every segment key directly referenced by any
/// `segmentMatch` clause in this flag's rules.
///
/// This does not resolve transitively through other flags (prerequisites)
/// or through segments that themselves reference segments.
pub fn segment_keys(&self) -> impl Iterator<Item = &str> + '_ {
self.rules.iter().flat_map(FlagRule::segment_keys)
}

pub(crate) fn resolve_variation_or_rollout(
&self,
vr: &VariationOrRollout,
Expand Down Expand Up @@ -693,4 +708,137 @@ mod tests {
assert!(with_specific_ratio.contains("\"migration\": {"));
assert!(with_specific_ratio.contains("\"checkRatio\": 42"));
}

#[test]
fn prerequisite_keys_returns_all_listed_prerequisites() {
let json = r#"{
"key": "flag",
"version": 1,
"on": true,
"targets": [],
"rules": [],
"prerequisites": [
{"key": "prereq-a", "variation": 0},
{"key": "prereq-b", "variation": 1}
],
"fallthrough": {"variation": 0},
"offVariation": null,
"variations": [false, true],
"clientSide": false,
"salt": "salty"
}"#;

let flag: Flag = serde_json::from_str(json).unwrap();
let keys: Vec<&str> = flag.prerequisite_keys().collect();
assert_eq!(keys, vec!["prereq-a", "prereq-b"]);
}

#[test]
fn prerequisite_keys_is_empty_when_flag_has_no_prerequisites() {
let json = r#"{
"key": "flag",
"version": 1,
"on": true,
"targets": [],
"rules": [],
"prerequisites": [],
"fallthrough": {"variation": 0},
"offVariation": null,
"variations": [false, true],
"clientSide": false,
"salt": "salty"
}"#;

let flag: Flag = serde_json::from_str(json).unwrap();
assert_eq!(flag.prerequisite_keys().count(), 0);
}

#[test]
fn segment_keys_collects_from_segment_match_clauses() {
let json = r#"{
"key": "flag",
"version": 1,
"on": true,
"targets": [],
"rules": [
{
"id": "r1",
"clauses": [
{
"attribute": "",
"op": "segmentMatch",
"values": ["seg-a", "seg-b"],
"negate": false
}
],
"variation": 1,
"trackEvents": false
},
{
"id": "r2",
"clauses": [
{
"attribute": "email",
"op": "in",
"values": ["foo@example.com"],
"negate": false
},
{
"attribute": "",
"op": "segmentMatch",
"values": ["seg-c"],
"negate": false
}
],
"variation": 1,
"trackEvents": false
}
],
"prerequisites": [],
"fallthrough": {"variation": 0},
"offVariation": null,
"variations": [false, true],
"clientSide": false,
"salt": "salty"
}"#;

let flag: Flag = serde_json::from_str(json).unwrap();
let mut refs: Vec<&str> = flag.segment_keys().collect();
refs.sort();
assert_eq!(refs, vec!["seg-a", "seg-b", "seg-c"]);
}

#[test]
fn segment_keys_ignores_non_segment_match_clauses() {
let json = r#"{
"key": "flag",
"version": 1,
"on": true,
"targets": [],
"rules": [
{
"id": "r1",
"clauses": [
{
"attribute": "email",
"op": "in",
"values": ["seg-a"],
"negate": false
}
],
"variation": 1,
"trackEvents": false
}
],
"prerequisites": [],
"fallthrough": {"variation": 0},
"offVariation": null,
"variations": [false, true],
"clientSide": false,
"salt": "salty"
}"#;

let flag: Flag = serde_json::from_str(json).unwrap();
assert_eq!(flag.segment_keys().count(), 0);
}
}
13 changes: 13 additions & 0 deletions src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,13 @@ impl Clause {
context_kind: Kind::default(),
}
}

/// Returns an iterator over the segment keys referenced by this clause,
/// or an empty iterator if the clause is not a `segmentMatch`.
pub(crate) fn segment_keys(&self) -> impl Iterator<Item = &str> + '_ {
let values = matches!(self.op, Op::SegmentMatch).then_some(&self.values);
values.into_iter().flatten().filter_map(|v| v.as_str())
}
}

impl FlagRule {
Expand All @@ -309,6 +316,12 @@ impl FlagRule {
Ok(true)
}

/// Returns an iterator over every segment key referenced by any
/// `segmentMatch` clause in this rule.
pub(crate) fn segment_keys(&self) -> impl Iterator<Item = &str> + '_ {
self.clauses.iter().flat_map(Clause::segment_keys)
}

#[cfg(test)]
pub(crate) fn new_segment_match(segment_keys: Vec<&str>, kind: Kind) -> Self {
Self {
Expand Down
67 changes: 67 additions & 0 deletions src/segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,24 @@ impl Segment {
Some(generation) => format!("{}.g{}", self.key, generation),
}
}

/// Returns an iterator over every segment key directly referenced by any
/// `segmentMatch` clause in this segment's rules.
///
/// Used to build a dependency graph of segment-to-segment references;
/// does not resolve transitively.
pub fn segment_keys(&self) -> impl Iterator<Item = &str> + '_ {
self.rules.iter().flat_map(SegmentRule::segment_keys)
}
}

impl SegmentRule {
/// Returns an iterator over every segment key referenced by any
/// `segmentMatch` clause in this rule.
pub(crate) fn segment_keys(&self) -> impl Iterator<Item = &str> + '_ {
self.clauses.iter().flat_map(Clause::segment_keys)
}

/// Determines if a context matches the provided segment rule.
///
/// A context will match if all segment clauses match; otherwise, this method returns false.
Expand Down Expand Up @@ -774,4 +789,56 @@ mod tests {
let segment = new_segment();
assert_eq!(segment.unbounded_context_kind, None);
}

#[test]
fn segment_keys_collects_from_segment_match_clauses() {
let json = r#"{
"key": "seg",
"included": [],
"excluded": [],
"rules": [
{
"id": "r1",
"clauses": [
{
"attribute": "",
"op": "segmentMatch",
"values": ["seg-a", "seg-b"],
"negate": false
}
]
},
{
"id": "r2",
"clauses": [
{
"attribute": "email",
"op": "in",
"values": ["foo@example.com"],
"negate": false
},
{
"attribute": "",
"op": "segmentMatch",
"values": ["seg-c"],
"negate": false
}
]
}
],
"salt": "salty",
"version": 1
}"#;

let segment: Segment = serde_json::from_str(json).unwrap();
let mut refs: Vec<&str> = segment.segment_keys().collect();
refs.sort();
assert_eq!(refs, vec!["seg-a", "seg-b", "seg-c"]);
}

#[test]
fn segment_keys_is_empty_for_plain_segment() {
let segment = new_segment();
assert_eq!(segment.segment_keys().count(), 0);
}
}
Loading