1use anyhow::{self, Result, bail};
5use clap::{ArgAction, Parser};
6use std::env;
7use std::path::PathBuf;
8use std::str::FromStr;
9use thiserror::Error;
10
11#[derive(Parser)]
18#[command(author, version, rename_all = "kebab-case")]
19pub(crate) struct Args {
20    #[arg(short, long)]
23    pub feature: String,
24
25    pub root: Option<PathBuf>,
29
30    #[arg(short, long = "dir")]
39    pub directories: Vec<Directory>,
40
41    #[arg(short, long = "package")]
44    pub packages: Vec<String>,
45
46    #[arg(long="no-workspace-update", action=ArgAction::SetFalse)]
48    pub workspace_update: bool,
49
50    #[arg(long)]
52    pub dry_run: bool,
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub(crate) struct Directory {
57    pub src: PathBuf,
58    pub dst: PathBuf,
59    pub suffix: Option<String>,
60}
61
62#[derive(Error, Debug)]
63pub(crate) enum DirectoryParseError {
64    #[error("Can't parse an existing source directory from '{0}'")]
65    NoSrc(String),
66
67    #[error("Can't parse a destination directory from '{0}'")]
68    NoDst(String),
69}
70
71impl FromStr for Directory {
72    type Err = anyhow::Error;
73
74    fn from_str(s: &str) -> Result<Self> {
75        let mut parts = s.split(':');
76
77        let Some(src_part) = parts.next() else {
78            bail!(DirectoryParseError::NoSrc(s.to_string()))
79        };
80
81        let Some(dst_part) = parts.next() else {
82            bail!(DirectoryParseError::NoDst(s.to_string()))
83        };
84
85        let suffix = parts.next().map(|sfx| sfx.to_string());
86
87        let cwd = env::current_dir()?;
88        let src = cwd.join(src_part);
89        let dst = cwd.join(dst_part);
90
91        if !src.is_dir() {
92            bail!(DirectoryParseError::NoSrc(src_part.to_string()));
93        }
94
95        Ok(Self { src, dst, suffix })
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use expect_test::expect;
102
103    use super::*;
104
105    #[test]
106    fn test_directory_parsing_everything() {
107        let dir = Directory::from_str("src:dst:suffix").unwrap();
109
110        let cwd = env::current_dir().unwrap();
111        let src = cwd.join("src");
112        let dst = cwd.join("dst");
113
114        assert_eq!(
115            dir,
116            Directory {
117                src,
118                dst,
119                suffix: Some("suffix".to_string()),
120            }
121        )
122    }
123
124    #[test]
125    fn test_directory_parsing_no_suffix() {
126        let dir = Directory::from_str("src:dst").unwrap();
128
129        let cwd = env::current_dir().unwrap();
130        let src = cwd.join("src");
131        let dst = cwd.join("dst");
132
133        assert_eq!(
134            dir,
135            Directory {
136                src,
137                dst,
138                suffix: None,
139            }
140        )
141    }
142
143    #[test]
144    fn test_directory_parsing_no_dst() {
145        let err = Directory::from_str("src").unwrap_err();
147        expect!["Can't parse a destination directory from 'src'"].assert_eq(&format!("{err}"));
148    }
149
150    #[test]
151    fn test_directory_parsing_src_non_existent() {
152        let err = Directory::from_str("i_dont_exist:dst").unwrap_err();
154        expect!["Can't parse an existing source directory from 'i_dont_exist'"]
155            .assert_eq(&format!("{err}"));
156    }
157
158    #[test]
159    fn test_directory_parsing_empty() {
160        let err = Directory::from_str("").unwrap_err();
162        expect!["Can't parse a destination directory from ''"].assert_eq(&format!("{err}"));
163    }
164}