1mod events;
7mod frames;
8mod hosted;
9mod mirrors;
10mod services;
11mod types;
12mod views;
13mod words;
14
15use std::fmt::Write;
16
17use yog_codegen::{Blocks, Header, Identifiers, Regenerate, camel, pascal};
18use yog_codegen_java::Java;
19use yog_data::Shape;
20use yog_data::schema::StructShape;
21
22pub use types::TYPES;
24use types::{from_data, java_type, nullable, same, to_data};
25
26pub(super) const HEADER: Header = Header {
28 tool: "yog-event",
29 source: "Yog-Mod-Loader/rust/crates/yog-event",
30 regenerate: Regenerate::Command("cargo run -p yog-event --bin yog-event-export"),
31};
32
33const IMPORTS: &[(&str, &str)] = &[
36 ("YogBus.", "dev.yog.YogBus"),
37 ("YogData.", "dev.yog.data.YogData"),
38 ("YogFrame.", "dev.yog.data.YogFrame"),
39 ("YogMessage", "dev.yog.YogMessage"),
40];
41
42const INDENT: &str = Java::INDENT;
44
45pub const PACKAGE: &str = "dev.yog.bus";
47pub const EVENTS: &str = events::CLASS;
49pub const SERVICES: &str = services::CLASS;
50
51pub fn field(snake: &str) -> String {
53 Java::ident(&camel(snake))
54}
55
56pub fn record_path(domain: &str, name: &str) -> String {
59 domain
60 .split('/')
61 .map(class_name)
62 .chain([name.to_owned()])
63 .collect::<Vec<_>>()
64 .join(".")
65}
66
67pub(super) fn class_name(segment: &str) -> String {
69 pascal(segment)
70}
71
72#[derive(Default)]
74struct Out {
75 text: String,
76 records: Vec<StructShape>,
77 members: String,
79}
80
81fn record(o: &mut Out, pad: &str, name: &str, s: &StructShape, body: &str) {
84 let Out {
85 text: out, records, ..
86 } = o;
87 let params: Vec<String> = s
88 .fields
89 .iter()
90 .map(|f| format!("{} {}", java_type(&(f.shape)(), records), field(f.name)))
91 .collect();
92 if !s.doc.is_empty() {
93 let _ = writeln!(out, "\n{pad}/** {} */", s.doc);
94 }
95 let _ = writeln!(out, "{pad}public record {name}({}) {{", params.join(", "));
96 let _ = writeln!(
97 out,
98 "{pad} public java.util.Map<String, Object> toData() {{\n{pad} java.util.Map<String, Object> m = new java.util.LinkedHashMap<>();"
99 );
100 for f in &s.fields {
101 let shape = (f.shape)();
102 let field = field(f.name);
103 let line = match nullable(&shape) {
104 Some(inner) => format!(
105 "if ({field} != null) m.put(\"{}\", {});",
106 f.name,
107 to_data(&inner, &field, 0)
108 ),
109 None => format!("m.put(\"{}\", {});", f.name, to_data(&shape, &field, 0)),
110 };
111 let _ = writeln!(out, "{pad} {line}");
112 }
113 let _ = writeln!(out, "{pad} return m;\n{pad} }}\n");
114 let args: Vec<String> = s
115 .fields
116 .iter()
117 .map(|f| from_data(&(f.shape)(), &format!("m.get(\"{}\")", f.name), 0))
118 .collect();
119 let _ = writeln!(
120 out,
121 "{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",
122 args.join(", ")
123 );
124 let _ = write!(
125 out,
126 "{pad} public static {name} fromBytes(byte[] bytes) {{\n{pad} return fromData(YogData.decode(bytes));\n{pad} }}"
127 );
128 out.push_str(&frames::codec(name, s, pad));
129 out.push_str(&views::view(name, s, pad));
130 let body = body.replace('\n', &format!("\n{pad}"));
131 let _ = writeln!(out, "{body}\n{pad}}}");
132}
133
134fn name(address: &str) -> String {
137 yog_ids::export::java_ref(yog_ids::export::JAVA_CLASS, address)
138 .unwrap_or_else(|| format!("{address:?}"))
139}
140
141fn target(address: &str, key: Option<&str>, constant: &str) -> String {
144 match key {
145 Some(k) => format!("YogBus.keyed({}, {})", name(address), field(k)),
146 None => format!("Address.{constant}"),
147 }
148}
149
150fn listen_target(address: &str, key: Option<&str>, constant: &str) -> String {
153 match key {
154 Some(_) => format!("YogBus.keyed({}, key)", name(address)),
155 None => format!("Address.{constant}"),
156 }
157}
158
159fn key_param(key: Option<&str>) -> &'static str {
161 if key.is_some() { "String key, " } else { "" }
162}
163
164struct Entry {
166 path: &'static str,
167 name: &'static str,
168 shape: Shape,
169 body: String,
170}
171
172fn close(out: &mut String, open: &mut Vec<String>, keep: usize) {
174 while open.len() > keep {
175 open.pop();
176 let _ = writeln!(out, "{}}}", INDENT.repeat(open.len() + 1));
177 }
178}
179
180fn file(
182 package: &str,
183 class: &str,
184 doc: &str,
185 mut o: Out,
186 entries: &[Entry],
187) -> (String, Vec<StructShape>) {
188 let _ = writeln!(
189 o.text,
190 "\n/** {doc} */\npublic final class {class} {{\n private {class}() {{}}"
191 );
192 let members = std::mem::take(&mut o.members);
193 o.text.push_str(&members);
194 let mut open: Vec<String> = Vec::new();
196 let mut opened: Vec<String> = Vec::new();
197 for e in entries {
198 let path: Vec<&str> = e.path.split('/').collect();
199 let common = open.iter().zip(&path).take_while(|(a, b)| a == *b).count();
200 close(&mut o.text, &mut open, common);
201 for (depth, seg) in path.iter().enumerate().skip(common) {
202 let whole = path[..=depth].join("/");
204 assert!(
205 !opened.contains(&whole),
206 "the entries of `{whole}` aren't together"
207 );
208 opened.push(whole);
209 let pad = INDENT.repeat(open.len() + 1);
210 let class = class_name(seg);
211 let _ = writeln!(
212 o.text,
213 "\n{pad}/** `{seg}`. */\n{pad}public static final class {class} {{\n{pad} private {class}() {{}}"
214 );
215 open.push((*seg).to_owned());
216 }
217 let Shape::Struct(s) = &e.shape else { continue };
218 assert!(
220 open.iter().all(|seg| class_name(seg) != e.name),
221 "{} is named like the class around it ({}): rename it",
222 e.name,
223 e.path
224 );
225 record(&mut o, &INDENT.repeat(open.len() + 1), e.name, s, &e.body);
226 }
227 close(&mut o.text, &mut open, 0);
228 finish(package, o)
229}
230
231fn types_file(package: &str, records: Vec<StructShape>) -> String {
233 let mut o = Out {
234 records,
235 ..Out::default()
236 };
237 let _ = writeln!(
238 o.text,
239 "\n/** The records the bus's messages share. */\npublic final class {TYPES} {{\n private {TYPES}() {{}}"
240 );
241 let mut done = 0;
242 while done < o.records.len() {
243 let s = o.records[done].clone();
244 record(&mut o, INDENT, s.name, &s, "");
245 done += 1;
246 }
247 unique(&o.records);
248 o.text.push_str(&words::classes(&o.records, INDENT));
249 finish(package, o).0
250}
251
252fn finish(package: &str, mut o: Out) -> (String, Vec<StructShape>) {
254 o.text.push_str("}\n");
255 let out = o.text;
256 let names = format!(
257 "{}.{}",
258 yog_ids::export::JAVA_PACKAGE,
259 yog_ids::export::JAVA_CLASS
260 );
261 let mut imports: Vec<String> = IMPORTS
262 .iter()
263 .filter(|(used, _)| out.contains(used))
264 .map(|(_, class)| (*class).to_owned())
265 .collect();
266 imports.push(names);
267 imports.sort();
268 let imports: String = imports
269 .iter()
270 .map(|class| format!("import {class};\n"))
271 .collect();
272 let text = format!(
273 "{}package {package};\n\n{imports}{out}",
274 HEADER.comment::<Java>()
275 );
276 (
277 text.lines()
278 .map(str::trim_end)
279 .collect::<Vec<_>>()
280 .join("\n")
281 + "\n",
282 o.records,
283 )
284}
285
286fn unique(records: &[StructShape]) {
289 for (i, a) in records.iter().enumerate() {
290 for b in &records[i + 1..] {
291 assert!(
292 a.name != b.name || same(a, b),
293 "two payload structs are named {}: rename one",
294 a.name
295 );
296 }
297 }
298}
299
300const PAYLOADS: &[fn() -> Shape] = &[
303 <crate::entity::Hanging as yog_data::Schema>::shape,
304 <crate::content::Content as yog_data::Schema>::shape,
306 <crate::mirror::Write as yog_data::Schema>::shape,
308 <crate::mirror::BlockScope as yog_data::Schema>::shape,
310 <crate::nanite::Mappings as yog_data::Schema>::shape,
312 <crate::nanite::Nanites as yog_data::Schema>::shape,
314];
315
316#[derive(Clone, Copy, PartialEq, Eq, Debug)]
319pub enum Side {
320 Api,
321 Host,
322}
323
324pub fn files(package: &str) -> [(Side, &'static str, String); 5] {
326 let (events, mut records) = events::events(package);
327 let (services, more) = services::services(package);
328 for r in more {
329 if !records.iter().any(|have| same(have, &r)) {
330 records.push(r);
331 }
332 }
333 for payload in PAYLOADS {
334 let Shape::Struct(r) = payload() else {
335 continue;
336 };
337 if !records.iter().any(|have| same(have, &r)) {
338 records.push(r);
339 }
340 }
341 unique(&records);
342 [
343 (Side::Api, events::CLASS, events),
344 (Side::Api, services::CLASS, services),
345 (Side::Api, TYPES, types_file(package, records)),
346 (Side::Api, mirrors::CLASS, mirrors::file(package)),
347 (Side::Host, hosted::CLASS, hosted::file(package)),
348 ]
349}
350
351#[cfg(test)]
352mod tests {
353 use yog_data::{Schema, Shape};
354
355 use super::unique;
356
357 mod a {
358 #[derive(yog_data::Schema)]
359 pub struct Declare {
360 pub name: String,
361 }
362 }
363
364 mod b {
365 #[derive(yog_data::Schema)]
366 pub struct Declare {
367 pub label: String,
368 }
369 }
370
371 fn shape<T: Schema>() -> yog_data::schema::StructShape {
372 let Shape::Struct(s) = T::shape() else {
373 unreachable!("a derived struct")
374 };
375 s
376 }
377
378 #[test]
379 #[should_panic(expected = "two payload structs are named Declare")]
380 fn two_structs_of_one_name_are_refused() {
381 unique(&[shape::<a::Declare>(), shape::<b::Declare>()]);
382 }
383
384 #[test]
385 fn one_struct_seen_twice_is_fine() {
386 unique(&[shape::<a::Declare>(), shape::<a::Declare>()]);
387 }
388}