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}