mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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:
committed by
GitHub
parent
ce235356bc
commit
f727096a1d
20
src/languages/azure_policy/compiler/conditions.rs
Normal file
20
src/languages/azure_policy/compiler/conditions.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
195
src/languages/azure_policy/compiler/core.rs
Normal file
195
src/languages/azure_policy/compiler/core.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/languages/azure_policy/compiler/count.rs
Normal file
29
src/languages/azure_policy/compiler/count.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/languages/azure_policy/compiler/count_any.rs
Normal file
6
src/languages/azure_policy/compiler/count_any.rs
Normal 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.
|
||||||
40
src/languages/azure_policy/compiler/count_bindings.rs
Normal file
40
src/languages/azure_policy/compiler/count_bindings.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/languages/azure_policy/compiler/effects.rs
Normal file
30
src/languages/azure_policy/compiler/effects.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
53
src/languages/azure_policy/compiler/expressions.rs
Normal file
53
src/languages/azure_policy/compiler/expressions.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/languages/azure_policy/compiler/fields.rs
Normal file
56
src/languages/azure_policy/compiler/fields.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/languages/azure_policy/compiler/metadata.rs
Normal file
52
src/languages/azure_policy/compiler/metadata.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
153
src/languages/azure_policy/compiler/mod.rs
Normal file
153
src/languages/azure_policy/compiler/mod.rs
Normal 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)
|
||||||
|
}
|
||||||
25
src/languages/azure_policy/compiler/template_dispatch.rs
Normal file
25
src/languages/azure_policy/compiler/template_dispatch.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
427
src/languages/azure_policy/compiler/utils.rs
Normal file
427
src/languages/azure_policy/compiler/utils.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user