feat(azure-policy): add compiler skeleton with core types and stubs (#674)

Add the compiler module structure with:
- core.rs: Compiler struct, CountBinding, new(), compile() pipeline,
  register allocation, span/emit helpers
- mod.rs: module declarations, public entry points
  (compile_policy_rule, compile_policy_definition, etc.)
- utils.rs: pure helper functions (path splitting, JSON conversion)
- Stub files for conditions, expressions, fields, template dispatch,
  count, effects, and metadata — real implementations follow in
  subsequent commits.
This commit is contained in:
Anand Krishnamoorthi
2026-04-20 15:29:26 -05:00
committed by GitHub
parent ce235356bc
commit f727096a1d
15 changed files with 1100 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(clippy::pattern_type_mismatch)]
//! Constraint / condition / LHS compilation.
//!
//! Stub — real implementation added in a later commit.
use anyhow::{bail, Result};
use crate::languages::azure_policy::ast::Constraint;
use super::core::Compiler;
impl Compiler {
pub(super) fn compile_constraint(&mut self, _constraint: &Constraint) -> Result<u8> {
let _ = self;
bail!("condition compilation not yet implemented")
}
}

View File

@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Implicit allOf for unbound `[*]` wildcard fields.
//!
//! Stub — real implementation added in a later commit.

View File

