pub const CORE_TYPES: &[&str] = &["int", "float", "bool", "null", "b64", "str", "map"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Constraint {
Pattern(String),
Range(Option<String>, Option<String>),
Enum(Vec<String>),
Length(Box<Constraint>),
Span(String),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UnionAlt {
pub name: String,
pub constraints: Vec<Constraint>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Annotation {
pub type_name: String,
pub union: Vec<UnionAlt>,
pub map_value: Option<String>,
pub constraints: Vec<Constraint>,
pub unit: Option<String>,
pub provenance: Option<String>,
}
pub(crate) const RE_SEPS: &[char] = &[':', ';', '%', '~', '@', '#'];
fn is_seg_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
}
fn is_unit_char(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '*' | '/' | '^' | '~' | '-')
}
pub fn parse_annotation(s: &str) -> Option<Annotation> {
let s = s.strip_prefix('!')?;
let cs: Vec<char> = s.chars().collect();
let mut i = 0;
let mut a = Annotation::default();
let first = read_seg(&cs, &mut i)?;
if first == "map" && cs.get(i) == Some(&'<') {
i += 1;
let v = read_seg(&cs, &mut i)?;
if cs.get(i) != Some(&'>') {
return None;
}
i += 1;
a.type_name = "map".into();
a.map_value = Some(v);
} else if CORE_TYPES.contains(&first.as_str()) {
a.type_name = first;
} else {
let mut path = first;
while cs.get(i) == Some(&'/') && cs.get(i + 1).is_some_and(|c| is_seg_char(*c)) {
i += 1;
let seg = read_seg(&cs, &mut i)?;
path.push('/');
path.push_str(&seg);
}
a.type_name = path;
}
while i < cs.len() {
match cs[i] {
'[' => {
let c = read_range(&cs, &mut i)?;
push_constraint(&mut a, c);
}
'{' => {
let c = read_enum(&cs, &mut i)?;
push_constraint(&mut a, c);
}
'/' => {
let c = read_pattern(&cs, &mut i)?;
push_constraint(&mut a, c);
}
'#' => {
i += 1;
let inner = match cs.get(i) {
Some('[') => read_range(&cs, &mut i)?,
Some('{') => read_enum(&cs, &mut i)?,
_ => return None,
};
push_constraint(&mut a, Constraint::Length(Box::new(inner)));
}
'(' => {
i += 1;
loop {
match cs.get(i) {
Some(')') => {
i += 1;
break;
}
Some('/') => {
let c = read_pattern(&cs, &mut i)?;
push_constraint(&mut a, c);
}
Some('[') => {
let c = read_range(&cs, &mut i)?;
push_constraint(&mut a, c);
}
Some('{') => {
let c = read_enum(&cs, &mut i)?;
push_constraint(&mut a, c);
}
Some('#') => {
i += 1;
let inner = match cs.get(i) {
Some('[') => read_range(&cs, &mut i)?,
Some('{') => read_enum(&cs, &mut i)?,
_ => return None,
};
push_constraint(&mut a, Constraint::Length(Box::new(inner)));
}
Some('.') => {
if cs.get(i + 1) != Some(&'.') {
return None;
}
let start = i;
i += 2;
while i < cs.len() && cs[i].is_ascii_alphabetic() {
i += 1;
}
let base: String = cs[start..i].iter().collect();
if base == "..lex" && cs.get(i) == Some(&'[') {
while i < cs.len() && cs[i] != ']' {
i += 1;
}
if i == cs.len() {
return None;
}
i += 1;
}
push_constraint(
&mut a,
Constraint::Span(cs[start..i].iter().collect()),
);
}
_ => return None,
}
}
}
':' => {
i += 1;
let start = i;
while i < cs.len() && is_unit_char(cs[i]) {
i += 1;
}
if i == start {
return None;
}
let u: String = cs[start..i].iter().collect();
crate::unit::canonicalize(&u)?;
a.unit = Some(u);
}
'?' => {
i += 1;
let start = i;
while i < cs.len() && !matches!(cs[i], ' ' | '\t') {
i += 1;
}
a.provenance = Some(cs[start..i].iter().collect());
}
' ' | '\t' => {
let mut any = false;
while i < cs.len() {
while i < cs.len() && matches!(cs[i], ' ' | '\t') {
i += 1;
}
if i >= cs.len() {
break;
}
let c = read_re_literal(&cs, &mut i)?;
if i < cs.len() && !matches!(cs[i], ' ' | '\t') {
return None;
}
push_constraint(&mut a, c);
any = true;
}
if !any {
return None;
}
}
'|' => {
i += 1;
let mut path = read_seg(&cs, &mut i)?;
while cs.get(i) == Some(&'/') && cs.get(i + 1).is_some_and(|c| is_seg_char(*c)) {
i += 1;
let seg = read_seg(&cs, &mut i)?;
path.push('/');
path.push_str(&seg);
}
a.union.push(UnionAlt {
name: path,
constraints: Vec::new(),
});
}
_ => return None,
}
}
Some(a)
}
fn push_constraint(a: &mut Annotation, c: Constraint) {
match a.union.last_mut() {
Some(alt) => alt.constraints.push(c),
None => a.constraints.push(c),
}
}
fn read_seg(cs: &[char], i: &mut usize) -> Option<String> {
let start = *i;
while *i < cs.len() && is_seg_char(cs[*i]) {
*i += 1;
}
if *i == start {
return None;
}
Some(cs[start..*i].iter().collect())
}
fn read_range(cs: &[char], i: &mut usize) -> Option<Constraint> {
debug_assert_eq!(cs[*i], '[');
*i += 1;
let start = *i;
while *i < cs.len() && cs[*i] != ']' {
*i += 1;
}
if *i == cs.len() {
return None;
}
let body: String = cs[start..*i].iter().collect();
*i += 1;
let (lo, hi) = body.split_once(',')?;
let opt = |s: &str| {
if s.is_empty() {
None
} else {
Some(s.to_string())
}
};
Some(Constraint::Range(opt(lo), opt(hi)))
}
fn read_enum(cs: &[char], i: &mut usize) -> Option<Constraint> {
debug_assert_eq!(cs[*i], '{');
*i += 1;
let start = *i;
while *i < cs.len() && cs[*i] != '}' {
*i += 1;
}
if *i == cs.len() {
return None;
}
let body: String = cs[start..*i].iter().collect();
*i += 1;
if body.is_empty() {
return None;
}
Some(Constraint::Enum(
body.split(',').map(str::to_string).collect(),
))
}
fn read_re_literal(cs: &[char], i: &mut usize) -> Option<Constraint> {
if cs.get(*i) != Some(&'r') || cs.get(*i + 1) != Some(&'e') {
return None;
}
let sep = *cs.get(*i + 2)?;
if !RE_SEPS.contains(&sep) {
return None;
}
*i += 3;
let mut body = String::new();
let mut esc = false;
loop {
let c = *cs.get(*i)?; *i += 1;
if c == sep {
return Some(Constraint::Pattern(body));
}
if c == '\'' {
return None;
}
if esc {
body.push(c);
esc = false;
} else if c == '\\' {
body.push('\\');
esc = true;
} else if c == '/' {
body.push_str("\\/");
} else {
body.push(c);
}
}
}
fn read_pattern(cs: &[char], i: &mut usize) -> Option<Constraint> {
debug_assert_eq!(cs[*i], '/');
*i += 1;
let mut body = String::new();
while *i < cs.len() {
match cs[*i] {
'\'' => return None,
'\\' => {
body.push('\\');
*i += 1;
if *i < cs.len() {
if cs[*i] == '\'' {
return None;
}
body.push(cs[*i]);
*i += 1;
}
}
'/' => {
*i += 1;
return Some(Constraint::Pattern(body));
}
c => {
body.push(c);
*i += 1;
}
}
}
None }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Item {
Anno(Box<Annotation>),
Named(String),
Constraint(Constraint),
}
pub fn parse_constraint_items(s: &str) -> Option<Vec<Item>> {
let cs: Vec<char> = s.chars().collect();
let mut i = 0;
let mut items = Vec::new();
loop {
while i < cs.len() && (cs[i] == ' ' || cs[i] == '\t') {
i += 1;
}
if i >= cs.len() {
break;
}
match cs[i] {
'/' => items.push(Item::Constraint(read_pattern(&cs, &mut i)?)),
'[' => items.push(Item::Constraint(read_range(&cs, &mut i)?)),
'{' => items.push(Item::Constraint(read_enum(&cs, &mut i)?)),
'#' => {
i += 1;
let inner = match cs.get(i) {
Some('[') => read_range(&cs, &mut i)?,
Some('{') => read_enum(&cs, &mut i)?,
_ => return None,
};
items.push(Item::Constraint(Constraint::Length(Box::new(inner))));
}
'.' => {
if cs.get(i + 1) != Some(&'.') {
return None;
}
let start = i;
while i < cs.len() && cs[i] != ' ' && cs[i] != '\t' {
i += 1;
}
items.push(Item::Constraint(Constraint::Span(
cs[start..i].iter().collect(),
)));
}
'!' => {
let start = i;
while i < cs.len() && cs[i] != ' ' && cs[i] != '\t' {
i += 1;
}
let tok: String = cs[start..i].iter().collect();
let a = parse_annotation(&tok)?;
if a.provenance.is_some() {
return None; }
items.push(Item::Anno(Box::new(a)));
}
'&' => {
i += 1;
let name = read_seg(&cs, &mut i)?;
items.push(Item::Named(name));
}
'r' => {
if i > 0 && !matches!(cs[i - 1], ' ' | '\t') {
return None;
}
let c = read_re_literal(&cs, &mut i)?;
if i < cs.len() && !matches!(cs[i], ' ' | '\t') {
return None;
}
items.push(Item::Constraint(c));
}
_ => return None,
}
}
Some(items)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_grammar_in_annotations() {
assert!(parse_annotation("!float:km").is_some());
assert!(parse_annotation("!float:m*s^-1").is_some());
assert!(parse_annotation("!float:kft").is_some());
assert!(parse_annotation("!float:~usd").is_none());
assert!(parse_annotation("!float:~EURO").is_none());
}
#[test]
fn union_alternative_constraints() {
let a = parse_annotation("!null|int[1,3600]").unwrap();
assert!(a.constraints.is_empty());
assert_eq!(a.union.len(), 1);
assert_eq!(a.union[0].name, "int");
assert_eq!(a.union[0].constraints.len(), 1);
let c = parse_annotation("!null(/^$/)|int(/^-?[0-9]+$/..num[1,3600])").unwrap();
assert_eq!(c.constraints.len(), 1); assert_eq!(c.union[0].constraints.len(), 3); assert!(matches!(
&c.union[0].constraints[1],
Constraint::Span(s) if s == "..num"
));
}
#[test]
fn re_literals() {
let a = parse_annotation("!str re~^https://[a-z.]+/.*~").unwrap();
assert_eq!(
a.constraints,
vec![Constraint::Pattern(r"^https:\/\/[a-z.]+\/.*".into())]
);
let b = parse_annotation(r"!str re~a\/b~").unwrap();
assert_eq!(b.constraints, vec![Constraint::Pattern(r"a\/b".into())]);
for sep in [':', ';', '%', '~', '@', '#'] {
assert!(parse_annotation(&format!("!str re{sep}abc{sep}")).is_some());
}
let u = parse_annotation("!null|str re:^a$:").unwrap();
assert!(u.constraints.is_empty());
assert_eq!(
u.union[0].constraints,
vec![Constraint::Pattern("^a$".into())]
);
let p = parse_annotation("!str?src re:^a$:").unwrap();
assert_eq!(p.provenance.as_deref(), Some("src"));
assert_eq!(p.constraints.len(), 1);
}
#[test]
fn re_literal_boundaries() {
assert!(parse_annotation("!strre:a:").is_none());
assert!(parse_constraint_items("re:a:{x,y}").is_none());
assert!(parse_constraint_items("[1,2]re:a:").is_none());
assert!(parse_constraint_items("re:a b: ..lex").is_some());
assert!(parse_constraint_items("..num re%[0-9]+%").is_some());
assert!(parse_annotation("!str re:^a$").is_none());
assert!(parse_annotation("!str re:it's:").is_none());
let a = parse_annotation(r"!str re:a\:").unwrap();
assert_eq!(a.constraints, vec![Constraint::Pattern("a\\".into())]);
}
#[test]
fn pattern_rejects_apostrophe() {
assert!(parse_annotation("!str/it's/").is_none());
assert!(parse_annotation(r"!str/it\'s/").is_none());
assert!(parse_constraint_items("/it's/ ..lex").is_none());
assert!(parse_annotation("!str/its/").is_some());
}
}