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}