glossa_codegen/generator/
output_bincode.rs

1use std::{
2  fs::File,
3  io::{self, BufWriter},
4  path::Path,
5};
6
7use glossa_shared::{MiniStr, fmt_compact, tap::Pipe};
8use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
9
10use crate::{
11  AnyResult,
12  generator::{Generator, MapType},
13  resources::L10nResMap,
14};
15
16impl<'h> Generator<'h> {
17  /// Retrieves the localization resource map containing all translation data
18  pub(crate) fn get_l10n_res_map(&self) -> &L10nResMap {
19    self
20      .get_resources()
21      .get_or_init_data()
22  }
23
24  /// Serializes data to binary format (bincode) with parallel file operations
25  ///
26  /// # Errors
27  ///
28  /// Returns [`AnyResult`] with error details for:
29  /// - File creation failures
30  /// - Serialization errors
31  fn encode_bincode<T, D>(&self, lang_id: D, data: T) -> AnyResult<()>
32  where
33    T: serde::Serialize,
34    D: core::fmt::Display,
35  {
36    bincode::serde::encode_into_std_write(
37      data,
38      &mut self.create_bincode_file(lang_id)?,
39      bincode::config::standard(),
40    )?;
41    Ok(())
42  }
43
44  /// Generates consolidated binary file containing all localization data
45  ///
46  /// > `>` "outdir()/all{bincode_suffix}"
47  ///
48  /// # Behavior
49  /// - For DSL maps: Creates "all{bincode_suffix}" containing DSL data
50  /// - For regular maps: Creates "all{bincode_suffix}" containing all language
51  ///   data
52  ///
53  /// # Errors
54  /// Returns [`AnyResult`] with error details for:
55  /// - File I/O failures
56  /// - Serialization errors
57  pub fn output_bincode_all_in_one(&'h self, map_type: MapType) -> AnyResult<()> {
58    let all = "all";
59
60    if map_type.is_dsl() {
61      return match self.get_or_init_dsl_maps() {
62        x if x.is_empty() => Ok(()),
63        data => self.encode_bincode(all, data),
64      };
65    }
66
67    let data = map_type.get_non_dsl_maps(self)?;
68    self.encode_bincode(all, data)
69  }
70
71  /// Generates individual binary files per language
72  ///
73  /// > `>` "outdir()/{language}{bincode_suffix}"
74  ///
75  /// # Behavior
76  /// - For DSL maps: Creates separate files for each glossa-DSL content
77  /// - For regular maps: Creates separate files for each language
78  /// - Skips empty datasets
79  ///
80  ///
81  /// ## Example
82  ///
83  /// "../../locales/en/unread.dsl.toml":
84  ///
85  /// ```toml
86  /// num-to-en = """
87  /// $num ->
88  ///   [0] zero
89  ///   [1] one
90  ///   [2] two
91  ///   [3] three
92  ///   *[other] {$num}
93  /// """
94  ///
95  /// unread = "unread message"
96  ///
97  /// unread-count = """
98  /// $num ->
99  ///   [0] No {unread}s.
100  ///   [1] You have { num-to-en } {unread}.
101  ///   *[other] You have { num-to-en } {unread}s.
102  /// """
103  ///
104  /// show-unread-messages-count = "{unread-count}"
105  /// ```
106  ///
107  /// rs_code:
108  ///
109  /// ```no_run
110  /// use glossa_codegen::{L10nResources, Generator, generator::MapType};
111  /// use std::path::Path;
112  ///
113  /// let resources = L10nResources::new("../../locales/");
114  ///
115  /// // Output to tmp/{language}.tmpl.bincode
116  /// Generator::default()
117  ///   .with_resources(resources)
118  ///   .with_outdir("tmp")
119  ///   .with_bincode_suffix(".tmpl.bincode".into())
120  ///   .output_bincode(MapType::DSL)?;
121  ///
122  /// let file = Path::new("tmp").join("en.tmpl.bincode");
123  /// let tmpl_maps = glossa_shared::decode::file::decode_single_file_to_dsl_map(file)?;
124  /// let unread_tmpl = tmpl_maps
125  ///   .get("unread")
126  ///   .expect("Failed to get DSL-AST (map_name: unread)");
127  ///
128  /// let get_text = |num_str| {
129  ///   unread_tmpl.get_with_context("show-unread-messages-count", &[("num", num_str)])
130  /// };
131  ///
132  /// let one = get_text("1")?;
133  /// assert_eq!(one, "You have one unread message.");
134  ///
135  /// let zero = get_text("0")?;
136  /// assert_eq!(zero, "No unread messages.");
137  ///
138  /// # Ok::<(), anyhow::Error>(())
139  /// ```
140  pub fn output_bincode(&'h self, map_type: MapType) -> AnyResult<()> {
141    if map_type.is_dsl() {
142      return match self.get_or_init_dsl_maps() {
143        x if x.is_empty() => Ok(()),
144        iter => iter
145          .par_iter()
146          .try_for_each(|(lang, data)| self.encode_bincode(lang, data)),
147      };
148    }
149
150    map_type
151      .get_non_dsl_maps(self)?
152      .par_iter()
153      .try_for_each(|(lang, data)| self.encode_bincode(lang, data))
154  }
155
156  /// Creates buffered bincode file writer with standardized naming
157  ///
158  /// # File Naming
159  /// Follows format: `{language}{suffix}`
160  /// - `suffix` configured via [`Generator::get_bincode_suffix`]
161  ///
162  /// # Errors
163  /// Returns [`io::Result`] for file creation failures
164  pub(crate) fn create_bincode_file<D: core::fmt::Display>(
165    &self,
166    language: D,
167  ) -> io::Result<BufWriter<File>> {
168    let suffix = self.get_bincode_suffix();
169    let bincode_name = fmt_compact!("{language}{suffix}");
170    let out_dir = self.get_outdir().as_deref();
171
172    create_buf_writer(out_dir, bincode_name)
173  }
174}
175
176/// Creates buffered file writer with error handling
177///
178/// # Errors
179/// Returns [`io::Result`] with error details for:
180///
181/// - Missing output directory
182/// - File creation failures
183pub(crate) fn create_buf_writer(
184  out_dir: Option<&Path>,
185  bincode_name: MiniStr,
186) -> io::Result<BufWriter<File>> {
187  out_dir
188    .ok_or_else(|| io::Error::other("Invalid outdir"))?
189    .join(bincode_name)
190    .pipe(File::create)?
191    .pipe(BufWriter::new)
192    .pipe(Ok)
193}
194
195#[cfg(test)]
196mod tests {
197  use testutils::simple_benchmark;
198
199  use super::*;
200  use crate::generator::dbg_generator::{en_gb_generator, new_generator};
201
202  #[ignore]
203  #[test]
204  fn test_output_tmpl_maps_to_bincode_files() -> AnyResult<()> {
205    new_generator()
206      .with_bincode_suffix(".tmpl.bincode".into())
207      .output_bincode(MapType::DSL)
208  }
209
210  #[ignore]
211  #[test]
212  fn doc_test_encode_and_decode_tmpl_bincode() -> AnyResult<()> {
213    let resources = crate::L10nResources::new("../../locales/");
214
215    // Output to tmp/{language}_dsl.bincode
216    Generator::default()
217      .with_resources(resources)
218      .with_outdir("tmp")
219      .with_bincode_suffix("_dsl.bincode".into())
220      .output_bincode(MapType::DSL)?;
221
222    let file = Path::new("tmp").join("en_dsl.bincode");
223    let dsl_maps = glossa_shared::decode::file::decode_single_file_to_dsl_map(file)?;
224
225    let unread_resolver = dsl_maps
226      .get("unread")
227      .expect("Failed to get DSL-AST (map_name: unread)");
228
229    let get_text = |num_str| {
230      unread_resolver
231        .get_with_context("show-unread-messages-count", &[("num", num_str)])
232    };
233
234    let one = get_text("1")?;
235    assert_eq!(one, "You have one unread message.");
236
237    let zero = get_text("0")?;
238    assert_eq!(zero, "No unread messages.");
239
240    Ok(())
241  }
242
243  #[ignore]
244  #[test]
245  fn test_encode_regular_aio_bincode() -> AnyResult<()> {
246    new_generator()
247      .with_bincode_suffix("_regular.bincode".into())
248      .output_bincode_all_in_one(MapType::Regular)
249  }
250
251  #[ignore]
252  #[test]
253  fn test_encode_regular_en_gb_bincode() -> AnyResult<()> {
254    en_gb_generator()
255      .with_bincode_suffix(".regular.bincode".into())
256      .output_bincode(MapType::Regular)
257  }
258
259  #[ignore]
260  #[test]
261  #[cfg(feature = "highlight")]
262  fn test_encode_highlight_aio_bincode() -> AnyResult<()> {
263    use crate::generator::dbg_generator;
264
265    dbg_generator::highlight_generator()
266      .with_bincode_suffix(".highlight.bincode".into())
267      .output_bincode_all_in_one(MapType::Highlight)
268  }
269
270  #[ignore]
271  #[test]
272  #[cfg(feature = "highlight")]
273  fn test_decode_highlight_aio() -> glossa_shared::decode::ResolverResult<()> {
274    let data =
275      glossa_shared::decode::file::decode_file_to_maps("tmp/all.highlight.bincode")?;
276
277    let en_maps = data.get("en").unwrap();
278    let value = en_maps
279      .get(&("md_md".into(), "pwsh".into()))
280      .unwrap();
281    println!("{value}");
282    Ok(())
283  }
284
285  #[ignore]
286  #[test]
287  fn test_encode_tmpl_aio_bincode() -> AnyResult<()> {
288    new_generator()
289      .with_bincode_suffix("_tmpl.bincode".into())
290      .output_bincode_all_in_one(MapType::DSL)
291  }
292
293  #[ignore]
294  #[test]
295  fn test_decode_tmpl_aio_bincode() -> AnyResult<()> {
296    let raw_map =
297      glossa_shared::decode::file::decode_file_to_dsl_maps("tmp/all_tmpl.bincode")?;
298
299    let zh_maps = raw_map.get("zh").unwrap();
300    let zh_unread_map = zh_maps.get("unread").unwrap();
301
302    let text = zh_unread_map
303      .get_with_context("show-unread-messages-count", &[("num", "2")])?;
304    dbg!(text);
305
306    Ok(())
307  }
308
309  /// Debug:
310  /// - decode from file
311  ///   - Time taken: 422.708µs
312  /// - decode from slice
313  ///   - Time taken: 335.333µs
314  ///
315  /// Release:
316  /// - decode from file
317  ///   - Time taken: 190.209µs
318  /// - decode from slice
319  ///   - Time taken: 63.25µs
320  #[ignore]
321  #[test]
322  fn bench_decode_regular_file() -> AnyResult<()> {
323    let file = Path::new("tmp").join("all_regular.bincode");
324
325    eprintln!("decode from file");
326    simple_benchmark(|| {
327      let _ = glossa_shared::decode::file::decode_file_to_maps(&file);
328    });
329
330    let bytes = std::fs::read(&file)?;
331    let decode_slice = || glossa_shared::decode::slice::decode_to_maps(&bytes);
332
333    eprintln!("decode from slice");
334    simple_benchmark(|| {
335      let _ = decode_slice();
336    });
337
338    Ok(())
339  }
340}