Skip to main content

wme_models/
version.rs

1//! Version and editor information types.
2//!
3//! This module contains types related to article revisions (versions), including:
4//! - [`Version`] - Complete revision metadata with credibility signals
5//! - [`Editor`] - Editor information and user groups
6//! - [`Scores`] - Quality scores (revert risk, reference risk, reference need)
7//! - [`Protection`] - Page protection settings
8//! - [`MaintenanceTags`] - Template counts for maintenance needs
9//!
10//! # Credibility Signals
11//!
12//! Several fields are marked as "Credibility Signals" in the API documentation.
13//! These provide qualitative metadata to help make informed decisions about data handling:
14//!
15//! - **Revert Risk Score**: Predicts whether a revision may be reverted
16//! - **Reference Risk Score**: Probability that references remain in the article
17//! - **Reference Need Score**: Proportion of uncited sentences needing citations
18//! - **Editor Information**: Edit count, user groups, registration date
19//! - **Maintenance Tags**: Counts of citation needed, POV, clarification, update templates
20//!
21//! # Example
22//!
23//! ```
24//! use wme_models::{Version, Editor};
25//! use chrono::Utc;
26//!
27//! let version = Version {
28//!     identifier: 1182847293,
29//!     editor: Some(Editor {
30//!         identifier: Some(12345),
31//!         name: Some("ExampleUser".to_string()),
32//!         is_bot: Some(false),
33//!         is_anonymous: Some(false),
34//!         date_started: Some(Utc::now()),
35//!         edit_count: Some(1500),
36//!         groups: Some(vec!["user".to_string(), "autoconfirmed".to_string()]),
37//!         is_admin: Some(false),
38//!         is_patroller: Some(false),
39//!         has_advanced_rights: Some(false),
40//!     }),
41//!     comment: Some("Fixed typo".to_string()),
42//!     tags: Some(vec!["mobile edit".to_string()]),
43//!     has_tag_needs_citation: Some(false),
44//!     is_minor_edit: Some(true),
45//!     is_flagged_stable: Some(true),
46//!     is_breaking_news: Some(false),
47//!     noindex: Some(false),
48//!     number_of_characters: Some(5000),
49//!     size: Some(wme_models::ArticleSize {
50//!         value: 15000,
51//!         unit_text: "B".to_string(),
52//!     }),
53//!     maintenance_tags: None,
54//!     scores: None,
55//! };
56//! ```
57
58use chrono::{DateTime, Utc};
59use serde::{Deserialize, Serialize};
60
61/// Version information for an article.
62///
63/// Represents a single revision of an article with comprehensive metadata
64/// including editor information, credibility signals, and quality scores.
65///
66/// # Key Fields
67///
68/// - `identifier` - Unique revision ID (different from article ID)
69/// - `editor` - Editor who made this revision
70/// - `scores` - Quality predictions from LiftWing models
71/// - `maintenance_tags` - Counts of maintenance templates
72/// - `is_flagged_stable` - Community-approved revision flag
73#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
74pub struct Version {
75    /// Revision identifier (unique for each edit)
76    pub identifier: u64,
77    /// Editor information
78    pub editor: Option<Editor>,
79    /// Edit comment
80    pub comment: Option<String>,
81    /// MediaWiki change tags
82    pub tags: Option<Vec<String>>,
83    /// Has "citation needed" tag
84    pub has_tag_needs_citation: Option<bool>,
85    /// Was this a minor edit
86    pub is_minor_edit: Option<bool>,
87    /// Community-approved revision
88    pub is_flagged_stable: Option<bool>,
89    /// Breaking news flag
90    pub is_breaking_news: Option<bool>,
91    /// Non-indexable to search engines
92    pub noindex: Option<bool>,
93    /// Character count from wikitext
94    pub number_of_characters: Option<u64>,
95    /// Article size
96    pub size: Option<ArticleSize>,
97    /// Maintenance template counts
98    pub maintenance_tags: Option<MaintenanceTags>,
99    /// Quality scores
100    pub scores: Option<Scores>,
101}
102
103/// Previous version reference.
104///
105/// Lightweight reference to the revision prior to the current one.
106#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
107pub struct PreviousVersion {
108    /// Revision identifier
109    pub identifier: u64,
110    /// Editor information
111    pub editor: Option<Editor>,
112    /// Number of characters in the previous revision
113    pub number_of_characters: Option<u64>,
114}
115
116/// Editor information.
117///
118/// Provides context about who made a revision. Anonymous editors (IP addresses)
119/// have no identifier. Temporary accounts (since Dec 2025) have identifiers
120/// but `is_anonymous` will be false.
121///
122/// # Editor Name Format
123///
124/// - **Registered users**: Username (e.g., "ExampleUser")
125/// - **Anonymous (legacy)**: IP address (e.g., "192.168.1.1")
126/// - **Temporary accounts**: `~YYYY-SERIAL` (e.g., "~2026-59431-3")
127#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
128pub struct Editor {
129    /// Editor identifier (none for anonymous users)
130    pub identifier: Option<u64>,
131    /// Editor name or IP address
132    pub name: Option<String>,
133    /// Is a bot
134    pub is_bot: Option<bool>,
135    /// Is an anonymous (IP) editor
136    pub is_anonymous: Option<bool>,
137    /// User registration timestamp
138    pub date_started: Option<DateTime<Utc>>,
139    /// Total edit count
140    pub edit_count: Option<u64>,
141    /// User groups (e.g., "admin", "autoconfirmed")
142    pub groups: Option<Vec<String>>,
143    /// Is an admin
144    pub is_admin: Option<bool>,
145    /// Is a patroller
146    pub is_patroller: Option<bool>,
147    /// Has advanced rights
148    pub has_advanced_rights: Option<bool>,
149}
150
151/// Article size information.
152///
153/// Size of the article in wikitext format.
154#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
155pub struct ArticleSize {
156    /// Size value in bytes
157    pub value: u64,
158    /// Unit text (usually "B")
159    pub unit_text: String,
160}
161
162/// Maintenance template counts.
163///
164/// Counts of occurrences of certain templates in the article body.
165/// These indicate areas that may need editor attention.
166#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
167pub struct MaintenanceTags {
168    /// Citation needed count
169    pub citation_needed_count: Option<u64>,
170    /// POV tag count
171    pub pov_count: Option<u64>,
172    /// Clarification needed count
173    pub clarification_needed_count: Option<u64>,
174    /// Update needed count
175    pub update_count: Option<u64>,
176}
177
178/// Quality scores.
179///
180/// Scores calculated as part of Wikimedia's LiftWing project.
181/// These provide credibility signals for revision quality.
182#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
183pub struct Scores {
184    /// Revert risk score (may revision be reverted?)
185    pub revertrisk: Option<RevertRisk>,
186    /// Reference risk score (will references remain?)
187    pub referencerisk: Option<ReferenceRisk>,
188    /// Reference need score (what needs citations?)
189    pub referenceneed: Option<ReferenceNeed>,
190}
191
192/// Revert risk score.
193///
194/// Predicts whether a revision may be reverted based on edit patterns.
195#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
196pub struct RevertRisk {
197    /// Revert risk prediction (true = likely to be reverted)
198    pub prediction: Option<bool>,
199    /// Revert risk probability details
200    pub probability: Option<serde_json::Value>,
201}
202
203/// Reference risk score.
204///
205/// Probability of references remaining in the article based on
206/// historical editorial activity on web domains used as references.
207/// Serves as a proxy for "source reliability".
208#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
209pub struct ReferenceRisk {
210    /// Reference risk score (0.0 to 1.0)
211    pub reference_risk_score: Option<f64>,
212}
213
214/// Reference need score.
215///
216/// Proportion of uncited sentences that need citations.
217/// Available for these Wikipedia languages: fa, it, zh, ru, pt, es, ja, de, fr, en.
218/// Only available for articles (Namespace 0).
219#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
220pub struct ReferenceNeed {
221    /// Reference need score (0.0 to 1.0)
222    pub reference_need_score: Option<f64>,
223}
224
225/// Protection settings.
226///
227/// Community-specific protections and restrictions on the article.
228/// Indicates which editor permissions are needed to edit or move the page.
229#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
230pub struct Protection {
231    /// Protection type (e.g., "edit", "move")
232    #[serde(rename = "type")]
233    pub protection_type: String,
234    /// Protection level (e.g., "autoconfirmed", "sysop")
235    pub level: String,
236    /// Expiration timestamp (None for never-expiring)
237    pub expiry: Option<DateTime<Utc>>,
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use chrono::Utc;
244
245    #[test]
246    fn test_version_creation() {
247        let version = Version {
248            identifier: 1182847293,
249            editor: Some(Editor {
250                identifier: Some(12345),
251                name: Some("TestUser".to_string()),
252                is_bot: Some(false),
253                is_anonymous: Some(false),
254                date_started: Some(Utc::now()),
255                edit_count: Some(1000),
256                groups: Some(vec!["user".to_string()]),
257                is_admin: Some(false),
258                is_patroller: Some(false),
259                has_advanced_rights: Some(false),
260            }),
261            comment: Some("Test edit".to_string()),
262            tags: Some(vec!["mobile edit".to_string()]),
263            has_tag_needs_citation: Some(false),
264            is_minor_edit: Some(false),
265            is_flagged_stable: Some(true),
266            is_breaking_news: Some(false),
267            noindex: Some(false),
268            number_of_characters: Some(5000),
269            size: Some(ArticleSize {
270                value: 15000,
271                unit_text: "B".to_string(),
272            }),
273            maintenance_tags: None,
274            scores: None,
275        };
276
277        assert_eq!(version.identifier, 1182847293);
278        assert!(version.is_flagged_stable.unwrap());
279    }
280
281    #[test]
282    fn test_editor_groups() {
283        let editor = Editor {
284            identifier: Some(12345),
285            name: Some("AdminUser".to_string()),
286            is_bot: Some(false),
287            is_anonymous: Some(false),
288            date_started: Some(Utc::now()),
289            edit_count: Some(5000),
290            groups: Some(vec![
291                "user".to_string(),
292                "autoconfirmed".to_string(),
293                "extendedconfirmed".to_string(),
294            ]),
295            is_admin: Some(true),
296            is_patroller: Some(true),
297            has_advanced_rights: Some(true),
298        };
299
300        let groups = editor.groups.as_ref().unwrap();
301        assert!(groups.contains(&"user".to_string()));
302        assert!(groups.contains(&"autoconfirmed".to_string()));
303        assert!(editor.is_admin.unwrap());
304    }
305
306    #[test]
307    fn test_maintenance_tags() {
308        let tags = MaintenanceTags {
309            citation_needed_count: Some(5),
310            pov_count: Some(1),
311            clarification_needed_count: Some(2),
312            update_count: Some(10),
313        };
314
315        assert_eq!(tags.citation_needed_count, Some(5));
316        assert_eq!(tags.pov_count, Some(1));
317    }
318
319    #[test]
320    fn test_protection() {
321        let protection = Protection {
322            protection_type: "edit".to_string(),
323            level: "autoconfirmed".to_string(),
324            expiry: None, // Never expires
325        };
326
327        assert_eq!(protection.protection_type, "edit");
328        assert_eq!(protection.level, "autoconfirmed");
329        assert!(protection.expiry.is_none());
330    }
331
332    #[test]
333    fn test_scores() {
334        let scores = Scores {
335            revertrisk: Some(RevertRisk {
336                prediction: Some(false),
337                probability: None,
338            }),
339            referencerisk: Some(ReferenceRisk {
340                reference_risk_score: Some(0.15),
341            }),
342            referenceneed: Some(ReferenceNeed {
343                reference_need_score: Some(0.25),
344            }),
345        };
346
347        assert_eq!(scores.revertrisk.unwrap().prediction, Some(false));
348        assert_eq!(
349            scores.referencerisk.unwrap().reference_risk_score,
350            Some(0.15)
351        );
352    }
353}