From 137a88ad8e40976d93768cc2defde5acec0d6965 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 8 Mar 2024 09:24:58 -0500 Subject: [PATCH] Generate Rust code and FTL code for a bundle of messages --- Cargo.lock | 42 +++++++ Cargo.toml | 1 + messages-codegen/Cargo.toml | 8 ++ messages-codegen/src/lib.rs | 70 +++++++++--- messages-codegen/src/main.rs | 135 +++++++++++++++++++++++ messages-codegen/test-data/messages.yaml | 14 +++ 6 files changed, 252 insertions(+), 18 deletions(-) create mode 100644 messages-codegen/src/main.rs create mode 100644 messages-codegen/test-data/messages.yaml diff --git a/Cargo.lock b/Cargo.lock index 5d300d1..0165d09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -658,6 +658,15 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.17.0" @@ -2701,6 +2710,13 @@ dependencies = [ "tokio", ] +[[package]] +name = "kifu-l10n" +version = "0.1.0" +dependencies = [ + "messages-codegen", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -2904,6 +2920,13 @@ dependencies = [ [[package]] name = "messages-codegen" version = "0.1.0" +dependencies = [ + "convert_case", + "quote", + "serde 1.0.193", + "serde_yaml", + "syn 2.0.48", +] [[package]] name = "mime" @@ -4160,6 +4183,19 @@ dependencies = [ "serde 1.0.193", ] +[[package]] +name = "serde_yaml" +version = "0.9.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15e0ef66bf939a7c890a0bf6d5a733c70202225f9888a89ed5c62298b019129" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde 1.0.193", + "unsafe-libyaml", +] + [[package]] name = "sgf" version = "0.1.0" @@ -5087,6 +5123,12 @@ dependencies = [ "traitobject", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" + [[package]] name = "url" version = "1.7.2" diff --git a/Cargo.toml b/Cargo.toml index d4565b7..a23b4e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "ifc", "kifu/core", "kifu/gtk", + "kifu/l10n", "l10n", "memorycache", "messages-codegen", diff --git a/messages-codegen/Cargo.toml b/messages-codegen/Cargo.toml index 426f5d7..c62ac76 100644 --- a/messages-codegen/Cargo.toml +++ b/messages-codegen/Cargo.toml @@ -5,4 +5,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +proc-macro = true + [dependencies] +convert_case = { version = "0.6" } +quote = { version = "*" } +syn = { version = "*" } +serde_yaml = { version = "*" } +serde = { version = "*", features = [ "derive" ] } diff --git a/messages-codegen/src/lib.rs b/messages-codegen/src/lib.rs index 6e5422d..cb17e37 100644 --- a/messages-codegen/src/lib.rs +++ b/messages-codegen/src/lib.rs @@ -9,6 +9,47 @@ * For overall file structure, I want the messages, L10N, and FluentErgo tied together. */ +use proc_macro::TokenStream; +use quote::quote; +use syn; + +/* I want to write a macro that reads something like this: + * messages! { + * Welcome(name: String) => "Hello, ${name}", + * GamesInDatabase(count: usize) => "{$count -> + * [one] There is one game in the database + * *[other] There are ${count} games in the database + * }", + * + * It generates an enumeration with all of the named values (Welcome, GamesInDatabase), it + * generates corresponding structures (Welcome{ name: String }, GamesInDatabase{ count: usize }), + * and it generates two implementations. One is an implementation that can be used to write all of + * the strings after => into a file. The other is an implementation that can be imported into a + * program, but which *does not* contain the strings. That would look more like this: + * + * messages.rs: + * + * enum Messages { + * Welcome(Welcome), + * GamesInDatabase(GamesInDatabase), + * } + * + * struct Welcome{ name: String } + * struct GamesInDatabase{ count: usize } + * + * message_strings.ftl: + * + * welcome = "Hello, ${name}", + * games-in-database = "{$count -> + * [one] There is one game in the database + * *[other] There are ${count} games in the database + * }", + * + * messages.rs can be imported into the resulting program, and the names of the strings are thus a + * part of the program, but the strings themselves are still data files. + */ + +/* #[macro_export] macro_rules! messages { ($($name:ident($($arg:ident: $argtype:ty)*) => $message:literal,)+) => { @@ -29,25 +70,18 @@ macro_rules! messages { })+ } } +*/ -#[cfg(test)] -mod test { - use super::*; +#[proc_macro_derive(Messages)] +pub fn messages_macro(input: TokenStream) -> TokenStream { + let syn::DeriveInput { ident, .. } = syn::parse_macro_input! {input}; - messages! { - Welcome(name: String) => "Hello, ${name}", - GamesInDatabase(count: usize) => "{$count -> - [one] There is one game in the database - *[other] There are ${count} games in the database -}", - } - - #[test] - fn it_can_generate_an_ftl() { - for s in Messages::gen_strings() { - println!("{}", s); + let gen = quote! { + impl Messages for #ident { + fn hello() { + println!("Hello from {}", stringify!(#ident)); + } } - - assert!(false); - } + }; + gen.into() } diff --git a/messages-codegen/src/main.rs b/messages-codegen/src/main.rs new file mode 100644 index 0000000..f41b3af --- /dev/null +++ b/messages-codegen/src/main.rs @@ -0,0 +1,135 @@ +use convert_case::{Case, Casing}; +use serde::Deserialize; +use serde_yaml; +use std::{collections::HashMap, fmt, fs::File}; + +#[derive(Debug, Clone, Deserialize)] +struct MessageJS { + #[serde(default)] + parameters: HashMap, + content: String, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Ty { + String, + Number, + Count, +} + +impl fmt::Display for Ty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + Ty::String => write!(f, "String")?, + Ty::Number => write!(f, "i32")?, + Ty::Count => write!(f, "usize")?, + } + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Parameter { + name: String, + ty: Ty, +} + +impl From<&str> for Parameter { + fn from(val: &str) -> Self { + Self { + name: "Nothing".to_owned(), + ty: Ty::String, + } + } +} + +impl From<(String, String)> for Parameter { + fn from((name, val): (String, String)) -> Self { + let ty = match val.as_ref() { + "string" => Ty::String, + "number" => Ty::Number, + "count" => Ty::Count, + _ => panic!("invalid value"), + }; + Self { name, ty } + } +} + +#[derive(Debug, Clone)] +struct Message { + name: String, + parameters: Vec, + content: String, +} + +impl Message { + fn from_js(name: String, source: MessageJS) -> Self { + Self { + name, + parameters: source + .parameters + .into_iter() + .map(|param| Parameter::from(param)) + .collect(), + content: source.content, + } + } + + fn rust_code(&self) -> String { + if self.parameters.is_empty() { + format!(r"pub struct {}; ", self.name.to_case(Case::Pascal)) + } else { + let mut struct_strs = vec![]; + + struct_strs.push(format!("pub struct {} {{", self.name.to_case(Case::Pascal))); + let mut parameters: Vec = self + .parameters + .iter() + .map(|param| format!(" {}: {},", param.name, param.ty)) + .collect(); + + struct_strs.append(&mut parameters); + struct_strs.push("}".to_owned()); + struct_strs.push("".to_owned()); + + struct_strs.push(format!( + "impl Message for {} {{", + self.name.to_case(Case::Pascal) + )); + struct_strs.push(format!(" pub fn localize(&self, bundle: &FluentBundle) -> String {{")); + struct_strs.push(" let mut args = FluentArgs::new();".to_owned()); + let mut parameters: Vec = self + .parameters + .iter() + .map(|param| format!(" args.set(\"{}\", self.{})", param.name, param.name)) + .collect(); + struct_strs.append(&mut parameters); + struct_strs.push("".to_owned()); + struct_strs.push(format!(" let msg = bundle.get_message(\"{}\").unwrap();", self.name)); + struct_strs.push(format!(" let mut errors = vec![]")); + struct_strs.push(format!(" msg.format_pattern(&msg.value().unwrap(), args, &mut errors)")); + struct_strs.push(" }".to_owned()); + struct_strs.push("}".to_owned()); + + struct_strs.join("\n") + } + } + + fn content(&self) -> String { + format!("{} = {}", self.name, self.content.clone()) + } +} + +fn main() { + let messages: HashMap = + serde_yaml::from_reader(File::open("test-data/messages.yaml").unwrap()).unwrap(); + + let messages = messages + .into_iter() + .map(|(name, msg)| Message::from_js(name, msg)); + for message in messages { + println!(); + println!("{}", message.rust_code()); + // println!("{}", message.content()); + } +} diff --git a/messages-codegen/test-data/messages.yaml b/messages-codegen/test-data/messages.yaml new file mode 100644 index 0000000..3b67f31 --- /dev/null +++ b/messages-codegen/test-data/messages.yaml @@ -0,0 +1,14 @@ +welcome: + content: "Welcome to Kifu" +hello: + parameters: + name: string + content: "Hello, ${name}" +games-in-database: + parameters: + count: count + content: | + {$count -> + [one] There is one game in the database + *[other] There are ${count} games in the database + }