commit d2d637c2d99bc048df4a4c8ea72f2bfe5162946c Author: Adrian Malacoda Date: Sun Feb 26 16:55:17 2017 -0600 initial commit diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6dd977d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name="stc" +version="0.0.1" +authors=["Adrian Malacoda "] + +[dependencies] +log = "0.3.6" +env_logger = "0.4.0" +sqlite = "0.23.4" +select = "0.3.0" +reqwest = "0.4.0" +serde_json = "0.9" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b48a803 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Search the City +Search the City (stc) searches for things. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2b8faa8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,27 @@ +extern crate reqwest; +extern crate select; +extern crate serde_json; + +pub mod searchers; + +use std::any::Any; + +pub trait Link: Any { + fn label (&self) -> &str; + fn url (&self) -> &str; + fn as_any(&self) -> &Any; +} + +impl Link for Box { + fn label (&self) -> &str { + (**self).label() + } + + fn url (&self) -> &str { + (**self).url() + } + + fn as_any (&self) -> &Any { + self + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ad74708 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,32 @@ +extern crate stc; + +use std::env; + +#[macro_use] +extern crate log; +extern crate env_logger; + +use stc::searchers::{Searcher, AggregateSearcher}; +use stc::searchers::mtg::{MtgCard, MtgSearcher}; +use stc::searchers::yugioh::{YugiohCard, YugiohSearcher}; + +fn main () { + env_logger::init().unwrap(); + + let term = env::args().nth(1).expect("please supply a search term as argument"); + let mut searchers = AggregateSearcher::new(); + searchers.add_searcher("mtg", Box::new(MtgSearcher::new())); + searchers.add_searcher("ygo", Box::new(YugiohSearcher::new())); + match searchers.exact_search(&term) { + Some(item) => { + if let Some(card) = item.as_any().downcast_ref::() { + println!("{:?}", card); + } else if let Some(card) = item.as_any().downcast_ref::() { + println!("{:?}", card); + } else { + println!("{}: {}", item.label(), item.url()); + } + }, + None => println!("not found...") + } +} diff --git a/src/searchers/mod.rs b/src/searchers/mod.rs new file mode 100644 index 0000000..2a232d6 --- /dev/null +++ b/src/searchers/mod.rs @@ -0,0 +1,75 @@ +pub mod mtg; +pub mod yugioh; + +use Link; +use std::any::Any; +use std::collections::BTreeMap; + +pub trait Searcher { + fn fuzzy_search (&self, name: &str) -> Option { + self.exact_search(name) + } + + fn exact_search (&self, name: &str) -> Option; +} + +type SearchFn = Box Option>>; +pub struct AggregateSearcher { + searchers: BTreeMap +} + +impl AggregateSearcher { + pub fn new () -> AggregateSearcher { + AggregateSearcher { + searchers: BTreeMap::new() + } + } + + pub fn add_searcher (&mut self, prefix: &str, searcher: Box>) { + let searcher_closure = move |name: String, fuzzy: bool| { + (if fuzzy { + searcher.fuzzy_search(&name[..]) + } else { + searcher.exact_search(&name[..]) + }).map(|value| Box::new(value) as Box) + }; + + self.searchers.insert(String::from(prefix), Box::new(searcher_closure)); + } + + fn run_all_searchers (&self, name: &str) -> Option> { + for (_, searcher) in &self.searchers { + let possible_value = searcher(String::from(name), false); + if possible_value.is_some() { + return possible_value; + } + } + + None + } +} + +impl Searcher> for AggregateSearcher { + fn exact_search (&self, full_name: &str) -> Option> { + match split_prefix(full_name, ":") { + Some((prefix, name)) => { + self.searchers.get(prefix).and_then(|searcher| searcher(String::from(name), true)) + .or_else(|| self.run_all_searchers(full_name)) + }, + None => { + self.run_all_searchers(full_name) + } + } + } +} + +pub fn split_prefix<'a> (input: &'a str, separator: &str) -> Option<(&'a str, &'a str)> { + if input.contains(separator) { + match input.split(separator).into_iter().next() { + Some(prefix) => { Some((prefix, input[prefix.len() + separator.len() ..].trim())) }, + None => None + } + } else { + None + } +} diff --git a/src/searchers/mtg.rs b/src/searchers/mtg.rs new file mode 100644 index 0000000..5237fd9 --- /dev/null +++ b/src/searchers/mtg.rs @@ -0,0 +1,112 @@ +use Link; +use searchers::Searcher; + +use reqwest; +use reqwest::Client; + +use serde_json; +use serde_json::Value; + +use std; +use std::io::Read; + +use std::any::Any; + +#[derive(Debug)] +pub struct MtgCard { + name: String, + cost: String, + typeline: String, + rules: String, + flavor: Option, + power: Option, + toughness: Option, + url: String, + image_url: String +} + +impl Link for MtgCard { + fn label (&self) -> &str { + &self.name + } + + fn url (&self) -> &str { + &self.url + } + + fn as_any (&self) -> &Any { + self + } +} + +pub struct MtgSearcher { + client: Client +} + +impl MtgSearcher { + pub fn new () -> MtgSearcher { + MtgSearcher { + client: Client::new().unwrap() + } + } + + fn do_search (&self, name: &str) -> Result { + let mut contents = String::new(); + let api_url = &format!("https://api.magicthegathering.io/v1/cards?name={}", name); + self.client.get(api_url).send()?.read_to_string(&mut contents)?; + Result::Ok(contents) + } +} + +fn parse_entry (page: String) -> Result { + let parsed: Value = serde_json::from_str(&page)?; + let ref parsed_entry = parsed["cards"][0]; + if let Some(_) = parsed_entry.as_object() { + Result::Ok(MtgCard { + name: parsed_entry["name"].as_str().map(String::from).expect("expected name in json data"), + cost: parsed_entry["manaCost"].as_str().map(String::from).expect("expected cost in json data"), + typeline: parsed_entry["type"].as_str().map(String::from).expect("expected type in json data"), + rules: parsed_entry["text"].as_str().map(String::from).expect("expected text in json data"), + flavor: parsed_entry["flavor"].as_str().map(String::from), + power: parsed_entry["power"].as_str().map(String::from), + toughness: parsed_entry["toughness"].as_str().map(String::from), + url: parsed_entry["imageUrl"].as_str().map(String::from).expect("expected image url in json data"), + image_url: parsed_entry["imageUrl"].as_str().map(String::from).expect("expected image url in json data") + }) + } else { + Result::Err(Error::Other(String::from("No card info found"))) + } +} + +impl Searcher for MtgSearcher { + fn exact_search (&self, name: &str) -> Option { + let search = format!(r#""{}""#, name); + self.do_search(&search).and_then(parse_entry).ok() + } +} + +#[derive(Debug)] +enum Error { + Http(reqwest::Error), + Io(std::io::Error), + Json(serde_json::Error), + Other(String) +} + +impl From for Error { + fn from (error: reqwest::Error) -> Error { + Error::Http(error) + } +} + +impl From for Error { + fn from (error: std::io::Error) -> Error { + Error::Io(error) + } +} + +impl From for Error { + fn from (error: serde_json::Error) -> Error { + Error::Json(error) + } +} diff --git a/src/searchers/yugioh.rs b/src/searchers/yugioh.rs new file mode 100644 index 0000000..0bad8be --- /dev/null +++ b/src/searchers/yugioh.rs @@ -0,0 +1,116 @@ +use Link; +use searchers::Searcher; + +use reqwest; +use reqwest::Client; + +use serde_json; +use serde_json::Value; + +use std; +use std::io::Read; + +use std::any::Any; + +#[derive(Debug)] +pub struct YugiohCard { + name: String, + card_type: String, + text: String, + subtype: Option, + family: Option, + atk: Option, + def: Option, + level: Option, + url: String, + image_url: String +} + +impl Link for YugiohCard { + fn label (&self) -> &str { + &self.name + } + + fn url (&self) -> &str { + &self.url + } + + fn as_any (&self) -> &Any { + self + } +} + +pub struct YugiohSearcher { + client: Client +} + +impl YugiohSearcher { + pub fn new () -> YugiohSearcher { + YugiohSearcher { + client: Client::new().unwrap() + } + } + + fn do_search (&self, name: &str) -> Result { + let mut contents = String::new(); + let api_url = &format!("http://yugiohprices.com/api/card_data/{}", name); + self.client.get(api_url).send()?.read_to_string(&mut contents)?; + Result::Ok(contents) + } +} + +fn parse_entry (page: String) -> Result { + let parsed: Value = serde_json::from_str(&page)?; + let ref parsed_entry = parsed["data"]; + if let Some(_) = parsed_entry.as_object() { + let card_name = parsed_entry["name"].as_str().map(String::from).expect("expected name in json data"); + let card_url = format!("http://yugioh.wikia.com/wiki/{}", card_name); + let card_image_url = format!("http://static.api3.studiobebop.net/ygo_data/card_images/{}.jpg", card_name.replace(" ", "_")); + Result::Ok(YugiohCard { + name: card_name, + card_type: parsed_entry["card_type"].as_str().map(String::from).expect("expected card type in json data"), + text: parsed_entry["text"].as_str().map(String::from).expect("expected text in json data"), + subtype: parsed_entry["type"].as_str().map(String::from), + family: parsed_entry["family"].as_str().map(String::from), + atk: parsed_entry["atk"].as_u64(), + def: parsed_entry["def"].as_u64(), + level: parsed_entry["level"].as_u64(), + url: card_url, + image_url: card_image_url + }) + } else { + Result::Err(Error::Other(String::from("No card info found"))) + } +} + +impl Searcher for YugiohSearcher { + fn exact_search (&self, name: &str) -> Option { + self.do_search(name).and_then(parse_entry).ok() + } +} + +#[derive(Debug)] +enum Error { + Http(reqwest::Error), + Io(std::io::Error), + Json(serde_json::Error), + Other(String) +} + +impl From for Error { + fn from (error: reqwest::Error) -> Error { + Error::Http(error) + } +} + +impl From for Error { + fn from (error: std::io::Error) -> Error { + Error::Io(error) + } +} + +impl From for Error { + fn from (error: serde_json::Error) -> Error { + Error::Json(error) + } +}