Skip to content

Commit 687be28

Browse files
authored
feat: add Azure Policy constraint parser (microsoft#658)
Add constraint.rs module that parses Azure Policy JSON constraints into span-annotated AST nodes: - Logical combinators: allOf, anyOf, not - Leaf conditions: field/value with all 19 operators - Count blocks: field-count and value-count with where clauses Public API: parse_constraint() parses a standalone constraint from JSON. Includes YAML-driven test suite with 6 test files covering operators, fields, expressions, logical combinators, count, and parse errors.
1 parent 95bffcb commit 687be28

11 files changed

Lines changed: 3203 additions & 15 deletions

File tree

src/languages/azure_policy/parser/constraint.rs

Lines changed: 411 additions & 0 deletions
Large diffs are not rendered by default.

src/languages/azure_policy/parser/error.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ pub enum ParseError {
3939
InvalidCountName { span: Span },
4040
/// `name` used without `value` in count.
4141
MisplacedCountName { span: Span },
42+
/// Multiple operator keys in a single condition.
43+
MultipleOperators { span: Span },
44+
/// A duplicate key was found in a JSON object.
45+
DuplicateKey { span: Span, key: String },
4246
/// A custom error message (e.g., from sub-parsing).
4347
Custom { span: Span, message: String },
4448
}
@@ -137,6 +141,16 @@ impl core::fmt::Display for ParseError {
137141
span.error("'name' can only be used with count-value")
138142
)
139143
}
144+
ParseError::MultipleOperators { ref span } => {
145+
write!(
146+
f,
147+
"{}",
148+
span.error("only one operator key allowed in a condition")
149+
)
150+
}
151+
ParseError::DuplicateKey { ref span, ref key } => {
152+
write!(f, "{}", span.error(&format!("duplicate key \"{}\"", key)))
153+
}
140154
ParseError::Custom {
141155
ref span,
142156
ref message,

src/languages/azure_policy/parser/mod.rs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,60 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
//! Core recursive-descent JSON parser for Azure Policy.
4+
//! Recursive-descent JSON parser for Azure Policy rule constraints.
55
//!
6-
//! Provides the low-level token-driven parser (`core::Parser`) that reads JSON from
7-
//! [`Lexer`] tokens, building span-annotated AST values in a single pass.
8-
//! No intermediate `serde_json::Value` is created.
6+
//! Parses Azure Policy JSON directly from [`Lexer`] tokens, building span-annotated
7+
//! AST nodes in a single pass. No intermediate `serde_json::Value` is created.
98
//!
10-
//! Higher-level policy-aware parsing (constraints, policy rules, policy
11-
//! definitions) is layered on top by sibling modules.
9+
//! The parser is policy-aware: when parsing JSON objects, it dispatches on key names
10+
//! (`allOf`, `anyOf`, `not`, `field`, `value`, `count`, operator names) to build
11+
//! the appropriate AST nodes.
1212
13-
// Parser internals are consumed by constraint/policy_rule/policy_definition
14-
// modules added in a subsequent PR.
15-
#[allow(dead_code)]
16-
pub(crate) mod core;
13+
mod constraint;
14+
mod core;
1715
mod error;
1816

1917
pub(super) use self::core::json_unescape;
2018
pub use error::ParseError;
2119

2220
use alloc::string::ToString as _;
2321

24-
use super::ast::{FieldKind, OperatorKind};
22+
use crate::lexer::{Source, TokenKind};
23+
24+
use super::ast::{Constraint, FieldKind, OperatorKind};
2525
use super::expr::ExprParser;
2626

27+
use self::core::Parser;
28+
29+
// ============================================================================
30+
// Public API
31+
// ============================================================================
32+
33+
/// Parse a standalone constraint from a JSON source.
34+
///
35+
/// A constraint is one of:
36+
/// - Logical combinator: `{ "allOf": [...] }`, `{ "anyOf": [...] }`, `{ "not": {...} }`
37+
/// - Leaf condition: `{ "field": "...", "equals": "..." }`
38+
/// - Count condition: `{ "count": { "field": "..." }, "greater": 0 }`
39+
pub fn parse_constraint(source: &Source) -> Result<Constraint, ParseError> {
40+
let mut parser = Parser::new(source)?;
41+
let constraint = parser.parse_constraint()?;
42+
43+
if parser.tok.0 != TokenKind::Eof {
44+
return Err(ParseError::UnexpectedToken {
45+
span: parser.tok.1.clone(),
46+
expected: "end of input",
47+
});
48+
}
49+
50+
Ok(constraint)
51+
}
52+
2753
// ============================================================================
28-
// Helper functions (used by constraint/policy_rule/policy_definition modules)
54+
// Helper functions (used across submodules)
2955
// ============================================================================
3056

3157
/// Check if a string is an ARM template expression (`[...]` but not `[[...`).
32-
#[allow(dead_code)]
3358
pub(super) fn is_template_expr(s: &str) -> bool {
3459
s.starts_with('[') && s.ends_with(']') && !s.starts_with("[[")
3560
}
@@ -51,7 +76,6 @@ fn unwrap(s: &str, prefix_len: usize, suffix_len: usize) -> &str {
5176
}
5277

5378
/// Classify a field string into a [`FieldKind`].
54-
#[allow(dead_code)]
5579
pub(super) fn classify_field(
5680
text: &str,
5781
span: &crate::lexer::Span,
@@ -96,7 +120,6 @@ pub(super) fn classify_field(
96120
}
97121

98122
/// Try to parse a lowercase key as an operator kind.
99-
#[allow(dead_code)]
100123
pub(super) fn parse_operator_kind(key: &str) -> Option<OperatorKind> {
101124
match key {
102125
"contains" => Some(OperatorKind::Contains),

tests/azure_policy/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
// Licensed under the MIT License.
33

44
mod normalization;
5+
mod parser_tests;

0 commit comments

Comments
 (0)