cratestack_core/schema/
composite_key.rs1pub fn parse_composite_id_attribute(raw: &str) -> Result<Vec<String>, String> {
11 let Some(inner) = raw
12 .strip_prefix("@@id(")
13 .and_then(|value| value.strip_suffix(')'))
14 else {
15 return Err(format!("unsupported composite id attribute `{raw}`"));
16 };
17
18 let Some(list) = inner
19 .trim()
20 .strip_prefix('[')
21 .and_then(|value| value.strip_suffix(']'))
22 else {
23 return Err(format!(
24 "composite id attribute `{raw}` must list fields as `@@id([field1, field2])`"
25 ));
26 };
27
28 let mut fields = Vec::new();
29 for part in list.split(',').map(str::trim) {
30 if part.is_empty() {
31 continue;
32 }
33 if !is_valid_field_name(part) {
34 return Err(format!(
35 "composite id attribute `{raw}` lists invalid field name `{part}`"
36 ));
37 }
38 if fields.contains(&part.to_owned()) {
39 return Err(format!(
40 "composite id attribute `{raw}` lists field `{part}` more than once"
41 ));
42 }
43 fields.push(part.to_owned());
44 }
45
46 if fields.len() < 2 {
47 return Err(format!(
48 "composite id attribute `{raw}` must list at least two fields; use a single-field `@id` instead"
49 ));
50 }
51
52 Ok(fields)
53}
54
55fn is_valid_field_name(value: &str) -> bool {
56 let mut chars = value.chars();
57 matches!(chars.next(), Some(first) if first.is_ascii_alphabetic() || first == '_')
58 && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
59}
60
61#[cfg(test)]
62mod tests {
63 use super::parse_composite_id_attribute;
64
65 #[test]
66 fn parses_two_fields() {
67 let fields = parse_composite_id_attribute("@@id([accountId, subject])").unwrap();
68 assert_eq!(fields, vec!["accountId".to_string(), "subject".to_string()]);
69 }
70
71 #[test]
72 fn rejects_missing_brackets() {
73 let error = parse_composite_id_attribute("@@id(accountId, subject)").unwrap_err();
74 assert!(error.contains("must list fields as"));
75 }
76
77 #[test]
78 fn rejects_single_field() {
79 let error = parse_composite_id_attribute("@@id([accountId])").unwrap_err();
80 assert!(error.contains("at least two fields"));
81 }
82
83 #[test]
84 fn rejects_duplicate_field() {
85 let error = parse_composite_id_attribute("@@id([accountId, accountId])").unwrap_err();
86 assert!(error.contains("more than once"));
87 }
88
89 #[test]
90 fn rejects_invalid_identifier() {
91 let error = parse_composite_id_attribute("@@id([account-id, subject])").unwrap_err();
92 assert!(error.contains("invalid field name"));
93 }
94}