chore: initial commit

This commit is contained in:
Kyle Clemens 2018-09-04 16:13:11 -04:00
commit b538545f67
Signed by: anna
GPG Key ID: 0B391D8F06FCD9E0
14 changed files with 536 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock

22
Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
cargo-features = ["edition"]
[package]
name = "lodestone_scraper"
version = "0.1.0"
authors = ["Kyle Clemens <git@kyleclemens.com>"]
edition = "2018"
[dependencies]
reqwest = "0.8"
failure = "0.1"
lazy_static = "1"
url = "1"
[dependencies.lodestone_parser]
git = "https://github.com/jkcclemens/lodestone_parser"
[dependencies.ffxiv_types]
version = "1"
default-features = false
features = ["worlds", "data_centers", "races", "clans"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Kyle Clemens
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# lodestone_scraper
A Lodestone client library.

5
src/builder.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod character_search;
pub mod free_company_search;
pub use self::character_search::CharacterSearchBuilder;
pub use self::free_company_search::FreeCompanySearchBuilder;

View File

@ -0,0 +1,135 @@
use crate::{
LodestoneScraper,
error::*,
models::ClassJobRole,
util::{Either, AsLodestone},
};
use ffxiv_types::{World, Race, Clan, DataCenter};
use lodestone_parser::models::{
GrandCompany,
character::Job,
search::{
Paginated,
character::CharacterSearchItem,
},
};
use url::Url;
#[derive(Debug)]
pub struct CharacterSearchBuilder<'a> {
scraper: &'a LodestoneScraper,
// q
name: Option<&'a str>,
// worldname
world: Option<Either<World, DataCenter>>,
// classjob
job: Option<Either<Job, ClassJobRole>>,
// race_tribe
race: Option<Either<Race, Clan>>,
// gcid
grand_company: Option<Vec<GrandCompany>>,
}
impl<'a> CharacterSearchBuilder<'a> {
pub fn new(scraper: &'a LodestoneScraper) -> Self {
CharacterSearchBuilder {
scraper,
name: None,
world: None,
job: None,
race: None,
grand_company: None,
}
}
pub fn name(&mut self, n: &'a str) -> &mut Self {
self.name = Some(n);
self
}
pub fn world(&mut self, w: World) -> &mut Self {
self.world = Some(Either::Left(w));
self
}
pub fn data_center(&mut self, dc: DataCenter) -> &mut Self {
self.world = Some(Either::Right(dc));
self
}
pub fn job(&mut self, j: Job) -> &mut Self {
self.job = Some(Either::Left(j));
self
}
pub fn role(&mut self, r: ClassJobRole) -> &mut Self {
self.job = Some(Either::Right(r));
self
}
pub fn race(&mut self, r: Race) -> &mut Self {
self.race = Some(Either::Left(r));
self
}
pub fn clan(&mut self, c: Clan) -> &mut Self {
self.race = Some(Either::Right(c));
self
}
pub fn grand_company(&mut self, gc: GrandCompany) -> &mut Self {
self.grand_company.get_or_insert_with(Default::default).push(gc);
self
}
pub fn send(&self) -> Result<Paginated<CharacterSearchItem>> {
let text = self.scraper.client
.get(self.as_url())
.send()
.map_err(Error::Net)?
.text()
.map_err(Error::Net)?;
lodestone_parser::parse_character_search(&text).map_err(Error::Parse)
}
pub fn as_url(&self) -> Url {
let mut url = crate::LODESTONE_URL.join("character/").unwrap();
{
let mut pairs = url.query_pairs_mut();
if let Some(ref name) = self.name {
pairs.append_pair("q", name);
}
match self.world {
Some(Either::Left(w)) => { pairs.append_pair("worldname", w.as_str()); },
Some(Either::Right(dc)) => { pairs.append_pair("worldname", &dc.as_lodestone()); },
_ => {},
}
match self.job {
Some(Either::Left(j)) => { pairs.append_pair("classjob", &j.as_lodestone().to_string()); },
Some(Either::Right(cjr)) => { pairs.append_pair("classjob", cjr.as_lodestone()); },
_ => {},
}
match self.race {
Some(Either::Left(r)) => { pairs.append_pair("race_tribe", &format!("race_{}", r.as_lodestone())); },
Some(Either::Right(c)) => { pairs.append_pair("race_tribe", &format!("tribe_{}", c.as_lodestone())); },
_ => {},
}
if let Some(ref gcs) = self.grand_company {
for gc in gcs {
pairs.append_pair("gcid", &gc.as_lodestone().to_string());
}
}
}
url
}
}

View File

