feat: add Free Company search
This commit is contained in:
parent
1775e7a7e2
commit
94c77d09cb
File diff suppressed because one or more lines are too long
|
@ -10,3 +10,4 @@ pub mod error;
|
|||
pub mod logic;
|
||||
|
||||
pub mod models;
|
||||
crate mod util;
|
||||
|
|
11
src/logic.rs
11
src/logic.rs
|
@ -8,6 +8,7 @@ use crate::{
|
|||
|
||||
use scraper::{
|
||||
Html,
|
||||
ElementRef,
|
||||
node::Element,
|
||||
};
|
||||
|
||||
|
@ -35,6 +36,16 @@ crate fn plain_parse(html: &Html, select: &scraper::Selector) -> Result<String>
|
|||
Ok(string)
|
||||
}
|
||||
|
||||
crate fn plain_parse_elem<'a>(html: ElementRef<'a>, select: &scraper::Selector) -> Result<String> {
|
||||
let string = html
|
||||
.select(select)
|
||||
.next()
|
||||
.ok_or(Error::missing_element(select))?
|
||||
.text()
|
||||
.collect();
|
||||
Ok(string)
|
||||
}
|
||||
|
||||
crate fn parse_id<'a>(a: &'a Element) -> Result<u64> {
|
||||
let href = a.attr("href").ok_or_else(|| Error::invalid_content("href on link", None))?;
|
||||
let last = href
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{
|
||||
error::*,
|
||||
logic::plain_parse_elem as plain_parse,
|
||||
models::{
|
||||
search::{
|
||||
Paginated,
|
||||
|
@ -66,16 +67,6 @@ fn parse_single<'a>(html: ElementRef<'a>) -> Result<CharacterSearchItem> {
|
|||
})
|
||||
}
|
||||
|
||||
fn plain_parse<'a>(html: ElementRef<'a>, select: &scraper::Selector) -> Result<String> {
|
||||
let string = html
|
||||
.select(select)
|
||||
.next()
|
||||
.ok_or(Error::missing_element(select))?
|
||||
.text()
|
||||
.collect();
|
||||
Ok(string)
|
||||
}
|
||||
|
||||
fn parse_id<'a>(html: ElementRef<'a>) -> Result<u64> {
|
||||
let e = html
|
||||
.select(&*ITEM_ID)
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
use crate::{
|
||||
error::*,
|
||||
logic::plain_parse_elem as plain_parse,
|
||||
models::{
|
||||
GrandCompany,
|
||||
search::{
|
||||
Paginated,
|
||||
free_company::{FreeCompanySearchItem, Active, RecruitmentStatus},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use chrono::{DateTime, Local, TimeZone, Utc};
|
||||
|
||||
use ffxiv_types::World;
|
||||
|
||||
use scraper::{Html, ElementRef};
|
||||
|
||||
use url::Url;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
selectors!(
|
||||
ITEM_ENTRY => ".ldst__window > .entry";
|
||||
|
||||
ITEM_ID => ".entry__block";
|
||||
ITEM_GRAND_COMPANY => ".entry__freecompany__box > p.entry__world:nth-of-type(1)";
|
||||
ITEM_NAME => ".entry__freecompany__box > .entry__name";
|
||||
ITEM_WORLD => ".entry__freecompany__box > p.entry__world:nth-of-type(3)";
|
||||
ITEM_CREST => ".entry__freecompany__crest__image > img";
|
||||
ITEM_ACTIVE_MEMBERS => ".entry__freecompany__fc-member";
|
||||
ITEM_ESTATE_BUILT => ".entry__freecompany__fc-housing";
|
||||
ITEM_FORMED => ".entry__freecompany__fc-day > script";
|
||||
ITEM_ACTIVE => ".entry__freecompany__fc-data > li.entry__freecompany__fc-active:nth-of-type(4)";
|
||||
ITEM_RECRUITMENT => ".entry__freecompany__fc-data > li.entry__freecompany__fc-active:nth-of-type(5)";
|
||||
);
|
||||
|
||||
pub fn parse(s: &str) -> Result<Paginated<FreeCompanySearchItem>> {
|
||||
let html = Html::parse_document(s);
|
||||
|
||||
let pagination = crate::logic::search::parse_pagination(&html)?;
|
||||
|
||||
let results: Vec<FreeCompanySearchItem> = html
|
||||
.select(&*ITEM_ENTRY)
|
||||
.map(|x| parse_single(x))
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
Ok(Paginated {
|
||||
pagination,
|
||||
results,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_single<'a>(html: ElementRef<'a>) -> Result<FreeCompanySearchItem> {
|
||||
let id = parse_id(html)?;
|
||||
let grand_company = parse_grand_company(html)?;
|
||||
let name = plain_parse(html, &*ITEM_NAME)?;
|
||||
let world = parse_world(html)?;
|
||||
let crest = parse_crest(html)?;
|
||||
let active_members = parse_active_members(html)?;
|
||||
let estate_built = parse_estate_built(html)?;
|
||||
let formed = parse_formed(html)?;
|
||||
let active = parse_active(html)?;
|
||||
let recruitment = parse_recruitment(html)?;
|
||||
|
||||
Ok(FreeCompanySearchItem {
|
||||
id,
|
||||
grand_company,
|
||||
name,
|
||||
world,
|
||||
crest,
|
||||
active_members,
|
||||
estate_built,
|
||||
formed,
|
||||
active,
|
||||
recruitment,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_id<'a>(html: ElementRef<'a>) -> Result<u64> {
|
||||
let e = html
|
||||
.select(&*ITEM_ID)
|
||||
.next()
|
||||
.ok_or_else(|| Error::missing_element(&*ITEM_ID))?;
|
||||
crate::logic::parse_id(e.value())
|
||||
}
|
||||
|
||||
fn parse_grand_company<'a>(html: ElementRef<'a>) -> Result<GrandCompany> {
|
||||
let gc_str = plain_parse(html, &*ITEM_GRAND_COMPANY)?;
|
||||
GrandCompany::parse(&gc_str)
|
||||
.ok_or_else(|| Error::invalid_content("valid grand company", Some(&gc_str)))
|
||||
}
|
||||
|
||||
fn parse_world<'a>(html: ElementRef<'a>) -> Result<World> {
|
||||
let world_str = plain_parse(html, &*ITEM_WORLD)?;
|
||||
World::from_str(&world_str)
|
||||
.map_err(|_| Error::invalid_content("valid world", Some(&world_str)))
|
||||
}
|
||||
|
||||
fn parse_crest<'a>(html: ElementRef<'a>) -> Result<Vec<Url>> {
|
||||
html.select(&*ITEM_CREST)
|
||||
.filter_map(|x| x.value().attr("src"))
|
||||
.map(|x| Url::from_str(x).map_err(Error::InvalidUrl))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_active_members<'a>(html: ElementRef<'a>) -> Result<u16> {
|
||||
plain_parse(html, &*ITEM_ACTIVE_MEMBERS)
|
||||
.and_then(|x| x.parse().map_err(Error::InvalidNumber))
|
||||
}
|
||||
|
||||
fn parse_estate_built<'a>(html: ElementRef<'a>) -> Result<bool> {
|
||||
let estate_built = plain_parse(html, &*ITEM_ESTATE_BUILT)?;
|
||||
let built = match estate_built.as_str() {
|
||||
"Estate Built" => true,
|
||||
"No Estate or Plot" => false,
|
||||
_ => return Err(Error::invalid_content("`Estate Built` or `No Estate or Plot`", Some(&estate_built))),
|
||||
};
|
||||
Ok(built)
|
||||
}
|
||||
|
||||
fn parse_formed<'a>(html: ElementRef<'a>) -> Result<DateTime<Utc>> {
|
||||
let script = html
|
||||
.select(&*ITEM_FORMED)
|
||||
.next()
|
||||
.ok_or(Error::missing_element(&*ITEM_FORMED))?
|
||||
.inner_html();
|
||||
|
||||
let timestamp = script
|
||||
.split("strftime(")
|
||||
.nth(1)
|
||||
.ok_or(Error::invalid_content("strftime call", Some(&script)))?
|
||||
.split(",")
|
||||
.next()
|
||||
.ok_or(Error::invalid_content("comma-separated strftime call", Some(&script)))?;
|
||||
let timestamp: i64 = timestamp.parse().map_err(Error::InvalidNumber)?;
|
||||
|
||||
let utc = Local.timestamp(timestamp, 0).with_timezone(&Utc);
|
||||
|
||||
Ok(utc)
|
||||
}
|
||||
|
||||
fn parse_active<'a>(html: ElementRef<'a>) -> Result<Active> {
|
||||
plain_parse(html, &*ITEM_ACTIVE)
|
||||
.and_then(|x| x
|
||||
.split(": ")
|
||||
.nth(1)
|
||||
.map(ToString::to_string)
|
||||
.ok_or_else(|| Error::invalid_content("activity split by `: `", Some(&x))))
|
||||
.and_then(|x| Active::parse(&x)
|
||||
.ok_or_else(|| Error::invalid_content("valid activity", Some(&x))))
|
||||
}
|
||||
|
||||
fn parse_recruitment<'a>(html: ElementRef<'a>) -> Result<RecruitmentStatus> {
|
||||
plain_parse(html, &*ITEM_RECRUITMENT)
|
||||
.and_then(|x| x
|
||||
.split(": ")
|
||||
.nth(1)
|
||||
.map(ToString::to_string)
|
||||
.ok_or_else(|| Error::invalid_content("recruitment status split by `: `", Some(&x))))
|
||||
.and_then(|x| RecruitmentStatus::parse(&x)
|
||||
.ok_or_else(|| Error::invalid_content("valid recruitment status", Some(&x))))
|
||||
}
|
|
@ -14,7 +14,7 @@ pub struct FreeCompany {
|
|||
pub name: String,
|
||||
pub world: World,
|
||||
pub slogan: String,
|
||||
#[serde(with = "multi_url")]
|
||||
#[serde(with = "crate::util::serde::multi_url")]
|
||||
pub crest: Vec<Url>,
|
||||
pub grand_company: GrandCompany,
|
||||
pub active_members: u16,
|
||||
|
@ -37,30 +37,3 @@ pub struct Estate {
|
|||
pub address: String,
|
||||
pub greeting: String,
|
||||
}
|
||||
|
||||
mod multi_url {
|
||||
use serde::{Deserializer, Deserialize, Serializer, ser::SerializeSeq};
|
||||
|
||||
use url::Url;
|
||||
|
||||
crate fn serialize<S>(urls: &Vec<Url>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(urls.len()))?;
|
||||
for url in urls {
|
||||
seq.serialize_element(&url_serde::Ser::new(url))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
crate fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Url>, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
{
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Wrapper(#[serde(with = "url_serde")] Url);
|
||||
|
||||
let urls = Vec::deserialize(deserializer)?;
|
||||
Ok(urls.into_iter().map(|Wrapper(u)| u).collect())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
use crate::models::GrandCompany;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use ffxiv_types::World;
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FreeCompanySearchItem {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub world: World,
|
||||
#[serde(with = "crate::util::serde::multi_url")]
|
||||
pub crest: Vec<Url>,
|
||||
pub grand_company: GrandCompany,
|
||||
pub active_members: u16,
|
||||
pub estate_built: bool,
|
||||
pub formed: DateTime<Utc>,
|
||||
pub active: Active,
|
||||
pub recruitment: RecruitmentStatus,
|
||||
}
|
||||
|
||||
ffxiv_enum!(Active {
|
||||
Always => "Always",
|
||||
Weekdays => "Weekdays",
|
||||
Weekends => "Weekends",
|
||||
Never => "Never",
|
||||
NotSpecified => "Not specified",
|
||||
});
|
||||
|
||||
ffxiv_enum!(RecruitmentStatus {
|
||||
Open => "Open",
|
||||
Closed => "Closed",
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
pub mod serde;
|
|
@ -0,0 +1 @@
|
|||
pub mod multi_url;
|
|
@ -0,0 +1,24 @@
|
|||
use serde::{Deserializer, Deserialize, Serializer, ser::SerializeSeq};
|
||||
|
||||
use url::Url;
|
||||
|
||||
crate fn serialize<S>(urls: &Vec<Url>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(urls.len()))?;
|
||||
for url in urls {
|
||||
seq.serialize_element(&url_serde::Ser::new(url))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
|
||||
crate fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Url>, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
{
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Wrapper(#[serde(with = "url_serde")] Url);
|
||||
|
||||
let urls = Vec::deserialize(deserializer)?;
|
||||
Ok(urls.into_iter().map(|Wrapper(u)| u).collect())
|
||||
}
|
Loading…
Reference in New Issue