use crate::{Endpoint, Error, Provider, Response, Result};
use std::borrow::Cow;
pub type HttpResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
pub trait Http {
fn url_encode<'a>(&mut self, s: &'a str) -> HttpResult<Cow<'a, str>>;
fn get(&mut self, url: &str) -> HttpResult<String>;
}
#[derive(Clone, PartialEq, PartialOrd, Hash, Debug, Default)]
pub struct Schema {
providers: Vec<Provider>,
}
#[derive(Clone, PartialEq, PartialOrd, Hash, Debug)]
pub struct MatchedEndpoint<'a> {
pub provider: &'a Provider,
pub endpoint: &'a Endpoint,
pub matched_scheme: &'a str,
}
impl Schema {
pub fn load_included() -> Self {
let json = include_str!("providers.json");
let providers = serde_json::from_str(&json)
.expect("Failed to load providers.json. This build of oembed is broken!");
Self { providers }
}
pub fn fetch_latest(http: &mut impl Http) -> Result<Self> {
Self::fetch_from_url(http, "https://oembed.com/providers.json")
}
pub fn fetch_from_url(http: &mut impl Http, url: &str) -> Result<Self> {
let s = http.get(url).map_err(|e| Error::HttpGet(e.into()))?;
let providers = serde_json::from_str(&s).map_err(|e| Error::ParseError(e))?;
Ok(Self { providers })
}
pub fn match_endpoint(&self, url: &str) -> Option<MatchedEndpoint> {
for provider in &self.providers {
for endpoint in &provider.endpoints {
if let Some(matched_scheme) = endpoint.match_url_scheme(url) {
return Some(MatchedEndpoint {
provider,
endpoint,
matched_scheme,
});
}
}
}
None
}
pub fn fetch(&self, http: &mut impl Http, url: &str) -> Option<Result<Response>> {
self.match_endpoint(url)
.map(|m| m.endpoint.fetch(http, url))
}
}
impl Endpoint {
pub fn fetch(&self, http: &mut impl Http, url: &str) -> Result<Response> {
let encoded_url = http.url_encode(url).map_err(|e| Error::HttpUrlEncode(e))?;
let request_url = format!("{}?format=json&url={}", self.url, encoded_url);
let s = http.get(&request_url).map_err(|e| Error::HttpGet(e))?;
serde_json::from_str(&s).map_err(|e| Error::ParseError(e))
}
fn match_url_scheme(&self, url: &str) -> Option<&str> {
self.schemes.as_ref().and_then(|schemes| {
schemes
.iter()
.filter(|s| url_matches_scheme(url, &s))
.next()
.map(|s| s.as_str())
})
}
}
fn url_matches_scheme(url: &str, scheme: &str) -> bool {
let mut url_chars = url.chars().peekable();
let mut scheme_chars = scheme.chars().peekable();
let mut current_scheme_char = scheme_chars.next();
while let Some(url_char) = url_chars.next() {
match current_scheme_char {
Some('*') if scheme_chars.peek() == url_chars.peek() => (),
Some('*') => continue,
Some(scheme_char) if scheme_char == url_char => (),
Some(_) | None => return false,
}
current_scheme_char = scheme_chars.next();
}
true
}
#[cfg(test)]
mod tests {
#[test]
fn url_matches_scheme() {
assert_eq!(super::url_matches_scheme("spotify:abc", "spotify:*"), true);
assert_eq!(
super::url_matches_scheme("https://youtu.be/v/5mMOsl8qpfc", "https://youtu.be/*"),
true
);
assert_eq!(
super::url_matches_scheme(
"https://www.youtube.com/watch?v=5mMOsl8qpfc",
"https://www.youtube.com/watch*"
),
true
);
assert_eq!(
super::url_matches_scheme(
"http://www.23hq.com/something/photo/a.jpg",
"http://www.23hq.com/*/photo/*",
),
true
)
}
}