use nom::{
character::streaming::char,
combinator::{cut, map, opt},
error::context,
sequence::{preceded, terminated, tuple},
};
use ordered_float::NotNan;
use crate::{
types::{
ActorId, CategoryId, ChangeTagDefId, ChangeTagId, CommentId,
ContentModel, Expiry, ExternalLinksId, FromSql,
FullPageTitle, IResult, LogId, MajorMime, MediaType, MinorMime,
PageAction, PageCount, PageId, PageNamespace, PageRestrictionsId,
PageRestrictionsOld, PageTitle, PageType, ProtectionLevel,
RecentChangesId, RevisionId, Sha1, Timestamp, UserGroup, UserId,
},
FromSqlTuple,
};
macro_rules! mediawiki_link {
(
$text:expr,
$page:expr $(,)?
) => {
concat! {"[",
$text,
"](https://www.mediawiki.org/wiki/",
$page,
")"
}
}
}
macro_rules! with_doc_comment {
(
$comment:expr,
$($item:item)+
) => {
#[doc = $comment]
$($item)+
}
}
macro_rules! database_table_doc {
(
$table_name:ident
) => {
concat! {
"Represents the ",
mediawiki_link!(
concat!("`", stringify!($table_name), "` table"),
concat!("Manual:", stringify!($table_name), "_table"),
),
".",
}
};
(
$table_name:ident, $page_name:literal
) => {
concat!(
"Represents the ",
mediawiki_link!(
concat!("`", stringify!($table_name), "` table"),
$page_name,
),
".",
)
};
}
macro_rules! impl_row_from_sql {
(
$table_name:ident $(: $page:literal)?
$output_type:ident {
$(
$(#[$field_meta:meta])*
$field_name:ident: $type_name:ty
),+
$(,)?
}
) => {
with_doc_comment! {
database_table_doc!($table_name $(, $page)?),
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct $output_type {
$(
$(#[$field_meta])*
pub $field_name: $type_name,
)+
}
impl<'input> FromSqlTuple<'input> for $output_type {
fn from_sql_tuple(s: &'input [u8]) -> IResult<'input, Self> {
let fields = cut(
map(
tuple((
$(
terminated(
context(
concat!(
"the field “",
stringify!($field_name),
"”"
),
<$type_name>::from_sql,
),
opt(char(','))
),
)+
)),
|($($field_name),+)| $output_type {
$($field_name,)+
}
)
);
context(
concat!("row of ", stringify!($table_name), " table"),
preceded(
char('('),
terminated(
fields,
char(')')
)
)
)(s)
}
}
}
};
(
$table_name:ident $(: $page:literal)?
$output_type:ident<$life:lifetime> {
$(
$(#[$field_meta:meta])*
$field_name:ident: $type_name:ty,
)+
}
) => {
with_doc_comment! {
database_table_doc!($table_name $(, $page)?),
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct $output_type<$life> {
$($(#[$field_meta])* pub $field_name: $type_name),+
}
impl<$life> FromSqlTuple<$life> for $output_type<$life> {
fn from_sql_tuple(s: &$life [u8]) -> IResult<$life, Self> {
let fields = cut(
map(
tuple((
$(
terminated(
context(
concat!(
"the field “",
stringify!($field_name),
"”"
),
<$type_name>::from_sql,
),
opt(char(','))
),
)+
)),
|($($field_name),+)| $output_type {
$($field_name,)+
}
),
);
context(
concat!("row in ", stringify!($table_name), " table"),
preceded(
char('('),
terminated(
fields,
char(')')
)
)
)(s)
}
}
}
};
}
impl_row_from_sql! {
babel: "Extension:Babel/babel_table"
Babel<'input> {
user: UserId,
lang: &'input str,
level: &'input str,
}
}
impl_row_from_sql! {
category
Category {
id: CategoryId,
title: PageTitle,
pages: PageCount,
subcats: PageCount,
files: PageCount,
}
}
impl_row_from_sql! {
categorylinks
CategoryLinks {
from: PageId,
to: PageTitle,
sortkey: Vec<u8>,
timestamp: Timestamp,
sortkey_prefix: Vec<u8>,
collation: String,
r#type: PageType,
}
}
impl_row_from_sql! {
change_tag
ChangeTag {
id: ChangeTagId,
recent_changes_id: Option<RecentChangesId>,
log_id: Option<LogId>,
revision_id: Option<RevisionId>,
params: Option<String>,
tag_id: ChangeTagDefId,
}
}
impl_row_from_sql! {
change_tag_def
ChangeTagDef {
id: ChangeTagDefId,
name: String,
user_defined: bool,
count: u64,
}
}
impl_row_from_sql! {
externallinks
ExternalLinks {
id: ExternalLinksId,
from: PageId,
to: String,
index: Vec<u8>,
index_60: Vec<u8>,
}
}
impl_row_from_sql! {
image
Image<'input> {
name: PageTitle,
size: u32,
width: i32,
height: i32,
metadata: String,
bits: i32,
media_type: MediaType<'input>,
major_mime: MajorMime<'input>,
minor_mime: MinorMime<'input>,
description_id: CommentId,
actor: ActorId,
timestamp: Timestamp,
sha1: Sha1<'input>,
}
}
impl_row_from_sql! {
imagelinks
ImageLinks {
from: PageId,
to: PageTitle,
from_namespace: PageNamespace,
}
}
impl_row_from_sql! {
iwlinks
InterwikiLinks<'input> {
from: PageId,
prefix: &'input str,
title: PageTitle,
}
}
impl_row_from_sql! {
langlinks
LangLinks<'input> {
from: PageId,
lang: &'input str,
title: FullPageTitle,
}
}
impl_row_from_sql! {
page_restrictions
PageRestrictions<'input> {
page: PageId,
r#type: PageAction<'input>,
level: ProtectionLevel<'input>,
cascade: bool,
user: Option<u32>,
expiry: Option<Expiry>,
id: PageRestrictionsId,
}
}
impl_row_from_sql! {
page
Page<'input> {
id: PageId,
namespace: PageNamespace,
title: PageTitle,
restrictions: PageRestrictionsOld<'input>,
is_redirect: bool,
is_new: bool,
random: NotNan<f64>,
touched: Timestamp,
links_updated: Option<Timestamp>,
latest: u32,
len: u32,
content_model: Option<ContentModel<'input>>,
lang: Option<&'input str>,
}
}
impl_row_from_sql! {
pagelinks
PageLinks {
from: PageId,
namespace: PageNamespace,
title: PageTitle,
from_namespace: PageNamespace,
}
}
impl_row_from_sql! {
page_props
PageProps<'input> {
page: PageId,
name: &'input str,
value: Vec<u8>,
sortkey: Option<NotNan<f64>>,
}
}
impl_row_from_sql! {
protected_titles
ProtectedTitles<'input> {
namespace: PageNamespace,
title: PageTitle,
user: UserId,
reason_id: CommentId,
timestamp: Timestamp,
expiry: Expiry,
create_perm: ProtectionLevel<'input>,
}
}
impl_row_from_sql! {
redirect
Redirect<'input> {
from: PageId,
namespace: PageNamespace,
title: PageTitle,
interwiki: Option<&'input str>,
fragment: Option<String>,
}
}
impl_row_from_sql! {
sites
Site<'input> {
id: u32,
global_key: &'input str,
r#type: &'input str,
group: &'input str,
source: &'input str,
language: &'input str,
protocol: &'input str,
domain: &'input [u8],
data: String,
forward: i8,
config: String,
}
}
impl_row_from_sql! {
site_stats
SiteStats {
row_id: u32,
total_edits: u64,
good_articles: u64,
total_pages: u64,
users: u64,
images: u64,
active_users: u64,
}
}
impl_row_from_sql! {
wbc_entity_usage: "Wikibase/Schema/wbc_entity_usage"
WikibaseClientEntityUsage<'input> {
row_id: u64,
entity_id: &'input str,
aspect: &'input str,
page_id: PageId,
}
}
#[test]
fn test_redirect() {
use bstr::B;
let item = r"(605368,1,'разблюто','','Discussion from Stephen G. Brown\'s talk-page')";
assert_eq!(
Redirect::from_sql_tuple(item.as_bytes()),
Ok((
B(""),
Redirect {
from: PageId::from(605368),
namespace: PageNamespace::from(1),
title: PageTitle::from("разблюто".to_string()),
interwiki: Some(""),
fragment: Some(
"Discussion from Stephen G. Brown's talk-page".to_string()
),
}
))
);
}
impl_row_from_sql! {
templatelinks
TemplateLinks {
from: PageId,
namespace: PageNamespace,
title: PageTitle,
from_namespace: PageNamespace,
}
}
impl_row_from_sql! {
user_former_groups
UserFormerGroups<'input> {
user: UserId,
group: UserGroup<'input>,
}
}
impl_row_from_sql! {
user_groups
UserGroups<'input> {
user: UserId,
group: UserGroup<'input>,
expiry: Option<Expiry>,
}
}