1mod author;
11mod builder;
12mod identity;
13mod workspace;
14
15pub use author::Author;
16pub use builder::ProjectBuilder;
17pub use identity::Identity;
18pub use workspace::Workspace;
19
20use std::collections::BTreeMap;
21use std::path::{Path, PathBuf};
22
23use cargo_metadata::Package;
24
25use crate::config::Config;
26
27pub const DEFAULT_MANIFEST_PATH: &str = "Cargo.toml";
29
30pub(crate) const METADATA_KEY: &str = "npmgen";
32
33#[derive(Debug, Clone, Default)]
36pub struct Overrides {
37 pub packages: Vec<String>,
39 pub workspace: bool,
41 pub exclude: Vec<String>,
43 pub bins: Vec<String>,
45 pub version: Option<String>,
47}
48
49#[derive(Debug, Clone)]
51pub struct Project {
52 pub identity: Identity,
53 pub version: String,
54 pub description: String,
55 pub author: Author,
56 pub license: String,
57 pub repository: String,
58 pub bin: String,
60 pub package: Option<String>,
62 pub config: Config,
63 pub workspace_root: PathBuf,
64 pub target_directory: PathBuf,
65}
66
67#[derive(Debug, thiserror::Error)]
69pub enum ProjectError {
70 #[error("running `cargo metadata`")]
71 Metadata {
72 #[source]
73 source: Box<cargo_metadata::Error>,
74 },
75
76 #[error("no workspace package named {name:?}")]
77 PackageNotFound { name: String },
78
79 #[error("package repository must be set to https://<host>/<owner>/<repo>")]
80 MissingRepository,
81
82 #[error("package {package:?} has no bin named {bin:?}")]
83 UnknownBin { package: String, bin: String },
84
85 #[error("no workspace bin named {bin:?}")]
86 BinNotInWorkspace { bin: String },
87
88 #[error("bin {bin:?} is defined by more than one package ({}); select one with --package", packages.join(", "))]
89 AmbiguousBin { bin: String, packages: Vec<String> },
90
91 #[error(
92 "nothing to publish: no selected package ships a binary (every match is a library or `publish = false`)"
93 )]
94 NothingToPublish,
95
96 #[error(
97 "{count} binaries match; narrow with --package/--bin, or use Project::discover for the full set"
98 )]
99 NotSingle { count: usize },
100
101 #[error("invalid project field {field}: {reason}")]
102 InvalidField {
103 field: &'static str,
104 reason: &'static str,
105 },
106
107 #[error(transparent)]
108 Config(#[from] crate::config::ConfigError),
109}
110
111impl Project {
112 pub fn builder(
115 scope: impl Into<String>,
116 name: impl Into<String>,
117 version: impl Into<String>,
118 ) -> ProjectBuilder {
119 ProjectBuilder::new(scope, name, version)
120 }
121
122 pub fn discover(
130 manifest_path: &Path,
131 overrides: &Overrides,
132 ) -> Result<Vec<Self>, ProjectError> {
133 let projects = Workspace::load(manifest_path)?.projects(overrides)?;
134 if projects.is_empty() {
135 return Err(ProjectError::NothingToPublish);
136 }
137 Ok(projects)
138 }
139
140 pub fn load(manifest_path: &Path, overrides: &Overrides) -> Result<Self, ProjectError> {
144 let mut projects = Self::discover(manifest_path, overrides)?;
145 match projects.len() {
146 1 => Ok(projects.pop().unwrap()),
147 count => Err(ProjectError::NotSingle { count }),
148 }
149 }
150
151 pub(crate) fn from_package_bin(
154 package: &Package,
155 bin: &str,
156 config: &Config,
157 overrides: &Overrides,
158 workspace_root: &Path,
159 target_directory: &Path,
160 ) -> Result<Self, ProjectError> {
161 let repository = package
162 .repository
163 .clone()
164 .ok_or(ProjectError::MissingRepository)?;
165 let base = Identity::from_repository(&repository, config.scope.as_deref())?;
166 let identity = Identity {
167 name: bin.to_owned(),
168 ..base
169 };
170 let version = overrides
171 .version
172 .clone()
173 .unwrap_or_else(|| package.version.to_string());
174 let license = config
175 .license
176 .clone()
177 .or_else(|| package.license.clone())
178 .unwrap_or_default();
179 Ok(Self {
180 identity,
181 version,
182 description: package.description.clone().unwrap_or_default(),
183 author: Author::parse(&package.authors.first().cloned().unwrap_or_default()),
184 license,
185 repository,
186 bin: bin.to_owned(),
187 package: Some(package.name.as_str().to_owned()),
188 config: config.clone(),
189 workspace_root: workspace_root.to_path_buf(),
190 target_directory: target_directory.to_path_buf(),
191 })
192 }
193
194 pub fn package_name(&self) -> String {
196 format!("{}/{}", self.identity.scope, self.identity.name)
197 }
198
199 pub fn variables(&self) -> BTreeMap<String, String> {
201 BTreeMap::from([
202 ("name".to_owned(), self.identity.name.clone()),
203 ("scope".to_owned(), self.identity.scope.clone()),
204 ("package".to_owned(), self.package_name()),
205 ("version".to_owned(), self.version.clone()),
206 ("description".to_owned(), self.description.clone()),
207 ("license".to_owned(), self.license.clone()),
208 ("repository".to_owned(), self.repository.clone()),
209 ("git_url".to_owned(), self.identity.git_url.clone()),
210 ("bin".to_owned(), self.bin.clone()),
211 ("author".to_owned(), self.author.full.clone()),
212 ("author_name".to_owned(), self.author.name.clone()),
213 (
214 "author_email".to_owned(),
215 self.author.email.clone().unwrap_or_default(),
216 ),
217 ])
218 }
219}
220
221#[cfg(test)]
224pub(crate) fn sample_project() -> Project {
225 Project {
226 identity: Identity {
227 scope: "@gglinnk".to_owned(),
228 name: "nocmd".to_owned(),
229 git_url: "git+https://github.com/gglinnk/nocmd.git".to_owned(),
230 },
231 version: "0.1.1".to_owned(),
232 description: "a hook".to_owned(),
233 author: Author::parse("Gabriel GRONDIN <gglinnk@protonmail.com>"),
234 license: "MIT".to_owned(),
235 repository: "https://github.com/gglinnk/nocmd".to_owned(),
236 bin: "nocmd".to_owned(),
237 package: Some("nocmd".to_owned()),
238 config: Config::default(),
239 workspace_root: PathBuf::from("."),
240 target_directory: PathBuf::from("target"),
241 }
242}