#![deny(missing_docs)]
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
type NamespaceID = crate::api::NamespaceID;
pub fn toggle_namespace_id(id: NamespaceID) -> Option<NamespaceID> {
match id {
n if n >= 0 && n % 2 == 0 => Some(n + 1),
n if n >= 0 && n % 2 == 1 => Some(n - 1),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Title {
title: String, namespace_id: NamespaceID,
}
impl Title {
pub fn new(title: &str, namespace_id: NamespaceID) -> Title {
Title {
title: Title::underscores_to_spaces(title),
namespace_id,
}
}
pub fn new_from_full(full_title: &str, api: &crate::api::Api) -> Self {
let Some(colon_pos) = full_title.find(':') else {
return Self::new(full_title, 0);
};
let namespace_name = Title::first_letter_uppercase(&full_title[..colon_pos]);
let title = Title::underscores_to_spaces(&full_title[colon_pos + 1..]);
let site_info = api.get_site_info();
let matches_namespace =
|ns_name: &str| -> bool { Title::underscores_to_spaces(ns_name) == namespace_name };
if let Some(namespaces) = site_info["query"]["namespaces"].as_object() {
for ns in namespaces.values() {
if ns["*"].as_str().is_some_and(matches_namespace)
|| ns["canonical"].as_str().is_some_and(matches_namespace)
{
return Self::new_from_namespace_object(title, ns);
}
}
}
if let Some(aliases) = site_info["query"]["namespacealiases"].as_array() {
for ns in aliases {
if ns["*"].as_str().is_some_and(matches_namespace) {
let namespace_id = ns["id"].as_i64().unwrap_or(0);
let title = match ns["case"].as_str() {
Some("first-letter") => Title::first_letter_uppercase(&title),
_ => title,
};
return Self::new(&title, namespace_id);
}
}
}
Self::new(full_title, 0)
}
fn new_from_namespace_object(title: String, ns: &serde_json::Value) -> Self {
let namespace_id = ns["id"].as_i64().unwrap_or_default();
let title = match ns["case"].as_str() {
Some("first-letter") => Title::first_letter_uppercase(&title),
_ => title,
};
Self::new(&title, namespace_id)
}
pub fn new_from_api_result(data: &serde_json::Value) -> Title {
let namespace_id = data["ns"].as_i64().unwrap_or(0);
let title = data["title"].as_str().unwrap_or("");
let title = if namespace_id != 0 {
title.find(':').map_or(title, |pos| &title[pos + 1..])
} else {
title
};
Title {
title: Title::underscores_to_spaces(title),
namespace_id,
}
}
pub fn namespace_id(&self) -> NamespaceID {
self.namespace_id
}
pub fn namespace_name<'a>(&self, api: &'a crate::api::Api) -> Option<&'a str> {
api.get_canonical_namespace_name(self.namespace_id)
}
pub fn local_namespace_name<'a>(&self, api: &'a crate::api::Api) -> Option<&'a str> {
api.get_local_namespace_name(self.namespace_id)
}
pub fn with_underscores(&self) -> String {
Title::spaces_to_underscores(&self.title)
}
pub fn pretty(&self) -> &str {
&self.title }
pub fn full_with_underscores(&self, api: &crate::api::Api) -> Option<String> {
Some(
match Title::spaces_to_underscores(self.local_namespace_name(api)?).as_str() {
"" => self.with_underscores(),
ns => ns.to_owned() + ":" + &self.with_underscores(),
},
)
}
pub fn full_pretty(&self, api: &crate::api::Api) -> Option<String> {
Some(
match Title::underscores_to_spaces(self.local_namespace_name(api)?).as_str() {
"" => self.pretty().to_string(),
ns => ns.to_owned() + ":" + self.pretty(),
},
)
}
pub fn spaces_to_underscores(s: &str) -> String {
s.trim().replace(' ', "_")
}
pub fn underscores_to_spaces(s: &str) -> String {
s.replace('_', " ").trim().to_string()
}
pub fn first_letter_uppercase(s: &str) -> String {
let s = Title::underscores_to_spaces(s);
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => {
let f = unicode_case_mapping::to_titlecase(f);
if f[0] == 0 {
s
} else {
f.into_iter()
.filter_map(|c| if c != 0 { char::from_u32(c) } else { None })
.collect::<String>()
+ c.as_str()
}
}
}
}
pub fn toggle_talk(&mut self) {
self.namespace_id = toggle_namespace_id(self.namespace_id).unwrap_or(self.namespace_id);
}
pub fn into_toggle_talk(self) -> Self {
Title::new(
&self.title,
toggle_namespace_id(self.namespace_id).unwrap_or(self.namespace_id),
)
}
}
impl Display for Title {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.pretty())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::*;
use wiremock;
async fn wd_api() -> (wiremock::MockServer, Api) {
let server = crate::test_helpers::test_helpers_mod::start_wikidata_mock().await;
let api = Api::new(&server.uri()).await.unwrap();
(server, api)
}
#[tokio::test]
async fn new_from_full_main_namespace() {
let (_server, api) = wd_api().await;
assert_eq!(
Title::new_from_full("Main namespace", &api),
Title::new("Main namespace", 0)
);
}
#[tokio::test]
async fn new_from_full_canonical_namespace() {
let (_server, api) = wd_api().await;
assert_eq!(
Title::new_from_full("File:Some file.jpg", &api),
Title::new("Some file.jpg", 6)
);
}
#[tokio::test]
async fn new_from_full_canonical_namespace_with_colon() {
let (_server, api) = wd_api().await;
assert_eq!(
Title::new_from_full("Project talk:A project:yes, really", &api),
Title::new("A project:yes, really", 5)
);
}
#[tokio::test]
async fn new_from_full_namespace_alias() {
let (_server, api) = wd_api().await;
assert_eq!(
Title::new_from_full("Item:Q12345", &api),
Title::new("Q12345", 0)
);
}
#[tokio::test]
async fn new_from_full_special_namespace() {
let (_server, api) = wd_api().await;
assert_eq!(
Title::new_from_full("Special:A title", &api),
Title::new("A title", -1)
);
}
#[tokio::test]
async fn new_from_full_invalid_namespace() {
let (_server, api) = wd_api().await;
assert_eq!(
Title::new_from_full("This is not a namespace:A title", &api),
Title::new("This is not a namespace:A title", 0)
);
}
#[tokio::test]
async fn spaces_to_underscores() {
assert_eq!(
Title::spaces_to_underscores(" A little test "),
"A_little__test"
);
}
#[tokio::test]
async fn underscores_to_spaces() {
assert_eq!(
Title::underscores_to_spaces("_A_little__test_"),
"A little test"
);
}
#[tokio::test]
async fn first_letter_uppercase() {
assert_eq!(Title::first_letter_uppercase(""), "");
assert_eq!(Title::first_letter_uppercase("FooBar"), "FooBar");
assert_eq!(Title::first_letter_uppercase("fooBar"), "FooBar");
assert_eq!(Title::first_letter_uppercase("über"), "Über");
assert_eq!(Title::first_letter_uppercase("ვიკიპედია"), "ვიკიპედია");
}
#[tokio::test]
async fn full() {
let (_server, api) = wd_api().await;
let title = Title::new_from_full("User talk:Magnus_Manske", &api);
assert_eq!(
title.full_pretty(&api),
Some("User talk:Magnus Manske".to_string())
);
assert_eq!(
title.full_with_underscores(&api),
Some("User_talk:Magnus_Manske".to_string())
);
}
#[test]
fn display_trait() {
let title = Title::new("Test Page", 0);
assert_eq!(format!("{}", title), "Test Page");
}
#[test]
fn toggle_namespace_id_content_to_talk() {
assert_eq!(toggle_namespace_id(0), Some(1));
assert_eq!(toggle_namespace_id(2), Some(3));
assert_eq!(toggle_namespace_id(4), Some(5));
}
#[test]
fn toggle_namespace_id_talk_to_content() {
assert_eq!(toggle_namespace_id(1), Some(0));
assert_eq!(toggle_namespace_id(3), Some(2));
assert_eq!(toggle_namespace_id(5), Some(4));
}
#[test]
fn toggle_namespace_id_special() {
assert_eq!(toggle_namespace_id(-1), None);
assert_eq!(toggle_namespace_id(-2), None);
}
#[test]
fn title_with_underscores() {
let title = Title::new("Test Page", 0);
assert_eq!(title.with_underscores(), "Test_Page");
}
#[test]
fn title_pretty() {
let title = Title::new("Test_Page", 0);
assert_eq!(title.pretty(), "Test Page");
}
#[test]
fn title_namespace_id() {
let title = Title::new("Test", 6);
assert_eq!(title.namespace_id(), 6);
}
#[test]
fn new_from_api_result_with_namespace() {
let data = json!({"title": "Talk:Test Page", "ns": 1});
let title = Title::new_from_api_result(&data);
assert_eq!(title.pretty(), "Test Page");
assert_eq!(title.namespace_id(), 1);
}
#[test]
fn new_from_api_result_main_namespace() {
let data = json!({"title": "Main Page", "ns": 0});
let title = Title::new_from_api_result(&data);
assert_eq!(title.pretty(), "Main Page");
assert_eq!(title.namespace_id(), 0);
}
#[test]
fn new_from_api_result_missing_fields() {
let data = json!({});
let title = Title::new_from_api_result(&data);
assert_eq!(title.pretty(), "");
assert_eq!(title.namespace_id(), 0);
}
#[test]
fn title_equality() {
assert_eq!(Title::new("Foo", 0), Title::new("Foo", 0));
assert_ne!(Title::new("Foo", 0), Title::new("Bar", 0));
assert_ne!(Title::new("Foo", 0), Title::new("Foo", 1));
}
}