@ -0,0 +1,95 @@
use crate::{
LodestoneScraper,
error::*,
util::{Either, AsLodestone},
};
use ffxiv_types::{World, DataCenter};
use lodestone_parser::models::{
GrandCompany,
search::{
Paginated,
free_company::FreeCompanySearchItem,
},
};
use url::Url;
#[derive(Debug)]
pub struct FreeCompanySearchBuilder<'a> {
scraper: &'a LodestoneScraper,
// q
name: Option<&'a str>,
// worldname
world: Option<Either<World, DataCenter>>,
// gcid
grand_company: Option<Vec<GrandCompany>>,
}
impl<'a> FreeCompanySearchBuilder<'a> {
pub fn new(scraper: &'a LodestoneScraper) -> Self {
FreeCompanySearchBuilder {
scraper,
name: None,
world: None,
grand_company: None,
}
}
pub fn name(&mut self, n: &'a str) -> &mut Self {
self.name = Some(n);
self
}
pub fn world(&mut self, w: World) -> &mut Self {
self.world = Some(Either::Left(w));
self
}
pub fn data_center(&mut self, dc: DataCenter) -> &mut Self {
self.world = Some(Either::Right(dc));
self
}
pub fn grand_company(&mut self, gc: GrandCompany) -> &mut Self {
self.grand_company.get_or_insert_with(Default::default).push(gc);
self
}
pub fn send(&self) -> Result<Paginated<FreeCompanySearchItem>> {
let text = self.scraper.client
.get(self.as_url())
.send()
.map_err(Error::Net)?
.text()
.map_err(Error::Net)?;
lodestone_parser::parse_free_company_search(&text).map_err(Error::Parse)
}
pub fn as_url(&self) -> Url {
let mut url = crate::LODESTONE_URL.join("freecompany/").unwrap();
{
let mut pairs = url.query_pairs_mut();
if let Some(ref name) = self.name {
pairs.append_pair("q", name);
}
match self.world {
Some(Either::Left(w)) => { pairs.append_pair("worldname", w.as_str()); },
Some(Either::Right(dc)) => { pairs.append_pair("worldname", &dc.as_lodestone()); },
_ => {},
}
if let Some(ref gcs) = self.grand_company {
for gc in gcs {
pairs.append_pair("gcid", &gc.as_lodestone().to_string());
}
}
}
url
}
}

11
src/error.rs Normal file
View File

@ -0,0 +1,11 @@
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "network error: {}", _0)]
Net(reqwest::Error),
#[fail(display = "url parse error: {}", _0)]
Url(url::ParseError),
#[fail(display = "lodestone parse error: {}", _0)]
Parse(lodestone_parser::error::Error),
}

73
src/lib.rs Normal file
View File

@ -0,0 +1,73 @@
#[macro_use] extern crate failure;
use lazy_static::lazy_static;
use lodestone_parser::models::{
character::Character,
free_company::FreeCompany,
};
use reqwest::Client;
use url::Url;
use std::str::FromStr;
pub mod builder;
pub mod error;
pub mod models;
crate mod util;
use crate::error::*;
#[derive(Debug)]
pub struct LodestoneScraper {
client: Client,
}
impl Default for LodestoneScraper {
fn default() -> Self {
let client = Client::new();
LodestoneScraper { client }
}
}
lazy_static! {
static ref LODESTONE_URL: Url = Url::from_str("https://na.finalfantasyxiv.com/lodestone/").unwrap();
}
impl LodestoneScraper {
fn route(s: &str) -> Result<Url> {
LODESTONE_URL.join(s).map_err(Error::Url)
}
pub fn character(&self, id: u64) -> Result<Character> {
let url = LodestoneScraper::route(&format!("character/{}", id))?;
let text = self.client
.get(url)
.send()
.map_err(Error::Net)?
.text()
.map_err(Error::Net)?;
lodestone_parser::parse_character(id, &text).map_err(Error::Parse)
}
pub fn character_search(&self) -> builder::CharacterSearchBuilder {
builder::CharacterSearchBuilder::new(self)
}
pub fn free_company(&self, id: u64) -> Result<FreeCompany> {
let url = LodestoneScraper::route(&format!("freecompany/{}", id))?;
let text = self.client
.get(url)
.send()
.map_err(Error::Net)?
.text()
.map_err(Error::Net)?;
lodestone_parser::parse_free_company(id, &text).map_err(Error::Parse)
}
pub fn free_company_search(&self) -> builder::FreeCompanySearchBuilder {
builder::FreeCompanySearchBuilder::new(self)
}
}

3
src/models.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod class_job_role;
pub use self::class_job_role::ClassJobRole;

