pub(super) struct BuiltinRule {
pub content: &'static str,
pub deps: &'static [&'static str],
}
pub(super) const SPACE_RULE: &str = r##"| " " | "\n"{1,2} [ \t]{0,20}"##;
pub(super) const PRIMITIVE_RULES: &[(&str, BuiltinRule)] = &[
(
"boolean",
BuiltinRule {
content: r##"("true" | "false")"##,
deps: &[],
},
),
(
"decimal-part",
BuiltinRule {
content: r##"[0-9]{1,16}"##,
deps: &[],
},
),
(
"integral-part",
BuiltinRule {
content: r##"[0] | [1-9] [0-9]{0,15}"##,
deps: &[],
},
),
(
"number",
BuiltinRule {
content: r##"("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)?"##,
deps: &["integral-part", "decimal-part"],
},
),
(
"integer",
BuiltinRule {
content: r##"("-"? integral-part)"##,
deps: &["integral-part"],
},
),
(
"value",
BuiltinRule {
content: r##"object | array | string | number | boolean | null"##,
deps: &["object", "array", "string", "number", "boolean", "null"],
},
),
(
"object",
BuiltinRule {
content: r##""{" space ( string ":" space value ("," space string ":" space value)* )? space "}""##,
deps: &["string", "value"],
},
),
(
"array",
BuiltinRule {
content: r##""[" space ( value ("," space value)* )? space "]""##,
deps: &["value"],
},
),
(
"uuid",
BuiltinRule {
content: r##""\"" [0-9a-fA-F]{8} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{12} "\"""##,
deps: &[],
},
),
(
"char",
BuiltinRule {
content: r##"[^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})"##,
deps: &[],
},
),
(
"string",
BuiltinRule {
content: r##""\"" char* "\"""##,
deps: &["char"],
},
),
(
"null",
BuiltinRule {
content: r##""null""##,
deps: &[],
},
),
];
pub(super) const STRING_FORMAT_RULES: &[(&str, BuiltinRule)] = &[
(
"date",
BuiltinRule {
content: r##"[0-9]{4} "-" ( "0" [1-9] | "1" [0-2] ) "-" ( "0" [1-9] | [1-2] [0-9] | "3" [0-1] )"##,
deps: &[],
},
),
(
"time",
BuiltinRule {
content: r##"([01] [0-9] | "2" [0-3]) ":" [0-5] [0-9] ":" [0-5] [0-9] ( "." [0-9]{3} )? ( "Z" | ( "+" | "-" ) ( [01] [0-9] | "2" [0-3] ) ":" [0-5] [0-9] )"##,
deps: &[],
},
),
(
"date-time",
BuiltinRule {
content: r##"date "T" time"##,
deps: &["date", "time"],
},
),
(
"date-string",
BuiltinRule {
content: r##""\"" date "\"""##,
deps: &["date"],
},
),
(
"time-string",
BuiltinRule {
content: r##""\"" time "\"""##,
deps: &["time"],
},
),
(
"date-time-string",
BuiltinRule {
content: r##""\"" date-time "\"""##,
deps: &["date-time"],
},
),
];
pub(super) fn builtin(name: &str) -> Option<&'static BuiltinRule> {
PRIMITIVE_RULES
.iter()
.chain(STRING_FORMAT_RULES.iter())
.find(|(n, _)| *n == name)
.map(|(_, r)| r)
}
pub(super) fn primitive(name: &str) -> Option<&'static BuiltinRule> {
PRIMITIVE_RULES
.iter()
.find(|(n, _)| *n == name)
.map(|(_, r)| r)
}
pub(super) fn is_reserved_name(name: &str) -> bool {
name == "root" || builtin(name).is_some()
}
pub(super) fn format_literal(literal: &str) -> String {
let mut out = String::with_capacity(literal.len() + 2);
out.push('"');
for c in literal.chars() {
match c {
'\r' => out.push_str("\\r"),
'\n' => out.push_str("\\n"),
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
c => out.push(c),
}
}
out.push('"');
out
}
pub(super) fn escape_in_range(c: char, out: &mut String) {
match c {
'\r' => out.push_str("\\r"),
'\n' => out.push_str("\\n"),
'"' => out.push_str("\\\""),
'-' => out.push_str("\\x2D"),
']' => out.push_str("\\]"),
'[' => out.push_str("\\["),
'\\' => out.push_str("\\\\"),
c => out.push(c),
}
}
pub(super) fn build_repetition(
item_rule: &str,
min_items: u64,
max_items: Option<u64>,
separator_rule: &str,
) -> String {
if max_items == Some(0) {
return String::new();
}
if min_items == 0 && max_items == Some(1) {
return format!("{item_rule}?");
}
if separator_rule.is_empty() {
if min_items == 1 && max_items.is_none() {
return format!("{item_rule}+");
}
if min_items == 0 && max_items.is_none() {
return format!("{item_rule}*");
}
let max = max_items.map(|m| m.to_string()).unwrap_or_default();
return format!("{item_rule}{{{min_items},{max}}}");
}
let inner = build_repetition(
&format!("({separator_rule} {item_rule})"),
min_items.saturating_sub(1),
max_items.map(|m| m - 1),
"",
);
let result = format!("{item_rule} {inner}");
if min_items == 0 {
format!("({result})?")
} else {
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repetition_matches_upstream_shapes() {
assert_eq!(build_repetition("a", 0, Some(0), ""), "");
assert_eq!(build_repetition("a", 0, Some(1), ""), "a?");
assert_eq!(build_repetition("a", 1, None, ""), "a+");
assert_eq!(build_repetition("a", 0, None, ""), "a*");
assert_eq!(build_repetition("a", 3, None, ""), "a{3,}");
assert_eq!(build_repetition("a", 1, Some(4), ""), "a{1,4}");
assert_eq!(
build_repetition("boolean", 0, None, "\",\" space"),
"(boolean (\",\" space boolean)*)?"
);
assert_eq!(
build_repetition("boolean", 2, None, "\",\" space"),
"boolean (\",\" space boolean)+"
);
assert_eq!(
build_repetition("boolean", 0, Some(2), "\",\" space"),
"(boolean (\",\" space boolean)?)?"
);
assert_eq!(
build_repetition("item", 3, Some(5), "\",\" space"),
"item (\",\" space item){2,4}"
);
}
#[test]
fn builtin_lookup_finds_both_tables() {
assert!(primitive("string").is_some());
assert!(primitive("date-string").is_none());
assert!(builtin("date-string").is_some());
assert!(is_reserved_name("root"));
assert!(is_reserved_name("number"));
assert!(!is_reserved_name("space"));
}
#[test]
fn literal_escaping() {
assert_eq!(format_literal(" \r \n \" \\ "), r##"" \r \n \" \\ ""##);
}
}