1use std::{collections::HashMap, path::PathBuf};
2
3use derive_builder::Builder;
4use indexmap::IndexMap;
5use node_semver::{Range, Version};
6use serde::{Deserialize, Deserializer, Serialize};
7use serde_json::Value;
8
9use crate::{CorgiVersionMetadata, VersionMetadata};
10
11#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct CorgiManifest {
14 #[serde(skip_serializing_if = "Option::is_none")]
15 pub name: Option<String>,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub version: Option<Version>,
18 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
19 pub dependencies: IndexMap<String, String>,
20 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
21 pub dev_dependencies: IndexMap<String, String>,
22 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
23 pub optional_dependencies: IndexMap<String, String>,
24 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
25 pub peer_dependencies: IndexMap<String, String>,
26 #[serde(default, alias = "bundleDependencies", alias = "bundledDependencies")]
27 pub bundled_dependencies: Option<BundledDependencies>,
28}
29
30#[derive(Builder, Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct Manifest {
33 #[builder(setter(into, strip_option), default)]
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub name: Option<String>,
40
41 #[builder(setter(strip_option), default)]
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub version: Option<Version>,
48
49 #[builder(setter(into, strip_option), default)]
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub description: Option<String>,
52
53 #[builder(setter(into, strip_option), default)]
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub homepage: Option<String>,
56
57 #[serde(default, alias = "licence", skip_serializing_if = "Option::is_none")]
58 #[builder(setter(into, strip_option), default)]
59 pub license: Option<String>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
62 #[builder(setter(strip_option), default)]
63 pub bugs: Option<Bugs>,
64
65 #[serde(default, skip_serializing_if = "Vec::is_empty")]
66 #[builder(default)]
67 pub keywords: Vec<String>,
68
69 #[builder(setter(strip_option), default)]
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub bin: Option<Bin>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
78 #[builder(setter(strip_option), default)]
79 pub author: Option<PersonField>,
80
81 #[serde(default, skip_serializing_if = "Vec::is_empty")]
82 #[builder(default)]
83 pub contributors: Vec<PersonField>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
86 #[builder(default)]
87 pub files: Option<Vec<String>>,
88
89 #[builder(setter(into, strip_option), default)]
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub main: Option<String>,
92
93 #[builder(setter(strip_option), default)]
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub man: Option<Man>,
96
97 #[serde(skip, default)]
98 #[builder(default)]
99 pub directories: Option<Directories>,
100
101 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
102 #[builder(setter(into, strip_option), default)]
103 pub module_type: Option<String>,
104
105 #[builder(setter(strip_option), default)]
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub exports: Option<Exports>,
108
109 #[builder(setter(strip_option), default)]
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub imports: Option<Imports>,
112
113 #[builder(setter(strip_option), default)]
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub repository: Option<Repository>,
121
122 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
127 #[builder(default)]
128 pub scripts: HashMap<String, String>,
129
130 #[builder(setter(strip_option), default)]
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub config: Option<Value>,
133
134 #[serde(
138 default,
139 deserialize_with = "object_or_bust",
140 skip_serializing_if = "HashMap::is_empty"
141 )]
142 #[builder(default)]
143 pub engines: HashMap<String, Range>,
144
145 #[serde(default, skip_serializing_if = "Vec::is_empty")]
146 #[builder(default)]
147 pub os: Vec<String>,
148
149 #[serde(default, skip_serializing_if = "Vec::is_empty")]
150 #[builder(default)]
151 pub cpu: Vec<String>,
152
153 #[serde(skip_serializing_if = "Option::is_none")]
154 #[builder(setter(strip_option), default)]
155 pub private: Option<bool>,
156
157 #[serde(
158 default,
159 rename = "publishConfig",
160 skip_serializing_if = "HashMap::is_empty"
161 )]
162 #[builder(default)]
163 pub publish_config: HashMap<String, Value>,
164
165 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
167 #[builder(default)]
168 pub dependencies: IndexMap<String, String>,
169
170 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
171 #[builder(default)]
172 pub dev_dependencies: IndexMap<String, String>,
173
174 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
175 #[builder(default)]
176 pub optional_dependencies: IndexMap<String, String>,
177
178 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
179 #[builder(default)]
180 pub peer_dependencies: IndexMap<String, String>,
181
182 #[serde(
183 default,
184 alias = "bundleDependencies",
185 alias = "bundledDependencies",
186 skip_serializing_if = "empty_bundled_dependencies"
187 )]
188 #[builder(default)]
189 pub bundled_dependencies: Option<BundledDependencies>,
190
191 #[serde(default, skip_serializing_if = "Vec::is_empty")]
192 #[builder(default)]
193 pub workspaces: Vec<String>,
194
195 #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
196 #[builder(default)]
197 pub _rest: HashMap<String, Value>,
198}
199
200impl From<CorgiManifest> for Manifest {
201 fn from(value: CorgiManifest) -> Self {
202 Manifest {
203 name: value.name,
204 version: value.version,
205 dependencies: value.dependencies,
206 dev_dependencies: value.dev_dependencies,
207 optional_dependencies: value.optional_dependencies,
208 peer_dependencies: value.peer_dependencies,
209 bundled_dependencies: value.bundled_dependencies,
210 ..Default::default()
211 }
212 }
213}
214
215impl From<Manifest> for CorgiManifest {
216 fn from(value: Manifest) -> Self {
217 CorgiManifest {
218 name: value.name,
219 version: value.version,
220 dependencies: value.dependencies,
221 dev_dependencies: value.dev_dependencies,
222 optional_dependencies: value.optional_dependencies,
223 peer_dependencies: value.peer_dependencies,
224 bundled_dependencies: value.bundled_dependencies,
225 }
226 }
227}
228
229impl From<CorgiManifest> for CorgiVersionMetadata {
230 fn from(value: CorgiManifest) -> Self {
231 CorgiVersionMetadata {
232 manifest: value,
233 ..Default::default()
234 }
235 }
236}
237
238impl From<Manifest> for VersionMetadata {
239 fn from(value: Manifest) -> Self {
240 VersionMetadata {
241 manifest: value,
242 ..Default::default()
243 }
244 }
245}
246
247fn object_or_bust<'de, D, K, V>(deserializer: D) -> std::result::Result<HashMap<K, V>, D::Error>
248where
249 D: Deserializer<'de>,
250 K: std::hash::Hash + Eq + Deserialize<'de>,
251 V: Deserialize<'de>,
252{
253 let val: ObjectOrBust<K, V> = Deserialize::deserialize(deserializer)?;
254 if let ObjectOrBust::Object(map) = val {
255 Ok(map)
256 } else {
257 Ok(HashMap::new())
258 }
259}
260
261#[derive(Deserialize)]
262#[serde(untagged)]
263enum ObjectOrBust<K, V>
264where
265 K: std::hash::Hash + Eq,
266{
267 Object(HashMap<K, V>),
268 Value(serde_json::Value),
269}
270
271#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
272#[serde(untagged)]
273pub enum BundledDependencies {
274 All(bool),
275 Some(Vec<String>),
276}
277
278fn empty_bundled_dependencies(bundled: &Option<BundledDependencies>) -> bool {
279 match bundled {
280 None => true,
281 Some(BundledDependencies::All(all)) => !all,
282 Some(BundledDependencies::Some(deps)) => deps.is_empty(),
283 }
284}
285
286#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
287#[serde(untagged)]
288pub enum Bugs {
289 Str(String),
290 Obj {
291 url: Option<String>,
292 email: Option<String>,
293 },
294}
295
296#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
298#[serde(untagged)]
299pub enum PersonField {
300 Str(String),
301 Obj(Person),
302}
303
304#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
305pub struct Person {
306 pub name: Option<String>,
307 pub email: Option<String>,
308 pub url: Option<String>,
309}
310
311#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
312pub struct Directories {
313 pub bin: Option<PathBuf>,
314 pub man: Option<PathBuf>,
315}
316
317#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
318#[serde(untagged)]
319pub enum Bin {
320 Str(String),
321 Hash(HashMap<String, PathBuf>),
322 Array(Vec<PathBuf>),
323}
324
325#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
326#[serde(untagged)]
327pub enum Man {
328 Str(String),
329 Vec(Vec<String>),
330}
331
332#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
333#[serde(untagged)]
334pub enum Exports {
335 Str(String),
336 Vec(Vec<String>),
337 Obj(HashMap<String, Exports>),
338 Other(Value),
339}
340
341#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
342#[serde(untagged)]
343pub enum Imports {
344 Str(String),
345 Vec(Vec<String>),
346 Obj(HashMap<String, Imports>),
347 Other(Value),
348}
349
350#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
351#[serde(untagged)]
352pub enum Repository {
353 Str(String),
354 Obj {
355 #[serde(rename = "type")]
356 repo_type: Option<String>,
357 url: Option<String>,
358 directory: Option<String>,
359 },
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 use miette::{IntoDiagnostic, Result};
367 use pretty_assertions::assert_eq;
368
369 #[test]
370 fn basic_from_json() -> Result<()> {
371 let string = r#"
372{
373 "name": "hello",
374 "version": "1.2.3",
375 "description": "description",
376 "homepage": "https://foo.dev",
377 "devDependencies": {
378 "foo": "^3.2.1"
379 }
380}
381 "#;
382 let mut deps = IndexMap::new();
383 deps.insert(String::from("foo"), String::from("^3.2.1"));
384 let parsed = serde_json::from_str::<Manifest>(string).into_diagnostic()?;
385 assert_eq!(
386 parsed,
387 ManifestBuilder::default()
388 .name("hello")
389 .version("1.2.3".parse()?)
390 .description("description")
391 .homepage("https://foo.dev")
392 .dev_dependencies(deps)
393 .build()
394 .unwrap()
395 );
396 Ok(())
397 }
398
399 #[test]
400 fn empty() -> Result<()> {
401 let string = "{}";
402 let parsed = serde_json::from_str::<Manifest>(string).into_diagnostic()?;
403 assert_eq!(parsed, ManifestBuilder::default().build().unwrap());
404 Ok(())
405 }
406
407 #[test]
408 fn string_props() -> Result<()> {
409 let string = r#"
410{
411 "name": "hello",
412 "description": "description",
413 "homepage": "https://foo.dev",
414 "license": "Parity-7.0",
415 "main": "index.js",
416 "keywords": ["foo", "bar"],
417 "files": ["*.js"],
418 "os": ["windows", "darwin"],
419 "cpu": ["x64"],
420 "bundleDependencies": [
421 "mydep"
422 ],
423 "workspaces": [
424 "packages/*"
425 ]
426}
427 "#;
428 let parsed = serde_json::from_str::<Manifest>(string).into_diagnostic()?;
429 assert_eq!(
430 parsed,
431 ManifestBuilder::default()
432 .name("hello")
433 .description("description")
434 .homepage("https://foo.dev")
435 .license("Parity-7.0")
436 .main("index.js")
437 .keywords(vec!["foo".into(), "bar".into()])
438 .files(Some(vec!["*.js".into()]))
439 .os(vec!["windows".into(), "darwin".into()])
440 .cpu(vec!["x64".into()])
441 .bundled_dependencies(Some(BundledDependencies::Some(vec!["mydep".into()])))
442 .workspaces(vec!["packages/*".into()])
443 .build()
444 .unwrap()
445 );
446 Ok(())
447 }
448
449 #[test]
450 fn array_engines() -> Result<()> {
451 let string = r#"
452{
453 "engines": []
454}
455 "#;
456 let parsed = serde_json::from_str::<Manifest>(string).into_diagnostic()?;
457 assert_eq!(
458 parsed,
459 ManifestBuilder::default()
460 .engines(HashMap::new())
461 .build()
462 .unwrap()
463 );
464 Ok(())
465 }
466
467 #[test]
468 fn licence_alias() -> Result<()> {
469 let string = r#"
470{
471 "licence": "Parity-7.0"
472}
473 "#;
474 let parsed = serde_json::from_str::<Manifest>(string).into_diagnostic()?;
475 assert_eq!(
476 parsed,
477 ManifestBuilder::default()
478 .license("Parity-7.0")
479 .build()
480 .unwrap()
481 );
482 Ok(())
483 }
484
485 #[test]
486 fn parse_version() -> Result<()> {
487 let string = r#"
488{
489 "version": "1.2.3"
490}
491 "#;
492 let parsed = serde_json::from_str::<Manifest>(string).into_diagnostic()?;
493 assert_eq!(
494 parsed,
495 ManifestBuilder::default()
496 .version("1.2.3".parse()?)
497 .build()
498 .unwrap()
499 );
500
501 let string = r#"
502{
503 "version": "invalid"
504}
505 "#;
506 let parsed = serde_json::from_str::<Manifest>(string);
507 assert!(parsed.is_err());
508 Ok(())
509 }
510
511 #[test]
512 fn bool_props() -> Result<()> {
513 let string = r#"
514{
515 "private": true
516}
517 "#;
518 let parsed = serde_json::from_str::<Manifest>(string).into_diagnostic()?;
519 assert_eq!(
520 parsed,
521 ManifestBuilder::default().private(true).build().unwrap()
522 );
523 Ok(())
524 }
525}