View File

@ -0,0 +1,13 @@
#[derive(Debug, Clone, Copy)]
pub enum ClassJobRole {
ClassTank,
ClassHealer,
ClassDps,
JobTank,
JobHealer,
JobDps,
ClassDiscipleOfTheHand,
ClassDiscipleOfTheLand,
}

5
src/util.rs Normal file
View File

@ -0,0 +1,5 @@
crate mod as_lodestone;
crate mod either;
crate use self::as_lodestone::AsLodestone;
crate use self::either::Either;

131
src/util/as_lodestone.rs Normal file
View File

@ -0,0 +1,131 @@
use crate::models::ClassJobRole;
use ffxiv_types::{DataCenter, Race, Clan};
use lodestone_parser::models::{
GrandCompany,
character::Job,
};
crate trait AsLodestone {
type Representation;
fn as_lodestone(&self) -> Self::Representation;
}
impl AsLodestone for DataCenter {
type Representation = String;
fn as_lodestone(&self) -> Self::Representation {
format!("_dc_{}", self.as_str())
}
}
impl AsLodestone for ClassJobRole {
type Representation = &'static str;
fn as_lodestone(&self) -> Self::Representation {
match *self {
ClassJobRole::ClassTank => "_class_TANK",
ClassJobRole::ClassHealer => "_class_HEALER",
ClassJobRole::ClassDps => "_class_DPS",
ClassJobRole::JobTank => "_job_TANK",
ClassJobRole::JobHealer => "_job_HEALER",
ClassJobRole::JobDps => "_job_DPS",
ClassJobRole::ClassDiscipleOfTheHand => "_class_CRAFTER",
ClassJobRole::ClassDiscipleOfTheLand => "_class_GATHERER",
}
}
}
impl AsLodestone for Job {
type Representation = u8;
fn as_lodestone(&self) -> Self::Representation {
match *self {
Job::Gladiator => 1,
Job::Pugilist => 2,
Job::Marauder => 3,
Job::Lancer => 4,
Job::Archer => 5,
Job::Conjurer => 6,
Job::Thaumaturge => 7,
Job::Carpenter => 8,
Job::Blacksmith => 9,
Job::Armorer => 10,
Job::Goldsmith => 11,
Job::Leatherworker => 12,
Job::Weaver => 13,
Job::Alchemist => 14,
Job::Culinarian => 15,
Job::Miner => 16,
Job::Botanist => 17,
Job::Fisher => 18,
Job::Paladin => 19,
Job::Monk => 20,
Job::Warrior => 21,
Job::Dragoon => 22,
Job::Bard => 23,
Job::WhiteMage => 24,
Job::BlackMage => 25,
Job::Arcanist => 26,
Job::Summoner => 27,
Job::Scholar => 28,
Job::Rogue => 29,
Job::Ninja => 30,
Job::Machinist => 31,
Job::DarkKnight => 32,
Job::Astrologian => 33,
Job::Samurai => 34,
Job::RedMage => 35,
}
}
}
impl AsLodestone for GrandCompany {
type Representation = u8;
fn as_lodestone(&self) -> Self::Representation {
match *self {
GrandCompany::Maelstrom => 1,
GrandCompany::TwinAdders => 2,
GrandCompany::Flames => 3,
}
}
}
impl AsLodestone for Race {
type Representation = u8;
fn as_lodestone(&self) -> Self::Representation {
match *self {
Race::Hyur => 1,
Race::Elezen => 2,
Race::Lalafell => 3,
Race::Miqote => 4,
Race::Roegadyn => 5,
Race::AuRa => 6,
}
}
}
impl AsLodestone for Clan {
type Representation = u8;
fn as_lodestone(&self) -> Self::Representation {
match *self {
Clan::Midlander => 1,
Clan::Highlander => 2,
Clan::Wildwood => 3,
Clan::Duskwight => 4,
Clan::Plainsfolk => 5,
Clan::Dunesfolk => 6,
Clan::SeekerOfTheSun => 7,
Clan::KeeperOfTheMoon => 8,
Clan::SeaWolf => 9,
Clan::Hellsguard => 10,
Clan::Raen => 11,
Clan::Xaela => 12,
}
}
}

16
src/util/either.rs Normal file
View File

@ -0,0 +1,16 @@
crate enum Either<L, R> {
Left(L),
Right(R),
}
impl<L, R> std::fmt::Debug for Either<L, R>
where L: std::fmt::Debug,
R: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
Either::Left(ref l) => f.debug_tuple("Left").field(l).finish(),
Either::Right(ref r) => f.debug_tuple("Right").field(r).finish(),
}
}
}