use code_ranker_plugin_api::{
PromptTemplate,
attrs::ValueType,
level::{AttributeGroup, AttributeSpec, CycleKindSpec, Direction},
};
use serde::Deserialize;
use std::collections::BTreeMap;
use std::sync::LazyLock;
static BUILTIN_TOML: &str = include_str!("../metrics/builtin.toml");
static PROMPT_MD: &str = include_str!("../metrics/prompt.md");
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct FieldDef {
#[serde(default = "crate::registry::default_value_type")]
value_type: String,
label: Option<String>,
name: Option<String>,
short: Option<String>,
description: Option<String>,
remediation: Option<String>,
formula_cel: Option<String>,
formula_pretty: Option<String>,
formula_js: Option<String>,
direction: Option<String>,
category: Option<String>,
abbreviate: Option<bool>,
#[serde(default = "crate::registry::default_omit_at")]
omit_at: f64,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct ReportView {
#[serde(default)]
columns: Vec<String>,
#[serde(default)]
default_sort: Vec<String>,
#[serde(default)]
card: Vec<String>,
#[serde(default)]
size: Vec<String>,
#[serde(default)]
filter: Vec<String>,
#[serde(default)]
stats: BTreeMap<String, String>,
}
#[derive(Debug, Deserialize)]
struct Builtin {
#[serde(default)]
categories: BTreeMap<String, AttributeGroup>,
#[serde(default)]
ast: BTreeMap<String, FieldDef>,
#[serde(default)]
fields: BTreeMap<String, FieldDef>,
#[serde(default)]
coupling: BTreeMap<String, FieldDef>,
#[serde(default)]
cycles: BTreeMap<String, CycleKindSpec>,
#[serde(default)]
report: ReportView,
}
static BUILTIN: LazyLock<Builtin> =
LazyLock::new(|| toml::from_str(BUILTIN_TOML).expect("metrics/builtin.toml parses"));
mod write;
pub use write::{write_derived, write_metrics};
#[derive(Debug, Clone, Default)]
pub struct Views {
pub columns: Vec<String>,
pub default_sort: Vec<String>,
pub card: Vec<String>,
pub size: Vec<String>,
pub filter: Vec<String>,
}
pub fn views() -> Views {
Views {
columns: BUILTIN.report.columns.clone(),
default_sort: BUILTIN.report.default_sort.clone(),
card: BUILTIN.report.card.clone(),
size: BUILTIN.report.size.clone(),
filter: BUILTIN.report.filter.clone(),
}
}
fn br(s: &str) -> String {
s.replace('\n', "<br>")
}
fn value_type(s: &str) -> ValueType {
match s {
"int" => ValueType::Int,
"bool" => ValueType::Bool,
"str" | "string" => ValueType::Str,
_ => ValueType::Float,
}
}
fn direction(s: Option<&str>) -> Direction {
match s {
Some("lower_better") => Direction::LowerBetter,
Some("higher_better") => Direction::HigherBetter,
_ => Direction::Neutral,
}
}
fn to_spec(d: &FieldDef) -> AttributeSpec {
AttributeSpec {
value_type: value_type(&d.value_type),
label: d.label.clone(),
name: d.name.clone().or_else(|| d.label.clone()),
short: d.short.clone().or_else(|| d.label.clone()),
description: d.description.as_deref().map(br),
remediation: d.remediation.as_deref().map(br),
formula: d.formula_pretty.clone(),
calc: d.formula_js.clone(),
direction: direction(d.direction.as_deref()),
abbreviate: d.abbreviate,
group: d.category.clone(),
thresholds: None,
omit_at: d.omit_at,
}
}
pub fn metric_specs() -> (
BTreeMap<String, AttributeSpec>,
BTreeMap<String, AttributeGroup>,
) {
let mut specs = BTreeMap::new();
for (k, d) in &BUILTIN.ast {
if d.label.is_some() {
specs.insert(k.clone(), to_spec(d));
}
}
for (k, d) in &BUILTIN.fields {
specs.insert(k.clone(), to_spec(d));
}
(specs, BUILTIN.categories.clone())
}
pub fn coupling_specs() -> (
BTreeMap<String, AttributeSpec>,
BTreeMap<String, AttributeGroup>,
) {
let specs = BUILTIN
.coupling
.iter()
.map(|(k, d)| (k.clone(), to_spec(d)))
.collect();
let mut groups = BTreeMap::new();
if let Some(g) = BUILTIN.categories.get("coupling") {
groups.insert("coupling".to_string(), g.clone());
}
(specs, groups)
}
pub fn cycle_specs() -> BTreeMap<String, CycleKindSpec> {
BUILTIN.cycles.clone()
}
pub fn prompt_template() -> PromptTemplate {
parse_prompt(PROMPT_MD)
}
pub fn prompt_template_from(md: &str) -> PromptTemplate {
parse_prompt(md)
}
fn parse_prompt(md: &str) -> PromptTemplate {
let mut t = PromptTemplate::default();
let mut field = String::new();
let mut body: Vec<&str> = Vec::new();
let flush = |field: &str, body: &[&str], t: &mut PromptTemplate| {
let nonblank = || body.iter().filter(|l| !l.trim().is_empty());
match field {
"intro" => t.intro = nonblank().cloned().collect::<Vec<_>>().join(" "),
"doc_note" => t.doc_note = nonblank().cloned().collect::<Vec<_>>().join(" "),
"focus" => t.focus = nonblank().cloned().collect::<Vec<_>>().join(" "),
"cycle_note" => t.cycle_note = nonblank().cloned().collect::<Vec<_>>().join(" "),
"task" => t.task = nonblank().map(|l| l.trim_end().to_string()).collect(),
_ => {}
}
};
for line in md.lines() {
if let Some(h) = line.strip_prefix("## ") {
flush(&field, &body, &mut t);
field = h.trim().to_string();
body.clear();
} else if !field.is_empty() {
body.push(line);
}
}
flush(&field, &body, &mut t);
t
}
pub fn stat_keys() -> Vec<String> {
BUILTIN
.report
.stats
.iter()
.filter(|(k, formula)| **formula == format!("agg('{k}', 'avg', 'not_empty')"))
.map(|(k, _)| k.clone())
.collect()
}
pub fn aggregate_formulas() -> BTreeMap<String, String> {
BUILTIN.report.stats.clone()
}
#[cfg(test)]
#[path = "builtin_test.rs"]
mod tests;