Generate Rust code and FTL code for a bundle of messages

This commit is contained in:
Savanni D'Gerinel 2024-03-08 09:24:58 -05:00
parent f8c19b2d18
commit 137a88ad8e
6 changed files with 252 additions and 18 deletions

42
Cargo.lock generated
View File

@ -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"

View File

@ -20,6 +20,7 @@ members = [
"ifc",
"kifu/core",
"kifu/gtk",
"kifu/l10n",
"l10n",
"memorycache",
"messages-codegen",

View File

@ -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" ] }

View File

@ -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);
}
assert!(false);
let gen = quote! {
impl Messages for #ident {
fn hello() {
println!("Hello from {}", stringify!(#ident));
}
}
};
gen.into()
}

View File

@ -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<String, String>,
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<Parameter>,
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<String> = 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<String> = 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<String, MessageJS> =
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());
}
}

View File

@ -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
}