oro_package_spec/
lib.rs

1//! NPM package specifier parser. This is the stuff that takes something like
2//! `foo@^1.2.3` and turns it into something meaningful.
3
4use std::fmt;
5use std::path::PathBuf;
6use std::str::FromStr;
7
8use node_semver::{Range, Version};
9use nom::combinator::all_consuming;
10use nom::Err;
11
12pub use crate::error::{PackageSpecError, SpecErrorKind};
13pub use crate::gitinfo::{GitHost, GitInfo};
14use crate::parsers::package;
15
16mod error;
17mod gitinfo;
18mod parsers;
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub enum VersionSpec {
22    Tag(String),
23    Version(Version),
24    Range(Range),
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub enum PackageSpec {
29    Dir {
30        path: PathBuf,
31    },
32    Alias {
33        name: String,
34        spec: Box<PackageSpec>,
35    },
36    Npm {
37        scope: Option<String>,
38        name: String,
39        requested: Option<VersionSpec>,
40    },
41    Git(GitInfo),
42}
43
44impl PackageSpec {
45    pub fn is_alias(&self) -> bool {
46        matches!(self, PackageSpec::Alias { .. })
47    }
48
49    pub fn is_npm(&self) -> bool {
50        use PackageSpec::*;
51        match self {
52            Alias { spec, .. } => spec.is_npm(),
53            Dir { .. } | Git(..) => false,
54            Npm { .. } => true,
55        }
56    }
57
58    pub fn target(&self) -> &PackageSpec {
59        use PackageSpec::*;
60        match self {
61            Alias { ref spec, .. } => {
62                if spec.is_alias() {
63                    spec.target()
64                } else {
65                    spec
66                }
67            }
68            _ => self,
69        }
70    }
71
72    pub fn target_mut(&mut self) -> &mut PackageSpec {
73        use PackageSpec::*;
74        match self {
75            Alias { spec, .. } => {
76                if spec.is_alias() {
77                    spec.target_mut()
78                } else {
79                    spec
80                }
81            }
82            _ => self,
83        }
84    }
85
86    pub fn requested(&self) -> String {
87        use PackageSpec::*;
88        match self {
89            Dir { path } => format!("{}", path.display()),
90            Git(info) => format!("{info}"),
91            Npm { ref requested, .. } => requested
92                .as_ref()
93                .map(|r| r.to_string())
94                .unwrap_or_else(|| "*".to_string()),
95            Alias { ref spec, .. } => {
96                format!(
97                    "{}{}",
98                    if let Npm { .. } = **spec { "npm:" } else { "" },
99                    spec
100                )
101            }
102        }
103    }
104}
105
106impl FromStr for PackageSpec {
107    type Err = PackageSpecError;
108
109    fn from_str(s: &str) -> Result<Self, Self::Err> {
110        parse_package_spec(s)
111    }
112}
113
114impl fmt::Display for PackageSpec {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        use PackageSpec::*;
117        match self {
118            Dir { path } => write!(f, "{}", path.display()),
119            Git(info) => write!(f, "{info}"),
120            Npm {
121                ref name,
122                ref requested,
123                ..
124            } => {
125                write!(f, "{name}")?;
126                if let Some(req) = requested {
127                    write!(f, "@{req}")?;
128                }
129                Ok(())
130            }
131            Alias { ref name, ref spec } => {
132                write!(f, "{name}@")?;
133                if let Npm { .. } = **spec {
134                    write!(f, "npm:")?;
135                }
136                write!(f, "{spec}")
137            }
138        }
139    }
140}
141
142impl fmt::Display for VersionSpec {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        use VersionSpec::*;
145        match self {
146            Tag(tag) => write!(f, "{tag}"),
147            Version(v) => write!(f, "{v}"),
148            Range(range) => write!(f, "{range}"),
149        }
150    }
151}
152
153fn parse_package_spec<I>(input: I) -> Result<PackageSpec, PackageSpecError>
154where
155    I: AsRef<str>,
156{
157    let input = input.as_ref();
158    match all_consuming(package::package_spec)(input) {
159        Ok((_, arg)) => Ok(arg),
160        Err(err) => Err(match err {
161            Err::Error(e) | Err::Failure(e) => PackageSpecError {
162                input: input.into(),
163                offset: e.input.as_ptr() as usize - input.as_ptr() as usize,
164                kind: if let Some(kind) = e.kind {
165                    kind
166                } else if let Some(ctx) = e.context {
167                    SpecErrorKind::Context(ctx)
168                } else {
169                    SpecErrorKind::Other
170                },
171            },
172            Err::Incomplete(_) => PackageSpecError {
173                input: input.into(),
174                offset: input.len() - 1,
175                kind: SpecErrorKind::IncompleteInput,
176            },
177        }),
178    }
179}