lodestone-parser/src/logic/character.rs

173 lines
5.1 KiB
Rust

use crate::{
error::*,
logic::plain_parse,
models::{
GrandCompany,
character::{
Character,
CityState,
Gender,
GrandCompanyInfo,
},
}
};
use ffxiv_types::{World, Race, Clan, Guardian};
use scraper::Html;
use std::str::FromStr;
selectors!(
PROFILE_NAME => ".frame__chara__name";
PROFILE_WORLD => ".frame__chara__world";
PROFILE_TITLE => ".frame__chara__title";
PROFILE_NAME_DAY => ".character-block__birth";
PROFILE_RACE_CLAN_GENDER => "div.character-block:nth-of-type(1) > .character-block__box > .character-block__name";
PROFILE_GUARDIAN => "div.character-block:nth-of-type(2) > .character-block__box > .character-block__name";
PROFILE_CITY_STATE => "div.character-block:nth-of-type(3) > .character-block__box > .character-block__name";
PROFILE_GRAND_COMPANY => "div.character-block:nth-of-type(4) > .character-block__box > .character-block__name";
PROFILE_FREE_COMPANY => ".character__freecompany__name > h4 > a";
PROFILE_TEXT => ".character__selfintroduction";
);
pub fn parse(id: u64, html: &str) -> Result<Character> {
let html = Html::parse_document(html);
let name = plain_parse(&html, &*PROFILE_NAME)?;
let world = parse_world(&html)?;
let title = parse_title(&html);
let (race, clan, gender) = parse_rcg(&html)?;
let name_day = plain_parse(&html, &*PROFILE_NAME_DAY)?;
let guardian = parse_guardian(&html)?;
let city_state = parse_city_state(&html)?;
let grand_company = parse_grand_company(&html)?;
let free_company_id = parse_free_company_id(&html)?;
let profile_text = plain_parse(&html, &*PROFILE_TEXT)?.trim().to_string();
Ok(Character {
id,
name,
world,
race,
clan,
gender,
title,
name_day,
guardian,
city_state,
grand_company,
free_company_id,
profile_text,
})
}
fn parse_world(html: &Html) -> Result<World> {
let world_str = plain_parse(&html, &*PROFILE_WORLD)?;
World::from_str(&world_str)
.map_err(|_| Error::invalid_content("valid world", Some(&world_str)))
}
fn parse_title(html: &Html) -> Option<String> {
html
.select(&*PROFILE_TITLE)
.next()
.map(|x| x.text().collect())
}
fn parse_rcg(html: &Html) -> Result<(Race, Clan, Gender)> {
let mut rcg = html
.select(&*PROFILE_RACE_CLAN_GENDER)
.next()
.ok_or_else(|| Error::missing_element(&*PROFILE_RACE_CLAN_GENDER))?
.text();
let race_str = rcg
.next()
.ok_or_else(|| Error::invalid_content("first of two parts in race/gender", None))?;
let race = Race::from_str(race_str)
.map_err(|_| Error::invalid_content("valid race", Some(race_str)))?;
let clan_gender_str = rcg
.next()
.ok_or_else(|| Error::invalid_content("second of two parts in race/gender", None))?;
let mut clan_gender_split = clan_gender_str.split(" / ");
let clan_str = clan_gender_split
.next()
.ok_or_else(|| Error::invalid_content("clan/gender split by `/`", Some(clan_gender_str)))?;
let clan = Clan::from_str(clan_str)
.map_err(|_| Error::invalid_content("valid clan", Some(clan_str)))?;
let gender_str = clan_gender_split
.next()
.ok_or_else(|| Error::invalid_content("clan/gender split by `/`", Some(clan_gender_str)))?;
let gender = Gender::parse(gender_str)
.ok_or_else(|| Error::invalid_content("valid gender", Some(gender_str)))?;
Ok((race, clan, gender))
}
fn parse_guardian(html: &Html) -> Result<Guardian> {
let guardian_str = plain_parse(&html, &*PROFILE_GUARDIAN)?;
guardian_str
.split(",")
.next()
.ok_or_else(|| Error::invalid_content("first part of guardian", Some(&guardian_str)))
.and_then(|x| Guardian::from_str(&x)
.map_err(|_| Error::invalid_content("valid guardian", Some(&guardian_str))))
}
fn parse_city_state(html: &Html) -> Result<CityState> {
let city_state_str = plain_parse(&html, &*PROFILE_CITY_STATE)?;
CityState::parse(&city_state_str)
.ok_or_else(|| Error::invalid_content("valid city-state", Some(&city_state_str)))
}
fn parse_grand_company(html: &Html) -> Result<Option<GrandCompanyInfo>> {
let text = html
.select(&*PROFILE_GRAND_COMPANY)
.next()
.map(|x| x.text().collect::<String>());
let text = match text {
Some(t) => t,
None => return Ok(None),
};
let mut x = text.split(" / ");
let gc_str = x
.next()
.ok_or_else(|| Error::invalid_content("gc/rank separated by `/`", Some(&text)))?;
let grand_company = GrandCompany::parse(gc_str)
.ok_or_else(|| Error::invalid_content("valid grand company", Some(&text)))?;
let rank = x
.next()
.ok_or_else(|| Error::invalid_content("gc/rank separated by `/`", Some(&text)))?
.to_string();
Ok(Some(GrandCompanyInfo {
grand_company,
rank,
}))
}
fn parse_free_company_id(html: &Html) -> Result<Option<u64>> {
let elem = match html
.select(&*PROFILE_FREE_COMPANY)
.next()
{
Some(e) => e,
None => return Ok(None),
};
let href = elem.value().attr("href").ok_or_else(|| Error::invalid_content("href on FC link", None))?;
let last = href
.split('/')
.filter(|x| !x.is_empty())
.last()
.ok_or_else(|| Error::invalid_content("href separated by `/`", Some(&href)))?;
last.parse().map(Some).map_err(Error::InvalidNumber)
}