#![no_std]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ValueType {
String,
Number,
List,
Entity,
Any,
}
#[cfg(test)]
mod value_type_tests {
use super::*;
#[test]
fn variants_are_pairwise_distinct() {
let all = [
ValueType::String,
ValueType::Number,
ValueType::List,
ValueType::Entity,
ValueType::Any,
];
for i in 0..all.len() {
for j in (i + 1)..all.len() {
assert_ne!(
all[i], all[j],
"variants at index {i} and {j} are unexpectedly equal"
);
}
}
}
#[test]
fn value_type_is_copy_and_eq() {
let a = ValueType::Number;
let b = a; assert_eq!(a, b);
assert_ne!(ValueType::Number, ValueType::String);
}
#[test]
fn all_variants_are_const_constructible() {
const ALL: [ValueType; 5] = [
ValueType::String,
ValueType::Number,
ValueType::List,
ValueType::Entity,
ValueType::Any,
];
assert_eq!(ALL.len(), 5);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PipeSpec {
pub name: &'static str,
pub input: ValueType,
pub output: ValueType,
}
pub const PIPE_SPECS: &[PipeSpec] = &[
PipeSpec {
name: "plural",
input: ValueType::Number,
output: ValueType::String,
},
PipeSpec {
name: "pluralize",
input: ValueType::Number,
output: ValueType::String,
},
PipeSpec {
name: "article",
input: ValueType::Any,
output: ValueType::String,
},
PipeSpec {
name: "join",
input: ValueType::List,
output: ValueType::String,
},
PipeSpec {
name: "ordinal",
input: ValueType::Number,
output: ValueType::String,
},
PipeSpec {
name: "words",
input: ValueType::Number,
output: ValueType::String,
},
PipeSpec {
name: "truncate",
input: ValueType::List,
output: ValueType::List,
},
PipeSpec {
name: "capitalize",
input: ValueType::Any,
output: ValueType::String,
},
PipeSpec {
name: "refer",
input: ValueType::Any,
output: ValueType::String,
},
PipeSpec {
name: "possessive",
input: ValueType::Any,
output: ValueType::String,
},
PipeSpec {
name: "verb",
input: ValueType::Any,
output: ValueType::String,
},
PipeSpec {
name: "syn",
input: ValueType::Any,
output: ValueType::String,
},
PipeSpec {
name: "relative",
input: ValueType::Number,
output: ValueType::String,
},
PipeSpec {
name: "since_last",
input: ValueType::Number,
output: ValueType::String,
},
PipeSpec {
name: "quantify",
input: ValueType::Number,
output: ValueType::String,
},
PipeSpec {
name: "proportion",
input: ValueType::Number,
output: ValueType::String,
},
PipeSpec {
name: "hedge",
input: ValueType::Number,
output: ValueType::String,
},
PipeSpec {
name: "negated",
input: ValueType::Any,
output: ValueType::String,
},
PipeSpec {
name: "choose",
input: ValueType::Any,
output: ValueType::String,
},
PipeSpec {
name: "demonstrative",
input: ValueType::Any,
output: ValueType::String,
},
];
#[allow(clippy::match_like_matches_macro)]
pub const fn types_compatible(actual: ValueType, expected: ValueType) -> bool {
match (actual, expected) {
(ValueType::Any, _) | (_, ValueType::Any) => true,
(ValueType::String, ValueType::String) => true,
(ValueType::Number, ValueType::Number) => true,
(ValueType::List, ValueType::List) => true,
(ValueType::Entity, ValueType::Entity) => true,
_ => false,
}
}
pub const fn pipe_spec(name: &str) -> Option<&'static PipeSpec> {
let mut i = 0;
while i < PIPE_SPECS.len() {
if byte_eq(PIPE_SPECS[i].name.as_bytes(), name.as_bytes()) {
return Some(&PIPE_SPECS[i]);
}
i += 1;
}
None
}
pub const fn schema_lookup(schema: &[(&str, ValueType)], slot: &str) -> Option<ValueType> {
let mut i = 0;
while i < schema.len() {
if byte_eq(schema[i].0.as_bytes(), slot.as_bytes()) {
return Some(schema[i].1);
}
i += 1;
}
None
}
pub(crate) const fn byte_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
#[cfg(test)]
mod pipe_spec_tests {
use super::*;
#[test]
fn all_twenty_pipes_are_registered() {
assert_eq!(PIPE_SPECS.len(), 20);
}
#[test]
fn pluralize_is_number_to_string() {
let p = pipe_spec("pluralize").expect("pluralize must be registered");
assert_eq!(p.input, ValueType::Number);
assert_eq!(p.output, ValueType::String);
}
#[test]
fn truncate_is_list_to_list_for_chain_compatibility() {
let p = pipe_spec("truncate").expect("truncate must be registered");
assert_eq!(p.input, ValueType::List);
assert_eq!(p.output, ValueType::List, "truncate must chain into join");
}
#[test]
fn join_is_list_to_string() {
let p = pipe_spec("join").expect("join must be registered");
assert_eq!(p.input, ValueType::List);
assert_eq!(p.output, ValueType::String);
}
#[test]
fn refer_is_any_to_string() {
let p = pipe_spec("refer").expect("refer must be registered");
assert_eq!(p.input, ValueType::Any);
assert_eq!(p.output, ValueType::String);
}
#[test]
fn possessive_is_any_to_string() {
let p = pipe_spec("possessive").expect("possessive must be registered");
assert_eq!(p.input, ValueType::Any);
assert_eq!(p.output, ValueType::String);
}
#[test]
fn unknown_pipe_lookup_returns_none() {
assert!(pipe_spec("nonexistent").is_none());
}
#[test]
fn pipe_spec_lookup_is_const_evaluable() {
const SPEC: Option<&'static PipeSpec> = pipe_spec("pluralize");
assert!(
matches!(SPEC, Some(s) if s.input == ValueType::Number && s.output == ValueType::String)
);
}
#[test]
fn pipe_spec_unknown_is_const_evaluable() {
const MISSING: Option<&'static PipeSpec> = pipe_spec("nonexistent_pipe_xyz");
assert!(MISSING.is_none());
}
}
#[cfg(test)]
mod types_compatible_tests {
use super::*;
#[test]
fn any_matches_every_concrete_type() {
assert!(types_compatible(ValueType::Any, ValueType::Number));
assert!(types_compatible(ValueType::Number, ValueType::Any));
assert!(types_compatible(ValueType::Any, ValueType::Any));
}
#[test]
fn same_concrete_types_match() {
assert!(types_compatible(ValueType::Number, ValueType::Number));
assert!(types_compatible(ValueType::String, ValueType::String));
assert!(types_compatible(ValueType::List, ValueType::List));
assert!(types_compatible(ValueType::Entity, ValueType::Entity));
}
#[test]
fn distinct_concrete_types_reject() {
assert!(!types_compatible(ValueType::Number, ValueType::String));
assert!(!types_compatible(ValueType::List, ValueType::Number));
assert!(!types_compatible(ValueType::String, ValueType::Entity));
}
#[test]
fn compat_is_const_evaluable() {
const {
assert!(types_compatible(ValueType::Number, ValueType::Number));
assert!(!types_compatible(ValueType::Number, ValueType::List));
}
}
#[test]
fn compatibility_is_symmetric() {
assert_eq!(
types_compatible(ValueType::Number, ValueType::String),
types_compatible(ValueType::String, ValueType::Number),
);
assert_eq!(
types_compatible(ValueType::Number, ValueType::Any),
types_compatible(ValueType::Any, ValueType::Number),
);
assert_eq!(
types_compatible(ValueType::List, ValueType::Entity),
types_compatible(ValueType::Entity, ValueType::List),
);
}
}
#[cfg(test)]
mod schema_lookup_tests {
use super::*;
const FIXTURE: &[(&str, ValueType)] = &[
("name", ValueType::String),
("count", ValueType::Number),
("items", ValueType::List),
("actor", ValueType::Entity),
];
#[test]
fn found_key_returns_type() {
assert_eq!(schema_lookup(FIXTURE, "name"), Some(ValueType::String));
assert_eq!(schema_lookup(FIXTURE, "count"), Some(ValueType::Number));
assert_eq!(schema_lookup(FIXTURE, "items"), Some(ValueType::List));
assert_eq!(schema_lookup(FIXTURE, "actor"), Some(ValueType::Entity));
}
#[test]
fn missing_key_returns_none() {
assert_eq!(schema_lookup(FIXTURE, "absent"), None);
}
#[test]
fn empty_schema_returns_none() {
assert_eq!(schema_lookup(&[], "anything"), None);
}
#[test]
fn lookup_is_const_evaluable() {
const FOUND: Option<ValueType> = schema_lookup(FIXTURE, "count");
const MISSING: Option<ValueType> = schema_lookup(FIXTURE, "absent");
assert_eq!(FOUND, Some(ValueType::Number));
assert_eq!(MISSING, None);
}
#[test]
fn similar_but_longer_key_does_not_match() {
assert_eq!(schema_lookup(FIXTURE, "namer"), None);
assert_eq!(schema_lookup(FIXTURE, "nam"), None);
}
#[test]
fn duplicate_keys_return_first_match() {
const DUPE: &[(&str, ValueType)] = &[("x", ValueType::Number), ("x", ValueType::String)];
assert_eq!(schema_lookup(DUPE, "x"), Some(ValueType::Number));
}
#[test]
fn lookup_is_case_sensitive() {
assert_eq!(schema_lookup(FIXTURE, "Name"), None);
assert_eq!(schema_lookup(FIXTURE, "NAME"), None);
assert_eq!(schema_lookup(FIXTURE, "ACTOR"), None);
}
}