Skip to main content

yog_event/
java.rs

1//! The bus's messages for Java: a record per event (`dev.yog.bus.YogEvents`)
2//! and per service (`YogServices`), from the same schemas, that encodes
3//! itself and goes on the bus — the host fires events and calls services
4//! through these instead of a JNI method each, and reads the replies.
5
6mod 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
20/// What the generated file says it is.
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`.";
22
23/// The imports, each added when the file uses it. Java's own types are
24/// written in full (`java.util.List`): a message may be named `List`.
25const 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
32/// One level of indentation.
33const INDENT: &str = "    ";
34
35/// `player_name` → `playerName`.
36fn 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
52/// Java's reserved words: a field named like one gets a `_` after it.
53const 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
112/// A payload field's Java name: `player_name` → `playerName`, `interface` → `interface_`.
113fn 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
122/// `key_binding` → `KeyBinding`.
123pub(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/// The file being written, and the payload records it still owes.
132#[derive(Default)]
133struct Out {
134    text: String,
135    records: Vec<StructShape>,
136    /// The class's own members, written before its nested ones.
137    members: String,
138}
139
140/// A record with `s`'s fields: `toData()` builds the payload, `fromData` /
141/// `fromBytes` read one back; `body` adds the message's methods.
142fn 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
193/// `address` in Java: its name in the hosts' `YogNames` (one source), or the
194/// string itself if no name has it.
195fn name(address: &str) -> String {
196    yog_ids::export::java_ref(yog_ids::export::JAVA_CLASS, address)
197        .unwrap_or_else(|| format!("{address:?}"))
198}
199
200/// Where a message goes: its own address (`Address.<constant>`, resolved
201/// once), or its branch for the key field's value.
202fn 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
209/// A message to write: its place in the tree, its name, payload and methods.
210struct Entry {
211    path: &'static str,
212    name: &'static str,
213    shape: Shape,
214    body: String,
215}
216
217/// Closes the classes of `open` down to `keep`.
218fn 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
225/// Class `class` (documented `doc`) in `package`: `entries` nested by their paths.
226fn 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    // The class path open now (`Player`, `Session`), closed and opened as the paths go.
240    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            // A class opened again would be a second class of one name.
248            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        // Java refuses a member type named like a class around it.
264        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
276/// `class` holding `records` — and the records they hold in turn.
277fn 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
297/// The class closed, with its header and imports: its text, and the records it needs.
298fn 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
328/// Panics when two different payload structs share a record name — they'd
329/// be one class in `YogTypes`.
330fn 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
342/// Payloads that travel inside other bytes (entity data), not as messages:
343/// Java builds them too.
344const PAYLOADS: &[fn() -> Shape] = &[
345    <crate::entity::Hanging as yog_data::Schema>::shape,
346    // The content the runtime hands the hosts at registration (`content_blob`).
347    <crate::content::Content as yog_data::Schema>::shape,
348];
349
350/// Every generated class: its name and text.
351pub 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}