initial commit
This commit is contained in:
commit
d2d637c2d9
12
Cargo.toml
Normal file
12
Cargo.toml
Normal 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
2
README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# Search the City
|
||||
Search the City (stc) searches for things.
|
27
src/lib.rs
Normal file
27
src/lib.rs
Normal 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
32
src/main.rs
Normal 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
75
src/searchers/mod.rs
Normal 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
112
src/searchers/mtg.rs
Normal 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
116
src/searchers/yugioh.rs
Normal 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)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user