@@ -0,0 +1,195 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code)]
#![allow(clippy::pattern_type_mismatch)]
//! Core `Compiler` struct, main compilation pipeline, and register/emit
//! infrastructure.
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
use anyhow::{anyhow, bail, Result};
use crate::rvm::program::{Program, SpanInfo};
use crate::rvm::Instruction;
use crate::{Rc, Value};
use crate::languages::azure_policy::ast::PolicyRule;
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
#[derive(Debug, Clone)]
pub(super) struct CountBinding {
pub(super) name: Option<String>,
pub(super) field_wildcard_prefix: Option<String>,
pub(super) current_reg: u8,
}
#[derive(Debug, Default)]
pub(super) struct Compiler {
pub(super) program: Program,
pub(super) register_counter: u8,
/// High-water mark of `register_counter`.
pub(super) register_high_water: u8,
pub(super) source_to_index: BTreeMap<String, usize>,
pub(super) builtin_index: BTreeMap<String, u16>,
pub(super) count_bindings: Vec<CountBinding>,
/// Cached register for `LoadInput` — allocated once on first use.
pub(super) cached_input_reg: Option<u8>,
/// Cached register for `LoadContext` — allocated once on first use.
pub(super) cached_context_reg: Option<u8>,
/// Map from lowercase fully-qualified alias name → short name.
pub(super) alias_map: BTreeMap<String, String>,
/// Map from lowercase fully-qualified alias name → modifiable flag.
pub(super) alias_modifiable: BTreeMap<String, bool>,
/// Default values for policy parameters.
pub(super) parameter_defaults: Option<Value>,
/// When set, field conditions resolve against this register instead of
/// `input.resource`. Used for `existenceCondition`.
pub(super) resource_override_reg: Option<u8>,
// -- Metadata accumulators ---------------------------------------------
pub(super) observed_field_kinds: BTreeSet<String>,
pub(super) observed_aliases: BTreeSet<String>,
pub(super) observed_tag_names: BTreeSet<String>,
pub(super) observed_operators: BTreeSet<String>,
pub(super) observed_resource_types: BTreeSet<String>,
pub(super) observed_uses_count: bool,
pub(super) observed_has_dynamic_fields: bool,
pub(super) observed_has_wildcard_aliases: bool,
/// When `true`, unknown aliases are silently treated as raw property paths.
pub(super) alias_fallback_to_raw: bool,
}
// ---------------------------------------------------------------------------
// Core infrastructure
// ---------------------------------------------------------------------------
impl Compiler {
pub(super) fn new() -> Self {
Self {
register_counter: 0,
..Self::default()
}
}
pub(super) fn compile(mut self, rule: &PolicyRule) -> Result<Rc<Program>> {
let cond_reg = self.compile_constraint(&rule.condition)?;
self.emit(
Instruction::ReturnUndefinedIfNotTrue {
condition: cond_reg,
},
&rule.span,
);
let effect_reg = self.compile_effect(rule)?;
self.emit(
Instruction::Return { value: effect_reg },
&rule.then_block.span,
);
self.program.main_entry_point = 0;
self.program.entry_points.insert("main".to_string(), 0);
self.program.dispatch_window_size = self.register_high_water.max(2);
self.program.max_rule_window_size = 0;
if !self.program.builtin_info_table.is_empty() {
self.program.initialize_resolved_builtins()?;
}
self.program
.validate_limits()
.map_err(|message| anyhow!(message))?;
self.populate_compiled_annotations();
Ok(Rc::new(self.program))
}
// -- register / span / emit helpers ------------------------------------
/// Restore `register_counter` to `saved` while protecting cached registers.
pub(super) fn restore_register_counter(&mut self, saved: u8) {
let mut floor = saved;
if let Some(r) = self.cached_input_reg {
floor = floor.max(r.saturating_add(1));
}
if let Some(r) = self.cached_context_reg {
floor = floor.max(r.saturating_add(1));
}
self.register_counter = floor;
}
pub(super) fn alloc_register(&mut self) -> Result<u8> {
if self.register_counter == u8::MAX {
bail!("azure-policy compiler exhausted RVM registers");
}
let reg = self.register_counter;
self.register_counter = self.register_counter.saturating_add(1);
if self.register_counter > self.register_high_water {
self.register_high_water = self.register_counter;
}
Ok(reg)
}
pub(super) fn span_info(&mut self, span: &crate::lexer::Span) -> SpanInfo {
let path = span.source.get_path().to_string();
let source_index = if let Some(index) = self.source_to_index.get(path.as_str()) {
*index
} else {
let index = self
.program
.add_source(path.clone(), span.source.get_contents().to_string());
self.source_to_index.insert(path, index);
index
};
SpanInfo::from_lexer_span(span, source_index)
}
pub(super) fn emit(&mut self, instruction: Instruction, span: &crate::lexer::Span) {
let span_info = self.span_info(span);
self.program.add_instruction(instruction, Some(span_info));
}
/// Return the PC (instruction index) that the *next* emitted instruction
/// will occupy.
pub(super) fn current_pc(&self) -> Result<u16> {
u16::try_from(self.program.instructions.len())
.map_err(|_| anyhow!("instruction index overflow"))
}
/// Patch tracked instruction indices, setting their `end_pc` field.
pub(super) fn patch_end_pc(&mut self, pcs: &[u16], end_pc: u16) -> Result<()> {
for &pc in pcs {
let idx = usize::from(pc);
let instr = self
.program
.instructions
.get_mut(idx)
.ok_or_else(|| anyhow!("patch_end_pc: pc {} out of bounds", pc))?;
match instr {
Instruction::LogicalBlockStart {
end_pc: ref mut ep, ..
}
| Instruction::AllOfNext {
end_pc: ref mut ep, ..
}
| Instruction::AnyOfNext {
end_pc: ref mut ep, ..
} => {
*ep = end_pc;
}
_ => {
bail!("patch_end_pc: unexpected instruction at pc {}", pc);
}
}
}
Ok(())
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code)]
//! `count` / `count.where` loop compilation.
//!
//! Stub — real implementation added in a later commit.
use anyhow::{bail, Result};
use crate::languages::azure_policy::ast::{Condition, CountNode};
use super::core::Compiler;
impl Compiler {
pub(super) fn compile_count(&mut self, _count_node: &CountNode) -> Result<u8> {
let _ = self;
bail!("count compilation not yet implemented")
}
pub(super) const fn try_compile_count_as_any(
&mut self,
_count_node: &CountNode,
_condition: &Condition,
) -> Result<Option<u8>> {
_ = self.register_counter;
Ok(None)
}
}

