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))]
47#[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(
53 feature = "py-binding",
54 pyclass(module = "rmskin_builder", from_py_object)
55)]
56pub struct CliArgs {
57 #[cfg_attr(feature = "clap", arg(short, long, default_value = "./"))]
62 pub path: Option<PathBuf>,
63
64 #[cfg_attr(feature = "clap", arg(short = 'V', long))]
68 version: Option<String>,
69
70 #[cfg_attr(feature = "clap", arg(short, long, verbatim_doc_comment))]
77 author: Option<String>,
78
79 #[cfg_attr(feature = "clap", arg(short, long, verbatim_doc_comment))]
85 title: Option<String>,
86
87 #[cfg_attr(
91 feature = "clap",
92 arg(short, long, alias = "dir_out", default_value = "./")
93 )]
94 pub dir_out: Option<PathBuf>,
95}
96
97const GH_REPO: &str = "GITHUB_REPOSITORY";
98const GH_REF: &str = "GITHUB_REF";
99const GH_SHA: &str = "GITHUB_SHA";
100const GH_ACTOR: &str = "GITHUB_ACTOR";
101
102impl CliArgs {
103 pub fn get_version(&self) -> Result<String, CliError> {
107 if let Some(version) = &self.version {
108 return Ok(version.clone());
109 }
110 if let Ok(gh_ref) = env::var(GH_REF) {
111 if let Some(stripped) = gh_ref.strip_prefix("refs/tags/") {
112 Ok(stripped.to_string())
113 } else if let Ok(gh_sha) = env::var(GH_SHA) {
114 let len = gh_sha.len().saturating_sub(7);
115 Ok(gh_sha[len..].to_string())
116 } else {
117 Ok("x0x.y0y".to_string())
118 }
119 } else {
120 if let Ok(result) = Command::new("git").args(["describe", "--tags"]).output() {
123 Ok(String::from_utf8(result.stdout.trim_ascii().to_vec())?)
124 } else {
125 let result = Command::new("git")
126 .args(["log", "-1", "--format=\"%h\""])
127 .output()?;
128 Ok(String::from_utf8(result.stdout.trim_ascii().to_vec())?)
129 }
130 }
131 }
132
133 pub fn get_author(&self) -> Result<String, CliError> {
140 if let Some(author) = &self.author {
141 Ok(author.clone())
142 } else if let Ok(actor) = env::var(GH_ACTOR) {
143 Ok(actor)
144 } else {
145 let result = Command::new("git")
146 .args(["config", "get", "user.name"])
147 .output()?;
148 Ok(String::from_utf8(result.stdout.trim_ascii().to_vec())?)
149 }
150 }
151
152 pub fn get_title(&self) -> Result<String, CliError> {
158 if let Some(title) = &self.title {
159 Ok(title.to_owned())
160 } else {
161 if let Ok(mut repo) = env::var(GH_REPO) {
162 let divider = repo
163 .find('/')
164 .ok_or(CliError::MalformedRepoName(repo.to_owned()))?
165 + 1;
166 return Ok(repo.split_off(divider));
167 }
168 let curr_dir = env::current_dir()?;
169 Ok(curr_dir
170 .file_name()
171 .ok_or(CliError::UnknownWorkingDirectory)?
172 .to_string_lossy()
173 .to_string())
174 }
175 }
176}
177
178impl CliArgs {
179 pub fn version(&mut self, value: Option<String>) {
181 self.version = value;
182 }
183
184 pub fn author(&mut self, value: Option<String>) {
186 self.author = value;
187 }
188
189 pub fn title(&mut self, value: Option<String>) {
191 self.title = value;
192 }
193}
194
195#[cfg(feature = "py-binding")]
196#[cfg_attr(feature = "py-binding", pymethods)]
197impl CliArgs {
198 #[getter("version")]
199 pub fn get_version_py(&self) -> PyResult<String> {
200 use pyo3::exceptions::{PyIOError, PyValueError};
201
202 self.get_version().map_err(|e| match e {
203 CliError::Io(err) => PyIOError::new_err(err.to_string()),
204 CliError::Utf8Error(err) => PyValueError::new_err(err.to_string()),
205 CliError::UnknownWorkingDirectory => PyValueError::new_err("Unknown working directory"),
206 CliError::MalformedRepoName(err) => {
207 PyValueError::new_err(format!("Repository named malformed: {err}"))
208 }
209 })
210 }
211
212 #[getter("author")]
213 pub fn get_author_py(&self) -> PyResult<String> {
214 use pyo3::exceptions::{PyIOError, PyValueError};
215
216 self.get_author().map_err(|e| match e {
217 CliError::Io(err) => PyIOError::new_err(err.to_string()),
218 CliError::Utf8Error(err) => PyValueError::new_err(err.to_string()),
219 CliError::UnknownWorkingDirectory => PyValueError::new_err("Unknown working directory"),
220 CliError::MalformedRepoName(err) => {
221 PyValueError::new_err(format!("Repository named malformed: {err}"))
222 }
223 })
224 }
225
226 #[getter("title")]
227 pub fn get_title_py(&self) -> PyResult<String> {
228 use pyo3::exceptions::{PyIOError, PyValueError};
229
230 self.get_title().map_err(|e| match e {
231 CliError::Io(err) => PyIOError::new_err(err.to_string()),
232 CliError::Utf8Error(err) => PyValueError::new_err(err.to_string()),
233 CliError::UnknownWorkingDirectory => PyValueError::new_err("Unknown working directory"),
234 CliError::MalformedRepoName(err) => {
235 PyValueError::new_err(format!("Repository named malformed: {err}"))
236 }
237 })
238 }
239
240 #[setter]
241 pub fn set_version(&mut self, value: Option<String>) {
242 self.version(value);
243 }
244
245 #[setter]
246 pub fn set_author(&mut self, value: Option<String>) {
247 self.author(value);
248 }
249
250 #[setter]
251 pub fn set_title(&mut self, value: Option<String>) {
252 self.title(value);
253 }
254
255 pub fn __repr__(&self) -> String {
256 format!("{self:?}")
257 }
258
259 #[new]
260 pub fn new() -> Self {
261 Self::default()
262 }
263}
264
265#[cfg(test)]
266mod test {
267 use super::{CliArgs, CliError, GH_ACTOR, GH_REF, GH_REPO, GH_SHA};
268 use std::env;
269
270 #[test]
271 fn version() {
272 unsafe {
273 env::remove_var(GH_REF);
274 env::remove_var(GH_SHA);
275 }
276 let mut args = CliArgs::default();
277 args.version(Some(args.get_version().unwrap()));
278 assert!(!args.get_version().unwrap().is_empty());
279 }
280
281 #[test]
282 fn version_ci_push() {
283 let sha = "DEADBEEF";
284 unsafe {
285 env::set_var(GH_REF, "");
286 env::set_var(GH_SHA, sha);
287 }
288 let args = CliArgs::default();
289 assert!(sha.ends_with(&args.get_version().unwrap()));
290 }
291
292 #[test]
293 fn version_ci_tag() {
294 let tag = "v1.2.3";
295 unsafe {
296 env::set_var(GH_REF, format!("refs/tags/{tag}").as_str());
297 env::remove_var(GH_SHA);
298 }
299 let args = CliArgs::default();
300 assert_eq!(args.get_version().unwrap().as_str(), tag);
301 }
302
303 #[test]
304 fn version_ci_default() {
305 let tag = "x0x.y0y";
306 unsafe {
307 env::set_var(GH_REF, "");
308 env::remove_var(GH_SHA);
309 }
310 let args = CliArgs::default();
311 assert_eq!(args.get_version().unwrap().as_str(), tag);
312 }
313
314 #[test]
315 fn author() {
316 unsafe {
317 env::remove_var(GH_ACTOR);
318 }
319 let mut args = CliArgs::default();
320 args.author(Some(args.get_author().unwrap()));
321 assert!(!args.get_author().unwrap().is_empty());
324 }
325
326 #[test]
327 fn author_ci() {
328 let author = "2bndy5";
329 unsafe {
330 env::set_var(GH_ACTOR, author);
331 }
332 let args = CliArgs::default();
333 assert_eq!(args.get_author().unwrap().as_str(), author);
334 }
335
336 #[test]
337 fn title() {
338 unsafe {
339 env::remove_var(GH_REPO);
340 }
341 let mut args = CliArgs::default();
342 args.title(Some(args.get_title().unwrap()));
343 assert_eq!(
344 args.get_title().unwrap().as_str(),
345 env::current_dir()
346 .unwrap()
347 .file_name()
348 .unwrap()
349 .to_str()
350 .unwrap()
351 );
352 }
353
354 #[test]
355 fn title_ci() {
356 unsafe {
357 env::set_var(GH_REPO, "2bndy5/rmskin-action");
358 }
359 let args = CliArgs::default();
360 assert_eq!(args.get_title().unwrap(), "rmskin-action".to_string());
361 }
362
363 #[test]
364 fn title_ci_bad() {
365 let bad_repo_name = "2bndy5\\rmskin-action";
366 unsafe {
367 env::set_var(GH_REPO, bad_repo_name);
368 }
369 let args = CliArgs::default();
370 let title = args.get_title();
371 assert!(title.is_err());
372 if let Err(CliError::MalformedRepoName(bad_name)) = title {
373 assert_eq!(&bad_name, bad_repo_name);
374 }
375 }
376}