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