mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat(azure_policy): test runner, compiler fixes, and example program (#700)
Adds the YAML test runner that exercises the companion test data PRs, plus several compiler fixes surfaced during testing: - Removed parameter register caching that produced wrong results inside short-circuiting allOf/anyOf blocks; added literal-index caching for parameter defaults to avoid repeated O(n) literal-table scans - Simplified cross-resource effect details to only emit roleDefinitionIds and type (deployment templates are not evaluated for compliance) - Replaced guid/uniqueString builtins with clear "unsupported" errors - Normalized datetime output to ISO 8601 with Z suffix - Added azure_policy parser MAX_COL constant (8192) for long template expressions, keeping the global DEFAULT_MAX_COL at 1024 - Added rvm to azure_policy feature dependencies since the compiler targets RVM bytecode Also restructures the example binary into examples/regorus/ with new azure-policy-eval and azure-policy-aliases subcommands, adds C# alias normalization tests, and documents Azure Policy support in the README. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
7f42115b63
commit
4c92fb4d92
@@ -50,8 +50,10 @@ pub(super) struct Compiler {
|
||||
pub(super) alias_modifiable: BTreeMap<String, bool>,
|
||||
/// Default values for policy parameters.
|
||||
pub(super) parameter_defaults: Option<Value>,
|
||||
/// Cached register for the parameter defaults literal.
|
||||
pub(super) cached_defaults_reg: Option<u8>,
|
||||
/// Cached literal-table index for `parameter_defaults` (or an empty object
|
||||
/// when no defaults exist). Populated on first `parameters()` call to avoid
|
||||
/// repeated O(n) literal-table scans and deep `Value` clones.
|
||||
pub(super) cached_defaults_literal_idx: Option<u16>,
|
||||
/// When set, field conditions resolve against this register instead of
|
||||
/// `input.resource`. Used for `existenceCondition`.
|
||||
pub(super) resource_override_reg: Option<u8>,
|
||||
@@ -126,9 +128,6 @@ impl Compiler {
|
||||
if let Some(r) = self.cached_context_reg {
|
||||
floor = floor.max(r.saturating_add(1));
|
||||
}
|
||||
if let Some(r) = self.cached_defaults_reg {
|
||||
floor = floor.max(r.saturating_add(1));
|
||||
}
|
||||
self.register_counter = floor;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,13 @@ use anyhow::{anyhow, bail, Result};
|
||||
use crate::languages::azure_policy::ast::{
|
||||
EffectKind, EffectNode, Expr, ExprLiteral, JsonValue, ObjectEntry, PolicyRule,
|
||||
};
|
||||
use crate::languages::azure_policy::compiler::utils::json_value_to_runtime;
|
||||
use crate::rvm::instructions::ObjectCreateParams;
|
||||
use crate::rvm::Instruction;
|
||||
use crate::Value;
|
||||
|
||||
use super::core::Compiler;
|
||||
use super::expressions::check_json_depth;
|
||||
|
||||
impl Compiler {
|
||||
// -- main dispatch ------------------------------------------------------
|
||||
@@ -451,9 +453,11 @@ impl Compiler {
|
||||
|
||||
/// Build cross-resource effect details for the returned result object.
|
||||
///
|
||||
/// Preserves all detail fields except `existenceCondition` (which is
|
||||
/// compiled and evaluated inline). Known Azure fields are emitted with
|
||||
/// canonical casing regardless of source casing.
|
||||
/// Only emits `roleDefinitionIds` and `type` into the structured result.
|
||||
/// All other fields (`existenceCondition`, `deployment`, `name`,
|
||||
/// `resourceGroupName`, etc.) are either evaluated inline during
|
||||
/// compilation or are ARM deployment metadata that the policy evaluation
|
||||
/// engine does not interpret.
|
||||
pub(super) fn compile_cross_resource_details(
|
||||
&mut self,
|
||||
effect_name_reg: u8,
|
||||
@@ -467,17 +471,26 @@ impl Compiler {
|
||||
let mut detail_keys: Vec<(u16, u8)> = Vec::new();
|
||||
|
||||
for ObjectEntry { key, value, .. } in entries {
|
||||
// existenceCondition is evaluated inline — not included in result.
|
||||
if key.eq_ignore_ascii_case("existenceCondition") {
|
||||
continue;
|
||||
// Only emit `roleDefinitionIds` and `type` into the structured
|
||||
// result. All other fields (existenceCondition, deployment,
|
||||
// name, resourceGroupName, etc.) are either evaluated inline
|
||||
// during compilation or are ARM deployment metadata that the
|
||||
// policy evaluation engine does not interpret.
|
||||
if key.eq_ignore_ascii_case("roleDefinitionIds") {
|
||||
check_json_depth(value, 0).map_err(|_| {
|
||||
value
|
||||
.span()
|
||||
.error("JSON value nesting exceeds maximum depth")
|
||||
})?;
|
||||
let val = json_value_to_runtime(value)?;
|
||||
let reg = self.load_literal(val, value.span())?;
|
||||
let key_idx = self.add_literal_u16(Value::from("roleDefinitionIds"))?;
|
||||
detail_keys.push((key_idx, reg));
|
||||
} else if key.eq_ignore_ascii_case("type") {
|
||||
let reg = self.compile_json_value(value, value.span())?;
|
||||
let key_idx = self.add_literal_u16(Value::from("type"))?;
|
||||
detail_keys.push((key_idx, reg));
|
||||
}
|
||||
|
||||
let reg = self.compile_json_value(value, value.span())?;
|
||||
|
||||
// Canonicalize known Azure field names.
|
||||
let canonical_key = canonicalize_detail_key(key);
|
||||
let key_idx = self.add_literal_u16(Value::from(canonical_key))?;
|
||||
detail_keys.push((key_idx, reg));
|
||||
}
|
||||
|
||||
if detail_keys.is_empty() {
|
||||
@@ -576,21 +589,29 @@ impl Compiler {
|
||||
Some(effect_name.to_string())
|
||||
}
|
||||
|
||||
/// Map a lowercase effect name string to its `EffectKind`.
|
||||
pub(super) fn effect_kind_from_string(effect_name: &str) -> Option<EffectKind> {
|
||||
let normalized = effect_name.to_lowercase();
|
||||
Some(match normalized.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,
|
||||
_ => return None,
|
||||
})
|
||||
/// Map an effect name string, matched case-insensitively, to its `EffectKind`.
|
||||
pub(super) const fn effect_kind_from_string(effect_name: &str) -> Option<EffectKind> {
|
||||
if effect_name.eq_ignore_ascii_case("deny") {
|
||||
Some(EffectKind::Deny)
|
||||
} else if effect_name.eq_ignore_ascii_case("audit") {
|
||||
Some(EffectKind::Audit)
|
||||
} else if effect_name.eq_ignore_ascii_case("append") {
|
||||
Some(EffectKind::Append)
|
||||
} else if effect_name.eq_ignore_ascii_case("auditIfNotExists") {
|
||||
Some(EffectKind::AuditIfNotExists)
|
||||
} else if effect_name.eq_ignore_ascii_case("deployIfNotExists") {
|
||||
Some(EffectKind::DeployIfNotExists)
|
||||
} else if effect_name.eq_ignore_ascii_case("disabled") {
|
||||
Some(EffectKind::Disabled)
|
||||
} else if effect_name.eq_ignore_ascii_case("modify") {
|
||||
Some(EffectKind::Modify)
|
||||
} else if effect_name.eq_ignore_ascii_case("denyAction") {
|
||||
Some(EffectKind::DenyAction)
|
||||
} else if effect_name.eq_ignore_ascii_case("manual") {
|
||||
Some(EffectKind::Manual)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// -- host await request -------------------------------------------------
|
||||
@@ -731,10 +752,10 @@ fn detect_effect_family_from_details(rule: &PolicyRule) -> EffectFamily {
|
||||
let mut has_operations = false;
|
||||
|
||||
for entry in entries {
|
||||
match entry.key.to_lowercase().as_str() {
|
||||
"type" => has_type = true,
|
||||
"operations" => has_operations = true,
|
||||
_ => {}
|
||||
if entry.key.eq_ignore_ascii_case("type") {
|
||||
has_type = true;
|
||||
} else if entry.key.eq_ignore_ascii_case("operations") {
|
||||
has_operations = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -748,8 +769,8 @@ fn detect_effect_family_from_details(rule: &PolicyRule) -> EffectFamily {
|
||||
EffectFamily::Modify
|
||||
} else {
|
||||
// Check for Append-shaped object: { "field": …, "value": … }
|
||||
let has_field = entries.iter().any(|e| e.key.to_lowercase() == "field");
|
||||
let has_value = entries.iter().any(|e| e.key.to_lowercase() == "value");
|
||||
let has_field = entries.iter().any(|e| e.key.eq_ignore_ascii_case("field"));
|
||||
let has_value = entries.iter().any(|e| e.key.eq_ignore_ascii_case("value"));
|
||||
if has_field && has_value {
|
||||
EffectFamily::Append
|
||||
} else {
|
||||
@@ -776,25 +797,6 @@ fn unescape_arm_literal(s: &str) -> alloc::string::String {
|
||||
.map_or_else(|| s.into(), |rest| format!("[{rest}"))
|
||||
}
|
||||
|
||||
/// Canonicalize known Azure Policy detail field names to their standard casing.
|
||||
///
|
||||
/// Case-insensitive matching produces the canonical form used by Azure;
|
||||
/// unknown keys are passed through unchanged.
|
||||
fn canonicalize_detail_key(key: &str) -> alloc::string::String {
|
||||
match key.to_lowercase().as_str() {
|
||||
"roledefinitionids" => "roleDefinitionIds".into(),
|
||||
"type" => "type".into(),
|
||||
"name" => "name".into(),
|
||||
"kind" => "kind".into(),
|
||||
"resourcegroupname" => "resourceGroupName".into(),
|
||||
"existencescope" => "existenceScope".into(),
|
||||
"deployment" => "deployment".into(),
|
||||
"deploymentscope" => "deploymentScope".into(),
|
||||
"evaluationdelay" => "evaluationDelay".into(),
|
||||
_ => key.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an RVM object from a set of `(literal_key_idx, value_reg)` pairs.
|
||||
///
|
||||
/// This is the common pattern used throughout effect compilation:
|
||||
|
||||
@@ -18,12 +18,15 @@ use alloc::vec::Vec;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use crate::languages::azure_policy::compiler::utils::json_value_to_runtime;
|
||||
|
||||
use crate::languages::azure_policy::ast::{JsonValue, ObjectEntry};
|
||||
use crate::rvm::instructions::ArrayCreateParams;
|
||||
use crate::rvm::Instruction;
|
||||
|
||||
use super::core::Compiler;
|
||||
use super::effects::build_object_from_keys;
|
||||
use super::expressions::check_json_depth;
|
||||
use crate::Value;
|
||||
|
||||
impl Compiler {
|
||||
@@ -37,8 +40,12 @@ impl Compiler {
|
||||
details: Option<&JsonValue>,
|
||||
span: &crate::lexer::Span,
|
||||
) -> Result<u8> {
|
||||
// When details is absent or not an object, return the bare effect.
|
||||
// Azure Policy accepts this — the effect is reported for compliance
|
||||
// evaluation even when remediation details are missing. Erroring here
|
||||
// would reject policies that the real engine considers valid.
|
||||
let Some(JsonValue::Object(_, entries)) = details else {
|
||||
bail!(span.error("Modify effect requires 'details' to be an object"));
|
||||
return self.wrap_effect_result(effect_name_reg, None, span);
|
||||
};
|
||||
|
||||
// Extract roleDefinitionIds and operations from details entries.
|
||||
@@ -185,8 +192,21 @@ impl Compiler {
|
||||
has_value = true;
|
||||
}
|
||||
"condition" => {
|
||||
// Condition may contain template expressions.
|
||||
let reg = self.compile_json_value(value, value.span())?;
|
||||
// The `condition` field is NOT evaluated during policy
|
||||
// rule evaluation. It is a remediation instruction:
|
||||
// when Azure's remediation engine applies the modify
|
||||
// effect it evaluates this condition against the
|
||||
// resource to decide whether to execute the specific
|
||||
// operation. We preserve it verbatim (as a literal
|
||||
// string) so the consumer receives the original
|
||||
// expression, e.g. `"[equals(field('tags.env'), '')]"`.
|
||||
check_json_depth(value, 0).map_err(|_| {
|
||||
value
|
||||
.span()
|
||||
.error("JSON value nesting exceeds maximum depth")
|
||||
})?;
|
||||
let runtime_value = json_value_to_runtime(value)?;
|
||||
let reg = self.load_literal(runtime_value, value.span())?;
|
||||
let key_idx = self.add_literal_u16(Value::from("condition"))?;
|
||||
op_keys.push((key_idx, reg));
|
||||
}
|
||||
@@ -227,7 +247,9 @@ impl Compiler {
|
||||
span: &crate::lexer::Span,
|
||||
) -> Result<u8> {
|
||||
let Some(details) = details else {
|
||||
bail!(span.error("Append effect requires 'details'"));
|
||||
// When details is absent, return the bare effect. Same rationale
|
||||
// as modify: Azure Policy accepts this for compliance evaluation.
|
||||
return self.wrap_effect_result(effect_name_reg, None, span);
|
||||
};
|
||||
|
||||
let item_regs = match details {
|
||||
|
||||
@@ -26,7 +26,13 @@ impl Compiler {
|
||||
span: &crate::lexer::Span,
|
||||
) -> Result<u8> {
|
||||
match voe {
|
||||
ValueOrExpr::Value(value) => self.compile_json_value(value, span),
|
||||
// The parser's `json_to_value_or_expr` already resolved template
|
||||
// expressions and unescaped `[[` → `[` literals. Skip the
|
||||
// top-level template-expression check so an unescaped string like
|
||||
// `"[not-an-expression]"` (originally `"[[not-an-expression]"`) is
|
||||
// not re-parsed as a template expression. Nested arrays/objects
|
||||
// still get full template-expression handling at depth > 0.
|
||||
ValueOrExpr::Value(value) => self.compile_json_value_inner(value, span, 0, true),
|
||||
ValueOrExpr::Expr { expr, .. } => self.compile_expr(expr),
|
||||
}
|
||||
}
|
||||
@@ -36,14 +42,23 @@ impl Compiler {
|
||||
value: &crate::languages::azure_policy::ast::JsonValue,
|
||||
span: &crate::lexer::Span,
|
||||
) -> Result<u8> {
|
||||
self.compile_json_value_inner(value, span, 0)
|
||||
self.compile_json_value_inner(value, span, 0, false)
|
||||
}
|
||||
|
||||
/// Compile a JSON value to a register.
|
||||
///
|
||||
/// `resolved_top` — when `true`, the top-level string has already been
|
||||
/// through `json_to_value_or_expr` (template expressions extracted, `[[`
|
||||
/// unescaped). Skip the template-expression check at this level so that
|
||||
/// an unescaped `"[literal]"` is not re-parsed. Recursive calls for
|
||||
/// array elements and object values always pass `false` since those
|
||||
/// nested values have not been pre-resolved.
|
||||
fn compile_json_value_inner(
|
||||
&mut self,
|
||||
value: &crate::languages::azure_policy::ast::JsonValue,
|
||||
span: &crate::lexer::Span,
|
||||
depth: usize,
|
||||
resolved_top: bool,
|
||||
) -> Result<u8> {
|
||||
if depth > MAX_JSON_DEPTH {
|
||||
bail!(span.error(&format!(
|
||||
@@ -55,18 +70,22 @@ impl Compiler {
|
||||
use crate::languages::azure_policy::parser::is_template_expr;
|
||||
|
||||
// Standalone string template expressions like `"[concat(...)]"`
|
||||
// must be compiled so they evaluate at runtime.
|
||||
if let JsonValue::Str(str_span, s) = value {
|
||||
if is_template_expr(s) {
|
||||
let inner = s
|
||||
.strip_prefix('[')
|
||||
.and_then(|inner| inner.strip_suffix(']'))
|
||||
.ok_or_else(|| {
|
||||
str_span.error("invalid template expression: missing brackets")
|
||||
})?;
|
||||
let expr = ExprParser::parse_from_brackets(inner, str_span)
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
return self.compile_expr(&expr);
|
||||
// must be compiled so they evaluate at runtime. Skip this check
|
||||
// when the caller has already resolved template expressions (e.g.
|
||||
// values coming from `ValueOrExpr::Value`).
|
||||
if !resolved_top {
|
||||
if let JsonValue::Str(str_span, s) = value {
|
||||
if is_template_expr(s) {
|
||||
let inner = s
|
||||
.strip_prefix('[')
|
||||
.and_then(|inner| inner.strip_suffix(']'))
|
||||
.ok_or_else(|| {
|
||||
str_span.error("invalid template expression: missing brackets")
|
||||
})?;
|
||||
let expr = ExprParser::parse_from_brackets(inner, str_span)
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
return self.compile_expr(&expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +130,7 @@ impl Compiler {
|
||||
) -> Result<u8> {
|
||||
let mut element_regs = Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
let reg = self.compile_json_value_inner(item, item.span(), depth)?;
|
||||
let reg = self.compile_json_value_inner(item, item.span(), depth, false)?;
|
||||
element_regs.push(reg);
|
||||
}
|
||||
|
||||
@@ -140,7 +159,8 @@ impl Compiler {
|
||||
) -> Result<u8> {
|
||||
let mut keys: Vec<(u16, u8)> = Vec::with_capacity(entries.len());
|
||||
for entry in entries {
|
||||
let val_reg = self.compile_json_value_inner(&entry.value, entry.value.span(), depth)?;
|
||||
let val_reg =
|
||||
self.compile_json_value_inner(&entry.value, entry.value.span(), depth, false)?;
|
||||
let key_idx = self.add_literal_u16(Value::from(entry.key.clone()))?;
|
||||
keys.push((key_idx, val_reg));
|
||||
}
|
||||
@@ -228,17 +248,26 @@ impl Compiler {
|
||||
let input_reg = self.load_input(span)?;
|
||||
let params_reg =
|
||||
self.emit_chained_index_literal_path(input_reg, &["parameters"], span)?;
|
||||
let defaults_reg = if let Some(reg) = self.cached_defaults_reg {
|
||||
reg
|
||||
} else {
|
||||
let reg = if let Some(ref defaults) = self.parameter_defaults {
|
||||
self.load_literal(defaults.clone(), span)?
|
||||
} else {
|
||||
self.load_literal(Value::new_object(), span)?
|
||||
};
|
||||
self.cached_defaults_reg = Some(reg);
|
||||
reg
|
||||
let defaults_literal_idx = match self.cached_defaults_literal_idx {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
let val = self
|
||||
.parameter_defaults
|
||||
.clone()
|
||||
.unwrap_or_else(Value::new_object);
|
||||
let idx = self.add_literal_u16(val)?;
|
||||
self.cached_defaults_literal_idx = Some(idx);
|
||||
idx
|
||||
}
|
||||
};
|
||||
let defaults_reg = self.alloc_register()?;
|
||||
self.emit(
|
||||
Instruction::Load {
|
||||
dest: defaults_reg,
|
||||
literal_idx: defaults_literal_idx,
|
||||
},
|
||||
span,
|
||||
);
|
||||
let name_reg = self.load_literal(Value::from(param_name), span)?;
|
||||
self.emit_builtin_call(
|
||||
"azure.policy.get_parameter",
|
||||
|
||||
@@ -302,9 +302,10 @@ impl Compiler {
|
||||
// -- JSON / misc functions --
|
||||
"json" => self.emit_builtin_call_from_args("azure.policy.fn.json", args, span)?,
|
||||
"join" => self.emit_builtin_call_from_args("azure.policy.fn.join", args, span)?,
|
||||
"guid" => self.emit_builtin_call_from_args("azure.policy.fn.guid", args, span)?,
|
||||
"uniquestring" => {
|
||||
self.emit_builtin_call_from_args("azure.policy.fn.unique_string", args, span)?
|
||||
"guid" | "uniquestring" => {
|
||||
bail!(span.error(&alloc::format!(
|
||||
"unsupported template function '{function_name}' (deployment-template functions are not evaluated for compliance)"
|
||||
)));
|
||||
}
|
||||
"items" => self.emit_builtin_call_from_args("azure.policy.fn.items", args, span)?,
|
||||
"indexfromend" => {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
pub mod aliases;
|
||||
pub mod ast;
|
||||
#[cfg(feature = "rvm")]
|
||||
pub(crate) mod compiler;
|
||||
pub mod compiler;
|
||||
pub mod expr;
|
||||
pub mod parser;
|
||||
pub mod strings;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::{String, ToString as _};
|
||||
use alloc::vec::Vec;
|
||||
use core::num::NonZeroU32;
|
||||
|
||||
use crate::lexer::{Lexer, Source, Span, Token, TokenKind};
|
||||
|
||||
@@ -146,9 +147,22 @@ pub(super) struct Parser<'source> {
|
||||
}
|
||||
|
||||
impl<'source> Parser<'source> {
|
||||
/// Column-width limit for Azure Policy definitions.
|
||||
///
|
||||
/// Azure Policy definitions are often serialized as single-line JSON with
|
||||
/// deeply nested template expressions, requiring a much higher limit than
|
||||
/// the standard Rego default.
|
||||
pub const MAX_COL: u32 = 8192;
|
||||
|
||||
// Safety: 8192 != 0, so this is always `Some`.
|
||||
const MAX_COL_NZ: Option<NonZeroU32> = NonZeroU32::new(Self::MAX_COL);
|
||||
|
||||
/// Create a new parser for the given source.
|
||||
///
|
||||
/// Uses [`Self::MAX_COL`] because Azure Policy definitions are often
|
||||
/// serialized as single-line JSON with deeply nested template expressions.
|
||||
pub fn new(source: &'source Source) -> Result<Self, ParseError> {
|
||||
Self::new_with_max_col(source, None)
|
||||
Self::new_with_max_col(source, Self::MAX_COL_NZ)
|
||||
}
|
||||
|
||||
/// Create a new parser with an optional column-width override.
|
||||
|
||||
@@ -43,6 +43,13 @@ use super::expr::ExprParser;
|
||||
|
||||
use self::core::Parser;
|
||||
|
||||
/// Column-width limit for Azure Policy definitions.
|
||||
///
|
||||
/// Azure Policy definitions are often serialized as single-line JSON with
|
||||
/// deeply nested template expressions, requiring a much higher limit than
|
||||
/// the standard Rego default (1024).
|
||||
pub const MAX_COL: u32 = Parser::MAX_COL;
|
||||
|
||||
// ============================================================================
|
||||
// Public API
|
||||
// ============================================================================
|
||||
@@ -62,12 +69,14 @@ pub fn parse_policy_rule(source: &Source) -> Result<PolicyRule, ParseError> {
|
||||
parse_policy_rule_with_max_col(source, None)
|
||||
}
|
||||
|
||||
/// Like [`parse_policy_rule`] but with an optional column-width override.
|
||||
/// Like [`parse_policy_rule`] but with an explicit column-width override.
|
||||
///
|
||||
/// When `max_col` is `None`, uses [`MAX_COL`] (the Azure Policy default).
|
||||
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)?;
|
||||
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 {
|
||||
@@ -92,12 +101,14 @@ pub fn parse_policy_definition(source: &Source) -> Result<PolicyDefinition, Pars
|
||||
parse_policy_definition_with_max_col(source, None)
|
||||
}
|
||||
|
||||
/// Like [`parse_policy_definition`] but with an optional column-width override.
|
||||
/// Like [`parse_policy_definition`] but with an explicit column-width override.
|
||||
///
|
||||
/// When `max_col` is `None`, uses [`MAX_COL`] (the Azure Policy default).
|
||||
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)?;
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user