use indexmap::IndexMap;
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, Deserialize, Default)]
pub struct Blueprint {
#[serde(default)]
pub settings: Settings,
#[serde(default)]
pub nodes: IndexMap<String, NodeSpec>,
#[serde(default)]
pub compute: Vec<ComputeOp>,
}
#[derive(Debug, Deserialize, Default)]
pub struct Settings {
#[serde(default, alias = "root")]
pub input_root: Option<String>,
#[serde(default)]
pub output_path: Option<String>,
#[serde(default, alias = "output")]
pub output_file: Option<String>,
#[serde(default)]
pub auto_purge: bool,
}
impl Settings {
pub fn resolved_output(&self, input_root: &std::path::Path) -> Option<PathBuf> {
let output_file = self.output_file.as_ref()?;
let base = match &self.output_path {
Some(p) => std::path::PathBuf::from(p),
None => input_root.to_path_buf(),
};
Some(base.join(output_file))
}
}
#[derive(Debug, Deserialize, Default)]
pub struct NodeSpec {
#[serde(default)]
pub csv: Option<String>,
#[serde(default)]
pub pk: Option<String>,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub parent: Option<String>,
#[serde(default)]
pub parent_fk: Option<String>,
#[serde(default)]
pub properties: IndexMap<String, String>,
#[serde(default)]
pub skipped: Vec<String>,
#[serde(default)]
pub filter: IndexMap<String, serde_json::Value>,
#[serde(default)]
pub connections: Connections,
#[serde(default)]
pub sub_nodes: IndexMap<String, NodeSpec>,
#[serde(default)]
pub timeseries: Option<TimeseriesSpec>,
}
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Connections {
#[serde(default)]
pub fk_edges: IndexMap<String, FkEdge>,
#[serde(default)]
pub junction_edges: IndexMap<String, JunctionEdge>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct FkEdge {
pub target: String,
pub fk: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct JunctionEdge {
pub csv: String,
pub source_fk: String,
pub target: String,
pub target_fk: String,
#[serde(default)]
pub properties: Vec<String>,
#[serde(default)]
pub property_types: IndexMap<String, String>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum TimeKey {
Single(String),
Composite(IndexMap<String, String>),
}
#[derive(Debug, Deserialize)]
pub struct TimeseriesSpec {
pub time_key: TimeKey,
#[serde(default)]
pub channels: IndexMap<String, String>,
#[serde(default)]
pub resolution: Option<String>,
#[serde(default)]
pub units: IndexMap<String, String>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "op", rename_all = "lowercase")]
pub enum ComputeOp {
Derive {
from: String,
set: IndexMap<String, String>,
},
Filter {
from: String,
#[serde(rename = "where")]
where_expr: String,
#[serde(default)]
into: Option<String>,
},
Chain {
from: String,
group_by: Vec<String>,
order_by: String,
edge: String,
},
Calendar {
#[serde(rename = "type", default = "default_calendar_type")]
node_type: String,
start: String,
end: String,
#[serde(default = "default_next_day_edge")]
next_edge: String,
#[serde(default)]
in_month_edge: Option<String>,
#[serde(default)]
in_quarter_edge: Option<String>,
#[serde(default)]
in_year_edge: Option<String>,
#[serde(default)]
links: Vec<CalendarLink>,
},
Aggregate {
from: String,
group_by: Vec<String>,
into: String,
agg: IndexMap<String, String>,
#[serde(default)]
edges: Vec<AggregateEdge>,
},
}
#[derive(Debug, Deserialize, Clone)]
pub struct CalendarLink {
pub from: String,
pub date_col: String,
pub edge: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct AggregateEdge {
pub to: String,
pub fk: String,
pub edge: String,
}
fn default_calendar_type() -> String {
"Date".to_string()
}
fn default_next_day_edge() -> String {
"NEXT_DAY".to_string()
}
pub fn load_blueprint_file(path: &std::path::Path) -> Result<Blueprint, String> {
let bytes = std::fs::read(path)
.map_err(|e| format!("Blueprint file not found: {}: {}", path.display(), e))?;
serde_json::from_slice(&bytes).map_err(|e| format!("Invalid blueprint JSON: {}", e))
}