Skip to main content

visitor_demo/
visitor_demo.rs

1//! Example: Using the Visitor pattern to analyze schemas.
2//!
3//! This example demonstrates:
4//! - Implementing custom visitors
5//! - Traversing AST nodes
6//! - Collecting information from schemas
7//! - Building relationships between models
8//!
9//! Run with: cargo run --package nautilus-schema --example visitor_demo
10
11use nautilus_schema::{
12    ast::*,
13    visitor::{walk_model, Visitor},
14    Lexer, Parser, Result,
15};
16use std::collections::{HashMap, HashSet};
17
18const SCHEMA: &str = r#"
19datasource db {
20  provider = "postgresql"
21  url = env("DATABASE_URL")
22}
23
24enum Role {
25  USER
26  ADMIN
27  MODERATOR
28}
29
30model User {
31  id        Int      @id @default(autoincrement())
32  email     String   @unique
33  username  String   @unique
34  role      Role     @default(USER)
35  createdAt DateTime @default(now())
36  
37  posts     Post[]
38  comments  Comment[]
39  profile   Profile?
40  
41  @@map("users")
42}
43
44model Profile {
45  id     Int    @id @default(autoincrement())
46  userId Int    @unique
47  bio    String?
48  avatar String?
49  
50  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
51  
52  @@map("profiles")
53}
54
55model Post {
56  id        Int      @id @default(autoincrement())
57  authorId  Int
58  title     String
59  content   String
60  published Boolean  @default(false)
61  createdAt DateTime @default(now())
62  
63  author    User     @relation(fields: [authorId], references: [id])
64  comments  Comment[]
65  
66  @@map("posts")
67  @@index([authorId])
68}
69
70model Comment {
71  id        Int      @id @default(autoincrement())
72  postId    Int
73  authorId  Int
74  content   String
75  createdAt DateTime @default(now())
76  
77  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
78  author    User     @relation(fields: [authorId], references: [id])
79  
80  @@map("comments")
81  @@index([postId, authorId])
82}
83"#;
84
85#[derive(Default, Debug)]
86struct SchemaStats {
87    models: usize,
88    enums: usize,
89    total_fields: usize,
90    unique_constraints: usize,
91    indexes: usize,
92    relations: usize,
93    optional_fields: usize,
94    array_fields: usize,
95}
96
97impl Visitor for SchemaStats {
98    fn visit_model(&mut self, model: &ModelDecl) -> Result<()> {
99        self.models += 1;
100        self.total_fields += model.fields.len();
101
102        for attr in &model.attributes {
103            if let ModelAttribute::Index { .. } = attr {
104                self.indexes += 1
105            }
106        }
107
108        walk_model(self, model)
109    }
110
111    fn visit_enum(&mut self, _enum_decl: &EnumDecl) -> Result<()> {
112        self.enums += 1;
113        Ok(())
114    }
115
116    fn visit_field(&mut self, field: &FieldDecl) -> Result<()> {
117        if field.is_optional() {
118            self.optional_fields += 1;
119        }
120        if field.is_array() {
121            self.array_fields += 1;
122        }
123
124        for attr in &field.attributes {
125            match attr {
126                FieldAttribute::Unique => self.unique_constraints += 1,
127                FieldAttribute::Relation { .. } => self.relations += 1,
128                _ => {}
129            }
130        }
131
132        Ok(())
133    }
134}
135
136#[derive(Default)]
137struct RelationshipGraph {
138    // model_name -> [(related_model, field_name, is_required)]
139    relationships: HashMap<String, Vec<(String, String, bool)>>,
140}
141
142impl Visitor for RelationshipGraph {
143    fn visit_model(&mut self, model: &ModelDecl) -> Result<()> {
144        let model_name = model.name.value.clone();
145
146        for field in &model.fields {
147            if let FieldType::UserType(related_type) = &field.field_type {
148                if field.has_relation_attribute() || field.is_array() {
149                    let is_required = !field.is_optional() && !field.is_array();
150
151                    self.relationships
152                        .entry(model_name.clone())
153                        .or_default()
154                        .push((related_type.clone(), field.name.value.clone(), is_required));
155                }
156            }
157        }
158
159        walk_model(self, model)
160    }
161}
162
163impl RelationshipGraph {
164    fn print(&self) {
165        println!("🔗 Relationship Graph:");
166        for (model, relations) in &self.relationships {
167            println!("\n  {} has:", model);
168            for (related, field_name, is_required) in relations {
169                let cardinality = if *is_required { "one" } else { "zero or more" };
170                println!(
171                    "    - {} {} via field '{}'",
172                    cardinality, related, field_name
173                );
174            }
175        }
176    }
177}
178
179#[derive(Default)]
180struct DefaultValueCollector {
181    // model.field -> default_expr
182    defaults: HashMap<String, String>,
183}
184
185impl Visitor for DefaultValueCollector {
186    fn visit_model(&mut self, model: &ModelDecl) -> Result<()> {
187        let model_name = model.name.value.clone();
188
189        for field in &model.fields {
190            for attr in &field.attributes {
191                if let FieldAttribute::Default(expr, _) = attr {
192                    let key = format!("{}.{}", model_name, field.name.value);
193                    let value = format!("{:?}", expr);
194                    self.defaults.insert(key, value);
195                }
196            }
197        }
198
199        walk_model(self, model)
200    }
201}
202
203struct NamingValidator {
204    errors: Vec<String>,
205}
206
207impl NamingValidator {
208    fn new() -> Self {
209        Self { errors: Vec::new() }
210    }
211}
212
213impl Visitor for NamingValidator {
214    fn visit_model(&mut self, model: &ModelDecl) -> Result<()> {
215        if !model.name.value.chars().next().unwrap().is_uppercase() {
216            self.errors.push(format!(
217                "Model '{}' should start with uppercase",
218                model.name.value
219            ));
220        }
221
222        walk_model(self, model)
223    }
224
225    fn visit_field(&mut self, field: &FieldDecl) -> Result<()> {
226        if !field.name.value.chars().next().unwrap().is_lowercase() {
227            self.errors.push(format!(
228                "Field '{}' should start with lowercase",
229                field.name.value
230            ));
231        }
232
233        Ok(())
234    }
235
236    fn visit_enum(&mut self, enum_decl: &EnumDecl) -> Result<()> {
237        if !enum_decl.name.value.chars().next().unwrap().is_uppercase() {
238            self.errors.push(format!(
239                "Enum '{}' should start with uppercase",
240                enum_decl.name.value
241            ));
242        }
243
244        for variant in &enum_decl.variants {
245            if !variant
246                .name
247                .value
248                .chars()
249                .all(|c| c.is_uppercase() || c == '_')
250            {
251                self.errors.push(format!(
252                    "Enum variant '{}' should be UPPERCASE",
253                    variant.name.value
254                ));
255            }
256        }
257
258        Ok(())
259    }
260}
261
262#[derive(Default)]
263struct MigrationOrderer {
264    // Models with their foreign key dependencies
265    dependencies: HashMap<String, HashSet<String>>,
266}
267
268impl Visitor for MigrationOrderer {
269    fn visit_model(&mut self, model: &ModelDecl) -> Result<()> {
270        let model_name = model.name.value.clone();
271        let mut deps = HashSet::new();
272
273        for field in &model.fields {
274            if let FieldType::UserType(related_type) = &field.field_type {
275                for attr in &field.attributes {
276                    if let FieldAttribute::Relation {
277                        fields: Some(_), ..
278                    } = attr
279                    {
280                        deps.insert(related_type.clone());
281                    }
282                }
283            }
284        }
285
286        self.dependencies.insert(model_name, deps);
287        walk_model(self, model)
288    }
289}
290
291impl MigrationOrderer {
292    fn print(&self) {
293        println!("📦 Migration Order (based on foreign key dependencies):");
294        println!("  Models should be created in this order:\n");
295
296        let mut remaining: HashSet<_> = self.dependencies.keys().cloned().collect();
297        let mut order = Vec::new();
298
299        while !remaining.is_empty() {
300            let can_create: Vec<_> = remaining
301                .iter()
302                .filter(|m| {
303                    self.dependencies[*m]
304                        .iter()
305                        .all(|dep| order.contains(dep) || !remaining.contains(dep))
306                })
307                .cloned()
308                .collect();
309
310            if can_create.is_empty() {
311                println!("  ⚠️  Circular dependency detected!");
312                break;
313            }
314
315            for model in can_create {
316                order.push(model.clone());
317                remaining.remove(&model);
318
319                let deps_list: Vec<_> = self.dependencies[&model].iter().collect();
320                if deps_list.is_empty() {
321                    println!("  {}. {} (no dependencies)", order.len(), model);
322                } else {
323                    println!(
324                        "  {}. {} (depends on: {})",
325                        order.len(),
326                        model,
327                        deps_list
328                            .iter()
329                            .map(|s| s.as_str())
330                            .collect::<Vec<_>>()
331                            .join(", ")
332                    );
333                }
334            }
335        }
336    }
337}
338
339fn main() -> Result<()> {
340    println!("=== Nautilus Schema Visitor Pattern Demo ===\n");
341
342    let mut lexer = Lexer::new(SCHEMA);
343    let mut tokens = Vec::new();
344    loop {
345        let token = lexer.next_token()?;
346        if matches!(token.kind, nautilus_schema::TokenKind::Eof) {
347            tokens.push(token);
348            break;
349        }
350        tokens.push(token);
351    }
352    let schema = Parser::new(&tokens, SCHEMA).parse_schema()?;
353
354    println!(
355        "Parsed schema with {} declarations\n",
356        schema.declarations.len()
357    );
358    let separator = "=".repeat(60);
359    println!("{}", separator);
360
361    println!("\n1️⃣  Schema Statistics\n");
362    let mut stats = SchemaStats::default();
363    stats.visit_schema(&schema)?;
364    println!("{:#?}", stats);
365
366    println!("\n{}", separator);
367
368    println!("\n2️⃣  Relationship Analysis\n");
369    let mut graph = RelationshipGraph::default();
370    graph.visit_schema(&schema)?;
371    graph.print();
372
373    println!("\n{}", separator);
374
375    println!("\n3️⃣  Default Values\n");
376    let mut defaults = DefaultValueCollector::default();
377    defaults.visit_schema(&schema)?;
378    for (field, default) in &defaults.defaults {
379        println!("  {} = {}", field, default);
380    }
381
382    println!("\n{}", separator);
383
384    println!("\n4️⃣  Naming Convention Validation\n");
385    let mut validator = NamingValidator::new();
386    validator.visit_schema(&schema)?;
387    if validator.errors.is_empty() {
388        println!("  ✅ All names follow conventions!");
389    } else {
390        println!("  ⚠️  Found {} naming issues:", validator.errors.len());
391        for error in &validator.errors {
392            println!("    - {}", error);
393        }
394    }
395
396    println!("\n{}", separator);
397
398    println!("\n5️⃣  Migration Planning\n");
399    let mut orderer = MigrationOrderer::default();
400    orderer.visit_schema(&schema)?;
401    orderer.print();
402
403    println!("\n{}", separator);
404    println!("\n✅ Visitor demo completed successfully!");
405
406    Ok(())
407}