1use intent_parser::ast;
8
9use crate::types::map_type;
10use crate::{Language, doc_text, format_ensures_item, format_expr, to_camel_case};
11
12const 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
69fn 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
79fn 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
88pub fn generate(file: &ast::File) -> String {
90 let lang = Language::Swift;
91 let mut out = String::new();
92
93 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 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 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 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 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 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 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 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 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}