1#![no_std]
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum ValueType {
17 String,
18 Number,
19 List,
20 Entity,
21 Any,
26}
27
28#[cfg(test)]
29mod value_type_tests {
30 use super::*;
31
32 #[test]
33 fn variants_are_pairwise_distinct() {
34 let all = [
35 ValueType::String,
36 ValueType::Number,
37 ValueType::List,
38 ValueType::Entity,
39 ValueType::Any,
40 ];
41 for i in 0..all.len() {
42 for j in (i + 1)..all.len() {
43 assert_ne!(
44 all[i], all[j],
45 "variants at index {i} and {j} are unexpectedly equal"
46 );
47 }
48 }
49 }
50
51 #[test]
52 fn value_type_is_copy_and_eq() {
53 let a = ValueType::Number;
54 let b = a; assert_eq!(a, b);
56 assert_ne!(ValueType::Number, ValueType::String);
57 }
58
59 #[test]
60 fn all_variants_are_const_constructible() {
61 const ALL: [ValueType; 5] = [
62 ValueType::String,
63 ValueType::Number,
64 ValueType::List,
65 ValueType::Entity,
66 ValueType::Any,
67 ];
68 assert_eq!(ALL.len(), 5);
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct PipeSpec {
82 pub name: &'static str,
83 pub input: ValueType,
84 pub output: ValueType,
85}
86
87pub const PIPE_SPECS: &[PipeSpec] = &[
105 PipeSpec {
106 name: "plural",
107 input: ValueType::Number,
108 output: ValueType::String,
109 },
110 PipeSpec {
111 name: "pluralize",
112 input: ValueType::Number,
113 output: ValueType::String,
114 },
115 PipeSpec {
116 name: "article",
117 input: ValueType::Any,
118 output: ValueType::String,
119 },
120 PipeSpec {
121 name: "join",
122 input: ValueType::List,
123 output: ValueType::String,
124 },
125 PipeSpec {
126 name: "ordinal",
127 input: ValueType::Number,
128 output: ValueType::String,
129 },
130 PipeSpec {
131 name: "words",
132 input: ValueType::Number,
133 output: ValueType::String,
134 },
135 PipeSpec {
136 name: "truncate",
137 input: ValueType::List,
138 output: ValueType::List,
139 },
140 PipeSpec {
141 name: "capitalize",
142 input: ValueType::Any,
143 output: ValueType::String,
144 },
145 PipeSpec {
146 name: "refer",
147 input: ValueType::Any,
148 output: ValueType::String,
149 },
150 PipeSpec {
151 name: "possessive",
152 input: ValueType::Any,
153 output: ValueType::String,
154 },
155 PipeSpec {
156 name: "verb",
157 input: ValueType::Any,
158 output: ValueType::String,
159 },
160 PipeSpec {
161 name: "syn",
162 input: ValueType::Any,
163 output: ValueType::String,
164 },
165 PipeSpec {
166 name: "relative",
167 input: ValueType::Number,
168 output: ValueType::String,
169 },
170 PipeSpec {
171 name: "since_last",
172 input: ValueType::Number,
173 output: ValueType::String,
174 },
175 PipeSpec {
176 name: "quantify",
177 input: ValueType::Number,
178 output: ValueType::String,
179 },
180 PipeSpec {
181 name: "proportion",
182 input: ValueType::Number,
183 output: ValueType::String,
184 },
185 PipeSpec {
186 name: "hedge",
187 input: ValueType::Number,
188 output: ValueType::String,
189 },
190 PipeSpec {
191 name: "negated",
192 input: ValueType::Any,
193 output: ValueType::String,
194 },
195 PipeSpec {
196 name: "choose",
197 input: ValueType::Any,
198 output: ValueType::String,
199 },
200 PipeSpec {
201 name: "demonstrative",
202 input: ValueType::Any,
203 output: ValueType::String,
204 },
205];
206
207#[allow(clippy::match_like_matches_macro)]
220pub const fn types_compatible(actual: ValueType, expected: ValueType) -> bool {
221 match (actual, expected) {
222 (ValueType::Any, _) | (_, ValueType::Any) => true,
223 (ValueType::String, ValueType::String) => true,
224 (ValueType::Number, ValueType::Number) => true,
225 (ValueType::List, ValueType::List) => true,
226 (ValueType::Entity, ValueType::Entity) => true,
227 _ => false,
228 }
229}
230
231pub const fn pipe_spec(name: &str) -> Option<&'static PipeSpec> {
234 let mut i = 0;
235 while i < PIPE_SPECS.len() {
236 if byte_eq(PIPE_SPECS[i].name.as_bytes(), name.as_bytes()) {
237 return Some(&PIPE_SPECS[i]);
238 }
239 i += 1;
240 }
241 None
242}
243
244pub const fn schema_lookup(schema: &[(&str, ValueType)], slot: &str) -> Option<ValueType> {
251 let mut i = 0;
252 while i < schema.len() {
253 if byte_eq(schema[i].0.as_bytes(), slot.as_bytes()) {
254 return Some(schema[i].1);
255 }
256 i += 1;
257 }
258 None
259}
260
261pub(crate) const fn byte_eq(a: &[u8], b: &[u8]) -> bool {
264 if a.len() != b.len() {
265 return false;
266 }
267 let mut i = 0;
268 while i < a.len() {
269 if a[i] != b[i] {
270 return false;
271 }
272 i += 1;
273 }
274 true
275}
276
277#[cfg(test)]
278mod pipe_spec_tests {
279 use super::*;
280
281 #[test]
282 fn all_twenty_pipes_are_registered() {
283 assert_eq!(PIPE_SPECS.len(), 20);
284 }
285
286 #[test]
287 fn pluralize_is_number_to_string() {
288 let p = pipe_spec("pluralize").expect("pluralize must be registered");
289 assert_eq!(p.input, ValueType::Number);
290 assert_eq!(p.output, ValueType::String);
291 }
292
293 #[test]
294 fn truncate_is_list_to_list_for_chain_compatibility() {
295 let p = pipe_spec("truncate").expect("truncate must be registered");
296 assert_eq!(p.input, ValueType::List);
297 assert_eq!(p.output, ValueType::List, "truncate must chain into join");
298 }
299
300 #[test]
301 fn join_is_list_to_string() {
302 let p = pipe_spec("join").expect("join must be registered");
303 assert_eq!(p.input, ValueType::List);
304 assert_eq!(p.output, ValueType::String);
305 }
306
307 #[test]
308 fn refer_is_any_to_string() {
309 let p = pipe_spec("refer").expect("refer must be registered");
310 assert_eq!(p.input, ValueType::Any);
311 assert_eq!(p.output, ValueType::String);
312 }
313
314 #[test]
315 fn possessive_is_any_to_string() {
316 let p = pipe_spec("possessive").expect("possessive must be registered");
317 assert_eq!(p.input, ValueType::Any);
318 assert_eq!(p.output, ValueType::String);
319 }
320
321 #[test]
322 fn unknown_pipe_lookup_returns_none() {
323 assert!(pipe_spec("nonexistent").is_none());
324 }
325
326 #[test]
327 fn pipe_spec_lookup_is_const_evaluable() {
328 const SPEC: Option<&'static PipeSpec> = pipe_spec("pluralize");
329 assert!(
333 matches!(SPEC, Some(s) if s.input == ValueType::Number && s.output == ValueType::String)
334 );
335 }
336
337 #[test]
338 fn pipe_spec_unknown_is_const_evaluable() {
339 const MISSING: Option<&'static PipeSpec> = pipe_spec("nonexistent_pipe_xyz");
340 assert!(MISSING.is_none());
341 }
342}
343
344#[cfg(test)]
345mod types_compatible_tests {
346 use super::*;
347
348 #[test]
349 fn any_matches_every_concrete_type() {
350 assert!(types_compatible(ValueType::Any, ValueType::Number));
351 assert!(types_compatible(ValueType::Number, ValueType::Any));
352 assert!(types_compatible(ValueType::Any, ValueType::Any));
353 }
354
355 #[test]
356 fn same_concrete_types_match() {
357 assert!(types_compatible(ValueType::Number, ValueType::Number));
358 assert!(types_compatible(ValueType::String, ValueType::String));
359 assert!(types_compatible(ValueType::List, ValueType::List));
360 assert!(types_compatible(ValueType::Entity, ValueType::Entity));
361 }
362
363 #[test]
364 fn distinct_concrete_types_reject() {
365 assert!(!types_compatible(ValueType::Number, ValueType::String));
366 assert!(!types_compatible(ValueType::List, ValueType::Number));
367 assert!(!types_compatible(ValueType::String, ValueType::Entity));
368 }
369
370 #[test]
371 fn compat_is_const_evaluable() {
372 const {
373 assert!(types_compatible(ValueType::Number, ValueType::Number));
374 assert!(!types_compatible(ValueType::Number, ValueType::List));
375 }
376 }
377
378 #[test]
379 fn compatibility_is_symmetric() {
380 assert_eq!(
384 types_compatible(ValueType::Number, ValueType::String),
385 types_compatible(ValueType::String, ValueType::Number),
386 );
387 assert_eq!(
388 types_compatible(ValueType::Number, ValueType::Any),
389 types_compatible(ValueType::Any, ValueType::Number),
390 );
391 assert_eq!(
392 types_compatible(ValueType::List, ValueType::Entity),
393 types_compatible(ValueType::Entity, ValueType::List),
394 );
395 }
396}
397
398#[cfg(test)]
399mod schema_lookup_tests {
400 use super::*;
401
402 const FIXTURE: &[(&str, ValueType)] = &[
403 ("name", ValueType::String),
404 ("count", ValueType::Number),
405 ("items", ValueType::List),
406 ("actor", ValueType::Entity),
407 ];
408
409 #[test]
410 fn found_key_returns_type() {
411 assert_eq!(schema_lookup(FIXTURE, "name"), Some(ValueType::String));
412 assert_eq!(schema_lookup(FIXTURE, "count"), Some(ValueType::Number));
413 assert_eq!(schema_lookup(FIXTURE, "items"), Some(ValueType::List));
414 assert_eq!(schema_lookup(FIXTURE, "actor"), Some(ValueType::Entity));
415 }
416
417 #[test]
418 fn missing_key_returns_none() {
419 assert_eq!(schema_lookup(FIXTURE, "absent"), None);
420 }
421
422 #[test]
423 fn empty_schema_returns_none() {
424 assert_eq!(schema_lookup(&[], "anything"), None);
425 }
426
427 #[test]
428 fn lookup_is_const_evaluable() {
429 const FOUND: Option<ValueType> = schema_lookup(FIXTURE, "count");
430 const MISSING: Option<ValueType> = schema_lookup(FIXTURE, "absent");
431 assert_eq!(FOUND, Some(ValueType::Number));
432 assert_eq!(MISSING, None);
433 }
434
435 #[test]
436 fn similar_but_longer_key_does_not_match() {
437 assert_eq!(schema_lookup(FIXTURE, "namer"), None);
439 assert_eq!(schema_lookup(FIXTURE, "nam"), None);
440 }
441
442 #[test]
443 fn duplicate_keys_return_first_match() {
444 const DUPE: &[(&str, ValueType)] = &[("x", ValueType::Number), ("x", ValueType::String)];
448 assert_eq!(schema_lookup(DUPE, "x"), Some(ValueType::Number));
449 }
450
451 #[test]
452 fn lookup_is_case_sensitive() {
453 assert_eq!(schema_lookup(FIXTURE, "Name"), None);
455 assert_eq!(schema_lookup(FIXTURE, "NAME"), None);
456 assert_eq!(schema_lookup(FIXTURE, "ACTOR"), None);
457 }
458}