Skip to main content

panproto_schema/
schema.rs

1//! Core schema data structures.
2//!
3//! A [`Schema`] is a model of a protocol's schema theory GAT. It stores
4//! vertices, binary edges, hyper-edges, constraints, required-edge
5//! declarations, and NSID mappings. Precomputed adjacency indices
6//! (`outgoing`, `incoming`, `between`) enable fast traversal.
7
8use std::collections::HashMap;
9
10use panproto_gat::Name;
11use serde::{Deserialize, Serialize};
12use smallvec::SmallVec;
13
14/// A schema vertex.
15///
16/// Each vertex has a unique `id`, a `kind` drawn from the protocol's
17/// recognized vertex kinds, and an optional NSID (namespace identifier).
18#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct Vertex {
20    /// Unique vertex identifier within the schema.
21    pub id: Name,
22    /// The vertex kind (e.g., `"record"`, `"object"`, `"string"`).
23    pub kind: Name,
24    /// Optional namespace identifier (e.g., `"app.bsky.feed.post"`).
25    pub nsid: Option<Name>,
26}
27
28/// A binary edge between two vertices.
29///
30/// Edges are directed: they go from `src` to `tgt`. The `kind` determines
31/// the structural role (e.g., `"prop"`, `"record-schema"`), and `name`
32/// provides an optional label (e.g., the property name).
33#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
34pub struct Edge {
35    /// Source vertex ID.
36    pub src: Name,
37    /// Target vertex ID.
38    pub tgt: Name,
39    /// Edge kind (e.g., `"prop"`, `"record-schema"`).
40    pub kind: Name,
41    /// Optional edge label (e.g., a property name like `"text"`).
42    pub name: Option<Name>,
43}
44
45/// A hyper-edge (present only when the schema theory includes `ThHypergraph`).
46///
47/// Hyper-edges connect multiple vertices via a labeled signature.
48#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
49pub struct HyperEdge {
50    /// Unique hyper-edge identifier.
51    pub id: Name,
52    /// Hyper-edge kind.
53    pub kind: Name,
54    /// Maps label names to vertex IDs.
55    #[serde(with = "crate::serde_helpers::sorted_map")]
56    pub signature: HashMap<Name, Name>,
57    /// The label that identifies the parent vertex.
58    pub parent_label: Name,
59}
60
61/// A constraint on a vertex.
62///
63/// Constraints restrict the values a vertex can hold (e.g., maximum
64/// string length, format pattern).
65#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
66pub struct Constraint {
67    /// The constraint sort (e.g., `"maxLength"`, `"format"`).
68    pub sort: Name,
69    /// The constraint value (e.g., `"3000"`, `"at-uri"`).
70    pub value: String,
71}
72
73/// A variant in a coproduct (sum type / union).
74///
75/// Each variant is injected into a parent vertex (the union/coproduct)
76/// with an optional discriminant tag.
77#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub struct Variant {
79    /// Unique variant identifier.
80    pub id: Name,
81    /// The parent coproduct vertex this variant belongs to.
82    pub parent_vertex: Name,
83    /// Optional discriminant tag.
84    pub tag: Option<Name>,
85}
86
87/// An ordering annotation on an edge.
88///
89/// Records that the children reached via this edge are ordered,
90/// with a specific position index.
91#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
92pub struct Ordering {
93    /// The edge being ordered.
94    pub edge: Edge,
95    /// Position in the ordered collection.
96    pub position: u32,
97}
98
99/// A recursion point (fixpoint marker) in the schema.
100///
101/// Marks a vertex as a recursive reference to another vertex,
102/// satisfying the fold-unfold law: `unfold(fold(v)) = v`.
103///
104/// The marker vertex is the key this is filed under in
105/// [`Schema::recursion_points`], and is deliberately not repeated here. Naming
106/// it twice would make the two copies independently settable, and
107/// deserialisation accepts whatever a file says: a schema filing a marker under
108/// one name while the marker claimed another would be a contradiction no
109/// constructor could rule out and every reader would have to pick a side.
110/// Inducing, validation, diffing, hashing and the protocol emitters key on the
111/// map; the search network and the span's right leg read the marker. With one
112/// copy they cannot disagree, which is a stronger guarantee than any check
113/// could give, since a check has to be run and this cannot be skipped.
114/// [`ValidationError::DanglingRecursionPoint`](crate::ValidationError) covers
115/// what remains, a marker naming a vertex the schema does not have.
116#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
117pub struct RecursionPoint {
118    /// The target vertex this unfolds to.
119    pub target_vertex: Name,
120}
121
122/// A span connecting two vertices through a common source.
123///
124/// Spans model correspondences, diffs, and migrations:
125/// `left ← span → right`.
126#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
127pub struct Span {
128    /// Unique span identifier.
129    pub id: Name,
130    /// Left vertex of the span.
131    pub left: Name,
132    /// Right vertex of the span.
133    pub right: Name,
134}
135
136/// Use-counting mode for an edge.
137///
138/// Captures the substructural distinction between edges that can
139/// be used freely (structural), exactly once (linear), or at most
140/// once (affine).
141#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
142pub enum UsageMode {
143    /// Can be used any number of times (default).
144    #[default]
145    Structural,
146    /// Must be used exactly once (e.g., protobuf `oneof`).
147    Linear,
148    /// Can be used at most once.
149    Affine,
150}
151
152/// Specification of a coercion between two value kinds.
153///
154/// Contains the forward coercion expression, an optional inverse for
155/// round-tripping, and the coercion class classifying the round-trip behavior.
156#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
157pub struct CoercionSpec {
158    /// Forward coercion expression (source to target).
159    pub forward: panproto_expr::Expr,
160    /// Inverse coercion expression (target to source) for the `put` direction.
161    pub inverse: Option<panproto_expr::Expr>,
162    /// Round-trip classification.
163    pub class: panproto_gat::CoercionClass,
164}
165
166/// A schema: a model of the protocol's schema theory.
167///
168/// Contains both the raw data (vertices, edges, constraints, etc.) and
169/// precomputed adjacency indices for efficient graph traversal.
170#[derive(Clone, Debug, Serialize, Deserialize)]
171pub struct Schema {
172    /// The protocol this schema belongs to.
173    pub protocol: String,
174    /// Vertices keyed by their ID.
175    #[serde(with = "crate::serde_helpers::sorted_map")]
176    pub vertices: HashMap<Name, Vertex>,
177    /// Edges keyed by the edge itself, value is the edge kind.
178    #[serde(with = "crate::serde_helpers::map_as_vec")]
179    pub edges: HashMap<Edge, Name>,
180    /// Hyper-edges keyed by their ID.
181    #[serde(with = "crate::serde_helpers::sorted_map")]
182    pub hyper_edges: HashMap<Name, HyperEdge>,
183    /// Constraints per vertex ID.
184    #[serde(with = "crate::serde_helpers::sorted_map")]
185    pub constraints: HashMap<Name, Vec<Constraint>>,
186    /// Required edges per vertex ID.
187    #[serde(with = "crate::serde_helpers::sorted_map")]
188    pub required: HashMap<Name, Vec<Edge>>,
189    /// NSID mapping: vertex ID to NSID string.
190    #[serde(with = "crate::serde_helpers::sorted_map")]
191    pub nsids: HashMap<Name, Name>,
192    /// Declared entry vertices.
193    ///
194    /// Semantically, this is the finite family of basepoints that makes
195    /// the schema a *pointed* schema: `E → Ob(C_S)` selecting the sorts
196    /// at which the W-algebra of instances may be rooted. Parsers set
197    /// this explicitly per their protocol's notion of a top-level
198    /// definition (a record, a top-level type, a path root, etc.);
199    /// consumers that need to choose an instance root consult it via
200    /// [`primary_entry`].
201    ///
202    /// Empty means the parser declined to supply a pointing; consumers
203    /// should then either fall back to a deterministic (but non-
204    /// canonical) selection or report an error. Order is preserved for
205    /// reproducibility; the set carries no duplicates.
206    #[serde(default)]
207    pub entries: Vec<Name>,
208
209    /// Coproduct variants per union vertex ID.
210    #[serde(default, with = "crate::serde_helpers::sorted_map")]
211    pub variants: HashMap<Name, Vec<Variant>>,
212    /// Edge ordering positions (edge → position index).
213    #[serde(default, with = "crate::serde_helpers::map_as_vec_default")]
214    pub orderings: HashMap<Edge, u32>,
215    /// Recursion points (fixpoint markers).
216    #[serde(default, with = "crate::serde_helpers::sorted_map")]
217    pub recursion_points: HashMap<Name, RecursionPoint>,
218    /// Spans connecting pairs of vertices.
219    #[serde(default, with = "crate::serde_helpers::sorted_map")]
220    pub spans: HashMap<Name, Span>,
221    /// Edge usage modes (default: `Structural` for all).
222    #[serde(default, with = "crate::serde_helpers::map_as_vec_default")]
223    pub usage_modes: HashMap<Edge, UsageMode>,
224    /// Whether each vertex uses nominal identity (`true`) or
225    /// structural identity (`false`). Absent = structural.
226    #[serde(default, with = "crate::serde_helpers::sorted_map")]
227    pub nominal: HashMap<Name, bool>,
228
229    // -- enrichment fields --
230    /// Coercion specifications: `(source_kind, target_kind)` to coercion spec.
231    #[serde(default, with = "crate::serde_helpers::map_as_vec_default")]
232    pub coercions: HashMap<(Name, Name), CoercionSpec>,
233    /// Merger expressions: `vertex_id` to merger expression.
234    #[serde(default, with = "crate::serde_helpers::sorted_map")]
235    pub mergers: HashMap<Name, panproto_expr::Expr>,
236    /// Default value expressions: `vertex_id` to default expression.
237    #[serde(default, with = "crate::serde_helpers::sorted_map")]
238    pub defaults: HashMap<Name, panproto_expr::Expr>,
239    /// Conflict resolution policy expressions: `sort_name` to policy expression.
240    #[serde(default, with = "crate::serde_helpers::sorted_map")]
241    pub policies: HashMap<Name, panproto_expr::Expr>,
242
243    // -- precomputed indices --
244    /// Outgoing edges per vertex ID.
245    #[serde(with = "crate::serde_helpers::sorted_map")]
246    pub outgoing: HashMap<Name, SmallVec<Edge, 4>>,
247    /// Incoming edges per vertex ID.
248    #[serde(with = "crate::serde_helpers::sorted_map")]
249    pub incoming: HashMap<Name, SmallVec<Edge, 4>>,
250    /// Edges between a specific `(src, tgt)` pair.
251    #[serde(with = "crate::serde_helpers::map_as_vec")]
252    pub between: HashMap<(Name, Name), SmallVec<Edge, 2>>,
253}
254
255impl Schema {
256    /// Strip every constraint whose sort belongs to the layout
257    /// enrichment fibre (per [`panproto_gat::is_layout_sort`]).
258    ///
259    /// This is the schema-level forgetful U sending a decorated schema
260    /// to its abstract base in the parse/decorate/emit lens. Idempotent.
261    #[must_use]
262    pub fn forget_layout(&self) -> Self {
263        let mut clone = self.clone();
264        clone.forget_layout_in_place();
265        clone
266    }
267
268    /// In-place variant of [`Self::forget_layout`].
269    pub fn forget_layout_in_place(&mut self) {
270        for constraints in self.constraints.values_mut() {
271            constraints.retain(|c| !panproto_gat::is_layout_sort(c.sort.as_ref()));
272        }
273        // Drop now-empty vertex entries so equality is structural.
274        self.constraints.retain(|_, cs| !cs.is_empty());
275    }
276
277    /// Returns `true` when no constraint sort belongs to the layout
278    /// enrichment fibre. This is the well-formedness predicate for an
279    /// [`AbstractSchema`](crate::AbstractSchema).
280    #[must_use]
281    pub fn is_layout_free(&self) -> bool {
282        self.constraints.values().all(|cs| {
283            cs.iter()
284                .all(|c| !panproto_gat::is_layout_sort(c.sort.as_ref()))
285        })
286    }
287
288    /// Look up a vertex by ID.
289    #[must_use]
290    pub fn vertex(&self, id: &str) -> Option<&Vertex> {
291        self.vertices.get(id)
292    }
293
294    /// Return all outgoing edges from the given vertex.
295    #[must_use]
296    pub fn outgoing_edges(&self, vertex_id: &str) -> &[Edge] {
297        self.outgoing.get(vertex_id).map_or(&[], SmallVec::as_slice)
298    }
299
300    /// Return all incoming edges to the given vertex.
301    #[must_use]
302    pub fn incoming_edges(&self, vertex_id: &str) -> &[Edge] {
303        self.incoming.get(vertex_id).map_or(&[], SmallVec::as_slice)
304    }
305
306    /// Return edges between a specific `(src, tgt)` pair.
307    #[must_use]
308    #[inline]
309    pub fn edges_between(&self, src: &str, tgt: &str) -> &[Edge] {
310        self.between
311            .get(&(Name::from(src), Name::from(tgt)))
312            .map_or(&[], SmallVec::as_slice)
313    }
314
315    /// Returns `true` if the given vertex ID exists in this schema.
316    #[must_use]
317    #[inline]
318    pub fn has_vertex(&self, id: &str) -> bool {
319        self.vertices.contains_key(id)
320    }
321
322    /// Returns the number of vertices in the schema.
323    #[must_use]
324    pub fn vertex_count(&self) -> usize {
325        self.vertices.len()
326    }
327
328    /// Returns the number of edges in the schema.
329    #[must_use]
330    pub fn edge_count(&self) -> usize {
331        self.edges.len()
332    }
333
334    /// Return the declared entry vertices.
335    ///
336    /// See [`Schema::entries`] for semantics. Use [`primary_entry`] for
337    /// callers that need a single root and want a deterministic
338    /// fallback when no entries are declared.
339    #[must_use]
340    pub fn entry_vertices(&self) -> &[Name] {
341        &self.entries
342    }
343
344    /// Return every constraint attached to the given vertex.
345    ///
346    /// Tree-sitter-derived schemas attach byte ranges, interstitials,
347    /// formatting, and `field:<name>` entries here.
348    #[must_use]
349    pub fn constraints_for(&self, vertex_id: &str) -> &[Constraint] {
350        self.constraints
351            .get(&Name::from(vertex_id))
352            .map_or(&[], Vec::as_slice)
353    }
354
355    /// Return the text value of a tree-sitter `field('<name>', ...)`
356    /// anonymous-token child on the given vertex, if any.
357    ///
358    /// Tree-sitter rules of the form
359    /// `field('op', choice('+', '-', '*', '/'))` attach a field name to
360    /// an unnamed token alternative. The walker emits the token's text
361    /// as a `field:<name>` constraint on the parent vertex; this is the
362    /// supported accessor for that text.
363    ///
364    /// Returns `None` if no `field:<name>` constraint exists on
365    /// `vertex_id`. Named-node field children continue to surface as
366    /// edges (use [`outgoing_edges`](Self::outgoing_edges) and filter
367    /// by [`Edge::kind`](crate::Edge::kind) for those).
368    #[must_use]
369    pub fn field_text(&self, vertex_id: &str, field_name: &str) -> Option<&str> {
370        let sort = format!("field:{field_name}");
371        self.constraints
372            .get(&Name::from(vertex_id))?
373            .iter()
374            .find(|c| c.sort.as_ref() == sort.as_str())
375            .map(|c| c.value.as_str())
376    }
377}
378
379/// Choose a single entry vertex for a schema.
380///
381/// Returns the first declared entry if any. Otherwise falls back to a
382/// deterministic, protocol-agnostic choice: among vertex ids sorted
383/// lexicographically, the first vertex that is a source of at least
384/// one edge and a target of none (an edgeless "root" in the signature
385/// graph); failing that, the first vertex that has outgoing edges at
386/// all; failing that, the lexicographically first vertex; `None` only
387/// for an empty schema.
388///
389/// The fallback is explicitly *non-canonical*: it exists so that legacy
390/// schemas without declared entries remain usable, but new parsers
391/// should always supply at least one entry via
392/// [`SchemaBuilder::entry`](crate::SchemaBuilder::entry) so that the
393/// pointing is part of the schema's semantics rather than recovered by
394/// heuristic.
395#[must_use]
396pub fn primary_entry(schema: &Schema) -> Option<&Name> {
397    if let Some(first) = schema.entries.first() {
398        return Some(first);
399    }
400
401    let mut ids: Vec<&Name> = schema.vertices.keys().collect();
402    ids.sort();
403
404    let has_outgoing = |id: &Name| -> bool {
405        schema
406            .outgoing
407            .get(id)
408            .is_some_and(|edges| !edges.is_empty())
409    };
410    let has_incoming = |id: &Name| -> bool {
411        schema
412            .incoming
413            .get(id)
414            .is_some_and(|edges| !edges.is_empty())
415    };
416
417    if let Some(id) = ids
418        .iter()
419        .copied()
420        .find(|id| has_outgoing(id) && !has_incoming(id))
421    {
422        return Some(id);
423    }
424    if let Some(id) = ids.iter().copied().find(|id| has_outgoing(id)) {
425        return Some(id);
426    }
427    ids.into_iter().next()
428}