freedesktop_categories_codegen/
lib.rs

1//! Parser and code generator for `freedesktop-categories`.
2//!
3//! Fetches the latest DocBook version of the [Desktop Menu Specification][dm] from
4//! Freedesktop.org, parses the XML, builds a static `phf` hash map of all the categories, and
5//! saves the generated Rust code to a file which can be included in your Rust project during a
6//! Cargo build.
7//!
8//! [dm]: https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html
9
10#![deny(missing_debug_implementations)]
11#![forbid(unsafe_code)]
12
13extern crate amxml;
14extern crate chrono;
15extern crate curl;
16extern crate phf_codegen;
17
18pub use error::Error;
19
20use std::env;
21use std::fmt::{Display, Formatter, Result as FmtResult};
22use std::fs::{self, File};
23use std::io::Write;
24use std::path::{Path, PathBuf};
25
26use amxml::dom::new_document;
27use curl::easy::Easy;
28
29use generate::CategoryMap;
30
31mod error;
32mod generate;
33
34const SPEC_URL: &str = "https://specifications.freedesktop.org/menu-spec/";
35
36/// Version of the specification to download.
37#[derive(Debug)]
38pub enum Version {
39    V090,
40    V091,
41    V092,
42    V100,
43    V110,
44    Latest,
45}
46
47impl Display for Version {
48    fn fmt(&self, fmt: &mut Formatter) -> FmtResult {
49        match *self {
50            Version::V090 => write!(fmt, "0.9"),
51            Version::V091 => write!(fmt, "0.91"),
52            Version::V092 => write!(fmt, "0.92"),
53            Version::V100 => write!(fmt, "1.0"),
54            Version::V110 => write!(fmt, "1.1"),
55            Version::Latest => write!(fmt, "latest"),
56        }
57    }
58}
59
60/// Specification parser and code generator.
61#[derive(Debug)]
62pub struct DesktopMenuSpec {
63    xml_cache_dir: Option<PathBuf>,
64    always_download: bool,
65    output_name: &'static str,
66    version: Version,
67}
68
69impl DesktopMenuSpec {
70    /// Creates a new code generator.
71    pub fn new() -> Self {
72        DesktopMenuSpec {
73            xml_cache_dir: None,
74            always_download: false,
75            output_name: "map.rs",
76            version: Version::Latest,
77        }
78    }
79
80    /// Overrides the path where the XML file should be downloaded and cached.
81    ///
82    /// This value is `$OUT_DIR` by default.
83    pub fn xml_cache_dir<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self {
84        self.xml_cache_dir = Some(path.into());
85        self
86    }
87
88    /// Always download the XML file again, even if it is already present in the cache.
89    ///
90    /// This value is `false` by default.
91    pub fn always_download(&mut self, value: bool) -> &mut Self {
92        self.always_download = value;
93        self
94    }
95
96    /// Sets the name of the Rust output file. This name should include a `.rs` extension suffix.
97    ///
98    /// This value is `map.rs` by default.
99    pub fn output_file_name(&mut self, name: &'static str) -> &mut Self {
100        self.output_name = name;
101        self
102    }
103
104    /// Specifies which version of the spec we wish to generate.
105    ///
106    /// This value is `Version::Latest` by default.
107    pub fn version(&mut self, ver: Version) -> &mut Self {
108        self.version = ver;
109        self
110    }
111
112    /// Generates a static hash map of application categories and saves it to a file.
113    ///
114    /// Returns `Ok(())` if successful, returns `Err(Error)` otherwise.
115    pub fn gen_static_map(&self) -> Result<(), Error> {
116        let cache_dir = self
117            .xml_cache_dir
118            .clone()
119            .unwrap_or(env::var("OUT_DIR")?.into());
120
121        // Remove the DocBook-specific symbols so the XML can be parsed normally.
122        let xml = fetch_or_download(&self.version, &cache_dir, self.always_download)?
123            .replace("&version", "version")
124            .replace("&dtd-version", "dtd-version");
125
126        let doc = new_document(&xml)?;
127        let root = doc.root_element();
128
129        let map = CategoryMap::generate(&root)?;
130        let out = Path::new(&env::var("OUT_DIR")?).join(self.output_name);
131        map.write_file(&out)?;
132
133        Ok(())
134    }
135}
136
137fn fetch_or_download(
138    ver: &Version,
139    out_dir: &Path,
140    always_download: bool,
141) -> Result<String, Error> {
142    let file_name = format!("menu-spec-{}.xml", ver);
143    let path = Path::new(&out_dir).join(&file_name);
144
145    if !path.exists() || always_download {
146        let mut file = File::create(&path)?;
147        let mut handle = Easy::new();
148        handle.url(&format!("{}/{}", SPEC_URL, file_name))?;
149
150        let mut transfer = handle.transfer();
151        transfer.write_function(|data| {
152            file.write(data)
153                .expect("Unable to write received data to file");
154            Ok(data.len())
155        })?;
156        transfer.perform()?;
157    }
158
159    fs::read_to_string(&path).map_err(|e| e.into())
160}