1mod events;
7mod frames;
8mod services;
9mod types;
10mod views;
11mod words;
12
13use std::fmt::Write;
14
15use yog_data::Shape;
16use yog_data::schema::StructShape;
17
18use types::{TYPES, from_data, java_type, nullable, same, to_data};
19
20const HEADER: &str = "Generated by yog-event from Yog-Mod-Loader/rust/crates/yog-event — don't edit; regenerate with `cargo run -p yog-event --bin yog-event-export`.";
22
23const IMPORTS: &[(&str, &str)] = &[
26 ("YogBus.", "dev.yog.YogBus"),
27 ("YogData.", "dev.yog.data.YogData"),
28 ("YogFrame.", "dev.yog.data.YogFrame"),
29 ("YogMessage", "dev.yog.YogMessage"),
30];
31
32const INDENT: &str = " ";
34
35fn camel(snake: &str) -> String {
37 let mut out = String::new();
38 let mut upper = false;
39 for c in snake.chars() {
40 if c == '_' {
41 upper = true;
42 } else if upper {
43 out.extend(c.to_uppercase());
44 upper = false;
45 } else {
46 out.push(c);
47 }
48 }
49 out
50}
51
52const JAVA_KEYWORDS: &[&str] = &[
54 "abstract",
55 "assert",
56 "boolean",
57 "break",
58 "byte",
59 "case",
60 "catch",
61 "char",
62 "class",
63 "const",
64 "continue",
65 "default",
66 "do",
67 "double",
68 "else",
69 "enum",
70 "extends",
71 "final",
72 "finally",
73 "float",
74 "for",
75 "goto",
76 "if",
77 "implements",
78 "import",
79 "instanceof",
80 "int",
81 "interface",
82 "long",
83 "native",
84 "new",
85 "package",
86 "private",
87 "protected",
88 "public",
89 "return",
90 "short",
91 "static",
92 "strictfp",
93 "super",
94 "switch",
95 "synchronized",
96 "this",
97 "throw",
98 "throws",
99 "transient",
100 "try",
101 "void",
102 "volatile",
103 "while",
104 "true",
105 "false",
106 "null",
107 "record",
108 "var",
109 "yield",
110];
111
112fn field(snake: &str) -> String {
114 let name = camel(snake);
115 if JAVA_KEYWORDS.contains(&name.as_str()) {
116 format!("{name}_")
117 } else {
118 name
119 }
120}
121
122pub(super) fn class_name(segment: &str) -> String {
124 let mut out = camel(segment);
125 if let Some(first) = out.get(..1) {
126 out.replace_range(..1, &first.to_uppercase());
127 }
128 out
129}
130
131#[derive(Default)]
133struct Out {
134 text: String,
135 records: Vec<StructShape>,
136 members: String,
138}
139
140fn record(o: &mut Out, pad: &str, name: &str, s: &StructShape, body: &str) {
143 let Out {
144 text: out, records, ..
145 } = o;
146 let params: Vec<String> = s
147 .fields
148 .iter()
149 .map(|f| format!("{} {}", java_type(&(f.shape)(), records), field(f.name)))
150 .collect();
151 if !s.doc.is_empty() {
152 let _ = writeln!(out, "\n{pad}/** {} */", s.doc);
153 }
154 let _ = writeln!(out, "{pad}public record {name}({}) {{", params.join(", "));
155 let _ = writeln!(
156 out,
157 "{pad} public java.util.Map<String, Object> toData() {{\n{pad} java.util.Map<String, Object> m = new java.util.LinkedHashMap<>();"
158 );
159 for f in &s.fields {
160 let shape = (f.shape)();
161 let field = field(f.name);
162 let line = match nullable(&shape) {
163 Some(inner) => format!(
164 "if ({field} != null) m.put(\"{}\", {});",
165 f.name,
166 to_data(&inner, &field, 0)
167 ),
168 None => format!("m.put(\"{}\", {});", f.name, to_data(&shape, &field, 0)),
169 };
170 let _ = writeln!(out, "{pad} {line}");
171 }
172 let _ = writeln!(out, "{pad} return m;\n{pad} }}\n");
173 let args: Vec<String> = s
174 .fields
175 .iter()
176 .map(|f| from_data(&(f.shape)(), &format!("m.get(\"{}\")", f.name), 0))
177 .collect();
178 let _ = writeln!(
179 out,
180 "{pad} public static {name} fromData(Object data) {{\n{pad} java.util.Map<String, Object> m = YogData.map(data);\n{pad} return new {name}({});\n{pad} }}\n",
181 args.join(", ")
182 );
183 let _ = write!(
184 out,
185 "{pad} public static {name} fromBytes(byte[] bytes) {{\n{pad} return fromData(YogData.decode(bytes));\n{pad} }}"
186 );
187 out.push_str(&frames::codec(name, s, pad));
188 out.push_str(&views::view(name, s, pad));
189 let body = body.replace('\n', &format!("\n{pad}"));
190 let _ = writeln!(out, "{body}\n{pad}}}");
191}
192
193fn name(address: &str) -> String {
196 yog_ids::export::java_ref(yog_ids::export::JAVA_CLASS, address)
197 .unwrap_or_else(|| format!("{address:?}"))
198}
199
200fn target(address: &str, key: Option<&str>, constant: &str) -> String {
203 match key {
204 Some(k) => format!("YogBus.keyed({}, {})", name(address), field(k)),
205 None => format!("Address.{constant}"),
206 }
207}
208
209struct Entry {
211 path: &'static str,
212 name: &'static str,
213 shape: Shape,
214 body: String,
215}
216
217fn close(out: &mut String, open: &mut Vec<String>, keep: usize) {
219 while open.len() > keep {
220 open.pop();
221 let _ = writeln!(out, "{}}}", INDENT.repeat(open.len() + 1));
222 }
223}
224
225fn file(
227 package: &str,
228 class: &str,
229 doc: &str,
230 mut o: Out,
231 entries: &[Entry],
232) -> (String, Vec<StructShape>) {
233 let _ = writeln!(
234 o.text,
235 "\n/** {doc} */\npublic final class {class} {{\n private {class}() {{}}"
236 );
237 let members = std::mem::take(&mut o.members);
238 o.text.push_str(&members);
239 let mut open: Vec<String> = Vec::new();
241 let mut opened: Vec<String> = Vec::new();
242 for e in entries {
243 let path: Vec<&str> = e.path.split('/').collect();
244 let common = open.iter().zip(&path).take_while(|(a, b)| a == *b).count();
245 close(&mut o.text, &mut open, common);
246 for (depth, seg) in path.iter().enumerate().skip(common) {
247 let whole = path[..=depth].join("/");
249 assert!(
250 !opened.contains(&whole),
251 "the entries of `{whole}` aren't together"
252 );
253 opened.push(whole);
254 let pad = INDENT.repeat(open.len() + 1);
255 let class = class_name(seg);
256 let _ = writeln!(
257 o.text,
258 "\n{pad}/** `{seg}`. */\n{pad}public static final class {class} {{\n{pad} private {class}() {{}}"
259 );
260 open.push((*seg).to_owned());
261 }
262 let Shape::Struct(s) = &e.shape else { continue };
263 assert!(
265 open.iter().all(|seg| class_name(seg) != e.name),
266 "{} is named like the class around it ({}): rename it",
267 e.name,
268 e.path
269 );
270 record(&mut o, &INDENT.repeat(open.len() + 1), e.name, s, &e.body);
271 }
272 close(&mut o.text, &mut open, 0);
273 finish(package, o)
274}
275
276fn types_file(package: &str, records: Vec<StructShape>) -> String {
278 let mut o = Out {
279 records,
280 ..Out::default()
281 };
282 let _ = writeln!(
283 o.text,
284 "\n/** The records the bus's messages share. */\npublic final class {TYPES} {{\n private {TYPES}() {{}}"
285 );
286 let mut done = 0;
287 while done < o.records.len() {
288 let s = o.records[done].clone();
289 record(&mut o, INDENT, s.name, &s, "");
290 done += 1;
291 }
292 unique(&o.records);
293 o.text.push_str(&words::classes(&o.records, INDENT));
294 finish(package, o).0
295}
296
297fn finish(package: &str, mut o: Out) -> (String, Vec<StructShape>) {
299 o.text.push_str("}\n");
300 let out = o.text;
301 let names = format!(
302 "{}.{}",
303 yog_ids::export::JAVA_PACKAGE,
304 yog_ids::export::JAVA_CLASS
305 );
306 let mut imports: Vec<String> = IMPORTS
307 .iter()
308 .filter(|(used, _)| out.contains(used))
309 .map(|(_, class)| (*class).to_owned())
310 .collect();
311 imports.push(names);
312 imports.sort();
313 let imports: String = imports
314 .iter()
315 .map(|class| format!("import {class};\n"))
316 .collect();
317 let text = format!("// {HEADER}\npackage {package};\n\n{imports}{out}");
318 (
319 text.lines()
320 .map(str::trim_end)
321 .collect::<Vec<_>>()
322 .join("\n")
323 + "\n",
324 o.records,
325 )
326}
327
328fn unique(records: &[StructShape]) {
331 for (i, a) in records.iter().enumerate() {
332 for b in &records[i + 1..] {
333 assert!(
334 a.name != b.name || same(a, b),
335 "two payload structs are named {}: rename one",
336 a.name
337 );
338 }
339 }
340}
341
342const PAYLOADS: &[fn() -> Shape] = &[
345 <crate::entity::Hanging as yog_data::Schema>::shape,
346 <crate::content::Content as yog_data::Schema>::shape,
348];
349
350pub fn files(package: &str) -> [(&'static str, String); 3] {
352 let (events, mut records) = events::events(package);
353 let (services, more) = services::services(package);
354 for r in more {
355 if !records.iter().any(|have| same(have, &r)) {
356 records.push(r);
357 }
358 }
359 for payload in PAYLOADS {
360 let Shape::Struct(r) = payload() else {
361 continue;
362 };
363 if !records.iter().any(|have| same(have, &r)) {
364 records.push(r);
365 }
366 }
367 unique(&records);
368 [
369 (events::CLASS, events),
370 (services::CLASS, services),
371 (TYPES, types_file(package, records)),
372 ]
373}
374
375#[cfg(test)]
376mod tests {
377 use yog_data::{Schema, Shape};
378
379 use super::unique;
380
381 mod a {
382 #[derive(yog_data::Schema)]
383 pub struct Declare {
384 pub name: String,
385 }
386 }
387
388 mod b {
389 #[derive(yog_data::Schema)]
390 pub struct Declare {
391 pub label: String,
392 }
393 }
394
395 fn shape<T: Schema>() -> yog_data::schema::StructShape {
396 let Shape::Struct(s) = T::shape() else {
397 unreachable!("a derived struct")
398 };
399 s
400 }
401
402 #[test]
403 #[should_panic(expected = "two payload structs are named Declare")]
404 fn two_structs_of_one_name_are_refused() {
405 unique(&[shape::<a::Declare>(), shape::<b::Declare>()]);
406 }
407
408 #[test]
409 fn one_struct_seen_twice_is_fine() {
410 unique(&[shape::<a::Declare>(), shape::<a::Declare>()]);
411 }
412}