mod events;
mod frames;
mod mirrors;
mod services;
mod types;
mod views;
mod words;
use std::fmt::Write;
use yog_data::Shape;
use yog_data::schema::StructShape;
use types::{TYPES, from_data, java_type, nullable, same, to_data};
const 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`.";
const IMPORTS: &[(&str, &str)] = &[
("YogBus.", "dev.yog.YogBus"),
("YogData.", "dev.yog.data.YogData"),
("YogFrame.", "dev.yog.data.YogFrame"),
("YogMessage", "dev.yog.YogMessage"),
];
const INDENT: &str = " ";
fn camel(snake: &str) -> String {
let mut out = String::new();
let mut upper = false;
for c in snake.chars() {
if c == '_' {
upper = true;
} else if upper {
out.extend(c.to_uppercase());
upper = false;
} else {
out.push(c);
}
}
out
}
const JAVA_KEYWORDS: &[&str] = &[
"abstract",
"assert",
"boolean",
"break",
"byte",
"case",
"catch",
"char",
"class",
"const",
"continue",
"default",
"do",
"double",
"else",
"enum",
"extends",
"final",
"finally",
"float",
"for",
"goto",
"if",
"implements",
"import",
"instanceof",
"int",
"interface",
"long",
"native",
"new",
"package",
"private",
"protected",
"public",
"return",
"short",
"static",
"strictfp",
"super",
"switch",
"synchronized",
"this",
"throw",
"throws",
"transient",
"try",
"void",
"volatile",
"while",
"true",
"false",
"null",
"record",
"var",
"yield",
];
fn field(snake: &str) -> String {
let name = camel(snake);
if JAVA_KEYWORDS.contains(&name.as_str()) {
format!("{name}_")
} else {
name
}
}
pub(super) fn class_name(segment: &str) -> String {
let mut out = camel(segment);
if let Some(first) = out.get(..1) {
out.replace_range(..1, &first.to_uppercase());
}
out
}
#[derive(Default)]
struct Out {
text: String,
records: Vec<StructShape>,
members: String,
}
fn record(o: &mut Out, pad: &str, name: &str, s: &StructShape, body: &str) {
let Out {
text: out, records, ..
} = o;
let params: Vec<String> = s
.fields
.iter()
.map(|f| format!("{} {}", java_type(&(f.shape)(), records), field(f.name)))
.collect();
if !s.doc.is_empty() {
let _ = writeln!(out, "\n{pad}/** {} */", s.doc);
}
let _ = writeln!(out, "{pad}public record {name}({}) {{", params.join(", "));
let _ = writeln!(
out,
"{pad} public java.util.Map<String, Object> toData() {{\n{pad} java.util.Map<String, Object> m = new java.util.LinkedHashMap<>();"
);
for f in &s.fields {
let shape = (f.shape)();
let field = field(f.name);
let line = match nullable(&shape) {
Some(inner) => format!(
"if ({field} != null) m.put(\"{}\", {});",
f.name,
to_data(&inner, &field, 0)
),
None => format!("m.put(\"{}\", {});", f.name, to_data(&shape, &field, 0)),
};
let _ = writeln!(out, "{pad} {line}");
}
let _ = writeln!(out, "{pad} return m;\n{pad} }}\n");
let args: Vec<String> = s
.fields
.iter()
.map(|f| from_data(&(f.shape)(), &format!("m.get(\"{}\")", f.name), 0))
.collect();
let _ = writeln!(
out,
"{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",
args.join(", ")
);
let _ = write!(
out,
"{pad} public static {name} fromBytes(byte[] bytes) {{\n{pad} return fromData(YogData.decode(bytes));\n{pad} }}"
);
out.push_str(&frames::codec(name, s, pad));
out.push_str(&views::view(name, s, pad));
let body = body.replace('\n', &format!("\n{pad}"));
let _ = writeln!(out, "{body}\n{pad}}}");
}
fn name(address: &str) -> String {
yog_ids::export::java_ref(yog_ids::export::JAVA_CLASS, address)
.unwrap_or_else(|| format!("{address:?}"))
}
fn target(address: &str, key: Option<&str>, constant: &str) -> String {
match key {
Some(k) => format!("YogBus.keyed({}, {})", name(address), field(k)),
None => format!("Address.{constant}"),
}
}
struct Entry {
path: &'static str,
name: &'static str,
shape: Shape,
body: String,
}
fn close(out: &mut String, open: &mut Vec<String>, keep: usize) {
while open.len() > keep {
open.pop();
let _ = writeln!(out, "{}}}", INDENT.repeat(open.len() + 1));
}
}
fn file(
package: &str,
class: &str,
doc: &str,
mut o: Out,
entries: &[Entry],
) -> (String, Vec<StructShape>) {
let _ = writeln!(
o.text,
"\n/** {doc} */\npublic final class {class} {{\n private {class}() {{}}"
);
let members = std::mem::take(&mut o.members);
o.text.push_str(&members);
let mut open: Vec<String> = Vec::new();
let mut opened: Vec<String> = Vec::new();
for e in entries {
let path: Vec<&str> = e.path.split('/').collect();
let common = open.iter().zip(&path).take_while(|(a, b)| a == *b).count();
close(&mut o.text, &mut open, common);
for (depth, seg) in path.iter().enumerate().skip(common) {
let whole = path[..=depth].join("/");
assert!(
!opened.contains(&whole),
"the entries of `{whole}` aren't together"
);
opened.push(whole);
let pad = INDENT.repeat(open.len() + 1);
let class = class_name(seg);
let _ = writeln!(
o.text,
"\n{pad}/** `{seg}`. */\n{pad}public static final class {class} {{\n{pad} private {class}() {{}}"
);
open.push((*seg).to_owned());
}
let Shape::Struct(s) = &e.shape else { continue };
assert!(
open.iter().all(|seg| class_name(seg) != e.name),
"{} is named like the class around it ({}): rename it",
e.name,
e.path
);
record(&mut o, &INDENT.repeat(open.len() + 1), e.name, s, &e.body);
}
close(&mut o.text, &mut open, 0);
finish(package, o)
}
fn types_file(package: &str, records: Vec<StructShape>) -> String {
let mut o = Out {
records,
..Out::default()
};
let _ = writeln!(
o.text,
"\n/** The records the bus's messages share. */\npublic final class {TYPES} {{\n private {TYPES}() {{}}"
);
let mut done = 0;
while done < o.records.len() {
let s = o.records[done].clone();
record(&mut o, INDENT, s.name, &s, "");
done += 1;
}
unique(&o.records);
o.text.push_str(&words::classes(&o.records, INDENT));
finish(package, o).0
}
fn finish(package: &str, mut o: Out) -> (String, Vec<StructShape>) {
o.text.push_str("}\n");
let out = o.text;
let names = format!(
"{}.{}",
yog_ids::export::JAVA_PACKAGE,
yog_ids::export::JAVA_CLASS
);
let mut imports: Vec<String> = IMPORTS
.iter()
.filter(|(used, _)| out.contains(used))
.map(|(_, class)| (*class).to_owned())
.collect();
imports.push(names);
imports.sort();
let imports: String = imports
.iter()
.map(|class| format!("import {class};\n"))
.collect();
let text = format!("// {HEADER}\npackage {package};\n\n{imports}{out}");
(
text.lines()
.map(str::trim_end)
.collect::<Vec<_>>()
.join("\n")
+ "\n",
o.records,
)
}
fn unique(records: &[StructShape]) {
for (i, a) in records.iter().enumerate() {
for b in &records[i + 1..] {
assert!(
a.name != b.name || same(a, b),
"two payload structs are named {}: rename one",
a.name
);
}
}
}
const PAYLOADS: &[fn() -> Shape] = &[
<crate::entity::Hanging as yog_data::Schema>::shape,
<crate::content::Content as yog_data::Schema>::shape,
<crate::mirror::Write as yog_data::Schema>::shape,
<crate::mirror::BlockScope as yog_data::Schema>::shape,
<crate::nanite::Mappings as yog_data::Schema>::shape,
<crate::nanite::Nanites as yog_data::Schema>::shape,
];
pub fn files(package: &str) -> [(&'static str, String); 4] {
let (events, mut records) = events::events(package);
let (services, more) = services::services(package);
for r in more {
if !records.iter().any(|have| same(have, &r)) {
records.push(r);
}
}
for payload in PAYLOADS {
let Shape::Struct(r) = payload() else {
continue;
};
if !records.iter().any(|have| same(have, &r)) {
records.push(r);
}
}
unique(&records);
[
(events::CLASS, events),
(services::CLASS, services),
(TYPES, types_file(package, records)),
(mirrors::CLASS, mirrors::file(package)),
]
}
#[cfg(test)]
mod tests {
use yog_data::{Schema, Shape};
use super::unique;
mod a {
#[derive(yog_data::Schema)]
pub struct Declare {
pub name: String,
}
}
mod b {
#[derive(yog_data::Schema)]
pub struct Declare {
pub label: String,
}
}
fn shape<T: Schema>() -> yog_data::schema::StructShape {
let Shape::Struct(s) = T::shape() else {
unreachable!("a derived struct")
};
s
}
#[test]
#[should_panic(expected = "two payload structs are named Declare")]
fn two_structs_of_one_name_are_refused() {
unique(&[shape::<a::Declare>(), shape::<b::Declare>()]);
}
#[test]
fn one_struct_seen_twice_is_fine() {
unique(&[shape::<a::Declare>(), shape::<a::Declare>()]);
}
}