use std::collections::HashMap;
use std::sync::Arc;
type ValidatorFn = Arc<dyn Fn(&serde_json::Value) -> bool + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ComponentId(pub String);
impl ComponentId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
}
impl std::fmt::Display for ComponentId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for ComponentId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Default)]
pub struct ComponentRegistry {
templates: HashMap<ComponentId, String>,
insertion_order: Vec<ComponentId>,
validators: HashMap<ComponentId, ValidatorFn>,
}
impl ComponentRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with_standard_components() -> Self {
let mut registry = Self::new();
registry.register_standard_components();
registry
}
pub fn register_standard_components(&mut self) {
self.register(
ComponentId::new("score-card"),
include_str!("../../templates/components/score_card.typ").to_string(),
);
self.register(
ComponentId::new("finding"),
include_str!("../../templates/components/finding.typ").to_string(),
);
self.register(
ComponentId::new("audit-table"),
include_str!("../../templates/components/audit_table.typ").to_string(),
);
self.register(
ComponentId::new("section"),
include_str!("../../templates/components/section.typ").to_string(),
);
self.register(
ComponentId::new("image"),
include_str!("../../templates/components/image.typ").to_string(),
);
self.register(
ComponentId::new("callout"),
include_str!("../../templates/components/callout.typ").to_string(),
);
self.register(
ComponentId::new("summary-box"),
include_str!("../../templates/components/summary_box.typ").to_string(),
);
self.register(
ComponentId::new("list"),
include_str!("../../templates/components/list.typ").to_string(),
);
self.register(
ComponentId::new("divider"),
include_str!("../../templates/components/divider.typ").to_string(),
);
self.register(
ComponentId::new("progress-bar"),
include_str!("../../templates/components/progress_bar.typ").to_string(),
);
self.register(
ComponentId::new("key-value-list"),
include_str!("../../templates/components/key_value_list.typ").to_string(),
);
self.register(
ComponentId::new("watermark"),
include_str!("../../templates/components/watermark.typ").to_string(),
);
self.register(
ComponentId::new("page-break"),
include_str!("../../templates/components/page_break.typ").to_string(),
);
self.register(
ComponentId::new("chart"),
include_str!("../../templates/components/chart.typ").to_string(),
);
self.register(
ComponentId::new("sparkline"),
include_str!("../../templates/components/sparkline.typ").to_string(),
);
self.register(
ComponentId::new("gauge"),
include_str!("../../templates/components/gauge.typ").to_string(),
);
self.register(
ComponentId::new("crosstab"),
include_str!("../../templates/components/crosstab.typ").to_string(),
);
self.register(
ComponentId::new("pivot-table"),
include_str!("../../templates/components/pivot_table.typ").to_string(),
);
self.register(
ComponentId::new("barcode"),
include_str!("../../templates/components/barcode.typ").to_string(),
);
self.register(
ComponentId::new("label"),
include_str!("../../templates/components/label.typ").to_string(),
);
self.register(
ComponentId::new("textblock"),
include_str!("../../templates/components/text.typ").to_string(),
);
self.register(
ComponentId::new("number-field"),
include_str!("../../templates/components/number_field.typ").to_string(),
);
self.register(
ComponentId::new("date-field"),
include_str!("../../templates/components/date_field.typ").to_string(),
);
self.register(
ComponentId::new("resource-field"),
include_str!("../../templates/components/resource_field.typ").to_string(),
);
self.register(
ComponentId::new("side-label"),
include_str!("../../templates/components/side_label.typ").to_string(),
);
self.register(
ComponentId::new("table-of-contents"),
include_str!("../../templates/components/table_of_contents.typ").to_string(),
);
self.register(
ComponentId::new("metric-card"),
include_str!("../../templates/components/metric_card.typ").to_string(),
);
self.register(
ComponentId::new("hero-summary"),
include_str!("../../templates/components/hero_summary.typ").to_string(),
);
self.register(
ComponentId::new("product-hero"),
include_str!("../../templates/components/product_hero.typ").to_string(),
);
self.register(
ComponentId::new("card-dashboard"),
include_str!("../../templates/components/card_dashboard.typ").to_string(),
);
self.register(
ComponentId::new("roadmap-block"),
include_str!("../../templates/components/roadmap_block.typ").to_string(),
);
self.register(
ComponentId::new("severity-overview"),
include_str!("../../templates/components/severity_overview.typ").to_string(),
);
self.register(
ComponentId::new("cover-page"),
include_str!("../../templates/components/cover_page.typ").to_string(),
);
self.register(
ComponentId::new("module-comparison"),
include_str!("../../templates/components/module_comparison.typ").to_string(),
);
self.register(
ComponentId::new("portfolio-summary"),
include_str!("../../templates/components/portfolio_summary.typ").to_string(),
);
self.register(
ComponentId::new("benchmark-table"),
include_str!("../../templates/components/benchmark_table.typ").to_string(),
);
self.register(
ComponentId::new("eyebrow"),
include_str!("../../templates/components/eyebrow.typ").to_string(),
);
self.register(
ComponentId::new("status-pill"),
include_str!("../../templates/components/status_pill.typ").to_string(),
);
self.register(
ComponentId::new("stat"),
include_str!("../../templates/components/stat.typ").to_string(),
);
self.register(
ComponentId::new("stat-pair"),
include_str!("../../templates/components/stat_pair.typ").to_string(),
);
self.register(
ComponentId::new("score-band"),
include_str!("../../templates/components/score_band.typ").to_string(),
);
self.register(
ComponentId::new("trend-tile"),
include_str!("../../templates/components/trend_tile.typ").to_string(),
);
self.register(
ComponentId::new("section-header-split"),
include_str!("../../templates/components/section_header_split.typ").to_string(),
);
self.register(
ComponentId::new("phase-block"),
include_str!("../../templates/components/phase_block.typ").to_string(),
);
self.register(
ComponentId::new("checklist-panel"),
include_str!("../../templates/components/checklist_panel.typ").to_string(),
);
self.register(
ComponentId::new("metric-strip"),
include_str!("../../templates/components/metric_strip.typ").to_string(),
);
self.register(
ComponentId::new("impact-grid"),
include_str!("../../templates/components/impact_grid.typ").to_string(),
);
self.register(
ComponentId::new("spotlight-card"),
include_str!("../../templates/components/spotlight_card.typ").to_string(),
);
self.register(
ComponentId::new("comparison-block"),
include_str!("../../templates/components/comparison_block.typ").to_string(),
);
self.register(
ComponentId::new("comparison-cluster"),
include_str!("../../templates/components/comparison_cluster.typ").to_string(),
);
self.register(
ComponentId::new("feature-grid"),
include_str!("../../templates/components/feature_grid.typ").to_string(),
);
self.register(
ComponentId::new("cta-box"),
include_str!("../../templates/components/cta_box.typ").to_string(),
);
self.register(
ComponentId::new("testimonial"),
include_str!("../../templates/components/testimonial.typ").to_string(),
);
self.register(
ComponentId::new("process-flow"),
include_str!("../../templates/components/process_flow.typ").to_string(),
);
self.register(
ComponentId::new("timeline"),
include_str!("../../templates/components/timeline.typ").to_string(),
);
self.register(
ComponentId::new("funnel"),
include_str!("../../templates/components/funnel.typ").to_string(),
);
self.register(
ComponentId::new("problem-solution"),
include_str!("../../templates/components/problem_solution.typ").to_string(),
);
self.register(
ComponentId::new("before-after"),
include_str!("../../templates/components/before_after.typ").to_string(),
);
self.register(
ComponentId::new("why-it-matters"),
include_str!("../../templates/components/why_it_matters.typ").to_string(),
);
self.register(
ComponentId::new("fact-box"),
include_str!("../../templates/components/fact_box.typ").to_string(),
);
self.register(
ComponentId::new("quote-block"),
include_str!("../../templates/components/quote_block.typ").to_string(),
);
self.register(
ComponentId::new("benefit-strip"),
include_str!("../../templates/components/benefit_strip.typ").to_string(),
);
self.register(
ComponentId::new("pricing-card"),
include_str!("../../templates/components/pricing_card.typ").to_string(),
);
self.register(
ComponentId::new("recommendation-card"),
include_str!("../../templates/components/recommendation_card.typ").to_string(),
);
self.register(
ComponentId::new("step-card-row"),
include_str!("../../templates/components/step_card_row.typ").to_string(),
);
self.register(
ComponentId::new("columns"),
include_str!("../../templates/components/columns.typ").to_string(),
);
self.register(
ComponentId::new("faq-list"),
include_str!("../../templates/components/faq_list.typ").to_string(),
);
self.register(
ComponentId::new("use-case-card"),
include_str!("../../templates/components/use_case_card.typ").to_string(),
);
self.register(
ComponentId::new("logo-strip"),
include_str!("../../templates/components/logo_strip.typ").to_string(),
);
self.register(
ComponentId::new("pull-quote"),
include_str!("../../templates/components/pull_quote.typ").to_string(),
);
self.register(
ComponentId::new("big-number"),
include_str!("../../templates/components/big_number.typ").to_string(),
);
self.register(
ComponentId::new("glossary-list"),
include_str!("../../templates/components/glossary_list.typ").to_string(),
);
self.register(
ComponentId::new("diagnosis-panel"),
include_str!("../../templates/components/diagnosis_panel.typ").to_string(),
);
self.register(
ComponentId::new("dominant-issue-spotlight"),
include_str!("../../templates/components/dominant_issue_spotlight.typ").to_string(),
);
self.register(
ComponentId::new("wrong-right-block"),
include_str!("../../templates/components/wrong_right_block.typ").to_string(),
);
self.register(
ComponentId::new("grid-component"),
include_str!("../../templates/components/grid.typ").to_string(),
);
self.register(
ComponentId::new("flow-group"),
include_str!("../../templates/components/flow_group.typ").to_string(),
);
}
pub fn register(&mut self, id: ComponentId, template: String) {
if !self.templates.contains_key(&id) {
self.insertion_order.push(id.clone());
}
self.templates.insert(id, template);
}
pub fn register_with_validator(
&mut self,
id: ComponentId,
template: String,
validator: impl Fn(&serde_json::Value) -> bool + Send + Sync + 'static,
) {
self.templates.insert(id.clone(), template);
self.validators.insert(id, Arc::new(validator));
}
pub fn get_template(&self, id: &ComponentId) -> Option<&String> {
self.templates.get(id)
}
pub fn has_component(&self, id: &ComponentId) -> bool {
self.templates.contains_key(id)
}
pub fn validate(&self, id: &ComponentId, data: &serde_json::Value) -> bool {
self.validators.get(id).map(|v| v(data)).unwrap_or(true)
}
pub fn list_components(&self) -> Vec<&ComponentId> {
self.insertion_order.iter().collect()
}
}
impl std::fmt::Debug for ComponentRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ComponentRegistry")
.field("templates", &self.templates.keys().collect::<Vec<_>>())
.field("validators_count", &self.validators.len())
.finish()
}
}