Skip to main content

usage_config/
props.rs

1//! What `#[derive(usage::Config)]` generates, and how flattened groups compose.
2//!
3//! A derive expansion sees one struct. A settings struct that flattens another — pitchfork
4//! keeps eight groups in eight structs — therefore joins tables through this trait's
5//! associated const, the same way `usage::Cli` joins a flattened group's flags: the child
6//! declares its own slice, and the parent concatenates at compile time. Nothing is assembled
7//! at run time, and a prop's id is its position in the joined slice.
8
9use crate::read::Fold;
10use crate::registry::PropMeta;
11use crate::spec::PropSpec;
12
13/// A group of settings declared in code.
14///
15/// Implemented by `#[derive(usage::Config)]`, not by hand: the derive is what keeps
16/// [`Props::PROPS`] and [`Props::read_at`] describing the same fields in the same order,
17/// which is the invariant everything here leans on.
18pub trait Props: Sized {
19    /// This group's settings, in declaration order.
20    ///
21    /// A flattened child's props follow the parent's own, so an id is a position in the
22    /// parent's joined slice — which is why reading takes a `base`.
23    const PROPS: &'static [PropMeta];
24
25    /// Spec-only metadata parallel to [`Props::PROPS`].
26    const PROP_SPECS: &'static [PropSpec];
27
28    /// Read this group's fields from a fold, its props starting at `base`.
29    ///
30    /// `None` means a field could not be read and the fold has recorded why. Every field is
31    /// still visited first — the errors are a list, not the first thing found — so a caller
32    /// checks [`Fold::finish`] before treating `None` as anything but "already reported".
33    #[doc(hidden)]
34    fn read_at(fold: &mut Fold<'_>, base: u16) -> Option<Self>;
35}
36
37/// Join flattened groups' spec metadata in the same order as their properties.
38pub const fn concat_prop_specs<const N: usize>(groups: &[&[PropSpec]]) -> [PropSpec; N] {
39    let mut out = [PropSpec::EMPTY; N];
40    let mut at = 0;
41    let mut g = 0;
42    while g < groups.len() {
43        let group = groups[g];
44        let mut i = 0;
45        while i < group.len() {
46            out[at] = group[i];
47            at += 1;
48            i += 1;
49        }
50        g += 1;
51    }
52    assert!(
53        at == N,
54        "`N` must be the summed length of the groups, or property spec metadata would not \
55         line up with its setting"
56    );
57    out
58}
59
60/// Join groups of prop metadata into one slice, at compile time.
61///
62/// The settings counterpart of `usage_argv::spec::concat_flag_metas`, for the same reason: a
63/// flattened struct's props belong in the parent's registry, and the parent's macro expansion
64/// has only a type to reach them through.
65///
66/// `N` must be the summed length of `groups`. Two groups claiming the same *name* are refused
67/// here, at compile time — the parent and the struct it flattens each declared it, a collision
68/// neither expansion can see. A name is a key or an alias, because [`Registry::lookup`] checks
69/// both and takes the first match: an alias colliding with another group's key makes one of
70/// the two unreachable by that name, which is the same bug as a duplicate key and quieter.
71///
72/// [`Registry::lookup`]: crate::Registry::lookup
73pub const fn concat_props<const N: usize>(groups: &[&[PropMeta]]) -> [PropMeta; N] {
74    let mut out = [PropMeta::new("", crate::ty::Ty::Any); N];
75    let mut at = 0;
76    let mut g = 0;
77    while g < groups.len() {
78        let group = groups[g];
79        let mut i = 0;
80        while i < group.len() {
81            out[at] = group[i];
82            at += 1;
83            i += 1;
84        }
85        g += 1;
86    }
87    assert!(
88        at == N,
89        "`N` must be the summed length of the groups, or the registry would describe a \
90         setting that does not exist"
91    );
92    let mut a = 0;
93    while a < N {
94        let mut b = a + 1;
95        while b < N {
96            assert!(
97                !str_eq(out[a].key, out[b].key),
98                "two flattened groups declare the same setting key, so one of them could \
99                 never be reached: give one of them another key or prefix"
100            );
101            // Each one's key against the other's aliases, and then alias against alias. A
102            // lookup does not care which kind of name it matched, so neither can this.
103            assert!(
104                !names_any(out[a].key, out[b].aliases),
105                "one flattened group's setting key is another's alias, so a lookup for that \
106                 name could only ever reach one of them: rename one of the two"
107            );
108            assert!(
109                !names_any(out[b].key, out[a].aliases),
110                "one flattened group's setting key is another's alias, so a lookup for that \
111                 name could only ever reach one of them: rename one of the two"
112            );
113            let mut i = 0;
114            while i < out[a].aliases.len() {
115                assert!(
116                    !names_any(out[a].aliases[i], out[b].aliases),
117                    "two flattened groups declare the same alias, so a lookup for it could \
118                     only ever reach one of them: rename one of the two"
119                );
120                i += 1;
121            }
122            b += 1;
123        }
124        a += 1;
125    }
126    out
127}
128
129/// Whether `name` is one of `names`, in a const context.
130const fn names_any(name: &str, names: &[&str]) -> bool {
131    let mut i = 0;
132    while i < names.len() {
133        if str_eq(name, names[i]) {
134            return true;
135        }
136        i += 1;
137    }
138    false
139}
140
141/// Whether two strings are equal, in a const context.
142const fn str_eq(a: &str, b: &str) -> bool {
143    let a = a.as_bytes();
144    let b = b.as_bytes();
145    if a.len() != b.len() {
146        return false;
147    }
148    let mut i = 0;
149    while i < a.len() {
150        if a[i] != b[i] {
151            return false;
152        }
153        i += 1;
154    }
155    true
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::ty::Ty;
162
163    /// The refusals, exercised at run time.
164    ///
165    /// `concat_props` is a `const fn`, so in its real use — a `static` initializer — these
166    /// assertions are a compile error, which a test cannot observe. Called at run time the
167    /// same assertion panics, which one can.
168    mod one_name_reaches_one_setting {
169        use super::*;
170
171        #[test]
172        #[should_panic(expected = "same setting key")]
173        fn two_groups_cannot_declare_one_key() {
174            static A: &[PropMeta] = &[PropMeta::new("jobs", Ty::Uint)];
175            static B: &[PropMeta] = &[PropMeta::new("jobs", Ty::Uint)];
176            let _ = concat_props::<2>(&[A, B]);
177        }
178
179        #[test]
180        #[should_panic(expected = "is another's alias")]
181        fn an_alias_cannot_shadow_another_groups_key() {
182            static A: &[PropMeta] = &[PropMeta {
183                aliases: &["threads"],
184                ..PropMeta::new("jobs", Ty::Uint)
185            }];
186            static B: &[PropMeta] = &[PropMeta::new("threads", Ty::Uint)];
187            let _ = concat_props::<2>(&[A, B]);
188        }
189
190        /// The same collision found from the other side, which is a separate comparison.
191        #[test]
192        #[should_panic(expected = "is another's alias")]
193        fn a_key_cannot_be_shadowed_by_a_later_groups_alias() {
194            static A: &[PropMeta] = &[PropMeta::new("threads", Ty::Uint)];
195            static B: &[PropMeta] = &[PropMeta {
196                aliases: &["threads"],
197                ..PropMeta::new("jobs", Ty::Uint)
198            }];
199            let _ = concat_props::<2>(&[A, B]);
200        }
201
202        #[test]
203        #[should_panic(expected = "same alias")]
204        fn two_groups_cannot_declare_one_alias() {
205            static A: &[PropMeta] = &[PropMeta {
206                aliases: &["shared"],
207                ..PropMeta::new("jobs", Ty::Uint)
208            }];
209            static B: &[PropMeta] = &[PropMeta {
210                aliases: &["shared"],
211                ..PropMeta::new("threads", Ty::Uint)
212            }];
213            let _ = concat_props::<2>(&[A, B]);
214        }
215
216        /// And distinct names join, which is the case that has to keep working.
217        #[test]
218        fn distinct_names_join() {
219            static A: &[PropMeta] = &[PropMeta {
220                aliases: &["concurrency"],
221                ..PropMeta::new("jobs", Ty::Uint)
222            }];
223            static B: &[PropMeta] = &[PropMeta {
224                aliases: &["task.concurrency"],
225                ..PropMeta::new("task.jobs", Ty::Uint)
226            }];
227            let joined = concat_props::<2>(&[A, B]);
228            assert_eq!(joined[0].key, "jobs");
229            assert_eq!(joined[1].key, "task.jobs");
230        }
231    }
232
233    #[test]
234    fn groups_join_in_order_and_ids_are_positions() {
235        static OWN: &[PropMeta] = &[PropMeta::new("jobs", Ty::Uint)];
236        static CHILD: &[PropMeta] = &[
237            PropMeta::new("task.output", Ty::String),
238            PropMeta::new("task.jobs", Ty::Uint),
239        ];
240        const N: usize = OWN.len() + CHILD.len();
241        static JOINED: [PropMeta; N] = concat_props(&[OWN, CHILD]);
242        assert_eq!(JOINED[0].key, "jobs");
243        assert_eq!(JOINED[1].key, "task.output");
244        assert_eq!(JOINED[2].key, "task.jobs");
245    }
246
247    #[test]
248    fn spec_metadata_joins_in_the_same_order_as_props() {
249        static OWN: &[PropSpec] = &[PropSpec {
250            help_heading: Some("Performance"),
251            writes_to: None,
252            extensions: &[],
253        }];
254        static CHILD: &[PropSpec] = &[
255            PropSpec {
256                writes_to: Some("git"),
257                ..PropSpec::EMPTY
258            },
259            PropSpec::EMPTY,
260        ];
261        const N: usize = OWN.len() + CHILD.len();
262        static JOINED: [PropSpec; N] = concat_prop_specs(&[OWN, CHILD]);
263        assert_eq!(JOINED[0].help_heading, Some("Performance"));
264        assert_eq!(JOINED[1].writes_to, Some("git"));
265        assert_eq!(JOINED[2], PropSpec::EMPTY);
266    }
267}