use super::OutputFormat;
use super::doc::Doc;
use super::renderer::Writer;
use crate::PathDisplayExt;
pub(crate) fn apply_jsonpath(value: &serde_json::Value, expr: &str) -> String {
let expr = expr.trim();
let expr = expr.strip_prefix('{').unwrap_or(expr);
let expr = expr.strip_suffix('}').unwrap_or(expr);
let expr = expr.strip_prefix('.').unwrap_or(expr);
if expr.is_empty() {
return format_jsonpath_result(value);
}
let results = walk_jsonpath(value, expr);
match results.len() {
0 => String::new(),
1 => format_jsonpath_result(results[0]),
_ => {
results
.iter()
.map(|v| format_jsonpath_result(v))
.collect::<Vec<_>>()
.join("\n")
}
}
}
pub(crate) fn name_from_value(value: &serde_json::Value) -> Option<String> {
for key in &["name", "context", "phase", "resourceType", "url"] {
if let Some(s) = value.get(key).and_then(|v| v.as_str()) {
return Some(s.to_string());
}
}
for key in &["applyId"] {
if let Some(n) = value.get(key).and_then(|v| v.as_i64()) {
return Some(n.to_string());
}
}
None
}
pub(crate) fn walk_jsonpath<'a>(
value: &'a serde_json::Value,
path: &str,
) -> Vec<&'a serde_json::Value> {
if path.is_empty() {
return vec![value];
}
let (segment, rest) = split_jsonpath_segment(path);
if let Some(bracket_pos) = segment.find('[') {
let key = &segment[..bracket_pos];
let bracket_expr = &segment[bracket_pos + 1..segment.len() - 1];
let target = if key.is_empty() {
value
} else {
match value.get(key) {
Some(v) => v,
None => return vec![],
}
};
let arr = match target.as_array() {
Some(a) => a,
None => return vec![],
};
if bracket_expr == "*" {
arr.iter()
.flat_map(|elem| walk_jsonpath(elem, rest))
.collect()
} else if let Some(colon_pos) = bracket_expr.find(':') {
let start = bracket_expr[..colon_pos]
.parse::<usize>()
.unwrap_or(0)
.min(arr.len());
let end = bracket_expr[colon_pos + 1..]
.parse::<usize>()
.unwrap_or(arr.len())
.min(arr.len());
if start >= end {
vec![]
} else if rest.is_empty() {
arr[start..end].iter().collect()
} else {
arr[start..end]
.iter()
.flat_map(|elem| walk_jsonpath(elem, rest))
.collect()
}
} else {
let idx: usize = match bracket_expr.parse() {
Ok(i) => i,
Err(_) => return vec![],
};
match arr.get(idx) {
Some(elem) => walk_jsonpath(elem, rest),
None => vec![],
}
}
} else {
match value.get(segment) {
Some(v) => walk_jsonpath(v, rest),
None => vec![],
}
}
}
pub(crate) fn split_jsonpath_segment(path: &str) -> (&str, &str) {
let mut in_bracket = false;
for (i, c) in path.char_indices() {
match c {
'[' => in_bracket = true,
']' => in_bracket = false,
'.' if !in_bracket => {
return (&path[..i], &path[i + 1..]);
}
_ => {}
}
}
(path, "")
}
pub(crate) fn format_jsonpath_result(value: &serde_json::Value) -> String {
match value {
serde_json::Value::Null => String::new(),
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Number(n) => n.to_string(),
_ => serde_json::to_string_pretty(value).unwrap_or_default(),
}
}
pub(crate) fn emit_structured(sink_stdout: &dyn Writer, doc: &Doc, format: &OutputFormat) -> bool {
match format {
OutputFormat::Table | OutputFormat::Wide => false,
OutputFormat::Json => {
let v = doc.data_or_self_json();
let text = serde_json::to_string_pretty(&v).unwrap_or_default();
sink_stdout.write_line(&text);
true
}
OutputFormat::Yaml => {
let v = doc.data_or_self_json();
let yaml = serde_yaml::to_string(&v).unwrap_or_default();
let trimmed = yaml.strip_prefix("---\n").unwrap_or(&yaml);
sink_stdout.write_line(trimmed.trim_end());
true
}
OutputFormat::Name => {
let v = doc.data_or_self_json();
match v {
serde_json::Value::Array(arr) => {
for item in arr {
if let Some(name) = name_from_value(&item) {
sink_stdout.write_line(&name);
}
}
}
obj => {
if let Some(name) = name_from_value(&obj) {
sink_stdout.write_line(&name);
}
}
}
true
}
OutputFormat::Jsonpath(expr) => {
let v = doc.data_or_self_json();
sink_stdout.write_line(&apply_jsonpath(&v, expr));
true
}
OutputFormat::Template(tmpl) => {
render_template_to(sink_stdout, doc, tmpl);
true
}
OutputFormat::TemplateFile(path) => {
let path = crate::expand_tilde(path);
match std::fs::read_to_string(&path) {
Ok(tmpl) => render_template_to(sink_stdout, doc, &tmpl),
Err(e) => {
sink_stdout.write_line(&format!(
"failed to read template file '{}': {}",
path.posix(),
e
));
}
}
true
}
}
}
fn render_template_to(sink_stdout: &dyn Writer, doc: &Doc, template: &str) {
let v = doc.data_or_self_json();
let mut tera = tera::Tera::default();
let tmpl_name = "__inline__";
if let Err(e) = tera.add_raw_template(tmpl_name, template) {
sink_stdout.write_line(&format!("invalid template: {e}"));
return;
}
match v {
serde_json::Value::Array(arr) => {
for item in arr {
match tera::Context::from_value(item.clone()) {
Ok(ctx) => match tera.render(tmpl_name, &ctx) {
Ok(rendered) => sink_stdout.write_line(&rendered),
Err(e) => sink_stdout.write_line(&format!("template render error: {e}")),
},
Err(e) => sink_stdout.write_line(&format!("template context error: {e}")),
}
}
}
other => match tera::Context::from_value(other) {
Ok(ctx) => match tera.render(tmpl_name, &ctx) {
Ok(rendered) => sink_stdout.write_line(&rendered),
Err(e) => sink_stdout.write_line(&format!("template render error: {e}")),
},
Err(e) => sink_stdout.write_line(&format!("template context error: {e}")),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::renderer::StringSink;
use std::sync::{Arc, Mutex};
fn capture() -> (Arc<Mutex<String>>, StringSink) {
let buf = Arc::new(Mutex::new(String::new()));
let sink = StringSink(buf.clone());
(buf, sink)
}
fn take(buf: &Arc<Mutex<String>>) -> String {
buf.lock().unwrap().clone()
}
fn fixture() -> serde_json::Value {
serde_json::json!({
"items": [
{"name": "alpha", "value": 1},
{"name": "beta", "value": 2},
{"name": "gamma", "value": 3},
],
"nested": {"inner": {"field": "deep"}},
"applyId": 42,
})
}
#[test]
fn apply_jsonpath_strips_brace_and_dot_prefix() {
assert_eq!(apply_jsonpath(&fixture(), "{.applyId}"), "42");
}
#[test]
fn apply_jsonpath_traverses_nested_objects() {
assert_eq!(apply_jsonpath(&fixture(), ".nested.inner.field"), "deep");
}
#[test]
fn apply_jsonpath_indexes_into_array() {
assert_eq!(apply_jsonpath(&fixture(), ".items[0].name"), "alpha");
}
#[test]
fn apply_jsonpath_wildcards_collect_all_elements() {
assert_eq!(
apply_jsonpath(&fixture(), ".items[*].name"),
"alpha\nbeta\ngamma"
);
}
#[test]
fn apply_jsonpath_slices_array_range() {
let v = fixture();
let items = v["items"].as_array().unwrap();
let expected = format!(
"{}\n{}",
serde_json::to_string_pretty(&items[0]).unwrap(),
serde_json::to_string_pretty(&items[1]).unwrap(),
);
assert_eq!(apply_jsonpath(&v, ".items[0:2]"), expected);
}
#[test]
fn apply_jsonpath_slice_with_empty_range_returns_empty() {
assert_eq!(apply_jsonpath(&fixture(), ".items[3:3]"), "");
}
#[test]
fn apply_jsonpath_slice_start_past_end_returns_empty() {
assert_eq!(apply_jsonpath(&fixture(), ".items[10:20]"), "");
}
#[test]
fn apply_jsonpath_returns_empty_for_missing_key() {
assert_eq!(apply_jsonpath(&fixture(), ".nonexistent"), "");
}
#[test]
fn apply_jsonpath_returns_empty_for_invalid_array_index() {
assert_eq!(apply_jsonpath(&fixture(), ".items[99].name"), "");
}
#[test]
fn apply_jsonpath_returns_empty_for_non_numeric_index() {
assert_eq!(apply_jsonpath(&fixture(), ".items[abc].name"), "");
}
#[test]
fn apply_jsonpath_empty_expr_returns_root_formatted() {
let v = fixture();
let expected = serde_json::to_string_pretty(&v).unwrap();
assert_eq!(apply_jsonpath(&v, ""), expected);
}
#[test]
fn apply_jsonpath_braces_optional() {
assert_eq!(apply_jsonpath(&fixture(), "applyId"), "42");
}
#[test]
fn name_from_value_picks_name_first() {
let v = serde_json::json!({"name": "X", "context": "Y"});
assert_eq!(name_from_value(&v), Some("X".to_string()));
}
#[test]
fn name_from_value_falls_back_to_context_when_no_name() {
let v = serde_json::json!({"context": "ctx"});
assert_eq!(name_from_value(&v), Some("ctx".to_string()));
}
#[test]
fn name_from_value_falls_back_to_phase() {
let v = serde_json::json!({"phase": "apply"});
assert_eq!(name_from_value(&v), Some("apply".to_string()));
}
#[test]
fn name_from_value_falls_back_to_resource_type() {
let v = serde_json::json!({"resourceType": "Module"});
assert_eq!(name_from_value(&v), Some("Module".to_string()));
}
#[test]
fn name_from_value_falls_back_to_url() {
let v = serde_json::json!({"url": "https://x"});
assert_eq!(name_from_value(&v), Some("https://x".to_string()));
}
#[test]
fn name_from_value_falls_back_to_apply_id_numeric() {
let v = serde_json::json!({"applyId": 7});
assert_eq!(name_from_value(&v), Some("7".to_string()));
}
#[test]
fn name_from_value_returns_none_when_no_known_keys() {
let v = serde_json::json!({"unrelated": "x"});
assert_eq!(name_from_value(&v), None);
}
fn doc_with(value: serde_json::Value) -> Doc {
Doc::new().with_data(value)
}
#[test]
fn emit_structured_table_returns_false_and_writes_nothing() {
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!({"name": "x"}));
let handled = emit_structured(&sink, &doc, &OutputFormat::Table);
assert!(!handled);
assert_eq!(take(&buf), "");
}
#[test]
fn emit_structured_wide_returns_false_and_writes_nothing() {
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!({"name": "x"}));
let handled = emit_structured(&sink, &doc, &OutputFormat::Wide);
assert!(!handled);
assert_eq!(take(&buf), "");
}
#[test]
fn emit_structured_json_writes_pretty_json() {
let (buf, sink) = capture();
let payload = serde_json::json!({"name": "x", "n": 1});
let doc = doc_with(payload.clone());
let handled = emit_structured(&sink, &doc, &OutputFormat::Json);
assert!(handled);
let expected = format!("{}\n", serde_json::to_string_pretty(&payload).unwrap());
assert_eq!(take(&buf), expected);
}
#[test]
fn emit_structured_yaml_strips_leading_doc_marker() {
let (buf, sink) = capture();
let payload = serde_json::json!({"name": "x", "n": 1});
let doc = doc_with(payload.clone());
let handled = emit_structured(&sink, &doc, &OutputFormat::Yaml);
assert!(handled);
let yaml_raw = serde_yaml::to_string(&payload).unwrap();
let expected_body = yaml_raw
.strip_prefix("---\n")
.unwrap_or(&yaml_raw)
.trim_end();
let captured = take(&buf);
assert!(
!captured.starts_with("---\n"),
"expected leading doc-marker stripped, got: {captured:?}"
);
assert_eq!(captured, format!("{expected_body}\n"));
}
#[test]
fn emit_structured_name_writes_name_for_object() {
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!({"name": "x"}));
let handled = emit_structured(&sink, &doc, &OutputFormat::Name);
assert!(handled);
assert_eq!(take(&buf), "x\n");
}
#[test]
fn emit_structured_name_writes_name_per_element_for_array() {
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!([
{"name": "alpha"},
{"name": "beta"},
]));
let handled = emit_structured(&sink, &doc, &OutputFormat::Name);
assert!(handled);
assert_eq!(take(&buf), "alpha\nbeta\n");
}
#[test]
fn emit_structured_name_skips_elements_with_no_known_keys() {
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!([
{"name": "alpha"},
{"unrelated": "skipped"},
{"name": "gamma"},
]));
let handled = emit_structured(&sink, &doc, &OutputFormat::Name);
assert!(handled);
assert_eq!(take(&buf), "alpha\ngamma\n");
}
#[test]
fn emit_structured_jsonpath_writes_result_of_apply_jsonpath() {
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!({"name": "x"}));
let handled = emit_structured(&sink, &doc, &OutputFormat::Jsonpath("{.name}".into()));
assert!(handled);
assert_eq!(take(&buf), "x\n");
}
#[test]
fn emit_structured_template_renders_inline_template() {
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!({"name": "alpha"}));
let handled = emit_structured(
&sink,
&doc,
&OutputFormat::Template("name={{ name }}".into()),
);
assert!(handled);
assert_eq!(take(&buf), "name=alpha\n");
}
#[test]
fn emit_structured_template_renders_per_array_element() {
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!([
{"name": "alpha"},
{"name": "beta"},
]));
let handled = emit_structured(&sink, &doc, &OutputFormat::Template("{{ name }}".into()));
assert!(handled);
assert_eq!(take(&buf), "alpha\nbeta\n");
}
#[test]
fn emit_structured_template_invalid_template_writes_error_line() {
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!({"name": "x"}));
let handled = emit_structured(&sink, &doc, &OutputFormat::Template("{{ ".into()));
assert!(handled);
let captured = take(&buf);
assert!(
captured.starts_with("invalid template: "),
"expected 'invalid template:' prefix, got: {captured:?}"
);
assert_eq!(
captured.lines().count(),
1,
"expected exactly one captured line"
);
}
#[test]
fn emit_structured_template_render_error_writes_error_line() {
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!({"foo": "bar"}));
let handled = emit_structured(
&sink,
&doc,
&OutputFormat::Template("{{ foo | NONEXISTENT }}".into()),
);
assert!(handled);
let captured = take(&buf);
assert!(
captured.starts_with("template render error: "),
"expected 'template render error:' prefix, got: {captured:?}"
);
assert_eq!(
captured.lines().count(),
1,
"expected exactly one captured line"
);
}
#[test]
fn emit_structured_template_file_missing_writes_error_line() {
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!({"name": "x"}));
let missing = std::path::PathBuf::from("/nonexistent/cfgd-structured-test/path.tmpl");
let handled = emit_structured(&sink, &doc, &OutputFormat::TemplateFile(missing));
assert!(handled);
let captured = take(&buf);
assert!(
captured.starts_with("failed to read template file"),
"expected 'failed to read template file' prefix, got: {captured:?}"
);
}
#[test]
fn emit_structured_template_file_reads_and_renders() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("greeting.tera");
std::fs::write(&path, "hello {{ name }}").unwrap();
let (buf, sink) = capture();
let doc = doc_with(serde_json::json!({"name": "alpha"}));
let handled = emit_structured(&sink, &doc, &OutputFormat::TemplateFile(path));
assert!(handled);
assert_eq!(take(&buf), "hello alpha\n");
}
}