Skip to main content

kdl_codegen/
ir.rs

1//! Schema IR — the intermediate representation between the KDL parser and the
2//! language emitters.
3//!
4//! This is the central contract of `club-kdl-codegen`: the parser produces a
5//! [`Schema`], and every [`crate::Emitter`] consumes one. Keeping the IR a
6//! plain data structure (no behaviour) lets the parser and emitters evolve
7//! independently.
8//!
9//! The IR spans three dialects, all reachable from a single [`Schema`]:
10//!
11//! - **data dialect** — [`TypeDef`] (`struct` / `enum`) built from [`Field`]
12//!   and [`Ty`]. Models standalone named *value* types (embedded, no identity).
13//! - **entity dialect** — [`Record`] (`record`) and [`Relation`]
14//!   (`relation`), modelling persisted entities with identity and the graph
15//!   edges between them. These map to SurrealDB `DEFINE TABLE` statements
16//!   (`TYPE NORMAL` for records, `TYPE RELATION` for relations). They reuse
17//!   the data dialect's [`Field`].
18//! - **protocol dialect** — [`Protocol`] / [`Channel`] / [`Request`] /
19//!   [`Event`], modelling KDL channel schemas. Payload definitions reuse the
20//!   data dialect's [`Field`], so an emitter writes field-rendering logic once
21//!   and it applies to standalone types, entities, and channel payloads.
22//!
23//! The distinction between a `struct` and a `record` is **identity**: a
24//! `struct` is an embedded value type (no `id`, never becomes a table), while
25//! a `record` has an `id` and becomes a first-class table. A field referring
26//! to a `struct` / `enum` uses a bare [`Ty::Named`] (embedded); a field
27//! referring to a `record` uses [`Ty::Link`] (a stored reference).
28//!
29//! Legacy constructs (`service` / `method` / `stream` / `send` / `recv`) are
30//! intentionally **not** modelled — see CLAUDE.md "Legacy は残さない". The IR
31//! describes only the modern channel dialect.
32//!
33//! See the design memory `mem_1Cb5mWnMTdzXfJVoNGFwup` and `ROADMAP.md`
34//! (Phase 1) for the full plan.
35
36// =============================================================================
37// Schema root
38// =============================================================================
39
40/// A whole KDL schema file: standalone type definitions, entity / relation
41/// definitions, plus an optional protocol definition.
42#[derive(Debug, Clone, PartialEq, Eq, Default)]
43pub struct Schema {
44    /// Standalone value-type definitions (`struct` / `enum`) in source order.
45    pub types: Vec<TypeDef>,
46    /// Entity definitions (`record`) in source order — persisted tables with
47    /// identity.
48    pub records: Vec<Record>,
49    /// Graph edge definitions (`relation`) in source order — `TYPE RELATION`
50    /// tables connecting two records.
51    pub relations: Vec<Relation>,
52    /// The protocol definition, if the file declares one. A file may contain
53    /// only data types, only a protocol, or both.
54    pub protocol: Option<Protocol>,
55}
56
57// =============================================================================
58// Data dialect — standalone named types
59// =============================================================================
60
61/// A single named type definition.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum TypeDef {
64    /// A record type with named fields.
65    Struct {
66        /// Type name (e.g. `"User"`).
67        name: String,
68        /// Optional documentation string (`description="..."`).
69        description: Option<String>,
70        /// Fields in source order.
71        fields: Vec<Field>,
72    },
73    /// An enumeration of string-valued variants.
74    Enum {
75        /// Type name (e.g. `"Role"`).
76        name: String,
77        /// Optional documentation string (`description="..."`).
78        description: Option<String>,
79        /// Variant names in source order.
80        variants: Vec<String>,
81    },
82}
83
84/// A field of a [`TypeDef::Struct`], a [`Record`] / [`Relation`], or of a
85/// protocol-dialect payload ([`Request`] / [`Event`] / [`Message`]).
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct Field {
88    /// Field name.
89    pub name: String,
90    /// Field type.
91    pub ty: Ty,
92    /// Whether the field is required. `false` maps to the target's optional
93    /// form (Rust `Option<T>`, TS `?:`, Zod `.optional()`, SurrealQL `option<T>`).
94    pub required: bool,
95    /// Whether an `object`-typed field is schemaless (`flexible=#true`). Only
96    /// meaningful for [`Ty::Primitive`]`(`[`Prim::Json`]`)`; emitted by the
97    /// SurrealQL target as `FLEXIBLE TYPE object`.
98    pub flexible: bool,
99    /// An optional default value, as written in the KDL `default="..."`
100    /// property. Carried verbatim; emitters quote / render it per target.
101    pub default: Option<String>,
102    /// An optional documentation string, from the KDL `description="..."`
103    /// property. Emitted as a doc comment (Rust `///`, TS `/** */` JSDoc,
104    /// Zod `.describe(...)`, SurrealQL `COMMENT '...'`).
105    pub description: Option<String>,
106    /// Value constraints, from the KDL `min` / `max` / `min_length` /
107    /// `max_length` / `pattern` properties. Emitted only by the Zod and
108    /// SurrealQL targets — the Rust / TypeScript type system cannot express
109    /// them.
110    pub constraints: Constraints,
111}
112
113/// Value constraints attached to a [`Field`].
114///
115/// These are language-agnostic validation metadata: a numeric range, a
116/// string / array length bound, and a regular-expression pattern. They are
117/// propagated to the **Zod** and **SurrealQL** emitters (which produce runtime
118/// validators / database `ASSERT`s); the Rust and TypeScript type systems
119/// cannot express them, so those emitters drop constraints entirely.
120#[derive(Debug, Clone, PartialEq, Eq, Default)]
121pub struct Constraints {
122    /// Inclusive lower bound for a numeric field (`min=N`).
123    pub min: Option<i64>,
124    /// Inclusive upper bound for a numeric field (`max=N`).
125    pub max: Option<i64>,
126    /// Inclusive minimum length for a string / array field (`min_length=N`).
127    pub min_length: Option<u64>,
128    /// Inclusive maximum length for a string / array field (`max_length=N`).
129    pub max_length: Option<u64>,
130    /// A regular-expression pattern a string field must match (`pattern="..."`).
131    pub pattern: Option<String>,
132}
133
134impl Constraints {
135    /// Whether no constraint is set.
136    pub fn is_empty(&self) -> bool {
137        self.min.is_none()
138            && self.max.is_none()
139            && self.min_length.is_none()
140            && self.max_length.is_none()
141            && self.pattern.is_none()
142    }
143}
144
145/// A field type.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub enum Ty {
148    /// A built-in primitive.
149    Primitive(Prim),
150    /// A homogeneous array of another type.
151    Array(Box<Ty>),
152    /// A reference to a [`TypeDef`] (`struct` / `enum`) by name — an
153    /// **embedded** value type.
154    Named(String),
155    /// A reference to a [`Record`] by name — a **stored link** (`link<Name>`
156    /// in KDL). Distinct from [`Self::Named`]: a link is a foreign key / edge
157    /// target, not an embedded value.
158    Link(String),
159    /// A union of two or more alternative types (`A | B | ...` in KDL).
160    /// Members are kept in source order.
161    Union(Vec<Ty>),
162    /// A string-literal type (`'value'` in KDL). Used as a union member to
163    /// express closed string sets (`'public' | 'private'`).
164    Literal(String),
165}
166
167/// A built-in primitive type.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum Prim {
170    /// UTF-8 string.
171    String,
172    /// Signed integer.
173    Int,
174    /// Floating-point number.
175    Float,
176    /// Boolean.
177    Bool,
178    /// Date-time.
179    Datetime,
180    /// Arbitrary JSON value.
181    Json,
182}
183
184// =============================================================================
185// Entity dialect — records and relations
186// =============================================================================
187
188/// An entity definition (`record`) — a persisted type with identity.
189///
190/// Unlike a [`TypeDef::Struct`] (an embedded value), a `Record` carries an
191/// `id` and maps to a SurrealDB `DEFINE TABLE ... TYPE NORMAL` statement.
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct Record {
194    /// Record name (e.g. `"Atlas"`).
195    pub name: String,
196    /// Optional documentation string (`description="..."`).
197    pub description: Option<String>,
198    /// How the record's `id` is generated.
199    pub id_strategy: IdStrategy,
200    /// Fields in source order. The `id` field is *not* listed here — it is
201    /// described by [`Self::id_strategy`].
202    pub fields: Vec<Field>,
203}
204
205/// How a [`Record`]'s identifier is generated.
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
207pub enum IdStrategy {
208    /// Time-ordered UUID v7. The default when `id` carries no `strategy`.
209    #[default]
210    Uuidv7,
211    /// Lexicographically sortable ULID.
212    Ulid,
213    /// The id is supplied by the caller (no automatic generation).
214    Manual,
215}
216
217/// A graph edge definition (`relation`) — a typed, directed connection
218/// between two [`Record`]s.
219///
220/// Maps to a SurrealDB `DEFINE TABLE ... TYPE RELATION IN <from> OUT <to>`
221/// statement. A relation may carry its own [`Field`]s (a "property edge").
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct Relation {
224    /// Relation name (e.g. `"derivedFrom"`).
225    pub name: String,
226    /// Optional documentation string (`description="..."`).
227    pub description: Option<String>,
228    /// The record name at the `in` end of the edge.
229    pub from: String,
230    /// The record name at the `out` end of the edge.
231    pub to: String,
232    /// Whether each `(from, to)` pair must be unique.
233    pub unique: bool,
234    /// Edge-property fields in source order.
235    pub fields: Vec<Field>,
236}
237
238// =============================================================================
239// Protocol dialect — channel schemas
240// =============================================================================
241
242/// A protocol definition: the top-level grouping of [`Channel`]s.
243#[derive(Debug, Clone, PartialEq, Eq, Default)]
244pub struct Protocol {
245    /// Protocol name (e.g. `"ping-pong"`).
246    pub name: String,
247    /// Protocol version string (e.g. `"2.0.0"`).
248    pub version: String,
249    /// Optional namespace, used by emitters for module / package placement.
250    pub namespace: Option<String>,
251    /// Optional human-readable description.
252    pub description: Option<String>,
253    /// Channels in source order.
254    pub channels: Vec<Channel>,
255}
256
257/// A communication channel — the unit of request/response and event traffic.
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct Channel {
260    /// Channel name (e.g. `"ping-pong"`).
261    pub name: String,
262    /// Which peer opens the channel.
263    pub from: ChannelFrom,
264    /// How long the channel lives.
265    pub lifetime: ChannelLifetime,
266    /// Wire backend. Defaults to [`ChannelBackend::Stream`].
267    pub backend: ChannelBackend,
268    /// Demux identifier, required when [`Self::backend`] is
269    /// [`ChannelBackend::Datagram`]. A positive integer (`1..`).
270    pub channel_id: Option<u64>,
271    /// When `Some(tag)`, the emitters generate a discriminated-union
272    /// **envelope** type bundling every [`Self::requests`] entry, internally
273    /// tagged by the JSON field named `tag` (`channel "ipc" envelope="t"` ⇒
274    /// `Some("t")`). `None` leaves the channel emitting per-request payloads
275    /// only — the channel output is then byte-identical to a pre-envelope
276    /// build.
277    pub envelope: Option<String>,
278    /// Request/response definitions in source order. Always empty for a
279    /// datagram channel (datagram channels carry events only).
280    pub requests: Vec<Request>,
281    /// Event definitions in source order.
282    pub events: Vec<Event>,
283}
284
285/// Which peer initiates (opens) a [`Channel`].
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub enum ChannelFrom {
288    /// The client opens the channel.
289    Client,
290    /// The server opens the channel.
291    Server,
292    /// Either peer may open the channel.
293    Either,
294}
295
296/// How long a [`Channel`] lives.
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum ChannelLifetime {
299    /// Opened and closed per request.
300    Transient,
301    /// Held open for the duration of the connection.
302    Persistent,
303}
304
305/// The wire backend a [`Channel`] runs over.
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
307pub enum ChannelBackend {
308    /// QUIC bidirectional stream — ordered and reliable. The default.
309    #[default]
310    Stream,
311    /// QUIC datagram — unordered, unreliable, bounded by the MTU. Requires a
312    /// [`Channel::channel_id`].
313    Datagram,
314}
315
316/// A request/response pair within a [`Channel`].
317#[derive(Debug, Clone, PartialEq, Eq)]
318pub struct Request {
319    /// Request name (e.g. `"Ping"`).
320    pub name: String,
321    /// Request payload fields in source order.
322    pub fields: Vec<Field>,
323    /// The response payload, if the request returns one.
324    pub returns: Option<Message>,
325}
326
327/// A push event within a [`Channel`].
328#[derive(Debug, Clone, PartialEq, Eq)]
329pub struct Event {
330    /// Event name (e.g. `"MetricUpdate"`).
331    pub name: String,
332    /// Event payload fields in source order.
333    pub fields: Vec<Field>,
334}
335
336/// A named payload message — the `returns` block of a [`Request`].
337#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct Message {
339    /// Message name (e.g. `"Pong"`).
340    pub name: String,
341    /// Payload fields in source order.
342    pub fields: Vec<Field>,
343}