Skip to main content

pointlock_store/projection/
schema.rs

1//! Projection JSON Schema generation — the same pipeline leg as the IR's
2//! `schema_gen` (02 §1.1, spine §10.2): Rust DTOs → JSON Schema →
3//! `@pointlock/projection-types` + golden fixtures.
4//!
5//! Unlike the IR schema, projection schemas do NOT strip the null branch
6//! schemars adds for `Option<T>`: the projection deliberately carries
7//! BOTH option regimes of the ledger — absent-when-none fields (never
8//! emitted as null) and explicit-null fields (`supervisePolicy`,
9//! suspension `reason` — the ledger is self-describing there). A schema
10//! that accepts null-or-absent covers both regimes; the wire never emits
11//! anything the schema rejects.
12
13use schemars::{JsonSchema, generate::SchemaSettings};
14use serde_json::Value;
15
16use super::{FlowGraphView, HumanInboxEntry, RunOverview, StepDossierView, TimelinePage};
17
18/// One schema family of the projection protocol (closed five — spine
19/// §10.1; the timeline family's root is the page envelope, which embeds
20/// `RunTimelineEntry`).
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ProjectionSchemaFamily {
23    /// `FlowGraphView`.
24    FlowGraph,
25    /// `TimelinePage` (embeds `RunTimelineEntry`).
26    RunTimeline,
27    /// `StepDossierView`.
28    StepDossier,
29    /// `HumanInboxEntry`.
30    HumanInbox,
31    /// `RunOverview`.
32    RunOverview,
33}
34
35/// The closed family list, in canonical emission order.
36pub const PROJECTION_SCHEMA_FAMILIES: [ProjectionSchemaFamily; 5] = [
37    ProjectionSchemaFamily::FlowGraph,
38    ProjectionSchemaFamily::RunTimeline,
39    ProjectionSchemaFamily::StepDossier,
40    ProjectionSchemaFamily::HumanInbox,
41    ProjectionSchemaFamily::RunOverview,
42];
43
44impl ProjectionSchemaFamily {
45    /// Kebab-case artifact stem (`<stem>.schema.json`).
46    pub fn stem(self) -> &'static str {
47        match self {
48            ProjectionSchemaFamily::FlowGraph => "flow-graph-view",
49            ProjectionSchemaFamily::RunTimeline => "run-timeline",
50            ProjectionSchemaFamily::StepDossier => "step-dossier-view",
51            ProjectionSchemaFamily::HumanInbox => "human-inbox-entry",
52            ProjectionSchemaFamily::RunOverview => "run-overview",
53        }
54    }
55
56    /// Root type title.
57    pub fn title(self) -> &'static str {
58        match self {
59            ProjectionSchemaFamily::FlowGraph => "FlowGraphView",
60            ProjectionSchemaFamily::RunTimeline => "TimelinePage",
61            ProjectionSchemaFamily::StepDossier => "StepDossierView",
62            ProjectionSchemaFamily::HumanInbox => "HumanInboxEntry",
63            ProjectionSchemaFamily::RunOverview => "RunOverview",
64        }
65    }
66
67    /// The pinned `$id` URN (versioned with `projectionVersion`).
68    pub fn schema_id(self) -> String {
69        format!("urn:pointlock:schema:projection:v1:{}", self.stem())
70    }
71}
72
73fn root_schema<T: JsonSchema>(family: ProjectionSchemaFamily) -> Value {
74    let settings = SchemaSettings::draft2020_12();
75    let mut generator = settings.into_generator();
76    let schema = generator.root_schema_for::<T>();
77    let mut doc = serde_json::to_value(&schema).expect("schema serializes to JSON");
78    if let Value::Object(root) = &mut doc {
79        root.insert("$id".to_owned(), Value::String(family.schema_id()));
80        root.insert("title".to_owned(), Value::String(family.title().to_owned()));
81    }
82    doc
83}
84
85/// Generates one family's schema.
86pub fn projection_schema(family: ProjectionSchemaFamily) -> Value {
87    match family {
88        ProjectionSchemaFamily::FlowGraph => root_schema::<FlowGraphView>(family),
89        ProjectionSchemaFamily::RunTimeline => root_schema::<TimelinePage>(family),
90        ProjectionSchemaFamily::StepDossier => root_schema::<StepDossierView>(family),
91        ProjectionSchemaFamily::HumanInbox => root_schema::<HumanInboxEntry>(family),
92        ProjectionSchemaFamily::RunOverview => root_schema::<RunOverview>(family),
93    }
94}
95
96/// Generates all five families, in canonical order.
97pub fn projection_schemas() -> Vec<(ProjectionSchemaFamily, Value)> {
98    PROJECTION_SCHEMA_FAMILIES
99        .iter()
100        .map(|&family| (family, projection_schema(family)))
101        .collect()
102}