diff --git a/schemas/FreeCompany.md b/schemas/FreeCompany.md index 0630158..e68a843 100644 --- a/schemas/FreeCompany.md +++ b/schemas/FreeCompany.md @@ -6,11 +6,13 @@ |`world`|`String`|The world the Free Company is on.| |`slogan`|`String`|The Free Company's slogan.| |`crest`|`Array` of `String`|The image URLs that are layered to created the Free Company crest.| +|`grand_company`|`String`|The Grand Company that the Free Company is affiliated with. Will be one of `"Flames"`, `"TwinAdders"`, or `"Maelstrom"`.| |`active_members`|`u16`|The amount of active members. |`rank`|`u8`|The Free Company's rank ([1,8]).| |`pvp_rankings`|`PvpRankings`|The Free Company's PvP rankings.| |`formed`|`DateTime` (UTC, RFC3339 formatted)|The date and time at which the Free Company was created.| |`estate`|`Estate?`|The Free Company's estate.| +|`reputation`|`Map` of `String` to `u8`|The reputation the Free Company has with each Grand Company. The keys are all possible values of the `grand_company` field.| ## PvpRankings diff --git a/src/logic/free_company.rs b/src/logic/free_company.rs index 76a626d..030363b 100644 --- a/src/logic/free_company.rs +++ b/src/logic/free_company.rs @@ -1,6 +1,6 @@ use crate::{ error::*, - models::free_company::{FreeCompany, PvpRankings, Estate}, + models::free_company::{FreeCompany, PvpRankings, Estate, GrandCompany}, }; use chrono::{DateTime, Local, TimeZone, Utc}; @@ -11,9 +11,13 @@ use scraper::Html; use url::Url; -use std::str::FromStr; +use std::{ + collections::BTreeMap, + str::FromStr, +}; selectors!( + FC_GRAND_COMPANY => "p.entry__freecompany__gc:nth-of-type(1)"; FC_NAME => ".entry__freecompany__name"; FC_WORLD => "p.entry__freecompany__gc:nth-of-type(3)"; FC_SLOGAN => ".freecompany__text__message.freecompany__text"; @@ -22,6 +26,11 @@ selectors!( FC_FORMED => "p.freecompany__text:nth-of-type(5) > script"; FC_ACTIVE_MEMBERS => "p.freecompany__text:nth-of-type(6)"; FC_RANK => "p.freecompany__text:nth-of-type(7)"; + + FC_REPUTATION => ".freecompany__reputation__data"; + FC_REPUTATION_NAME => ".freecompany__reputation__gcname"; + FC_REPUTATION_RANK => ".freecompany__reputation__rank"; + FC_WEEKLY_RANKING => ".character__ranking__data tr:nth-of-type(1) > th"; FC_MONTHLY_RANKING => ".character__ranking__data tr:nth-of-type(2) > th"; @@ -34,6 +43,7 @@ selectors!( pub fn parse(id: u64, html: &str) -> Result { let html = Html::parse_document(html); + let grand_company = parse_grand_company(&html)?; let name = plain_parse(&html, &*FC_NAME)?; let world = parse_world(&html)?; let slogan = plain_parse(&html, &*FC_SLOGAN)?; @@ -46,17 +56,20 @@ pub fn parse(id: u64, html: &str) -> Result { }; let formed = parse_formed(&html)?; let estate = parse_estate(&html)?; + let reputation = parse_reputation(&html)?; Ok(FreeCompany { name, world, slogan, crest, + grand_company, active_members, rank, pvp_rankings, formed, estate, + reputation, }) } @@ -153,3 +166,49 @@ fn parse_crest(html: &Html) -> Result> { .map(|x| Url::parse(x).map_err(Error::InvalidUrl)) .collect() } + +fn parse_grand_company(html: &Html) -> Result { + let text = plain_parse(html, &*FC_GRAND_COMPANY)?; + let name = text + .split(" <") + .next() + .ok_or_else(|| Error::invalid_content("grand company and reputation", Some(&text)))?; + GrandCompany::parse(&name) + .ok_or_else(|| Error::invalid_content("valid grand company", Some(&name))) +} + +fn parse_reputation(html: &Html) -> Result> { + let mut reps = BTreeMap::new(); + + for elem in html.select(&*FC_REPUTATION).into_iter() { + let name: String = elem + .select(&*FC_REPUTATION_NAME) + .next() + .ok_or_else(|| Error::missing_element(&*FC_REPUTATION_NAME))? + .text() + .collect(); + let gc = GrandCompany::parse(&name) + .ok_or_else(|| Error::invalid_content("valid grand company", Some(&name)))?; + let rank_elem = elem + .select(&*FC_REPUTATION_RANK) + .next() + .ok_or_else(|| Error::missing_element(&*FC_REPUTATION_RANK))?; + let color_class = rank_elem + .value() + .classes() + .find(|x| x.starts_with("color_")) + .ok_or_else(|| Error::invalid_content("color_## class", None))?; + let rank: u8 = color_class + .split("color_") + .nth(1) + .ok_or_else(|| Error::invalid_content("color_## class", Some(&color_class))) + .and_then(|x| x.parse().map_err(Error::InvalidNumber))?; + reps.insert(gc, rank); + } + + if reps.len() != 3 { + return Err(Error::invalid_content("three grand companies with reputation", None)); + } + + Ok(reps) +} diff --git a/src/models.rs b/src/models.rs index 4bc5f65..9407f6b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,5 +1,6 @@ macro_rules! ffxiv_enum { - ($name:ident { $($variant:ident => $str_repr:expr),+$(,)? }) => { + ($(#[$meta:meta])* $name:ident { $($variant:ident => $str_repr:expr),+$(,)? }) => { + $(#[$meta])* #[derive(Debug, Serialize)] pub enum $name { $($variant,)+ diff --git a/src/models/free_company.rs b/src/models/free_company.rs index 45012d0..52a298a 100644 --- a/src/models/free_company.rs +++ b/src/models/free_company.rs @@ -4,6 +4,8 @@ use ffxiv_types::World; use url::Url; +use std::collections::BTreeMap; + #[derive(Debug, Serialize)] pub struct FreeCompany { pub name: String, @@ -11,13 +13,24 @@ pub struct FreeCompany { pub slogan: String, #[serde(serialize_with = "multi_url")] pub crest: Vec, + pub grand_company: GrandCompany, pub active_members: u16, pub rank: u8, pub pvp_rankings: PvpRankings, pub formed: DateTime, pub estate: Option, + pub reputation: BTreeMap, } +ffxiv_enum!( + #[derive(PartialEq, Eq, PartialOrd, Ord)] + GrandCompany { + Flames => "Immortal Flames", + Maelstrom => "Maelstrom", + TwinAdders => "Order of the Twin Adder", + } +); + #[derive(Debug, Serialize)] pub struct PvpRankings { pub weekly: Option,