use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Version {
pub identifier: u64,
pub editor: Option<Editor>,
pub comment: Option<String>,
pub tags: Option<Vec<String>>,
pub has_tag_needs_citation: Option<bool>,
pub is_minor_edit: Option<bool>,
pub is_flagged_stable: Option<bool>,
pub is_breaking_news: Option<bool>,
pub noindex: Option<bool>,
pub number_of_characters: Option<u64>,
pub size: Option<ArticleSize>,
pub maintenance_tags: Option<MaintenanceTags>,
pub scores: Option<Scores>,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct PreviousVersion {
pub identifier: u64,
pub editor: Option<Editor>,
#[serde(skip_serializing_if = "Option::is_none")]
pub number_of_characters: Option<u64>,
}
impl<'de> Deserialize<'de> for PreviousVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawPreviousVersion {
identifier: Option<u64>,
#[serde(default)]
number_of_characters: Option<u64>,
}
let raw = RawPreviousVersion::deserialize(deserializer)?;
match raw.identifier {
Some(id) => Ok(PreviousVersion {
identifier: id,
editor: None,
number_of_characters: raw.number_of_characters,
}),
None => Err(serde::de::Error::custom(
"previous_version must have an identifier field",
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct OptionalPreviousVersion(pub Option<PreviousVersion>);
impl<'de> Deserialize<'de> for OptionalPreviousVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawPreviousVersion {
identifier: Option<u64>,
#[serde(default)]
number_of_characters: Option<u64>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum RawOptPreviousVersion {
None,
Some(RawPreviousVersion),
}
match RawOptPreviousVersion::deserialize(deserializer)? {
RawOptPreviousVersion::None => Ok(OptionalPreviousVersion(None)),
RawOptPreviousVersion::Some(raw) => match raw.identifier {
Some(id) => Ok(OptionalPreviousVersion(Some(PreviousVersion {
identifier: id,
editor: None,
number_of_characters: raw.number_of_characters,
}))),
None => Ok(OptionalPreviousVersion(None)),
},
}
}
}
impl Serialize for OptionalPreviousVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match &self.0 {
Some(pv) => pv.serialize(serializer),
None => serializer.serialize_none(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Editor {
pub identifier: Option<u64>,
pub name: Option<String>,
pub is_bot: Option<bool>,
pub is_anonymous: Option<bool>,
pub date_started: Option<DateTime<Utc>>,
pub edit_count: Option<u64>,
pub groups: Option<Vec<String>>,
pub is_admin: Option<bool>,
pub is_patroller: Option<bool>,
pub has_advanced_rights: Option<bool>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ArticleSize {
pub value: u64,
pub unit_text: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct MaintenanceTags {
pub citation_needed_count: Option<u64>,
pub pov_count: Option<u64>,
pub clarification_needed_count: Option<u64>,
pub update_count: Option<u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Scores {
pub revertrisk: Option<RevertRisk>,
pub referencerisk: Option<ReferenceRisk>,
pub referenceneed: Option<ReferenceNeed>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct RevertRisk {
pub prediction: Option<bool>,
pub probability: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ReferenceRisk {
pub reference_risk_score: Option<f64>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ReferenceNeed {
pub reference_need_score: Option<f64>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Protection {
#[serde(rename = "type")]
pub protection_type: String,
pub level: String,
#[serde(deserialize_with = "deserialize_expiry")]
pub expiry: Option<DateTime<Utc>>,
}
fn deserialize_expiry<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum RawExpiry {
DateTime(DateTime<Utc>),
Infinity(String),
Null,
}
match RawExpiry::deserialize(deserializer)? {
RawExpiry::DateTime(dt) => Ok(Some(dt)),
RawExpiry::Infinity(s) if s == "infinity" => Ok(None),
RawExpiry::Infinity(s) => Err(serde::de::Error::custom(format!(
"invalid expiry value: {}",
s
))),
RawExpiry::Null => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn test_version_creation() {
let version = Version {
identifier: 1182847293,
editor: Some(Editor {
identifier: Some(12345),
name: Some("TestUser".to_string()),
is_bot: Some(false),
is_anonymous: Some(false),
date_started: Some(Utc::now()),
edit_count: Some(1000),
groups: Some(vec!["user".to_string()]),
is_admin: Some(false),
is_patroller: Some(false),
has_advanced_rights: Some(false),
}),
comment: Some("Test edit".to_string()),
tags: Some(vec!["mobile edit".to_string()]),
has_tag_needs_citation: Some(false),
is_minor_edit: Some(false),
is_flagged_stable: Some(true),
is_breaking_news: Some(false),
noindex: Some(false),
number_of_characters: Some(5000),
size: Some(ArticleSize {
value: 15000,
unit_text: "B".to_string(),
}),
maintenance_tags: None,
scores: None,
};
assert_eq!(version.identifier, 1182847293);
assert!(version.is_flagged_stable.unwrap());
}
#[test]
fn test_editor_groups() {
let editor = Editor {
identifier: Some(12345),
name: Some("AdminUser".to_string()),
is_bot: Some(false),
is_anonymous: Some(false),
date_started: Some(Utc::now()),
edit_count: Some(5000),
groups: Some(vec![
"user".to_string(),
"autoconfirmed".to_string(),
"extendedconfirmed".to_string(),
]),
is_admin: Some(true),
is_patroller: Some(true),
has_advanced_rights: Some(true),
};
let groups = editor.groups.as_ref().unwrap();
assert!(groups.contains(&"user".to_string()));
assert!(groups.contains(&"autoconfirmed".to_string()));
assert!(editor.is_admin.unwrap());
}
#[test]
fn test_maintenance_tags() {
let tags = MaintenanceTags {
citation_needed_count: Some(5),
pov_count: Some(1),
clarification_needed_count: Some(2),
update_count: Some(10),
};
assert_eq!(tags.citation_needed_count, Some(5));
assert_eq!(tags.pov_count, Some(1));
}
#[test]
fn test_protection() {
let protection = Protection {
protection_type: "edit".to_string(),
level: "autoconfirmed".to_string(),
expiry: None, };
assert_eq!(protection.protection_type, "edit");
assert_eq!(protection.level, "autoconfirmed");
assert!(protection.expiry.is_none());
}
#[test]
fn test_scores() {
let scores = Scores {
revertrisk: Some(RevertRisk {
prediction: Some(false),
probability: None,
}),
referencerisk: Some(ReferenceRisk {
reference_risk_score: Some(0.15),
}),
referenceneed: Some(ReferenceNeed {
reference_need_score: Some(0.25),
}),
};
assert_eq!(scores.revertrisk.unwrap().prediction, Some(false));
assert_eq!(
scores.referencerisk.unwrap().reference_risk_score,
Some(0.15)
);
}
}