#![allow(clippy::pattern_type_mismatch)]
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;
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),
}
}
pub(super) fn split_count_wildcard_path(path: &str) -> Result<(String, Option<String>)> {
let wildcard_index = path
.find("[*]")
.ok_or_else(|| anyhow!("wildcard path must contain [*]: {}", path))?;
let (prefix_str, rest) = path.split_at(wildcard_index);
let prefix = prefix_str.trim_end_matches('.');
if prefix.is_empty() {
bail!(
"wildcard path must have a non-empty prefix before [*]: {}",
path
);
}
let after_wildcard = rest.strip_prefix("[*]").ok_or_else(|| {
anyhow!(
"wildcard path could not be parsed after [*] split: {}",
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))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum PathSegment {
Key(String),
Index(u64),
}
fn strip_matching_outer_quotes(s: &str) -> Option<&str> {
s.strip_prefix('\'')
.and_then(|inner| inner.strip_suffix('\''))
.or_else(|| {
s.strip_prefix('"')
.and_then(|inner| inner.strip_suffix('"'))
})
}
pub(super) fn split_path_without_wildcards(path: &str) -> Result<Vec<PathSegment>> {
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(PathSegment::Key(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(PathSegment::Key(t.to_string()));
}
token.clear();
}
']' => {
if !in_bracket {
bail!("unexpected closing bracket in path: {}", path);
}
in_bracket = false;
after_bracket = true;
let raw = bracket.trim();
if raw.is_empty() {
bail!("empty bracket in path: {}", path);
}
let segment = if let Some(key) = strip_matching_outer_quotes(raw) {
if key.is_empty() {
bail!("empty quoted key in path: {}", path);
}
PathSegment::Key(key.to_string())
} else {
raw.parse::<u64>()
.map_or_else(|_| PathSegment::Key(raw.to_string()), PathSegment::Index)
};
parts.push(segment);
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(PathSegment::Key(t.to_string()));
}
Ok(parts)
}
pub(super) 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) => {
s.strip_prefix("[[").map_or_else(
|| Ok(Value::from(s.clone())),
|unescaped| Ok(Value::from(alloc::format!("[{unescaped}"))),
)
}
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,
}
}
#[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();
}
#[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_string_double_bracket_escape() {
let v = json_value_to_runtime(&JsonValue::Str(dummy_span(), "[[escaped]".into())).unwrap();
assert_eq!(v, Value::from("[escaped]".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);
}
#[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() {
let (prefix, suffix) = split_count_wildcard_path("a[*].b[*].c").unwrap();
assert_eq!(prefix, "a");
assert_eq!(suffix.as_deref(), Some("b[*].c"));
}
fn key(name: &str) -> PathSegment {
PathSegment::Key(name.to_string())
}
#[test]
fn path_simple_dotted() {
assert_eq!(
split_path_without_wildcards("a.b.c").unwrap(),
vec![key("a"), key("b"), key("c")]
);
}
#[test]
fn path_bracket_quoted() {
assert_eq!(
split_path_without_wildcards("tags['key']").unwrap(),
vec![key("tags"), key("key")]
);
}
#[test]
fn path_bracket_numeric() {
assert_eq!(
split_path_without_wildcards("a[0].b").unwrap(),
vec![key("a"), PathSegment::Index(0), key("b")]
);
}
#[test]
fn path_bracket_quoted_digits_is_a_key() {
assert_eq!(
split_path_without_wildcards("tags['123']").unwrap(),
vec![key("tags"), key("123")]
);
assert_eq!(
split_path_without_wildcards("tags[\"123\"]").unwrap(),
vec![key("tags"), key("123")]
);
}
#[test]
fn path_empty_quoted_key_is_an_error() {
for path in ["a['']", "a[\"\"]", "a[ '' ]"] {
let err = split_path_without_wildcards(path).unwrap_err().to_string();
assert!(err.contains("empty quoted key in path"), "{path}: {err}");
}
let err = split_path_without_wildcards("a[]").unwrap_err().to_string();
assert!(err.contains("empty bracket in path"), "{err}");
}
#[test]
fn path_bracket_preserves_quotes_inside_delimiters() {
assert_eq!(
split_path_without_wildcards("properties.config[\"'123'\"]").unwrap(),
vec![key("properties"), key("config"), key("'123'")]
);
assert_eq!(
split_path_without_wildcards("properties.config['\"123\"']").unwrap(),
vec![key("properties"), key("config"), key("\"123\"")]
);
}
#[test]
fn path_bracket_preserves_mismatched_quotes() {
assert_eq!(
split_path_without_wildcards("properties.config[\"123']").unwrap(),
vec![key("properties"), key("config"), key("\"123'")]
);
assert_eq!(
split_path_without_wildcards("properties.config['123\"]").unwrap(),
vec![key("properties"), key("config"), key("'123\"")]
);
}
#[test]
fn path_dotted_digits_is_a_key() {
assert_eq!(
split_path_without_wildcards("tags.123").unwrap(),
vec![key("tags"), key("123")]
);
}
#[test]
fn path_single_segment() {
assert_eq!(
split_path_without_wildcards("name").unwrap(),
vec![key("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();
}
}