1use nom::{
9 branch::alt,
10 bytes::streaming::tag,
11 combinator::{map, map_res},
12 error::context,
13};
14
15use std::{convert::TryFrom, ops::Deref, str::FromStr};
16
17#[cfg(feature = "serialization")]
18use serde::{Deserialize, Deserializer, Serialize, Serializer};
19
20#[cfg(test)]
21use bstr::B;
22
23use crate::from_sql::FromSql;
24use crate::from_sql::IResult;
25
26pub use ordered_float::NotNan;
28
29pub use chrono::NaiveDateTime;
31
32pub use chrono::{Datelike, Timelike};
34
35macro_rules! impl_wrapper {
36 (
38 $(#[$attrib:meta])*
39 $wrapper:ident<$l1:lifetime>: &$l2:lifetime $wrapped_type:ty
40 ) => {
41 impl_wrapper! {
42 @maybe_copy [&$l2 $wrapped_type]
43 $(#[$attrib])*
44 #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
45 pub struct $wrapper<$l1>(pub &$l2 $wrapped_type);
46
47 impl<$l1> FromSql<$l1> for $wrapper<$l1> {
48 fn from_sql(s: &$l1 [u8]) -> IResult<$l1, Self> {
49 context(
50 stringify!($wrapper),
51 map(<&$l2 $wrapped_type>::from_sql, $wrapper)
52 )(s)
53 }
54 }
55
56 #[allow(unused)]
57 impl<$l1> $wrapper<$l1> {
58 pub const fn into_inner(self) -> &$l2 $wrapped_type {
59 self.0
60 }
61 }
62
63 impl<$l1> From<$wrapper<$l1>> for &$l2 $wrapped_type {
64 fn from(val: $wrapper<$l1>) -> Self {
65 val.0
66 }
67 }
68
69 impl<$l1> From<&$l2 $wrapped_type> for $wrapper<$l1> {
70 fn from(val: &$l2 $wrapped_type) -> Self {
71 Self(val)
72 }
73 }
74 }
75 };
76 (
77 $(#[$attrib:meta])*
78 $wrapper:ident: $wrapped:ident
79 ) => {
80 impl_wrapper! {
81 @maybe_copy [$wrapped]
82 $(#[$attrib])*
83 #[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
84 pub struct $wrapper(pub $wrapped);
85
86 impl<'input> FromSql<'input> for $wrapper {
87 fn from_sql(s: &'input [u8]) -> IResult<'input, Self> {
88 context(
89 stringify!($wrapper),
90 map(<$wrapped>::from_sql, $wrapper)
91 )(s)
92 }
93 }
94
95 #[allow(unused)]
96 impl $wrapper {
97 pub fn into_inner(self) -> $wrapped {
98 self.0
99 }
100 }
101
102 impl From<$wrapper> for $wrapped {
103 fn from(val: $wrapper) -> Self {
104 val.0
105 }
106 }
107
108 impl<'a> From<&'a $wrapper> for &'a $wrapped {
109 fn from(val: &'a $wrapper) -> Self {
110 &val.0
111 }
112 }
113
114 impl From<$wrapped> for $wrapper {
115 fn from(val: $wrapped) -> Self {
116 Self(val)
117 }
118 }
119 }
120 };
121 (
122 @maybe_copy [$(u32)? $(i32)? $(&$l:lifetime $t:ty)?]
123 $($rest:item)+
124 ) => {
125 #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
126 $($rest)+
127 };
128 (
129 @maybe_copy [$($anything:tt)?]
130 $($rest:item)+
131 ) => {
132 #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
133 $($rest)+
134 };
135}
136
137impl_wrapper! {
138 #[doc = "
139Represents
140[`page_id`](https://www.mediawiki.org/wiki/Manual:Page_table#page_id),
141the primary key of the `page` table, as well as other fields in
142other tables that correspond to it.
143"]
144 PageId: u32
145}
146
147impl_wrapper! {
148 #[doc = "
149Represents the
150[`page_namespace`](https://www.mediawiki.org/wiki/Manual:Page_table#page_namespace)
151field of the `page` table.
152"]
153 PageNamespace: i32
154}
155
156impl_wrapper! {
157 #[doc="
158Represents the
159[`page_title`](https://www.mediawiki.org/wiki/Manual:Page_table#page_title)
160field of the `page` table, a title with underscores.
161"]
162 PageTitle: String
163}
164
165impl_wrapper! {
166 #[doc="
167Represents a page title with namespace and with spaces rather than underscores,
168as in the
169[`ll_title`](https://www.mediawiki.org/wiki/Manual:Langlinks_table#ll_title)
170field of the `langlinks` table.
171"]
172 FullPageTitle: String
173}
174
175impl_wrapper! {
176 #[doc="
177Represents [`lt_id`](https://www.mediawiki.org/wiki/Manual:Linktarget_table#lt_id),
178the primary key of the `linktarget` table.
179"]
180 LinkTargetId: u64
181}
182
183impl_wrapper! {
184 #[doc = "
185Represents
186[`cat_id`](https://www.mediawiki.org/wiki/Manual:Category_table#cat_id),
187the primary key of the `category` table.
188"]
189 CategoryId: u32
190}
191
192impl_wrapper! {
193 #[doc = "
194Represents
195[`cat_pages`](https://www.mediawiki.org/wiki/Manual:Category_table#cat_id),
196[`cat_subcats`](https://www.mediawiki.org/wiki/Manual:Category_table#cat_subcats),
197and [`cat_files`](https://www.mediawiki.org/wiki/Manual:Category_table#cat_files)
198fields of the `category` table. They should logically be greater than
199or equal to 0, but because of errors can be negative.
200"]
201 PageCount: i32
202}
203
204impl_wrapper! {
205 #[doc = "
206Represents
207[`log_id`](https://www.mediawiki.org/wiki/Manual:Logging_table#log_id),
208the primary key of the `logging` table.
209"]
210 LogId: u32
211}
212
213impl_wrapper! {
214 #[doc = "
215Represents
216[`ct_id`](https://www.mediawiki.org/wiki/Manual:Change_tag_table#ct_id),
217the primary key of the `change_tag` table.
218"]
219 ChangeTagId: u32
220}
221
222impl_wrapper! {
223 #[doc = "
224Represents
225[`rev_id`](https://www.mediawiki.org/wiki/Manual:Revision_table#rev_id),
226the primary key of the `revision` table.
227"]
228 RevisionId: u32
229}
230
231impl_wrapper! {
232 #[doc = "
233Represents
234[`ctd_id`](https://www.mediawiki.org/wiki/Manual:Change_tag_def_table#ctd_id),
235the primary key of the `change_tag_def` table.
236"]
237 ChangeTagDefinitionId: u32
238}
239
240impl_wrapper! {
241 #[doc = "
242Represents
243[`rc_id`](https://www.mediawiki.org/wiki/Manual:Recentchanges_table#rc_id),
244the primary key of the `recentchanges` table.
245"]
246 RecentChangeId: u32
247}
248
249impl_wrapper! {
250 #[doc = "
251Represents
252[`el_id`](https://www.mediawiki.org/wiki/Manual:Externallinks_table#el_id),
253the primary key of the `externallinks` table.
254"]
255 ExternalLinkId: u32
256}
257
258impl_wrapper! {
259 #[doc = "
260Represents the
261[`img_minor_mime`](https://www.mediawiki.org/wiki/Manual:Image_table#img_minor_mime)
262field of the `image` table.
263"]
264 MinorMime<'a>: &'a str
265}
266
267impl_wrapper! {
268 #[doc = "
269Represents
270[`comment_id`](https://www.mediawiki.org/wiki/Manual:Comment_table#comment_id),
271the primary key of the `comment` table.
272"]
273 CommentId: u32
274}
275
276impl_wrapper! {
277 #[doc = "
278Represents
279[`actor_id`](https://www.mediawiki.org/wiki/Manual:Actor_table#actor_id),
280the primary key of the `actor` table.
281"]
282 ActorId: u32
283}
284
285impl_wrapper! {
286 #[doc = "
287Represents a SHA-1 hash in base 36, for instance in the
288[`img_sha1`](https://www.mediawiki.org/wiki/Manual:Image_table#img_sha1)
289field of the `image` table.
290"]
291 Sha1<'a>: &'a str
292}
293
294impl_wrapper! {
295 #[doc = "
296Represents
297[`pr_id`](https://www.mediawiki.org/wiki/Manual:Page_restrictions_table#pr_id),
298the primary key of the `page_restrictions` table.
299"]
300 PageRestrictionId: u32
301}
302
303impl_wrapper! {
304 #[doc = "
305Represents
306[`user_id`](https://www.mediawiki.org/wiki/Manual:User_table#user_id),
307the primary key of the `user` table.
308"]
309 UserId: u32
310}
311
312impl_wrapper! {
313 #[doc = "
314Represents the name of a user group, such as the
315[`ug_group`](https://www.mediawiki.org/wiki/Manual:User_groups_table#ug_group)
316field of the `user_groups` table.
317"]
318 UserGroup<'a>: &'a str
319}
320
321#[test]
322fn test_copy_for_wrappers() {
323 use static_assertions::*;
324 assert_impl_all!(PageId: Copy);
325 assert_not_impl_all!(PageTitle: Copy);
326 assert_impl_all!(PageNamespace: Copy);
327 assert_impl_all!(UserGroup: Copy);
328}
329
330#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
334#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
335pub struct Timestamp(pub NaiveDateTime);
336
337impl<'input> FromSql<'input> for Timestamp {
338 fn from_sql(s: &'input [u8]) -> IResult<'input, Self> {
339 context(
340 "Timestamp in yyyymmddhhmmss or yyyy-mm-dd hh:mm::ss format",
341 map_res(<&str>::from_sql, |s| {
342 NaiveDateTime::parse_from_str(
343 s,
344 if s.len() == 14 {
345 "%Y%m%d%H%M%S"
346 } else {
347 "%Y-%m-%d %H:%M:%S"
348 },
349 )
350 .map(Timestamp)
351 }),
352 )(s)
353 }
354}
355
356impl Deref for Timestamp {
357 type Target = NaiveDateTime;
358
359 fn deref(&self) -> &Self::Target {
360 &self.0
361 }
362}
363
364#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
368#[cfg_attr(
369 feature = "serialization",
370 derive(Serialize, Deserialize),
371 serde(try_from = "&str", into = "String")
372)]
373pub enum Expiry {
374 Timestamp(Timestamp),
375 Infinity,
376}
377
378impl TryFrom<&str> for Expiry {
379 type Error = <NaiveDateTime as FromStr>::Err;
380
381 fn try_from(s: &str) -> Result<Self, Self::Error> {
382 match s {
383 "infinity" => Ok(Expiry::Infinity),
384 s => Ok(Expiry::Timestamp(Timestamp(s.parse()?))),
385 }
386 }
387}
388
389impl From<Expiry> for String {
390 fn from(e: Expiry) -> Self {
391 match e {
392 Expiry::Timestamp(t) => t.to_string(),
393 Expiry::Infinity => "infinity".to_string(),
394 }
395 }
396}
397
398impl<'input> FromSql<'input> for Expiry {
399 fn from_sql(s: &'input [u8]) -> IResult<'input, Self> {
400 context(
401 "Expiry",
402 alt((
403 map(Timestamp::from_sql, Expiry::Timestamp),
404 context("“infinity”", map(tag("'infinity'"), |_| Expiry::Infinity)),
405 )),
406 )(s)
407 }
408}
409
410#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
440#[cfg_attr(
441 feature = "serialization",
442 derive(Serialize, Deserialize),
443 serde(try_from = "&str", into = "&'static str")
444)]
445pub enum PageType {
446 Page,
447 Subcat,
448 File,
449}
450
451impl<'a> TryFrom<&'a str> for PageType {
453 type Error = &'a str;
454
455 fn try_from(s: &'a str) -> Result<Self, &'a str> {
456 use PageType::*;
457 match s {
458 "page" => Ok(Page),
459 "subcat" => Ok(Subcat),
460 "file" => Ok(File),
461 other => Err(other),
462 }
463 }
464}
465
466impl From<PageType> for &'static str {
467 fn from(s: PageType) -> &'static str {
468 use PageType::*;
469 match s {
470 Page => "page",
471 Subcat => "subcat",
472 File => "file",
473 }
474 }
475}
476
477impl<'a> FromSql<'a> for PageType {
478 fn from_sql(s: &'a [u8]) -> IResult<'a, Self> {
479 context("PageType", map_res(<&str>::from_sql, PageType::try_from))(s)
480 }
481}
482
483#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
487#[cfg_attr(
488 feature = "serialization",
489 derive(Serialize, Deserialize),
490 serde(from = "&'a str", into = "&'a str")
491)]
492pub enum PageAction<'a> {
493 Edit,
494 Move,
495 Reply,
496 Upload,
497 All,
498 Other(&'a str),
499}
500
501impl<'a> From<&'a str> for PageAction<'a> {
502 fn from(s: &'a str) -> Self {
503 use PageAction::*;
504 match s {
505 "edit" => Edit,
506 "move" => Move,
507 "reply" => Reply,
508 "upload" => Upload,
509 _ => Other(s),
510 }
511 }
512}
513
514impl<'a> From<PageAction<'a>> for &'a str {
515 fn from(p: PageAction<'a>) -> Self {
516 use PageAction::*;
517 match p {
518 Edit => "edit",
519 Move => "move",
520 Reply => "reply",
521 Upload => "upload",
522 All => "all",
523 Other(s) => s,
524 }
525 }
526}
527
528impl<'a> FromSql<'a> for PageAction<'a> {
529 fn from_sql(s: &'a [u8]) -> IResult<'a, Self> {
530 map(<&str>::from_sql, PageAction::from)(s)
531 }
532}
533
534#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
539#[cfg_attr(
540 feature = "serialization",
541 derive(Serialize, Deserialize),
542 serde(from = "&'a str", into = "&'a str")
543)]
544pub enum ProtectionLevel<'a> {
545 Autoconfirmed,
546 ExtendedConfirmed,
547 Sysop,
548 TemplateEditor,
549 EditProtected,
550 EditSemiProtected,
551 None,
553 Other(&'a str),
554}
555
556impl<'a> From<&'a str> for ProtectionLevel<'a> {
557 fn from(s: &'a str) -> Self {
558 use ProtectionLevel::*;
559 match s {
560 "autoconfirmed" => Autoconfirmed,
561 "extendedconfirmed" => ExtendedConfirmed,
562 "templateeditor" => TemplateEditor,
563 "sysop" => Sysop,
564 "editprotected" => EditProtected,
565 "editsemiprotected" => EditSemiProtected,
566 "" => None,
567 _ => Other(s),
568 }
569 }
570}
571
572impl<'a> From<ProtectionLevel<'a>> for &'a str {
573 fn from(p: ProtectionLevel<'a>) -> &'a str {
574 use ProtectionLevel::*;
575 match p {
576 Autoconfirmed => "autoconfirmed",
577 ExtendedConfirmed => "extendedconfirmed",
578 TemplateEditor => "templateeditor",
579 Sysop => "sysop",
580 EditProtected => "editprotected",
581 EditSemiProtected => "editsemiprotected",
582 None => "",
583 Other(s) => s,
584 }
585 }
586}
587
588impl<'a> FromSql<'a> for ProtectionLevel<'a> {
589 fn from_sql(s: &'a [u8]) -> IResult<'a, Self> {
590 context(
591 "ProtectionLevel",
592 map(<&str>::from_sql, ProtectionLevel::from),
593 )(s)
594 }
595}
596
597#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
601#[cfg_attr(
602 feature = "serialization",
603 derive(Serialize, Deserialize),
604 serde(from = "&'a str", into = "&'a str")
605)]
606pub enum ContentModel<'a> {
607 Wikitext,
608 Scribunto,
609 Text,
610 Css,
611 SanitizedCss,
612 JavaScript,
613 Json,
614 #[cfg_attr(feature = "serialization", serde(borrow))]
615 Other(&'a str),
616}
617
618impl<'a> From<&'a str> for ContentModel<'a> {
619 fn from(s: &'a str) -> Self {
620 use ContentModel::*;
621 match s {
622 "wikitext" => Wikitext,
623 "Scribunto" => Scribunto,
624 "text" => Text,
625 "css" => Css,
626 "sanitized-css" => SanitizedCss,
627 "javascript" => JavaScript,
628 "json" => Json,
629 _ => Other(s),
630 }
631 }
632}
633
634impl<'a> From<ContentModel<'a>> for &'a str {
635 fn from(c: ContentModel<'a>) -> Self {
636 use ContentModel::*;
637 match c {
638 Wikitext => "wikitext",
639 Scribunto => "Scribunto",
640 Text => "text",
641 Css => "css",
642 SanitizedCss => "sanitized-css",
643 JavaScript => "javascript",
644 Json => "json",
645 Other(s) => s,
646 }
647 }
648}
649
650impl<'a> FromSql<'a> for ContentModel<'a> {
651 fn from_sql(s: &'a [u8]) -> IResult<'a, Self> {
652 context("ContentModel", map(<&str>::from_sql, ContentModel::from))(s)
653 }
654}
655
656#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
660#[cfg_attr(
661 feature = "serialization",
662 derive(Serialize, Deserialize),
663 serde(from = "&'a str", into = "&'a str")
664)]
665pub enum MediaType<'a> {
666 Unknown,
667 Bitmap,
668 Drawing,
669 Audio,
670 Video,
671 Multimedia,
672 Office,
673 Text,
674 Executable,
675 Archive,
676 ThreeDimensional,
677 #[cfg_attr(feature = "serialization", serde(borrow))]
678 Other(&'a str),
679}
680
681impl<'a> From<&'a str> for MediaType<'a> {
682 fn from(s: &'a str) -> Self {
683 use MediaType::*;
684 match s {
685 "UNKNOWN" => Unknown,
686 "BITMAP" => Bitmap,
687 "DRAWING" => Drawing,
688 "AUDIO" => Audio,
689 "VIDEO" => Video,
690 "MULTIMEDIA" => Multimedia,
691 "OFFICE" => Office,
692 "TEXT" => Text,
693 "EXECUTABLE" => Executable,
694 "ARCHIVE" => Archive,
695 "3D" => ThreeDimensional,
696 _ => Other(s),
697 }
698 }
699}
700
701impl<'a> From<MediaType<'a>> for &'a str {
702 fn from(s: MediaType<'a>) -> Self {
703 use MediaType::*;
704 match s {
705 Unknown => "UNKNOWN",
706 Bitmap => "BITMAP",
707 Drawing => "DRAWING",
708 Audio => "AUDIO",
709 Video => "VIDEO",
710 Multimedia => "MULTIMEDIA",
711 Office => "OFFICE",
712 Text => "TEXT",
713 Executable => "EXECUTABLE",
714 Archive => "ARCHIVE",
715 ThreeDimensional => "3D",
716 Other(s) => s,
717 }
718 }
719}
720
721impl<'a> FromSql<'a> for MediaType<'a> {
722 fn from_sql(s: &'a [u8]) -> IResult<'a, Self> {
723 context("MediaType", map(<&str>::from_sql, MediaType::from))(s)
724 }
725}
726
727#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
731#[cfg_attr(
732 feature = "serialization",
733 derive(Serialize, Deserialize),
734 serde(from = "&'a str", into = "&'a str")
735)]
736pub enum MajorMime<'a> {
737 Unknown,
738 Application,
739 Audio,
740 Image,
741 Text,
742 Video,
743 Message,
744 Model,
745 Multipart,
746 #[cfg_attr(feature = "serialization", serde(borrow))]
747 Other(&'a str),
748}
749
750impl<'a> From<&'a str> for MajorMime<'a> {
751 fn from(s: &'a str) -> Self {
752 use MajorMime::*;
753 match s {
754 "unknown" => Unknown,
755 "application" => Application,
756 "audio" => Audio,
757 "image" => Image,
758 "text" => Text,
759 "video" => Video,
760 "message" => Message,
761 "model" => Model,
762 "multipart" => Multipart,
763 _ => Other(s),
764 }
765 }
766}
767
768impl<'a> From<MajorMime<'a>> for &'a str {
769 fn from(s: MajorMime<'a>) -> Self {
770 use MajorMime::*;
771 match s {
772 Unknown => "unknown",
773 Application => "application",
774 Audio => "audio",
775 Image => "image",
776 Text => "text",
777 Video => "video",
778 Message => "message",
779 Model => "model",
780 Multipart => "multipart",
781 Other(s) => s,
782 }
783 }
784}
785
786impl<'a> FromSql<'a> for MajorMime<'a> {
787 fn from_sql(s: &'a [u8]) -> IResult<'a, Self> {
788 context("MajorMime", map(<&str>::from_sql, MajorMime::from))(s)
789 }
790}
791
792#[test]
793fn test_bool() {
794 for (s, v) in &[(B("0"), false), (B("1"), true)] {
795 assert_eq!(bool::from_sql(s), Ok((B(""), *v)));
796 }
797}
798
799#[test]
800fn test_numbers() {
801 fn from_utf8(s: &[u8]) -> &str {
802 std::str::from_utf8(s).unwrap()
803 }
804
805 let f = B("0.37569 ");
807 let res = f64::from_sql(f);
808 assert_eq!(res, Ok((B(" "), from_utf8(f).trim_end().parse().unwrap())));
809
810 for i in &[B("1 "), B("-1 ")] {
811 assert_eq!(
812 i32::from_sql(i),
813 Ok((B(" "), from_utf8(i).trim_end().parse().unwrap()))
814 );
815 }
816}
817
818#[test]
819fn test_string() {
820 let strings = &[
821 (B(r"'\''"), r"'"),
822 (br"'\\'", r"\"),
823 (br"'\n'", "\n"),
824 (br"'string'", r"string"),
825 (
826 br#"'English_words_ending_in_\"-vorous\",_\"-phagous\"_and_similar_endings'"#,
827 r#"English_words_ending_in_"-vorous",_"-phagous"_and_similar_endings"#,
828 ),
829 ];
830 for (s, unescaped) in strings {
831 assert_eq!(String::from_sql(s), Ok((B(""), (*unescaped).to_string())));
832 }
833}
834
835#[cfg(feature = "serialization")]
836pub(crate) fn serialize_not_nan<S>(not_nan: &NotNan<f64>, serializer: S) -> Result<S::Ok, S::Error>
837where
838 S: Serializer,
839{
840 serializer.serialize_f64(not_nan.into_inner())
841}
842
843#[cfg(feature = "serialization")]
844pub(crate) fn deserialize_not_nan<'de, D>(deserializer: D) -> Result<NotNan<f64>, D::Error>
845where
846 D: Deserializer<'de>,
847{
848 NotNan::new(f64::deserialize(deserializer)?).map_err(serde::de::Error::custom)
849}
850
851#[cfg(feature = "serialization")]
852pub(crate) fn serialize_option_not_nan<S>(
853 not_nan: &Option<NotNan<f64>>,
854 serializer: S,
855) -> Result<S::Ok, S::Error>
856where
857 S: Serializer,
858{
859 not_nan.map(|n| n.into_inner()).serialize(serializer)
860}
861
862#[cfg(feature = "serialization")]
863pub(crate) fn deserialize_option_not_nan<'de, D>(
864 deserializer: D,
865) -> Result<Option<NotNan<f64>>, D::Error>
866where
867 D: Deserializer<'de>,
868{
869 <Option<f64>>::deserialize(deserializer)?
870 .map(|v| NotNan::new(v).map_err(serde::de::Error::custom))
871 .transpose()
872}