sea_core/parser/
ast_schema.rs

1//! AST Schema types for JSON serialization and schema generation.
2//!
3//! This module provides serializable versions of the AST types with JsonSchema
4//! derives for programmatic JSON Schema generation. These mirror the types in
5//! `ast.rs` but are specifically designed for schema export and JSON serialization.
6//!
7//! ## Design Rationale
8//!
9//! The schema types are separate from the internal AST types to:
10//! - Provide a stable JSON API contract that doesn't change with internal refactoring
11//! - Allow the internal AST to evolve without breaking downstream tools
12//! - Enable precise control over JSON serialization format (e.g., tagged enums)
13//! - Support JSON Schema generation via `schemars`
14//!
15//! ## Usage
16//!
17//! Convert from internal AST to schema types:
18//! ```rust,ignore
19//! use sea_core::parser::{parse, ast_schema};
20//!
21//! let internal_ast = parse(source)?;
22//! let schema_ast: ast_schema::Ast = internal_ast.into();
23//! let json = serde_json::to_string_pretty(&schema_ast)?;
24//! ```
25//!
26//! ## Schema Generation
27//!
28//! To regenerate the schema, run:
29//! ```bash
30//! cargo test generate_ast_schema -- --ignored --nocapture
31//! ```
32
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35
36#[cfg(feature = "json-schema")]
37use schemars::JsonSchema;
38
39// =========================================================================
40// File Metadata
41// =========================================================================
42
43/// Import declaration for a module file
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
46pub struct ImportDecl {
47    pub specifier: ImportSpecifier,
48    pub from_module: String,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
53#[serde(tag = "type")]
54pub enum ImportSpecifier {
55    Named { items: Vec<ImportItem> },
56    Wildcard { alias: String },
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
61pub struct ImportItem {
62    pub name: String,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub alias: Option<String>,
65}
66
67/// File-level metadata from header annotations
68#[derive(Debug, Clone, Serialize, Deserialize, Default)]
69#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
70pub struct FileMetadata {
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub namespace: Option<String>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub version: Option<String>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub owner: Option<String>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub profile: Option<String>,
79    #[serde(default, skip_serializing_if = "Vec::is_empty")]
80    pub imports: Vec<ImportDecl>,
81}
82
83// =========================================================================
84// Policy Metadata
85// =========================================================================
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
89pub enum PolicyKind {
90    Constraint,
91    Derivation,
92    Obligation,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
97pub enum PolicyModality {
98    Obligation,
99    Prohibition,
100    Permission,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
105pub struct PolicyMetadata {
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub kind: Option<PolicyKind>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub modality: Option<PolicyModality>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub priority: Option<i32>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub rationale: Option<String>,
114    #[serde(default, skip_serializing_if = "Vec::is_empty")]
115    pub tags: Vec<String>,
116}
117
118// =========================================================================
119// Metric Metadata
120// =========================================================================
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
124pub enum Severity {
125    Info,
126    Warning,
127    Error,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
132pub struct MetricMetadata {
133    /// Refresh interval in ISO 8601 duration format (e.g., "PT1H")
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub refresh_interval: Option<String>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub unit: Option<String>,
138    /// Decimal value as string for precision
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub threshold: Option<String>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub severity: Option<Severity>,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub target: Option<String>,
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub window: Option<String>,
147}
148
149// =========================================================================
150// Mapping and Projection
151// =========================================================================
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
155pub enum TargetFormat {
156    Calm,
157    Kg,
158    Sbvr,
159    Protobuf,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
164pub struct MappingRule {
165    pub primitive_type: String,
166    pub primitive_name: String,
167    pub target_type: String,
168    #[serde(default)]
169    pub fields: HashMap<String, serde_json::Value>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
174pub struct ProjectionOverride {
175    pub primitive_type: String,
176    pub primitive_name: String,
177    #[serde(default)]
178    pub fields: HashMap<String, serde_json::Value>,
179}
180
181// =========================================================================
182// Expression Types (for Policy and Metric)
183// =========================================================================
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
187pub struct WindowSpec {
188    pub duration: u64,
189    pub unit: String,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
193#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
194pub enum BinaryOp {
195    And,
196    Or,
197    Equal,
198    NotEqual,
199    GreaterThan,
200    LessThan,
201    GreaterThanOrEqual,
202    LessThanOrEqual,
203    Plus,
204    Minus,
205    Multiply,
206    Divide,
207    Contains,
208    StartsWith,
209    EndsWith,
210    Matches,
211    HasRole,
212    Before,
213    After,
214    During,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
219pub enum UnaryOp {
220    Not,
221    Negate,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
225#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
226pub enum Quantifier {
227    ForAll,
228    Exists,
229    ExistsUnique,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
234pub enum AggregateFunction {
235    Count,
236    Sum,
237    Min,
238    Max,
239    Avg,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
243#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
244#[serde(tag = "type")]
245pub enum Expression {
246    Literal {
247        value: serde_json::Value,
248    },
249    QuantityLiteral {
250        /// Decimal value as string for precision
251        value: String,
252        unit: String,
253    },
254    TimeLiteral {
255        timestamp: String,
256    },
257    IntervalLiteral {
258        start: String,
259        end: String,
260    },
261    Variable {
262        name: String,
263    },
264    GroupBy {
265        variable: String,
266        collection: Box<Expression>,
267        #[serde(skip_serializing_if = "Option::is_none")]
268        filter: Option<Box<Expression>>,
269        key: Box<Expression>,
270        condition: Box<Expression>,
271    },
272    Binary {
273        op: BinaryOp,
274        left: Box<Expression>,
275        right: Box<Expression>,
276    },
277    Unary {
278        op: UnaryOp,
279        operand: Box<Expression>,
280    },
281    Cast {
282        operand: Box<Expression>,
283        target_type: String,
284    },
285    Quantifier {
286        quantifier: Quantifier,
287        variable: String,
288        collection: Box<Expression>,
289        condition: Box<Expression>,
290    },
291    MemberAccess {
292        object: String,
293        member: String,
294    },
295    Aggregation {
296        function: AggregateFunction,
297        collection: Box<Expression>,
298        #[serde(skip_serializing_if = "Option::is_none")]
299        field: Option<String>,
300        #[serde(skip_serializing_if = "Option::is_none")]
301        filter: Option<Box<Expression>>,
302    },
303    AggregationComprehension {
304        function: AggregateFunction,
305        variable: String,
306        collection: Box<Expression>,
307        #[serde(skip_serializing_if = "Option::is_none")]
308        window: Option<WindowSpec>,
309        predicate: Box<Expression>,
310        projection: Box<Expression>,
311        #[serde(skip_serializing_if = "Option::is_none")]
312        target_unit: Option<String>,
313    },
314}
315
316// =========================================================================
317// AST Node Types
318// =========================================================================
319
320/// Spanned AST node with source location information
321#[derive(Debug, Clone, Serialize, Deserialize)]
322#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
323pub struct SpannedAstNode {
324    pub node: AstNode,
325    /// 1-indexed line number
326    pub line: usize,
327    /// 1-indexed column number
328    pub column: usize,
329}
330
331/// AST Node types representing all SEA DSL declarations
332#[derive(Debug, Clone, Serialize, Deserialize)]
333#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
334#[serde(tag = "type")]
335pub enum AstNode {
336    /// Export wrapper for public declarations
337    Export { declaration: Box<SpannedAstNode> },
338
339    /// Entity declaration
340    Entity {
341        name: String,
342        #[serde(skip_serializing_if = "Option::is_none")]
343        version: Option<String>,
344        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
345        annotations: HashMap<String, serde_json::Value>,
346        #[serde(skip_serializing_if = "Option::is_none")]
347        domain: Option<String>,
348    },
349
350    /// Resource declaration
351    Resource {
352        name: String,
353        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
354        annotations: HashMap<String, serde_json::Value>,
355        #[serde(skip_serializing_if = "Option::is_none")]
356        unit_name: Option<String>,
357        #[serde(skip_serializing_if = "Option::is_none")]
358        domain: Option<String>,
359    },
360
361    /// Flow declaration - resource transfer between entities
362    Flow {
363        resource_name: String,
364        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
365        annotations: HashMap<String, serde_json::Value>,
366        from_entity: String,
367        to_entity: String,
368        #[serde(skip_serializing_if = "Option::is_none")]
369        quantity: Option<i32>,
370    },
371
372    /// Pattern declaration - named regex for string validation
373    Pattern { name: String, regex: String },
374
375    /// Role declaration - participant category
376    Role {
377        name: String,
378        #[serde(skip_serializing_if = "Option::is_none")]
379        domain: Option<String>,
380    },
381
382    /// Relation declaration - predicate connecting roles
383    Relation {
384        name: String,
385        subject_role: String,
386        predicate: String,
387        object_role: String,
388        #[serde(skip_serializing_if = "Option::is_none")]
389        via_flow: Option<String>,
390    },
391
392    /// Dimension declaration for units
393    Dimension { name: String },
394
395    /// Unit declaration - custom unit definition
396    UnitDeclaration {
397        symbol: String,
398        dimension: String,
399        /// Decimal conversion factor as string
400        factor: String,
401        base_unit: String,
402    },
403
404    /// Policy declaration - validation rule or constraint
405    Policy {
406        name: String,
407        #[serde(skip_serializing_if = "Option::is_none")]
408        version: Option<String>,
409        metadata: PolicyMetadata,
410        expression: Expression,
411    },
412
413    /// Instance declaration - entity instance with field values
414    Instance {
415        name: String,
416        entity_type: String,
417        #[serde(default)]
418        fields: HashMap<String, Expression>,
419    },
420
421    /// ConceptChange declaration - version migration
422    ConceptChange {
423        name: String,
424        from_version: String,
425        to_version: String,
426        migration_policy: String,
427        breaking_change: bool,
428    },
429
430    /// Metric declaration - observable metric
431    Metric {
432        name: String,
433        expression: Expression,
434        metadata: MetricMetadata,
435    },
436
437    /// Mapping declaration - format mapping rules
438    MappingDecl {
439        name: String,
440        target: TargetFormat,
441        rules: Vec<MappingRule>,
442    },
443
444    /// Projection declaration - output configuration
445    ProjectionDecl {
446        name: String,
447        target: TargetFormat,
448        overrides: Vec<ProjectionOverride>,
449    },
450}
451
452// =========================================================================
453// Root AST Structure
454// =========================================================================
455
456/// Abstract Syntax Tree for SEA DSL
457#[derive(Debug, Clone, Serialize, Deserialize)]
458#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
459pub struct Ast {
460    pub metadata: FileMetadata,
461    pub declarations: Vec<SpannedAstNode>,
462}
463
464// =========================================================================
465// Schema Generation (test-only)
466// =========================================================================
467
468#[cfg(all(test, feature = "json-schema"))]
469mod tests {
470    use super::*;
471    use schemars::schema_for;
472
473    #[test]
474    #[ignore] // Run manually: cargo test generate_ast_schema -- --ignored --nocapture
475    fn generate_ast_schema() {
476        let schema = schema_for!(Ast);
477        let json = serde_json::to_string_pretty(&schema).expect("Failed to serialize schema");
478
479        // Write to schemas directory
480        let schema_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
481            .parent()
482            .unwrap()
483            .join("schemas")
484            .join("ast-v3.schema.json");
485
486        std::fs::write(&schema_path, &json).expect("Failed to write schema file");
487
488        println!("Schema written to: {}", schema_path.display());
489        println!("\n{}", json);
490    }
491}