1use crate::read::Fold;
10use crate::registry::PropMeta;
11use crate::spec::PropSpec;
12
13pub trait Props: Sized {
19 const PROPS: &'static [PropMeta];
24
25 const PROP_SPECS: &'static [PropSpec];
27
28 #[doc(hidden)]
34 fn read_at(fold: &mut Fold<'_>, base: u16) -> Option<Self>;
35}
36
37pub 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
60pub 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 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
129const 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
141const 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 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 #[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 #[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}