1use clap::Parser;
2use futures_util::pin_mut;
3use futures_util::StreamExt;
4use regex::Regex;
5
6fn validate_date(val: &str) -> Result<String, String> {
7 let datetime_regex = Regex::new(
8 r"^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$",
9 )
10 .unwrap();
11
12 if datetime_regex.is_match(val) {
13 Ok(val.to_string())
14 } else {
15 Err(String::from("Invalid date format. Use YYYY-MM-DDTHH:MM:SS plus optional fractional seconds plus an optional timezone specifier in the form Z, +XX, -XX, +XX:XX, or -XX:XX (cf. <https://core.trac.wordpress.org/ticket/41032>)."))
16 }
17}
18
19#[derive(Parser)]
23#[command(name = "pf", author, version, about)]
24pub struct Opt {
25 pub url: String,
27
28 #[arg(long, value_parser=validate_date)]
30 pub before: Option<String>,
31
32 #[arg(long, value_parser=validate_date)]
34 pub modified_before: Option<String>,
35
36 #[arg(long, value_parser=validate_date)]
38 pub after: Option<String>,
39
40 #[arg(long, value_parser=validate_date)]
42 pub modified_after: Option<String>,
43
44 #[arg(long)]
46 pub exclude: Vec<u16>,
47
48 #[arg(long)]
50 pub categories_exclude: Vec<u16>,
51
52 #[arg(long)]
54 pub tags_exclude: Vec<u16>,
55}
56
57impl Opt {
58 fn to_finder_config(&self) -> pf_lib::FinderConfig {
60 pf_lib::FinderConfig {
61 url: self.url.clone(),
62 target: pf_lib::FinderTarget::Posts {
63 categories_exclude: self.categories_exclude.clone(),
64 tags_exclude: self.tags_exclude.clone(),
65 },
66 before: self.before.clone(),
67 modified_before: self.modified_before.clone(),
68 after: self.after.clone(),
69 modified_after: self.modified_after.clone(),
70 exclude: self.exclude.clone(),
71 }
72 }
73}
74
75pub async fn run(opt: Opt) -> Result<(), Box<dyn std::error::Error>> {
77 let mut config = opt.to_finder_config();
78 print_stream(&config).await?;
79 config.target = pf_lib::FinderTarget::Media;
80 print_stream(&config).await?;
81 Ok(())
82}
83
84async fn print_stream(config: &pf_lib::FinderConfig) -> Result<(), Box<dyn std::error::Error>> {
86 let stream = pf_lib::find(config);
87 pin_mut!(stream);
88 while let Some(res) = stream.next().await {
89 match res {
90 Ok(url) => println!("{}", url),
91 Err(e) => eprintln!("{}", e),
92 }
93 }
94 Ok(())
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn test_validate_date() {
103 assert!(validate_date("2023-01-01T00:00:00").is_ok());
104 assert!(validate_date("2023-01-01T00:00:00Z").is_ok());
105 assert!(validate_date("2023-01-01T00:00:00+00:00").is_ok());
106 assert!(validate_date("2023-01-01T00:00:00-00:00").is_ok());
107 assert!(validate_date("2023-01-01T00:00:00+00").is_ok());
108 assert!(validate_date("2023-01-01T00:00:00-00").is_ok());
109 assert!(validate_date("2023-01-01T00:00:00+00:00").is_ok());
110 assert!(validate_date("2023-01-01T00:00:00-00:00").is_ok());
111 assert!(validate_date("2023-01-01T00:00:00.123Z").is_ok());
112 assert!(validate_date("2023-01-01T00:00:00.123+00:00").is_ok());
113 assert!(validate_date("2023-01-01T00:00:00.123-00:00").is_ok());
114 assert!(validate_date("2023-01-01T00:00:00.123+00").is_ok());
115 assert!(validate_date("2023-01-01T00:00:00.123-00").is_ok());
116 assert!(validate_date("2023-01-01T00:00:00.123+00:00").is_ok());
117 assert!(validate_date("2023-01-01T00:00:00.123-00:00").is_ok());
118 assert!(validate_date("2023-01-01").is_err());
119 assert!(validate_date("2023-01-01T00:00:00+").is_err());
120 assert!(validate_date("2023-01-01T00:00:00-").is_err());
121 assert!(validate_date("2023-01-01T00:00:00+00:").is_err());
122 assert!(validate_date("2023-01-01T00:00:00-00:").is_err());
123 assert!(validate_date("2023-01-01T00:00:00+00:00:00").is_err());
124 }
125
126 #[test]
127 fn test_opt_parsing() {
128 let args = vec![
129 "pf",
130 "http://example.com",
131 "--before",
132 "2023-01-01T00:00:00",
133 "--modified-before",
134 "2023-01-01T00:00:00",
135 "--after",
136 "2023-01-01T00:00:00",
137 "--modified-after",
138 "2023-01-01T00:00:00",
139 "--exclude",
140 "1",
141 "--exclude",
142 "2",
143 "--categories-exclude",
144 "3",
145 "--tags-exclude",
146 "4",
147 ];
148 let opt = Opt::parse_from(args);
149 assert_eq!(opt.url, "http://example.com");
150 assert_eq!(opt.before, Some("2023-01-01T00:00:00".to_string()));
151 assert_eq!(opt.modified_before, Some("2023-01-01T00:00:00".to_string()));
152 assert_eq!(opt.after, Some("2023-01-01T00:00:00".to_string()));
153 assert_eq!(opt.modified_after, Some("2023-01-01T00:00:00".to_string()));
154 assert_eq!(opt.exclude, vec![1, 2]);
155 assert_eq!(opt.categories_exclude, vec![3]);
156 assert_eq!(opt.tags_exclude, vec![4]);
157 }
158
159 #[test]
160 fn test_to_finder_config() {
161 let opt = Opt {
162 url: "http://example.com".to_string(),
163 before: Some("2023-01-01T00:00:00".to_string()),
164 modified_before: Some("2023-01-01T00:00:00".to_string()),
165 after: Some("2023-01-01T00:00:00".to_string()),
166 modified_after: Some("2023-01-01T00:00:00".to_string()),
167 exclude: vec![1, 2],
168 categories_exclude: vec![3],
169 tags_exclude: vec![4],
170 };
171 let config = opt.to_finder_config();
172 assert_eq!(config.url, "http://example.com");
173 assert_eq!(config.before, Some("2023-01-01T00:00:00".to_string()));
174 assert_eq!(
175 config.modified_before,
176 Some("2023-01-01T00:00:00".to_string())
177 );
178 assert_eq!(config.after, Some("2023-01-01T00:00:00".to_string()));
179 assert_eq!(
180 config.modified_after,
181 Some("2023-01-01T00:00:00".to_string())
182 );
183 assert_eq!(config.exclude, vec![1, 2]);
184 assert_eq!(
185 config.target,
186 pf_lib::FinderTarget::Posts {
187 categories_exclude: vec![3],
188 tags_exclude: vec![4],
189 }
190 );
191 }
192
193 #[tokio::test]
194 async fn test_print_stream() {
195 let config = pf_lib::FinderConfig {
196 url: "http://example.com".to_string(),
197 target: pf_lib::FinderTarget::Posts {
198 categories_exclude: vec![],
199 tags_exclude: vec![],
200 },
201 before: None,
202 modified_before: None,
203 after: None,
204 modified_after: None,
205 exclude: vec![],
206 };
207 let result = print_stream(&config).await;
208 assert!(result.is_ok());
209 }
210}