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}