View File

@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Existence-pattern optimization (count → Any loop).
//!
//! Stub — real implementation added in a later commit.

View File

@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code)]
//! Count-binding resolution and `current()` references.
//!
//! Stub — real implementation added in a later commit.
use anyhow::{bail, Result};
use super::core::{Compiler, CountBinding};
impl Compiler {
pub(super) const fn resolve_count_binding(
&self,
_field_path: &str,
) -> Result<Option<CountBinding>> {
_ = self.register_counter;
Ok(None)
}
pub(super) fn compile_from_binding(
&mut self,
_binding: CountBinding,
_field_path: &str,
_span: &crate::lexer::Span,
) -> Result<u8> {
let _ = self;
bail!("count binding compilation not yet implemented")
}
pub(super) fn compile_current_reference(
&mut self,
_key: &str,
_span: &crate::lexer::Span,
) -> Result<u8> {
let _ = self;
bail!("current() reference not yet implemented")
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code)]
//! Effect compilation (dispatch + cross-resource).
//!
//! Stub — real implementation added in a later commit.
use anyhow::{bail, Result};
use crate::languages::azure_policy::ast::PolicyRule;
use super::core::Compiler;
impl Compiler {
pub(super) fn compile_effect(&mut self, _rule: &PolicyRule) -> Result<u8> {
let _ = self;
bail!("effect compilation not yet implemented")
}
pub(super) fn wrap_effect_result(
&mut self,
_effect_name_reg: u8,
_details_reg: Option<u8>,
_span: &crate::lexer::Span,
) -> Result<u8> {
let _ = self;
bail!("wrap_effect_result not yet implemented")
}
}

View File

@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Modify / Append effect detail compilation.
//!
//! Stub — real implementation added in a later commit.

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code)]
//! Template-expression and call-expression compilation.
//!
//! Stub — real implementation added in a later commit.
use anyhow::{bail, Result};
use crate::languages::azure_policy::ast::{Expr, JsonValue, ValueOrExpr};
use super::core::Compiler;
impl Compiler {
pub(super) fn compile_value_or_expr(
&mut self,
_voe: &ValueOrExpr,
_span: &crate::lexer::Span,
) -> Result<u8> {
let _ = self;
bail!("expression compilation not yet implemented")
}
pub(super) fn compile_json_value(
&mut self,
_value: &JsonValue,
_span: &crate::lexer::Span,
) -> Result<u8> {
let _ = self;
bail!("JSON value compilation not yet implemented")
}
pub(super) fn compile_expr(&mut self, _expr: &Expr) -> Result<u8> {
let _ = self;
bail!("expression compilation not yet implemented")
}
pub(super) fn compile_call_expr(
&mut self,
_span: &crate::lexer::Span,
_func: &str,
_args: &[Expr],
) -> Result<u8> {
let _ = self;
bail!("call expression compilation not yet implemented")
}
pub(super) fn compile_call_args(&mut self, _args: &[Expr]) -> Result<alloc::vec::Vec<u8>> {
let _ = self;
bail!("call args compilation not yet implemented")
}
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code)]
//! Field-kind and resource-path compilation.
//!
//! Stub — real implementation added in a later commit.
use anyhow::{bail, Result};
use crate::languages::azure_policy::ast::FieldKind;
use super::core::Compiler;
impl Compiler {
pub(super) fn compile_field_kind(
&mut self,
_kind: &FieldKind,
_span: &crate::lexer::Span,
) -> Result<u8> {
let _ = self;
bail!("field compilation not yet implemented")
}
pub(super) fn compile_field_path_expression(
&mut self,
_field_path: &str,
_span: &crate::lexer::Span,
) -> Result<u8> {
let _ = self;
bail!("field path compilation not yet implemented")
}
pub(super) fn compile_resource_path_value(
&mut self,
_field_path: &str,
_span: &crate::lexer::Span,
) -> Result<u8> {
let _ = self;
bail!("resource path compilation not yet implemented")
}
pub(super) fn compile_resource_root(&mut self, _span: &crate::lexer::Span) -> Result<u8> {
let _ = self;
bail!("resource root compilation not yet implemented")
}
pub(super) fn compile_field_wildcard_collect(
&mut self,
_field_path: &str,
_span: &crate::lexer::Span,
) -> Result<u8> {
let _ = self;
bail!("wildcard collect not yet implemented")
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code)]
//! Annotation accumulation and metadata population.
//!
//! Stub — real implementation added in a later commit.
use crate::languages::azure_policy::ast::{EffectNode, OperatorKind, PolicyDefinition, PolicyRule};
use super::core::Compiler;
impl Compiler {
pub(super) const fn record_field_kind(&mut self, _name: &str) {
_ = self.register_counter;
}
pub(super) const fn record_alias(&mut self, _path: &str) {
_ = self.register_counter;
}
pub(super) const fn record_tag_name(&mut self, _tag: &str) {
_ = self.register_counter;
}
pub(super) const fn record_operator(&mut self, _kind: &OperatorKind) {
_ = self.register_counter;
}
pub(super) const fn record_resource_type_from_condition(
&mut self,
_condition: &crate::languages::azure_policy::ast::Condition,
) {
_ = self.register_counter;
}
#[allow(clippy::unused_self)]
pub(super) fn resolve_effect_annotation(&self, rule: &PolicyRule) -> alloc::string::String {
rule.then_block.effect.raw.clone()
}
#[allow(clippy::unused_self)]
pub(super) fn resolve_effect_kind(
&self,
effect: &EffectNode,
) -> crate::languages::azure_policy::ast::EffectKind {
effect.kind.clone()
}
pub(super) const fn populate_compiled_annotations(&mut self) {
_ = self.register_counter;
}
pub(super) const fn populate_definition_metadata(&mut self, _defn: &PolicyDefinition) {
_ = self.register_counter;
}
}

