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}