Skip to main content

mwtitle/
site_info.rs

1/*
2Copyright (C) 2021 Kunal Mehta <legoktm@debian.org>
3
4This program is free software: you can redistribute it and/or modify
5it under the terms of the GNU General Public License as published by
6the Free Software Foundation, either version 3 of the License, or
7(at your option) any later version.
8
9This program is distributed in the hope that it will be useful,
10but WITHOUT ANY WARRANTY; without even the implied warranty of
11MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12GNU General Public License for more details.
13
14You should have received a copy of the GNU General Public License
15along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18//! A type to represent a [siteinfo](https://www.mediawiki.org/wiki/API:Siteinfo) response.
19
20use serde::{de, Deserialize};
21use std::collections::HashMap;
22
23use crate::{Error, Result};
24
25// TODO: Port all this to mwapi_responses
26
27/// Represents a [siteinfo response](https://www.mediawiki.org/wiki/API:Siteinfo)
28/// suitable for making a [`TitleCodec`](crate::TitleCodec) or a [`NamespaceMap`](crate::NamespaceMap).
29#[derive(Debug, Deserialize)]
30pub struct Response {
31    pub query: SiteInfo,
32}
33
34/// Represents the `query` field of a siteinfo response.
35///
36/// Can be deserialized from `formatversion=1` or `formatversion=2` of siteinfo,
37/// as long as the response contains  `general`, `namespaces`, `namespacealiases`,
38/// and optionally `interwikimap`.
39/// `interwikimap` is required when the `SiteInfo` is passed
40/// to [`TitleCodec::from_site_info`](crate::TitleCodec::from_site_info) to create
41/// a [`TitleCodec`](crate::TitleCodec) that parses interwikis,
42/// but is not required for [`NamespaceMap::from_site_info`](crate::NamespaceMap::from_site_info).
43#[derive(Clone, Debug, Deserialize)]
44pub struct SiteInfo {
45    pub general: General,
46    pub namespaces: HashMap<String, NamespaceInfo>,
47    #[serde(rename = "namespacealiases")]
48    pub namespace_aliases: Vec<NamespaceAlias>,
49    #[serde(default)]
50    #[serde(rename = "interwikimap")]
51    pub interwiki_map: Vec<Interwiki>,
52}
53
54/// Represents the `general` field of a [`SiteInfo`].
55///
56/// Contains only the fields required for [`TitleCodec`](crate::TitleCodec).
57#[derive(Clone, Debug, Deserialize)]
58pub struct General {
59    #[serde(rename = "mainpage")]
60    pub main_page: String,
61    pub lang: String,
62    #[serde(rename = "legaltitlechars")]
63    pub legal_title_chars: String,
64}
65
66/// Represents a namespace object in the `namespaces` field of a [`SiteInfo`].
67///
68/// Contains only the fields required to generate a [`NamespaceMap`](crate::NamespaceMap) that can be used
69/// by a [`TitleCodec`](crate::TitleCodec) to parse the namespace in a [`Title`](crate::Title).
70/// Supports `formatversion=1` with the local namespace name in the `"*"` field,
71/// and `formatversion=2` with the local namespace name in the `"name"` field.
72#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
73pub struct NamespaceInfo {
74    pub id: i32,
75    pub case: String,
76    #[serde(alias = "*")]
77    pub name: String,
78    pub canonical: Option<String>,
79}
80
81impl NamespaceInfo {
82    /// Fallibly convert a `HashMap<String, String>` and a `String` into a `NamespaceInfo`.
83    ///
84    /// # Errors
85    /// Fails if any of the keys `"id"`, `"case"` is missing,
86    /// or if the value for `"id"` cannot be parsed as an `i32`.
87    pub fn try_from_iter<I: IntoIterator<Item = (String, String)>>(
88        iter: I,
89    ) -> Result<Self> {
90        use Error::*;
91        let mut items: Vec<_> = iter
92            .into_iter()
93            .filter(|(k, _)| {
94                ["id", "case", "name", "canonical"].contains(&k.as_str())
95            })
96            .collect();
97        let mut get_string = move |key_to_find| {
98            items
99                .iter()
100                .position(|(k, _)| k == key_to_find)
101                .map(|pos| items.remove(pos).1)
102                .ok_or(NamespaceInfoMissingKey(key_to_find))
103        };
104        let id = get_string("id")?;
105        let id = id.parse().map_err(|_| NamespaceInfoInvalidId(id))?;
106        Ok(NamespaceInfo {
107            id,
108            case: get_string("case")?,
109            name: get_string("name")?,
110            canonical: get_string("canonical").ok(),
111        })
112    }
113}
114
115#[test]
116fn test_namespace_info_from_iter() {
117    let (input, expected) = (
118        [("id", "0"), ("case", "first-letter"), ("name", "")],
119        NamespaceInfo {
120            id: 0,
121            case: "first-letter".into(),
122            name: "".into(),
123            canonical: None,
124        },
125    );
126    assert_eq!(
127        NamespaceInfo::try_from_iter(
128            input
129                .into_iter()
130                .map(|(k, v)| (k.to_string(), v.to_string()))
131        )
132        .map_err(|e| e.to_string()),
133        Ok(expected)
134    );
135}
136
137/// Represents a namespace alias object in the `namespacealiases` field of a [`SiteInfo`].
138///
139/// Supports `formatversion=1` with the alias in the `"*"` field,
140/// and `formatversion=2` with the alias in the `"alias"` field.
141#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
142pub struct NamespaceAlias {
143    pub id: i32,
144    #[serde(alias = "*")]
145    pub alias: String,
146}
147
148/// Represents an interwiki object in the `interwikimap` field of a [`SiteInfo`].
149///
150/// Contains only the fields required to generate two [`InterwikiSet`s](crate::InterwikiSet) that can be used
151/// by a [`TitleCodec`](crate::TitleCodec) to parse the interwikis in a [`Title`](crate::Title).
152/// Supports `formatversion=1` where the `localinterwiki` field is `""` for local interwikis,
153/// and `formatversion=2` where it is `true`.
154#[derive(Clone, Debug, Deserialize)]
155pub struct Interwiki {
156    pub prefix: String,
157    #[serde(default)]
158    #[serde(rename = "localinterwiki")]
159    #[serde(deserialize_with = "deserialize_bool_or_string")]
160    pub local_interwiki: bool,
161}
162
163/// Enum to represent booleans in both `formatversion=1` (present empty string
164/// for true) and `formatversion=2` (real booleans).
165#[derive(Clone, Debug, Deserialize)]
166#[serde(untagged)]
167enum BooleanCompat {
168    V2(bool),
169    // allow dead code because serde needs the type to pick the right format
170    #[allow(dead_code)]
171    V1(String),
172}
173
174fn deserialize_bool_or_string<'de, D>(deserializer: D) -> Result<bool, D::Error>
175where
176    D: de::Deserializer<'de>,
177{
178    let val = BooleanCompat::deserialize(deserializer)?;
179    Ok(match val {
180        BooleanCompat::V2(bool) => bool,
181        // Merely the string being present is true. If absent, bool::default()
182        // would be invoked for false.
183        BooleanCompat::V1(_) => true,
184    })
185}