Generate Rust code and FTL code for a bundle of messages
This commit is contained in:
parent
f8c19b2d18
commit
137a88ad8e
|
@ -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"
|
||||
|
|
|
@ -20,6 +20,7 @@ members = [
|
|||
"ifc",
|
||||
"kifu/core",
|
||||
"kifu/gtk",
|
||||
"kifu/l10n",
|
||||
"l10n",
|
||||
"memorycache",
|
||||
"messages-codegen",
|
||||
|
|
|
@ -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" ] }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue