initial commit

This commit is contained in:
Adrian Malacoda 2017-02-26 16:55:17 -06:00
commit d2d637c2d9
7 changed files with 376 additions and 0 deletions

12
Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name="stc"
version="0.0.1"
authors=["Adrian Malacoda <adrian.malacoda@monarch-pass.net>"]
[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"

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# Search the City
Search the City (stc) searches for things.

27
src/lib.rs Normal file
View File

@ -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<Link + 'static> {
fn label (&self) -> &str {
(**self).label()
}
fn url (&self) -> &str {
(**self).url()
}
fn as_any (&self) -> &Any {
self
}
}

32
src/main.rs Normal file
View File

@ -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::<MtgCard>() {
println!("{:?}", card);
} else if let Some(card) = item.as_any().downcast_ref::<YugiohCard>() {
println!("{:?}", card);
} else {
println!("{}: {}", item.label(), item.url());
}
},
None => println!("not found...")
}
}

75
src/searchers/mod.rs Normal file
View File

@ -0,0 +1,75 @@
pub mod mtg;
pub mod yugioh;
use Link;
use std::any::Any;
use std::collections::BTreeMap;
pub trait Searcher<T: Link> {
fn fuzzy_search (&self, name: &str) -> Option<T> {
self.exact_search(name)
}
fn exact_search (&self, name: &str) -> Option<T>;
}
type SearchFn = Box<Fn(String, bool) -> Option<Box<Link>>>;
pub struct AggregateSearcher {
searchers: BTreeMap<String, SearchFn>
}
impl AggregateSearcher {
pub fn new () -> AggregateSearcher {
AggregateSearcher {
searchers: BTreeMap::new()
}
}
pub fn add_searcher<T: Link + 'static> (&mut self, prefix: &str, searcher: Box<Searcher<T>>) {
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<Link>)
};
self.searchers.insert(String::from(prefix), Box::new(searcher_closure));
}
fn run_all_searchers (&self, name: &str) -> Option<Box<Link>> {
for (_, searcher) in &self.searchers {
let possible_value = searcher(String::from(name), false);
if possible_value.is_some() {
return possible_value;
}
}
None
}
}
impl Searcher<Box<Link>> for AggregateSearcher {
fn exact_search (&self, full_name: &str) -> Option<Box<Link>> {
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
}
}

112
src/searchers/mtg.rs Normal file
View File

@ -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<String>,
power: Option<String>,
toughness: Option<String>,
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<String, Error> {
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<MtgCard, Error> {
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<MtgCard> for MtgSearcher {
fn exact_search (&self, name: &str) -> Option<MtgCard> {
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<reqwest::Error> for Error {
fn from (error: reqwest::Error) -> Error {
Error::Http(error)
}
}
impl From<std::io::Error> for Error {
fn from (error: std::io::Error) -> Error {
Error::Io(error)
}
}
impl From<serde_json::Error> for Error {
fn from (error: serde_json::Error) -> Error {
Error::Json(error)
}
}

116
src/searchers/yugioh.rs Normal file
View File

@ -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<String>,
family: Option<String>,
atk: Option<u64>,
def: Option<u64>,
level: Option<u64>,
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<String, Error> {
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<YugiohCard, Error> {
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<YugiohCard> for YugiohSearcher {
fn exact_search (&self, name: &str) -> Option<YugiohCard> {
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<reqwest::Error> for Error {
fn from (error: reqwest::Error) -> Error {
Error::Http(error)
}
}
impl From<std::io::Error> for Error {
fn from (error: std::io::Error) -> Error {
Error::Io(error)
}
}
impl From<serde_json::Error> for Error {
fn from (error: serde_json::Error) -> Error {
Error::Json(error)
}
}