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 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>, 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 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 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}