use std::collections::BTreeMap;
use std::sync::OnceLock;
use serde::{Deserialize, Serialize};
mod generated {
include!("generated/metadata.rs");
}
pub use generated::{CATALOG_REVISION, CATALOG_VERSION};
const CATALOG_JSON: &str = include_str!("generated/catalog.json");
const CATALOG_SCHEMA_JSON: &str = include_str!("../schema/catalog.schema.json");
const PROVIDER_PACK_SCHEMA_JSON: &str = include_str!("../schema/provider-pack.schema.json");
static CATALOG: OnceLock<Catalog> = OnceLock::new();
#[must_use]
pub fn catalog() -> &'static Catalog {
CATALOG.get_or_init(|| {
serde_json::from_str(CATALOG_JSON).expect("generated catalog must match the Rust contract")
})
}
#[must_use]
pub const fn catalog_json() -> &'static str {
CATALOG_JSON
}
#[must_use]
pub const fn catalog_schema_json() -> &'static str {
CATALOG_SCHEMA_JSON
}
#[must_use]
pub const fn provider_pack_schema_json() -> &'static str {
PROVIDER_PACK_SCHEMA_JSON
}
#[must_use]
pub fn icon_svg(asset_path: &str) -> Option<&'static str> {
generated::icon_svg(asset_path)
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Catalog {
#[serde(rename = "$schema")]
pub schema: String,
pub schema_version: String,
pub catalog_version: String,
pub reserved_theme_ids: Vec<String>,
pub fallbacks: CatalogFallbacks,
pub font_metrics: Vec<FontMetrics>,
pub themes: Vec<Theme>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CatalogFallbacks {
pub missing_theme_id: String,
pub missing_icon_id: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FontMetrics {
pub id: String,
pub family: String,
pub version: String,
pub units_per_em: u32,
pub ascent: i32,
pub descent: i32,
pub line_gap: u32,
pub default_advance: u32,
pub wide_advance: u32,
pub wide_ranges: Vec<UnicodeRange>,
pub glyph_advances: BTreeMap<String, u32>,
pub provenance: Provenance,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UnicodeRange {
pub start: String,
pub end: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Theme {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub palette: Palette,
pub typography: Typography,
pub node_kind_fallbacks: NodeKindFallbacks,
pub connector: ConnectorStyle,
pub icons: Vec<Icon>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Palette {
pub canvas: String,
pub surface: String,
pub surface_muted: String,
pub text: String,
pub text_muted: String,
pub border: String,
pub accent: String,
pub danger: String,
pub connector: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Typography {
pub font_metrics_id: String,
pub node_label_size_milli_px: u32,
pub node_detail_size_milli_px: u32,
pub group_label_size_milli_px: u32,
pub edge_label_size_milli_px: u32,
pub line_height_permille: u32,
pub label_weight: u16,
pub detail_weight: u16,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NodeKindFallbacks {
pub actor: NodeVisual,
pub client: NodeVisual,
pub service: NodeVisual,
#[serde(rename = "function")]
pub function_: NodeVisual,
pub worker: NodeVisual,
pub database: NodeVisual,
pub cache: NodeVisual,
pub queue: NodeVisual,
pub storage: NodeVisual,
pub external: NodeVisual,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NodeVisual {
pub shape: NodeShape,
pub fill: PaletteToken,
pub stroke: PaletteToken,
pub text: PaletteToken,
pub accent: PaletteToken,
pub corner_radius_milli_px: u32,
pub fallback_icon_id: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum NodeShape {
RoundedRectangle,
Capsule,
Circle,
Cylinder,
Hexagon,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum PaletteToken {
Canvas,
Surface,
SurfaceMuted,
Text,
TextMuted,
Border,
Accent,
Danger,
Connector,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConnectorStyle {
pub stroke: PaletteToken,
pub text: PaletteToken,
pub label_background: PaletteToken,
pub width_milli_px: u32,
pub arrow_size_milli_px: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub dash_milli_px: Option<Vec<u32>>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Icon {
pub id: String,
pub subject: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub asset: IconAsset,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IconAsset {
pub path: String,
pub view_box: [i32; 4],
pub provenance: Provenance,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Provenance {
pub source_url: String,
pub source_revision: String,
pub copyright: String,
pub license_spdx: String,
pub license_file: String,
pub modified: bool,
pub redistribution: Redistribution,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Redistribution {
pub cargo: bool,
pub npm: bool,
pub wasm: bool,
pub commercial_applications: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderPack {
#[serde(rename = "$schema")]
pub schema: String,
pub schema_version: String,
pub pack_version: String,
pub provider: ProviderPackIdentity,
pub distribution_mode: ProviderPackDistributionMode,
pub source: ProviderPackSource,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub additional_sources: Vec<ProviderPackAdditionalSource>,
pub rights: ProviderPackRights,
pub notice: ProviderPackNotice,
pub icons: Vec<ProviderIcon>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderPackIdentity {
pub id: String,
pub name: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProviderPackDistributionMode {
UserImported,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderPackSource {
pub page_url: String,
pub archive_url: String,
pub archive_sha256: String,
pub release: String,
pub retrieved_at: String,
pub terms_url: String,
pub terms_reviewed_at: String,
pub review_after: String,
pub copyright: String,
pub license_id: String,
pub archive_license_included: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderPackAdditionalSource {
pub id: String,
#[serde(flatten)]
pub source: ProviderPackSource,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderPackRights {
pub terms_acceptance_required: bool,
pub permitted_outputs: Vec<ProviderPackPermittedOutput>,
pub redistribution: ProviderPackRedistribution,
pub processing: ProviderPackProcessing,
pub modification_policy: ProviderPackModificationPolicy,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProviderPackPermittedOutput {
ArchitectureDiagram,
TrainingMaterial,
Documentation,
Whitepaper,
Presentation,
DataSheet,
Poster,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderPackRedistribution {
pub cargo: bool,
pub npm: bool,
pub wasm: bool,
pub web_asset: bool,
pub native_binary: bool,
pub generated_output: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderPackProcessing {
pub local_only: bool,
pub automatic_download: bool,
pub server_upload: bool,
pub preserve_colors: bool,
pub preserve_geometry: bool,
pub product_name_nearby: bool,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProviderPackModificationPolicy {
VisualPreservationOnly,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderPackNotice {
pub attribution: String,
pub terms_summary: String,
pub non_endorsement: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderIcon {
pub id: String,
pub subject: String,
pub product_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub brand_source_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub brand_guidelines_url: Option<String>,
pub recommended_node_kind: ProviderNodeKind,
pub asset: ProviderIconAsset,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProviderNodeKind {
Actor,
Client,
Service,
Function,
Worker,
Database,
Cache,
Queue,
Storage,
External,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderIconAsset {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_id: Option<String>,
pub path: String,
pub original_path: String,
pub view_box: [i32; 4],
pub original_sha256: String,
pub processed_sha256: String,
pub transformations: Vec<ProviderPackTransformation>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProviderPackTransformation {
RemoveMetadata,
InlineStyles,
RemoveUnusedIdentifiers,
NamespaceIdentifiers,
ScaleViewBoxToIntegers,
NormalizeXml,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_catalog_matches_public_metadata() {
let catalog = catalog();
assert_eq!(catalog.schema_version, "1.0");
assert_eq!(catalog.catalog_version, CATALOG_VERSION);
assert!(CATALOG_REVISION.starts_with("sha256:"));
assert_eq!(CATALOG_REVISION.len(), 71);
assert_eq!(icon_svg("assets/missing.svg"), None);
assert_eq!(
catalog
.themes
.iter()
.map(|theme| theme.id.as_str())
.collect::<Vec<_>>(),
["default", "light", "dark"]
);
assert!(
catalog
.themes
.iter()
.flat_map(|theme| &theme.icons)
.all(|icon| icon_svg(&icon.asset.path).is_some())
);
}
#[test]
fn embedded_catalog_round_trips_semantically() {
let reparsed: Catalog = serde_json::from_str(catalog_json()).unwrap();
let serialized = serde_json::to_value(&reparsed).unwrap();
let source: serde_json::Value = serde_json::from_str(catalog_json()).unwrap();
let schema: serde_json::Value = serde_json::from_str(catalog_schema_json()).unwrap();
assert_eq!(serialized, source);
assert_eq!(
schema["$schema"],
"https://json-schema.org/draft/2020-12/schema"
);
let provider_schema: serde_json::Value =
serde_json::from_str(provider_pack_schema_json()).unwrap();
assert_eq!(
provider_schema["$id"],
"https://raw.githubusercontent.com/stack-sh/theme/main/schemas/provider-pack.schema.json"
);
}
#[test]
fn multi_source_provider_pack_round_trips_semantically() {
let source = include_str!("../../../tests/fixtures/provider-pack/multi-source.json");
let pack: ProviderPack = serde_json::from_str(source).unwrap();
assert_eq!(pack.schema_version, "1.1");
assert_eq!(pack.additional_sources.len(), 1);
assert_eq!(pack.additional_sources[0].id, "categories");
assert_eq!(pack.icons[0].asset.source_id.as_deref(), Some("categories"));
assert_eq!(
pack.icons[0].brand_guidelines_url.as_deref(),
Some("https://example.com/acme/brand-guidelines")
);
assert_eq!(
serde_json::to_value(&pack).unwrap(),
serde_json::from_str::<serde_json::Value>(source).unwrap()
);
}
}