Skip to main content

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    /// Constructs a [Reference] from a literal top-level attribute name, escaping it per the
149    /// backwards-compatibility rules so a name beginning with '/' is not read as pointer syntax.
150    pub(crate) fn from_literal_name(name: &str) -> Self {
151        if !name.starts_with('/') {
152            return Self::new(name);
153        }
154        let mut escaped = name.replace('~', "~0").replace('/', "~1");
155        escaped.insert(0, '/');
156        Self::new(escaped)
157    }
158
159    /// Returns true if the reference is valid.
160    pub fn is_valid(&self) -> bool {
161        !matches!(&self.variant, Variant::Error(_))
162    }
163
164    /// If the reference is invalid, this method returns an error description; otherwise, it
165    /// returns an empty string.
166    pub fn error(&self) -> String {
167        match &self.variant {
168            Variant::Error(e) => e.to_string(),
169            _ => "".to_owned(),
170        }
171    }
172
173    /// Returns the number of path components in the reference.
174    ///
175    /// For a simple attribute reference such as "name" with no leading slash, this returns 1.
176    ///
177    /// For an attribute reference with a leading slash, it is the number of slash-delimited path
178    /// components after the initial slash.
179    /// # Example
180    /// ```
181    /// # use crate::launchdarkly_server_sdk_evaluation::Reference;
182    /// assert_eq!(Reference::new("a").depth(), 1);
183    /// assert_eq!(Reference::new("/a/b").depth(), 2);
184    /// ```
185    pub fn depth(&self) -> usize {
186        match &self.variant {
187            Variant::Pointer(components) => components.len(),
188            Variant::PlainName => 1,
189            _ => 0,
190        }
191    }
192
193    /// Retrieves a single path component from the attribute reference.
194    ///
195    /// Returns the attribute name for a simple attribute reference such as "name" with no leading slash, if index is zero.
196    ///
197    /// Returns the specified path component if index is less than [Reference::depth], and the reference begins with a slash.
198    ///
199    /// If index is out of range, it returns None.
200    ///
201    /// # Examples
202    /// ```
203    /// # use launchdarkly_server_sdk_evaluation::Reference;
204    /// assert_eq!(Reference::new("a").component(0), Some("a"));
205    /// assert_eq!(Reference::new("/a/b").component(1), Some("b"));
206    /// assert_eq!(Reference::new("/a/b").component(2), None);
207    /// ```
208    pub fn component(&self, index: usize) -> Option<&str> {
209        match (&self.variant, index) {
210            (Variant::Pointer(components), _) => components.get(index).map(|c| c.as_str()),
211            (Variant::PlainName, 0) => Some(&self.input),
212            _ => None,
213        }
214    }
215
216    // Checks if the Reference resolves to a Context's 'kind' attribute.
217    pub(crate) fn is_kind(&self) -> bool {
218        matches!((self.depth(), self.component(0)), (1, Some(comp)) if comp == "kind")
219    }
220
221    fn unescape_path(path: &str) -> Result<String, Error> {
222        // If there are no tildes then there's definitely nothing to do
223        if !path.contains('~') {
224            return Ok(path.to_string());
225        }
226
227        let mut out = String::new();
228
229        let mut iter = path.chars().peekable();
230        while let Some(c) = iter.next() {
231            if c != '~' {
232                out.push(c);
233                continue;
234            }
235            if iter.peek().is_none() {
236                return Err(Error::InvalidEscapeSequence);
237            }
238
239            let unescaped = match iter.next().unwrap() {
240                '0' => '~',
241                '1' => '/',
242                _ => return Err(Error::InvalidEscapeSequence),
243            };
244            out.push(unescaped);
245        }
246
247        Ok(out)
248    }
249}
250
251impl Default for Reference {
252    /// A default [Reference] is empty and invalid.
253    fn default() -> Self {
254        Reference::new("")
255    }
256}
257
258/// Displays the input string used to construct the [Reference].
259impl Display for Reference {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
261        write!(f, "{}", self.input)
262    }
263}
264
265impl<S> From<S> for Reference
266where
267    S: AsRef<str>,
268{
269    fn from(reference: S) -> Self {
270        Reference::new(reference)
271    }
272}
273
274impl From<Reference> for String {
275    fn from(r: Reference) -> Self {
276        r.input
277    }
278}
279
280#[derive(Debug, Deserialize, PartialEq)]
281#[serde(transparent)]
282/// Represents an attribute name, found in pre-Context data.
283/// AttributeNames are incapable of referring to nested values, and instead only
284/// refer to top-level attributes.  
285pub(crate) struct AttributeName(String);
286
287impl AttributeName {
288    /// Constructs an AttributeName, which can be converted into an equivalent [Reference].
289    #[cfg(test)]
290    pub(crate) fn new(s: String) -> Self {
291        Self(s)
292    }
293}
294
295impl Default for AttributeName {
296    fn default() -> Self {
297        Self("".to_owned())
298    }
299}
300
301impl From<AttributeName> for Reference {
302    /// AttributeNames are converted into References based on the presence or
303    /// absence of a leading '/'.
304    ///
305    /// Although References are able to represent plain, top-level attribute
306    /// names, they cannot represent those that begin with a leading '/' because that signifies
307    /// the pointer syntax.
308    ///
309    /// Therefore, if the first character is a '/' the string must be escaped.
310    ///
311    /// This results in the equivalent [Reference] representation of that [AttributeName].
312    ///
313    /// Note that References constructed from an AttributeName will serialize to the
314    /// string passed into the Reference constructor, not the original AttributeName. This
315    /// is desirable since data should be "upgraded" into the new format as it is encountered.
316    fn from(name: AttributeName) -> Self {
317        Reference::from_literal_name(&name.0)
318    }
319}
320
321#[cfg(test)]
322pub(crate) mod proptest_generators {
323    use super::{AttributeName, Reference};
324    use proptest::prelude::*;
325
326    // This regular expression is meant to match our spec for an acceptable attribute string,
327    // both those representing attribute references, and those representing literal attribute
328    // names.
329    // A. Plain attribute names are handled by the first alternative (not beginning with '/')
330    // B. Attribute references are handled by the second alternative.
331    //    1) Starts with a slash
332    //    2) Followed by any character that isn't a / or ~ (they must be escaped with ~1 and ~0)
333    //    3) Or, an occurrence of ~1 or ~0
334    //    4) At least one of 2) or 3) is required.
335    //    The path component can repeat one or more times.
336    prop_compose! {
337        // Generate any string that could represent a valid reference, either using
338        // JSON-pointer-like syntax, or plain attribute name. Will not return an empty string.
339        pub(crate) fn any_valid_ref_string()(s in "([^/].*|(/([^/~]|~[01])+)+)") -> String {
340            s
341        }
342
343    }
344
345    prop_compose! {
346         pub(crate) fn any_valid_plain_name()(s in "([^/].*)") -> String {
347            s
348         }
349    }
350
351    prop_compose! {
352         pub(crate) fn any_attribute_name()(s in any_valid_ref_string()) -> AttributeName {
353            AttributeName::new(s)
354         }
355    }
356
357    prop_compose! {
358        // Generate any valid reference.
359        pub(crate) fn any_valid_ref()(s in any_valid_ref_string()) -> Reference {
360            Reference::new(s)
361        }
362    }
363
364    prop_compose! {
365        // Generate any reference, invalid or not. May generate empty strings.
366        pub(crate) fn any_ref()(s in any::<String>()) -> Reference {
367            Reference::new(s)
368        }
369    }
370
371    prop_compose! {
372        pub(crate) fn any_valid_ref_transformed_from_attribute_name()(s in any_valid_ref_string()) -> Reference {
373            Reference::from(AttributeName::new(s))
374        }
375    }
376
377    prop_compose! {
378        // Generate any literal reference, valid or not. May generate empty strings.
379        pub(crate) fn any_ref_transformed_from_attribute_name()(s in any::<String>()) -> Reference {
380            Reference::from(AttributeName::new(s))
381        }
382    }
383
384    prop_compose! {
385        pub(crate) fn any_valid_plain_ref()(s in any_valid_plain_name()) -> Reference {
386            Reference::new(s)
387        }
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::{AttributeName, Error, Reference};
394    use crate::proptest_generators::*;
395    use proptest::prelude::*;
396    use test_case::test_case;
397
398    proptest! {
399        #[test]
400        fn regex_creates_valid_references(reference in any_valid_ref()) {
401            prop_assert!(reference.is_valid());
402        }
403    }
404
405    proptest! {
406        // Although this should be a subset of the previous test, it's still useful to
407        // assert that it obeys the property of generating valid references on its own.
408        #[test]
409        fn regex_creates_valid_plain_references(reference in any_valid_plain_ref()) {
410            prop_assert!(reference.is_valid());
411        }
412    }
413
414    proptest! {
415        #[test]
416        fn plain_references_have_single_component(reference in any_valid_plain_ref()) {
417            prop_assert_eq!(reference.depth(), 1);
418        }
419    }
420
421    proptest! {
422        #[test]
423        fn attribute_names_are_valid_references(reference in any_valid_ref_transformed_from_attribute_name()) {
424            prop_assert!(reference.is_valid());
425            prop_assert_eq!(reference.depth(), 1);
426        }
427    }
428
429    proptest! {
430        #[test]
431        fn attribute_name_references_have_single_component(reference in any_valid_ref_transformed_from_attribute_name()) {
432            prop_assert_eq!(reference.depth(), 1);
433            let component = reference.component(0);
434            prop_assert!(component.is_some(), "component 0 should exist");
435        }
436    }
437
438    proptest! {
439        #[test]
440        fn raw_returns_input_unmodified(s in any::<String>()) {
441            let a = Reference::new(s.clone());
442            prop_assert_eq!(a.to_string(), s);
443        }
444    }
445
446    #[test]
447    fn default_reference_is_invalid() {
448        assert!(!Reference::default().is_valid());
449    }
450
451    #[test_case("", Error::Empty; "Empty reference")]
452    #[test_case("/", Error::Empty; "Single slash")]
453    #[test_case("//", Error::DoubleOrTrailingSlash; "Double slash")]
454    #[test_case("/a//b", Error::DoubleOrTrailingSlash; "Double slash in middle")]
455    #[test_case("/a/b/", Error::DoubleOrTrailingSlash; "Trailing slash")]
456    #[test_case("/~3", Error::InvalidEscapeSequence; "Tilde must be followed by 0 or 1 only")]
457    #[test_case("/testing~something", Error::InvalidEscapeSequence; "Tilde cannot be alone")]
458    #[test_case("/m~~0", Error::InvalidEscapeSequence; "Extra tilde before valid escape")]
459    #[test_case("/a~", Error::InvalidEscapeSequence; "Tilde cannot be followed by nothing")]
460    fn invalid_references(input: &str, error: Error) {
461        let reference = Reference::new(input);
462        assert!(!reference.is_valid());
463        assert_eq!(error.to_string(), reference.error());
464    }
465
466    #[test_case("key")]
467    #[test_case("kind")]
468    #[test_case("name")]
469    #[test_case("name/with/slashes")]
470    #[test_case("name~0~1with-what-looks-like-escape-sequences")]
471    fn plain_reference_syntax(input: &str) {
472        let reference = Reference::new(input);
473        assert!(reference.is_valid());
474        assert_eq!(input, reference.to_string());
475        assert_eq!(
476            input,
477            reference
478                .component(0)
479                .expect("Failed to get first component")
480        );
481        assert_eq!(1, reference.depth());
482    }
483
484    #[test_case("/key", "key")]
485    #[test_case("/kind", "kind")]
486    #[test_case("/name", "name")]
487    #[test_case("/custom", "custom")]
488    fn pointer_syntax(input: &str, path: &str) {
489        let reference = Reference::new(input);
490        assert!(reference.is_valid());
491        assert_eq!(input, reference.to_string());
492        assert_eq!(
493            path,
494            reference
495                .component(0)
496                .expect("Failed to get first component")
497        );
498        assert_eq!(1, reference.depth())
499    }
500
501    #[test_case("/a/b", 2, 0, "a")]
502    #[test_case("/a/b", 2, 1, "b")]
503    #[test_case("/a~1b/c", 2, 0, "a/b")]
504    #[test_case("/a~1b/c", 2, 1, "c")]
505    #[test_case("/a/10/20/30x", 4, 1, "10")]
506    #[test_case("/a/10/20/30x", 4, 2, "20")]
507    #[test_case("/a/10/20/30x", 4, 3, "30x")]
508    fn handles_subcomponents(input: &str, len: usize, index: usize, expected_name: &str) {
509        let reference = Reference::new(input);
510        assert!(reference.is_valid());
511        assert_eq!(input, reference.input);
512        assert_eq!(len, reference.depth());
513        assert_eq!(expected_name, reference.component(index).unwrap());
514    }
515
516    #[test]
517    fn can_handle_invalid_index_requests() {
518        let reference = Reference::new("/a/b/c");
519        assert!(reference.is_valid());
520        assert!(reference.component(0).is_some());
521        assert!(reference.component(1).is_some());
522        assert!(reference.component(2).is_some());
523        assert!(reference.component(3).is_none());
524    }
525
526    #[test_case("/a/b", "/~1a~1b")]
527    #[test_case("a", "a")]
528    #[test_case("a~1b", "a~1b")]
529    #[test_case("/a~1b", "/~1a~01b")]
530    #[test_case("/a~0b", "/~1a~00b")]
531    #[test_case("", "")]
532    #[test_case("/", "/~1")]
533    fn attribute_name_equality(name: &str, reference: &str) {
534        let as_name = AttributeName::new(name.to_owned());
535        let reference = Reference::new(reference);
536        assert_eq!(Reference::from(as_name), reference);
537    }
538
539    #[test]
540    fn is_kind() {
541        assert!(Reference::new("/kind").is_kind());
542        assert!(Reference::new("kind").is_kind());
543        assert!(Reference::from(AttributeName::new("kind".to_owned())).is_kind());
544
545        assert!(!Reference::from(AttributeName::new("/kind".to_owned())).is_kind());
546        assert!(!Reference::new("foo").is_kind());
547    }
548}