url_cleaner_engine/types/actions.rs
1//! Logic for how a [`TaskState`] should be modified.
2
3use std::str::{FromStr, Utf8Error};
4use std::collections::HashSet;
5use std::borrow::Cow;
6
7use serde::{Serialize, Deserialize};
8use serde_with::{serde_as, SetPreventDuplicates};
9use thiserror::Error;
10#[cfg(feature = "http")]
11use reqwest::header::HeaderMap;
12#[expect(unused_imports, reason = "Used in doc comment.")]
13use url::{Url, PathSegmentsMut};
14use ::percent_encoding::percent_decode_str as pds;
15
16use crate::glue::*;
17use crate::types::*;
18use crate::util::*;
19
20/// Actions are how [`TaskState`]s get manipulated to clean URLs.
21///
22/// Please note that, in general, when a [`Action`] returns an [`Err`], the [`TaskState`] may still be modified. For example:
23/// ```
24/// use url_cleaner_engine::types::*;
25/// url_cleaner_engine::task_state!(task_state, url = "https://example.com");
26///
27/// Action::All(vec![
28/// Action::SetPath("/change".into()),
29/// Action::Error("This won't revert the above".into()),
30/// Action::SetPath("/this-wont-happen".into())
31/// ]).apply(&mut task_state).unwrap_err();
32///
33/// assert_eq!(task_state.url, "https://example.com/change");
34/// ```
35///
36/// This is because reverting on an error requires keeping a copy of the input state, which is very expensive and, if the error is just going to be returned as the result of the [`Task`], not useful.
37///
38/// If you need to revert the [`TaskState`] when an error is returned, use [`Self::RevertOnError`] to revert the effects but still return the error, and optionally [`Self::IgnoreError`] to ignore the error.
39#[serde_as]
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Suitability)]
41#[serde(deny_unknown_fields)]
42#[serde(remote = "Self")]
43pub enum Action {
44 /// Does nothing.
45 /// # Examples
46 /// ```
47 /// use url_cleaner_engine::types::*;
48 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com");
49 ///
50 /// Action::None.apply(&mut task_state).unwrap();
51 ///
52 /// assert_eq!(task_state.url, "https://example.com/");
53 /// ```
54 None,
55 /// Always returns the error [`ActionError::ExplicitError`] with the included message.
56 /// # Errors
57 /// Always returns the error [`ActionError::ExplicitError`].
58 /// # Examples
59 /// ```
60 /// use url_cleaner_engine::types::*;
61 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com");
62 ///
63 /// Action::Error("...".into()).apply(&mut task_state).unwrap_err();
64 ///
65 /// assert_eq!(task_state.url, "https://example.com/");
66 /// ```
67 Error(String),
68 /// Prints debug info about the contained [`Self`] and the current [`TaskStateView`], then returns its return value.
69 /// # Errors
70 /// If the call to [`Self::apply`] returns an error, that error is returned after the debug info is printed.
71 #[suitable(never)]
72 Debug(Box<Self>),
73
74 /// If the call to [`Self::If::if`] passes, apply [`Self::If::then`].
75 ///
76 /// If the call to [`Self::If::if`] fails and [`Self::If::else`] is [`Some`], apply [`Self::If::else`].
77 /// # Errors
78 #[doc = edoc!(checkerr(Condition), applyerr(Self))]
79 /// # Examples
80 /// ```
81 /// use url_cleaner_engine::types::*;
82 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com");
83 ///
84 /// Action::If {
85 /// r#if : Condition::Always,
86 /// then : Box::new(Action::None),
87 /// r#else: Some(Box::new(Action::Error("...".into())))
88 /// }.apply(&mut task_state).unwrap();
89 ///
90 /// Action::If {
91 /// r#if : Condition::Never,
92 /// then : Box::new(Action::None),
93 /// r#else: Some(Box::new(Action::Error("...".into())))
94 /// }.apply(&mut task_state).unwrap_err();
95 ///
96 /// Action::If {
97 /// r#if : Condition::Always,
98 /// then : Box::new(Action::None),
99 /// r#else: None
100 /// }.apply(&mut task_state).unwrap();
101 ///
102 /// Action::If {
103 /// r#if : Condition::Never,
104 /// then : Box::new(Action::None),
105 /// r#else: None
106 /// }.apply(&mut task_state).unwrap();
107 /// ```
108 If {
109 /// The [`Condition`] to decide between [`Self::If::then`] and [`Self::If::else`].
110 r#if: Condition,
111 /// The [`Self`] to apply if [`Self::If::if`] passes.
112 then: Box<Self>,
113 /// The [`Self`] to apply if [`Self::If::if`] fails.
114 ///
115 /// Defaults to [`None`].
116 #[serde(default, skip_serializing_if = "is_default")]
117 r#else: Option<Box<Self>>
118 },
119 /// Applies the contained [`Self`]s in order.
120 ///
121 /// Please note that if one of the contained [`Self`]s returns an error, previous calls to [`Self::apply`] aren't reverted.
122 /// # Errors
123 #[doc = edoc!(applyerr(Self, 3))]
124 /// # Examples
125 /// ```
126 /// use url_cleaner_engine::types::*;
127 /// url_cleaner_engine::task_state!(task_state);
128 ///
129 /// Action::All(vec![
130 /// Action::SetHost("example2.com".into()),
131 /// Action::Error("...".into()),
132 /// Action::SetHost("example3.com".into()),
133 /// ]).apply(&mut task_state).unwrap_err();
134 ///
135 /// assert_eq!(task_state.url, "https://example2.com/");
136 /// ```
137 All(Vec<Self>),
138 /// Gets the value specified by [`Self::PartMap::part`], indexes [`Self::PartMap::map`], and applies the returned [`Self`]
139 ///
140 /// If the call to [`Map::get`] returns [`None`], does nothing..
141 /// # Errors
142 #[doc = edoc!(applyerr(Self))]
143 /// # Examples
144 /// ```
145 /// use url_cleaner_engine::types::*;
146 /// url_cleaner_engine::task_state!(task_state);
147 ///
148 /// Action::PartMap {
149 /// part: UrlPart::Host,
150 /// map: Map {
151 /// map: [
152 /// ("example.com".into(), Action::Error("...".into()))
153 /// ].into(),
154 /// if_none: None,
155 /// r#else: None
156 /// }
157 /// }.apply(&mut task_state).unwrap_err();
158 /// ```
159 PartMap {
160 /// The [`UrlPart`] to index [`Self::PartMap::map`] with.
161 part: UrlPart,
162 /// The [`Map`] to index with [`Self::PartMap::part`].
163 #[serde(flatten)]
164 map: Map<Self>
165 },
166 /// Gets the string specified by [`Self::StringMap::value`], indexes [`Self::StringMap::map`], and applies the returned [`Self`].
167 ///
168 /// If the call to [`Map::get`] returns [`None`], does nothing.
169 /// # Errors
170 #[doc = edoc!(geterr(StringSource), applyerr(Self))]
171 /// # Examples
172 /// ```
173 /// use url_cleaner_engine::types::*;
174 /// url_cleaner_engine::task_state!(task_state);
175 ///
176 /// Action::StringMap {
177 /// value: StringSource::String("a".into()),
178 /// map: Map {
179 /// map: [
180 /// ("a".into(), Action::Error("...".into()))
181 /// ].into(),
182 /// if_none: None,
183 /// r#else: None
184 /// }
185 /// }.apply(&mut task_state).unwrap_err();
186 /// ```
187 StringMap {
188 /// The [`StringSource`] to index [`Self::StringMap::map`] with.
189 value: StringSource,
190 /// The [`Map`] to index with [`Self::StringMap::value`].
191 #[serde(flatten)]
192 map: Map<Self>
193 },
194 /// Gets the name of the partition [`Self::PartNamedPartitioning::part`] is in in the specified [`NamedPartitioning`], indexes [`Self::PartNamedPartitioning::map`] with the partition name, and if the [`Map`] has a [`Self`] there, applies it.
195 /// # Errors
196 #[doc = edoc!(geterr(StringSource, 2), getnone(StringSource, Action, 2), notfound(NamedPartitioning, Action), applyerr(Self))]
197 PartNamedPartitioning {
198 /// The [`NamedPartitioning`] to search in.
199 named_partitioning: StringSource,
200 /// The [`UrlPart`] whose value to find in the [`NamedPartitioning`].
201 part: UrlPart,
202 /// The [`Map`] to index.
203 #[serde(flatten)]
204 map: Map<Self>
205 },
206 /// [`Self::PartNamedPartitioning`] but uses each [`UrlPart`] in [`Self::FirstMatchingPartNamedPartitioning`] until a match is found.
207 /// # Errors
208 #[doc = edoc!(geterr(StringSource, 2), getnone(StringSource, Action, 2), notfound(NamedPartitioning, Action), applyerr(Self))]
209 /// # Examples
210 /// ```
211 /// use std::borrow::Cow;
212 /// use url_cleaner_engine::types::*;
213 ///
214 /// url_cleaner_engine::task_state!(task_state, url = "https://abc.example.com", params = Params {
215 /// named_partitionings: Cow::Owned([
216 /// (
217 /// "a".into(),
218 /// NamedPartitioning::try_from_iter([
219 /// ("b".into(), vec![Some("example.com".into())])
220 /// ]).unwrap(),
221 /// )
222 /// ].into()),
223 /// ..Default::default()
224 /// });
225 ///
226 /// Action::FirstMatchingPartNamedPartitioning {
227 /// named_partitioning: "a".into(),
228 /// parts: vec![UrlPart::NormalizedHost, UrlPart::RegDomain],
229 /// map: [
230 /// ("b".to_string(), Action::SetPath("/123".into()))
231 /// ].into(),
232 /// }.apply(&mut task_state).unwrap();
233 ///
234 /// assert_eq!(task_state.url.path(), "/123");
235 /// ```
236 FirstMatchingPartNamedPartitioning {
237 /// The [`NamedPartitioning`] to search in.
238 named_partitioning: StringSource,
239 /// The [`UrlPart`]s whose value to find in the [`NamedPartitioning`].
240 parts: Vec<UrlPart>,
241 /// The [`Map`] to index.
242 #[serde(flatten)]
243 map: Map<Self>
244 },
245 /// Gets the name of the partition [`Self::StringNamedPartitioning::value`] is in in the specified [`NamedPartitioning`], indexes [`Self::StringNamedPartitioning::map`] with the partition name, and if the [`Map`] has a [`Self`] there, applies it.
246 /// # Errors
247 #[doc = edoc!(geterr(StringSource), getnone(StringSource, Action), notfound(NamedPartitioning, Action), applyerr(Self))]
248 StringNamedPartitioning {
249 /// The [`NamedPartitioning`] to search in.
250 named_partitioning: StringSource,
251 /// The [`StringSource`] whose value to find in the [`NamedPartitioning`].
252 value: StringSource,
253 /// The [`Map`] to index.
254 #[serde(flatten)]
255 map: Map<Self>
256 },
257 /// [`Self::StringNamedPartitioning`] but uses each [`StringSource`] in [`Self::FirstMatchingStringNamedPartitioning`] until a match is found.
258 /// # Errors
259 #[doc = edoc!(geterr(StringSource), getnone(StringSource, Action), notfound(NamedPartitioning, Action), applyerr(Self))]
260 /// # Examples
261 /// ```
262 /// use std::borrow::Cow;
263 /// use url_cleaner_engine::types::*;
264 ///
265 /// url_cleaner_engine::task_state!(task_state, url = "https://abc.example.com", params = Params {
266 /// named_partitionings: Cow::Owned([
267 /// (
268 /// "a".into(),
269 /// NamedPartitioning::try_from_iter([
270 /// ("b".into(), vec![Some("example.com".into())])
271 /// ]).unwrap(),
272 /// )
273 /// ].into()),
274 /// ..Default::default()
275 /// });
276 ///
277 /// Action::FirstMatchingStringNamedPartitioning {
278 /// named_partitioning: "a".into(),
279 /// values: vec![StringSource::Part(UrlPart::NormalizedHost), StringSource::Part(UrlPart::RegDomain)],
280 /// map: [
281 /// ("b".to_string(), Action::SetPath("/123".into()))
282 /// ].into(),
283 /// }.apply(&mut task_state).unwrap();
284 ///
285 /// assert_eq!(task_state.url.path(), "/123");
286 /// ```
287 FirstMatchingStringNamedPartitioning {
288 /// The [`NamedPartitioning`] to search in.
289 named_partitioning: StringSource,
290 /// The [`StringSource`] whose value to find in the [`NamedPartitioning`].
291 values: Vec<StringSource>,
292 /// The [`Map`] to index.
293 #[serde(flatten)]
294 map: Map<Self>
295 },
296
297
298
299 /// Repeat [`Self::Repeat::actions`] until either the [`TaskState::url`] and [`TaskState::scratchpad`] end up in the same state or the rules were executed [`Self::Repeat::limit`] times.
300 /// # Errors
301 #[doc = edoc!(applyerr(Self, 3))]
302 Repeat {
303 /// The [`Self`]s to repeat.
304 actions: Vec<Action>,
305 /// The maximum amount of times to repeat.
306 ///
307 /// Defaults to 10.
308 #[serde(default = "get_10_u64")]
309 limit: u64
310 },
311
312
313
314 /// If the contained [`Self`] returns an error, ignore it.
315 ///
316 /// Does not revert any successful calls to [`Self::apply`]. For that, also use [`Self::RevertOnError`].
317 /// # Examples
318 /// ```
319 /// use url_cleaner_engine::types::*;
320 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com");
321 ///
322 /// Action::IgnoreError(Box::new(
323 /// Action::RevertOnError(Box::new(
324 /// Action::All(vec![
325 /// Action::SetPath("/change".into()),
326 /// Action::Error("This won't revert the above".into()),
327 /// Action::SetPath("/wont-happen".into())
328 /// ])
329 /// ))
330 /// )).apply(&mut task_state).unwrap(); // Error is ignored.
331 ///
332 /// assert_eq!(task_state.url, "https://example.com/"); // The first `Action::SetPath` is reverted.
333 /// ```
334 IgnoreError(Box<Self>),
335 /// If the contained [`Self`] returns an error, revert the [`TaskState`] to its previous state then return the error.
336 ///
337 /// To ignore errors, put this in a [`Self::IgnoreError`].
338 /// # Errors
339 #[doc = edoc!(applyerr(Self))]
340 /// # Examples
341 /// ```
342 /// use url_cleaner_engine::types::*;
343 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com");
344 ///
345 /// Action::RevertOnError(Box::new(
346 /// Action::All(vec![
347 /// Action::SetPath("/change".into()),
348 /// Action::Error("This won't revert the above".into()),
349 /// Action::SetPath("/wont-happen".into())
350 /// ])
351 /// )).apply(&mut task_state).unwrap_err(); // Still returns an error.
352 ///
353 /// assert_eq!(task_state.url, "https://example.com/"); // The first `Action::SetPath` is reverted.
354 /// ```
355 RevertOnError(Box<Self>),
356 /// If [`Self::TryElse::try`]'s call to [`Self::apply`] returns an error, apply [`Self::TryElse::else`].
357 /// # Errors
358 #[doc = edoc!(applyerrte(Self, Action))]
359 TryElse {
360 /// The [`Self`] to try first.
361 r#try: Box<Self>,
362 /// The [`Self`] to try if [`Self::TryElse::try`] returns an error.
363 r#else: Box<Self>
364 },
365 /// Applies the contained [`Self`]s in order, stopping as soon as a call to [`Self::apply`] doesn't return an error.
366 /// # Errors
367 #[doc = edoc!(applyerrfne(Self, Action))]
368 FirstNotError(Vec<Self>),
369
370 // Whole
371
372 /// Sets [`UrlPart::Whole`].
373 SetWhole(StringSource),
374 /// [`Url::join`].
375 /// # Errors
376 #[doc = edoc!(geterr(StringSource), callerr(Url::join))]
377 /// # Examples
378 /// ```
379 /// use url_cleaner_engine::types::*;
380 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com/a/b/c");
381 ///
382 /// Action::Join("..".into()).apply(&mut task_state).unwrap();
383 /// assert_eq!(task_state.url, "https://example.com/a/");
384 ///
385 ///
386 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com/a/b/c/");
387 ///
388 /// Action::Join("..".into()).apply(&mut task_state).unwrap();
389 /// assert_eq!(task_state.url, "https://example.com/a/b/");
390 /// ```
391 Join(StringSource),
392
393 // Scheme
394
395 /// [`BetterUrl::set_scheme`].
396 /// # Errors
397 #[doc = edoc!(callerr(BetterUrl::set_scheme))]
398 SetScheme(StringSource),
399
400 // Host
401
402 /// [`BetterUrl::set_host`].
403 /// # Errors
404 #[doc = edoc!(callerr(BetterUrl::set_host))]
405 SetHost(StringSource),
406 /// [`BetterUrl::set_subdomain`].
407 /// # Errors
408 #[doc = edoc!(callerr(BetterUrl::set_subdomain))]
409 SetSubdomain(StringSource),
410 /// [`BetterUrl::set_reg_domain`].
411 /// # Errors
412 #[doc = edoc!(callerr(BetterUrl::set_reg_domain))]
413 SetRegDomain(StringSource),
414 /// [`BetterUrl::set_domain`].
415 /// # Errors
416 #[doc = edoc!(callerr(BetterUrl::set_domain))]
417 SetDomain(StringSource),
418 /// [`BetterUrl::set_domain_middle`].
419 /// # Errors
420 #[doc = edoc!(callerr(BetterUrl::set_domain_middle))]
421 SetDomainMiddle(StringSource),
422 /// [`BetterUrl::set_not_domain_suffix`].
423 /// # Errors
424 #[doc = edoc!(callerr(BetterUrl::set_not_domain_suffix))]
425 SetNotDomainSuffix(StringSource),
426 /// [`BetterUrl::set_domain_suffix`].
427 /// # Errors
428 #[doc = edoc!(callerr(BetterUrl::set_domain_suffix))]
429 SetDomainSuffix(StringSource),
430 /// [`BetterUrl::domain_segment`].
431 /// # Errors
432 #[doc = edoc!(geterr(StringSource), callerr(BetterUrl::domain_segment))]
433 SetDomainSegment {
434 /// The index to insert the segment at.
435 index: isize,
436 /// The value to insert.
437 value: StringSource
438 },
439 /// [`BetterUrl::subdomain_segment`].
440 /// # Errors
441 #[doc = edoc!(geterr(StringSource), callerr(BetterUrl::subdomain_segment))]
442 SetSubdomainSegment {
443 /// The index to insert the segment at.
444 index: isize,
445 /// The value to insert.
446 value: StringSource
447 },
448 /// [`BetterUrl::domain_suffix_segment`].
449 /// # Errors
450 #[doc = edoc!(geterr(StringSource), callerr(BetterUrl::domain_suffix_segment))]
451 SetDomainSuffixSegment {
452 /// The index to insert the segment at.
453 index: isize,
454 /// The value to insert.
455 value: StringSource
456 },
457 /// [`BetterUrl::insert_domain_segment`].
458 /// # Errors
459 #[doc = edoc!(geterr(StringSource), callerr(BetterUrl::insert_domain_segment))]
460 InsertDomainSegment {
461 /// The index to insert the segment at.
462 index: isize,
463 /// The value to insert.
464 value: StringSource
465 },
466 /// [`BetterUrl::insert_subdomain_segment`].
467 /// # Errors
468 #[doc = edoc!(geterr(StringSource), callerr(BetterUrl::insert_subdomain_segment))]
469 InsertSubdomainSegment {
470 /// The index to insert the segment at.
471 index: isize,
472 /// The value to insert.
473 value: StringSource
474 },
475 /// [`BetterUrl::insert_domain_suffix_segment`].
476 /// # Errors
477 #[doc = edoc!(geterr(StringSource), callerr(BetterUrl::insert_domain_suffix_segment))]
478 InsertDomainSuffixSegment {
479 /// The index to insert the segment at.
480 index: isize,
481 /// The value to insert.
482 value: StringSource
483 },
484
485 /// [`BetterUrl::set_fqdn`] to [`true`]
486 /// # Errors
487 #[doc = edoc!(callerr(BetterUrl::set_fqdn))]
488 EnsureFqdnPeriod,
489 /// [`BetterUrl::set_fqdn`] to [`false`]
490 /// # Errors
491 #[doc = edoc!(callerr(BetterUrl::set_fqdn))]
492 RemoveFqdnPeriod,
493
494 /// [`BetterUrl::set_path`].
495 SetPath(StringSource),
496 /// Removes the specified [`UrlPart::PathSegment`].
497 /// # Errors
498 #[doc = edoc!(callerr(BetterUrl::set_path_segment))]
499 RemovePathSegment(isize),
500 /// [`BetterUrl::set_path_segment`].
501 /// # Errors
502 #[doc = edoc!(geterr(StringSource), callerr(BetterUrl::set_path_segment))]
503 SetPathSegment {
504 /// The [`UrlPart::PathSegment`] to set.
505 index: isize,
506 /// The value to set it to.
507 value: StringSource
508 },
509 /// [`BetterUrl::insert_path_segment`].
510 /// # Errors
511 #[doc = edoc!(geterr(StringSource), getnone(StringSource, ActionError), callerr(BetterUrl::insert_path_segment))]
512 InsertPathSegment {
513 /// The index to insert it at.
514 index: isize,
515 /// The value to insert.
516 value: StringSource
517 },
518 /// [`BetterUrl::set_path_segment`].
519 /// # Errors
520 #[doc = edoc!(geterr(StringSource), callerr(BetterUrl::set_path_segment))]
521 SetRawPathSegment {
522 /// The [`UrlPart::PathSegment`] to set.
523 index: isize,
524 /// The value to set it to.
525 value: StringSource
526 },
527 /// [`BetterUrl::insert_path_segment`].
528 /// # Errors
529 #[doc = edoc!(geterr(StringSource), getnone(StringSource, ActionError), callerr(BetterUrl::insert_path_segment))]
530 InsertRawPathSegment {
531 /// The index to insert it at.
532 index: isize,
533 /// The value to insert.
534 value: StringSource
535 },
536 /// [`PathSegmentsMut::pop_if_empty`].
537 /// # Errors
538 #[doc = edoc!(callerr(BetterUrl::path_segments_mut))]
539 RemoveEmptyLastPathSegment,
540 /// [`PathSegmentsMut::pop_if_empty`] and [`PathSegmentsMut::push`].
541 /// # Errors
542 #[doc = edoc!(geterr(StringSource), getnone(StringSource, ActionError), callerr(BetterUrl::path_segments_mut))]
543 RemoveEmptyLastPathSegmentAndInsertNew(StringSource),
544 /// Remove the first `n` path segments.
545 ///
546 /// The number of path segments after this succeeds is equal to the number of path segments before this is applied minus `n`.
547 ///
548 /// Because a path can't have zero segments, this means trying to remove all segments counts as not having enough segments. If this is a serious ergonomics issue for you, I'll prioritize making a workaround.
549 /// # Errors
550 #[doc = edoc!(callerr(BetterUrl::remove_first_n_path_segments))]
551 RemoveFirstNPathSegments(usize),
552 /// Keep the first `n` path segments.
553 ///
554 /// The number of path segments after this succeeds is equal to `n`.
555 ///
556 /// Because a path can't have zero segments, this means trying to keep zero segments always errors. This is easy to just not do.
557 /// # Errors
558 #[doc = edoc!(callerr(BetterUrl::keep_first_n_path_segments))]
559 KeepFirstNPathSegments(usize),
560 /// Remove the last `n` path segments.
561 ///
562 /// The number of path segments after this succeeds is equal to the number of path segments before this is applied minus `n`.
563 ///
564 /// Because a path can't have zero segments, this means trying to remove all segments counts as not having enough segments. If this is a serious ergonomics issue for you, I'll prioritize making a workaround.
565 /// # Errors
566 #[doc = edoc!(callerr(BetterUrl::remove_last_n_path_segments))]
567 RemoveLastNPathSegments(usize),
568 /// Keep the last `n` path segments.
569 ///
570 /// The number of path segments after this succeeds is equal to `n`.
571 ///
572 /// Because a path can't have zero segments, this means trying to keep zero segments always errors. This is easy to just not do.
573 /// # Errors
574 #[doc = edoc!(callerr(BetterUrl::keep_last_n_path_segments))]
575 KeepLastNPathSegments(usize),
576
577
578
579 /// [`BetterUrl::set_query`].
580 /// # Errors
581 #[doc = edoc!(geterr(StringSource), callerr(BetterUrl::set_query))]
582 SetQuery(StringSource),
583 /// [`BetterUrl::set_query_param`]
584 /// # Errors
585 #[doc = edoc!(geterr(StringSource), callerr(BetterUrl::set_query_param))]
586 SetQueryParam {
587 /// The query param to set.
588 param: QueryParamSelector,
589 /// The value to set it to.
590 value: StringSource
591 },
592 /// Remove the entire [`UrlPart::Query`].
593 /// # Examples
594 /// ```
595 /// use url_cleaner_engine::types::*;
596 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com?a=2");
597 ///
598 /// Action::RemoveQuery.apply(&mut task_state).unwrap();
599 /// assert_eq!(task_state.url, "https://example.com/");
600 /// ```
601 RemoveQuery,
602 /// If the [`Url::query`] is `Some("")`, set it to [`None`].
603 RemoveEmptyQuery,
604 /// Removes all query parameters with the specified name.
605 ///
606 /// For performance reasons, if the resulting query is empty, this instead sets it to [`None`].
607 /// # Errors
608 #[doc = edoc!(geterr(StringSource), getnone(StringSource, Action))]
609 /// # Examples
610 /// ```
611 /// use url_cleaner_engine::types::*;
612 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com?a=2&b=3&a=4&c=5");
613 ///
614 /// Action::RemoveQueryParam("a".into()).apply(&mut task_state).unwrap();
615 /// assert_eq!(task_state.url.query(), Some("b=3&c=5"));
616 /// Action::RemoveQueryParam("b".into()).apply(&mut task_state).unwrap();
617 /// assert_eq!(task_state.url.query(), Some("c=5"));
618 /// Action::RemoveQueryParam("c".into()).apply(&mut task_state).unwrap();
619 /// assert_eq!(task_state.url.query(), None);
620 /// ```
621 RemoveQueryParam(StringSource),
622 /// Keeps all query parameters with the specified name.
623 ///
624 /// For performance reasons, if the resulting query is empty, this instead sets it to [`None`].
625 AllowQueryParam(StringSource),
626 /// Removes all query params with names in the specified [`HashSet`].
627 ///
628 /// For performance reasons, if the resulting query is empty, this instead sets it to [`None`].
629 /// # Errors
630 #[doc = edoc!(geterr(StringSource), getnone(StringSource, Action))]
631 /// # Examples
632 /// ```
633 /// use url_cleaner_engine::types::*;
634 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com?a=2&b=3&%61=4&c=5");
635 ///
636 /// Action::RemoveQueryParams(["a".to_string(), "b".to_string()].into()).apply(&mut task_state).unwrap();
637 /// assert_eq!(task_state.url.query(), Some("c=5"));
638 /// Action::RemoveQueryParams(["c".to_string()].into()).apply(&mut task_state).unwrap();
639 /// assert_eq!(task_state.url.query(), None);
640 /// ```
641 RemoveQueryParams(#[serde_as(as = "SetPreventDuplicates<_>")] HashSet<String>),
642 /// Keeps only query params with names in the specified [`HashSet`].
643 ///
644 /// For performance reasons, if the resulting query is empty, this instead sets it to [`None`].
645 /// # Examples
646 /// ```
647 /// use url_cleaner_engine::types::*;
648 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com?a=2&b=3&%61=4&c=5");
649 ///
650 /// Action::AllowQueryParams(["a".to_string(), "b".to_string()].into()).apply(&mut task_state).unwrap();
651 /// assert_eq!(task_state.url.query(), Some("a=2&b=3&%61=4"));
652 /// Action::AllowQueryParams(["c".to_string()].into()).apply(&mut task_state).unwrap();
653 /// assert_eq!(task_state.url.query(), None);
654 /// ```
655 AllowQueryParams(#[serde_as(as = "SetPreventDuplicates<_>")] HashSet<String>),
656 /// Removes all query params with names matching the specified [`StringMatcher`].
657 ///
658 /// For performance reasons, if the resulting query is empty, this instead sets it to [`None`].
659 /// # Errors
660 #[doc = edoc!(checkerr(StringMatcher))]
661 /// # Examples
662 /// ```
663 /// use url_cleaner_engine::types::*;
664 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com?a=2&b=3&%61=4&c=5");
665 ///
666 /// Action::RemoveQueryParamsMatching(StringMatcher::Is("a".into())).apply(&mut task_state).unwrap();
667 /// assert_eq!(task_state.url.query(), Some("b=3&c=5"));
668 /// Action::RemoveQueryParamsMatching(StringMatcher::Is("b".into())).apply(&mut task_state).unwrap();
669 /// assert_eq!(task_state.url.query(), Some("c=5"));
670 /// Action::RemoveQueryParamsMatching(StringMatcher::Is("c".into())).apply(&mut task_state).unwrap();
671 /// assert_eq!(task_state.url.query(), None);
672 /// ```
673 RemoveQueryParamsMatching(StringMatcher),
674 /// Keeps only query params with names matching the specified [`StringMatcher`].
675 ///
676 /// For performance reasons, if the resulting query is empty, this instead sets it to [`None`].
677 /// # Errors
678 #[doc = edoc!(checkerr(StringMatcher))]
679 /// # Examples
680 /// ```
681 /// use url_cleaner_engine::types::*;
682 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com?a=2&b=3&%61=4&c=5");
683 ///
684 /// Action::AllowQueryParamsMatching(StringMatcher::Is("a".into())).apply(&mut task_state).unwrap();
685 /// assert_eq!(task_state.url.query(), Some("a=2&%61=4"));
686 /// Action::AllowQueryParamsMatching(StringMatcher::Is("b".into())).apply(&mut task_state).unwrap();
687 /// assert_eq!(task_state.url.query(), None);
688 /// ```
689 AllowQueryParamsMatching(StringMatcher),
690 /// Extreme shorthand for handling universal query parameters.
691 /// # Errors
692 #[doc = edoc!(notfound(Set, Action))]
693 ///
694 /// If the list isn't found, returns the error [`ActionError::ListNotFound`].
695 RemoveQueryParamsInSetOrStartingWithAnyInList {
696 /// The name of the [`Set`] in [`Params::sets`] to use.
697 set: String,
698 /// The name of the list in [`Params::lists`] to use.
699 list: String
700 },
701 /// Rename the specified query parameter to the specified name.
702 /// # Errors
703 #[doc = edoc!(callerr(BetterUrl::rename_query_param))]
704 RenameQueryParam {
705 /// The query parameter to rename.
706 from: QueryParamSelector,
707 /// The name to rename it to.
708 to: StringSource
709 },
710
711 /// Sets [`UrlPart::Whole`] to the value of the first query parameter with a name determined by the [`TaskState`].
712 /// # Errors
713 /// If the URL doesn't have a query, returns the error [`ActionError::NoQuery`].
714 ///
715 /// If the specified query param isn't found, returns the error [`ActionError::QueryParamNotFound`].
716 ///
717 /// If the specified query param doesn't have a value, returns the error [`ActionError::QueryParamNoValue`].
718 ///
719 #[doc = edoc!(geterr(StringSource), getnone(StringSource, Action))]
720 ///
721 /// If no matching query parameter is found, returns the error [`ActionError::QueryParamNotFound`].
722 /// # Examples
723 /// ```
724 /// use url_cleaner_engine::types::*;
725 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com?redirect=https://example.com/2");
726 ///
727 /// Action::GetUrlFromQueryParam("redirect".into()).apply(&mut task_state).unwrap();
728 /// assert_eq!(task_state.url, "https://example.com/2");
729 ///
730 /// Action::GetUrlFromQueryParam("redirect".into()).apply(&mut task_state).unwrap_err();
731 /// ```
732 GetUrlFromQueryParam(StringSource),
733
734 // Fragment
735
736 /// Removes the [`UrlPart::Fragment`].
737 RemoveFragment,
738 /// If the [`Url::fragment`] is `Some("")`, set it to [`None`].
739 RemoveEmptyFragment,
740
741 // General parts
742
743 /// Sets the specified [`UrlPart`] to the specified value.
744 /// # Errors
745 #[doc = edoc!(geterr(StringSource), seterr(UrlPart))]
746 /// # Examples
747 /// ```
748 /// use url_cleaner_engine::types::*;
749 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com");
750 ///
751 /// Action::SetPart {part: UrlPart::Path, value: "abc".into()}.apply(&mut task_state).unwrap();
752 /// assert_eq!(task_state.url, "https://example.com/abc");
753 /// ```
754 SetPart {
755 /// The part to set the value of.
756 part: UrlPart,
757 /// The value to set the part to.
758 value: StringSource
759 },
760 /// If the specified [`UrlPart`] is [`Some`], applies [`Self::ModifyPart::modification`].
761 ///
762 /// If the part is [`None`], does nothing.
763 /// # Errors
764 #[doc = edoc!(applyerr(StringModification), seterr(UrlPart))]
765 /// # Examples
766 /// ```
767 /// use url_cleaner_engine::types::*;
768 ///
769 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com");
770 ///
771 /// Action::ModifyPart {part: UrlPart::Path, modification: StringModification::Set("abc".into())}.apply(&mut task_state).unwrap();
772 /// assert_eq!(task_state.url, "https://example.com/abc");
773 ///
774 /// Action::ModifyPart {part: UrlPart::Query, modification: StringModification::Set("abc".into())}.apply(&mut task_state).unwrap();
775 /// assert_eq!(task_state.url, "https://example.com/abc?abc");
776 /// ```
777 ModifyPart {
778 /// The part to modify.
779 part: UrlPart,
780 /// The modification to apply to the part.
781 modification: StringModification
782 },
783 /// If the specified [`UrlPart`] is [`Some`], apply [`Self::ModifyPartIfSome::modification`].
784 /// # Errors
785 #[doc = edoc!(applyerr(StringModification), seterr(UrlPart))]
786 ModifyPartIfSome {
787 /// The [`UrlPart`] to modify.
788 part: UrlPart,
789 /// The [`StringModification`] to apply.
790 modification: StringModification
791 },
792 /// Sets [`Self::CopyPart::to`] to the value of [`Self::CopyPart::from`], leaving [`Self::CopyPart::from`] unchanged.
793 /// # Errors
794 #[doc = edoc!(seterr(UrlPart))]
795 /// # Examples
796 /// ```
797 /// use url_cleaner_engine::types::*;
798 ///
799 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com/abc#def");
800 ///
801 /// Action::CopyPart {from: UrlPart::Fragment, to: UrlPart::Path}.apply(&mut task_state).unwrap();
802 /// assert_eq!(task_state.url, "https://example.com/def#def");
803 /// ```
804 CopyPart {
805 /// The part whose value to copy.
806 from: UrlPart,
807 /// The part whose value to set.
808 to: UrlPart
809 },
810 /// Sets [`Self::CopyPart::to`] to the value of [`Self::CopyPart::from`], then sets [`Self::CopyPart::from`] to [`None`].
811 /// # Errors
812 #[doc = edoc!(seterr(UrlPart, 2))]
813 /// # Examples
814 /// ```
815 /// use url_cleaner_engine::types::*;
816 ///
817 /// url_cleaner_engine::task_state!(task_state, url = "https://example.com/abc#def");
818 ///
819 /// Action::MovePart {from: UrlPart::Fragment, to: UrlPart::Path}.apply(&mut task_state).unwrap();
820 /// assert_eq!(task_state.url, "https://example.com/def");
821 /// ```
822 MovePart {
823 /// The part whose value to move.
824 from: UrlPart,
825 /// The part whose value to set.
826 to: UrlPart
827 },
828
829 // Misc.
830
831 /// Sends an HTTP GET request to the current [`TaskState::url`], and sets it either to the value of the response's `Location` header (if the response is a redirect) or the final URL after redirects.
832 ///
833 /// If the `cache` feature flag is enabled, caches the operation with the subject `redirect`, the key set to the input URL, and the value set to the returned URL.
834 /// # Errors
835 #[cfg_attr(feature = "cache", doc = edoc!(callerr(Cache::read), callnone(Cache::read, ActionError::CachedUrlIsNone), callerr(BetterUrl::parse)))]
836 #[cfg_attr(feature = "cache", doc = "")]
837 #[doc = edoc!(callerr(TaskStateView::http_client), callerr(reqwest::blocking::RequestBuilder::send))]
838 ///
839 /// If the response is a redirect:
840 ///
841 /// - If the `Location` header is missing, returns the error [`ActionError::LocationHeaderNotFound`].
842 ///
843 #[doc = edoc!(listitem, callerr(std::str::from_utf8), callerr(BetterUrl::parse))]
844 #[cfg_attr(feature = "cache", doc = "")]
845 #[cfg_attr(feature = "cache", doc = edoc!(callerr(Cache::write)))]
846 #[cfg(feature = "http")]
847 ExpandRedirect {
848 /// If [`Some`], expand this URL instead.
849 ///
850 /// Defaults to [`None`].
851 #[serde(default, skip_serializing_if = "is_default")]
852 url: Option<StringSource>,
853 /// The extra headers to send.
854 ///
855 /// Defaults to an empty [`HeaderMap`].
856 #[serde(default, skip_serializing_if = "is_default", with = "serde_headermap")]
857 headers: HeaderMap,
858 /// The [`HttpClientConfigDiff`] to apply.
859 ///
860 /// Defaults to [`None`].
861 ///
862 /// Boxed because it's massive.
863 #[serde(default, skip_serializing_if = "is_default")]
864 http_client_config_diff: Option<Box<HttpClientConfigDiff>>
865 },
866 /// Sets the specified [`Scratchpad::flags`] to [`Self::SetScratchpadFlag::value`].
867 /// # Errors
868 #[doc = edoc!(geterr(StringSource), getnone(StringSource, Action))]
869 /// # Examples
870 /// ```
871 /// use url_cleaner_engine::types::*;
872 ///
873 /// url_cleaner_engine::task_state!(task_state);
874 ///
875 /// assert_eq!(task_state.scratchpad.flags.contains("abc"), false);
876 /// Action::SetScratchpadFlag {name: "abc".into(), value: true}.apply(&mut task_state).unwrap();
877 /// assert_eq!(task_state.scratchpad.flags.contains("abc"), true);
878 /// ```
879 SetScratchpadFlag {
880 /// The name of the flag to set.
881 name: StringSource,
882 /// The value to set the flag to.
883 value: bool
884 },
885 /// Sets the specified [`Scratchpad::vars`] to [`Self::SetScratchpadVar::value`].
886 /// # Errors
887 #[doc = edoc!(geterr(StringSource))]
888 /// # Examples
889 /// ```
890 /// use url_cleaner_engine::types::*;
891 ///
892 /// url_cleaner_engine::task_state!(task_state);
893 ///
894 /// Action::SetScratchpadVar {name: "abc".into(), value: "def".into()}.apply(&mut task_state).unwrap();
895 /// assert_eq!(task_state.scratchpad.vars.get("abc").map(|x| &**x), Some("def"));
896 /// Action::SetScratchpadVar {name: "abc".into(), value: StringSource::None}.apply(&mut task_state).unwrap();
897 /// assert_eq!(task_state.scratchpad.vars.get("abc").map(|x| &**x), None);
898 /// ```
899 SetScratchpadVar {
900 /// The name of the var to set.
901 name: StringSource,
902 /// The value to set the var to.
903 value: StringSource
904 },
905 /// If the specified [`Scratchpad::vars`] is [`Some`], applies [`Self::ModifyScratchpadVar::modification`].
906 ///
907 /// If the part is [`None`], does nothing.
908 /// # Errors
909 #[doc = edoc!(geterr(StringSource), getnone(StringSource, Action), applyerr(StringModification))]
910 /// # Examples
911 /// ```
912 /// use url_cleaner_engine::types::*;
913 ///
914 /// url_cleaner_engine::task_state!(task_state);
915 ///
916 /// Action::ModifyScratchpadVar {name: "abc".into(), modification: StringModification::Set("123".into())}.apply(&mut task_state).unwrap();
917 /// assert_eq!(task_state.scratchpad.vars.get("abc").map(|x| &**x), Some("123"));
918 /// Action::ModifyScratchpadVar {name: "abc".into(), modification: StringModification::Set(StringSource::None)}.apply(&mut task_state).unwrap();
919 /// assert_eq!(task_state.scratchpad.vars.get("abc").map(|x| &**x), None);
920 /// ```
921 ModifyScratchpadVar {
922 /// The name of the var to modify.
923 name: StringSource,
924 /// The modification to apply.
925 modification: StringModification
926 },
927 /// If an entry with a subject of [`Self::CacheUrl::subject`] and a key of [`TaskState::url`] exists in the [`TaskState::cache`], sets the URL to the entry's value.
928 ///
929 /// If no such entry exists, applies [`Self::CacheUrl::action`] and inserts a new entry equivalent to applying it.
930 ///
931 /// Does not cache the [`TaskState::scratchpad`].
932 /// # Errors
933 #[doc = edoc!(callerr(Cache::read), callnone(Cache::read, ActionError::CachedUrlIsNone), callerr(BetterUrl::parse), applyerr(Self), callerr(Cache::write))]
934 #[cfg(feature = "cache")]
935 CacheUrl {
936 /// The subject for the cache entry.
937 subject: StringSource,
938 /// The action to apply and cache.
939 action: Box<Self>
940 },
941 /// Applies a [`Self`] from [`TaskState::commons`]'s [`Commons::actions`].
942 /// # Errors
943 #[doc = edoc!(geterr(StringSource), getnone(StringSource, Action), commonnotfound(Self, Action), callerr(CommonCallArgsSource::build), applyerr(Self))]
944 /// # Examples
945 /// ```
946 /// use url_cleaner_engine::types::*;
947 ///
948 /// url_cleaner_engine::task_state!(task_state, commons = Commons {
949 /// actions: [("abc".into(), Action::None)].into(),
950 /// ..Default::default()
951 /// });
952 ///
953 /// Action::Common(CommonCall {name: Box::new("abc".into()), args: Default::default()}).apply(&mut task_state).unwrap();
954 /// ```
955 Common(CommonCall),
956 /// Gets a [`Self`] from [`TaskStateView::common_args`]'s [`CommonCallArgs::actions`] and applies it.
957 /// # Errors
958 /// If [`TaskStateView::common_args`] is [`None`], returns the error [`ActionError::NotInCommonContext`].
959 ///
960 #[doc = edoc!(commoncallargnotfound(Self, Action), applyerr(Self))]
961 CommonCallArg(StringSource),
962 /// Calls the specified function and returns its value.
963 ///
964 /// Because this uses function pointers, this plays weirdly with [`PartialEq`]/[`Eq`].
965 /// # Errors
966 #[doc = edoc!(callerr(Self::Custom::0))]
967 /// # Examples
968 /// ```
969 /// use url_cleaner_engine::types::*;
970 ///
971 /// url_cleaner_engine::task_state!(task_state);
972 ///
973 /// fn some_complex_operation(task_state: &mut TaskState) -> Result<(), ActionError> {
974 /// Ok(())
975 /// }
976 ///
977 /// Action::Custom(some_complex_operation).apply(&mut task_state).unwrap();
978 /// ```
979 #[cfg(feature = "custom")]
980 #[suitable(never)]
981 #[serde(skip)]
982 Custom(fn(&mut TaskState) -> Result<(), ActionError>)
983}
984
985string_or_struct_magic!(Action);
986
987/// The error returned when trying to deserialize a [`StringModification`] variant with fields that aren't all defaultable.
988#[derive(Debug, Error)]
989#[error("Tried deserializing undefaultable or Action unknown variant {0}.")]
990pub struct NonDefaultableActionVariant(String);
991
992impl From<&str> for NonDefaultableActionVariant {
993 fn from(value: &str) -> Self {
994 value.to_string().into()
995 }
996}
997
998impl From<String> for NonDefaultableActionVariant {
999 fn from(value: String) -> Self {
1000 Self(value)
1001 }
1002}
1003
1004impl FromStr for Action {
1005 type Err = NonDefaultableActionVariant;
1006
1007 fn from_str(s: &str) -> Result<Self, Self::Err> {
1008 Ok(match s {
1009 "None" => Action::None,
1010 "EnsureFqdnPeriod" => Action::EnsureFqdnPeriod,
1011 "RemoveFqdnPeriod" => Action::RemoveFqdnPeriod,
1012 "RemoveEmptyLastPathSegment" => Action::RemoveEmptyLastPathSegment,
1013 "RemoveQuery" => Action::RemoveQuery,
1014 "RemoveEmptyQuery" => Action::RemoveEmptyQuery,
1015 "RemoveFragment" => Action::RemoveFragment,
1016 "RemoveEmptyFragment" => Action::RemoveEmptyFragment,
1017 #[cfg(feature = "http")]
1018 "ExpandRedirect" => Action::ExpandRedirect {url: Default::default(), headers: Default::default(), http_client_config_diff: Default::default()},
1019 _ => return Err(s.into())
1020 })
1021 }
1022}
1023
1024/// Helper function to get the default [`Action::Repeat::limit`].
1025const fn get_10_u64() -> u64 {10}
1026
1027/// The enum of errors [`Action::apply`] can return.
1028#[derive(Debug, Error)]
1029pub enum ActionError {
1030 /// Returned when a [`Action::Error`] is used.
1031 #[error("Explicit error: {0}")]
1032 ExplicitError(String),
1033 /// Returned when both [`Action`]s in a [`Action::TryElse`] return errors.
1034 #[error("Both Actions in a Action::TryElse returned errors.")]
1035 TryElseError {
1036 /// The error returned by [`Action::TryElse::try`].
1037 try_error: Box<Self>,
1038 /// The error returned by [`Action::TryElse::else`].
1039 else_error: Box<Self>
1040 },
1041 /// Returned when all [`Action`]s in a [`Action::FirstNotError`] error.
1042 #[error("All Actions in a Action::FirstNotError errored.")]
1043 FirstNotErrorErrors(Vec<Self>),
1044
1045 /// Returned when a [`StringSourceError`] is encountered.
1046 #[error(transparent)]
1047 StringSourceError(#[from] StringSourceError),
1048 /// Returned when a part of the URL is [`None`] where it has to be [`Some`].
1049 #[error("A StringSource returned None where it had to return Some.")]
1050 StringSourceIsNone,
1051 /// Returned when a [`StringModificationError`] is encountered.
1052 #[error(transparent)]
1053 StringModificationError(#[from] StringModificationError),
1054 /// Returned when a [`StringMatcherError`] is encountered.
1055 #[error(transparent)]
1056 StringMatcherError(#[from] StringMatcherError),
1057 /// Returned when a [`StringLocationError`] is encountered.
1058 #[error(transparent)]
1059 StringLocationError(#[from] StringLocationError),
1060
1061 /// Returned when a [`SetSchemeError`] is encountered.
1062 #[error(transparent)]
1063 SetSchemeError(#[from] SetSchemeError),
1064 /// Returned when attempting to set a URL's scheme to [`None`].
1065 #[error("Attempted to set the URL's scheme to None.")]
1066 SchemeCannotBeNone,
1067
1068 /// Returned when a [`SetHostError`] is encountered.
1069 #[error(transparent)]
1070 SetHostError(#[from] SetHostError),
1071 /// Returned when a [`SetSubdomainError`] is encountered.
1072 #[error(transparent)]
1073 SetSubdomainError(#[from] SetSubdomainError),
1074 /// Returned when a [`SetRegDomainError`] is encountered.
1075 #[error(transparent)]
1076 SetRegDomainError(#[from] SetRegDomainError),
1077 /// Returned when a [`SetDomainError`] is encountered.
1078 #[error(transparent)]
1079 SetDomainError(#[from] SetDomainError),
1080 /// Returned when a [`SetDomainMiddleError`] is encountered.
1081 #[error(transparent)]
1082 SetDomainMiddleError(#[from] SetDomainMiddleError),
1083 /// Returned when a [`SetNotDomainSuffixError`] is encountered.
1084 #[error(transparent)]
1085 SetNotDomainSuffixError(#[from] SetNotDomainSuffixError),
1086 /// Returned when a [`SetDomainSuffixError`] is encountered.
1087 #[error(transparent)]
1088 SetDomainSuffixError(#[from] SetDomainSuffixError),
1089 /// Returned when a [`SetFqdnError`] is encountered.
1090 #[error(transparent)]
1091 SetFqdnError(#[from] SetFqdnError),
1092
1093 /// Returned when a [`InsertDomainSegmentError`] is encountered.
1094 #[error(transparent)]
1095 InsertDomainSegmentError(#[from] InsertDomainSegmentError),
1096 /// Returned when a [`InsertSubdomainSegmentError`] is encountered.
1097 #[error(transparent)]
1098 InsertSubdomainSegmentError(#[from] InsertSubdomainSegmentError),
1099 /// Returned when a [`InsertDomainSuffixSegmentError`] is encountered.
1100 #[error(transparent)]
1101 InsertDomainSuffixSegmentError(#[from] InsertDomainSuffixSegmentError),
1102 /// Returned when a [`SetDomainSegmentError`] is encountered.
1103 #[error(transparent)]
1104 SetDomainSegmentError(#[from] SetDomainSegmentError),
1105 /// Returned when a [`SetSubdomainSegmentError`] is encountered.
1106 #[error(transparent)]
1107 SetSubdomainSegmentError(#[from] SetSubdomainSegmentError),
1108 /// Returned when a [`SetDomainSuffixSegmentError`] is encountered.
1109 #[error(transparent)]
1110 SetDomainSuffixSegmentError(#[from] SetDomainSuffixSegmentError),
1111
1112 /// Returned when attempting to set a URL's path to [`None`].
1113 #[error("Attempted to set the URL's path to None.")]
1114 PathCannotBeNone,
1115 /// Returned when attempting to get the path segments of a URL with no path segments.
1116 #[error("Attempted to get the path segments of a URL with no path segments.")]
1117 UrlDoesNotHavePathSegments,
1118 /// Returned when a [`SetPathSegmentError`] is encountered.
1119 #[error(transparent)]
1120 SetPathSegmentError(#[from] SetPathSegmentError),
1121 /// Returned when a [`InsertPathSegmentError`] is encountered.
1122 #[error(transparent)]
1123 InsertPathSegmentError(#[from] InsertPathSegmentError),
1124 /// Returned when attempting to keep/remove more path segments than are available.
1125 #[error("Attempted to keep/remove more path segments than were available.")]
1126 NotEnoughPathSegments,
1127 /// Returned when a [`RemoveFirstNPathSegmentsError`] is encountered.
1128 #[error(transparent)]
1129 RemoveFirstNPathSegmentsError(#[from] RemoveFirstNPathSegmentsError),
1130 /// Returned when a [`KeepFirstNPathSegmentsError`] is encountered.
1131 #[error(transparent)]
1132 KeepFirstNPathSegmentsError(#[from] KeepFirstNPathSegmentsError),
1133 /// Returned when a [`RemoveLastNPathSegmentsError`] is encountered.
1134 #[error(transparent)]
1135 RemoveLastNPathSegmentsError(#[from] RemoveLastNPathSegmentsError),
1136 /// Returned when a [`KeepLastNPathSegmentsError`] is encountered.
1137 #[error(transparent)]
1138 KeepLastNPathSegmentsError(#[from] KeepLastNPathSegmentsError),
1139
1140 /// Returned when attempting to get the value of a query param from a URL with no query.
1141 #[error("Attempted to get the value of a query param from a URL with no query.")]
1142 NoQuery,
1143 /// Returned when attempting to get the value of a query param that wasn't found.
1144 #[error("Attempted to get the value of a query param that wasn't found.")]
1145 QueryParamNotFound,
1146 /// Returned when attempting to get the value of a query param that didn't have a value.
1147 #[error("Attempted to get the value of a query param that didn't have a value.")]
1148 QueryParamNoValue,
1149 /// Returned when a [`RenameQueryParamError`] is encountered.
1150 #[error(transparent)]
1151 RenameQueryParamError(#[from] RenameQueryParamError),
1152 /// Returned when a [`SetQueryParamError`] is encountered.
1153 #[error(transparent)]
1154 SetQueryParamError(#[from] SetQueryParamError),
1155
1156 /// Returned when a [`url::ParseError`] is encountered.
1157 #[error(transparent)]
1158 UrlParseError(#[from] url::ParseError),
1159 /// Returned when a [`Utf8Error`] is encountered.
1160 #[error(transparent)]
1161 Utf8Error(#[from] Utf8Error),
1162 /// Returned when a [`SetUrlPartError`] is encountered.
1163 #[error(transparent)]
1164 SetUrlPartError(#[from] SetUrlPartError),
1165
1166 /// Returned when a [`ConditionError`] is encountered.
1167 #[error(transparent)]
1168 ConditionError(#[from] ConditionError),
1169
1170 /// Returned when a [`NamedPartitioning`] with the specified name isn't found.
1171 #[error("A NamedPartitioning with the specified name wasn't found.")]
1172 NamedPartitioningNotFound,
1173 /// Returned when a [`Set`] with the specified name isn't found.
1174 #[error("A Set with the specified name wasn't found.")]
1175 SetNotFound,
1176 /// Returned when a list with the specified name isn't found.
1177 #[error("A list with the specified name wasn't found.")]
1178 ListNotFound,
1179
1180 /// Returned when a [`reqwest::Error`] is encountered.
1181 #[cfg(feature = "http")]
1182 #[error(transparent)]
1183 ReqwestError(#[from] reqwest::Error),
1184 /// Returned when a redirect's `Location` header isn't found.
1185 #[cfg(feature = "http")]
1186 #[error("The redirect's Location header wasn't found")]
1187 LocationHeaderNotFound,
1188 /// Returned when a [`reqwest::header::ToStrError`] is encountered.
1189 #[cfg(feature = "http")]
1190 #[error(transparent)]
1191 ToStrError(#[from] reqwest::header::ToStrError),
1192
1193 /// Returned when attempting to get a URL from the cache but its value is [`None`].
1194 #[cfg(feature = "cache")]
1195 #[error("Attempted to get a URL from the cache but its value was None.")]
1196 CachedUrlIsNone,
1197 /// Returned when a [`ReadFromCacheError`] is encountered.
1198 #[cfg(feature = "cache")]
1199 #[error(transparent)]
1200 ReadFromCacheError(#[from] ReadFromCacheError),
1201 /// Returned when a [`WriteToCacheError`] is encountered.
1202 #[cfg(feature = "cache")]
1203 #[error(transparent)]
1204 WriteToCacheError(#[from] WriteToCacheError),
1205
1206 /// Returned when a [`CommonCallArgsError`] is encountered.
1207 #[error(transparent)]
1208 CommonCallArgsError(#[from] CommonCallArgsError),
1209 /// Returned when a [`Action`] with the specified name isn't found in the [`Commons::actions`].
1210 #[error("An Action with the specified name wasn't found in the Commons::actions.")]
1211 CommonActionNotFound,
1212 /// Returned when trying to use [`Action::CommonCallArg`] outside of a common context.
1213 #[error("Tried to use Action::CommonCallArg outside of a common context.")]
1214 NotInCommonContext,
1215 /// Returned when the [`Action`] requested from an [`Action::CommonCallArg`] isn't found.
1216 #[error("The Action requested from an Action::CommonCallArg wasn't found.")]
1217 CommonCallArgActionNotFound,
1218 /// An arbitrary [`std::error::Error`] returned by [`Action::Custom`].
1219 #[error(transparent)]
1220 #[cfg(feature = "custom")]
1221 Custom(Box<dyn std::error::Error + Send>)
1222}
1223
1224impl Action {
1225 /// Applies the specified variant of [`Self`].
1226 ///
1227 /// If an error is returned, `task_state` may be left in a partially modified state.
1228 /// # Errors
1229 /// See each variant of [`Self`] for when each variant returns an error.
1230 #[allow(clippy::missing_panics_doc, reason = "Can't happen.")]
1231 pub fn apply(&self, task_state: &mut TaskState) -> Result<(), ActionError> {
1232 debug!(Action::apply, self, task_state.debug_helper());
1233
1234 match self {
1235 // Debug/constants
1236
1237 Self::None => {},
1238 Self::Error(msg) => Err(ActionError::ExplicitError(msg.clone()))?,
1239 Self::Debug(action) => {
1240 let old_task_state = format!("{:?}", task_state.debug_helper());
1241 let return_value=action.apply(task_state);
1242 eprintln!("=== Action::Debug ===\nOld task_state: {old_task_state}\nReturn value: {return_value:?}\nNew task_state: {:?}", task_state.debug_helper());
1243 return_value?
1244 },
1245
1246 // Error handling
1247
1248 Self::IgnoreError(action) => {let _ = action.apply(task_state);},
1249 Self::TryElse {r#try, r#else} => match r#try.apply(task_state) {
1250 Ok(x) => x,
1251 Err(try_error) => match r#else.apply(task_state) {
1252 Ok(x) => x,
1253 Err(else_error) => Err(ActionError::TryElseError {try_error: Box::new(try_error), else_error: Box::new(else_error)})?
1254 }
1255 },
1256 Self::FirstNotError(actions) => {
1257 let mut errors = Vec::new();
1258 for action in actions {
1259 match action.apply(task_state) {
1260 Ok(()) => return Ok(()),
1261 Err(e) => errors.push(e)
1262 }
1263 }
1264 Err(ActionError::FirstNotErrorErrors(errors))?
1265 },
1266 Self::RevertOnError(action) => {
1267 let old_url = task_state.url.clone();
1268 let old_scratchpad = task_state.scratchpad.clone();
1269 if let Err(e) = action.apply(task_state) {
1270 *task_state.url = old_url;
1271 *task_state.scratchpad = old_scratchpad;
1272 Err(e)?;
1273 }
1274 },
1275
1276 // Logic
1277
1278 Self::If {r#if, then, r#else} => if r#if.check(&task_state.to_view())? {
1279 then.apply(task_state)?;
1280 } else if let Some(r#else) = r#else {
1281 r#else.apply(task_state)?;
1282 },
1283 Self::All(actions) => {
1284 for action in actions {
1285 action.apply(task_state)?;
1286 }
1287 },
1288 Self::Repeat{actions, limit} => {
1289 let mut previous_url;
1290 let mut previous_scratchpad;
1291 for _ in 0..*limit {
1292 previous_url = task_state.url.to_string();
1293 previous_scratchpad = task_state.scratchpad.clone();
1294 for action in actions {
1295 action.apply(task_state)?;
1296 }
1297 if task_state.url == &previous_url && task_state.scratchpad == &previous_scratchpad {break;}
1298 }
1299 },
1300
1301 // Maps
1302
1303 Self::PartMap {part , map} => if let Some(action) = map.get(part .get( task_state.url ) ) {action.apply(task_state)?;},
1304 Self::StringMap {value, map} => if let Some(action) = map.get(value.get(&task_state.to_view())?) {action.apply(task_state)?;},
1305
1306 Self::PartNamedPartitioning {named_partitioning, part , map} => if let Some(action) = map.get(task_state.params.named_partitionings.get(get_str!(named_partitioning, task_state, ActionError)).ok_or(ActionError::NamedPartitioningNotFound)?.get_partition_of(part.get(task_state.url).as_deref())) {action.apply(task_state)?;}
1307 Self::FirstMatchingPartNamedPartitioning {named_partitioning, parts, map} => {
1308 let named_partitioning = task_state.params.named_partitionings.get(get_str!(named_partitioning, task_state, ActionError)).ok_or(ActionError::NamedPartitioningNotFound)?;
1309 for part in parts.iter() {
1310 if let Some(action) = map.get(named_partitioning.get_partition_of(part.get(task_state.url).as_deref())) {
1311 return action.apply(task_state);
1312 }
1313 }
1314 }
1315 Self::StringNamedPartitioning {named_partitioning, value, map} => if let Some(action) = map.get(task_state.params.named_partitionings.get(get_str!(named_partitioning, task_state, ActionError)).ok_or(ActionError::NamedPartitioningNotFound)?.get_partition_of(get_option_str!(value, task_state) )) {action.apply(task_state)?;}
1316 Self::FirstMatchingStringNamedPartitioning {named_partitioning, values, map} => {
1317 let named_partitioning = task_state.params.named_partitionings.get(get_str!(named_partitioning, task_state, ActionError)).ok_or(ActionError::NamedPartitioningNotFound)?;
1318 for value in values.iter() {
1319 if let Some(action) = map.get(named_partitioning.get_partition_of(get_option_str!(value, task_state))) {
1320 return action.apply(task_state);
1321 }
1322 }
1323 }
1324
1325 // Whole
1326
1327 Self::SetWhole(new) => *task_state.url = BetterUrl::parse(get_new_str!(new, task_state, ActionError))?,
1328 Self::Join(with) => *task_state.url=task_state.url.join(get_str!(with, task_state, ActionError))?.into(),
1329
1330 // Scheme
1331
1332 Self::SetScheme(to) => task_state.url.set_scheme(get_new_str!(to, task_state, ActionError))?,
1333
1334 // Domain
1335
1336 Self::SetHost (to) => task_state.url.set_host (get_new_option_str!(to, task_state))?,
1337 Self::SetSubdomain (to) => task_state.url.set_subdomain (get_new_option_str!(to, task_state))?,
1338 Self::SetRegDomain (to) => task_state.url.set_reg_domain (get_new_option_str!(to, task_state))?,
1339 Self::SetDomain (to) => task_state.url.set_domain (get_new_option_str!(to, task_state))?,
1340 Self::SetDomainMiddle (to) => task_state.url.set_domain_middle (get_new_option_str!(to, task_state))?,
1341 Self::SetNotDomainSuffix(to) => task_state.url.set_not_domain_suffix(get_new_option_str!(to, task_state))?,
1342 Self::SetDomainSuffix (to) => task_state.url.set_domain_suffix (get_new_option_str!(to, task_state))?,
1343
1344 Self::SetDomainSegment {index, value} => task_state.url.set_domain_segment (*index, get_new_option_str!(value, task_state))?,
1345 Self::SetSubdomainSegment {index, value} => task_state.url.set_subdomain_segment (*index, get_new_option_str!(value, task_state))?,
1346 Self::SetDomainSuffixSegment {index, value} => task_state.url.set_domain_suffix_segment (*index, get_new_option_str!(value, task_state))?,
1347 Self::InsertDomainSegment {index, value} => task_state.url.insert_domain_segment (*index, get_new_str!(value, task_state, ActionError))?,
1348 Self::InsertSubdomainSegment {index, value} => task_state.url.insert_subdomain_segment (*index, get_new_str!(value, task_state, ActionError))?,
1349 Self::InsertDomainSuffixSegment {index, value} => task_state.url.insert_domain_suffix_segment(*index, get_new_str!(value, task_state, ActionError))?,
1350
1351 Self::EnsureFqdnPeriod => task_state.url.set_fqdn(true)?,
1352 Self::RemoveFqdnPeriod => task_state.url.set_fqdn(false)?,
1353
1354 // Path
1355
1356 Self::SetPath(to) => task_state.url.set_path(get_new_str!(to, task_state, ActionError)),
1357
1358 Self::RemovePathSegment (index) => task_state.url.set_path_segment(*index, None)?,
1359 Self::SetPathSegment {index, value} => task_state.url.set_path_segment (*index, get_new_option_str!(value, task_state))?,
1360 Self::InsertPathSegment {index, value} => task_state.url.insert_path_segment (*index, get_new_str!(value, task_state, ActionError))?,
1361 Self::SetRawPathSegment {index, value} => task_state.url.set_raw_path_segment (*index, get_new_option_str!(value, task_state))?,
1362 Self::InsertRawPathSegment {index, value} => task_state.url.insert_raw_path_segment(*index, get_new_str!(value, task_state, ActionError))?,
1363 Self::RemoveEmptyLastPathSegment => {task_state.url.path_segments_mut().ok_or(ActionError::UrlDoesNotHavePathSegments)?.pop_if_empty();},
1364 Self::RemoveEmptyLastPathSegmentAndInsertNew(value) => {
1365 let value = get_new_str!(value, task_state, ActionError);
1366 let mut segments_mut = task_state.url.path_segments_mut().ok_or(ActionError::UrlDoesNotHavePathSegments)?;
1367 segments_mut.pop_if_empty();
1368 segments_mut.push(value);
1369 },
1370 Self::RemoveFirstNPathSegments(n) => task_state.url.remove_first_n_path_segments(*n)?,
1371 Self::KeepFirstNPathSegments (n) => task_state.url.keep_first_n_path_segments (*n)?,
1372 Self::RemoveLastNPathSegments (n) => task_state.url.remove_last_n_path_segments (*n)?,
1373 Self::KeepLastNPathSegments (n) => task_state.url.keep_last_n_path_segments (*n)?,
1374
1375 // Query
1376
1377 Self::SetQuery(to) => task_state.url.set_query(get_new_option_str!(to, task_state)),
1378 Self::SetQueryParam {param: QueryParamSelector {name, index}, value} => task_state.url.set_query_param(name, *index, get_new_option_str!(value, task_state).map(Some))?,
1379 Self::RemoveQuery => task_state.url.set_query(None),
1380 Self::RemoveEmptyQuery => if task_state.url.query() == Some("") {task_state.url.set_query(None)},
1381 Self::RemoveQueryParam(name) => if let Some(query) = task_state.url.query() {
1382 let mut new = String::with_capacity(query.len());
1383 let name = get_str!(name, task_state, ActionError);
1384 for param in query.split('&') {
1385 if pds(param.split('=').next().expect("The first segment to always exist.")).ne(name.bytes()) {
1386 if !new.is_empty() {new.push('&');}
1387 new.push_str(param);
1388 }
1389 }
1390 if new.len() != query.len() {
1391 task_state.url.set_query(Some(&*new).filter(|x| !x.is_empty()));
1392 }
1393 },
1394 Self::AllowQueryParam(name) => if let Some(query) = task_state.url.query() {
1395 let mut new = String::with_capacity(query.len());
1396 let name = get_str!(name, task_state, ActionError);
1397 for param in query.split('&') {
1398 if pds(param.split('=').next().expect("The first segment to always exist.")).eq(name.bytes()) {
1399 if !new.is_empty() {new.push('&');}
1400 new.push_str(param);
1401 }
1402 }
1403 if new.len() != query.len() {
1404 task_state.url.set_query(Some(&*new).filter(|x| !x.is_empty()));
1405 }
1406 },
1407 Self::RemoveQueryParams(names) => if let Some(query) = task_state.url.query() {
1408 let mut new = String::with_capacity(query.len());
1409 for param in query.split('&') {
1410 if !names.contains(&*pds(param.split('=').next().expect("The first segment to always exist.")).decode_utf8_lossy()) {
1411 if !new.is_empty() {new.push('&');}
1412 new.push_str(param);
1413 }
1414 }
1415 if new.len() != query.len() {
1416 task_state.url.set_query(Some(&*new).filter(|x| !x.is_empty()));
1417 }
1418 },
1419 Self::AllowQueryParams(names) => if let Some(query) = task_state.url.query() {
1420 let mut new = String::with_capacity(query.len());
1421 for param in query.split('&') {
1422 if names.contains(&*pds(param.split('=').next().expect("The first segment to always exist.")).decode_utf8_lossy()) {
1423 if !new.is_empty() {new.push('&');}
1424 new.push_str(param);
1425 }
1426 }
1427 if new.len() != query.len() {
1428 task_state.url.set_query(Some(&*new).filter(|x| !x.is_empty()));
1429 }
1430 },
1431 Self::RemoveQueryParamsMatching(matcher) => if let Some(query) = task_state.url.query() {
1432 let mut new = String::with_capacity(query.len());
1433 for param in query.split('&') {
1434 if !matcher.check(Some(&*pds(param.split('=').next().expect("The first segment to always exist.")).decode_utf8_lossy()), &task_state.to_view())? {
1435 if !new.is_empty() {new.push('&');}
1436 new.push_str(param);
1437 }
1438 }
1439 if new.len() != query.len() {
1440 task_state.url.set_query(Some(&*new).filter(|x| !x.is_empty()));
1441 }
1442 },
1443 Self::AllowQueryParamsMatching(matcher) => if let Some(query) = task_state.url.query() {
1444 let mut new = String::with_capacity(query.len());
1445 for param in query.split('&') {
1446 if matcher.check(Some(&*pds(param.split('=').next().expect("The first segment to always exist.")).decode_utf8_lossy()), &task_state.to_view())? {
1447 if !new.is_empty() {new.push('&');}
1448 new.push_str(param);
1449 }
1450 }
1451 if new.len() != query.len() {
1452 task_state.url.set_query(Some(&*new).filter(|x| !x.is_empty()));
1453 }
1454 },
1455 Self::RemoveQueryParamsInSetOrStartingWithAnyInList {set, list} => if let Some(query) = task_state.url.query() {
1456 let mut new = String::with_capacity(query.len());
1457 let set = task_state.params.sets.get(set).ok_or(ActionError::SetNotFound)?;
1458 let list = task_state.params.lists.get(list).ok_or(ActionError::ListNotFound)?;
1459 for param in query.split('&') {
1460 let name = pds(param.split('=').next().expect("The first segment to always exist.")).decode_utf8_lossy();
1461 if !(set.contains(Some(&*name)) || list.iter().any(|x| name.starts_with(x))) {
1462 if !new.is_empty() {new.push('&');}
1463 new.push_str(param);
1464 }
1465 }
1466 if new.len() != query.len() {
1467 task_state.url.set_query(Some(&*new).filter(|x| !x.is_empty()));
1468 }
1469 },
1470 Self::RenameQueryParam {from, to} => task_state.url.rename_query_param(&from.name, from.index, get_new_str!(to, task_state, ActionError))?,
1471
1472 Self::GetUrlFromQueryParam(name) => match task_state.url.query_param(get_str!(name, task_state, ActionError), 0) {
1473 Some(Some(Some(new_url))) => {*task_state.url = BetterUrl::parse(&new_url)?;},
1474 Some(Some(None)) => Err(ActionError::QueryParamNoValue)?,
1475 Some(None) => Err(ActionError::QueryParamNotFound)?,
1476 None => Err(ActionError::NoQuery)?
1477 },
1478
1479 // Fragment
1480
1481 Self::RemoveFragment => task_state.url.set_fragment(None),
1482 Self::RemoveEmptyFragment => if task_state.url.fragment() == Some("") {task_state.url.set_fragment(None)},
1483
1484 // General parts
1485
1486 Self::SetPart {part, value} => part.set(task_state.url, get_new_option_str!(value, task_state))?,
1487
1488 Self::ModifyPart {part, modification} => {
1489 let mut temp = part.get(task_state.url);
1490 modification.apply(&mut temp, &task_state.to_view())?;
1491 part.set(task_state.url, temp.map(Cow::into_owned).as_deref())?;
1492 },
1493 Self::ModifyPartIfSome {part, modification} => {
1494 if let mut temp @ Some(_) = part.get(task_state.url) {
1495 modification.apply(&mut temp, &task_state.to_view())?;
1496 part.set(task_state.url, temp.map(Cow::into_owned).as_deref())?;
1497 }
1498 },
1499
1500 Self::CopyPart {from, to} => to.set(task_state.url, from.get(task_state.url).map(|x| x.into_owned()).as_deref())?,
1501 Self::MovePart {from, to} => {
1502 to.set(task_state.url, from.get(task_state.url).map(|x| x.into_owned()).as_deref())?;
1503 from.set(task_state.url, None)?;
1504 },
1505
1506 // Misc.
1507
1508 #[cfg(feature = "http")]
1509 Self::ExpandRedirect {url, headers, http_client_config_diff} => {
1510 let (url, use_task) = match url {
1511 Some(url) => (get_cow!(url, task_state, ActionError), false),
1512 None => (Cow::Borrowed(task_state.url.as_str()), true)
1513 };
1514 let _unthread_handle = task_state.unthreader.unthread();
1515 #[cfg(feature = "cache")]
1516 if let Some(entry) = task_state.cache.read(CacheEntryKeys {subject: "redirect", key: &url})? {
1517 *task_state.url = BetterUrl::parse(&entry.value.ok_or(ActionError::CachedUrlIsNone)?)?;
1518 return Ok(());
1519 }
1520 #[cfg(feature = "cache")]
1521 let start = std::time::Instant::now();
1522 let response = task_state.to_view().http_client(http_client_config_diff.as_deref())?.get(&*url).headers(headers.clone()).send()?;
1523 let new_url = if response.status().is_redirection() {
1524 BetterParseOptions::default().base_url(Some(&*if use_task {Cow::Borrowed(&**task_state.url)} else {Cow::Owned(Url::parse(&url)?)}))
1525 .parse(std::str::from_utf8(response.headers().get("location").ok_or(ActionError::LocationHeaderNotFound)?.as_bytes())?)?
1526 } else {
1527 response.url().clone().into()
1528 };
1529 #[cfg(feature = "cache")]
1530 let duration = start.elapsed();
1531 #[cfg(feature = "cache")]
1532 task_state.cache.write(NewCacheEntry {
1533 subject: "redirect",
1534 key: &url,
1535 value: Some(new_url.as_str()),
1536 duration
1537 })?;
1538 *task_state.url=new_url;
1539 },
1540
1541 Self::SetScratchpadFlag {name, value} => {
1542 let name = get_string!(name, task_state, ActionError);
1543 match value {
1544 true => task_state.scratchpad.flags.insert( name),
1545 false => task_state.scratchpad.flags.remove(&name)
1546 };
1547 },
1548 Self::SetScratchpadVar {name, value} => match get_option_string!(value, task_state) {
1549 Some(value) => {let _ = task_state.scratchpad.vars.insert( get_string!(name, task_state, ActionError), value);}
1550 None => {let _ = task_state.scratchpad.vars.remove(&get_string!(name, task_state, ActionError));}
1551 },
1552 Self::ModifyScratchpadVar {name, modification} => {
1553 let name = get_string!(name, task_state, ActionError);
1554 let mut value = task_state.scratchpad.vars.get(&name).map(|x| Cow::Borrowed(&**x));
1555 modification.apply(&mut value, &task_state.to_view())?;
1556 match value {
1557 Some(value) => {let _ = task_state.scratchpad.vars.insert( name, value.into_owned());},
1558 None => {let _ = task_state.scratchpad.vars.remove(&name);}
1559 }
1560 },
1561 #[cfg(feature = "cache")]
1562 Self::CacheUrl {subject, action} => {
1563 let _unthread_handle = task_state.unthreader.unthread();
1564 let subject = get_string!(subject, task_state, ActionError);
1565 if let Some(entry) = task_state.cache.read(CacheEntryKeys {subject: &subject, key: task_state.url.as_str()})? {
1566 *task_state.url = BetterUrl::parse(&entry.value.ok_or(ActionError::CachedUrlIsNone)?)?;
1567 return Ok(());
1568 }
1569 let old_url = task_state.url.to_string();
1570 let start = std::time::Instant::now();
1571 action.apply(task_state)?;
1572 let duration = start.elapsed();
1573 task_state.cache.write(NewCacheEntry {
1574 subject: &subject,
1575 key: &old_url,
1576 value: Some(task_state.url.as_str()),
1577 duration
1578 })?;
1579 },
1580 Self::Common(common_call) => {
1581 task_state.commons.actions.get(get_str!(common_call.name, task_state, ActionError)).ok_or(ActionError::CommonActionNotFound)?.apply(&mut TaskState {
1582 common_args: Some(&common_call.args.build(&task_state.to_view())?),
1583 url : task_state.url,
1584 scratchpad : task_state.scratchpad,
1585 context : task_state.context,
1586 job_context: task_state.job_context,
1587 params : task_state.params,
1588 commons : task_state.commons,
1589 #[cfg(feature = "cache")]
1590 cache : task_state.cache,
1591 unthreader : task_state.unthreader
1592 })?
1593 },
1594 Self::CommonCallArg(name) => task_state.common_args.ok_or(ActionError::NotInCommonContext)?.actions.get(get_str!(name, task_state, ActionError)).ok_or(ActionError::CommonCallArgActionNotFound)?.apply(task_state)?,
1595 #[cfg(feature = "custom")]
1596 Self::Custom(function) => function(task_state)?
1597 };
1598 Ok(())
1599 }
1600}