From b538545f67fec975781b6f8dea0f881b0898bce3 Mon Sep 17 00:00:00 2001 From: Kyle Clemens Date: Tue, 4 Sep 2018 16:13:11 -0400 Subject: [PATCH] chore: initial commit --- .gitignore | 3 + Cargo.toml | 22 +++++ LICENSE | 21 +++++ README.md | 3 + src/builder.rs | 5 ++ src/builder/character_search.rs | 135 +++++++++++++++++++++++++++++ src/builder/free_company_search.rs | 95 ++++++++++++++++++++ src/error.rs | 11 +++ src/lib.rs | 73 ++++++++++++++++ src/models.rs | 3 + src/models/class_job_role.rs | 13 +++ src/util.rs | 5 ++ src/util/as_lodestone.rs | 131 ++++++++++++++++++++++++++++ src/util/either.rs | 16 ++++ 14 files changed, 536 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/builder.rs create mode 100644 src/builder/character_search.rs create mode 100644 src/builder/free_company_search.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/models.rs create mode 100644 src/models/class_job_role.rs create mode 100644 src/util.rs create mode 100644 src/util/as_lodestone.rs create mode 100644 src/util/either.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9a17318 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +cargo-features = ["edition"] + +[package] +name = "lodestone_scraper" +version = "0.1.0" +authors = ["Kyle Clemens "] + +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"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c59bec4 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..14768f8 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# lodestone_scraper + +A Lodestone client library. diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..180fd76 --- /dev/null +++ b/src/builder.rs @@ -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; diff --git a/src/builder/character_search.rs b/src/builder/character_search.rs new file mode 100644 index 0000000..c7fd855 --- /dev/null +++ b/src/builder/character_search.rs @@ -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>, + // classjob + job: Option>, + // race_tribe + race: Option>, + // gcid + grand_company: Option>, +} + +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> { + 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 + } +} diff --git a/src/builder/free_company_search.rs b/src/builder/free_company_search.rs new file mode 100644 index 0000000..a664929 --- /dev/null +++ b/src/builder/free_company_search.rs @@ -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>, + // gcid + grand_company: Option>, +} + +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> { + 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 + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..3785e8d --- /dev/null +++ b/src/error.rs @@ -0,0 +1,11 @@ +pub type Result = std::result::Result; + +#[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), +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2c9e431 --- /dev/null +++ b/src/lib.rs @@ -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 { + LODESTONE_URL.join(s).map_err(Error::Url) + } + + pub fn character(&self, id: u64) -> Result { + 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 { + 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) + } +} diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..e8fa437 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,3 @@ +pub mod class_job_role; + +pub use self::class_job_role::ClassJobRole; diff --git a/src/models/class_job_role.rs b/src/models/class_job_role.rs new file mode 100644 index 0000000..f714595 --- /dev/null +++ b/src/models/class_job_role.rs @@ -0,0 +1,13 @@ +#[derive(Debug, Clone, Copy)] +pub enum ClassJobRole { + ClassTank, + ClassHealer, + ClassDps, + + JobTank, + JobHealer, + JobDps, + + ClassDiscipleOfTheHand, + ClassDiscipleOfTheLand, +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..cae8c97 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,5 @@ +crate mod as_lodestone; +crate mod either; + +crate use self::as_lodestone::AsLodestone; +crate use self::either::Either; diff --git a/src/util/as_lodestone.rs b/src/util/as_lodestone.rs new file mode 100644 index 0000000..9288a60 --- /dev/null +++ b/src/util/as_lodestone.rs @@ -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, + } + } +} diff --git a/src/util/either.rs b/src/util/either.rs new file mode 100644 index 0000000..4c0e333 --- /dev/null +++ b/src/util/either.rs @@ -0,0 +1,16 @@ +crate enum Either { + Left(L), + Right(R), +} + +impl std::fmt::Debug for Either + 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(), + } + } +}