Skip to main content

intent_codegen/
swift.rs

1//! Swift skeleton code generator.
2//!
3//! Generates Swift structs with Codable conformance for entities,
4//! enums with String raw values for union types, and throwing
5//! functions for actions.
6
7use intent_parser::ast;
8
9use crate::types::map_type;
10use crate::{Language, doc_text, format_ensures_item, format_expr, to_camel_case};
11
12/// Swift reserved keywords that cannot be used as identifiers.
13const SWIFT_KEYWORDS: &[&str] = &[
14    "associatedtype",
15    "class",
16    "deinit",
17    "enum",
18    "extension",
19    "fileprivate",
20    "func",
21    "import",
22    "init",
23    "inout",
24    "internal",
25    "let",
26    "open",
27    "operator",
28    "private",
29    "precedencegroup",
30    "protocol",
31    "public",
32    "rethrows",
33    "return",
34    "static",
35    "struct",
36    "subscript",
37    "super",
38    "switch",
39    "throws",
40    "typealias",
41    "var",
42    "break",
43    "case",
44    "catch",
45    "continue",
46    "default",
47    "defer",
48    "do",
49    "else",
50    "fallthrough",
51    "for",
52    "guard",
53    "if",
54    "in",
55    "repeat",
56    "throw",
57    "try",
58    "where",
59    "while",
60    "as",
61    "false",
62    "is",
63    "nil",
64    "self",
65    "true",
66    "type",
67];
68
69/// Escape a Swift identifier if it collides with a reserved keyword.
70fn safe_ident(name: &str) -> String {
71    let camel = to_camel_case(name);
72    if SWIFT_KEYWORDS.contains(&camel.as_str()) {
73        format!("`{camel}`")
74    } else {
75        camel
76    }
77}
78
79/// Capitalize the first character of a string.
80fn capitalize(s: &str) -> String {
81    let mut chars = s.chars();
82    match chars.next() {
83        None => String::new(),
84        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
85    }
86}
87
88/// Generate Swift skeleton code from a parsed intent file.
89pub fn generate(file: &ast::File) -> String {
90    let lang = Language::Swift;
91    let mut out = String::new();
92
93    // Header
94    out.push_str(&format!(
95        "// Generated from {}.intent. DO NOT EDIT.\n",
96        file.module.name
97    ));
98    if let Some(doc) = &file.doc {
99        out.push('\n');
100        for line in &doc.lines {
101            out.push_str(&format!("// {line}\n"));
102        }
103    }
104    out.push('\n');
105
106    // Import Foundation for UUID, Decimal, Date
107    out.push_str("import Foundation\n\n");
108
109    for item in &file.items {
110        match item {
111            ast::TopLevelItem::Entity(e) => generate_entity(&mut out, e, &lang),
112            ast::TopLevelItem::Action(a) => generate_action(&mut out, a, &lang),
113            ast::TopLevelItem::Invariant(inv) => generate_invariant(&mut out, inv),
114            ast::TopLevelItem::EdgeCases(ec) => generate_edge_cases(&mut out, ec),
115            ast::TopLevelItem::Test(_) => {}
116        }
117    }
118
119    out
120}
121
122fn generate_entity(out: &mut String, entity: &ast::EntityDecl, lang: &Language) {
123    // Emit enum types for union-typed fields
124    for field in &entity.fields {
125        if let ast::TypeKind::Union(variants) = &field.ty.ty {
126            let enum_name = format!("{}{}", entity.name, capitalize(&field.name));
127            generate_union_enum(out, &enum_name, variants);
128        }
129    }
130
131    // Doc comment
132    if let Some(doc) = &entity.doc {
133        for line in doc_text(doc).lines() {
134            out.push_str(&format!("/// {line}\n"));
135        }
136    }
137
138    out.push_str(&format!("struct {}: Codable {{\n", entity.name));
139
140    for field in &entity.fields {
141        let ty = if let ast::TypeKind::Union(_) = &field.ty.ty {
142            let enum_name = format!("{}{}", entity.name, capitalize(&field.name));
143            if field.ty.optional {
144                format!("{enum_name}?")
145            } else {
146                enum_name
147            }
148        } else {
149            map_type(&field.ty, lang)
150        };
151        out.push_str(&format!("    let {}: {ty}\n", safe_ident(&field.name)));
152    }
153
154    out.push_str("}\n\n");
155}
156
157fn generate_union_enum(out: &mut String, name: &str, variants: &[ast::TypeKind]) {
158    let names: Vec<&str> = variants
159        .iter()
160        .filter_map(|v| match v {
161            ast::TypeKind::Simple(n) => Some(n.as_str()),
162            _ => None,
163        })
164        .collect();
165
166    out.push_str(&format!("enum {name}: String, Codable {{\n"));
167    for n in &names {
168        let case_name = to_camel_case(n);
169        out.push_str(&format!("    case {case_name} = \"{n}\"\n"));
170    }
171    out.push_str("}\n\n");
172}
173
174fn generate_action(out: &mut String, action: &ast::ActionDecl, lang: &Language) {
175    let fn_name = to_camel_case(&action.name);
176
177    // Doc comment
178    if let Some(doc) = &action.doc {
179        for line in doc_text(doc).lines() {
180            out.push_str(&format!("/// {line}\n"));
181        }
182    }
183
184    // Requires
185    if let Some(req) = &action.requires {
186        out.push_str("///\n/// - Requires:\n");
187        for cond in &req.conditions {
188            out.push_str(&format!("///   - `{}`\n", format_expr(cond)));
189        }
190    }
191
192    // Ensures
193    if let Some(ens) = &action.ensures {
194        out.push_str("///\n/// - Ensures:\n");
195        for item in &ens.items {
196            out.push_str(&format!("///   - `{}`\n", format_ensures_item(item)));
197        }
198    }
199
200    // Properties
201    if let Some(props) = &action.properties {
202        out.push_str("///\n/// - Properties:\n");
203        for entry in &props.entries {
204            out.push_str(&format!(
205                "///   - {}: {}\n",
206                entry.key,
207                crate::format_prop_value(&entry.value)
208            ));
209        }
210    }
211
212    // Function signature
213    let params: Vec<String> = action
214        .params
215        .iter()
216        .map(|p| {
217            let ty = map_type(&p.ty, lang);
218            format!("{}: {ty}", safe_ident(&p.name))
219        })
220        .collect();
221
222    out.push_str(&format!(
223        "func {fn_name}({}) throws {{\n",
224        params.join(", ")
225    ));
226    out.push_str(&format!("    fatalError(\"TODO: implement {fn_name}\")\n"));
227    out.push_str("}\n\n");
228}
229
230fn generate_invariant(out: &mut String, inv: &ast::InvariantDecl) {
231    out.push_str(&format!("// Invariant: {}\n", inv.name));
232    if let Some(doc) = &inv.doc {
233        for line in doc_text(doc).lines() {
234            out.push_str(&format!("// {line}\n"));
235        }
236    }
237    out.push_str(&format!("// {}\n\n", format_expr(&inv.body)));
238}
239
240fn generate_edge_cases(out: &mut String, ec: &ast::EdgeCasesDecl) {
241    out.push_str("// Edge cases:\n");
242    for rule in &ec.rules {
243        out.push_str(&format!(
244            "// when {} => {}()\n",
245            format_expr(&rule.condition),
246            rule.action.name,
247        ));
248    }
249    out.push('\n');
250}