oro_common/
manifest.rs

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    /// The name of the package.
34    ///
35    /// If this is missing, it usually indicates that this package exists only to
36    /// describe a workspace, similar to Cargo's notion of a "virtual manifest".
37    #[builder(setter(into, strip_option), default)]
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub name: Option<String>,
40
41    /// The version of the package.
42    ///
43    /// Package managers generally require this to be populated to actually publish
44    /// the package, but will tolerate its absence during local development.
45    #[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    /// Information about the names and locations of binaries this package provides.
70    ///
71    /// Use [`crate::BuildManifest::from_manifest`][] to get a normalized version
72    /// of this field (and other related fields).
73    #[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    /// Information about the repository this project is hosted in.
114    ///
115    /// [`Repository::Str`][] can contain many different formats (or plain garbage),
116    /// we recommend trying to `.parse()` it as oro-package-spec's GitInfo type,
117    /// as it understands most of the relevant formats.
118    #[builder(setter(strip_option), default)]
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub repository: Option<Repository>,
121
122    /// Information about build scripts the package uses.
123    ///
124    /// Use [`crate::BuildManifest::from_manifest`][] to get a normalized version
125    /// of this field (and other related fields).
126    #[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    // NOTE: using object_or_bust here because lodash has `"engines": []` in
135    // some versions? This is obviously obnoxious, but we're playing
136    // whack-a-mole here.
137    #[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    // Deps
166    #[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/// Represents a human!
297#[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}