Skip to main content

launchdarkly_server_sdk/fdv2/
model.rs

1use launchdarkly_server_sdk_evaluation::{Flag, Segment};
2
3use crate::stores::change_set::{self, ItemChange};
4use crate::stores::store_types::StorageItem;
5
6use super::wire::{DeleteObject, PutObject};
7
8/// Identifies a point in the flag-data stream, echoed back to request the next changes.
9pub type Selector = Option<String>;
10
11/// Whether a change set is a full payload, an incremental update, or carries no changes.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ChangeSetKind {
14    /// The change set carries no changes.
15    None,
16    /// The change set is a complete payload that replaces all data.
17    Full,
18    /// The change set is an incremental update to existing data.
19    Partial,
20}
21
22#[derive(Debug)]
23pub(super) enum FDv2Change {
24    Put(PutObject),
25    Delete(DeleteObject),
26}
27
28#[derive(Debug)]
29pub(super) struct ChangeSet {
30    pub(super) kind: ChangeSetKind,
31    pub(super) changes: Vec<FDv2Change>,
32    pub(super) selector: Selector,
33}
34
35impl TryFrom<ChangeSet> for change_set::ChangeSet {
36    type Error = serde_json::Error;
37
38    fn try_from(wire_cs: ChangeSet) -> Result<Self, Self::Error> {
39        let ChangeSet {
40            kind,
41            changes,
42            selector,
43        } = wire_cs;
44
45        let mut translated = Vec::with_capacity(changes.len());
46        for change in changes {
47            match change {
48                FDv2Change::Put(put) => match put.kind.as_str() {
49                    "flag" => translated.push(ItemChange::Flag {
50                        key: put.key,
51                        item: StorageItem::Item(serde_json::from_value::<Flag>(put.object)?),
52                    }),
53                    "segment" => translated.push(ItemChange::Segment {
54                        key: put.key,
55                        item: StorageItem::Item(serde_json::from_value::<Segment>(put.object)?),
56                    }),
57                    other => warn!("FDv2: unknown kind '{other}' in put-object, skipping"),
58                },
59                FDv2Change::Delete(del) => match del.kind.as_str() {
60                    "flag" => translated.push(ItemChange::Flag {
61                        key: del.key,
62                        item: StorageItem::Tombstone(del.version),
63                    }),
64                    "segment" => translated.push(ItemChange::Segment {
65                        key: del.key,
66                        item: StorageItem::Tombstone(del.version),
67                    }),
68                    other => warn!("FDv2: unknown kind '{other}' in delete-object, skipping"),
69                },
70            }
71        }
72
73        Ok(change_set::ChangeSet {
74            kind,
75            changes: translated,
76            selector,
77        })
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::test_common::{basic_flag, basic_segment};
85    use serde_json::json;
86
87    fn put(kind: &str, key: &str, version: u64, object: serde_json::Value) -> FDv2Change {
88        FDv2Change::Put(PutObject {
89            version,
90            kind: kind.into(),
91            key: key.into(),
92            object,
93        })
94    }
95
96    fn delete(kind: &str, key: &str, version: u64) -> FDv2Change {
97        FDv2Change::Delete(DeleteObject {
98            version,
99            kind: kind.into(),
100            key: key.into(),
101        })
102    }
103
104    #[test]
105    fn none_kind_translates_to_empty_changeset() {
106        let wire = ChangeSet {
107            kind: ChangeSetKind::None,
108            changes: vec![],
109            selector: Some("s-1".into()),
110        };
111        let out = change_set::ChangeSet::try_from(wire).unwrap();
112        assert_eq!(out.kind, ChangeSetKind::None);
113        assert!(out.changes.is_empty());
114        assert_eq!(out.selector.as_deref(), Some("s-1"));
115    }
116
117    #[test]
118    fn typed_puts_and_deletes_pass_through() {
119        let flag_json = serde_json::to_value(basic_flag("f1")).unwrap();
120        let segment_json = serde_json::to_value(basic_segment("s1")).unwrap();
121        let wire = ChangeSet {
122            kind: ChangeSetKind::Full,
123            changes: vec![
124                put("flag", "f1", 1, flag_json),
125                put("segment", "s1", 2, segment_json),
126                delete("flag", "old", 3),
127            ],
128            selector: Some("s-full".into()),
129        };
130        let out = change_set::ChangeSet::try_from(wire).unwrap();
131        assert_eq!(out.changes.len(), 3);
132        assert!(
133            matches!(&out.changes[0], ItemChange::Flag { key, item: StorageItem::Item(f) }
134            if key == "f1" && f.key == "f1")
135        );
136        assert!(
137            matches!(&out.changes[1], ItemChange::Segment { key, item: StorageItem::Item(s) }
138            if key == "s1" && s.key == "s1")
139        );
140        assert!(
141            matches!(&out.changes[2], ItemChange::Flag { key, item: StorageItem::Tombstone(3) }
142            if key == "old")
143        );
144    }
145
146    #[test]
147    fn unknown_kind_is_dropped() {
148        let wire = ChangeSet {
149            kind: ChangeSetKind::Partial,
150            changes: vec![put("mystery", "k", 1, json!({}))],
151            selector: None,
152        };
153        let out = change_set::ChangeSet::try_from(wire).unwrap();
154        assert!(out.changes.is_empty());
155    }
156
157    #[test]
158    fn parse_failure_returns_err() {
159        let wire = ChangeSet {
160            kind: ChangeSetKind::Full,
161            changes: vec![put("flag", "bad", 1, json!({"garbage": true}))],
162            selector: None,
163        };
164        assert!(change_set::ChangeSet::try_from(wire).is_err());
165    }
166}