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
210fn listen_target(address: &str, key: Option<&str>, constant: &str) -> String {
213 match key {
214 Some(_) => format!("YogBus.keyed({}, key)", name(address)),
215 None => format!("Address.{constant}"),
216 }
217}
218
219fn key_param(key: Option<&str>) -> &'static str {
221 if key.is_some() { "String key, " } else { "" }
222}
223
224struct Entry {
226 path: &'static str,
227 name: &'static str,
228 shape: Shape,
229 body: String,
230}
231
232fn close(out: &mut String, open: &mut Vec<String>, keep: usize) {
234 while open.len() > keep {
235 open.pop();
236 let _ = writeln!(out, "{}}}", INDENT.repeat(open.len() + 1));
237 }
238}
239
240fn file(
242 package: &str,
243 class: &str,
244 doc: &str,
245 mut o: Out,
246 entries: &[Entry],
247) -> (String, Vec<StructShape>) {
248 let _ = writeln!(
249 o.text,
250 "\n/** {doc} */\npublic final class {class} {{\n private {class}() {{}}"
251 );
252 let members = std::mem::take(&mut o.members);
253 o.text.push_str(&members);
254 let mut open: Vec<String> = Vec::new();
256 let mut opened: Vec<String> = Vec::new();
257 for e in entries {
258 let path: Vec<&str> = e.path.split('/').collect();
259 let common = open.iter().zip(&path).take_while(|(a, b)| a == *b).count();
260 close(&mut o.text, &mut open, common);
261 for (depth, seg) in path.iter().enumerate().skip(common) {
262 let whole = path[..=depth].join("/");
264 assert!(
265 !opened.contains(&whole),
266 "the entries of `{whole}` aren't together"
267 );
268 opened.push(whole);
269 let pad = INDENT.repeat(open.len() + 1);
270 let class = class_name(seg);
271 let _ = writeln!(
272 o.text,
273 "\n{pad}/** `{seg}`. */\n{pad}public static final class {class} {{\n{pad} private {class}() {{}}"
274 );
275 open.push((*seg).to_owned());
276 }
277 let Shape::Struct(s) = &e.shape else { continue };
278 assert!(
280 open.iter().all(|seg| class_name(seg) != e.name),
281 "{} is named like the class around it ({}): rename it",
282 e.name,
283 e.path
284 );
285 record(&mut o, &INDENT.repeat(open.len() + 1), e.name, s, &e.body);
286 }
287 close(&mut o.text, &mut open, 0);
288 finish(package, o)
289}
290
291fn types_file(package: &str, records: Vec<StructShape>) -> String {
293 let mut o = Out {
294 records,
295 ..Out::default()
296 };
297 let _ = writeln!(
298 o.text,
299 "\n/** The records the bus's messages share. */\npublic final class {TYPES} {{\n private {TYPES}() {{}}"
300 );
301 let mut done = 0;
302 while done < o.records.len() {
303 let s = o.records[done].clone();
304 record(&mut o, INDENT, s.name, &s, "");
305 done += 1;
306 }
307 unique(&o.records);
308 o.text.push_str(&words::classes(&o.records, INDENT));
309 finish(package, o).0
310}
311
312fn finish(package: &str, mut o: Out) -> (String, Vec<StructShape>) {
314 o.text.push_str("}\n");
315 let out = o.text;
316 let names = format!(
317 "{}.{}",
318 yog_ids::export::JAVA_PACKAGE,
319 yog_ids::export::JAVA_CLASS
320 );
321 let mut imports: Vec<String> = IMPORTS
322 .iter()
323 .filter(|(used, _)| out.contains(used))
324 .map(|(_, class)| (*class).to_owned())
325 .collect();
326 imports.push(names);
327 imports.sort();
328 let imports: String = imports
329 .iter()
330 .map(|class| format!("import {class};\n"))
331 .collect();
332 let text = format!("// {HEADER}\npackage {package};\n\n{imports}{out}");
333 (
334 text.lines()
335 .map(str::trim_end)
336 .collect::<Vec<_>>()
337 .join("\n")
338 + "\n",
339 o.records,
340 )
341}
342
343fn unique(records: &[StructShape]) {
346 for (i, a) in records.iter().enumerate() {
347 for b in &records[i + 1..] {
348 assert!(
349 a.name != b.name || same(a, b),
350 "two payload structs are named {}: rename one",
351 a.name
352 );
353 }
354 }
355}
356
357const PAYLOADS: &[fn() -> Shape] = &[
360 <crate::entity::Hanging as yog_data::Schema>::shape,
361 <crate::content::Content as yog_data::Schema>::shape,
363 <crate::mirror::Write as yog_data::Schema>::shape,
365 <crate::mirror::BlockScope as yog_data::Schema>::shape,
367 <crate::nanite::Mappings as yog_data::Schema>::shape,
369 <crate::nanite::Nanites as yog_data::Schema>::shape,
371];
372
373pub fn files(package: &str) -> [(&'static str, String); 4] {
375 let (events, mut records) = events::events(package);
376 let (services, more) = services::services(package);
377 for r in more {
378 if !records.iter().any(|have| same(have, &r)) {
379 records.push(r);
380 }
381 }
382 for payload in PAYLOADS {
383 let Shape::Struct(r) = payload() else {
384 continue;
385 };
386 if !records.iter().any(|have| same(have, &r)) {
387 records.push(r);
388 }
389 }
390 unique(&records);
391 [
392 (events::CLASS, events),
393 (services::CLASS, services),
394 (TYPES, types_file(package, records)),
395 (mirrors::CLASS, mirrors::file(package)),
396 ]
397}
398
399#[cfg(test)]
400mod tests {
401 use yog_data::{Schema, Shape};
402
403 use super::unique;
404
405 mod a {
406 #[derive(yog_data::Schema)]
407 pub struct Declare {
408 pub name: String,
409 }
410 }
411
412 mod b {
413 #[derive(yog_data::Schema)]
414 pub struct Declare {
415 pub label: String,
416 }
417 }
418
419 fn shape<T: Schema>() -> yog_data::schema::StructShape {
420 let Shape::Struct(s) = T::shape() else {
421 unreachable!("a derived struct")
422 };
423 s
424 }
425
426 #[test]
427 #[should_panic(expected = "two payload structs are named Declare")]
428 fn two_structs_of_one_name_are_refused() {
429 unique(&[shape::<a::Declare>(), shape::<b::Declare>()]);
430 }
431
432 #[test]
433 fn one_struct_seen_twice_is_fine() {
434 unique(&[shape::<a::Declare>(), shape::<a::Declare>()]);
435 }
436}