feat: add character search

This commit is contained in:
Anna 2018-09-03 16:40:30 -04:00
parent db4c96040e
commit 98dd9192ec
7 changed files with 247 additions and 31 deletions

View File

@ -1,6 +1,15 @@
use crate::error::*;
use crate::{
error::*,
models::{
GrandCompany,
character::GrandCompanyInfo,
},
};
use scraper::Html;
use scraper::{
Html,
node::Element,
};
macro_rules! selectors {
($($name:ident => $selector:expr);+$(;)?) => {
@ -14,6 +23,7 @@ macro_rules! selectors {
pub mod character;
pub mod free_company;
pub mod search;
crate fn plain_parse(html: &Html, select: &scraper::Selector) -> Result<String> {
let string = html
@ -24,3 +34,30 @@ crate fn plain_parse(html: &Html, select: &scraper::Selector) -> Result<String>
.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
.split('/')
.filter(|x| !x.is_empty())
.last()
.ok_or_else(|| Error::invalid_content("href separated by `/`", Some(&href)))?;
last.parse().map_err(Error::InvalidNumber)
}
crate fn parse_grand_company(text: &str) -> Result<GrandCompanyInfo> {
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(GrandCompanyInfo {
grand_company,
rank,
})
}

View File

@ -1,14 +1,11 @@
use crate::{
error::*,
logic::plain_parse,
models::{
GrandCompany,
character::{
Character,
CityState,
Gender,
GrandCompanyInfo,
},
models::character::{
Character,
CityState,
Gender,
GrandCompanyInfo,
}
};
@ -138,20 +135,7 @@ fn parse_grand_company(html: &Html) -> Result<Option<GrandCompanyInfo>> {
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,
}))
crate::logic::parse_grand_company(&text).map(Some)
}
fn parse_free_company_id(html: &Html) -> Result<Option<u64>> {
@ -162,11 +146,5 @@ fn parse_free_company_id(html: &Html) -> Result<Option<u64>> {
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)
crate::logic::parse_id(elem.value()).map(Some)
}

44
src/logic/search.rs Normal file
View File

@ -0,0 +1,44 @@
use crate::{
error::*,
models::search::Pagination,
};
use scraper::Html;
pub mod character;
selectors!(
PAGINATION_TOTAL => ".parts__total";
PAGINATION_PAGES => ".btn__pager__current";
);
crate fn parse_pagination(html: &Html) -> Result<Pagination> {
let total_str = crate::logic::plain_parse(&html, &*PAGINATION_TOTAL)?;
let total_results: u64 = total_str
.split(' ')
.next()
.unwrap() // will always have a first element
.parse()
.map_err(Error::InvalidNumber)?;
let pages_str = crate::logic::plain_parse(&html, &*PAGINATION_PAGES)?;
let mut pages_split = pages_str.split(' ');
let current_page: u64 = pages_split
.nth(1)
.ok_or_else(|| Error::invalid_content("4 items in pages string", None))?
.parse()
.map_err(Error::InvalidNumber)?;
let total_pages: u64 = pages_split
.nth(1)
.ok_or_else(|| Error::invalid_content("4 items in pages string", None))?
.parse()
.map_err(Error::InvalidNumber)?;
Ok(Pagination {
current_page,
total_pages,
total_results,
})
}

View File

@ -0,0 +1,126 @@
use crate::{
error::*,
models::{
search::{
Paginated,
character::CharacterSearchItem,
},
character::GrandCompanyInfo,
}
};
use ffxiv_types::World;
use scraper::{Html, ElementRef};
use url::Url;
use std::str::FromStr;
selectors!(
ITEM_ENTRY => ".ldst__window .entry";
ITEM_ID => ".entry__link";
ITEM_FACE => ".entry__chara__face > img";
ITEM_NAME => ".entry__name";
ITEM_WORLD => ".entry__world";
ITEM_GRAND_COMPANY => ".entry__chara_info .js__tooltip";
ITEM_FREE_COMPANY => ".entry__freecompany__link";
);
pub fn parse(html: &str) -> Result<Paginated<CharacterSearchItem>> {
let html = Html::parse_document(html);
let pagination = crate::logic::search::parse_pagination(&html)?;
let results: Vec<CharacterSearchItem> = html
.select(&*ITEM_ENTRY)
.map(|x| parse_single(x))
.collect::<Result<_>>()?;
Ok(Paginated {
pagination,
results,
})
}
fn parse_single<'a>(html: ElementRef<'a>) -> Result<CharacterSearchItem> {
let id = parse_id(html)?;
let name = plain_parse(html, &*ITEM_NAME)?;
let world = parse_world(html)?;
let grand_company = parse_grand_company(html)?;
let free_company_id = parse_free_company_id(html)?;
let face = parse_face(html)?;
Ok(CharacterSearchItem {
id,
name,
world,
grand_company,
free_company_id,
face,
})
}
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)
.next()
.ok_or_else(|| Error::missing_element(&*ITEM_ID))?;
crate::logic::parse_id(e.value())
}
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_free_company_id<'a>(html: ElementRef<'a>) -> Result<Option<u64>> {
let elem = match html
.select(&*ITEM_FREE_COMPANY)
.next()
{
Some(e) => e,
None => return Ok(None),
};
crate::logic::parse_id(elem.value()).map(Some)
}
fn parse_grand_company<'a>(html: ElementRef<'a>) -> Result<Option<GrandCompanyInfo>> {
let text = html
.select(&*ITEM_GRAND_COMPANY)
.next()
.and_then(|x| x.value().attr("data-tooltip"));
let text = match text {
Some(t) => t,
None => return Ok(None),
};
crate::logic::parse_grand_company(text).map(Some)
}
fn parse_face<'a>(html: ElementRef<'a>) -> Result<Url> {
let face_elem = html
.select(&*ITEM_FACE)
.next()
.ok_or_else(|| Error::missing_element(&*ITEM_FACE))?;
let src = face_elem
.value()
.attr("src")
.ok_or_else(|| Error::invalid_content("src on face img element", None))?;
Url::from_str(src).map_err(Error::InvalidUrl)
}

View File

@ -26,6 +26,7 @@ macro_rules! ffxiv_enum {
pub mod character;
pub mod free_company;
pub mod search;
ffxiv_enum!(
#[derive(PartialEq, Eq, PartialOrd, Ord)]

14
src/models/search.rs Normal file
View File

@ -0,0 +1,14 @@
pub mod character;
#[derive(Debug, Serialize)]
pub struct Pagination {
pub current_page: u64,
pub total_pages: u64,
pub total_results: u64,
}
#[derive(Debug, Serialize)]
pub struct Paginated<T> {
pub pagination: Pagination,
pub results: Vec<T>,
}

View File

@ -0,0 +1,16 @@
use crate::models::character::GrandCompanyInfo;
use ffxiv_types::World;
use url::Url;
#[derive(Debug, Serialize)]
pub struct CharacterSearchItem {
pub id: u64,
pub name: String,
pub world: World,
pub grand_company: Option<GrandCompanyInfo>,
pub free_company_id: Option<u64>,
#[serde(with = "url_serde")]
pub face: Url,
}