use std::fmt::Write as _;
use super::model::StatKind;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ReportFlow {
pub header: Header,
pub nodes: Vec<FlowNode>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Header {
pub lines: Vec<HeaderLine>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CollectionRef<'a> {
pub reference: &'a str,
pub alias: Option<&'a str>,
}
pub fn split_collection_ref(value: &str) -> (&str, Option<&str>) {
let value = value.trim();
let mut it = value.rsplitn(2, char::is_whitespace);
let (Some(last), Some(head)) = (it.next(), it.next()) else {
return (value, None);
};
let head = head.trim_end();
if head
.rsplit(char::is_whitespace)
.next()
.is_some_and(|w| w.eq_ignore_ascii_case("AS"))
&& !last.is_empty()
{
let reference = head[..head.len() - 2].trim_end();
if !reference.is_empty() {
return (reference, Some(last));
}
}
(value, None)
}
#[cfg_attr(not(feature = "gui"), allow(dead_code))]
pub fn split_label_class(value: &str) -> (&str, &str) {
match value.split_once('=') {
Some((name, synonyms)) => (name.trim(), synonyms.trim()),
None => (value.trim(), ""),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HeaderLine {
Directive { key: String, value: String },
Comment(String),
}
impl Header {
pub fn get(&self, key: &str) -> Option<&str> {
self.lines.iter().find_map(|l| match l {
HeaderLine::Directive { key: k, value } if k.eq_ignore_ascii_case(key) => {
Some(value.as_str())
}
_ => None,
})
}
pub fn get_all(&self, key: &str) -> Vec<&str> {
self.lines
.iter()
.filter_map(|l| match l {
HeaderLine::Directive { key: k, value } if k.eq_ignore_ascii_case(key) => {
Some(value.as_str())
}
_ => None,
})
.collect()
}
pub fn collection(&self) -> Option<&str> {
self.get("collection").map(|v| split_collection_ref(v).0)
}
pub fn collections(&self) -> Vec<CollectionRef<'_>> {
self.get_all("collection")
.into_iter()
.map(|v| {
let (reference, alias) = split_collection_ref(v);
CollectionRef { reference, alias }
})
.collect()
}
pub fn output(&self) -> Option<&str> {
self.get("output")
}
pub fn labels(&self) -> Vec<&str> {
self.get_all("labels")
}
pub fn columns(&self) -> Option<&str> {
self.get("columns")
}
pub fn root(&self) -> Option<&str> {
self.get("root")
}
pub fn baseline(&self) -> Option<&str> {
self.get("baseline")
}
pub fn environment(&self) -> Option<&str> {
self.get("environment")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FlowNode {
Assign { key: String, value: String },
Param(ParamDecl),
ListDecl { name: String, producer: Producer },
Comment(String),
Request { name: String },
Report(ReportStmt),
ForEach {
pattern: Pattern,
producer: Producer,
body: Vec<FlowNode>,
parallel: Option<ParallelSpec>,
},
ForEnvs {
var: String,
clause: EnvClause,
body: Vec<FlowNode>,
parallel: Option<ParallelSpec>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ParallelSpec {
pub degree: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParamDecl {
pub kind: ParamKind,
pub name: String,
pub default: Option<String>,
pub label: Option<String>,
}
impl ParamDecl {
pub fn prompt(&self) -> String {
match &self.label {
Some(l) if !l.trim().is_empty() => l.trim().to_string(),
_ => derive_prompt(&self.name),
}
}
}
fn derive_prompt(name: &str) -> String {
let words: Vec<String> = name
.split('_')
.filter(|w| !w.is_empty())
.map(|w| {
let mixed = w.chars().any(|c| c.is_ascii_uppercase())
&& w.chars().any(|c| c.is_ascii_lowercase());
if mixed {
w.to_string()
} else {
w.to_ascii_lowercase()
}
})
.collect();
let mut out = words.join(" ");
if let Some(first) = out.chars().next()
&& first.is_ascii_lowercase()
&& !out
.split_whitespace()
.next()
.is_some_and(|w| w.chars().any(|c| c.is_ascii_uppercase()))
{
out.replace_range(0..first.len_utf8(), &first.to_ascii_uppercase().to_string());
}
out
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ParamKind {
#[default]
Text,
Number,
Env,
Folder,
File,
Choice(Vec<String>),
}
impl ParamKind {
pub fn keyword(&self) -> &'static str {
match self {
ParamKind::Text => "TEXT",
ParamKind::Number => "NUMBER",
ParamKind::Env => "ENV",
ParamKind::Folder => "FOLDER",
ParamKind::File => "FILE",
ParamKind::Choice(_) => "CHOICE",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReportStmt {
Request {
name: String,
alias: Option<String>,
response_fmt: Option<ResponseFmt>,
show: Vec<ShowField>,
hide: Vec<String>,
with: Vec<WithItem>,
},
Vars(Vec<String>),
VarAs {
var: String,
name: String,
stats: Vec<StatKind>,
image: Option<ImageSpec>,
truth: Option<String>,
detail: bool,
},
Computed {
template: String,
name: String,
stats: Vec<StatKind>,
image: Option<ImageSpec>,
truth: Option<String>,
detail: bool,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ImageSpec {
pub height: Option<u32>,
pub width: Option<u32>,
pub fit: bool,
}
pub const DEFAULT_IMAGE_HEIGHT: u32 = 110;
impl ImageSpec {
pub fn scaled_size(&self, natural: (u32, u32)) -> Option<(f64, f64)> {
if self.fit {
return None;
}
let (nw, nh) = (natural.0.max(1) as f64, natural.1.max(1) as f64);
Some(match (self.width, self.height) {
(Some(w), Some(h)) => (w as f64, h as f64),
(Some(w), None) => (w as f64, w as f64 * nh / nw),
(None, Some(h)) => (h as f64 * nw / nh, h as f64),
(None, None) => {
let h = DEFAULT_IMAGE_HEIGHT as f64;
(h * nw / nh, h)
}
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WithItem {
ResponseFmt(ResponseFmt),
Field {
name: String,
query: String,
stats: Vec<StatKind>,
image: Option<ImageSpec>,
truth: Option<String>,
detail: bool,
},
Comment(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResponseFmt {
Raw,
Pretty,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Producer {
List(Vec<Element>),
Files { dir: String, glob: Option<String> },
Folders {
dir: String,
glob: Option<String>,
roles: Vec<RoleBinding>,
},
Tuples { path: String },
Zip(Vec<Producer>),
Concat(Vec<Producer>),
Named(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoleBinding {
pub name: String,
pub glob: String,
pub optional: bool,
}
#[cfg(test)]
impl RoleBinding {
pub fn required(name: impl Into<String>, glob: impl Into<String>) -> Self {
RoleBinding {
name: name.into(),
glob: glob.into(),
optional: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Element {
Scalar(String),
Tuple(Vec<String>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pattern {
pub binders: Vec<Binder>,
pub rest: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Binder {
Named(String),
Discard,
}
impl Pattern {
pub fn single(name: impl Into<String>) -> Self {
Pattern {
binders: vec![Binder::Named(name.into())],
rest: false,
}
}
pub fn is_single(&self) -> bool {
self.binders.len() == 1 && !self.rest
}
pub fn named(&self) -> impl Iterator<Item = &str> {
self.binders.iter().filter_map(|b| match b {
Binder::Named(n) => Some(n.as_str()),
Binder::Discard => None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoleRef {
Env(String),
File(String),
}
impl RoleRef {
pub fn target(&self) -> &str {
match self {
RoleRef::Env(n) => n,
RoleRef::File(p) => p,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ShowField {
pub field: String,
pub stats: Vec<StatKind>,
}
impl ShowField {
pub fn name(&self) -> &str {
&self.field
}
pub fn to_text(&self) -> String {
format!("{}{}", self.field, stats_text(&self.stats))
}
}
impl From<&str> for ShowField {
fn from(field: &str) -> Self {
ShowField {
field: field.to_string(),
stats: Vec::new(),
}
}
}
impl From<String> for ShowField {
fn from(field: String) -> Self {
ShowField {
field,
stats: Vec::new(),
}
}
}
impl PartialEq<str> for ShowField {
fn eq(&self, other: &str) -> bool {
self.field == other
}
}
impl PartialEq<String> for ShowField {
fn eq(&self, other: &String) -> bool {
self.stats.is_empty() && &self.field == other
}
}
pub(crate) fn show_text(fields: &[ShowField]) -> String {
fields
.iter()
.map(ShowField::to_text)
.collect::<Vec<_>>()
.join(", ")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EnvClause {
Plain(Vec<String>),
Roles {
baseline: Vec<RoleRef>,
comparisons: Vec<RoleRef>,
baseline_show: Vec<ShowField>,
},
}
const INDENT: &str = " ";
impl ReportFlow {
pub fn params(&self) -> Vec<&ParamDecl> {
self.nodes
.iter()
.filter_map(|n| match n {
FlowNode::Param(p) => Some(p),
_ => None,
})
.collect()
}
pub fn to_text(&self) -> String {
let mut out = String::new();
for line in &self.header.lines {
match line {
HeaderLine::Directive { key, value } => {
let _ = writeln!(out, "# {key}: {value}");
}
HeaderLine::Comment(c) => {
let _ = writeln!(out, "#{c}");
}
}
}
if !self.header.lines.is_empty() && !self.nodes.is_empty() {
out.push('\n');
}
for node in &self.nodes {
write_node(&mut out, node, 0);
}
out
}
pub fn column_stats(&self) -> std::collections::HashMap<String, Vec<StatKind>> {
let mut out = std::collections::HashMap::new();
collect_column_stats(&self.nodes, &mut out);
out
}
pub fn column_images(&self) -> std::collections::HashMap<String, ImageSpec> {
let mut out = std::collections::HashMap::new();
collect_column_images(&self.nodes, &mut out);
out
}
pub fn column_truths(&self) -> std::collections::HashMap<String, String> {
let mut out = std::collections::HashMap::new();
collect_column_truths(&self.nodes, &mut out);
out
}
pub fn column_details(&self) -> std::collections::HashSet<String> {
let mut out = std::collections::HashSet::new();
collect_column_details(&self.nodes, &mut out);
out
}
}
fn collect_column_images(
nodes: &[FlowNode],
out: &mut std::collections::HashMap<String, ImageSpec>,
) {
for node in nodes {
match node {
FlowNode::Report(ReportStmt::VarAs { name, image, .. })
| FlowNode::Report(ReportStmt::Computed { name, image, .. }) => {
if let Some(img) = image {
out.insert(name.clone(), *img);
}
}
FlowNode::Report(ReportStmt::Request {
name, alias, with, ..
}) => {
let a = alias
.clone()
.unwrap_or_else(|| name.rsplit('/').next().unwrap_or(name).to_string());
for item in with {
if let WithItem::Field {
name: fname,
image: Some(img),
..
} = item
{
out.insert(format!("{a}.{fname}"), *img);
}
}
}
FlowNode::ForEach { body, .. } | FlowNode::ForEnvs { body, .. } => {
collect_column_images(body, out);
}
_ => {}
}
}
}
fn collect_column_details(nodes: &[FlowNode], out: &mut std::collections::HashSet<String>) {
for node in nodes {
match node {
FlowNode::Report(ReportStmt::VarAs { name, detail, .. })
| FlowNode::Report(ReportStmt::Computed { name, detail, .. }) => {
if *detail {
out.insert(name.clone());
}
}
FlowNode::Report(ReportStmt::Request {
name, alias, with, ..
}) => {
let a = alias
.clone()
.unwrap_or_else(|| name.rsplit('/').next().unwrap_or(name).to_string());
for item in with {
if let WithItem::Field {
name: fname,
detail: true,
..
} = item
{
out.insert(format!("{a}.{fname}"));
}
}
}
FlowNode::ForEach { body, .. } | FlowNode::ForEnvs { body, .. } => {
collect_column_details(body, out);
}
_ => {}
}
}
}
fn collect_column_truths(nodes: &[FlowNode], out: &mut std::collections::HashMap<String, String>) {
for node in nodes {
match node {
FlowNode::Report(ReportStmt::VarAs { name, truth, .. })
| FlowNode::Report(ReportStmt::Computed { name, truth, .. }) => {
if let Some(t) = truth {
out.insert(name.clone(), t.clone());
}
}
FlowNode::Report(ReportStmt::Request {
name, alias, with, ..
}) => {
let a = alias
.clone()
.unwrap_or_else(|| name.rsplit('/').next().unwrap_or(name).to_string());
for item in with {
if let WithItem::Field {
name: fname,
truth: Some(t),
..
} = item
{
out.insert(format!("{a}.{fname}"), t.clone());
}
}
}
FlowNode::ForEach { body, .. } | FlowNode::ForEnvs { body, .. } => {
collect_column_truths(body, out);
}
_ => {}
}
}
}
fn collect_column_stats(
nodes: &[FlowNode],
out: &mut std::collections::HashMap<String, Vec<StatKind>>,
) {
for node in nodes {
match node {
FlowNode::Report(ReportStmt::VarAs { name, stats, .. })
| FlowNode::Report(ReportStmt::Computed { name, stats, .. })
if !stats.is_empty() =>
{
out.insert(name.clone(), stats.clone());
}
FlowNode::Report(ReportStmt::Request {
name,
alias,
with,
show,
..
}) => {
let a = alias
.clone()
.unwrap_or_else(|| name.rsplit('/').next().unwrap_or(name).to_string());
for f in show {
if !f.stats.is_empty() {
out.insert(format!("{a}.{}", f.field), f.stats.clone());
}
}
for item in with {
if let WithItem::Field {
name: fname, stats, ..
} = item
&& !stats.is_empty()
{
out.insert(format!("{a}.{fname}"), stats.clone());
}
}
}
FlowNode::ForEnvs { body, clause, .. } => {
if let EnvClause::Roles { baseline_show, .. } = clause {
for f in baseline_show {
if !f.stats.is_empty() {
out.insert(format!("baseline.*.{}", f.field), f.stats.clone());
}
}
}
collect_column_stats(body, out);
}
FlowNode::ForEach { body, .. } => {
collect_column_stats(body, out);
}
_ => {}
}
}
}
fn indent(out: &mut String, depth: usize) {
for _ in 0..depth {
out.push_str(INDENT);
}
}
fn write_node(out: &mut String, node: &FlowNode, depth: usize) {
indent(out, depth);
match node {
FlowNode::Assign { key, value } => {
let _ = writeln!(out, "{key}={value}");
}
FlowNode::ListDecl { name, producer } => {
let _ = writeln!(out, "LIST {name} = {}", producer_text(producer));
}
FlowNode::Param(p) => {
let _ = writeln!(out, "{}", param_text(p));
}
FlowNode::Comment(text) => {
let _ = writeln!(out, "#{text}");
}
FlowNode::Request { name } => {
let _ = writeln!(out, "REQUEST {}", name_text(name));
}
FlowNode::Report(stmt) => write_report(out, stmt, depth),
FlowNode::ForEach {
pattern,
producer,
body,
parallel,
} => {
let _ = writeln!(
out,
"{}FOR {} IN {}",
parallel_prefix(parallel),
pattern_text(pattern),
producer_text(producer)
);
for n in body {
write_node(out, n, depth + 1);
}
indent(out, depth);
out.push_str("END\n");
}
FlowNode::ForEnvs {
var,
clause,
body,
parallel,
} => {
let _ = writeln!(
out,
"{}FOR {var} IN ENVS {}",
parallel_prefix(parallel),
env_clause_text(clause)
);
for n in body {
write_node(out, n, depth + 1);
}
indent(out, depth);
out.push_str("END\n");
}
}
}
fn parallel_prefix(p: &Option<ParallelSpec>) -> String {
match p {
None => String::new(),
Some(ParallelSpec { degree: None }) => "PARALLEL ".to_string(),
Some(ParallelSpec { degree: Some(n) }) => format!("PARALLEL({n}) "),
}
}
fn write_report(out: &mut String, stmt: &ReportStmt, depth: usize) {
match stmt {
ReportStmt::Request {
name,
alias,
response_fmt,
show,
hide,
with,
} => {
let _ = write!(out, "REPORT REQUEST {}", name_text(name));
if let Some(a) = alias {
let _ = write!(out, " AS {}", name_text(a));
}
if let Some(fmt) = response_fmt {
let _ = write!(out, " RESPONSE {}", fmt_text(*fmt));
}
if !show.is_empty() {
let _ = write!(out, " SHOW({})", show_text(show));
}
if !hide.is_empty() {
let _ = write!(out, " HIDE({})", hide.join(", "));
}
if with.is_empty() {
out.push('\n');
} else {
out.push_str(" WITH\n");
for item in with {
indent(out, depth + 1);
out.push_str(&with_item_text(item));
out.push('\n');
}
indent(out, depth);
out.push_str("END\n");
}
}
ReportStmt::Vars(vars) => {
if vars.len() == 1 {
let _ = writeln!(out, "REPORT {}", vars[0]);
} else {
let _ = writeln!(out, "REPORT ({})", vars.join(", "));
}
}
ReportStmt::VarAs {
var,
name,
stats,
image,
truth,
detail,
} => {
let _ = writeln!(
out,
"REPORT {var} AS {}{}{}{}{}",
name_text(name),
stats_text(stats),
image_text(image.as_ref()),
truth_text(truth.as_deref()),
detail_text(*detail)
);
}
ReportStmt::Computed {
template,
name,
stats,
image,
truth,
detail,
} => {
let _ = writeln!(
out,
"REPORT {} AS {}{}{}{}{}",
quote(template),
name_text(name),
stats_text(stats),
image_text(image.as_ref()),
truth_text(truth.as_deref()),
detail_text(*detail)
);
}
}
}
pub(crate) fn with_item_text(item: &WithItem) -> String {
match item {
WithItem::ResponseFmt(fmt) => format!("RESPONSE {}", fmt_text(*fmt)),
WithItem::Comment(text) => format!("#{text}"),
WithItem::Field {
name,
query,
stats,
image,
truth,
detail,
} => format!(
"{}: {query}{}{}{}{}",
name_text(name),
stats_text(stats),
image_text(image.as_ref()),
truth_text(truth.as_deref()),
detail_text(*detail)
),
}
}
pub(crate) fn detail_text(detail: bool) -> String {
if detail {
" DETAIL".to_string()
} else {
String::new()
}
}
fn stats_text(stats: &[StatKind]) -> String {
if stats.is_empty() {
return String::new();
}
let list: Vec<&str> = stats.iter().map(|s| s.keyword()).collect();
format!(" STATISTICS({})", list.join(", "))
}
pub(crate) fn image_text(image: Option<&ImageSpec>) -> String {
let Some(img) = image else {
return String::new();
};
let mut opts: Vec<String> = Vec::new();
if img.fit {
opts.push("FIT".to_string());
}
if let Some(w) = img.width {
opts.push(format!("WIDTH {w}"));
}
if let Some(h) = img.height {
opts.push(format!("HEIGHT {h}"));
}
if opts.is_empty() {
" IMAGE".to_string()
} else {
format!(" IMAGE({})", opts.join(", "))
}
}
pub(crate) fn truth_text(truth: Option<&str>) -> String {
match truth {
Some(t) => format!(" TRUTH {}", quote(t)),
None => String::new(),
}
}
fn fmt_text(fmt: ResponseFmt) -> &'static str {
match fmt {
ResponseFmt::Raw => "RAW",
ResponseFmt::Pretty => "PRETTY",
}
}
fn pattern_text(p: &Pattern) -> String {
if p.is_single() {
return binder_text(&p.binders[0]);
}
let mut parts: Vec<String> = p.binders.iter().map(binder_text).collect();
if p.rest {
parts.push("...".to_string());
}
format!("({})", parts.join(", "))
}
fn binder_text(b: &Binder) -> String {
match b {
Binder::Named(n) => n.clone(),
Binder::Discard => "_".to_string(),
}
}
fn producer_text(p: &Producer) -> String {
match p {
Producer::List(elems) => {
let items: Vec<String> = elems.iter().map(element_text).collect();
format!("[{}]", items.join(", "))
}
Producer::Files { dir, glob } => match glob {
Some(g) => format!("FILES {} MATCH {}", quote(dir), quote(g)),
None => format!("FILES {}", quote(dir)),
},
Producer::Folders { dir, glob, roles } => {
let mut out = format!("FOLDERS {}", quote(dir));
if let Some(g) = glob {
out.push_str(&format!(" MATCH {}", quote(g)));
}
if !roles.is_empty() {
let rs: Vec<String> = roles
.iter()
.map(|r| {
let opt = if r.optional { "?" } else { "" };
format!("{}={}{opt}", r.name, quote(&r.glob))
})
.collect();
out.push_str(&format!(" WITH {}", rs.join(", ")));
}
out
}
Producer::Tuples { path } => format!("TUPLES FROM {}", quote(path)),
Producer::Zip(ps) => {
let items: Vec<String> = ps.iter().map(producer_text).collect();
format!("ZIP({})", items.join(", "))
}
Producer::Concat(ps) => {
let items: Vec<String> = ps.iter().map(producer_text).collect();
format!("CONCAT({})", items.join(", "))
}
Producer::Named(n) => n.clone(),
}
}
fn element_text(e: &Element) -> String {
match e {
Element::Scalar(s) => quote(s),
Element::Tuple(items) => {
let parts: Vec<String> = items.iter().map(|s| quote(s)).collect();
format!("({})", parts.join(", "))
}
}
}
fn env_clause_text(c: &EnvClause) -> String {
match c {
EnvClause::Plain(names) => names
.iter()
.map(|s| quote(s))
.collect::<Vec<_>>()
.join(", "),
EnvClause::Roles {
baseline,
comparisons,
baseline_show,
} => {
let mut parts = Vec::new();
if !baseline.is_empty() {
let names: Vec<String> = baseline.iter().map(role_ref_text).collect();
let mut token = format!("BASELINE({})", names.join(", "));
if !baseline_show.is_empty() {
token.push_str(&format!(" SHOW({})", show_text(baseline_show)));
}
parts.push(token);
}
if !comparisons.is_empty() {
let names: Vec<String> = comparisons.iter().map(role_ref_text).collect();
parts.push(format!("COMPARISON({})", names.join(", ")));
}
parts.join(", ")
}
}
}
fn role_ref_text(r: &RoleRef) -> String {
match r {
RoleRef::Env(n) => quote(n),
RoleRef::File(p) => format!("FILE({})", quote(p)),
}
}
fn name_text(name: &str) -> String {
if name.is_empty()
|| name
.chars()
.any(|c| c.is_whitespace() || "()[],=\"".contains(c))
{
quote(name)
} else {
name.to_string()
}
}
impl FlowNode {
pub fn label(&self) -> String {
match self {
FlowNode::Assign { key, value } => format!("{key} = {value}"),
FlowNode::ListDecl { name, producer } => {
format!("LIST {name} = {}", producer_text(producer))
}
FlowNode::Param(p) => param_text(p),
FlowNode::Comment(text) => format!("#{text}"),
FlowNode::Request { name } => format!("REQUEST {name}"),
FlowNode::Report(stmt) => report_label(stmt),
FlowNode::ForEach {
pattern,
producer,
parallel,
..
} => format!(
"{}FOR {} IN {}",
parallel_prefix(parallel),
pattern_text(pattern),
producer_text(producer)
),
FlowNode::ForEnvs {
var,
clause,
parallel,
..
} => format!(
"{}FOR {var} IN ENVS {}",
parallel_prefix(parallel),
env_clause_text(clause)
),
}
}
pub fn header_line(&self) -> String {
match self {
FlowNode::Report(ReportStmt::Request {
name,
alias,
response_fmt,
show,
hide,
..
}) => {
let mut out = format!("REPORT REQUEST {}", name_text(name));
if let Some(a) = alias {
let _ = write!(out, " AS {}", name_text(a));
}
if let Some(fmt) = response_fmt {
let _ = write!(out, " RESPONSE {}", fmt_text(*fmt));
}
if !show.is_empty() {
let _ = write!(out, " SHOW({})", show_text(show));
}
if !hide.is_empty() {
let _ = write!(out, " HIDE({})", hide.join(", "));
}
out
}
_ => self.label(),
}
}
pub fn request_name(&self) -> Option<&str> {
match self {
FlowNode::Request { name } => Some(name),
FlowNode::Report(ReportStmt::Request { name, .. }) => Some(name),
_ => None,
}
}
pub fn is_loop(&self) -> bool {
matches!(self, FlowNode::ForEach { .. } | FlowNode::ForEnvs { .. })
}
pub fn body_mut(&mut self) -> Option<&mut Vec<FlowNode>> {
match self {
FlowNode::ForEach { body, .. } | FlowNode::ForEnvs { body, .. } => Some(body),
_ => None,
}
}
}
fn report_label(stmt: &ReportStmt) -> String {
match stmt {
ReportStmt::Request {
name,
alias,
response_fmt,
show,
hide,
with,
} => {
let mut out = format!("REPORT REQUEST {name}");
if let Some(a) = alias {
let _ = write!(out, " AS {a}");
}
if let Some(fmt) = response_fmt {
let _ = write!(out, " RESPONSE {}", fmt_text(*fmt));
}
if !show.is_empty() {
let _ = write!(out, " SHOW({})", show_text(show));
}
if !hide.is_empty() {
let _ = write!(out, " HIDE({})", hide.join(", "));
}
if !with.is_empty() {
out.push_str(" WITH …");
}
out
}
ReportStmt::Vars(vars) => {
if vars.len() == 1 {
format!("REPORT {}", vars[0])
} else {
format!("REPORT ({})", vars.join(", "))
}
}
ReportStmt::VarAs {
var,
name,
stats,
image,
truth,
detail,
} => {
format!(
"REPORT {var} AS {name}{}{}{}{}",
stats_text(stats),
image_text(image.as_ref()),
truth_text(truth.as_deref()),
detail_text(*detail)
)
}
ReportStmt::Computed {
template,
name,
stats,
image,
truth,
detail,
} => {
format!(
"REPORT {} AS {name}{}{}{}{}",
quote(template),
stats_text(stats),
image_text(image.as_ref()),
truth_text(truth.as_deref()),
detail_text(*detail)
)
}
}
}
fn param_text(p: &ParamDecl) -> String {
let mut out = String::from("PARAM ");
out.push_str(p.kind.keyword());
if let ParamKind::Choice(options) = &p.kind {
out.push('(');
for (i, o) in options.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str("e(o));
}
out.push(')');
}
out.push(' ');
out.push_str(&p.name);
if let Some(default) = &p.default {
out.push_str(" = ");
out.push_str("e(default));
}
if let Some(label) = &p.label {
out.push_str(" LABEL ");
out.push_str("e(label));
}
out
}
pub(crate) fn quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
_ => out.push(c),
}
}
out.push('"');
out
}