Skip to main content

scientific_workflow/configuration/
parameter_path.rs

1//! Canonical identities for arbitrarily nested JSON parameters.
2
3use std::fmt;
4
5/// One non-empty JSON object path represented as unescaped key segments.
6#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
7pub struct ParameterPath {
8    segments: Box<[Box<str>]>,
9    identifier: Box<str>,
10}
11
12impl ParameterPath {
13    pub(crate) fn root(key: impl Into<Box<str>>) -> Self {
14        Self::from_segments(vec![key.into()]).expect("root parameter key is nonempty")
15    }
16
17    pub(crate) fn from_segments(segments: Vec<Box<str>>) -> Option<Self> {
18        if segments.is_empty() {
19            return None;
20        }
21        let identifier = encode_identifier(&segments).into_boxed_str();
22        Some(Self {
23            segments: segments.into_boxed_slice(),
24            identifier,
25        })
26    }
27
28    /// Parses a canonical non-root JSON Pointer.
29    pub fn parse(key: &str) -> Option<Self> {
30        let pointer = key.strip_prefix('/')?;
31        let segments = pointer
32            .split('/')
33            .map(decode_pointer_segment)
34            .collect::<Option<Vec<_>>>()?;
35        Self::from_segments(segments.into_iter().map(String::into_boxed_str).collect())
36    }
37
38    pub(crate) fn appended(&self, segment: impl Into<Box<str>>) -> Self {
39        let mut segments = self.segments.to_vec();
40        segments.push(segment.into());
41        Self::from_segments(segments).expect("appending a nonempty path segment remains valid")
42    }
43
44    pub fn segments(&self) -> impl ExactSizeIterator<Item = &str> {
45        self.segments.iter().map(AsRef::as_ref)
46    }
47
48    pub fn is_ancestor_of(&self, other: &Self) -> bool {
49        self.segments.len() < other.segments.len()
50            && self
51                .segments
52                .iter()
53                .zip(other.segments.iter())
54                .all(|(left, right)| left == right)
55    }
56
57    /// Stable external JSON Pointer identifier.
58    pub fn identifier(&self) -> &str {
59        &self.identifier
60    }
61
62    pub fn to_json_pointer(&self) -> String {
63        let mut output = String::new();
64        for segment in &self.segments {
65            output.push('/');
66            for character in segment.chars() {
67                match character {
68                    '~' => output.push_str("~0"),
69                    '/' => output.push_str("~1"),
70                    _ => output.push(character),
71                }
72            }
73        }
74        output
75    }
76}
77
78impl fmt::Display for ParameterPath {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        formatter.write_str(self.identifier())
81    }
82}
83
84fn encode_identifier(segments: &[Box<str>]) -> String {
85    let mut output = String::new();
86    for segment in segments {
87        output.push('/');
88        for character in segment.chars() {
89            match character {
90                '~' => output.push_str("~0"),
91                '/' => output.push_str("~1"),
92                _ => output.push(character),
93            }
94        }
95    }
96    output
97}
98
99fn decode_pointer_segment(segment: &str) -> Option<String> {
100    let mut output = String::with_capacity(segment.len());
101    let mut characters = segment.chars();
102    while let Some(character) = characters.next() {
103        if character != '~' {
104            output.push(character);
105            continue;
106        }
107        match characters.next()? {
108            '0' => output.push('~'),
109            '1' => output.push('/'),
110            _ => return None,
111        }
112    }
113    Some(output)
114}