1use std::{env, path::PathBuf, process::Command, string::FromUtf8Error};
2use thiserror::Error;
3
4#[cfg(feature = "py-binding")]
5use pyo3::prelude::*;
6
7#[cfg(feature = "clap")]
8use clap::Parser;
9
10#[derive(Debug, Error)]
12pub enum CliError {
13 #[error("{0}")]
14 Io(#[from] std::io::Error),
15
16 #[error("{0}")]
17 Utf8Error(#[from] FromUtf8Error),
18
19 #[error("Unknown working directory name")]
20 UnknownWorkingDirectory,
21
22 #[error("Malformed repository name: {0}")]
23 MalformedRepoName(String),
24}
25
26#[cfg_attr(feature = "clap", derive(Parser, Debug, Default, Clone))]
47#[cfg_attr(not(feature = "clap"), derive(Debug, Default, Clone))]
48#[cfg_attr(
49 feature = "clap",
50 command(name = "rmskin-builder", about, long_about, verbatim_doc_comment)
51)]
52#[cfg_attr(feature = "py-binding", pyclass(module = "rmskin_builder"))]
53pub struct CliArgs {
54 #[cfg_attr(feature = "clap", arg(short, long, default_value = "./"))]
59 pub path: Option<PathBuf>,
60
61 #[cfg_attr(feature = "clap", arg(short = 'V', long))]
65 version: Option<String>,
66
67 #[cfg_attr(feature = "clap", arg(short, long, verbatim_doc_comment))]
74 author: Option<String>,
75
76 #[cfg_attr(feature = "clap", arg(short, long, verbatim_doc_comment))]
82 title: Option<String>,
83
84 #[cfg_attr(
88 feature = "clap",
89 arg(short, long, alias = "dir_out", default_value = "./")
90 )]
91 pub dir_out: Option<PathBuf>,
92}
93
94const GH_REPO: &str = "GITHUB_REPOSITORY";
95const GH_REF: &str = "GITHUB_REF";
96const GH_SHA: &str = "GITHUB_SHA";
97const GH_ACTOR: &str = "GITHUB_ACTOR";
98
99impl CliArgs {
100 pub fn get_version(&self) -> Result<String, CliError> {
104 if let Some(version) = &self.version {
105 return Ok(version.clone());
106 }
107 if let Ok(gh_ref) = env::var(GH_REF) {
108 if let Some(stripped) = gh_ref.strip_prefix("refs/tags/") {
109 Ok(stripped.to_string())
110 } else if let Ok(gh_sha) = env::var(GH_SHA) {
111 let len = gh_sha.len().saturating_sub(7);
112 Ok(gh_sha[len..].to_string())
113 } else {
114 Ok("x0x.y0y".to_string())
115 }
116 } else {
117 if let Ok(result) = Command::new("git").args(["describe", "--tags"]).output() {
120 Ok(String::from_utf8(result.stdout.trim_ascii().to_vec())?)
121 } else {
122 let result = Command::new("git")
123 .args(["log", "-1", "--format=\"%h\""])
124 .output()?;
125 Ok(String::from_utf8(result.stdout.trim_ascii().to_vec())?)
126 }
127 }
128 }
129
130 pub fn get_author(&self) -> Result<String, CliError> {
137 if let Some(author) = &self.author {
138 Ok(author.clone())
139 } else if let Ok(actor) = env::var(GH_ACTOR) {
140 return Ok(actor);
141 } else {
142 let result = Command::new("git")
143 .args(["config", "get", "user.name"])
144 .output()?;
145 return Ok(String::from_utf8(result.stdout.trim_ascii().to_vec())?);
146 }
147 }
148
149 pub fn get_title(&self) -> Result<String, CliError> {
155 if let Some(title) = &self.title {
156 Ok(title.to_owned())
157 } else {
158 if let Ok(mut repo) = env::var(GH_REPO) {
159 let divider = repo
160 .find('/')
161 .ok_or(CliError::MalformedRepoName(repo.to_owned()))?
162 + 1;
163 return Ok(repo.split_off(divider));
164 }
165 let curr_dir = env::current_dir()?;
166 Ok(curr_dir
167 .file_name()
168 .ok_or(CliError::UnknownWorkingDirectory)?
169 .to_string_lossy()
170 .to_string())
171 }
172 }
173}
174
175impl CliArgs {
176 pub fn version(&mut self, value: Option<String>) {
178 self.version = value;
179 }
180
181 pub fn author(&mut self, value: Option<String>) {
183 self.author = value;
184 }
185
186 pub fn title(&mut self, value: Option<String>) {
188 self.title = value;
189 }
190}
191
192#[cfg(feature = "py-binding")]
193#[cfg_attr(feature = "py-binding", pymethods)]
194impl CliArgs {
195 #[getter("version")]
196 pub fn get_version_py(&self) -> PyResult<String> {
197 use pyo3::exceptions::{PyIOError, PyValueError};
198
199 self.get_version().map_err(|e| match e {
200 CliError::Io(err) => PyIOError::new_err(err.to_string()),
201 CliError::Utf8Error(err) => PyValueError::new_err(err.to_string()),
202 CliError::UnknownWorkingDirectory => PyValueError::new_err("Unknown working directory"),
203 CliError::MalformedRepoName(err) => {
204 PyValueError::new_err(format!("Repository named malformed: {err}"))
205 }
206 })
207 }
208
209 #[getter("author")]
210 pub fn get_author_py(&self) -> PyResult<String> {
211 use pyo3::exceptions::{PyIOError, PyValueError};
212
213 self.get_author().map_err(|e| match e {
214 CliError::Io(err) => PyIOError::new_err(err.to_string()),
215 CliError::Utf8Error(err) => PyValueError::new_err(err.to_string()),
216 CliError::UnknownWorkingDirectory => PyValueError::new_err("Unknown working directory"),
217 CliError::MalformedRepoName(err) => {
218 PyValueError::new_err(format!("Repository named malformed: {err}"))
219 }
220 })
221 }
222
223 #[getter("title")]
224 pub fn get_title_py(&self) -> PyResult<String> {
225 use pyo3::exceptions::{PyIOError, PyValueError};
226
227 self.get_title().map_err(|e| match e {
228 CliError::Io(err) => PyIOError::new_err(err.to_string()),
229 CliError::Utf8Error(err) => PyValueError::new_err(err.to_string()),
230 CliError::UnknownWorkingDirectory => PyValueError::new_err("Unknown working directory"),
231 CliError::MalformedRepoName(err) => {
232 PyValueError::new_err(format!("Repository named malformed: {err}"))
233 }
234 })
235 }
236
237 #[setter]
238 pub fn set_version(&mut self, value: Option<String>) {
239 self.version(value);
240 }
241
242 #[setter]
243 pub fn set_author(&mut self, value: Option<String>) {
244 self.author(value);
245 }
246
247 #[setter]
248 pub fn set_title(&mut self, value: Option<String>) {
249 self.title(value);
250 }
251
252 pub fn __repr__(&self) -> String {
253 format!("{self:?}")
254 }
255
256 #[new]
257 pub fn new() -> Self {
258 Self::default()
259 }
260}
261
262#[cfg(test)]
263mod test {
264 use super::{CliArgs, CliError, GH_ACTOR, GH_REF, GH_REPO, GH_SHA};
265 use std::env;
266
267 #[test]
268 fn version() {
269 unsafe {
270 env::remove_var(GH_REF);
271 env::remove_var(GH_SHA);
272 }
273 let mut args = CliArgs::default();
274 args.version(Some(args.get_version().unwrap()));
275 assert!(!args.get_version().unwrap().is_empty());
276 }
277
278 #[test]
279 fn version_ci_push() {
280 let sha = "DEADBEEF";
281 unsafe {
282 env::set_var(GH_REF, "");
283 env::set_var(GH_SHA, sha);
284 }
285 let args = CliArgs::default();
286 assert!(sha.ends_with(&args.get_version().unwrap()));
287 }
288
289 #[test]
290 fn version_ci_tag() {
291 let tag = "v1.2.3";
292 unsafe {
293 env::set_var(GH_REF, format!("refs/tags/{tag}").as_str());
294 env::remove_var(GH_SHA);
295 }
296 let args = CliArgs::default();
297 assert_eq!(args.get_version().unwrap().as_str(), tag);
298 }
299
300 #[test]
301 fn version_ci_default() {
302 let tag = "x0x.y0y";
303 unsafe {
304 env::set_var(GH_REF, "");
305 env::remove_var(GH_SHA);
306 }
307 let args = CliArgs::default();
308 assert_eq!(args.get_version().unwrap().as_str(), tag);
309 }
310
311 #[test]
312 fn author() {
313 unsafe {
314 env::remove_var(GH_ACTOR);
315 }
316 let mut args = CliArgs::default();
317 args.author(Some(args.get_author().unwrap()));
318 assert!(!args.get_author().unwrap().is_empty());
321 }
322
323 #[test]
324 fn author_ci() {
325 let author = "2bndy5";
326 unsafe {
327 env::set_var(GH_ACTOR, author);
328 }
329 let args = CliArgs::default();
330 assert_eq!(args.get_author().unwrap().as_str(), author);
331 }
332
333 #[test]
334 fn title() {
335 unsafe {
336 env::remove_var(GH_REPO);
337 }
338 let mut args = CliArgs::default();
339 args.title(Some(args.get_title().unwrap()));
340 assert_eq!(
341 args.get_title().unwrap().as_str(),
342 env::current_dir()
343 .unwrap()
344 .file_name()
345 .unwrap()
346 .to_str()
347 .unwrap()
348 );
349 }
350
351 #[test]
352 fn title_ci() {
353 unsafe {
354 env::set_var(GH_REPO, "2bndy5/rmskin-action");
355 }
356 let args = CliArgs::default();
357 assert_eq!(args.get_title().unwrap(), "rmskin-action".to_string());
358 }
359
360 #[test]
361 fn title_ci_bad() {
362 let bad_repo_name = "2bndy5\\rmskin-action";
363 unsafe {
364 env::set_var(GH_REPO, bad_repo_name);
365 }
366 let args = CliArgs::default();
367 let title = args.get_title();
368 assert!(title.is_err());
369 if let Err(CliError::MalformedRepoName(bad_name)) = title {
370 assert_eq!(&bad_name, bad_repo_name);
371 }
372 }
373}