qt_build_utils/
qrc.rs

1// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
2// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5
6use std::{
7    io,
8    path::{Path, PathBuf},
9};
10
11/// An individial `<file>` line within a [QResource]
12#[derive(Debug, Clone)]
13pub struct QResourceFile {
14    alias: Option<String>,
15    // TODO: compression
16    // TODO: empty
17    path: PathBuf,
18}
19
20impl<T: AsRef<Path>> From<T> for QResourceFile {
21    fn from(value: T) -> Self {
22        Self::new(value)
23    }
24}
25
26impl QResourceFile {
27    /// Construct a [QResourceFile]
28    pub fn new(path: impl AsRef<Path>) -> Self {
29        Self {
30            alias: None,
31            path: path.as_ref().to_path_buf(),
32        }
33    }
34
35    /// Specify an alias for the [QResourceFile]
36    pub fn alias(mut self, alias: impl Into<String>) -> Self {
37        self.alias = Some(alias.into());
38        self
39    }
40
41    /// Get the path of this file
42    pub fn get_path(&self) -> &Path {
43        &self.path
44    }
45
46    fn write(self, writer: &mut impl io::Write) -> io::Result<()> {
47        let alias = self
48            .alias
49            .unwrap_or_else(|| self.path.to_string_lossy().to_string());
50        let alias = alias.escape_default();
51        #[cfg(test)]
52        let path = &self.path;
53        #[cfg(not(test))]
54        let path = std::fs::canonicalize(self.path)?;
55        writeln!(
56            writer,
57            "    <file alias=\"{alias}\">{path}</file>",
58            path = path.display()
59        )
60    }
61}
62
63/// A `<qresource>` block within a [QResources]
64#[derive(Debug, Clone)]
65pub struct QResource {
66    language: Option<String>,
67    prefix: Option<String>,
68    files: Vec<QResourceFile>,
69}
70
71impl Default for QResource {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl<T: Into<QResourceFile>> From<T> for QResource {
78    fn from(value: T) -> Self {
79        Self::new().file(value)
80    }
81}
82
83impl QResource {
84    /// Construct a [QResource]
85    pub fn new() -> Self {
86        Self {
87            language: None,
88            prefix: None,
89            files: vec![],
90        }
91    }
92
93    /// Add a [QResourceFile] to the [QResource]
94    pub fn file<T: Into<QResourceFile>>(mut self, file: T) -> Self {
95        self.files.push(file.into());
96        self
97    }
98
99    /// Add multiple [QResourceFile] to the [QResource]
100    pub fn files<T: Into<QResourceFile>>(mut self, files: impl IntoIterator<Item = T>) -> Self {
101        for file in files.into_iter() {
102            self.files.push(file.into());
103        }
104        self
105    }
106
107    /// Get the files inside of this resource
108    pub fn get_files(&self) -> impl Iterator<Item = &QResourceFile> {
109        self.files.iter()
110    }
111
112    /// Specify a language for the `<qresource>`
113    pub fn language(mut self, language: impl Into<String>) -> Self {
114        self.language = Some(language.into());
115        self
116    }
117
118    /// Specify a prefix for the `<qresource>`
119    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
120        self.prefix = Some(prefix.into());
121        self
122    }
123
124    /// Get the prefix, if set
125    pub fn get_prefix(&self) -> Option<&str> {
126        self.prefix.as_deref()
127    }
128
129    fn write(self, writer: &mut impl io::Write) -> io::Result<()> {
130        let language = self
131            .language
132            .map(|language| format!(" language=\"{}\"", language.escape_default()))
133            .unwrap_or_default();
134        let prefix = self
135            .prefix
136            .map(|prefix| format!(" prefix=\"{}\"", prefix.escape_default()))
137            .unwrap_or_default();
138
139        writeln!(writer, "  <qresource{language}{prefix}>")?;
140        for file in self.files.into_iter() {
141            file.write(writer)?;
142        }
143        writeln!(writer, "  </qresource>")
144    }
145}
146
147/// A helper for building Qt resource collection files
148#[derive(Debug, Clone)]
149pub struct QResources {
150    resources: Vec<QResource>,
151}
152
153impl Default for QResources {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159impl<T: IntoIterator<Item = impl Into<QResourceFile>>> From<T> for QResources {
160    fn from(value: T) -> Self {
161        Self::new().resource(QResource::new().files(value))
162    }
163}
164
165impl QResources {
166    /// Construct a [QResource]
167    pub fn new() -> Self {
168        Self { resources: vec![] }
169    }
170
171    /// Add a [QResource] to the [QResources]
172    pub fn resource<T: Into<QResource>>(mut self, resource: T) -> Self {
173        self.resources.push(resource.into());
174        self
175    }
176
177    /// Add multiple [QResource] to the [QResources]
178    pub fn resources<T: Into<QResource>>(mut self, resources: impl IntoIterator<Item = T>) -> Self {
179        for resource in resources.into_iter() {
180            self.resources.push(resource.into());
181        }
182        self
183    }
184
185    /// Get the resources as mutable references
186    pub fn get_resources_mut(&mut self) -> impl Iterator<Item = &mut QResource> {
187        self.resources.iter_mut()
188    }
189
190    /// Get the resources
191    pub fn get_resources(&self) -> impl Iterator<Item = &QResource> {
192        self.resources.iter()
193    }
194
195    /// Convert to a string representation
196    pub fn write(self, writer: &mut impl io::Write) -> io::Result<()> {
197        writeln!(writer, "<RCC>")?;
198        for resource in self.resources.into_iter() {
199            resource.write(writer)?;
200        }
201        writeln!(writer, "</RCC>")
202    }
203}
204
205#[cfg(test)]
206mod test {
207    use super::*;
208
209    #[test]
210    fn resource_file() {
211        let mut result = Vec::new();
212        QResourceFile::new("path")
213            .alias("alias")
214            .write(&mut result)
215            .unwrap();
216        assert_eq!(
217            String::from_utf8(result).unwrap(),
218            "    <file alias=\"alias\">path</file>\n"
219        );
220    }
221
222    #[test]
223    fn resource() {
224        let mut result = Vec::new();
225        QResource::new()
226            .language("language")
227            .prefix("prefix")
228            .write(&mut result)
229            .unwrap();
230        assert_eq!(
231            String::from_utf8(result).unwrap(),
232            "  <qresource language=\"language\" prefix=\"prefix\">\n  </qresource>\n"
233        );
234    }
235
236    #[test]
237    fn resources() {
238        let mut result = Vec::new();
239        QResources::new()
240            .resources(["a", "b"])
241            .resource(
242                QResource::new()
243                    .prefix("prefix")
244                    .files(["c", "d"])
245                    .file(QResourceFile::new("e").alias("alias")),
246            )
247            .write(&mut result)
248            .unwrap();
249        assert_eq!(
250            String::from_utf8(result).unwrap(),
251            "<RCC>
252  <qresource>
253    <file alias=\"a\">a</file>
254  </qresource>
255  <qresource>
256    <file alias=\"b\">b</file>
257  </qresource>
258  <qresource prefix=\"prefix\">
259    <file alias=\"c\">c</file>
260    <file alias=\"d\">d</file>
261    <file alias=\"alias\">e</file>
262  </qresource>
263</RCC>
264"
265        );
266    }
267
268    #[test]
269    fn resources_from_files() {
270        let mut result = Vec::new();
271        QResources::from(["a", "b"]).write(&mut result).unwrap();
272        assert_eq!(
273            String::from_utf8(result).unwrap(),
274            format!(
275                "<RCC>
276  <qresource>
277    <file alias=\"a\">a</file>
278    <file alias=\"b\">b</file>
279  </qresource>
280</RCC>
281",
282            )
283        );
284    }
285}