1use bern_ir::Application;
9use bern_protocol::{EnumDef, FieldDef, RecordDef, Type, TypeTable, Value};
10use std::path::Path;
11
12const P: &str = "bern_ffi::protocol";
13
14pub fn type_expr(ty: &Type) -> String {
15 match ty {
16 Type::Unit => format!("{P}::Type::Unit"),
17 Type::Bool => format!("{P}::Type::Bool"),
18 Type::Int => format!("{P}::Type::Int"),
19 Type::Float => format!("{P}::Type::Float"),
20 Type::Text => format!("{P}::Type::Text"),
21 Type::Enum { name } => format!("{P}::Type::Enum {{ name: {name:?}.into() }}"),
22 Type::Record { name } => format!("{P}::Type::Record {{ name: {name:?}.into() }}"),
23 Type::Optional { inner } => format!("{P}::Type::optional({})", type_expr(inner)),
24 Type::List { inner } => format!("{P}::Type::list({})", type_expr(inner)),
25 }
26}
27
28pub fn value_expr(value: &Value) -> String {
29 let list = |values: &[Value]| values.iter().map(value_expr).collect::<Vec<_>>().join(", ");
30 match value {
31 Value::Unit => format!("{P}::Value::Unit"),
32 Value::Bool(value) => format!("{P}::Value::Bool({value})"),
33 Value::Int(value) => format!("{P}::Value::Int({value})"),
34 Value::Float(value) => format!("{P}::Value::Float({value:?})"),
35 Value::Text(value) => format!("{P}::Value::Text({value:?}.into())"),
36 Value::Enum { variant, fields } => format!(
37 "{P}::Value::Enum {{ variant: {variant}, fields: vec![{}] }}",
38 list(fields)
39 ),
40 Value::Optional(None) => format!("{P}::Value::Optional(None)"),
41 Value::Optional(Some(value)) => format!("{P}::Value::some({})", value_expr(value)),
42 Value::Record(values) => format!("{P}::Value::Record(vec![{}])", list(values)),
43 Value::List(values) => format!("{P}::Value::List(vec![{}])", list(values)),
44 }
45}
46
47fn field_expr(field: &FieldDef) -> String {
48 format!(
49 "{P}::FieldDef {{ name: {:?}.into(), ty: {}, doc: String::new(), content: {} }}",
50 field.name,
51 type_expr(&field.ty),
52 field.content
53 )
54}
55
56fn fields_expr(fields: &[FieldDef]) -> String {
57 fields.iter().map(field_expr).collect::<Vec<_>>().join(", ")
58}
59
60fn enum_expr(def: &EnumDef) -> String {
61 let variants: Vec<_> = def
62 .variants
63 .iter()
64 .map(|variant| {
65 let label = match &variant.label {
68 Some(label) => format!(
69 "Some({P}::LocalizedText {{ key: {:?}.into(), translations: ::std::default::Default::default(), params: vec![{}] }})",
70 label.key,
71 label
72 .params
73 .iter()
74 .map(|param| format!("{param:?}.into()"))
75 .collect::<Vec<_>>()
76 .join(", ")
77 ),
78 None => "None".into(),
79 };
80 format!(
81 "{P}::VariantDef {{ name: {:?}.into(), fields: vec![{}], label: {label} }}",
82 variant.name,
83 fields_expr(&variant.fields)
84 )
85 })
86 .collect();
87 format!(
88 "{P}::EnumDef {{ name: {:?}.into(), variants: vec![{}] }}",
89 def.name,
90 variants.join(", ")
91 )
92}
93
94fn record_expr(def: &RecordDef) -> String {
95 format!(
96 "{P}::RecordDef {{ name: {:?}.into(), fields: vec![{}] }}",
97 def.name,
98 fields_expr(&def.fields)
99 )
100}
101
102pub fn types_expr(types: &TypeTable) -> String {
103 format!(
104 "{P}::TypeTable {{ enums: vec![{}], records: vec![{}] }}",
105 types
106 .enums
107 .iter()
108 .map(enum_expr)
109 .collect::<Vec<_>>()
110 .join(", "),
111 types
112 .records
113 .iter()
114 .map(record_expr)
115 .collect::<Vec<_>>()
116 .join(", ")
117 )
118}
119
120#[derive(Clone, Copy)]
122pub enum WindowPresentation {
123 Native,
124 Virtual,
125}
126
127pub fn session_spec(app: &Application, presentation: WindowPresentation) -> String {
129 let presentation = match presentation {
130 WindowPresentation::Native => "bern_ffi::runtime::Presentation::Native",
131 WindowPresentation::Virtual => "bern_ffi::runtime::Presentation::Virtual",
132 };
133 session_spec_with(app, &|_| presentation.to_string())
134}
135
136pub fn session_spec_with(
140 app: &Application,
141 presentation: &dyn Fn(&bern_ir::WindowDef) -> String,
142) -> String {
143 let sessions: Vec<String> = app
144 .sessions
145 .iter()
146 .map(|session| {
147 let fields: Vec<String> = session
148 .fields
149 .iter()
150 .map(|field| {
151 format!(
152 "({:?}.into(), {}, {})",
153 field.name,
154 type_expr(&field.ty),
155 value_expr(&field.initial)
156 )
157 })
158 .collect();
159 format!(
160 "bern_ffi::SessionType {{ name: {:?}.into(), fields: vec![{}] }}",
161 session.name,
162 fields.join(", ")
163 )
164 })
165 .collect();
166 let windows: Vec<String> = app
167 .windows
168 .windows
169 .iter()
170 .map(|window| {
171 let indices: Vec<String> = window
172 .sessions
173 .iter()
174 .filter_map(|name| {
175 app.sessions
176 .iter()
177 .position(|session| session.name == *name)
178 })
179 .map(|index| index.to_string())
180 .collect();
181 format!(
182 "bern_ffi::WindowKind {{ id: {:?}.into(), sessions: vec![{}], presentation: {} }}",
183 window.id,
184 indices.join(", "),
185 presentation(window)
186 )
187 })
188 .collect();
189 format!(
190 "bern_ffi::runtime::SessionSpec {{\n types: {},\n sessions: vec![\n {}\n ],\n windows: vec![\n {}\n ],\n }}",
191 types_expr(&app.types),
192 sessions.join(",\n "),
193 windows.join(",\n ")
194 )
195}
196
197pub fn players_expr(app: &Application) -> String {
199 app.players
200 .iter()
201 .map(|player| format!("{:?}.into()", player.id))
202 .collect::<Vec<_>>()
203 .join(", ")
204}
205
206pub fn resources_expr(app: &Application) -> String {
207 app.resources
208 .iter()
209 .map(|resource| format!("{:?}.into()", resource.name))
210 .collect::<Vec<_>>()
211 .join(", ")
212}
213
214pub fn engine_expr(context: &crate::BuildContext) -> String {
218 let adapter = |protocol: &str, engine: &str| {
219 format!("{protocol}::Adapter(<{engine} as ::std::default::Default>::default())")
220 };
221 let shell = adapter(&context.protocol_module, &context.engine_type);
222 if context.modules.is_empty() {
223 return shell;
224 }
225 let modules: Vec<String> = context
226 .modules
227 .iter()
228 .map(|module| {
229 let depends: Vec<String> = module
230 .depends
231 .iter()
232 .map(|id| format!("{id:?}.to_string()"))
233 .collect();
234 format!(
235 "::bern::__private::command::ModuleEngine {{ descriptor: {}::descriptor(), start: ::std::boxed::Box::new(|| ::std::boxed::Box::new({})), optional: {}, enabled: {}, depends: ::std::vec![{}] }}",
236 module.protocol_module,
237 adapter(&module.protocol_module, &module.engine_type),
238 module.optional,
239 module.enabled,
240 depends.join(", ")
241 )
242 })
243 .collect();
244 format!(
245 "::bern::__private::command::Composite::new(::std::boxed::Box::new({shell}), ::std::vec![{}]).unwrap_or_else(|error| panic!(\"{{error}}\"))",
246 modules.join(", ")
247 )
248}
249
250pub fn descriptor_expr(context: &crate::BuildContext) -> String {
253 let shell = format!("{}::descriptor()", context.protocol_module);
254 if context.modules.is_empty() {
255 return shell;
256 }
257 let modules: Vec<String> = context
258 .modules
259 .iter()
260 .map(|module| {
261 format!(
262 "::bern::__private::protocol::ModulePart {{ descriptor: {}::descriptor(), optional: {} }}",
263 module.protocol_module, module.optional
264 )
265 })
266 .collect();
267 format!(
268 "::std::boxed::Box::leak(::std::boxed::Box::new(::bern::__private::protocol::compose({shell}, &[{}]).unwrap_or_else(|error| panic!(\"{{error}}\"))))",
269 modules.join(", ")
270 )
271}
272
273pub fn module_dependencies(context: &crate::BuildContext, engines: bool) -> String {
276 if context.modules.is_empty() {
277 return String::new();
278 }
279 let path = |path: &std::path::Path| path.display().to_string().replace('\\', "/");
280 let mut lines = vec![format!(
281 "bern = {{ {} }}",
282 context.framework.dependency("bern")
283 )];
284 for module in &context.modules {
285 lines.push(format!(
286 "{} = {{ path = \"{}\" }}",
287 module.protocol_package,
288 path(&module.protocol_path)
289 ));
290 if engines {
291 lines.push(format!(
292 "{} = {{ path = \"{}\" }}",
293 module.engine_package,
294 path(&module.engine_path)
295 ));
296 }
297 }
298 lines.join("\n") + "\n"
299}
300
301pub fn path(path: &Path) -> String {
304 path.display().to_string().replace('\\', "/")
305}
306
307pub const RELEASE_PROFILE: &str = "codegen-units = 1\nlto = true\nstrip = true\n";
313
314pub fn profiles(context: &crate::BuildContext, fixed: &str) -> String {
323 let table = |text: &str| text.parse::<toml::Table>().unwrap_or_default();
324 let mut release = table(RELEASE_PROFILE);
325 let mut tables = toml::Table::new();
326 let own = std::fs::read_to_string(context.workspace_dir.join("Cargo.toml"))
327 .ok()
328 .and_then(|text| text.parse::<toml::Table>().ok())
329 .and_then(|manifest| manifest.get("profile")?.as_table().cloned())
330 .unwrap_or_default();
331 for (name, value) in own {
332 match (name.as_str(), value) {
333 ("release", toml::Value::Table(settings)) => release.extend(settings),
334 (_, value) => {
335 tables.insert(name, value);
336 }
337 }
338 }
339 release.extend(table(fixed));
340 tables.insert("release".into(), toml::Value::Table(release));
341 let mut root = toml::Table::new();
342 root.insert("profile".into(), toml::Value::Table(tables));
343 toml::to_string(&root).unwrap_or_default()
344}
345
346pub fn program(app: &Application) -> String {
348 app.app.id.replace('_', "-")
349}
350
351pub fn cli_rs(context: &crate::BuildContext, app: &Application) -> String {
353 format!(
354 r#"//! Generated by Bern. Application CLI derived from the command catalog.
355
356fn main() {{
357 std::process::exit(bern_adapter_cli::run(
358 {descriptor},
359 {bundle:?},
360 {name:?},
361 ));
362}}
363"#,
364 descriptor = descriptor_expr(context),
365 bundle = app.app.bundle_id,
366 name = program(app),
367 )
368}
369
370pub fn mcp_rs(context: &crate::BuildContext, app: &Application) -> String {
372 format!(
373 r#"//! Generated by Bern. MCP server derived from the command catalog.
374
375fn main() {{
376 std::process::exit(bern_adapter_mcp::run(
377 {descriptor},
378 {bundle:?},
379 {name:?},
380 {version:?},
381 ));
382}}
383"#,
384 descriptor = descriptor_expr(context),
385 bundle = app.app.bundle_id,
386 name = program(app),
387 version = app.app.version,
388 )
389}