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 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
21/// What the generated file says it is.
22const 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
24/// The imports, each added when the file uses it. Java's own types are
25/// written in full (`java.util.List`): a message may be named `List`.
26const 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
33/// One level of indentation.
34const INDENT: &str = "    ";
35
36/// `player_name` → `playerName`.
37fn 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
53/// Java's reserved words: a field named like one gets a `_` after it.
54const 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
113/// A payload field's Java name: `player_name` → `playerName`, `interface` → `interface_`.
114fn 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
123/// `key_binding` → `KeyBinding`.
124pub(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/// The file being written, and the payload records it still owes.
133#[derive(Default)]
134struct Out {
135    text: String,
136    records: Vec<StructShape>,
137    /// The class's own members, written before its nested ones.
138    members: String,
139}
140
141/// A record with `s`'s fields: `toData()` builds the payload, `fromData` /
142/// `fromBytes` read one back; `body` adds the message's methods.
143fn 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
194/// `address` in Java: its name in the hosts' `YogNames` (one source), or the
195/// string itself if no name has it.
196fn name(address: &str) -> String {
197    yog_ids::export::java_ref(yog_ids::export::JAVA_CLASS, address)
198        .unwrap_or_else(|| format!("{address:?}"))
199}
200
201/// Where a message goes: its own address (`Address.<constant>`, resolved
202/// once), or its branch for the key field's value.
203fn 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
210/// A message to write: its place in the tree, its name, payload and methods.
211struct Entry {
212    path: &'static str,
213    name: &'static str,
214    shape: Shape,
215    body: String,
216}
217
218/// Closes the classes of `open` down to `keep`.
219fn 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
226/// Class `class` (documented `doc`) in `package`: `entries` nested by their paths.
227fn 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    // The class path open now (`Player`, `Session`), closed and opened as the paths go.
241    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            // A class opened again would be a second class of one name.
249            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        // Java refuses a member type named like a class around it.
265        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
277/// `class` holding `records` — and the records they hold in turn.
278fn 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
298/// The class closed, with its header and imports: its text, and the records it needs.
299fn 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
329/// Panics when two different payload structs share a record name — they'd
330/// be one class in `YogTypes`.
331fn 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
343/// Payloads that travel inside other bytes (entity data), not as messages:
344/// Java builds them too.
345const PAYLOADS: &[fn() -> Shape] = &[
346    <crate::entity::Hanging as yog_data::Schema>::shape,
347    // The content the runtime hands the hosts at registration (`content_blob`).
348    <crate::content::Content as yog_data::Schema>::shape,
349    // A mirror provider's turn (sent to its mirror's own address).
350    <crate::mirror::Write as yog_data::Schema>::shape,
351    // The host's block mirror's scope (inside a declaration's scope bytes).
352    <crate::mirror::BlockScope as yog_data::Schema>::shape,
353];
354
355/// Every generated class: its name and text.
356pub 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}