mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat(azure-policy): add policy rule and policy definition parsers (#660)
Extend the Azure Policy parser to handle complete policyRule and
policyDefinition JSON structures, not just standalone constraints.
Policy rule parser (policy_rule.rs):
- Parse top-level { "if": ..., "then": ... } objects
- Extract effect kind (deny, audit, append, modify, etc.) into typed AST
- Parse "details" structurally when it is an object to pull out
existenceCondition as a first-class Constraint; fall back to opaque
JSON for non-object details (e.g. append array form)
- Detect duplicate/missing keys for "if", "then", "effect", "details"
Policy definition parser (policy_definition.rs):
- Handle both wrapped ARM envelope ({ "properties": { ... } }) and
unwrapped (properties-level keys at top level) forms
- Type-extract displayName, description, mode, metadata, parameters,
and policyRule; everything else goes into extra
- Parse parameter definitions with type, defaultValue, allowedValues,
and metadata; detect duplicate parameter names
- Duplicate key detection throughout
Grammar documentation (docs/azure-policy/azurepolicy.ebnf):
- Add formal EBNF grammar covering policy-rule, then-block,
constraints, conditions, all 19 operators, count expressions,
JSON values, and ARM template expressions
Test harness changes:
- Add parse_level field to YAML test cases: "constraint" (default),
"policy_rule", or "policy_definition"
- Un-skip three parse_errors cases that needed policy_rule-level parsing
- Add policy_rule.yaml with 12 cases covering all 9 effect kinds,
existenceCondition, parameterized effects, complex conditions, and
extra key handling
- Add policy_definition.yaml with wrapped, unwrapped, parameterized,
missing-policyRule, and duplicate-key error cases
This commit is contained in:
committed by
GitHub
parent
687be2850b
commit
8f740e2f6f
@@ -87,7 +87,7 @@ pub enum EffectKind {
|
||||
DenyAction,
|
||||
Manual,
|
||||
/// An effect value that wasn't recognized (may be a parameterized expression).
|
||||
/// Use [`EffectNode::raw`] to get the original text.
|
||||
/// The original text can be retrieved from [`EffectNode::raw`].
|
||||
Other,
|
||||
}
|
||||
|
||||
@@ -283,13 +283,18 @@ pub struct PolicyDefinition {
|
||||
/// Optional `metadata` (kept as raw JSON).
|
||||
pub metadata: Option<JsonValue>,
|
||||
|
||||
/// Parameter definitions as an ordered list; lookups should match `ParameterDefinition::name`.
|
||||
/// Parameter definitions as an ordered list; each entry includes its parameter name.
|
||||
pub parameters: Vec<ParameterDefinition>,
|
||||
|
||||
/// The parsed `policyRule`.
|
||||
pub policy_rule: PolicyRule,
|
||||
|
||||
/// Any other top-level fields not handled above (e.g., `id`, `name`, `type`, `policyType`).
|
||||
/// Any unrecognized fields collected during parsing.
|
||||
///
|
||||
/// In the **wrapped** form this includes both envelope-level keys
|
||||
/// (e.g., `id`, `name`, `type`) and unrecognized keys inside the
|
||||
/// inner `properties` object. In the **unwrapped** form it contains
|
||||
/// only unrecognized top-level keys.
|
||||
pub extra: Vec<ObjectEntry>,
|
||||
}
|
||||
|
||||
|
||||
@@ -18,18 +18,6 @@ use super::core::{CountInner, EntryValue, Parser};
|
||||
use super::error::ParseError;
|
||||
use super::parse_operator_kind;
|
||||
|
||||
/// Set `slot` to `val`, returning a [`ParseError::DuplicateKey`] if it was already set.
|
||||
fn set_once<T>(slot: &mut Option<T>, val: T, key: &str, span: &Span) -> Result<(), ParseError> {
|
||||
if slot.is_some() {
|
||||
return Err(ParseError::DuplicateKey {
|
||||
span: span.clone(),
|
||||
key: String::from(key),
|
||||
});
|
||||
}
|
||||
*slot = Some(val);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'source> Parser<'source> {
|
||||
/// Parse a constraint (a JSON object: logical combinator or leaf condition).
|
||||
pub fn parse_constraint(&mut self) -> Result<Constraint, ParseError> {
|
||||
@@ -191,7 +179,7 @@ impl<'source> Parser<'source> {
|
||||
expected: "JSON value for 'field'",
|
||||
});
|
||||
};
|
||||
set_once(&mut field, (key_span.clone(), jv), &key, &key_span)?;
|
||||
Self::set_once(&mut field, (key_span.clone(), jv), &key, &key_span)?;
|
||||
}
|
||||
"value" => {
|
||||
let EntryValue::Json(jv) = entry_value else {
|
||||
@@ -200,7 +188,7 @@ impl<'source> Parser<'source> {
|
||||
expected: "JSON value for 'value'",
|
||||
});
|
||||
};
|
||||
set_once(&mut value, (key_span.clone(), jv), &key, &key_span)?;
|
||||
Self::set_once(&mut value, (key_span.clone(), jv), &key, &key_span)?;
|
||||
}
|
||||
"count" => {
|
||||
let EntryValue::CountInner(ci) = entry_value else {
|
||||
@@ -209,7 +197,7 @@ impl<'source> Parser<'source> {
|
||||
expected: "object for 'count'",
|
||||
});
|
||||
};
|
||||
set_once(&mut count, (key_span.clone(), ci), &key, &key_span)?;
|
||||
Self::set_once(&mut count, (key_span.clone(), ci), &key, &key_span)?;
|
||||
}
|
||||
_ => {
|
||||
if let Some(op_kind) = parse_operator_kind(&key.to_lowercase()) {
|
||||
@@ -288,19 +276,19 @@ impl<'source> Parser<'source> {
|
||||
match key_lower.as_str() {
|
||||
"field" => {
|
||||
let jv = self.parse_json_value()?;
|
||||
set_once(&mut field, (key_span.clone(), jv), &key_lower, &key_span)?;
|
||||
Self::set_once(&mut field, (key_span.clone(), jv), &key_lower, &key_span)?;
|
||||
}
|
||||
"value" => {
|
||||
let jv = self.parse_json_value()?;
|
||||
set_once(&mut value, (key_span.clone(), jv), &key_lower, &key_span)?;
|
||||
Self::set_once(&mut value, (key_span.clone(), jv), &key_lower, &key_span)?;
|
||||
}
|
||||
"name" => {
|
||||
let jv = self.parse_json_value()?;
|
||||
set_once(&mut name, (key_span.clone(), jv), &key_lower, &key_span)?;
|
||||
Self::set_once(&mut name, (key_span.clone(), jv), &key_lower, &key_span)?;
|
||||
}
|
||||
"where" => {
|
||||
let c = self.parse_constraint()?;
|
||||
set_once(&mut where_, c, &key_lower, &key_span)?;
|
||||
Self::set_once(&mut where_, c, &key_lower, &key_span)?;
|
||||
}
|
||||
_ => {
|
||||
return Err(ParseError::UnrecognizedKey {
|
||||
|
||||
@@ -346,6 +346,27 @@ impl<'source> Parser<'source> {
|
||||
})
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Duplicate-key guard
|
||||
// ========================================================================
|
||||
|
||||
/// Set `slot` to `val`, returning [`ParseError::DuplicateKey`] if already set.
|
||||
pub(super) fn set_once<T>(
|
||||
slot: &mut Option<T>,
|
||||
val: T,
|
||||
key: &str,
|
||||
span: &Span,
|
||||
) -> Result<(), ParseError> {
|
||||
if slot.is_some() {
|
||||
return Err(ParseError::DuplicateKey {
|
||||
span: span.clone(),
|
||||
key: String::from(key),
|
||||
});
|
||||
}
|
||||
*slot = Some(val);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Conversion helpers
|
||||
// ========================================================================
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Recursive-descent JSON parser for Azure Policy rule constraints.
|
||||
//! Custom recursive-descent JSON parser for Azure Policy rules.
|
||||
//!
|
||||
//! Parses Azure Policy JSON directly from [`Lexer`] tokens, building span-annotated
|
||||
//! AST nodes in a single pass. No intermediate `serde_json::Value` is created.
|
||||
@@ -9,19 +9,34 @@
|
||||
//! The parser is policy-aware: when parsing JSON objects, it dispatches on key names
|
||||
//! (`allOf`, `anyOf`, `not`, `field`, `value`, `count`, operator names) to build
|
||||
//! the appropriate AST nodes.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use regorus::Source;
|
||||
//! use regorus::languages::azure_policy::parser;
|
||||
//!
|
||||
//! let json = r#"{ "if": { "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
|
||||
//! "then": { "effect": "deny" } }"#;
|
||||
//! let source = Source::from_contents("policy.json".into(), json.into())?;
|
||||
//! let rule = parser::parse_policy_rule(&source)?;
|
||||
//! ```
|
||||
|
||||
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::string::ToString as _;
|
||||
|
||||
use crate::lexer::{Source, TokenKind};
|
||||
|
||||
use super::ast::{Constraint, FieldKind, OperatorKind};
|
||||
use super::ast::{Constraint, FieldKind, OperatorKind, PolicyDefinition, PolicyRule};
|
||||
use super::expr::ExprParser;
|
||||
|
||||
use self::core::Parser;
|
||||
@@ -30,12 +45,56 @@ use self::core::Parser;
|
||||
// Public API
|
||||
// ============================================================================
|
||||
|
||||
/// Parse an Azure Policy rule from a JSON source.
|
||||
///
|
||||
/// The source should contain a complete `policyRule` JSON object:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "if": { "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
|
||||
/// "then": { "effect": "deny" }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Returns a span-annotated [`PolicyRule`] AST.
|
||||
pub fn parse_policy_rule(source: &Source) -> Result<PolicyRule, ParseError> {
|
||||
let mut parser = Parser::new(source)?;
|
||||
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)
|
||||
}
|
||||
|
||||
/// Parse a full Azure Policy definition from a JSON source.
|
||||
///
|
||||
/// Accepts two forms:
|
||||
/// 1. **Wrapped**: `{ "properties": { "policyRule": ..., ... }, "id": ..., ... }`
|
||||
/// 2. **Unwrapped**: `{ "displayName": ..., "policyRule": ..., ... }`
|
||||
///
|
||||
/// Returns a [`PolicyDefinition`] with typed fields for known properties
|
||||
/// and a catch-all list of `extra` entries for everything else.
|
||||
pub fn parse_policy_definition(source: &Source) -> Result<PolicyDefinition, ParseError> {
|
||||
let mut parser = Parser::new(source)?;
|
||||
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)
|
||||
}
|
||||
|
||||
/// Parse a standalone constraint from a JSON source.
|
||||
///
|
||||
/// A constraint is one of:
|
||||
/// - Logical combinator: `{ "allOf": [...] }`, `{ "anyOf": [...] }`, `{ "not": {...} }`
|
||||
/// - Leaf condition: `{ "field": "...", "equals": "..." }`
|
||||
/// - Count condition: `{ "count": { "field": "..." }, "greater": 0 }`
|
||||
/// Useful for parsing just the `"if"` part of a policy rule.
|
||||
pub fn parse_constraint(source: &Source) -> Result<Constraint, ParseError> {
|
||||
let mut parser = Parser::new(source)?;
|
||||
let constraint = parser.parse_constraint()?;
|
||||
|
||||
459
src/languages/azure_policy/parser/policy_definition.rs
Normal file
459
src/languages/azure_policy/parser/policy_definition.rs
Normal file
@@ -0,0 +1,459 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Parsing for the full Azure Policy definition envelope.
|
||||
//!
|
||||
//! Handles the outer `{ "properties": { ... } }` wrapper, extracting typed
|
||||
//! fields (`displayName`, `description`, `mode`, `parameters`, `policyRule`)
|
||||
//! and collecting everything else into `extra`.
|
||||
|
||||
use alloc::collections::BTreeSet;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::lexer::Span;
|
||||
|
||||
use crate::languages::azure_policy::ast::{
|
||||
JsonValue, ObjectEntry, ParameterDefinition, PolicyDefinition, PolicyRule,
|
||||
};
|
||||
|
||||
use super::core::Parser;
|
||||
use super::error::ParseError;
|
||||
|
||||
impl<'source> Parser<'source> {
|
||||
/// Parse a full Azure Policy definition JSON.
|
||||
///
|
||||
/// Accepts two forms:
|
||||
/// 1. **Wrapped**: `{ "properties": { ... }, ... }` — the standard ARM resource format.
|
||||
/// 2. **Unwrapped**: `{ "displayName": ..., "policyRule": ..., ... }` — just the
|
||||
/// `properties` contents directly.
|
||||
///
|
||||
/// In the wrapped form, fields outside `properties` (like `id`, `name`, `type`)
|
||||
/// are collected into `extra`.
|
||||
///
|
||||
/// Detection: if the top-level object has a `"properties"` key whose value
|
||||
/// is a JSON object, the wrapped path is taken. (In practice, the ARM
|
||||
/// resource envelope always uses `"properties"` to wrap the definition body.)
|
||||
pub fn parse_policy_definition(&mut self) -> Result<PolicyDefinition, ParseError> {
|
||||
let open = self.expect_symbol("{")?;
|
||||
|
||||
// Determine whether this looks like the wrapped ARM resource form by
|
||||
// scanning top-level keys as they are parsed.
|
||||
//
|
||||
// Because JSON keys can appear in any order, we handle the general case:
|
||||
// iterate all top-level keys; if we encounter "properties" whose value is
|
||||
// an object, take the wrapped path. Keys outside "properties" go into
|
||||
// `extra`.
|
||||
//
|
||||
// For the unwrapped form, all top-level keys are treated as
|
||||
// properties-level fields.
|
||||
let mut display_name: Option<String> = None;
|
||||
let mut description: Option<String> = None;
|
||||
let mut mode: Option<String> = None;
|
||||
let mut metadata: Option<JsonValue> = None;
|
||||
let mut parameters: Vec<ParameterDefinition> = Vec::new();
|
||||
let mut policy_rule: Option<PolicyRule> = None;
|
||||
let mut extra = Vec::new();
|
||||
|
||||
// Track whether we found a "properties" object (wrapped form).
|
||||
let mut found_properties = false;
|
||||
// Track whether we have seen any "properties" key (for duplicate detection).
|
||||
let mut seen_properties = false;
|
||||
// Track seen recognized keys for duplicate detection (unwrapped path).
|
||||
let mut seen_keys: Vec<String> = Vec::new();
|
||||
|
||||
if self.token_text() != "}" {
|
||||
loop {
|
||||
let (key_span, key) = self.expect_string()?;
|
||||
self.expect_symbol(":")?;
|
||||
|
||||
match key.to_lowercase().as_str() {
|
||||
"properties" if seen_properties => {
|
||||
return Err(ParseError::DuplicateKey {
|
||||
span: key_span,
|
||||
key,
|
||||
});
|
||||
}
|
||||
"properties" => {
|
||||
seen_properties = true;
|
||||
|
||||
if self.token_text() == "{" {
|
||||
// Wrapped form: parse inner properties object structurally.
|
||||
found_properties = true;
|
||||
self.parse_properties_fields(
|
||||
&mut display_name,
|
||||
&mut description,
|
||||
&mut mode,
|
||||
&mut metadata,
|
||||
&mut parameters,
|
||||
&mut policy_rule,
|
||||
&mut extra,
|
||||
&mut seen_keys,
|
||||
)?;
|
||||
} else {
|
||||
// Unwrapped form: "properties" is just a regular key.
|
||||
Self::handle_properties_key(
|
||||
self,
|
||||
key_span,
|
||||
&key,
|
||||
&mut display_name,
|
||||
&mut description,
|
||||
&mut mode,
|
||||
&mut metadata,
|
||||
&mut parameters,
|
||||
&mut policy_rule,
|
||||
&mut extra,
|
||||
&mut seen_keys,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
_ if !found_properties => {
|
||||
// Unwrapped form: dispatch on properties-level keys.
|
||||
Self::handle_properties_key(
|
||||
self,
|
||||
key_span,
|
||||
&key,
|
||||
&mut display_name,
|
||||
&mut description,
|
||||
&mut mode,
|
||||
&mut metadata,
|
||||
&mut parameters,
|
||||
&mut policy_rule,
|
||||
&mut extra,
|
||||
&mut seen_keys,
|
||||
)?;
|
||||
}
|
||||
_ => {
|
||||
// Wrapped form: keys outside "properties" go to extra.
|
||||
let value = self.parse_json_value()?;
|
||||
extra.push(ObjectEntry {
|
||||
key_span,
|
||||
key,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.token_text() == "," {
|
||||
self.advance()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let close = self.expect_symbol("}")?;
|
||||
let span = Span {
|
||||
source: open.source.clone(),
|
||||
line: open.line,
|
||||
col: open.col,
|
||||
start: open.start,
|
||||
end: close.end,
|
||||
};
|
||||
|
||||
let policy_rule = policy_rule.ok_or_else(|| ParseError::MissingKey {
|
||||
span: span.clone(),
|
||||
key: "policyRule",
|
||||
})?;
|
||||
|
||||
Ok(PolicyDefinition {
|
||||
span,
|
||||
display_name,
|
||||
description,
|
||||
mode,
|
||||
metadata,
|
||||
parameters,
|
||||
policy_rule,
|
||||
extra,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the interior of a `"properties": { ... }` object, dispatching
|
||||
/// recognized keys to their typed parsers.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn parse_properties_fields(
|
||||
&mut self,
|
||||
display_name: &mut Option<String>,
|
||||
description: &mut Option<String>,
|
||||
mode: &mut Option<String>,
|
||||
metadata: &mut Option<JsonValue>,
|
||||
parameters: &mut Vec<ParameterDefinition>,
|
||||
policy_rule: &mut Option<PolicyRule>,
|
||||
extra: &mut Vec<ObjectEntry>,
|
||||
seen_keys: &mut Vec<String>,
|
||||
) -> Result<(), ParseError> {
|
||||
self.expect_symbol("{")?;
|
||||
|
||||
if self.token_text() != "}" {
|
||||
loop {
|
||||
let (key_span, key) = self.expect_string()?;
|
||||
self.expect_symbol(":")?;
|
||||
|
||||
Self::handle_properties_key(
|
||||
self,
|
||||
key_span,
|
||||
&key,
|
||||
display_name,
|
||||
description,
|
||||
mode,
|
||||
metadata,
|
||||
parameters,
|
||||
policy_rule,
|
||||
extra,
|
||||
seen_keys,
|
||||
)?;
|
||||
|
||||
if self.token_text() == "," {
|
||||
self.advance()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.expect_symbol("}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle a single properties-level key, dispatching to the appropriate
|
||||
/// typed parser or collecting into `extra`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn handle_properties_key(
|
||||
&mut self,
|
||||
key_span: Span,
|
||||
key: &str,
|
||||
display_name: &mut Option<String>,
|
||||
description: &mut Option<String>,
|
||||
mode: &mut Option<String>,
|
||||
metadata: &mut Option<JsonValue>,
|
||||
parameters: &mut Vec<ParameterDefinition>,
|
||||
policy_rule: &mut Option<PolicyRule>,
|
||||
extra: &mut Vec<ObjectEntry>,
|
||||
seen_keys: &mut Vec<String>,
|
||||
) -> Result<(), ParseError> {
|
||||
let lk = key.to_lowercase();
|
||||
|
||||
// Reject duplicate recognized property keys regardless of value type.
|
||||
let recognized = matches!(
|
||||
lk.as_str(),
|
||||
"displayname" | "description" | "mode" | "metadata" | "parameters" | "policyrule"
|
||||
);
|
||||
if recognized {
|
||||
if seen_keys.contains(&lk) {
|
||||
return Err(ParseError::DuplicateKey {
|
||||
span: key_span,
|
||||
key: key.into(),
|
||||
});
|
||||
}
|
||||
seen_keys.push(lk.clone());
|
||||
}
|
||||
|
||||
match lk.as_str() {
|
||||
"displayname" => {
|
||||
let value = self.parse_json_value()?;
|
||||
assign_string_or_extra(value, display_name, key_span, key, extra);
|
||||
}
|
||||
"description" => {
|
||||
let value = self.parse_json_value()?;
|
||||
assign_string_or_extra(value, description, key_span, key, extra);
|
||||
}
|
||||
"mode" => {
|
||||
let value = self.parse_json_value()?;
|
||||
assign_string_or_extra(value, mode, key_span, key, extra);
|
||||
}
|
||||
"metadata" => {
|
||||
*metadata = Some(self.parse_json_value()?);
|
||||
}
|
||||
"parameters" => {
|
||||
// Parameters must be a JSON object; if not, push to extra.
|
||||
if self.token_text() == "{" {
|
||||
*parameters = self.parse_parameter_definitions()?;
|
||||
} else {
|
||||
let value = self.parse_json_value()?;
|
||||
extra.push(ObjectEntry {
|
||||
key_span,
|
||||
key: key.into(),
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
"policyrule" => {
|
||||
// Parse the policyRule directly from the token stream!
|
||||
*policy_rule = Some(self.parse_policy_rule()?);
|
||||
}
|
||||
_ => {
|
||||
let value = self.parse_json_value()?;
|
||||
extra.push(ObjectEntry {
|
||||
key_span,
|
||||
key: key.into(),
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a `"parameters": { ... }` object into a list of
|
||||
/// [`ParameterDefinition`], consuming tokens directly from the stream.
|
||||
fn parse_parameter_definitions(&mut self) -> Result<Vec<ParameterDefinition>, ParseError> {
|
||||
self.expect_symbol("{")?;
|
||||
|
||||
let mut defs = Vec::new();
|
||||
let mut seen_names = BTreeSet::new();
|
||||
|
||||
if self.token_text() != "}" {
|
||||
loop {
|
||||
let param = self.parse_single_parameter()?;
|
||||
if !seen_names.insert(param.name.to_lowercase()) {
|
||||
return Err(ParseError::DuplicateKey {
|
||||
span: param.name_span.clone(),
|
||||
key: param.name.clone(),
|
||||
});
|
||||
}
|
||||
defs.push(param);
|
||||
|
||||
if self.token_text() == "," {
|
||||
self.advance()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.expect_symbol("}")?;
|
||||
Ok(defs)
|
||||
}
|
||||
|
||||
/// Parse a single `"paramName": { ... }` entry.
|
||||
fn parse_single_parameter(&mut self) -> Result<ParameterDefinition, ParseError> {
|
||||
let (name_span, name) = self.expect_string()?;
|
||||
self.expect_symbol(":")?;
|
||||
|
||||
// Parameter definitions must be JSON objects.
|
||||
if self.token_text() != "{" {
|
||||
return Err(ParseError::Custom {
|
||||
span: name_span,
|
||||
message: alloc::format!("expected object for parameter '{}'", name),
|
||||
});
|
||||
}
|
||||
|
||||
let open = self.expect_symbol("{")?;
|
||||
|
||||
let mut param_type: Option<String> = None;
|
||||
let mut default_value: Option<JsonValue> = None;
|
||||
let mut allowed_values: Option<Vec<JsonValue>> = None;
|
||||
let mut metadata: Option<JsonValue> = None;
|
||||
let mut extra = Vec::new();
|
||||
// Track seen recognized keys for duplicate detection.
|
||||
let mut seen_type = false;
|
||||
let mut seen_default_value = false;
|
||||
let mut seen_allowed_values = false;
|
||||
let mut seen_metadata = false;
|
||||
|
||||
if self.token_text() != "}" {
|
||||
loop {
|
||||
let (ks, k) = self.expect_string()?;
|
||||
self.expect_symbol(":")?;
|
||||
|
||||
match k.to_lowercase().as_str() {
|
||||
"type" => {
|
||||
if seen_type {
|
||||
return Err(ParseError::DuplicateKey { span: ks, key: k });
|
||||
}
|
||||
seen_type = true;
|
||||
let value = self.parse_json_value()?;
|
||||
assign_string_or_extra(value, &mut param_type, ks, &k, &mut extra);
|
||||
}
|
||||
"defaultvalue" => {
|
||||
if seen_default_value {
|
||||
return Err(ParseError::DuplicateKey { span: ks, key: k });
|
||||
}
|
||||
seen_default_value = true;
|
||||
default_value = Some(self.parse_json_value()?);
|
||||
}
|
||||
"allowedvalues" => {
|
||||
if seen_allowed_values {
|
||||
return Err(ParseError::DuplicateKey { span: ks, key: k });
|
||||
}
|
||||
seen_allowed_values = true;
|
||||
let value = self.parse_json_value()?;
|
||||
if let JsonValue::Array(_, items) = value {
|
||||
allowed_values = Some(items);
|
||||
} else {
|
||||
extra.push(ObjectEntry {
|
||||
key_span: ks,
|
||||
key: k,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
"metadata" => {
|
||||
if seen_metadata {
|
||||
return Err(ParseError::DuplicateKey { span: ks, key: k });
|
||||
}
|
||||
seen_metadata = true;
|
||||
metadata = Some(self.parse_json_value()?);
|
||||
}
|
||||
_ => {
|
||||
let value = self.parse_json_value()?;
|
||||
extra.push(ObjectEntry {
|
||||
key_span: ks,
|
||||
key: k,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.token_text() == "," {
|
||||
self.advance()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let close = self.expect_symbol("}")?;
|
||||
let span = Span {
|
||||
source: open.source.clone(),
|
||||
line: open.line,
|
||||
col: open.col,
|
||||
start: open.start,
|
||||
end: close.end,
|
||||
};
|
||||
|
||||
Ok(ParameterDefinition {
|
||||
span,
|
||||
name,
|
||||
name_span,
|
||||
param_type,
|
||||
default_value,
|
||||
allowed_values,
|
||||
metadata,
|
||||
extra,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
/// If `value` is a plain string, assign it to `target`; otherwise push
|
||||
/// the entry to `extra` so callers don't have to repeat this pattern.
|
||||
fn assign_string_or_extra(
|
||||
value: JsonValue,
|
||||
target: &mut Option<String>,
|
||||
key_span: Span,
|
||||
key: &str,
|
||||
extra: &mut Vec<ObjectEntry>,
|
||||
) {
|
||||
if let JsonValue::Str(_, ref s) = value {
|
||||
*target = Some(s.clone());
|
||||
} else {
|
||||
extra.push(ObjectEntry {
|
||||
key_span,
|
||||
key: key.into(),
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
213
src/languages/azure_policy/parser/policy_rule.rs
Normal file
213
src/languages/azure_policy/parser/policy_rule.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Top-level policy rule parsing: `parse_policy_rule` and `parse_then_block`.
|
||||
|
||||
use crate::lexer::Span;
|
||||
|
||||
use crate::languages::azure_policy::ast::{
|
||||
Constraint, EffectKind, EffectNode, JsonValue, ObjectEntry, PolicyRule, ThenBlock,
|
||||
};
|
||||
|
||||
use super::core::Parser;
|
||||
use super::error::ParseError;
|
||||
|
||||
impl<'source> Parser<'source> {
|
||||
/// Parse the top-level `policyRule` object.
|
||||
pub fn parse_policy_rule(&mut self) -> Result<PolicyRule, ParseError> {
|
||||
let open = self.expect_symbol("{")?;
|
||||
|
||||
let mut condition: Option<Constraint> = None;
|
||||
let mut then_block: Option<ThenBlock> = None;
|
||||
|
||||
if self.token_text() != "}" {
|
||||
loop {
|
||||
let (key_span, key) = self.expect_string()?;
|
||||
self.expect_symbol(":")?;
|
||||
|
||||
match key.to_lowercase().as_str() {
|
||||
"if" => {
|
||||
let c = self.parse_constraint()?;
|
||||
Self::set_once(&mut condition, c, "if", &key_span)?;
|
||||
}
|
||||
"then" => {
|
||||
let tb = self.parse_then_block()?;
|
||||
Self::set_once(&mut then_block, tb, "then", &key_span)?;
|
||||
}
|
||||
_ => {
|
||||
let _ = self.parse_json_value()?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.token_text() == "," {
|
||||
self.advance()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let close = self.expect_symbol("}")?;
|
||||
let span = Span {
|
||||
source: open.source.clone(),
|
||||
line: open.line,
|
||||
col: open.col,
|
||||
start: open.start,
|
||||
end: close.end,
|
||||
};
|
||||
|
||||
let condition = condition.ok_or_else(|| ParseError::MissingKey {
|
||||
span: span.clone(),
|
||||
key: "if",
|
||||
})?;
|
||||
let then_block = then_block.ok_or_else(|| ParseError::MissingKey {
|
||||
span: span.clone(),
|
||||
key: "then",
|
||||
})?;
|
||||
|
||||
Ok(PolicyRule {
|
||||
span,
|
||||
condition,
|
||||
then_block,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the `"then"` block.
|
||||
fn parse_then_block(&mut self) -> Result<ThenBlock, ParseError> {
|
||||
let open = self.expect_symbol("{")?;
|
||||
|
||||
let mut effect: Option<EffectNode> = None;
|
||||
let mut details: Option<JsonValue> = None;
|
||||
let mut existence_condition: Option<Constraint> = None;
|
||||
|
||||
if self.token_text() != "}" {
|
||||
loop {
|
||||
let (key_span, key) = self.expect_string()?;
|
||||
self.expect_symbol(":")?;
|
||||
|
||||
match key.to_lowercase().as_str() {
|
||||
"effect" => {
|
||||
let (val_span, val_text) = self.expect_string()?;
|
||||
let kind = match val_text.to_lowercase().as_str() {
|
||||
"deny" => EffectKind::Deny,
|
||||
"audit" => EffectKind::Audit,
|
||||
"append" => EffectKind::Append,
|
||||
"auditifnotexists" => EffectKind::AuditIfNotExists,
|
||||
"deployifnotexists" => EffectKind::DeployIfNotExists,
|
||||
"disabled" => EffectKind::Disabled,
|
||||
"modify" => EffectKind::Modify,
|
||||
"denyaction" => EffectKind::DenyAction,
|
||||
"manual" => EffectKind::Manual,
|
||||
_ => EffectKind::Other,
|
||||
};
|
||||
let node = EffectNode {
|
||||
span: val_span,
|
||||
kind,
|
||||
raw: val_text,
|
||||
};
|
||||
Self::set_once(&mut effect, node, "effect", &key_span)?;
|
||||
}
|
||||
"details" => {
|
||||
// If details is an object, parse structurally to
|
||||
// recognize existenceCondition inline. Otherwise
|
||||
// (e.g., append's array form), parse as opaque JSON.
|
||||
if self.token_text() == "{" {
|
||||
let (det, ec) = self.parse_then_details()?;
|
||||
Self::set_once(&mut details, det, "details", &key_span)?;
|
||||
existence_condition = ec;
|
||||
} else {
|
||||
let det = self.parse_json_value()?;
|
||||
Self::set_once(&mut details, det, "details", &key_span)?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let _ = self.parse_json_value()?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.token_text() == "," {
|
||||
self.advance()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let close = self.expect_symbol("}")?;
|
||||
let span = Span {
|
||||
source: open.source.clone(),
|
||||
line: open.line,
|
||||
col: open.col,
|
||||
start: open.start,
|
||||
end: close.end,
|
||||
};
|
||||
|
||||
let effect = effect.ok_or_else(|| ParseError::MissingKey {
|
||||
span: span.clone(),
|
||||
key: "effect",
|
||||
})?;
|
||||
|
||||
Ok(ThenBlock {
|
||||
span,
|
||||
effect,
|
||||
details,
|
||||
existence_condition,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the `"details"` object, extracting `existenceCondition` as a
|
||||
/// structured [`Constraint`] directly from the token stream.
|
||||
///
|
||||
/// All other entries are collected into a generic `JsonValue::Object`.
|
||||
fn parse_then_details(&mut self) -> Result<(JsonValue, Option<Constraint>), ParseError> {
|
||||
let open = self.expect_symbol("{")?;
|
||||
|
||||
let mut entries = alloc::vec::Vec::new();
|
||||
let mut existence_condition: Option<Constraint> = None;
|
||||
|
||||
if self.token_text() != "}" {
|
||||
loop {
|
||||
let (key_span, key) = self.expect_string()?;
|
||||
self.expect_symbol(":")?;
|
||||
|
||||
if key.eq_ignore_ascii_case("existenceCondition") {
|
||||
// Parse the constraint directly from the token stream and
|
||||
// expose it separately via `ThenBlock::existence_condition`.
|
||||
// Intentionally omit the `existenceCondition` entry from
|
||||
// the `details` JsonValue since that field is returned
|
||||
// separately on the AST node.
|
||||
Self::set_once(
|
||||
&mut existence_condition,
|
||||
self.parse_constraint()?,
|
||||
"existenceCondition",
|
||||
&key_span,
|
||||
)?;
|
||||
} else {
|
||||
let value = self.parse_json_value()?;
|
||||
entries.push(ObjectEntry {
|
||||
key_span,
|
||||
key,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
if self.token_text() == "," {
|
||||
self.advance()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let close = self.expect_symbol("}")?;
|
||||
let span = Span {
|
||||
source: open.source.clone(),
|
||||
line: open.line,
|
||||
col: open.col,
|
||||
start: open.start,
|
||||
end: close.end,
|
||||
};
|
||||
|
||||
Ok((JsonValue::Object(span, entries), existence_condition))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user