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