refactor: make character parsing use errors
Add schema for characters.
This commit is contained in:
parent
0220f1a1d5
commit
db4c96040e
|
@ -0,0 +1,104 @@
|
||||||
|
# Character
|
||||||
|
|
||||||
|
|Key|Value|Description|
|
||||||
|
|---|---|---|
|
||||||
|
|`id`|`u64`|The character's ID on the Lodestone.|
|
||||||
|
|`name`|`String`|The character's name.|
|
||||||
|
|`world`|`String`|The world the character is on.|
|
||||||
|
|`race`|`Race`|The character's race. See Race section below.|
|
||||||
|
|`clan`|`Clan`|The character's clan. See Clan section below.|
|
||||||
|
|`gender`|`Gender`|The character's gender. See Gender section below.|
|
||||||
|
|`title`|`String?`|The character's title.|
|
||||||
|
|`name_day`|`String`|The character's birth date, in the form `"18th Sun of the 2nd Umbral Moon"`.|
|
||||||
|
|`guardian`|`Guardian`|The character's guardian deity. See Guardian section below.|
|
||||||
|
|`city_state`|`CityState`|The character's starting city-state. See CityState section below.|
|
||||||
|
|`grand_company`|`GrandCompanyInfo?`|The character's Grand Company affiliation and rank. See GrandCompanyInfo section below.|
|
||||||
|
|`free_company_id`|`u64?`|The ID of the character's Free Company, if any.|
|
||||||
|
|`profile_text`|`String`|The profile text for this character on the Lodestone. If empty, this will be the empty string (`""`).|
|
||||||
|
|
||||||
|
## Race
|
||||||
|
|
||||||
|
An enumerated type represented as a string.
|
||||||
|
|
||||||
|
|Race|String|
|
||||||
|
|---|---|
|
||||||
|
|Au Ra|`"AuRa"`|
|
||||||
|
|Elezen|`"Elezen"`|
|
||||||
|
|Hyur|`"Hyur"`|
|
||||||
|
|Lalafell|`"Lalafell"`|
|
||||||
|
|Miqo'te|`"Miqote"`|
|
||||||
|
|Roegadyn|`"Roegadyn"`|
|
||||||
|
|
||||||
|
## Clan
|
||||||
|
|
||||||
|
An enumerated type represented as a string.
|
||||||
|
|
||||||
|
|Clan|String|
|
||||||
|
|---|---|
|
||||||
|
|Raen|`"Raen"`|
|
||||||
|
|Xaela|`"Xaela"`|
|
||||||
|
|Duskwight|`"Duskwight"`|
|
||||||
|
|Wildwood|`"Wildwood"`|
|
||||||
|
|Highlander|`"Highlander"`|
|
||||||
|
|Midlander|`"Midlander"`|
|
||||||
|
|Dunesfolk|`"Dunesfolk"`|
|
||||||
|
|Plainsfolk|`"Plainsfolk"`|
|
||||||
|
|Seeker of the Moon|`"SeekerOfTheMoon"`|
|
||||||
|
|Seeker of the Sun|`"SeekerOfTheSun"`|
|
||||||
|
|Hellsguard|`"Hellsguard"`|
|
||||||
|
|Sea Wolf|`"SeaWolf"`|
|
||||||
|
|
||||||
|
## Gender
|
||||||
|
|
||||||
|
An enumerated type represented as a string.
|
||||||
|
|
||||||
|
|Gender|String|
|
||||||
|
|---|---|
|
||||||
|
|Male|`"Male"`|
|
||||||
|
|Female|`"Female"`|
|
||||||
|
|
||||||
|
## Guardian
|
||||||
|
|
||||||
|
An enumerated type represented as a string.
|
||||||
|
|
||||||
|
|Guardian|String|
|
||||||
|
|---|---|
|
||||||
|
|Althyk|`"Althyk"`|
|
||||||
|
|Azeyma|`"Azeyma"`|
|
||||||
|
|Byregot|`"Byregot"`|
|
||||||
|
|Halone|`"Halone"`|
|
||||||
|
|Llymlaen|`"Llymlaen"`|
|
||||||
|
|Menphina|`"Menphina"`|
|
||||||
|
|Nald'thal|`"NaldThal"`|
|
||||||
|
|Nophica|`"Nophica"`|
|
||||||
|
|Nymeia|`"Nymeia"`|
|
||||||
|
|Oschon|`"Oschon"`|
|
||||||
|
|Rhalgr|`"Rhalgr"`|
|
||||||
|
|Thaliak|`"Thaliak"`|
|
||||||
|
|
||||||
|
## CityState
|
||||||
|
|
||||||
|
An enumerated type represented as a string.
|
||||||
|
|
||||||
|
|City-state|String|
|
||||||
|
|---|---|
|
||||||
|
|Gridania|`"Gridania"`|
|
||||||
|
|Limsa Lominsa|`"LimsaLominsa"`|
|
||||||
|
|Ul'dah|`"UlDah"`|
|
||||||
|
|
||||||
|
## GrandCompanyInfo
|
||||||
|
|
||||||
|
|Key|Value|Description|
|
||||||
|
|---|---|---|
|
||||||
|
|`grand_company`|`GrandCompany`|The Grand Company. See GrandCompany section below.|
|
||||||
|
|`rank`|`String`|The character's rank in the Grand Company. Ex. `"Second Serpent Lieutenant"`|
|
||||||
|
|
||||||
|
## GrandCompany
|
||||||
|
|
||||||
|
An enumerated type represented as a string.
|
||||||
|
|
||||||
|
|Grand Company|String|
|
||||||
|
|---|---|
|
||||||
|
|Immortal Flames|`"Flames"`|
|
||||||
|
|Maelstrom|`"Maelstrom"`|
|
||||||
|
|Order of the Twin Adder|`"TwinAdders"`|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
|Key|Value|Description|
|
|Key|Value|Description|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|`id`|`u64`|The Free Company's ID on the Lodestone.|
|
||||||
|`name`|`String`|The name of the Free Company.|
|
|`name`|`String`|The name of the Free Company.|
|
||||||
|`world`|`String`|The world the Free Company is on.|
|
|`world`|`String`|The world the Free Company is on.|
|
||||||
|`slogan`|`String`|The Free Company's slogan.|
|
|`slogan`|`String`|The Free Company's slogan.|
|
||||||
|
|
14
src/logic.rs
14
src/logic.rs
|
@ -1,3 +1,7 @@
|
||||||
|
use crate::error::*;
|
||||||
|
|
||||||
|
use scraper::Html;
|
||||||
|
|
||||||
macro_rules! selectors {
|
macro_rules! selectors {
|
||||||
($($name:ident => $selector:expr);+$(;)?) => {
|
($($name:ident => $selector:expr);+$(;)?) => {
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -10,3 +14,13 @@ macro_rules! selectors {
|
||||||
|
|
||||||
pub mod character;
|
pub mod character;
|
||||||
pub mod free_company;
|
pub mod free_company;
|
||||||
|
|
||||||
|
crate fn plain_parse(html: &Html, select: &scraper::Selector) -> Result<String> {
|
||||||
|
let string = html
|
||||||
|
.select(select)
|
||||||
|
.next()
|
||||||
|
.ok_or(Error::missing_element(select))?
|
||||||
|
.text()
|
||||||
|
.collect();
|
||||||
|
Ok(string)
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
use crate::models::character::{
|
use crate::{
|
||||||
Character,
|
error::*,
|
||||||
CityState,
|
logic::plain_parse,
|
||||||
Gender,
|
models::{
|
||||||
GrandCompany,
|
GrandCompany,
|
||||||
GrandCompanyInfo,
|
character::{
|
||||||
|
Character,
|
||||||
|
CityState,
|
||||||
|
Gender,
|
||||||
|
GrandCompanyInfo,
|
||||||
|
},
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
use ffxiv_types::{Race, Clan, Guardian};
|
use ffxiv_types::{World, Race, Clan, Guardian};
|
||||||
|
|
||||||
use scraper::Html;
|
use scraper::Html;
|
||||||
|
|
||||||
|
@ -25,69 +31,26 @@ selectors!(
|
||||||
PROFILE_TEXT => ".character__selfintroduction";
|
PROFILE_TEXT => ".character__selfintroduction";
|
||||||
);
|
);
|
||||||
|
|
||||||
pub fn parse(id: u64, html: &str) -> Option<Character> {
|
pub fn parse(id: u64, html: &str) -> Result<Character> {
|
||||||
let html = Html::parse_document(html);
|
let html = Html::parse_document(html);
|
||||||
|
|
||||||
let name = html.select(&*PROFILE_NAME).next()?.text().collect();
|
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 world_str: String = html.select(&*PROFILE_WORLD).next()?.text().collect();
|
let name_day = plain_parse(&html, &*PROFILE_NAME_DAY)?;
|
||||||
let world = ffxiv_types::World::from_str(&world_str).ok()?;
|
let guardian = parse_guardian(&html)?;
|
||||||
|
|
||||||
let title: Option<String> = html
|
let city_state = parse_city_state(&html)?;
|
||||||
.select(&*PROFILE_TITLE)
|
|
||||||
.next()
|
|
||||||
.map(|x| x.text().collect());
|
|
||||||
|
|
||||||
let mut rcg = html.select(&*PROFILE_RACE_CLAN_GENDER).next()?.text();
|
let grand_company = parse_grand_company(&html)?;
|
||||||
|
|
||||||
let race = Race::from_str(rcg.next()?).ok()?;
|
let free_company_id = parse_free_company_id(&html)?;
|
||||||
|
|
||||||
let mut clan_gender_str = rcg.next()?.split(" / ");
|
let profile_text = plain_parse(&html, &*PROFILE_TEXT)?.trim().to_string();
|
||||||
|
|
||||||
let clan = Clan::from_str(clan_gender_str.next()?).ok()?;
|
Ok(Character {
|
||||||
|
|
||||||
let gender = Gender::parse(clan_gender_str.next()?)?;
|
|
||||||
|
|
||||||
let name_day = html.select(&*PROFILE_NAME_DAY).next()?.text().collect();
|
|
||||||
|
|
||||||
let guardian_str: String = html.select(&*PROFILE_GUARDIAN).next()?.text().collect();
|
|
||||||
let guardian = Guardian::from_str(guardian_str.split(",").next()?).ok()?;
|
|
||||||
|
|
||||||
let city_state_str: String = html.select(&*PROFILE_CITY_STATE).next()?.text().collect();
|
|
||||||
let city_state = CityState::parse(&city_state_str)?;
|
|
||||||
|
|
||||||
let grand_company: Option<GrandCompanyInfo> = html
|
|
||||||
.select(&*PROFILE_GRAND_COMPANY)
|
|
||||||
.next()
|
|
||||||
.map(|x| x.text().collect::<String>())
|
|
||||||
.and_then(|x| {
|
|
||||||
let mut x = x.split(" / ");
|
|
||||||
let gc = GrandCompany::parse(x.next()?)?;
|
|
||||||
Some(GrandCompanyInfo {
|
|
||||||
grand_company: gc,
|
|
||||||
rank: x.next()?.to_string(),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let free_company_id: Option<u64> = html
|
|
||||||
.select(&*PROFILE_FREE_COMPANY)
|
|
||||||
.next()
|
|
||||||
.and_then(|x| x.value().attr("href"))
|
|
||||||
.and_then(|x| x
|
|
||||||
.split('/')
|
|
||||||
.filter(|x| !x.is_empty())
|
|
||||||
.last())
|
|
||||||
.and_then(|x| x.parse().ok());
|
|
||||||
|
|
||||||
let profile_text = html
|
|
||||||
.select(&*PROFILE_TEXT)
|
|
||||||
.next()?
|
|
||||||
.text()
|
|
||||||
.collect::<String>()
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Some(Character {
|
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
world,
|
world,
|
||||||
|
@ -103,3 +66,107 @@ pub fn parse(id: u64, html: &str) -> Option<Character> {
|
||||||
profile_text,
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
error::*,
|
error::*,
|
||||||
models::free_company::{FreeCompany, PvpRankings, Estate, GrandCompany},
|
logic::plain_parse,
|
||||||
|
models::GrandCompany,
|
||||||
|
models::free_company::{FreeCompany, PvpRankings, Estate},
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::{DateTime, Local, TimeZone, Utc};
|
use chrono::{DateTime, Local, TimeZone, Utc};
|
||||||
|
@ -59,6 +61,7 @@ pub fn parse(id: u64, html: &str) -> Result<FreeCompany> {
|
||||||
let reputation = parse_reputation(&html)?;
|
let reputation = parse_reputation(&html)?;
|
||||||
|
|
||||||
Ok(FreeCompany {
|
Ok(FreeCompany {
|
||||||
|
id,
|
||||||
name,
|
name,
|
||||||
world,
|
world,
|
||||||
slogan,
|
slogan,
|
||||||
|
@ -73,16 +76,6 @@ pub fn parse(id: u64, html: &str) -> Result<FreeCompany> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plain_parse(html: &Html, select: &scraper::Selector) -> Result<String> {
|
|
||||||
let string = html
|
|
||||||
.select(select)
|
|
||||||
.next()
|
|
||||||
.ok_or(Error::missing_element(select))?
|
|
||||||
.text()
|
|
||||||
.collect();
|
|
||||||
Ok(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_world(html: &Html) -> Result<World> {
|
fn parse_world(html: &Html) -> Result<World> {
|
||||||
let world_str = plain_parse(html, &*FC_WORLD)?;
|
let world_str = plain_parse(html, &*FC_WORLD)?;
|
||||||
let trimmed = world_str.trim();
|
let trimmed = world_str.trim();
|
||||||
|
|
|
@ -26,3 +26,12 @@ macro_rules! ffxiv_enum {
|
||||||
|
|
||||||
pub mod character;
|
pub mod character;
|
||||||
pub mod free_company;
|
pub mod free_company;
|
||||||
|
|
||||||
|
ffxiv_enum!(
|
||||||
|
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
GrandCompany {
|
||||||
|
Flames => "Immortal Flames",
|
||||||
|
Maelstrom => "Maelstrom",
|
||||||
|
TwinAdders => "Order of the Twin Adder",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use super::GrandCompany;
|
||||||
|
|
||||||
use ffxiv_types::{World, Race, Clan, Guardian};
|
use ffxiv_types::{World, Race, Clan, Guardian};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
@ -32,12 +34,6 @@ ffxiv_enum!(Gender {
|
||||||
Female => "♀",
|
Female => "♀",
|
||||||
});
|
});
|
||||||
|
|
||||||
ffxiv_enum!(GrandCompany {
|
|
||||||
Flames => "Immortal Flames",
|
|
||||||
Maelstrom => "Maelstrom",
|
|
||||||
TwinAdders => "Order of the Twin Adder",
|
|
||||||
});
|
|
||||||
|
|
||||||
ffxiv_enum!(CityState {
|
ffxiv_enum!(CityState {
|
||||||
Gridania => "Gridania",
|
Gridania => "Gridania",
|
||||||
LimsaLominsa => "Limsa Lominsa",
|
LimsaLominsa => "Limsa Lominsa",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use super::GrandCompany;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use ffxiv_types::World;
|
use ffxiv_types::World;
|
||||||
|
@ -8,6 +10,7 @@ use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct FreeCompany {
|
pub struct FreeCompany {
|
||||||
|
pub id: u64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub world: World,
|
pub world: World,
|
||||||
pub slogan: String,
|
pub slogan: String,
|
||||||
|
@ -22,15 +25,6 @@ pub struct FreeCompany {
|
||||||
pub reputation: BTreeMap<GrandCompany, u8>,
|
pub reputation: BTreeMap<GrandCompany, u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
ffxiv_enum!(
|
|
||||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
GrandCompany {
|
|
||||||
Flames => "Immortal Flames",
|
|
||||||
Maelstrom => "Maelstrom",
|
|
||||||
TwinAdders => "Order of the Twin Adder",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct PvpRankings {
|
pub struct PvpRankings {
|
||||||
pub weekly: Option<u64>,
|
pub weekly: Option<u64>,
|
||||||
|
|
Loading…
Reference in New Issue