1use std::{error::Error, fmt, marker::PhantomData, str::FromStr};
2
3mod sealed {
4 pub trait ToStaticStr {
6 fn to_static_str(&self) -> &'static str;
7 }
8
9 pub trait RequestState {}
13 pub trait RequestField: ToStaticStr + Into<super::AnyField> {}
14}
15
16pub const TIMEMAP_BASE: &str = "https://web.archive.org/web/timemap/?";
20
21pub struct Filter<'a> {
27 invert: bool,
28 field: Field,
29 regex: &'a str,
30}
31
32impl<'a> Filter<'a> {
33 pub fn parse_from_str(s: &'a str) -> Result<Self, ParseFilterError> {
35 let (invert, input) = if s.starts_with('!') {
36 (true, s )
37 } else {
38 (false, s)
39 };
40
41 let mut split = input.splitn(2, ':');
42 let field: Field = if let Some(f) = split.next() {
43 f.parse().map_err(ParseFilterError::UnknownField)?
44 } else {
45 panic!("splitn should always return at least one item");
46 };
47
48 if let Some(regex) = split.next() {
49 Ok(Filter {
50 invert,
51 field,
52 regex,
53 })
54 } else {
55 Err(ParseFilterError::MissingColon)
56 }
57 }
58
59 pub fn to_owned(&self) -> FilterBuf {
61 FilterBuf {
62 invert: self.invert,
63 field: self.field,
64 regex: self.regex.to_owned(),
65 }
66 }
67}
68
69pub struct FilterBuf {
71 invert: bool,
72 field: Field,
73 regex: String,
74}
75
76impl FilterBuf {
77 pub fn as_ref(&self) -> Filter {
81 Filter {
82 invert: self.invert,
83 field: self.field,
84 regex: &self.regex,
85 }
86 }
87}
88
89#[derive(Debug)]
90pub enum ParseFilterError {
94 MissingColon,
95 UnknownField(UnknownFieldError),
96}
97
98impl Error for ParseFilterError {}
99
100impl fmt::Display for ParseFilterError {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match self {
103 Self::MissingColon => write!(f, "Missing `:` between field name and regex"),
104 Self::UnknownField(e) => write!(f, "{}", e),
105 }
106 }
107}
108
109impl FromStr for FilterBuf {
110 type Err = ParseFilterError;
111
112 fn from_str(s: &str) -> Result<Self, Self::Err> {
113 Filter::parse_from_str(s).map(|f| f.to_owned())
114 }
115}
116
117impl<'a> Filter<'a> {
118 pub fn new(field: Field, regex: &'a str) -> Self {
120 Self {
121 invert: false,
122 field,
123 regex,
124 }
125 }
126
127 pub fn inverted(field: Field, regex: &'a str) -> Self {
129 Self {
130 invert: true,
131 field,
132 regex,
133 }
134 }
135}
136
137pub struct Request<'a> {
157 url: &'a str,
159 output: Output,
161 fl: Vec<AnyField>,
163 filter: Vec<Filter<'a>>,
165 match_type: MatchType,
167 collapse: Option<Field>,
169}
170
171impl<'a> Request<'a> {
172 pub fn builder(url: &'a str) -> RequestBuilder<'a, BasicRequest> {
174 RequestBuilder::new(url)
175 }
176
177 pub fn to_url(&self) -> String {
179 use sealed::ToStaticStr;
180
181 let mut url = TIMEMAP_BASE.to_string();
182 url.push_str("url=");
183 url.push_str(&urlencoding::encode(self.url));
184 let mut fl_iter = self.fl.iter();
185 if let Some(s) = fl_iter.next() {
186 url.push_str("&fl=");
187 url.push_str(s.to_static_str());
188 for f in fl_iter {
189 url.push_str(",");
190 url.push_str(f.to_static_str());
191 }
192 }
193 if let Some(match_type) = self.match_type.opt_static_str() {
194 url.push_str("&matchType=");
195 url.push_str(match_type);
196 }
197 if let Some(output) = self.output.opt_static_str() {
198 url.push_str("&output=");
199 url.push_str(output);
200 }
201 if let Some(collapse) = self.collapse {
202 url.push_str("&collapse=");
203 url.push_str(collapse.to_static_str());
204 }
205 let filter_iter = self.filter.iter();
206 for filter in filter_iter {
207 url.push_str("&filter=");
208 if filter.invert {
209 url.push_str("!");
210 }
211 url.push_str(filter.field.to_static_str());
212 url.push_str(":");
213 url.push_str(filter.regex);
214 }
215 url
216 }
217}
218
219pub struct RequestBuilder<'a, S: sealed::RequestState> {
222 request: Request<'a>,
224 _state: PhantomData<S>,
226}
227
228pub struct BasicRequest;
230pub struct GroupedRequest;
232
233impl sealed::RequestState for BasicRequest {}
234impl sealed::RequestState for GroupedRequest {}
235
236impl<'a, S: sealed::RequestState> RequestBuilder<'a, S> {
237 pub fn done(self) -> Request<'a> {
239 self.request
240 }
241
242 pub fn match_type(mut self, match_type: MatchType) -> Self {
244 self.request.match_type = match_type;
245 self
246 }
247
248 pub fn match_prefix(self) -> Self {
250 self.match_type(MatchType::Prefix)
251 }
252
253 pub fn output(mut self, output: Output) -> Self {
255 self.request.output = output;
256 self
257 }
258
259 pub fn with_filter(mut self, filter: Filter<'a>) -> Self {
263 self.request.filter.push(filter);
264 self
265 }
266
267 pub fn filter(self, field: Field, regex: &'a str) -> Self {
269 self.with_filter(Filter::new(field, regex))
270 }
271
272 pub fn filter_inverted(self, field: Field, regex: &'a str) -> Self {
274 self.with_filter(Filter::inverted(field, regex))
275 }
276}
277
278impl<'a> RequestBuilder<'a, BasicRequest> {
279 pub fn new(url: &'a str) -> Self {
281 Self {
282 request: Request {
283 url,
284 output: Output::Default,
285 fl: Vec::new(),
286 match_type: MatchType::Exact,
287 collapse: None,
288 filter: Vec::new(),
289 },
290 _state: PhantomData,
291 }
292 }
293
294 pub fn with_field(mut self, field: Field) -> Self {
301 self.request.fl.push(field.into());
302 self
303 }
304
305 pub fn collapse(mut self, field: Field) -> RequestBuilder<'a, GroupedRequest> {
312 self.request.collapse = Some(field);
313 RequestBuilder {
314 request: self.request,
315 _state: PhantomData,
316 }
317 }
318}
319
320impl<'a> RequestBuilder<'a, GroupedRequest> {
321 pub fn with_field<F: sealed::RequestField>(mut self, field: F) -> Self {
329 self.request.fl.push(field.into());
330 self
331 }
332}
333
334#[derive(Copy, Clone, Debug, Eq, PartialEq)]
336pub enum Field {
337 Original,
339 Timestamp,
341 UrlKey,
344 MimeType,
346 StatusCode,
348 Digest,
350 Redirect,
352 RobotFlags,
354 Length,
356 Offset,
358 Filename,
360}
361
362impl sealed::RequestField for Field {}
363
364trait OptStaticStr {
368 fn opt_static_str(&self) -> Option<&'static str>;
369}
370
371trait HasDefaultVariant {
373 fn is_default(&self) -> bool;
374}
375
376#[derive(Copy, Clone, Debug, Eq, PartialEq)]
378pub enum GroupField {
379 EndTimestamp,
380 GroupCount,
381 UniqCount,
382}
383
384#[derive(Copy, Clone, Debug, Eq, PartialEq)]
386pub enum AnyField {
387 Basic(Field),
388 Group(GroupField),
389}
390
391impl sealed::ToStaticStr for AnyField {
392 fn to_static_str(&self) -> &'static str {
393 match self {
394 Self::Basic(f) => f.to_static_str(),
395 Self::Group(f) => f.to_static_str(),
396 }
397 }
398}
399
400impl From<Field> for AnyField {
401 fn from(f: Field) -> Self {
402 Self::Basic(f)
403 }
404}
405
406impl From<GroupField> for AnyField {
407 fn from(f: GroupField) -> Self {
408 Self::Group(f)
409 }
410}
411
412impl sealed::RequestField for GroupField {}
413
414impl sealed::ToStaticStr for Field {
415 fn to_static_str(&self) -> &'static str {
416 match self {
417 Self::Original => "original",
418 Self::Timestamp => "timestamp",
419 Self::UrlKey => "urlkey",
420 Self::MimeType => "mimetype",
421 Self::StatusCode => "statuscode",
422 Self::Digest => "digest",
423 Self::Redirect => "redirect",
424 Self::RobotFlags => "robotflags",
425 Self::Length => "length",
426 Self::Offset => "offset",
427 Self::Filename => "filename",
428 }
429 }
430}
431
432#[derive(Debug)]
433pub struct UnknownFieldError(String);
435
436impl fmt::Display for UnknownFieldError {
437 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438 write!(f, "Unknown field name `{}`", self.0)
439 }
440}
441
442impl Error for UnknownFieldError {}
443
444impl FromStr for Field {
445 type Err = UnknownFieldError;
446 fn from_str(s: &str) -> Result<Self, Self::Err> {
447 match s {
448 "original" => Ok(Self::Original),
449 "timestamp" => Ok(Self::Timestamp),
450 "urlkey" => Ok(Self::UrlKey),
451 "mimetype" => Ok(Self::MimeType),
452 "statuscode" => Ok(Self::StatusCode),
453 "digest" => Ok(Self::Digest),
454 "redirect" => Ok(Self::Redirect),
455 "robotflags" => Ok(Self::RobotFlags),
456 "length" => Ok(Self::Length),
457 "offset" => Ok(Self::Offset),
458 "filename" => Ok(Self::Filename),
459 _ => Err(UnknownFieldError(s.to_owned())),
460 }
461 }
462}
463
464impl sealed::ToStaticStr for GroupField {
465 fn to_static_str(&self) -> &'static str {
466 match self {
467 Self::EndTimestamp => "endtimestamp",
468 Self::GroupCount => "groupcount",
469 Self::UniqCount => "uniqcount",
470 }
471 }
472}
473
474#[derive(Debug, Eq, PartialEq)]
476pub enum MatchType {
477 Exact,
479 Prefix,
481}
482
483impl Default for MatchType {
484 fn default() -> Self {
485 Self::Exact
486 }
487}
488
489impl HasDefaultVariant for MatchType {
490 fn is_default(&self) -> bool {
491 *self == Self::Exact
492 }
493}
494
495impl OptStaticStr for MatchType {
496 fn opt_static_str(&self) -> Option<&'static str> {
497 match self {
498 Self::Exact => None,
499 Self::Prefix => Some("prefix"),
500 }
501 }
502}
503
504impl sealed::ToStaticStr for MatchType {
505 fn to_static_str(&self) -> &'static str {
506 match self {
507 Self::Exact => "exact",
508 Self::Prefix => "prefix",
509 }
510 }
511}
512
513#[derive(Debug, Eq, PartialEq)]
515pub enum Output {
516 Default,
518 Json,
520 Link,
522}
523
524impl HasDefaultVariant for Output {
525 fn is_default(&self) -> bool {
526 *self == Self::Default
527 }
528}
529
530impl OptStaticStr for Output {
531 fn opt_static_str(&self) -> Option<&'static str> {
532 match self {
533 Self::Default => None,
534 Self::Json => Some("json"),
535 Self::Link => Some("link"),
536 }
537 }
538}
539
540impl sealed::ToStaticStr for Output {
541 fn to_static_str(&self) -> &'static str {
542 match self {
543 Self::Default => "",
544 Self::Json => "json",
545 Self::Link => "link",
546 }
547 }
548}