View File

@@ -0,0 +1,153 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Azure Policy AST → RVM compiler.
//!
//! The compiler is split across several files:
//! - [`core`]: `Compiler` struct, main pipeline, register/emit helpers
//! - [`conditions`]: constraint / condition / LHS compilation
//! - [`conditions_wildcard`]: implicit allOf for unbound `[*]` fields
//! - [`count`]: `count` / `count.where` loops
//! - [`count_any`]: existence-pattern optimization (count → Any loop)
//! - [`count_bindings`]: count-binding resolution and `current()` references
//! - [`expressions`]: template-expression and call-expression compilation
//! - [`fields`]: field-kind and resource-path compilation
//! - [`template_dispatch`]: ARM template function dispatch
//! - [`effects`]: effect compilation (dispatch + cross-resource)
//! - [`effects_modify_append`]: Modify / Append detail compilation
//! - [`metadata`]: annotation accumulation and population
//! - [`utils`]: pure helper functions (path splitting, JSON conversion)
mod conditions;
mod conditions_wildcard;
mod core;
mod count;
mod count_any;
mod count_bindings;
mod effects;
mod effects_modify_append;
mod expressions;
mod fields;
mod metadata;
mod template_dispatch;
mod utils;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString as _};
use anyhow::Result;
use crate::languages::azure_policy::ast::{PolicyDefinition, PolicyRule};
use crate::rvm::program::Program;
use crate::{Rc, Value};
use self::core::Compiler;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Initialise compiler language metadata and effect annotation.
fn init_effect_annotation(compiler: &mut Compiler, rule: &PolicyRule) {
compiler.program.metadata.language = "azure_policy".to_string();
let effect = compiler.resolve_effect_annotation(rule);
compiler
.program
.metadata
.annotations
.insert("effect".to_string(), Value::String(effect.as_str().into()));
}
// ---------------------------------------------------------------------------
// Public entry points
// ---------------------------------------------------------------------------
/// Compile a parsed Azure Policy rule into an RVM program.
pub fn compile_policy_rule(rule: &PolicyRule) -> Result<Rc<Program>> {
let mut compiler = Compiler::new();
init_effect_annotation(&mut compiler, rule);
compiler.compile(rule)
}
/// Compile a parsed Azure Policy rule with alias resolution.
///
/// The `alias_map` maps lowercase fully-qualified alias names to their short
/// names. Obtain it from
/// [`AliasRegistry::alias_map()`](crate::languages::azure_policy::aliases::AliasRegistry::alias_map).
pub fn compile_policy_rule_with_aliases(
rule: &PolicyRule,
alias_map: BTreeMap<String, String>,
alias_modifiable: BTreeMap<String, bool>,
) -> Result<Rc<Program>> {
let mut compiler = Compiler::new();
compiler.alias_map = alias_map;
compiler.alias_modifiable = alias_modifiable;
init_effect_annotation(&mut compiler, rule);
compiler.compile(rule)
}
/// Compile a parsed Azure Policy definition into an RVM program.
///
/// This extracts the `policyRule` from the definition and compiles it.
/// Parameter `defaultValue`s are collected so that later compiler passes
/// (effect compilation, metadata population) can reference them.
pub fn compile_policy_definition(defn: &PolicyDefinition) -> Result<Rc<Program>> {
let mut compiler = Compiler::new();
compiler.parameter_defaults = Some(build_parameter_defaults(&defn.parameters)?);
compiler.populate_definition_metadata(defn);
init_effect_annotation(&mut compiler, &defn.policy_rule);
compiler.compile(&defn.policy_rule)
}
/// Compile a parsed Azure Policy definition with alias resolution.
pub fn compile_policy_definition_with_aliases(
defn: &PolicyDefinition,
alias_map: BTreeMap<String, String>,
alias_modifiable: BTreeMap<String, bool>,
) -> Result<Rc<Program>> {
let mut compiler = Compiler::new();
compiler.alias_map = alias_map;
compiler.alias_modifiable = alias_modifiable;
compiler.parameter_defaults = Some(build_parameter_defaults(&defn.parameters)?);
compiler.populate_definition_metadata(defn);
init_effect_annotation(&mut compiler, &defn.policy_rule);
compiler.compile(&defn.policy_rule)
}
/// Compile a parsed Azure Policy definition with alias resolution and
/// optional fallback behaviour for unknown aliases.
///
/// When `alias_fallback_to_raw` is `true`, field paths that do not resolve to
/// a known alias are silently treated as raw property paths.
pub fn compile_policy_definition_with_aliases_opts(
defn: &PolicyDefinition,
alias_map: BTreeMap<String, String>,
alias_modifiable: BTreeMap<String, bool>,
alias_fallback_to_raw: bool,
) -> Result<Rc<Program>> {
let mut compiler = Compiler::new();
compiler.alias_map = alias_map;
compiler.alias_modifiable = alias_modifiable;
compiler.alias_fallback_to_raw = alias_fallback_to_raw;
compiler.parameter_defaults = Some(build_parameter_defaults(&defn.parameters)?);
compiler.populate_definition_metadata(defn);
init_effect_annotation(&mut compiler, &defn.policy_rule);
compiler.compile(&defn.policy_rule)
}
/// Build a `Value::Object` of `{ param_name: defaultValue }` from
/// the parsed parameter definitions.
fn build_parameter_defaults(
params: &[crate::languages::azure_policy::ast::ParameterDefinition],
) -> Result<Value> {
use crate::languages::azure_policy::compiler::utils::json_value_to_runtime;
let mut obj = Value::new_object();
let map = obj.as_object_mut()?;
for param in params {
if let Some(ref default_val) = param.default_value {
let runtime_val = json_value_to_runtime(default_val)?;
map.insert(Value::from(param.name.clone()), runtime_val);
}
}
Ok(obj)
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code)]
//! ARM template function dispatch.
//!
//! Stub — real implementation added in a later commit.
use anyhow::Result;
use crate::languages::azure_policy::ast::Expr;
use super::core::Compiler;
impl Compiler {
pub(super) const fn compile_arm_template_function(
&mut self,
_function_name: &str,
_span: &crate::lexer::Span,
_args: &[Expr],
) -> Result<Option<u8>> {
_ = self.register_counter;
Ok(None)
}
}

