launchdarkly_server_sdk_evaluation/contexts/
attribute_reference.rs

1use serde::{Deserialize, Serialize, Serializer};
2use std::fmt::Display;
3
4#[derive(Clone, Hash, PartialEq, Eq, Debug, Serialize)]
5enum Error {
6    Empty,
7    InvalidEscapeSequence,
8    DoubleOrTrailingSlash,
9}
10
11impl Display for Error {
12    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
13        match self {
14            Error::Empty => write!(f, "Reference cannot be empty"),
15            Error::InvalidEscapeSequence => write!(f, "Reference contains invalid escape sequence"),
16            Error::DoubleOrTrailingSlash => {
17                write!(f, "Reference contains double or trailing slash")
18            }
19        }
20    }
21}
22
23/// Represents an attribute name or path expression identifying a value within a [crate::Context].
24///
25/// This can be used to retrieve a value with [crate::Context::get_value], or to identify an attribute or
26/// nested value that should be considered private with
27/// [crate::ContextBuilder::add_private_attribute] (the SDK configuration can also have a list of
28/// private attribute references).
29///
30/// This is represented as a separate type, rather than just a string, so that validation and parsing can
31/// be done ahead of time if an attribute reference will be used repeatedly later (such as in flag
32/// evaluations).
33///
34/// If the string starts with '/', then this is treated as a slash-delimited path reference where the
35/// first component is the name of an attribute, and subsequent components are the names of nested JSON
36/// object properties. In this syntax, the escape sequences "~0" and "~1" represent '~' and '/'
37/// respectively within a path component.
38///
39/// If the string does not start with '/', then it is treated as the literal name of an attribute.
40///
41/// # Example
42/// ```
43/// # use crate::launchdarkly_server_sdk_evaluation::{ContextBuilder, Context, Reference, AttributeValue};
44/// # use serde_json::json;
45/// # let context: Context = serde_json::from_value(json!(
46/// // Given the following JSON representation of a context:
47/// {
48///   "kind": "user",
49///   "key": "123",
50///   "name": "xyz",
51///   "address": {
52///     "street": "99 Main St.",
53///     "city": "Westview"
54///   },
55///   "a/b": "ok"
56/// }
57/// # )).unwrap();
58///
59/// assert_eq!(context.get_value(&Reference::new("name")),
60///     Some(AttributeValue::String("xyz".to_owned())));
61/// assert_eq!(context.get_value(&Reference::new("/address/street")),
62///     Some(AttributeValue::String("99 Main St.".to_owned())));
63/// assert_eq!(context.get_value(&Reference::new("a/b")),
64///     Some(AttributeValue::String("ok".to_owned())));
65/// assert_eq!(context.get_value(&Reference::new("/a~1b")),
66///     Some(AttributeValue::String("ok".to_owned())));
67/// ```
68#[derive(Clone, Hash, PartialEq, Eq, Debug)]
69pub struct Reference {
70    variant: Variant,
71    input: String,
72}
73
74#[derive(Clone, Hash, PartialEq, Eq, Debug)]
75enum Variant {
76    /// Represents a plain, top-level attribute name; does not start with a '/'.
77    PlainName,
78    /// Represents an attribute pointer; starts with a '/'.
79    Pointer(Vec<String>),
80    /// Represents an invalid input string.
81    Error(Error),
82}
83
84impl Serialize for Reference {
85    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
86    where
87        S: Serializer,
88    {
89        serializer.serialize_str(&self.input)
90    }
91}
92
93impl<'de> Deserialize<'de> for Reference {
94    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95    where
96        D: serde::Deserializer<'de>,
97    {
98        let s = String::deserialize(deserializer)?;
99        Ok(Reference::new(s))
100    }
101}
102
103impl Reference {
104    /// Construct a new context attribute reference.
105    ///
106    /// This constructor always returns a reference that preserves the original string, even if
107    /// validation fails, so that serializing the reference to JSON will produce the original
108    /// string.
109    pub fn new<S: AsRef<str>>(value: S) -> Self {
110        let value = value.as_ref();
111
112        if value.is_empty() || value == "/" {
113            return Self {
114                variant: Variant::Error(Error::Empty),
115                input: value.to_owned(),
116            };
117        }
118
119        if !value.starts_with('/') {
120            return Self {
121                variant: Variant::PlainName,
122                input: value.to_owned(),
123            };
124        }
125
126        let component_result = value[1..]
127            .split('/')
128            .map(|part| {
129                if part.is_empty() {
130                    return Err(Error::DoubleOrTrailingSlash);
131                }
132                Reference::unescape_path(part)
133            })
134            .collect::<Result<Vec<String>, Error>>();
135
136        match component_result {
137            Ok(components) => Self {
138                variant: Variant::Pointer(components),
139                input: value.to_owned(),
140            },
141            Err(e) => Self {
142                variant: Variant::Error(e),
143                input: value.to_owned(),
144            },
145        }
146    }
147
148    /// Returns true if the reference is valid.
149    pub fn is_valid(&self) -> bool {
150        !matches!(&self.variant, Variant::Error(_))
151    }
152
153    /// If the reference is invalid, this method returns an error description; otherwise, it
154    /// returns an empty string.
155    pub fn error(&self) -> String {
156        match &self.variant {
157            Variant::Error(e) => e.to_string(),
158            _ => "".to_owned(),
159        }
160    }
161
162    /// Returns the number of path components in the reference.
163    ///
164    /// For a simple attribute reference such as "name" with no leading slash, this returns 1.
165    ///
166    /// For an attribute reference with a leading slash, it is the number of slash-delimited path
167    /// components after the initial slash.
168    /// # Example
169    /// ```
170    /// # use crate::launchdarkly_server_sdk_evaluation::Reference;
171    /// assert_eq!(Reference::new("a").depth(), 1);
172    /// assert_eq!(Reference::new("/a/b").depth(), 2);
173    /// ```
174    pub fn depth(&self) -> usize {
175        match &self.variant {
176            Variant::Pointer(components) => components.len(),
177            Variant::PlainName => 1,
178            _ => 0,
179        }
180    }
181
182    /// Retrieves a single path component from the attribute reference.
183    ///
184    /// Returns the attribute name for a simple attribute reference such as "name" with no leading slash, if index is zero.
185    ///
186    /// Returns the specified path component if index is less than [Reference::depth], and the reference begins with a slash.
187    ///
188    /// If index is out of range, it returns None.
189    ///
190    /// # Examples
191    /// ```
192    /// # use launchdarkly_server_sdk_evaluation::Reference;
193    /// assert_eq!(Reference::new("a").component(0), Some("a"));
194    /// assert_eq!(Reference::new("/a/b").component(1), Some("b"));
195    /// assert_eq!(Reference::new("/a/b").component(2), None);
196    /// ```
197    pub fn component(&self, index: usize) -> Option<&str> {
198        match (&self.variant, index) {
199            (Variant::Pointer(components), _) => components.get(index).map(|c| c.as_str()),
200            (Variant::PlainName, 0) => Some(&self.input),
201            _ => None,
202        }
203    }
204
205    // Checks if the Reference resolves to a Context's 'kind' attribute.
206    pub(crate) fn is_kind(&self) -> bool {
207        matches!((self.depth(), self.component(0)), (1, Some(comp)) if comp == "kind")
208    }
209
210    fn unescape_path(path: &str) -> Result<String, Error> {
211        // If there are no tildes then there's definitely nothing to do
212        if !path.contains('~') {
213            return Ok(path.to_string());
214        }
215
216        let mut out = String::new();
217
218        let mut iter = path.chars().peekable();
219        while let Some(c) = iter.next() {
220            if c != '~' {
221                out.push(c);
222                continue;
223            }
224            if iter.peek().is_none() {
225                return Err(Error::InvalidEscapeSequence);
226            }
227
228            let unescaped = match iter.next().unwrap() {
229                '0' => '~',
230                '1' => '/',
231                _ => return Err(Error::InvalidEscapeSequence),
232            };
233            out.push(unescaped);
234        }
235
236        Ok(out)
237    }
238}
239
240impl Default for Reference {
241    /// A default [Reference] is empty and invalid.
242    fn default() -> Self {
243        Reference::new("")
244    }
245}
246
247/// Displays the input string used to construct the [Reference].
248impl Display for Reference {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
250        write!(f, "{}", self.input)
251    }
252}
253
254impl<S> From<S> for Reference
255where
256    S: AsRef<str>,
257{
258    fn from(reference: S) -> Self {
259        Reference::new(reference)
260    }
261}
262
263impl From<Reference> for String {
264    fn from(r: Reference) -> Self {
265        r.input
266    }
267}
268
269#[derive(Debug, Deserialize, PartialEq)]
270#[serde(transparent)]
271/// Represents an attribute name, found in pre-Context data.
272/// AttributeNames are incapable of referring to nested values, and instead only
273/// refer to top-level attributes.  
274pub(crate) struct AttributeName(String);
275
276impl AttributeName {
277    /// Constructs an AttributeName, which can be converted into an equivalent [Reference].
278    #[cfg(test)]
279    pub(crate) fn new(s: String) -> Self {
280        Self(s)
281    }
282}
283
284impl Default for AttributeName {
285    fn default() -> Self {
286        Self("".to_owned())
287    }
288}
289
290impl From<AttributeName> for Reference {
291    /// AttributeNames are converted into References based on the presence or
292    /// absence of a leading '/'.
293    ///
294    /// Although References are able to represent plain, top-level attribute
295    /// names, they cannot represent those that begin with a leading '/' because that signifies
296    /// the pointer syntax.
297    ///
298    /// Therefore, if the first character is a '/' the string must be escaped.
299    ///
300    /// This results in the equivalent [Reference] representation of that [AttributeName].
301    ///
302    /// Note that References constructed from an AttributeName will serialize to the
303    /// string passed into the Reference constructor, not the original AttributeName. This
304    /// is desirable since data should be "upgraded" into the new format as it is encountered.
305    fn from(name: AttributeName) -> Self {
306        if !name.0.starts_with('/') {
307            return Self::new(name.0);
308        }
309        let mut escaped = name.0.replace('~', "~0").replace('/', "~1");
310        escaped.insert(0, '/');
311        Self::new(escaped)
312    }
313}
314
315#[cfg(test)]
316pub(crate) mod proptest_generators {
317    use super::{AttributeName, Reference};
318    use proptest::prelude::*;
319
320    // This regular expression is meant to match our spec for an acceptable attribute string,
321    // both those representing attribute references, and those representing literal attribute
322    // names.
323    // A. Plain attribute names are handled by the first alternative (not beginning with '/')
324    // B. Attribute references are handled by the second alternative.
325    //    1) Starts with a slash
326    //    2) Followed by any character that isn't a / or ~ (they must be escaped with ~1 and ~0)
327    //    3) Or, an occurrence of ~1 or ~0
328    //    4) At least one of 2) or 3) is required.
329    //    The path component can repeat one or more times.
330    prop_compose! {
331        // Generate any string that could represent a valid reference, either using
332        // JSON-pointer-like syntax, or plain attribute name. Will not return an empty string.
333        pub(crate) fn any_valid_ref_string()(s in "([^/].*|(/([^/~]|~[01])+)+)") -> String {
334            s
335        }
336
337    }
338
339    prop_compose! {
340         pub(crate) fn any_valid_plain_name()(s in "([^/].*)") -> String {
341            s
342         }
343    }
344
345    prop_compose! {
346         pub(crate) fn any_attribute_name()(s in any_valid_ref_string()) -> AttributeName {
347            AttributeName::new(s)
348         }
349    }
350
351    prop_compose! {
352        // Generate any valid reference.
353        pub(crate) fn any_valid_ref()(s in any_valid_ref_string()) -> Reference {
354            Reference::new(s)
355        }
356    }
357
358    prop_compose! {
359        // Generate any reference, invalid or not. May generate empty strings.
360        pub(crate) fn any_ref()(s in any::<String>()) -> Reference {
361            Reference::new(s)
362        }
363    }
364
365    prop_compose! {
366        pub(crate) fn any_valid_ref_transformed_from_attribute_name()(s in any_valid_ref_string()) -> Reference {
367            Reference::from(AttributeName::new(s))
368        }
369    }
370
371    prop_compose! {
372        // Generate any literal reference, valid or not. May generate empty strings.
373        pub(crate) fn any_ref_transformed_from_attribute_name()(s in any::<String>()) -> Reference {
374            Reference::from(AttributeName::new(s))
375        }
376    }
377
378    prop_compose! {
379        pub(crate) fn any_valid_plain_ref()(s in any_valid_plain_name()) -> Reference {
380            Reference::new(s)
381        }
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::{AttributeName, Error, Reference};
388    use crate::proptest_generators::*;
389    use proptest::prelude::*;
390    use test_case::test_case;
391
392    proptest! {
393        #[test]
394        fn regex_creates_valid_references(reference in any_valid_ref()) {
395            prop_assert!(reference.is_valid());
396        }
397    }
398
399    proptest! {
400        // Although this should be a subset of the previous test, it's still useful to
401        // assert that it obeys the property of generating valid references on its own.
402        #[test]
403        fn regex_creates_valid_plain_references(reference in any_valid_plain_ref()) {
404            prop_assert!(reference.is_valid());
405        }
406    }
407
408    proptest! {
409        #[test]
410        fn plain_references_have_single_component(reference in any_valid_plain_ref()) {
411            prop_assert_eq!(reference.depth(), 1);
412        }
413    }
414
415    proptest! {
416        #[test]
417        fn attribute_names_are_valid_references(reference in any_valid_ref_transformed_from_attribute_name()) {
418            prop_assert!(reference.is_valid());
419            prop_assert_eq!(reference.depth(), 1);
420        }
421    }
422
423    proptest! {
424        #[test]
425        fn attribute_name_references_have_single_component(reference in any_valid_ref_transformed_from_attribute_name()) {
426            prop_assert_eq!(reference.depth(), 1);
427            let component = reference.component(0);
428            prop_assert!(component.is_some(), "component 0 should exist");
429        }
430    }
431
432    proptest! {
433        #[test]
434        fn raw_returns_input_unmodified(s in any::<String>()) {
435            let a = Reference::new(s.clone());
436            prop_assert_eq!(a.to_string(), s);
437        }
438    }
439
440    #[test]
441    fn default_reference_is_invalid() {
442        assert!(!Reference::default().is_valid());
443    }
444
445    #[test_case("", Error::Empty; "Empty reference")]
446    #[test_case("/", Error::Empty; "Single slash")]
447    #[test_case("//", Error::DoubleOrTrailingSlash; "Double slash")]
448    #[test_case("/a//b", Error::DoubleOrTrailingSlash; "Double slash in middle")]
449    #[test_case("/a/b/", Error::DoubleOrTrailingSlash; "Trailing slash")]
450    #[test_case("/~3", Error::InvalidEscapeSequence; "Tilde must be followed by 0 or 1 only")]
451    #[test_case("/testing~something", Error::InvalidEscapeSequence; "Tilde cannot be alone")]
452    #[test_case("/m~~0", Error::InvalidEscapeSequence; "Extra tilde before valid escape")]
453    #[test_case("/a~", Error::InvalidEscapeSequence; "Tilde cannot be followed by nothing")]
454    fn invalid_references(input: &str, error: Error) {
455        let reference = Reference::new(input);
456        assert!(!reference.is_valid());
457        assert_eq!(error.to_string(), reference.error());
458    }
459
460    #[test_case("key")]
461    #[test_case("kind")]
462    #[test_case("name")]
463    #[test_case("name/with/slashes")]
464    #[test_case("name~0~1with-what-looks-like-escape-sequences")]
465    fn plain_reference_syntax(input: &str) {
466        let reference = Reference::new(input);
467        assert!(reference.is_valid());
468        assert_eq!(input, reference.to_string());
469        assert_eq!(
470            input,
471            reference
472                .component(0)
473                .expect("Failed to get first component")
474        );
475        assert_eq!(1, reference.depth());
476    }
477
478    #[test_case("/key", "key")]
479    #[test_case("/kind", "kind")]
480    #[test_case("/name", "name")]
481    #[test_case("/custom", "custom")]
482    fn pointer_syntax(input: &str, path: &str) {
483        let reference = Reference::new(input);
484        assert!(reference.is_valid());
485        assert_eq!(input, reference.to_string());
486        assert_eq!(
487            path,
488            reference
489                .component(0)
490                .expect("Failed to get first component")
491        );
492        assert_eq!(1, reference.depth())
493    }
494
495    #[test_case("/a/b", 2, 0, "a")]
496    #[test_case("/a/b", 2, 1, "b")]
497    #[test_case("/a~1b/c", 2, 0, "a/b")]
498    #[test_case("/a~1b/c", 2, 1, "c")]
499    #[test_case("/a/10/20/30x", 4, 1, "10")]
500    #[test_case("/a/10/20/30x", 4, 2, "20")]
501    #[test_case("/a/10/20/30x", 4, 3, "30x")]
502    fn handles_subcomponents(input: &str, len: usize, index: usize, expected_name: &str) {
503        let reference = Reference::new(input);
504        assert!(reference.is_valid());
505        assert_eq!(input, reference.input);
506        assert_eq!(len, reference.depth());
507        assert_eq!(expected_name, reference.component(index).unwrap());
508    }
509
510    #[test]
511    fn can_handle_invalid_index_requests() {
512        let reference = Reference::new("/a/b/c");
513        assert!(reference.is_valid());
514        assert!(reference.component(0).is_some());
515        assert!(reference.component(1).is_some());
516        assert!(reference.component(2).is_some());
517        assert!(reference.component(3).is_none());
518    }
519
520    #[test_case("/a/b", "/~1a~1b")]
521    #[test_case("a", "a")]
522    #[test_case("a~1b", "a~1b")]
523    #[test_case("/a~1b", "/~1a~01b")]
524    #[test_case("/a~0b", "/~1a~00b")]
525    #[test_case("", "")]
526    #[test_case("/", "/~1")]
527    fn attribute_name_equality(name: &str, reference: &str) {
528        let as_name = AttributeName::new(name.to_owned());
529        let reference = Reference::new(reference);
530        assert_eq!(Reference::from(as_name), reference);
531    }
532
533    #[test]
534    fn is_kind() {
535        assert!(Reference::new("/kind").is_kind());
536        assert!(Reference::new("kind").is_kind());
537        assert!(Reference::from(AttributeName::new("kind".to_owned())).is_kind());
538
539        assert!(!Reference::from(AttributeName::new("/kind".to_owned())).is_kind());
540        assert!(!Reference::new("foo").is_kind());
541    }
542}