Skip to main content

ephemeris_documents/
ownership.rs

1//! Ownership declarations and the claims they make on key paths.
2
3use std::fmt;
4use std::str::FromStr;
5
6use facet::Facet;
7use facet_value::{VObject, Value};
8
9use crate::error::OwnershipError;
10use crate::path::{KeyPath, PathTokens};
11
12/// A nonempty key path selected for management in a structured document.
13#[derive(Debug, Clone, Facet, PartialEq, Eq)]
14#[facet(proxy = Vec<String>)]
15pub struct ManagedPath(Vec<String>);
16
17impl ManagedPath {
18    /// A path of one top-level key.
19    #[must_use]
20    pub fn single(key: String) -> Self {
21        Self(vec![key])
22    }
23
24    /// The keys from the root down.
25    #[must_use]
26    pub fn segments(&self) -> &[String] {
27        &self.0
28    }
29
30    /// `value` wrapped in one object per key of the path.
31    pub(crate) fn nest(&self, value: Value) -> Value {
32        self.0.iter().rev().fold(value, |inner, key| {
33            let mut object = VObject::new();
34            object.insert(key, inner);
35            Value::from(object)
36        })
37    }
38}
39
40impl TryFrom<Vec<String>> for ManagedPath {
41    type Error = OwnershipError;
42
43    fn try_from(segments: Vec<String>) -> Result<Self, Self::Error> {
44        if segments.is_empty() {
45            Err(OwnershipError::PathEmpty)
46        } else {
47            Ok(Self(segments))
48        }
49    }
50}
51
52impl FromStr for ManagedPath {
53    type Err = OwnershipError;
54
55    fn from_str(text: &str) -> Result<Self, Self::Err> {
56        Ok(Self(PathTokens::parse(text)?))
57    }
58}
59
60impl fmt::Display for ManagedPath {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        KeyPath::from(self).fmt(f)
63    }
64}
65
66impl From<&ManagedPath> for KeyPath {
67    fn from(path: &ManagedPath) -> Self {
68        Self::from(path.segments())
69    }
70}
71
72impl From<&ManagedPath> for Vec<String> {
73    fn from(path: &ManagedPath) -> Self {
74        path.0.clone()
75    }
76}
77
78/// Ownership mode shared by structured document formats.
79#[derive(Debug, Clone, Facet, PartialEq, Eq)]
80#[facet(tag = "type", deny_unknown_fields)]
81#[repr(u8)]
82pub enum DocumentOwnership {
83    /// Manage the complete document, preserving only explicitly unmanaged
84    /// paths.
85    #[facet(rename = "whole")]
86    Whole {
87        /// Paths whose current values remain user-owned.
88        #[facet(default)]
89        unmanaged: Vec<ManagedPath>,
90    },
91    /// Manage only the explicitly selected paths.
92    #[facet(rename = "selected")]
93    Selected {
94        /// Paths this contribution manages.
95        owned: Vec<ManagedPath>,
96    },
97}
98
99impl Default for DocumentOwnership {
100    fn default() -> Self {
101        Self::Whole {
102            unmanaged: Vec::new(),
103        }
104    }
105}
106
107impl DocumentOwnership {
108    /// Manage the whole document except the `unmanaged` paths.
109    #[must_use]
110    pub fn whole<I: IntoIterator<Item = ManagedPath>>(unmanaged: I) -> Self {
111        Self::Whole {
112            unmanaged: unmanaged.into_iter().collect(),
113        }
114    }
115
116    /// Manage only the `owned` paths.
117    #[must_use]
118    pub fn selected<I: IntoIterator<Item = ManagedPath>>(owned: I) -> Self {
119        Self::Selected {
120            owned: owned.into_iter().collect(),
121        }
122    }
123
124    /// Whether this ownership leaves any part of the document user-owned.
125    pub(crate) const fn is_partial(&self) -> bool {
126        match self {
127            Self::Whole { unmanaged } => !unmanaged.is_empty(),
128            Self::Selected { owned: _ } => true,
129        }
130    }
131}
132
133/// What a document owner holds at one path.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub(crate) enum Claim {
136    /// Ephemeris manages the path and everything below it.
137    Owned,
138    /// The user keeps the path and everything below it.
139    Unmanaged,
140    /// Both sides hold parts below the path.
141    Mixed,
142}
143
144/// The claim held at every path no exception reaches.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub(crate) enum DefaultClaim {
147    /// Ephemeris manages the path and everything below it.
148    Owned,
149    /// The user keeps the path and everything below it.
150    Unmanaged,
151}
152
153impl DefaultClaim {
154    pub(crate) const fn is_owned(self) -> bool {
155        matches!(self, Self::Owned)
156    }
157
158    const fn opposite(self) -> Self {
159        match self {
160            Self::Owned => Self::Unmanaged,
161            Self::Unmanaged => Self::Owned,
162        }
163    }
164
165    const fn claim(self) -> Claim {
166        match self {
167            Self::Owned => Claim::Owned,
168            Self::Unmanaged => Claim::Unmanaged,
169        }
170    }
171}
172
173/// Paths where no path is a prefix of another.
174#[derive(Debug, Clone, PartialEq, Eq)]
175struct PathSet(Vec<ManagedPath>);
176
177impl TryFrom<&[ManagedPath]> for PathSet {
178    type Error = OwnershipError;
179
180    fn try_from(paths: &[ManagedPath]) -> Result<Self, Self::Error> {
181        for (index, left) in paths.iter().enumerate() {
182            if let Some(right) = paths
183                .iter()
184                .skip(index.saturating_add(1))
185                .find(|right| left.0.starts_with(&right.0) || right.0.starts_with(&left.0))
186            {
187                return Err(OwnershipError::PathOverlap {
188                    first: left.clone(),
189                    second: right.clone(),
190                });
191            }
192        }
193        Ok(Self(paths.to_vec()))
194    }
195}
196
197impl PathSet {
198    /// Whether a member equals `path` or is a prefix of it.
199    fn covers(&self, path: &[String]) -> bool {
200        self.0
201            .iter()
202            .any(|member| path.starts_with(member.segments()))
203    }
204
205    /// Whether a member lies strictly below `path`.
206    fn has_below(&self, path: &[String]) -> bool {
207        self.0.iter().any(|member| {
208            member.segments().len() > path.len() && member.segments().starts_with(path)
209        })
210    }
211}
212
213/// A validated ownership declaration: a default claim and the paths that
214/// take the opposite claim.
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub(crate) struct Ownership {
217    default: DefaultClaim,
218    exceptions: PathSet,
219}
220
221impl TryFrom<&DocumentOwnership> for Ownership {
222    type Error = OwnershipError;
223
224    fn try_from(ownership: &DocumentOwnership) -> Result<Self, Self::Error> {
225        let (default, paths) = match ownership {
226            DocumentOwnership::Whole { unmanaged } => (DefaultClaim::Owned, unmanaged),
227            DocumentOwnership::Selected { owned } => (DefaultClaim::Unmanaged, owned),
228        };
229        Ok(Self {
230            default,
231            exceptions: PathSet::try_from(paths.as_slice())?,
232        })
233    }
234}
235
236impl Ownership {
237    /// The claim at every path no exception reaches.
238    pub(crate) const fn default_claim(&self) -> DefaultClaim {
239        self.default
240    }
241
242    /// The claim at `path`.
243    pub(crate) fn claim(&self, path: &[String]) -> Claim {
244        if self.exceptions.covers(path) {
245            self.default.opposite().claim()
246        } else if self.exceptions.has_below(path) {
247            Claim::Mixed
248        } else {
249            self.default.claim()
250        }
251    }
252}
253
254#[cfg(test)]
255mod tests;