View File

@@ -0,0 +1,427 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(dead_code, clippy::pattern_type_mismatch, clippy::redundant_pub_crate)]
//! Free helper functions used by the Azure Policy compiler.
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
use anyhow::{anyhow, bail, Result};
use crate::languages::azure_policy::ast::{Expr, ExprLiteral, JsonValue, ObjectEntry};
use crate::Value;
/// Extract a string literal from an expression, or bail.
pub(super) fn extract_string_literal(expr: &Expr) -> Result<String> {
match expr {
Expr::Literal {
value: ExprLiteral::String(value),
..
} => Ok(value.clone()),
other => bail!("expected string literal argument, found {:?}", other),
}
}
/// Split a count field path at the `[*]` wildcard into `(prefix, optional_suffix)`.
pub(super) fn split_count_wildcard_path(path: &str) -> Result<(String, Option<String>)> {
let wildcard_index = path
.find("[*]")
.ok_or_else(|| anyhow!("count.field must contain [*]: {}", path))?;
let (prefix_str, rest) = path.split_at(wildcard_index);
let prefix = prefix_str.trim_end_matches('.');
if prefix.is_empty() {
bail!(
"count.field must have a non-empty prefix before [*]: {}",
path
);
}
let after_wildcard = &rest[3..];
if after_wildcard.contains("[*]") {
bail!("nested [*] wildcards are not supported: {}", path);
}
let suffix_str = after_wildcard.trim_start_matches('.');
let suffix = if suffix_str.is_empty() {
None
} else {
Some(suffix_str.to_string())
};
Ok((prefix.to_string(), suffix))
}
/// Split a dotted path (without `[*]` wildcards) into its component segments.
///
/// Handles bracket notation:
/// - `tags['key']` → `["tags", "key"]`
/// - `properties['network-acls']` → `["properties", "network-acls"]`
/// - `properties.ipRules[0].value` → `["properties", "ipRules", "0", "value"]`
pub(super) fn split_path_without_wildcards(path: &str) -> Result<Vec<String>> {
if path.trim().is_empty() {
bail!("empty path");
}
if path.contains("[*]") {
bail!(
"wildcard field paths are not supported in this context: {}",
path
);
}
if path.ends_with('.') {
bail!("path must not end with '.': {}", path);
}
let mut parts = Vec::new();
let mut token = String::new();
let mut bracket = String::new();
let mut in_bracket = false;
let mut after_bracket = false;
for ch in path.chars() {
match ch {
'.' if !in_bracket => {
let t = token.trim();
if t.is_empty() && !after_bracket {
bail!("empty segment in path: {}", path);
}
if !t.is_empty() {
parts.push(t.to_string());
}
token.clear();
after_bracket = false;
}
'[' => {
if in_bracket {
bail!("nested brackets in path: {}", path);
}
in_bracket = true;
after_bracket = false;
let t = token.trim();
if !t.is_empty() {
parts.push(t.to_string());
}
token.clear();
}
']' => {
if !in_bracket {
bail!("unexpected closing bracket in path: {}", path);
}
in_bracket = false;
after_bracket = true;
let cleaned = bracket
.trim()
.trim_matches('"')
.trim_matches('\'')
.to_string();
if cleaned.is_empty() {
bail!("empty bracket in path: {}", path);
}
parts.push(cleaned);
bracket.clear();
}
_ => {
if in_bracket {
bracket.push(ch);
} else {
if after_bracket {
bail!("expected '.' or '[' after bracket in path: {}", path);
}
token.push(ch);
}
}
}
}
if in_bracket {
bail!("unclosed bracket in path: {}", path);
}
let t = token.trim();
if !t.is_empty() {
parts.push(t.to_string());
}
Ok(parts)
}
/// Convert a parsed JSON value from the Azure Policy AST into a runtime [`Value`].
pub(crate) fn json_value_to_runtime(value: &JsonValue) -> Result<Value> {
match value {
JsonValue::Null(_) => Ok(Value::Null),
JsonValue::Bool(_, b) => Ok(Value::Bool(*b)),
JsonValue::Number(_, raw) => {
Value::from_numeric_string(raw).map_err(|_| anyhow!("invalid number literal: {}", raw))
}
JsonValue::Str(_, s) => Ok(Value::from(s.clone())),
JsonValue::Array(_, items) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
out.push(json_value_to_runtime(item)?);
}
Ok(Value::from(out))
}
JsonValue::Object(_, entries) => {
let mut obj = Value::new_object();
let map = obj.as_object_mut()?;
for ObjectEntry {
key,
value: entry_value,
..
} in entries
{
map.insert(
Value::from(key.clone()),
json_value_to_runtime(entry_value)?,
);
}
Ok(obj)
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use alloc::vec;
use crate::lexer::Source;
use super::*;
fn dummy_span() -> crate::lexer::Span {
let source = Source::from_contents("test".into(), " ".into()).unwrap();
crate::lexer::Span {
source,
line: 1,
col: 1,
start: 0,
end: 0,
}
}
// -----------------------------------------------------------------------
// extract_string_literal
// -----------------------------------------------------------------------
#[test]
fn extract_string_literal_ok() {
let expr = Expr::Literal {
span: dummy_span(),
value: ExprLiteral::String("hello".into()),
};
assert_eq!(extract_string_literal(&expr).unwrap(), "hello");
}
#[test]
fn extract_string_literal_number_err() {
let expr = Expr::Literal {
span: dummy_span(),
value: ExprLiteral::Number("42".into()),
};
extract_string_literal(&expr).unwrap_err();
}
#[test]
fn extract_string_literal_ident_err() {
let expr = Expr::Ident {
span: dummy_span(),
name: "x".into(),
};
extract_string_literal(&expr).unwrap_err();
}
// -----------------------------------------------------------------------
// json_value_to_runtime
// -----------------------------------------------------------------------
#[test]
fn json_null() {
let v = json_value_to_runtime(&JsonValue::Null(dummy_span())).unwrap();
assert_eq!(v, Value::Null);
}
#[test]
fn json_bool() {
let v = json_value_to_runtime(&JsonValue::Bool(dummy_span(), true)).unwrap();
assert_eq!(v, Value::Bool(true));
}
#[test]
fn json_number_int() {
let v = json_value_to_runtime(&JsonValue::Number(dummy_span(), "42".into())).unwrap();
assert_eq!(v, Value::from(42_i64));
}
#[test]
fn json_number_float() {
let v = json_value_to_runtime(&JsonValue::Number(dummy_span(), "1.5".into())).unwrap();
assert_eq!(v, Value::from(1.5_f64));
}
#[test]
fn json_number_invalid() {
json_value_to_runtime(&JsonValue::Number(dummy_span(), "abc".into())).unwrap_err();
}
#[test]
fn json_string() {
let v = json_value_to_runtime(&JsonValue::Str(dummy_span(), "hello".into())).unwrap();
assert_eq!(v, Value::from("hello".to_string()));
}
#[test]
fn json_array() {
let arr = JsonValue::Array(
dummy_span(),
vec![
JsonValue::Bool(dummy_span(), true),
JsonValue::Null(dummy_span()),
],
);
let v = json_value_to_runtime(&arr).unwrap();
let items = v.as_array().unwrap();
assert_eq!(items.len(), 2);
}
#[test]
fn json_object() {
let obj = JsonValue::Object(
dummy_span(),
vec![ObjectEntry {
key_span: dummy_span(),
key: "k".into(),
value: JsonValue::Bool(dummy_span(), false),
}],
);
let v = json_value_to_runtime(&obj).unwrap();
let map = v.as_object().unwrap();
assert_eq!(map.len(), 1);
}
// -----------------------------------------------------------------------
// split_count_wildcard_path
// -----------------------------------------------------------------------
#[test]
fn wildcard_simple() {
let (prefix, suffix) = split_count_wildcard_path("a.b[*].c").unwrap();
assert_eq!(prefix, "a.b");
assert_eq!(suffix.as_deref(), Some("c"));
}
#[test]
fn wildcard_no_suffix() {
let (prefix, suffix) = split_count_wildcard_path("a[*]").unwrap();
assert_eq!(prefix, "a");
assert_eq!(suffix, None);
}
#[test]
fn wildcard_trailing_dot_prefix() {
let (prefix, _) = split_count_wildcard_path("a.[*].c").unwrap();
assert_eq!(prefix, "a");
}
#[test]
fn wildcard_missing() {
split_count_wildcard_path("a.b.c").unwrap_err();
}
#[test]
fn wildcard_empty_prefix() {
split_count_wildcard_path("[*].c").unwrap_err();
}
#[test]
fn wildcard_nested() {
split_count_wildcard_path("a[*].b[*].c").unwrap_err();
}
// -----------------------------------------------------------------------
// split_path_without_wildcards
// -----------------------------------------------------------------------
#[test]
fn path_simple_dotted() {
assert_eq!(
split_path_without_wildcards("a.b.c").unwrap(),
vec!["a", "b", "c"]
);
}
#[test]
fn path_bracket_quoted() {
assert_eq!(
split_path_without_wildcards("tags['key']").unwrap(),
vec!["tags", "key"]
);
}
#[test]
fn path_bracket_numeric() {
assert_eq!(
split_path_without_wildcards("a[0].b").unwrap(),
vec!["a", "0", "b"]
);
}
#[test]
fn path_single_segment() {
assert_eq!(split_path_without_wildcards("name").unwrap(), vec!["name"]);
}
#[test]
fn path_empty() {
split_path_without_wildcards("").unwrap_err();
}
#[test]
fn path_whitespace_only() {
split_path_without_wildcards(" ").unwrap_err();
}
#[test]
fn path_trailing_dot() {
split_path_without_wildcards("a.").unwrap_err();
}
#[test]
fn path_leading_dot() {
split_path_without_wildcards(".a").unwrap_err();
}
#[test]
fn path_consecutive_dots() {
split_path_without_wildcards("a..b").unwrap_err();
}
#[test]
fn path_wildcard_rejected() {
split_path_without_wildcards("a[*].b").unwrap_err();
}
#[test]
fn path_nested_brackets() {
split_path_without_wildcards("a[[0]]").unwrap_err();
}
#[test]
fn path_stray_close_bracket() {
split_path_without_wildcards("a]b").unwrap_err();
}
#[test]
fn path_unclosed_bracket() {
split_path_without_wildcards("a[0").unwrap_err();
}
#[test]
fn path_empty_bracket() {
split_path_without_wildcards("a[]").unwrap_err();
}
#[test]
fn path_char_after_bracket_without_separator() {
split_path_without_wildcards("a[0]b").unwrap_err();
}
}

View File

@@ -6,6 +6,8 @@
#[allow(clippy::pattern_type_mismatch)] #[allow(clippy::pattern_type_mismatch)]
pub mod aliases; pub mod aliases;
pub mod ast; pub mod ast;
#[cfg(feature = "rvm")]
pub mod compiler;
pub mod expr; pub mod expr;
pub mod parser; pub mod parser;
pub mod strings; pub mod strings;