1use serde::de::{self, MapAccess, Visitor};
2use serde::{Deserialize, Deserializer};
3use std::fmt;
4
5#[derive(Debug, Clone, Deserialize, Default)]
7pub struct Target {
8 pub selector: Option<String>,
10 pub text: Option<String>,
12}
13
14impl fmt::Display for Target {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 match (&self.selector, &self.text) {
17 (Some(s), _) => write!(f, "selector '{}'", s),
18 (_, Some(t)) => write!(f, "text '{}'", t),
19 _ => write!(f, "unknown"),
20 }
21 }
22}
23
24#[derive(Debug, Clone)]
26pub enum Action {
27 Goto(GotoAction),
29 Back,
30 Forward,
31 Reload,
32
33 Wait(WaitAction),
35 WaitForNetworkIdle(WaitForNetworkIdleAction),
36 WaitFor(WaitForAction),
37 WaitForVisible(WaitForAction),
38 WaitForHidden(WaitForAction),
39 WaitForText(WaitForTextAction),
40 WaitForUrl(WaitForUrlAction),
41 WaitForEmail(WaitForEmailAction),
42
43 Click(ClickAction),
45 TryClick(TargetAction),
46 TryClickAny(TryClickAnyAction),
47
48 Fill(FillAction),
50 Type(TypeAction),
51 Clear(ClearAction),
52 Select(SelectAction),
53 PressKey(PressKeyAction),
54
55 Hover(TargetAction),
57
58 SetCookie(SetCookieAction),
60 DeleteCookie(DeleteCookieAction),
61
62 Execute(ExecuteAction),
64
65 Scroll(ScrollAction),
67 ScrollTo(TargetAction),
68
69 Screenshot(ScreenshotAction),
71 Log(LogAction),
72 AssertText(AssertTextAction),
73 AssertUrl(AssertUrlAction),
74
75 IfTextExists(IfTextExistsAction),
77 IfSelectorExists(IfSelectorExistsAction),
78 Repeat(RepeatAction),
79
80 Include(IncludeAction),
82}
83
84impl Action {
85 pub fn name(&self) -> &'static str {
87 match self {
88 Self::Goto(_) => "goto",
89 Self::Back => "back",
90 Self::Forward => "forward",
91 Self::Reload => "reload",
92 Self::Wait(_) => "wait",
93 Self::WaitForNetworkIdle(_) => "wait_for_network_idle",
94 Self::WaitFor(_) => "wait_for",
95 Self::WaitForVisible(_) => "wait_for_visible",
96 Self::WaitForHidden(_) => "wait_for_hidden",
97 Self::WaitForText(_) => "wait_for_text",
98 Self::WaitForUrl(_) => "wait_for_url",
99 Self::WaitForEmail(_) => "wait_for_email",
100 Self::Click(_) => "click",
101 Self::TryClick(_) => "try_click",
102 Self::TryClickAny(_) => "try_click_any",
103 Self::Fill(_) => "fill",
104 Self::Type(_) => "type",
105 Self::Clear(_) => "clear",
106 Self::Select(_) => "select",
107 Self::PressKey(_) => "press_key",
108 Self::Hover(_) => "hover",
109 Self::SetCookie(_) => "set_cookie",
110 Self::DeleteCookie(_) => "delete_cookie",
111 Self::Execute(_) => "execute",
112 Self::Scroll(_) => "scroll",
113 Self::ScrollTo(_) => "scroll_to",
114 Self::Screenshot(_) => "screenshot",
115 Self::Log(_) => "log",
116 Self::AssertText(_) => "assert_text",
117 Self::AssertUrl(_) => "assert_url",
118 Self::IfTextExists(_) => "if_text_exists",
119 Self::IfSelectorExists(_) => "if_selector_exists",
120 Self::Repeat(_) => "repeat",
121 Self::Include(_) => "include",
122 }
123 }
124}
125
126const ACTION_NAMES: &[&str] = &[
127 "goto",
128 "back",
129 "forward",
130 "reload",
131 "wait",
132 "wait_for_network_idle",
133 "wait_for",
134 "wait_for_visible",
135 "wait_for_hidden",
136 "wait_for_text",
137 "wait_for_url",
138 "wait_for_email",
139 "click",
140 "try_click",
141 "try_click_any",
142 "fill",
143 "type",
144 "clear",
145 "select",
146 "press_key",
147 "hover",
148 "set_cookie",
149 "delete_cookie",
150 "execute",
151 "scroll",
152 "scroll_to",
153 "screenshot",
154 "log",
155 "assert_text",
156 "assert_url",
157 "if_text_exists",
158 "if_selector_exists",
159 "repeat",
160 "include",
161];
162
163impl<'de> Deserialize<'de> for Action {
164 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
165 where
166 D: Deserializer<'de>,
167 {
168 deserializer.deserialize_any(ActionVisitor)
169 }
170}
171
172struct ActionVisitor;
173
174impl<'de> Visitor<'de> for ActionVisitor {
175 type Value = Action;
176
177 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
178 formatter.write_str("an action (string for unit variants, or map with single key)")
179 }
180
181 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
182 where
183 E: de::Error,
184 {
185 match value {
186 "back" => Ok(Action::Back),
187 "forward" => Ok(Action::Forward),
188 "reload" => Ok(Action::Reload),
189 other => Err(de::Error::unknown_variant(
190 other,
191 &["back", "forward", "reload"],
192 )),
193 }
194 }
195
196 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
197 where
198 M: MapAccess<'de>,
199 {
200 let key: String = map
201 .next_key()?
202 .ok_or_else(|| de::Error::custom("expected action type key"))?;
203
204 let action = match key.as_str() {
205 "goto" => Action::Goto(map.next_value()?),
206 "back" => {
207 let _: serde_yaml::Value = map.next_value()?;
208 Action::Back
209 }
210 "forward" => {
211 let _: serde_yaml::Value = map.next_value()?;
212 Action::Forward
213 }
214 "reload" => {
215 let _: serde_yaml::Value = map.next_value()?;
216 Action::Reload
217 }
218 "wait" => Action::Wait(map.next_value()?),
219 "wait_for_network_idle" => Action::WaitForNetworkIdle(map.next_value()?),
220 "wait_for" => Action::WaitFor(map.next_value()?),
221 "wait_for_visible" => Action::WaitForVisible(map.next_value()?),
222 "wait_for_hidden" => Action::WaitForHidden(map.next_value()?),
223 "wait_for_text" => Action::WaitForText(map.next_value()?),
224 "wait_for_url" => Action::WaitForUrl(map.next_value()?),
225 "wait_for_email" => Action::WaitForEmail(map.next_value()?),
226 "click" => Action::Click(map.next_value()?),
227 "try_click" => Action::TryClick(map.next_value()?),
228 "try_click_any" => Action::TryClickAny(map.next_value()?),
229 "fill" => Action::Fill(map.next_value()?),
230 "type" => Action::Type(map.next_value()?),
231 "clear" => Action::Clear(map.next_value()?),
232 "select" => Action::Select(map.next_value()?),
233 "press_key" => Action::PressKey(map.next_value()?),
234 "hover" => Action::Hover(map.next_value()?),
235 "set_cookie" => Action::SetCookie(map.next_value()?),
236 "delete_cookie" => Action::DeleteCookie(map.next_value()?),
237 "execute" => Action::Execute(map.next_value()?),
238 "scroll" => Action::Scroll(map.next_value()?),
239 "scroll_to" => Action::ScrollTo(map.next_value()?),
240 "screenshot" => Action::Screenshot(map.next_value()?),
241 "log" => Action::Log(map.next_value()?),
242 "assert_text" => Action::AssertText(map.next_value()?),
243 "assert_url" => Action::AssertUrl(map.next_value()?),
244 "if_text_exists" => Action::IfTextExists(map.next_value()?),
245 "if_selector_exists" => Action::IfSelectorExists(map.next_value()?),
246 "repeat" => Action::Repeat(map.next_value()?),
247 "include" => Action::Include(map.next_value()?),
248 other => return Err(de::Error::unknown_variant(other, ACTION_NAMES)),
249 };
250
251 Ok(action)
252 }
253}
254
255#[derive(Debug, Clone, Deserialize)]
258pub struct GotoAction {
259 pub url: String,
260}
261
262#[derive(Debug, Clone, Deserialize)]
263pub struct WaitAction {
264 pub ms: u64,
265}
266
267fn default_idle_ms() -> u64 {
268 500
269}
270fn default_timeout_ms() -> u64 {
271 10000
272}
273
274#[derive(Debug, Clone, Deserialize)]
275pub struct WaitForNetworkIdleAction {
276 #[serde(default = "default_idle_ms")]
277 pub idle_ms: u64,
278 #[serde(default = "default_timeout_ms")]
279 pub timeout_ms: u64,
280}
281
282#[derive(Debug, Clone, Deserialize)]
283pub struct WaitForAction {
284 pub selector: String,
285 #[serde(default = "default_timeout_ms")]
286 pub timeout_ms: u64,
287}
288
289#[derive(Debug, Clone, Deserialize)]
290pub struct WaitForTextAction {
291 pub text: String,
292 #[serde(default = "default_timeout_ms")]
293 pub timeout_ms: u64,
294}
295
296#[derive(Debug, Clone, Deserialize)]
297pub struct WaitForUrlAction {
298 pub contains: String,
299 #[serde(default = "default_timeout_ms")]
300 pub timeout_ms: u64,
301}
302
303#[derive(Debug, Clone, Deserialize)]
304pub struct ImapConfigAction {
305 pub host: String,
306 #[serde(default = "ImapConfigAction::default_port")]
307 pub port: u16,
308 #[serde(default = "ImapConfigAction::default_tls")]
309 pub tls: bool,
310 pub username: String,
311 pub password: String,
312 #[serde(default = "ImapConfigAction::default_mailbox")]
313 pub mailbox: String,
314}
315
316impl ImapConfigAction {
317 fn default_port() -> u16 { 993 }
318 fn default_tls() -> bool { true }
319 fn default_mailbox() -> String { "INBOX".into() }
320}
321
322#[derive(Debug, Clone, Deserialize)]
323pub struct EmailFilterAction {
324 pub from: Option<String>,
325 pub subject_contains: Option<String>,
326 #[serde(default = "EmailFilterAction::default_unseen_only")]
327 pub unseen_only: bool,
328 pub since_minutes: Option<i64>,
329 #[serde(default)]
330 pub mark_seen: bool,
331}
332
333impl EmailFilterAction {
334 fn default_unseen_only() -> bool { true }
335}
336
337impl Default for EmailFilterAction {
338 fn default() -> Self {
339 Self {
340 from: None,
341 subject_contains: None,
342 unseen_only: true,
343 since_minutes: None,
344 mark_seen: false,
345 }
346 }
347}
348
349#[derive(Debug, Clone, Deserialize)]
350pub struct WaitForEmailAction {
351 pub imap: ImapConfigAction,
352 #[serde(default)]
353 pub filter: EmailFilterAction,
354 #[serde(default = "WaitForEmailAction::default_timeout_ms")]
355 pub timeout_ms: u64,
356 #[serde(default = "WaitForEmailAction::default_poll_interval_ms")]
357 pub poll_interval_ms: u64,
358 #[serde(default)]
359 pub extract: EmailExtractAction,
360 #[serde(default)]
361 pub action: Option<EmailAction>,
362}
363
364impl WaitForEmailAction {
365 fn default_timeout_ms() -> u64 { 120_000 }
366 fn default_poll_interval_ms() -> u64 { 2_000 }
367}
368
369#[derive(Debug, Clone, Deserialize, Default)]
370pub struct EmailExtractAction {
371 pub link: Option<EmailLinkExtract>,
372 pub code: Option<EmailCodeExtract>,
373}
374
375#[derive(Debug, Clone, Deserialize)]
376pub struct EmailLinkExtract {
377 pub allow_domains: Option<Vec<String>>,
378}
379
380#[derive(Debug, Clone, Deserialize)]
381pub struct EmailCodeExtract {
382 pub regex: String,
383}
384
385#[derive(Debug, Clone, Deserialize)]
386#[serde(rename_all = "snake_case")]
387pub enum EmailAction {
388 OpenLink(EmailOpenLinkAction),
389 Fill(EmailFillAction),
390}
391
392#[derive(Debug, Clone, Default)]
394pub struct EmailOpenLinkAction;
395
396impl<'de> Deserialize<'de> for EmailOpenLinkAction {
397 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
398 where
399 D: Deserializer<'de>,
400 {
401 let v = serde_yaml::Value::deserialize(deserializer)?;
403 match v {
404 serde_yaml::Value::Null | serde_yaml::Value::Mapping(_) => Ok(Self),
405 _ => Err(serde::de::Error::custom("expected null or empty map for open_link")),
406 }
407 }
408}
409
410#[derive(Debug, Clone, Deserialize)]
411pub struct EmailFillAction {
412 pub selector: String,
413}
414
415#[derive(Debug, Clone, Deserialize)]
416pub struct ClickAction {
417 #[serde(flatten)]
418 pub target: Target,
419 #[serde(default)]
420 pub human: bool,
421 #[serde(default)]
422 pub scroll_into_view: bool,
423}
424
425#[derive(Debug, Clone, Deserialize)]
426pub struct TryClickAnyAction {
427 pub selectors: Option<Vec<String>>,
428 pub texts: Option<Vec<String>>,
429}
430
431#[derive(Debug, Clone, Deserialize)]
432pub struct FillAction {
433 #[serde(flatten)]
434 pub target: Target,
435 pub value: String,
436 #[serde(default)]
437 pub human: bool,
438}
439
440#[derive(Debug, Clone, Deserialize)]
441pub struct TypeAction {
442 #[serde(flatten)]
443 pub target: Target,
444 pub value: String,
445}
446
447#[derive(Debug, Clone, Deserialize)]
448pub struct ClearAction {
449 #[serde(flatten)]
450 pub target: Target,
451}
452
453#[derive(Debug, Clone, Deserialize)]
454pub struct SelectAction {
455 #[serde(flatten)]
456 pub target: Target,
457 pub value: String,
458}
459
460#[derive(Debug, Clone, Deserialize)]
461pub struct PressKeyAction {
462 pub key: String,
463}
464
465#[derive(Debug, Clone, Deserialize)]
467pub struct TargetAction {
468 #[serde(flatten)]
469 pub target: Target,
470}
471
472#[derive(Debug, Clone, Deserialize)]
473pub struct SetCookieAction {
474 pub name: String,
475 pub value: String,
476 pub domain: Option<String>,
477 pub path: Option<String>,
478}
479
480#[derive(Debug, Clone, Deserialize)]
481pub struct DeleteCookieAction {
482 pub name: String,
483 pub domain: Option<String>,
484}
485
486#[derive(Debug, Clone, Deserialize)]
487pub struct ExecuteAction {
488 pub js: String,
489}
490
491fn default_scroll_amount() -> u32 {
492 1
493}
494
495#[derive(Debug, Clone, Deserialize)]
496pub struct ScrollAction {
497 pub direction: ScrollDirection,
498 #[serde(default = "default_scroll_amount")]
499 pub amount: u32,
500}
501
502#[derive(Debug, Clone, Deserialize)]
503#[serde(rename_all = "snake_case")]
504pub enum ScrollDirection {
505 Up,
506 Down,
507 Left,
508 Right,
509}
510
511#[derive(Debug, Clone, Deserialize)]
512pub struct ScreenshotAction {
513 pub path: String,
514}
515
516#[derive(Debug, Clone, Deserialize)]
517pub struct LogAction {
518 pub message: String,
519}
520
521#[derive(Debug, Clone, Deserialize)]
522pub struct AssertTextAction {
523 pub text: String,
524}
525
526#[derive(Debug, Clone, Deserialize)]
527pub struct AssertUrlAction {
528 pub contains: String,
529}
530
531#[derive(Debug, Clone, Deserialize)]
532pub struct IfTextExistsAction {
533 pub text: String,
534 #[serde(rename = "then")]
535 pub then_actions: Vec<Action>,
536 #[serde(rename = "else", default)]
537 pub else_actions: Vec<Action>,
538}
539
540#[derive(Debug, Clone, Deserialize)]
541pub struct IfSelectorExistsAction {
542 pub selector: String,
543 #[serde(rename = "then")]
544 pub then_actions: Vec<Action>,
545 #[serde(rename = "else", default)]
546 pub else_actions: Vec<Action>,
547}
548
549#[derive(Debug, Clone, Deserialize)]
550pub struct RepeatAction {
551 pub times: u32,
552 pub actions: Vec<Action>,
553}
554
555#[derive(Debug, Clone, Deserialize)]
557pub struct IncludeAction {
558 pub path: String,
560
561 #[serde(default)]
563 pub params: std::collections::HashMap<String, String>,
564}