Skip to main content

cmsis_pdsc_parser/
components.rs

1//! Contains the types required to represent a [PDSC Components](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_components_pg.html#element_components) element
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, PartialEq, Eq, Clone, Default, Deserialize, Serialize)]
6/// Represents the [PDSC components](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_components_pg.html#element_components) element
7///
8/// Groups all software bundles and standalone components published by a pack.
9pub struct Components {
10    /// Generator ID applied to all enclosed components when set
11    pub generator: Option<String>,
12
13    /// Component bundle definitions (0..*)
14    #[serde(rename = "bundle", default)]
15    pub bundles: Vec<Bundle>,
16
17    /// Standalone component definitions (0..*)
18    #[serde(rename = "component", default)]
19    pub components: Vec<Component>,
20}
21
22#[derive(Debug, PartialEq, Eq, Clone, Default, Deserialize, Serialize)]
23/// Represents a [bundle](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_components_pg.html#element_bundle) element
24///
25/// Groups a set of interdependent components under a shared class, version, and name.
26/// The `Cclass` and `Cversion` attributes are inherited by all enclosed components.
27pub struct Bundle {
28    /// Bundle name; becomes part of each enclosed component's ID
29    #[serde(rename = "Cbundle")]
30    pub bundle: String,
31
32    /// Component vendor; derives from package vendor if omitted
33    #[serde(rename = "Cvendor")]
34    pub vendor: Option<String>,
35
36    /// Component class shared by all enclosed components
37    #[serde(rename = "Cclass")]
38    pub class: String,
39
40    /// Version shared by all enclosed components unless individually overridden
41    #[serde(rename = "Cversion")]
42    pub version: String,
43
44    /// References a `licenseSet` identifier governing usage rights
45    #[serde(rename = "licenseSet")]
46    pub license_set: Option<String>,
47
48    /// References a changelog ID with the bundle change history
49    pub changelog: Option<String>,
50
51    /// Links to a `<generator>` entry in the same pack
52    pub generator: Option<String>,
53
54    /// Marks this variant as the preferred choice for automated updates
55    #[serde(rename = "isDefaultVariant")]
56    pub is_default_variant: Option<bool>,
57
58    /// Brief description of the bundle (max 256 characters)
59    pub description: String,
60
61    /// Path to the bundle documentation file relative to the pack root
62    pub doc: String,
63
64    /// Components enclosed in this bundle (1..*)
65    #[serde(rename = "component", default)]
66    pub components: Vec<Component>,
67}
68
69#[derive(Debug, PartialEq, Eq, Clone, Default, Deserialize, Serialize)]
70/// Represents a [component](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_components_pg.html#element_component) element
71///
72/// Defines a single software component. Used for both top-level components and components
73/// nested inside a bundle. When inside a bundle, `class` and `version` are absent (inherited).
74pub struct Component {
75    /// Component vendor; derives from package vendor if omitted
76    #[serde(rename = "Cvendor")]
77    pub vendor: Option<String>,
78
79    /// Component class; absent when nested in a bundle (inherited)
80    #[serde(rename = "Cclass")]
81    pub class: Option<String>,
82
83    /// Component group
84    #[serde(rename = "Cgroup")]
85    pub group: String,
86
87    /// Component sub-group (3–32 characters)
88    #[serde(rename = "Csub")]
89    pub sub: Option<String>,
90
91    /// Variant name (e.g. `release`, `debug`); mutually exclusive with other variants
92    #[serde(rename = "Cvariant")]
93    pub variant: Option<String>,
94
95    /// Component version; absent when nested in a bundle unless overriding
96    #[serde(rename = "Cversion")]
97    pub version: Option<String>,
98
99    /// API version consumed by this component
100    #[serde(rename = "Capiversion")]
101    pub api_version: Option<String>,
102
103    /// References a condition ID; component is included only if the condition is met
104    pub condition: Option<String>,
105
106    /// If `true`, suppresses the automatic resolver; component requires manual selection
107    pub custom: Option<bool>,
108
109    /// Number of simultaneous instances allowed (1–10); default is 1
110    #[serde(rename = "maxInstances")]
111    pub max_instances: Option<u32>,
112
113    /// Marks this variant as the preferred choice for automated updates
114    #[serde(rename = "isDefaultVariant")]
115    pub is_default_variant: Option<bool>,
116
117    /// Links to a `<generator>` entry in the same pack
118    pub generator: Option<String>,
119
120    /// References a `licenseSet` identifier governing usage rights
121    #[serde(rename = "licenseSet")]
122    pub license_set: Option<String>,
123
124    /// User-facing visibility (`always`, `never`, `maskable`); default is `always`
125    pub view: Option<String>,
126
127    /// References a changelog ID with the component change history
128    pub changelog: Option<String>,
129
130    /// Marks the component as deprecated; deprecated components should not be used in new designs
131    pub deprecated: Option<bool>,
132
133    /// Brief description of the component (max 256 characters)
134    pub description: String,
135
136    /// C preprocessor definitions injected verbatim into `RTE_Components.h`
137    #[serde(rename = "RTE_Components_h")]
138    pub rte_components_h: Option<String>,
139
140    /// Content pre-included globally for all project modules via `Pre_Include_Global.h`
141    #[serde(rename = "Pre_Include_Global_h")]
142    pub pre_include_global_h: Option<String>,
143
144    /// Content pre-included for this component's modules only via `Pre_Include_<Cclass>_<component>.h`
145    #[serde(rename = "Pre_Include_Local_Component_h")]
146    pub pre_include_local_component_h: Option<String>,
147
148    /// Source and header files that implement this component
149    #[serde(default)]
150    pub files: ComponentFiles,
151
152    /// Key/value metadata extensions for toolchain or IDE integration
153    #[serde(default)]
154    pub extensions: ComponentExtensions,
155
156    /// IDE-specific tool integration environments (0..1)
157    pub environments: Option<ComponentEnvironments>,
158}
159
160#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]
161/// Represents the [files](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_components_pg.html#element_files) grouping element inside a component
162pub struct ComponentFiles {
163    /// Individual file entries (1..*)
164    #[serde(rename = "file", default)]
165    pub files: Vec<ComponentFile>,
166}
167
168#[derive(Debug, PartialEq, Eq, Clone, Default, Deserialize, Serialize)]
169/// Represents a [file](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_components_pg.html#element_file) entry within a component
170///
171/// Attributes follow the PDSC `FileType` definition shared across components and APIs.
172pub struct ComponentFile {
173    /// File path relative to the pack root; may be a URL for `category="doc"`
174    pub name: String,
175
176    /// File category (e.g. `header`, `sourceC`, `doc`, `library`)
177    pub category: String,
178
179    /// Special handling: `config` (copied to project, user-editable) or `template`
180    pub attr: Option<String>,
181
182    /// References a condition ID; file included only when condition evaluates true
183    pub condition: Option<String>,
184
185    /// File-specific version; component version used if omitted
186    pub version: Option<String>,
187
188    /// Description/purpose required when `attr="template"`; groups template options
189    pub select: Option<String>,
190
191    /// Source path relative to PDSC; semicolon-separated list for libraries
192    pub src: Option<String>,
193
194    /// For `category="header"`: an incomplete include path for project-relative includes
195    pub path: Option<String>,
196
197    /// Target compiler/assembler (`c`, `cpp`, `c-cpp`, `asm`, `link`); inferred from extension if absent
198    pub language: Option<String>,
199
200    /// Header visibility (`public` or `private`); default is `public`
201    pub scope: Option<String>,
202
203    /// Publishing permission; default `true`
204    pub public: Option<bool>,
205
206    /// IDE project explorer location override
207    pub projectpath: Option<String>,
208
209    /// Deprecated, use `attr="config"` instead
210    pub copy: Option<String>,
211}
212
213#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]
214/// Represents the [extensions](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_components_pg.html#element_extensions) grouping element inside a component
215pub struct ComponentExtensions {
216    /// Key/value extension entries (1..*)
217    #[serde(rename = "extension", default)]
218    pub extensions: Vec<ComponentExtension>,
219}
220
221#[derive(Debug, PartialEq, Eq, Clone, Default, Deserialize, Serialize)]
222/// Represents an [extension](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_components_pg.html#element_extension) entry within the extensions group
223///
224/// Provides arbitrary key/value metadata for toolchain or IDE integration.
225pub struct ComponentExtension {
226    /// Extension identifier; unique within the component
227    pub key: String,
228
229    /// Value associated with the key
230    pub value: Option<String>,
231}
232
233#[derive(Debug, PartialEq, Eq, Clone, Default, Deserialize, Serialize)]
234/// Represents the [environments](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_components_pg.html#element_component_environments) grouping element inside a component
235pub struct ComponentEnvironments {
236    /// Tool environment entries (1..*)
237    #[serde(rename = "environment", default)]
238    pub environments: Vec<ComponentEnvironment>,
239}
240
241#[derive(Debug, PartialEq, Eq, Clone, Default, Deserialize, Serialize)]
242/// Represents a [component environment](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_components_pg.html#element_component_environment) entry
243///
244/// Identifies a specific development tool (e.g. `uv`, `iar`). Tool-specific child
245/// elements (often namespace-qualified) are silently ignored by the parser.
246pub struct ComponentEnvironment {
247    /// Development tool identifier
248    pub name: String,
249
250    /// Processor selector for multi-core devices
251    #[serde(rename = "Pname")]
252    pub processor_name: Option<String>,
253}
254
255#[cfg(test)]
256mod tests {
257    use crate::components::{ComponentEnvironment, ComponentExtension, ComponentFile, Components};
258
259    #[test]
260    fn parse_components() {
261        let xml_str = r#"<?xml version="1.0" encoding="UTF-8"?>
262<components generator="MyGen">
263    <component Cclass="Device" Cgroup="Startup" Cversion="1.0.0"
264               Cvendor="ARM" condition="CM4" licenseSet="all" changelog="CHANGES.txt"
265               custom="false" maxInstances="2" isDefaultVariant="true"
266               generator="StartupGen" view="always">
267        <description>Device startup files</description>
268        <files>
269            <file category="sourceC" name="Device/Source/startup.c"/>
270            <file category="header" name="Device/Include/system.h" public="true"/>
271        </files>
272        <extensions>
273            <extension key="schemaVersion" value="1.0"/>
274        </extensions>
275    </component>
276    <bundle Cbundle="MyRTOS" Cclass="RTOS" Cversion="5.6.0"
277            Cvendor="ARM" licenseSet="rtosLicense" changelog="RTOS_CHANGES.txt">
278        <description>ARM MyRTOS bundle</description>
279        <doc>documentation/MyRTOS.html</doc>
280        <component Cgroup="Kernel" Csub="Source">
281            <description>RTOS kernel source</description>
282            <files>
283                <file category="sourceC" name="RTOS/Source/kernel.c"/>
284            </files>
285            <extensions/>
286        </component>
287    </bundle>
288</components>"#;
289
290        let cs: Components = serde_roxmltree::from_str(xml_str).unwrap();
291        assert_eq!(cs.generator, Some("MyGen".to_string()));
292        assert_eq!(cs.components.len(), 1);
293        assert_eq!(cs.bundles.len(), 1);
294
295        let c = &cs.components[0];
296        assert_eq!(c.vendor, Some("ARM".to_string()));
297        assert_eq!(c.class, Some("Device".to_string()));
298        assert_eq!(c.group, "Startup");
299        assert_eq!(c.version, Some("1.0.0".to_string()));
300        assert_eq!(c.condition, Some("CM4".to_string()));
301        assert_eq!(c.license_set, Some("all".to_string()));
302        assert_eq!(c.changelog, Some("CHANGES.txt".to_string()));
303        assert_eq!(c.custom, Some(false));
304        assert_eq!(c.max_instances, Some(2));
305        assert_eq!(c.is_default_variant, Some(true));
306        assert_eq!(c.generator, Some("StartupGen".to_string()));
307        assert_eq!(c.view, Some("always".to_string()));
308        assert_eq!(c.description, "Device startup files");
309        assert_eq!(c.deprecated, None);
310        assert_eq!(c.sub, None);
311        assert_eq!(c.variant, None);
312        assert_eq!(c.api_version, None);
313        assert_eq!(c.rte_components_h, None);
314        assert_eq!(c.files.files.len(), 2);
315        assert_eq!(c.extensions.extensions.len(), 1);
316        assert_eq!(
317            c.extensions.extensions[0],
318            ComponentExtension {
319                key: "schemaVersion".to_string(),
320                value: Some("1.0".to_string()),
321            }
322        );
323        assert_eq!(c.environments, None);
324
325        let b = &cs.bundles[0];
326        assert_eq!(b.bundle, "MyRTOS");
327        assert_eq!(b.class, "RTOS");
328        assert_eq!(b.version, "5.6.0");
329        assert_eq!(b.vendor, Some("ARM".to_string()));
330        assert_eq!(b.license_set, Some("rtosLicense".to_string()));
331        assert_eq!(b.changelog, Some("RTOS_CHANGES.txt".to_string()));
332        assert_eq!(b.description, "ARM MyRTOS bundle");
333        assert_eq!(b.doc, "documentation/MyRTOS.html");
334        assert_eq!(b.components.len(), 1);
335
336        let bc = &b.components[0];
337        assert_eq!(bc.class, None);
338        assert_eq!(bc.version, None);
339        assert_eq!(bc.group, "Kernel");
340        assert_eq!(bc.sub, Some("Source".to_string()));
341        assert_eq!(bc.description, "RTOS kernel source");
342    }
343
344    #[test]
345    fn parse_component_bundle() {
346        let xml_str = r#"<?xml version="1.0" encoding="UTF-8"?>
347<components>
348    <bundle Cbundle="CMSIS" Cclass="CMSIS" Cversion="5.9.0">
349        <description>CMSIS software framework</description>
350        <doc>CMSIS/Documentation/html/index.html</doc>
351        <component Cgroup="CORE">
352            <description>CMSIS-CORE support for Cortex-M</description>
353            <files>
354                <file category="header" name="CMSIS/Core/Include/cmsis_compiler.h"/>
355                <file category="header" name="CMSIS/Core/Include/core_cm4.h" condition="CM4"/>
356            </files>
357            <extensions/>
358        </component>
359        <component Cgroup="DSP" Cvariant="Source" Cversion="1.10.1"
360                   isDefaultVariant="true">
361            <description>CMSIS-DSP library source</description>
362            <files>
363                <file category="sourceC" name="CMSIS/DSP/Source/BasicMathFunctions/arm_abs_f32.c"/>
364            </files>
365            <extensions>
366                <extension key="dsplicense" value="Apache-2.0"/>
367            </extensions>
368        </component>
369    </bundle>
370</components>"#;
371
372        let cs: Components = serde_roxmltree::from_str(xml_str).unwrap();
373        assert_eq!(cs.generator, None);
374        assert_eq!(cs.components.len(), 0);
375        assert_eq!(cs.bundles.len(), 1);
376
377        let b = &cs.bundles[0];
378        assert_eq!(b.bundle, "CMSIS");
379        assert_eq!(b.class, "CMSIS");
380        assert_eq!(b.version, "5.9.0");
381        assert_eq!(b.vendor, None);
382        assert_eq!(b.license_set, None);
383        assert_eq!(b.changelog, None);
384        assert_eq!(b.description, "CMSIS software framework");
385        assert_eq!(b.doc, "CMSIS/Documentation/html/index.html");
386        assert_eq!(b.components.len(), 2);
387
388        let c0 = &b.components[0];
389        assert_eq!(c0.group, "CORE");
390        assert_eq!(c0.class, None);
391        assert_eq!(c0.version, None);
392        assert_eq!(c0.variant, None);
393        assert_eq!(c0.is_default_variant, None);
394        assert_eq!(c0.files.files.len(), 2);
395        assert_eq!(c0.files.files[1].condition, Some("CM4".to_string()));
396        assert_eq!(c0.extensions.extensions.len(), 0);
397
398        let c1 = &b.components[1];
399        assert_eq!(c1.group, "DSP");
400        assert_eq!(c1.variant, Some("Source".to_string()));
401        assert_eq!(c1.version, Some("1.10.1".to_string()));
402        assert_eq!(c1.is_default_variant, Some(true));
403        assert_eq!(c1.extensions.extensions[0].key, "dsplicense");
404        assert_eq!(
405            c1.extensions.extensions[0].value,
406            Some("Apache-2.0".to_string())
407        );
408    }
409
410    #[test]
411    fn parse_component_files() {
412        let xml_str = r#"<?xml version="1.0" encoding="UTF-8"?>
413<components>
414    <component Cclass="USB" Cgroup="Core" Cvariant="Device" Cversion="6.15.0">
415        <deprecated>true</deprecated>
416        <description>USB Device stack (deprecated; use MDK-Middleware instead)</description>
417        <RTE_Components_h>#define RTE_USB_CORE</RTE_Components_h>
418        <Pre_Include_Global_h>#include "usb_config.h"</Pre_Include_Global_h>
419        <files>
420            <file category="header" name="USB/Include/rl_usb.h" scope="public"/>
421            <file category="header" name="USB/Config/USB_Config.h"
422                  attr="config" version="6.15.0" select="USB Config"/>
423            <file category="sourceC" name="USB/Source/usbd_core.c"
424                  condition="USB_Cond" src="USB/Source/usbd_core.c"
425                  language="c" public="false" projectpath="USB/Source"/>
426            <file category="doc" name="https://www.keil.com/pack/doc/mw/USB/html/index.html"/>
427        </files>
428        <extensions>
429            <extension key="schemaVersion" value="2.0"/>
430            <extension key="category" value="middleware"/>
431        </extensions>
432        <environments>
433            <environment name="uv" Pname="Core0"/>
434        </environments>
435    </component>
436</components>"#;
437
438        let cs: Components = serde_roxmltree::from_str(xml_str).unwrap();
439        let c = &cs.components[0];
440
441        assert_eq!(c.class, Some("USB".to_string()));
442        assert_eq!(c.group, "Core");
443        assert_eq!(c.variant, Some("Device".to_string()));
444        assert_eq!(c.version, Some("6.15.0".to_string()));
445        assert_eq!(c.deprecated, Some(true));
446        assert_eq!(
447            c.description,
448            "USB Device stack (deprecated; use MDK-Middleware instead)"
449        );
450        assert_eq!(c.rte_components_h, Some("#define RTE_USB_CORE".to_string()));
451        assert_eq!(
452            c.pre_include_global_h,
453            Some("#include \"usb_config.h\"".to_string())
454        );
455        assert_eq!(c.pre_include_local_component_h, None);
456
457        let files = &c.files.files;
458        assert_eq!(files.len(), 4);
459        assert_eq!(
460            files[0],
461            ComponentFile {
462                name: "USB/Include/rl_usb.h".to_string(),
463                category: "header".to_string(),
464                attr: None,
465                condition: None,
466                version: None,
467                select: None,
468                src: None,
469                path: None,
470                language: None,
471                scope: Some("public".to_string()),
472                public: None,
473                projectpath: None,
474                copy: None,
475            }
476        );
477        assert_eq!(files[1].attr, Some("config".to_string()));
478        assert_eq!(files[1].version, Some("6.15.0".to_string()));
479        assert_eq!(files[1].select, Some("USB Config".to_string()));
480        assert_eq!(files[2].condition, Some("USB_Cond".to_string()));
481        assert_eq!(files[2].src, Some("USB/Source/usbd_core.c".to_string()));
482        assert_eq!(files[2].language, Some("c".to_string()));
483        assert_eq!(files[2].public, Some(false));
484        assert_eq!(files[2].projectpath, Some("USB/Source".to_string()));
485        assert_eq!(
486            files[3].name,
487            "https://www.keil.com/pack/doc/mw/USB/html/index.html"
488        );
489        assert_eq!(files[3].category, "doc");
490
491        let exts = &c.extensions.extensions;
492        assert_eq!(exts.len(), 2);
493        assert_eq!(
494            exts[0],
495            ComponentExtension {
496                key: "schemaVersion".to_string(),
497                value: Some("2.0".to_string())
498            }
499        );
500        assert_eq!(
501            exts[1],
502            ComponentExtension {
503                key: "category".to_string(),
504                value: Some("middleware".to_string())
505            }
506        );
507
508        let envs = c.environments.as_ref().unwrap();
509        assert_eq!(
510            envs.environments,
511            vec![ComponentEnvironment {
512                name: "uv".to_string(),
513                processor_name: Some("Core0".to_string()),
514            }]
515        );
516    }
517}