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
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user