mod constraint;
mod core;
mod error;
mod policy_definition;
mod policy_rule;
pub(super) use self::core::json_unescape;
pub use error::ParseError;
use alloc::format;
use alloc::string::ToString as _;
use ::core::num::NonZeroU32;
use crate::lexer::{Source, TokenKind};
use super::ast::{Constraint, FieldKind, OperatorKind, PolicyDefinition, PolicyRule};
use super::expr::ExprParser;
use self::core::Parser;
pub const MAX_COL: u32 = Parser::MAX_COL;
pub fn parse_policy_rule(source: &Source) -> Result<PolicyRule, ParseError> {
parse_policy_rule_with_max_col(source, None)
}
pub fn parse_policy_rule_with_max_col(
source: &Source,
max_col: Option<NonZeroU32>,
) -> Result<PolicyRule, ParseError> {
let mut parser = Parser::new_with_max_col(source, max_col.or(NonZeroU32::new(MAX_COL)))?;
let rule = parser.parse_policy_rule()?;
if parser.tok.0 != TokenKind::Eof {
return Err(ParseError::UnexpectedToken {
span: parser.tok.1.clone(),
expected: "end of input",
});
}
Ok(rule)
}
pub fn parse_policy_definition(source: &Source) -> Result<PolicyDefinition, ParseError> {
parse_policy_definition_with_max_col(source, None)
}
pub fn parse_policy_definition_with_max_col(
source: &Source,
max_col: Option<NonZeroU32>,
) -> Result<PolicyDefinition, ParseError> {
let mut parser = Parser::new_with_max_col(source, max_col.or(NonZeroU32::new(MAX_COL)))?;
let defn = parser.parse_policy_definition()?;
if parser.tok.0 != TokenKind::Eof {
return Err(ParseError::UnexpectedToken {
span: parser.tok.1.clone(),
expected: "end of input",
});
}
Ok(defn)
}
pub fn parse_constraint(source: &Source) -> Result<Constraint, ParseError> {
let mut parser = Parser::new(source)?;
let constraint = parser.parse_constraint()?;
if parser.tok.0 != TokenKind::Eof {
return Err(ParseError::UnexpectedToken {
span: parser.tok.1.clone(),
expected: "end of input",
});
}
Ok(constraint)
}
pub(super) fn is_template_expr(s: &str) -> bool {
s.starts_with('[') && s.ends_with(']') && !s.starts_with("[[")
}
fn tail(s: &str, prefix_len: usize) -> &str {
s.get(prefix_len..).unwrap_or_default()
}
fn unwrap(s: &str, prefix_len: usize, suffix_len: usize) -> &str {
let end = s.len().saturating_sub(suffix_len);
s.get(prefix_len..end).unwrap_or_default()
}
fn strip_matching_outer_quotes(s: &str) -> Option<&str> {
s.strip_prefix('\'')
.and_then(|inner| inner.strip_suffix('\''))
.or_else(|| {
s.strip_prefix('"')
.and_then(|inner| inner.strip_suffix('"'))
})
}
pub(super) fn classify_field(
text: &str,
span: &crate::lexer::Span,
) -> Result<FieldKind, ParseError> {
let lower = text.to_lowercase();
match lower.as_str() {
"type" => Ok(FieldKind::Type),
"id" => Ok(FieldKind::Id),
"kind" => Ok(FieldKind::Kind),
"name" => Ok(FieldKind::Name),
"location" => Ok(FieldKind::Location),
"fullname" => Ok(FieldKind::FullName),
"tags" => Ok(FieldKind::Tags),
"identity.type" => Ok(FieldKind::IdentityType),
_ if lower.starts_with("identity.") => Ok(FieldKind::IdentityField(
tail(text, "identity.".len()).into(),
)),
"apiversion" => Ok(FieldKind::ApiVersion),
_ if lower.starts_with("tags.") => Ok(FieldKind::Tag(tail(text, "tags.".len()).into())),
_ if lower.starts_with("tags[") && text.ends_with(']') => {
let tag = unwrap(text, "tags[".len(), "]".len()).trim();
let quoted = strip_matching_outer_quotes(tag);
if quoted.unwrap_or(tag).is_empty() {
return Err(ParseError::Custom {
span: span.clone(),
message: format!("empty tag name in field: {text:?}"),
});
}
let kind = quoted.map_or_else(
|| {
if text.contains("[*]") {
FieldKind::Alias(text.into())
} else {
FieldKind::Tag(tag.into())
}
},
|quoted_tag| FieldKind::Tag(quoted_tag.into()),
);
Ok(kind)
}
_ if is_template_expr(text) => {
let inner = unwrap(text, 1, 1);
let expr = ExprParser::parse_from_brackets(inner, span).map_err(|e| {
ParseError::ExprParse {
span: span.clone(),
message: e.to_string(),
}
})?;
Ok(FieldKind::Expr(expr))
}
_ => Ok(FieldKind::Alias(text.into())),
}
}
pub(super) fn parse_operator_kind(key: &str) -> Option<OperatorKind> {
match key {
"contains" => Some(OperatorKind::Contains),
"containskey" => Some(OperatorKind::ContainsKey),
"equals" => Some(OperatorKind::Equals),
"greater" => Some(OperatorKind::Greater),
"greaterorequals" => Some(OperatorKind::GreaterOrEquals),
"exists" => Some(OperatorKind::Exists),
"in" => Some(OperatorKind::In),
"less" => Some(OperatorKind::Less),
"lessorequals" => Some(OperatorKind::LessOrEquals),
"like" => Some(OperatorKind::Like),
"match" => Some(OperatorKind::Match),
"matchinsensitively" => Some(OperatorKind::MatchInsensitively),
"notcontains" => Some(OperatorKind::NotContains),
"notcontainskey" => Some(OperatorKind::NotContainsKey),
"notequals" => Some(OperatorKind::NotEquals),
"notin" => Some(OperatorKind::NotIn),
"notlike" => Some(OperatorKind::NotLike),
"notmatch" => Some(OperatorKind::NotMatch),
"notmatchinsensitively" => Some(OperatorKind::NotMatchInsensitively),
_ => None,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn dummy_span() -> crate::lexer::Span {
let source =
crate::lexer::Source::from_contents("<test>".into(), alloc::string::String::new())
.unwrap();
crate::lexer::Span {
source,
line: 1,
col: 1,
start: 0,
end: 0,
}
}
#[test]
fn test_is_template_expr() {
assert!(is_template_expr("[field('type')]"));
assert!(is_template_expr("[concat('a', 'b')]"));
assert!(!is_template_expr("[[escaped]"));
assert!(!is_template_expr("not-an-expr"));
assert!(!is_template_expr("[no-end-bracket"));
}
#[test]
fn test_classify_field_builtin_names() {
let span = dummy_span();
assert!(matches!(classify_field("type", &span), Ok(FieldKind::Type)));
assert!(matches!(classify_field("Type", &span), Ok(FieldKind::Type)));
assert!(matches!(classify_field("name", &span), Ok(FieldKind::Name)));
assert!(matches!(
classify_field("location", &span),
Ok(FieldKind::Location)
));
}
#[test]
fn test_classify_field_tag_bracket_quotes() {
let span = dummy_span();
for (field, expected) in [
("tags['123']", "123"),
("tags[\"123\"]", "123"),
("tags['environment']", "environment"),
("tags[\"environment\"]", "environment"),
("tags[unquoted]", "unquoted"),
("tags['embedded\"quote']", "embedded\"quote"),
("tags['dept[*]']", "dept[*]"),
("tags[\"dept[*]\"]", "dept[*]"),
] {
match classify_field(field, &span) {
Ok(FieldKind::Tag(actual)) => assert_eq!(actual, expected),
other => panic!("expected Tag for {field:?}, got {other:?}"),
}
}
}
#[test]
fn test_classify_field_empty_bracket_tag_name_is_rejected() {
let span = dummy_span();
for field in ["tags[]", "tags['']", "tags[\"\"]"] {
assert!(
matches!(classify_field(field, &span), Err(ParseError::Custom { .. })),
"expected error for {field:?}"
);
}
}
#[test]
fn test_classify_field_tag_bracket_preserves_unmatched_quotes() {
let span = dummy_span();
for (field, expected) in [("tags[\"123']", "\"123'"), ("tags['123\"]", "'123\"")] {
match classify_field(field, &span) {
Ok(FieldKind::Tag(actual)) => assert_eq!(actual, expected),
other => panic!("expected Tag for {field:?}, got {other:?}"),
}
}
}
#[test]
fn test_classify_field_unquoted_tag_wildcards_are_not_tag_names() {
let span = dummy_span();
for field in ["tags[*]", "tags[a[*]]", "tags[*][*]"] {
match classify_field(field, &span) {
Ok(FieldKind::Alias(actual)) => assert_eq!(actual, field),
other => panic!("expected Alias for unquoted tag wildcard, got {other:?}"),
}
}
}
#[test]
fn test_classify_field_tag_bracket_trims_outer_whitespace() {
let span = dummy_span();
for (field, expected) in [
("tags[ env ]", "env"),
("tags[ 'env' ]", "env"),
("tags[ \"env\" ]", "env"),
("tags[\tenv\n]", "env"),
("tags[\u{3000}env\u{a0}]", "env"),
("tags[ my tag ]", "my tag"),
("tags[ ' env ' ]", " env "),
("tags[ \" a b \" ]", " a b "),
] {
match classify_field(field, &span) {
Ok(FieldKind::Tag(actual)) => assert_eq!(actual, expected, "field {field:?}"),
other => panic!("expected Tag for {field:?}, got {other:?}"),
}
}
}
#[test]
fn test_classify_field_tag_bracket_preserves_quoted_whitespace() {
let span = dummy_span();
for (field, expected) in [
("tags['my tag']", "my tag"),
("tags[' env ']", " env "),
("tags[\" a b \"]", " a b "),
("tags[' ']", " "),
("tags[\"\t\"]", "\t"),
] {
match classify_field(field, &span) {
Ok(FieldKind::Tag(actual)) => assert_eq!(actual, expected, "field {field:?}"),
other => panic!("expected Tag for {field:?}, got {other:?}"),
}
}
}
#[test]
fn test_classify_field_whitespace_only_bracket_tag_name_is_rejected() {
let span = dummy_span();
for field in [
"tags[ ]",
"tags[ ]",
"tags[\t\n]",
"tags[\u{3000}]",
"tags[ '' ]",
"tags[ \"\" ]",
] {
assert!(
matches!(classify_field(field, &span), Err(ParseError::Custom { .. })),
"expected error for {field:?}"
);
}
}
#[test]
fn test_classify_field_tag_bracket_padded_literals() {
let span = dummy_span();
for (field, expected) in [
("tags[ * ]", "*"),
("tags[ ' ]", "'"),
("tags[ 'env\" ]", "'env\""),
("TAGS[ Env ]", "Env"),
] {
match classify_field(field, &span) {
Ok(FieldKind::Tag(actual)) => assert_eq!(actual, expected, "field {field:?}"),
other => panic!("expected Tag for {field:?}, got {other:?}"),
}
}
assert!(matches!(
classify_field("tags[*]", &span),
Ok(FieldKind::Alias(_))
));
}
#[test]
fn test_classify_field_empty_tag_name_error_escapes_text() {
let span = dummy_span();
match classify_field("tags[\t\n]", &span) {
Err(ParseError::Custom { message, .. }) => {
assert!(message.contains("empty tag name"), "{message}");
assert!(message.contains("\"tags[\\t\\n]\""), "{message}");
assert!(
!message.contains('\t') && !message.contains('\n'),
"{message}"
);
}
other => panic!("expected empty tag name error, got {other:?}"),
}
}
#[test]
fn test_classify_field_alias() {
let span = dummy_span();
match classify_field("Microsoft.Compute/virtualMachines/storageProfile", &span) {
Ok(FieldKind::Alias(a)) => {
assert_eq!(a, "Microsoft.Compute/virtualMachines/storageProfile");
}
other => panic!("expected Alias, got {:?}", other),
}
}
#[test]
fn test_classify_field_expression() {
let span = dummy_span();
match classify_field("[field('type')]", &span) {
Ok(FieldKind::Expr(_)) => {}
other => panic!("expected Expr, got {:?}", other),
}
}
#[test]
fn test_classify_field_double_bracket_not_expr() {
let span = dummy_span();
match classify_field("[[escaped]", &span) {
Ok(FieldKind::Alias(a)) => assert_eq!(a, "[[escaped]"),
other => panic!("expected Alias, got {:?}", other),
}
}
}