launchdarkly_server_sdk_evaluation/contexts/
attribute_reference.rs1use 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#[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 PlainName,
78 Pointer(Vec<String>),
80 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 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 pub fn is_valid(&self) -> bool {
150 !matches!(&self.variant, Variant::Error(_))
151 }
152
153 pub fn error(&self) -> String {
156 match &self.variant {
157 Variant::Error(e) => e.to_string(),
158 _ => "".to_owned(),
159 }
160 }
161
162 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 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 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 !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 fn default() -> Self {
243 Reference::new("")
244 }
245}
246
247impl 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)]
271pub(crate) struct AttributeName(String);
275
276impl AttributeName {
277 #[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 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 prop_compose! {
331 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 pub(crate) fn any_valid_ref()(s in any_valid_ref_string()) -> Reference {
354 Reference::new(s)
355 }
356 }
357
358 prop_compose! {
359 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 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 #[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}