Skip to main content

cratestack_core/
schema.rs

1//! Schema IR — the parsed shape of a `.cstack` file. Every IR node
2//! carries source-span back-pointers so consumers can map errors to
3//! positions in the original text.
4
5pub mod composite_key;
6pub mod composite_unique;
7mod field_list;
8pub mod index_attribute;
9pub mod model;
10pub mod procedure;
11pub mod selection;
12pub mod view;
13
14use std::collections::BTreeSet;
15
16use serde::{Deserialize, Serialize};
17
18pub use composite_key::parse_composite_id_attribute;
19pub use composite_unique::parse_composite_unique_attribute;
20pub use index_attribute::{ParsedIndexAttribute, parse_index_attribute};
21pub use model::{
22    Attribute, EnumDecl, EnumVariant, Field, MixinDecl, Model, TypeArity, TypeDecl, TypeRef,
23};
24pub use procedure::{Procedure, ProcedureArg, ProcedureKind};
25pub use selection::SelectionQuery;
26pub use view::{View, ViewSource};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub struct SourceSpan {
30    pub start: usize,
31    pub end: usize,
32    pub line: usize,
33}
34
35/// Wire-shape the schema generates for. Picked once per schema (via
36/// the top-level `transport rest|rpc` directive) so generated servers
37/// and clients only carry one binding's worth of surface.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum TransportStyle {
41    #[default]
42    Rest,
43    Rpc,
44    Grpc,
45}
46
47impl TransportStyle {
48    pub const fn as_str(&self) -> &'static str {
49        match self {
50            TransportStyle::Rest => "rest",
51            TransportStyle::Rpc => "rpc",
52            TransportStyle::Grpc => "grpc",
53        }
54    }
55}
56
57/// An opt-in framework/database capability a schema announces via a
58/// top-level `extension <name> { }` block (cratestack#153). Declaring an
59/// extension only unlocks schema-visible *syntax* for that capability
60/// (e.g. `@no_rate_limit`, the `Vector(n)` scalar type) — it never gates
61/// codegen or runtime behavior by itself; that's a separate, same-named
62/// Cargo feature per consuming crate (cratestack#161, out of scope here).
63///
64/// This is a closed list by design, not an arbitrary-extension mechanism
65/// — see `docs/design/extensions.md` §7.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum ExtensionKind {
69    RateLimit,
70    Pgvector,
71}
72
73impl ExtensionKind {
74    /// Every recognized extension name, in a stable order — used to build
75    /// clear "expected one of: ..." error messages.
76    pub const ALL: [ExtensionKind; 2] = [ExtensionKind::RateLimit, ExtensionKind::Pgvector];
77
78    pub const fn as_str(&self) -> &'static str {
79        match self {
80            ExtensionKind::RateLimit => "rate_limit",
81            ExtensionKind::Pgvector => "pgvector",
82        }
83    }
84
85    /// Parses the bare name written after `extension` in `.cstack` source
86    /// (e.g. `rate_limit` in `extension rate_limit { }`). `None` for any
87    /// name outside the closed, framework-maintained list.
88    pub fn parse_name(name: &str) -> Option<Self> {
89        ExtensionKind::ALL
90            .into_iter()
91            .find(|kind| kind.as_str() == name)
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct Schema {
97    pub datasource: Option<Datasource>,
98    pub auth: Option<AuthBlock>,
99    pub config_blocks: Vec<ConfigBlock>,
100    pub mixins: Vec<MixinDecl>,
101    pub models: Vec<Model>,
102    pub types: Vec<TypeDecl>,
103    pub enums: Vec<EnumDecl>,
104    pub procedures: Vec<Procedure>,
105    #[serde(default)]
106    pub views: Vec<View>,
107    #[serde(default)]
108    pub transport: TransportStyle,
109    /// Opt-in framework/database capabilities this schema declared via
110    /// top-level `extension <name> { }` blocks (cratestack#153). Empty for
111    /// every schema that declares none — no behavior change.
112    #[serde(default)]
113    pub declared_extensions: BTreeSet<ExtensionKind>,
114}
115
116impl Schema {
117    pub fn summary(&self) -> OwnedSchemaSummary {
118        OwnedSchemaSummary {
119            mixins: self.mixins.iter().map(|mixin| mixin.name.clone()).collect(),
120            models: self.models.iter().map(|model| model.name.clone()).collect(),
121            types: self.types.iter().map(|ty| ty.name.clone()).collect(),
122            enums: self
123                .enums
124                .iter()
125                .map(|enum_decl| enum_decl.name.clone())
126                .collect(),
127            procedures: self
128                .procedures
129                .iter()
130                .map(|procedure| procedure.name.clone())
131                .collect(),
132            views: self.views.iter().map(|view| view.name.clone()).collect(),
133        }
134    }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct SchemaSummary {
139    pub mixins: &'static [&'static str],
140    pub models: &'static [&'static str],
141    pub types: &'static [&'static str],
142    pub enums: &'static [&'static str],
143    pub procedures: &'static [&'static str],
144    pub views: &'static [&'static str],
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct OwnedSchemaSummary {
149    pub mixins: Vec<String>,
150    pub models: Vec<String>,
151    pub types: Vec<String>,
152    pub enums: Vec<String>,
153    pub procedures: Vec<String>,
154    #[serde(default)]
155    pub views: Vec<String>,
156}
157
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct Datasource {
160    pub docs: Vec<String>,
161    pub name: String,
162    pub entries: Vec<ConfigEntry>,
163    pub span: SourceSpan,
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct AuthBlock {
168    pub docs: Vec<String>,
169    pub name: String,
170    pub fields: Vec<Field>,
171    pub span: SourceSpan,
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct ConfigBlock {
176    pub docs: Vec<String>,
177    pub name: String,
178    pub entries: Vec<String>,
179    pub span: SourceSpan,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct ConfigEntry {
184    pub key: String,
185    pub value: String,
186}