#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Clause {
Unique(Vec<String>),
Ref {
field: String,
target_arr: String,
target_field: String,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Header {
pub groups: Vec<Vec<Clause>>,
pub min: Option<u64>,
pub max: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Collection {
pub array: String,
pub header: Header,
}
pub fn tokens(s: &str) -> Vec<&str> {
let mut out = Vec::new();
let b = s.as_bytes();
let (mut start, mut i, mut q) = (0usize, 0usize, false);
while i < b.len() {
match b[i] {
b'"' => {
if q && b.get(i + 1) == Some(&b'"') {
i += 1;
} else {
q = !q;
}
}
b' ' | b'\t' if !q => {
if start < i {
out.push(&s[start..i]);
}
start = i + 1;
}
_ => {}
}
i += 1;
}
if start < b.len() {
out.push(&s[start..]);
}
out
}
fn split_q(s: &str, delim: u8) -> Vec<&str> {
let mut out = Vec::new();
let b = s.as_bytes();
let (mut start, mut i, mut q) = (0usize, 0usize, false);
while i < b.len() {
match b[i] {
b'"' => {
if q && b.get(i + 1) == Some(&b'"') {
i += 1;
} else {
q = !q;
}
}
x if x == delim && !q => {
out.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
i += 1;
}
out.push(&s[start..]);
out
}
fn all_digits(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
pub fn parse_header(toks: &[&str]) -> Option<Header> {
let mut h = Header::default();
for t in toks {
if let Some(n) = t.strip_prefix("min=").filter(|n| all_digits(n)) {
h.min = Some(n.parse().ok()?);
continue;
}
if let Some(n) = t.strip_prefix("max=").filter(|n| all_digits(n)) {
h.max = Some(n.parse().ok()?);
continue;
}
let mut group = Vec::new();
for spec in split_q(t, b'|') {
group.push(parse_spec(spec)?);
}
h.groups.push(group);
}
Some(h)
}
fn parse_spec(s: &str) -> Option<Clause> {
if s.ends_with("=!") {
let mut names = Vec::new();
for part in split_q(s, b',') {
names.push(parse_name(part.strip_suffix("=!")?, false)?);
}
return Some(Clause::Unique(names));
}
let parts = split_q(s, b'=');
let [field, path] = parts.as_slice() else {
return None;
};
let (target_arr, target_field) = parse_fk_path(path)?;
Some(Clause::Ref {
field: parse_name(field, false)?,
target_arr,
target_field,
})
}
fn parse_name(s: &str, allow_reserved: bool) -> Option<String> {
if let Some(rest) = s.strip_prefix('"') {
let inner = rest.strip_suffix('"')?;
if inner.is_empty() || rest.len() < 2 {
return None;
}
return Some(inner.replace("\"\"", "\""));
}
let ok = bare_ok(s) && (allow_reserved || (s != "min" && s != "max"));
ok.then(|| s.to_string())
}
fn bare_ok(s: &str) -> bool {
let b = s.as_bytes();
!b.is_empty()
&& (b[0].is_ascii_alphabetic() || b[0] == b'_')
&& b[1..]
.iter()
.all(|c| c.is_ascii_alphanumeric() || *c == b'_')
}
fn render_name(n: &str) -> String {
if bare_ok(n) {
n.to_string()
} else {
format!("\"{}\"", n.replace('"', "\"\""))
}
}
fn parse_fk_path(s: &str) -> Option<(String, String)> {
let i = s.find("/*::")?;
let (arr, field) = (&s[..i], &s[i + 4..]);
let steps: Vec<&str> = arr.strip_prefix('/')?.split('/').collect();
let (last, init) = steps.split_last()?;
if !init.iter().all(|st| !st.is_empty() && !st.starts_with('@')) {
return None;
}
let name = last.strip_prefix('@')?;
if name.is_empty() {
return None;
}
Some((arr.to_string(), parse_name(field, true)?))
}
pub fn render_compiled(h: &Header) -> String {
let mut parts: Vec<String> = h
.groups
.iter()
.map(|g| g.iter().map(render_clause).collect::<Vec<_>>().join("|"))
.collect();
if let Some(n) = h.min {
parts.push(format!("[min={n}]"));
}
if let Some(n) = h.max {
parts.push(format!("[max={n}]"));
}
parts.join(" ")
}
fn render_clause(c: &Clause) -> String {
match c {
Clause::Unique(names) => format!(
"[unique::{}]",
names
.iter()
.map(|n| render_name(n))
.collect::<Vec<_>>()
.join(",")
),
Clause::Ref {
field,
target_arr,
target_field,
} => format!(
"[ref::{}={target_arr}/*::{}]",
render_name(field),
render_name(target_field)
),
}
}
pub fn parse_compiled(s: &str) -> Option<Header> {
let mut h = Header::default();
for t in tokens(s) {
let mut group = Vec::new();
for c in split_q(t, b'|') {
let inner = c.strip_prefix('[')?.strip_suffix(']')?;
if let Some(r) = inner.strip_prefix("unique::") {
let names = split_q(r, b',')
.iter()
.map(|n| parse_name(n, true))
.collect::<Option<Vec<_>>>()?;
group.push(Clause::Unique(names));
} else if let Some(r) = inner.strip_prefix("ref::") {
let parts = split_q(r, b'=');
let [field, path] = parts.as_slice() else {
return None;
};
let (target_arr, target_field) = parse_fk_path(path)?;
group.push(Clause::Ref {
field: parse_name(field, true)?,
target_arr,
target_field,
});
} else if let Some(n) = inner.strip_prefix("min=").filter(|n| all_digits(n)) {
h.min = Some(n.parse().ok()?);
} else if let Some(n) = inner.strip_prefix("max=").filter(|n| all_digits(n)) {
h.max = Some(n.parse().ok()?);
} else {
return None;
}
}
if !group.is_empty() {
h.groups.push(group);
}
}
Some(h)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_roundtrip() {
let h = parse_header(&["id=!|host=!,port=!", "min=1", "max=50"]).unwrap();
assert_eq!(h.min, Some(1));
assert_eq!(h.max, Some(50));
assert_eq!(h.groups.len(), 1);
assert_eq!(h.groups[0].len(), 2);
let compiled = render_compiled(&h);
assert_eq!(
compiled,
"[unique::id]|[unique::host,port] [min=1] [max=50]"
);
assert_eq!(parse_compiled(&compiled).unwrap(), h);
}
#[test]
fn fk_clause() {
let h = parse_header(&["department=/@departments/*::name"]).unwrap();
assert_eq!(
h.groups[0][0],
Clause::Ref {
field: "department".into(),
target_arr: "/@departments".into(),
target_field: "name".into(),
}
);
assert_eq!(
render_compiled(&h),
"[ref::department=/@departments/*::name]"
);
let h = parse_header(&["d=/company/@departments/*::name"]).unwrap();
let Clause::Ref { target_arr, .. } = &h.groups[0][0] else {
panic!()
};
assert_eq!(target_arr, "/company/@departments");
}
#[test]
fn reserved_words() {
assert!(parse_header(&["min=!"]).is_none());
let h = parse_header(&["\"min\"=!"]).unwrap();
assert_eq!(h.groups[0][0], Clause::Unique(vec!["min".into()]));
assert_eq!(render_compiled(&h), "[unique::min]");
}
#[test]
fn malformed_specs() {
assert!(parse_header(&["host="]).is_none());
assert!(parse_header(&["host"]).is_none());
assert!(parse_header(&["=!"]).is_none());
assert!(parse_header(&["f=/@arr/name"]).is_none()); assert!(parse_header(&["f=/arr/*::name"]).is_none()); assert!(parse_header(&["min=1x"]).is_none()); assert!(parse_compiled("[key::/^a$/]").is_none()); }
}