cps_deps/
cps.rs

1use anyhow::{bail, Result};
2use serde::{Deserialize, Serialize};
3use serde_with::skip_serializing_none;
4use std::{collections::HashMap, fs::File, io::BufReader, path::Path, str::FromStr};
5
6const CPS_VERSION: &str = "0.11.0";
7
8#[skip_serializing_none]
9#[derive(Serialize, Deserialize, Debug, Default)]
10pub struct Platform {
11    pub c_runtime_vendor: Option<String>,
12    pub c_runtime_version: Option<String>,
13    pub clr_vendor: Option<String>,
14    pub clr_version: Option<String>,
15    pub cpp_runtime_vendor: Option<String>,
16    pub cpp_runtime_version: Option<String>,
17    pub isa: Option<String>,
18    pub jvm_vendor: Option<String>,
19    pub jvm_version: Option<String>,
20    pub kernel: Option<String>,
21    pub kernel_version: Option<String>,
22}
23
24#[skip_serializing_none]
25#[derive(Serialize, Deserialize, Debug, Default)]
26pub struct Requirement {
27    pub components: Option<Vec<String>>,
28    pub hints: Option<Vec<String>>,
29    pub version: Option<String>,
30}
31
32#[skip_serializing_none]
33#[derive(Serialize, Deserialize, Debug, Default)]
34pub struct ComponentFields {
35    pub location: Option<String>,
36    pub requires: Option<Vec<String>>,
37    pub configurations: Option<HashMap<String, Configuration>>,
38    pub compile_features: Option<Vec<String>>,
39    pub compile_flags: Option<LanguageStringList>,
40    pub definitions: Option<LanguageStringList>,
41    pub includes: Option<LanguageStringList>,
42    pub link_features: Option<Vec<String>>,
43    pub link_flags: Option<Vec<String>>,
44    pub link_languages: Option<Vec<String>>,
45    pub link_libraries: Option<Vec<String>>,
46    pub link_location: Option<String>,
47    pub link_requires: Option<String>,
48}
49
50impl ComponentFields {
51    /// Test if the has a location either through an attribute or all configurations
52    pub fn has_location(&self) -> bool {
53        if self.location.is_some() {
54            return true;
55        } else if let Some(configuration) = &self.configurations {
56            if configuration
57                .values()
58                .all(|config| config.location.is_some())
59            {
60                return true;
61            }
62        }
63        false
64    }
65}
66
67#[derive(Serialize, Deserialize, Debug)]
68#[serde(untagged)]
69#[allow(clippy::large_enum_variant)]
70pub enum MaybeComponent {
71    Component(Component),
72    Other(serde_json::Value),
73}
74
75impl MaybeComponent {
76    pub fn from_dylib_location(location: &str) -> Self {
77        Self::Component(Component::Dylib(ComponentFields {
78            location: Some(location.to_string()),
79            ..ComponentFields::default()
80        }))
81    }
82
83    pub fn from_archive_location(location: &str) -> Self {
84        Self::Component(Component::Archive(ComponentFields {
85            location: Some(location.to_string()),
86            ..ComponentFields::default()
87        }))
88    }
89}
90
91#[skip_serializing_none]
92#[derive(Serialize, Deserialize, Debug, Default)]
93#[serde(tag = "type", rename_all = "lowercase")]
94pub enum Component {
95    Archive(ComponentFields),
96    Dylib(ComponentFields),
97    Module(ComponentFields),
98    Jar(ComponentFields),
99    Interface(ComponentFields),
100    Symbolic(ComponentFields),
101    #[default]
102    Unknwon,
103}
104
105#[derive(Serialize, Deserialize, Debug)]
106#[serde(untagged)]
107pub enum LanguageStringList {
108    LanguageMap(HashMap<String, Vec<String>>),
109    List(Vec<String>),
110}
111
112impl LanguageStringList {
113    pub fn any_language_map(list: Vec<String>) -> Self {
114        Self::LanguageMap(HashMap::from([("*".to_string(), list)]))
115    }
116}
117
118#[skip_serializing_none]
119#[derive(Serialize, Deserialize, Debug, Default)]
120pub struct Configuration {
121    pub location: Option<String>,
122    pub requires: Option<Vec<String>>,
123    pub compile_features: Option<Vec<String>>,
124    pub compile_flags: Option<LanguageStringList>,
125    pub definitions: Option<LanguageStringList>,
126    pub includes: Option<LanguageStringList>,
127    pub link_features: Option<Vec<String>>,
128    pub link_flags: Option<Vec<String>>,
129    pub link_languages: Option<Vec<String>>,
130    pub link_libraries: Option<Vec<String>>,
131    pub link_location: Option<String>,
132    pub link_requires: Option<String>,
133}
134
135#[skip_serializing_none]
136#[derive(Serialize, Deserialize, Debug)]
137pub struct Package {
138    pub name: String,
139    pub cps_version: String,
140    pub components: HashMap<String, MaybeComponent>,
141
142    pub platform: Option<Platform>,
143    pub configuration: Option<String>, // required in configuration-specific cps and ignored otherwise
144    pub configurations: Option<Vec<String>>,
145    pub cps_path: Option<String>,
146    pub version: Option<String>,
147    pub version_schema: Option<String>,
148    pub description: Option<String>,
149    pub default_components: Option<Vec<String>>,
150    pub requires: Option<HashMap<String, Requirement>>,
151    pub compat_version: Option<String>,
152}
153
154pub fn parse_and_print_cps(filepath: &Path) -> Result<()> {
155    let file = File::open(filepath)?;
156    let reader = BufReader::new(file);
157    let package = Package::from_reader(reader)?;
158
159    dbg!(package);
160    Ok(())
161}
162
163impl FromStr for Package {
164    type Err = anyhow::Error;
165
166    fn from_str(data: &str) -> Result<Self> {
167        let package: Package = serde_json::from_str(data)?;
168        package.validate()?;
169        Ok(package)
170    }
171}
172
173impl Default for Package {
174    fn default() -> Self {
175        Self {
176            name: String::default(),
177            cps_version: CPS_VERSION.to_string(),
178            components: HashMap::default(),
179            platform: None,
180            configuration: None,
181            configurations: None,
182            cps_path: None,
183            version: None,
184            version_schema: None,
185            description: None,
186            default_components: None,
187            requires: None,
188            compat_version: None,
189        }
190    }
191}
192
193impl Package {
194    pub fn from_reader<R>(reader: R) -> Result<Self>
195    where
196        R: std::io::Read,
197    {
198        let package: Package = serde_json::from_reader(reader)?;
199        package.validate()?;
200        Ok(package)
201    }
202
203    /// Used by deserialization functions to validate CPS schema rules
204    pub fn validate(&self) -> Result<()> {
205        if self.cps_version != CPS_VERSION {
206            bail!("Unsupported CPS version: {}", self.cps_version);
207        }
208        for (name, component) in self.components.iter() {
209            match component {
210                MaybeComponent::Component(Component::Archive(fields)) => {
211                    if !fields.has_location() {
212                        bail!("Component `{}` is missing attribute `location`", name);
213                    }
214                }
215                MaybeComponent::Component(Component::Dylib(fields)) => {
216                    if !fields.has_location() {
217                        bail!("Component `{}` is missing attribute `location`", name);
218                    }
219                }
220                MaybeComponent::Component(Component::Module(fields)) => {
221                    if !fields.has_location() {
222                        bail!("Component `{}` is missing attribute `location`", name);
223                    }
224                }
225                MaybeComponent::Component(Component::Jar(fields)) => {
226                    if !fields.has_location() {
227                        bail!("Component `{}` is missing attribute `location`", name);
228                    }
229                }
230                _ => {}
231            }
232        }
233        Ok(())
234    }
235}
236
237#[test]
238fn test_parse_sample_cps() -> Result<()> {
239    // cps_version was manually added: https://github.com/cps-org/cps/issues/57
240    let sample_cps = r#"{
241    "name": "sample",
242    "description": "Sample CPS",
243    "license": "BSD",
244    "version": "1.2.0",
245    "compat_version": "0.8.0",
246    "cps_version": "0.11.0",
247    "platform": {
248        "isa": "x86_64",
249        "kernel": "linux",
250        "c_runtime_vendor": "gnu",
251        "c_runtime_version": "2.20",
252        "jvm_version": "1.6"
253    },
254    "configurations": [ "optimized", "debug" ],
255    "default_components": [ "sample" ],
256    "components": {
257        "sample-core": {
258        "type": "interface",
259        "definitions": [ "SAMPLE" ],
260        "includes": [ "@prefix@/include" ]
261        },
262        "sample": {
263        "type": "interface",
264        "configurations": {
265            "shared": {
266            "requires": [ ":sample-shared" ]
267            },
268            "static": {
269            "requires": [ ":sample-static" ]
270            }
271        }
272        },
273        "sample-shared": {
274        "type": "dylib",
275        "requires": [ ":sample-core" ],
276        "configurations": {
277            "optimized": {
278            "location": "@prefix@/lib64/libsample.so.1.2.0"
279            },
280            "debug": {
281            "location": "@prefix@/lib64/libsample_d.so.1.2.0"
282            }
283        }
284        },
285        "sample-static": {
286        "type": "archive",
287        "definitions": [ "SAMPLE_STATIC" ],
288        "requires": [ ":sample-core" ],
289        "configurations": {
290            "optimized": {
291            "location": "@prefix@/lib64/libsample.a"
292            },
293            "debug": {
294            "location": "@prefix@/lib64/libsample_d.a"
295            }
296        }
297        },
298        "sample-tool": {
299        "type": "exe",
300        "location": "@prefix@/bin/sample-tool"
301        },
302        "sample-java": {
303        "type": "jar",
304        "location": "@prefix@/share/java/sample.jar"
305        }
306    }
307}"#;
308
309    Package::from_str(sample_cps)?;
310    Ok(())
311}