use super::Kind;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValueProvider {
Text,
Closed(&'static [&'static str]),
Number,
HexColor,
Path { glob: &'static str },
DbMetricNames,
DbLabelKeys,
DbLabelKeyValuePairs,
Json,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Directive {
pub cli_flag: &'static str,
pub yaml_directive: &'static str,
pub yaml_form: YamlForm,
pub applies_to: KindMask,
pub target: DirectiveTarget,
pub value: ValueProvider,
pub repeatable: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum YamlForm {
Whitespace,
Equals,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectiveTarget {
ItemLabel,
ItemAsStem,
StyleField,
StyleSeries,
Destinations,
Body,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KindMask(pub u8);
impl KindMask {
pub const PLOT: KindMask = KindMask(1 << 0);
pub const TABLE: KindMask = KindMask(1 << 1);
pub const TEXT: KindMask = KindMask(1 << 2);
pub const FILE: KindMask = KindMask(1 << 3);
pub const DETAILS: KindMask = KindMask(1 << 4);
pub const FIGURES: KindMask = KindMask(0b00011);
pub const ALL: KindMask = KindMask(0b11111);
pub const fn contains(self, kind: Kind) -> bool {
(self.0 & kind_bit(kind)) != 0
}
pub const fn or(self, other: KindMask) -> KindMask {
KindMask(self.0 | other.0)
}
}
const fn kind_bit(kind: Kind) -> u8 {
match kind {
Kind::Plot => 1 << 0,
Kind::Table => 1 << 1,
Kind::Text => 1 << 2,
Kind::File => 1 << 3,
Kind::Details => 1 << 4,
}
}
pub const AGG_FNS: &[&str] = &["mean", "min", "max", "p50", "p99", "sum", "count"];
pub const PALETTE_NAMES: &[&str] = &[
"wong",
"cividis_5",
"ibm",
"tol_bright",
"tol_high_contrast",
"tol_light",
"tol_muted",
"viridis_5",
];
pub const LINE_STYLES: &[&str] = &["solid", "dashed", "dotted", "dashdot", "none"];
pub const MARKER_SHAPES: &[&str] = &[
"auto", "none", "circle", "square", "triangle", "diamond", "plus", "cross",
];
pub const AXIS_SCALES: &[&str] = &["linear", "log", "dec", "bin"];
pub const DESTINATION_NAMES: &[&str] = &["sessiondir", "stdout", "stderr", "none"];
pub const ALL_DIRECTIVES: &[Directive] = &[
Directive {
cli_flag: "--label",
yaml_directive: "label",
yaml_form: YamlForm::Whitespace,
applies_to: KindMask::ALL,
target: DirectiveTarget::ItemLabel,
value: ValueProvider::Text,
repeatable: false,
},
Directive {
cli_flag: "--as",
yaml_directive: "as",
yaml_form: YamlForm::Whitespace,
applies_to: KindMask::FIGURES,
target: DirectiveTarget::ItemAsStem,
value: ValueProvider::Text,
repeatable: false,
},
Directive {
cli_flag: "--to",
yaml_directive: "to",
yaml_form: YamlForm::Whitespace,
applies_to: KindMask::ALL,
target: DirectiveTarget::Destinations,
value: ValueProvider::Closed(DESTINATION_NAMES),
repeatable: false,
},
Directive {
cli_flag: "--palette",
yaml_directive: "palette",
yaml_form: YamlForm::Equals,
applies_to: KindMask::FIGURES,
target: DirectiveTarget::StyleField,
value: ValueProvider::Closed(PALETTE_NAMES),
repeatable: false,
},
Directive {
cli_flag: "--line",
yaml_directive: "line",
yaml_form: YamlForm::Equals,
applies_to: KindMask::PLOT,
target: DirectiveTarget::StyleField,
value: ValueProvider::Closed(LINE_STYLES),
repeatable: false,
},
Directive {
cli_flag: "--width",
yaml_directive: "width",
yaml_form: YamlForm::Equals,
applies_to: KindMask::PLOT,
target: DirectiveTarget::StyleField,
value: ValueProvider::Number,
repeatable: false,
},
Directive {
cli_flag: "--scale",
yaml_directive: "scale",
yaml_form: YamlForm::Equals,
applies_to: KindMask::PLOT,
target: DirectiveTarget::StyleField,
value: ValueProvider::Number,
repeatable: false,
},
Directive {
cli_flag: "--marker",
yaml_directive: "marker",
yaml_form: YamlForm::Equals,
applies_to: KindMask::PLOT,
target: DirectiveTarget::StyleField,
value: ValueProvider::Closed(MARKER_SHAPES),
repeatable: false,
},
Directive {
cli_flag: "--size",
yaml_directive: "size",
yaml_form: YamlForm::Equals,
applies_to: KindMask::PLOT,
target: DirectiveTarget::StyleField,
value: ValueProvider::Number,
repeatable: false,
},
Directive {
cli_flag: "--color",
yaml_directive: "color",
yaml_form: YamlForm::Equals,
applies_to: KindMask::FIGURES,
target: DirectiveTarget::StyleField,
value: ValueProvider::HexColor,
repeatable: false,
},
Directive {
cli_flag: "--figure-width",
yaml_directive: "figure_width",
yaml_form: YamlForm::Equals,
applies_to: KindMask::FIGURES,
target: DirectiveTarget::StyleField,
value: ValueProvider::Number,
repeatable: false,
},
Directive {
cli_flag: "--figure-height",
yaml_directive: "figure_height",
yaml_form: YamlForm::Equals,
applies_to: KindMask::FIGURES,
target: DirectiveTarget::StyleField,
value: ValueProvider::Number,
repeatable: false,
},
Directive {
cli_flag: "--metric",
yaml_directive: "metric",
yaml_form: YamlForm::Whitespace,
applies_to: KindMask::FIGURES,
target: DirectiveTarget::Body,
value: ValueProvider::DbMetricNames,
repeatable: false,
},
Directive {
cli_flag: "--over",
yaml_directive: "over",
yaml_form: YamlForm::Whitespace,
applies_to: KindMask::FIGURES,
target: DirectiveTarget::Body,
value: ValueProvider::DbLabelKeys,
repeatable: false,
},
Directive {
cli_flag: "--by",
yaml_directive: "by",
yaml_form: YamlForm::Whitespace,
applies_to: KindMask::FIGURES,
target: DirectiveTarget::Body,
value: ValueProvider::DbLabelKeys,
repeatable: false,
},
Directive {
cli_flag: "--where",
yaml_directive: "where",
yaml_form: YamlForm::Whitespace,
applies_to: KindMask::FIGURES,
target: DirectiveTarget::Body,
value: ValueProvider::DbLabelKeyValuePairs,
repeatable: false,
},
Directive {
cli_flag: "--agg",
yaml_directive: "agg",
yaml_form: YamlForm::Equals,
applies_to: KindMask::FIGURES,
target: DirectiveTarget::Body,
value: ValueProvider::Closed(AGG_FNS),
repeatable: false,
},
Directive {
cli_flag: "--xlabel",
yaml_directive: "xlabel",
yaml_form: YamlForm::Equals,
applies_to: KindMask::PLOT,
target: DirectiveTarget::Body,
value: ValueProvider::Text,
repeatable: false,
},
Directive {
cli_flag: "--ylabel",
yaml_directive: "ylabel",
yaml_form: YamlForm::Equals,
applies_to: KindMask::PLOT,
target: DirectiveTarget::Body,
value: ValueProvider::Text,
repeatable: false,
},
Directive {
cli_flag: "--x-scale",
yaml_directive: "x-scale",
yaml_form: YamlForm::Equals,
applies_to: KindMask::PLOT,
target: DirectiveTarget::Body,
value: ValueProvider::Closed(AXIS_SCALES),
repeatable: false,
},
Directive {
cli_flag: "--y-scale",
yaml_directive: "y-scale",
yaml_form: YamlForm::Equals,
applies_to: KindMask::PLOT,
target: DirectiveTarget::Body,
value: ValueProvider::Closed(AXIS_SCALES),
repeatable: false,
},
Directive {
cli_flag: "--style",
yaml_directive: "style",
yaml_form: YamlForm::Whitespace,
applies_to: KindMask::FIGURES,
target: DirectiveTarget::StyleSeries,
value: ValueProvider::Json,
repeatable: true,
},
];
pub fn directives_for(kind: Kind) -> impl Iterator<Item = &'static Directive> {
ALL_DIRECTIVES
.iter()
.filter(move |d| d.applies_to.contains(kind))
}
pub fn directive_by_cli_flag(flag: &str) -> Option<&'static Directive> {
ALL_DIRECTIVES.iter().find(|d| {
d.cli_flag == flag
|| d.cli_flag
.strip_prefix("--")
.is_some_and(|stripped| stripped == flag)
})
}
pub fn directive_by_yaml_keyword(keyword: &str) -> Option<&'static Directive> {
ALL_DIRECTIVES.iter().find(|d| d.yaml_directive == keyword)
}
pub fn cli_flags_for(kind: Kind) -> Vec<&'static str> {
directives_for(kind).map(|d| d.cli_flag).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_directive_has_a_unique_cli_flag() {
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
for d in ALL_DIRECTIVES {
assert!(
seen.insert(d.cli_flag),
"duplicate cli_flag '{}'",
d.cli_flag
);
}
}
#[test]
fn every_directive_has_a_unique_yaml_keyword() {
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
for d in ALL_DIRECTIVES {
assert!(
seen.insert(d.yaml_directive),
"duplicate yaml_directive '{}'",
d.yaml_directive
);
}
}
#[test]
fn every_cli_flag_starts_with_double_dash() {
for d in ALL_DIRECTIVES {
assert!(
d.cli_flag.starts_with("--"),
"cli_flag '{}' missing -- prefix",
d.cli_flag
);
}
}
#[test]
fn directives_for_plot_includes_axis_directives() {
let plot: Vec<&str> = directives_for(Kind::Plot).map(|d| d.cli_flag).collect();
assert!(plot.contains(&"--xlabel"));
assert!(plot.contains(&"--ylabel"));
assert!(plot.contains(&"--x-scale"));
assert!(plot.contains(&"--y-scale"));
assert!(plot.contains(&"--marker"));
assert!(plot.contains(&"--line"));
}
#[test]
fn directives_for_table_excludes_axis_and_marker_directives() {
let table: Vec<&str> = directives_for(Kind::Table).map(|d| d.cli_flag).collect();
assert!(!table.contains(&"--xlabel"));
assert!(!table.contains(&"--ylabel"));
assert!(!table.contains(&"--x-scale"));
assert!(!table.contains(&"--y-scale"));
assert!(!table.contains(&"--marker"));
assert!(!table.contains(&"--line"));
assert!(!table.contains(&"--width"));
assert!(!table.contains(&"--size"));
}
#[test]
fn directives_for_figures_share_data_shape_flags() {
for kind in [Kind::Plot, Kind::Table] {
let flags: Vec<&str> = directives_for(kind).map(|d| d.cli_flag).collect();
for required in ["--over", "--by", "--where", "--agg", "--metric"] {
assert!(
flags.contains(&required),
"{kind:?} missing data-shape flag {required}"
);
}
}
}
#[test]
fn directives_for_text_excludes_figure_directives() {
let text: Vec<&str> = directives_for(Kind::Text).map(|d| d.cli_flag).collect();
assert!(!text.contains(&"--over"));
assert!(!text.contains(&"--metric"));
assert!(!text.contains(&"--agg"));
assert!(text.contains(&"--label"));
}
#[test]
fn directive_by_cli_flag_round_trips() {
for d in ALL_DIRECTIVES {
let found = directive_by_cli_flag(d.cli_flag)
.unwrap_or_else(|| panic!("missing {}", d.cli_flag));
assert_eq!(found, d);
let stripped = d.cli_flag.trim_start_matches("--");
let found_stripped = directive_by_cli_flag(stripped)
.unwrap_or_else(|| panic!("missing stripped {}", stripped));
assert_eq!(found_stripped, d);
}
}
#[test]
fn directive_by_yaml_keyword_round_trips() {
for d in ALL_DIRECTIVES {
assert_eq!(directive_by_yaml_keyword(d.yaml_directive), Some(d));
}
assert_eq!(directive_by_yaml_keyword("nonexistent"), None);
}
#[test]
fn closed_sets_are_non_empty_and_sorted_or_explicit() {
let mut sorted = PALETTE_NAMES.to_vec();
sorted.sort();
assert!(!PALETTE_NAMES.is_empty());
for set in [AGG_FNS, LINE_STYLES, MARKER_SHAPES, AXIS_SCALES] {
assert!(!set.is_empty());
}
}
#[test]
fn style_is_repeatable_other_directives_are_not() {
let style = directive_by_cli_flag("--style").unwrap();
assert!(style.repeatable, "--style must be repeatable");
for d in ALL_DIRECTIVES {
if d.cli_flag != "--style" {
assert!(!d.repeatable, "{} should not be repeatable", d.cli_flag);
}
}
}
#[test]
fn kind_mask_figures_covers_plot_and_table_only() {
let m = KindMask::FIGURES;
assert!(m.contains(Kind::Plot));
assert!(m.contains(Kind::Table));
assert!(!m.contains(Kind::Text));
assert!(!m.contains(Kind::File));
assert!(!m.contains(Kind::Details));
}
}