use std::cmp::Ordering;
use std::str::FromStr;
use std::iter::IntoIterator;
use std::path::Path;
use std::fs::read_to_string;
use std::env::current_dir;
use debug_ignore::DebugIgnore;
use lazy_static::lazy_static;
use const_format::concatcp;
use directories::BaseDirs;
use serde::{Serialize, Deserialize};
use yaml_rust::{YamlLoader, Yaml};
use regex::Regex;
use super::default_types::default_media_types;
const DEFAULT_HOST: &str = "localhost";
const DEFAULT_PORT: u16 = 3000;
const DEFAULT_FORMAT: &str = "text/plain";
const DEFAULT_TIMEOUT: f32 = 10.0;
const DEFAULT_RETRY: u32 = 0;
const ENV_PREFIX: &str = "BOOMACK_";
const CWD_CONFIG_FILE: &str = "boomack";
const CWD_SERVER_CONFIG_FILE: &str = "boomack-server";
const USER_CONFIG_FILE: &str = ".boomack";
const USER_SERVER_CONFIG_FILE: &str = ".boomack-server";
fn with_def(v: &Option<String>, def: &str) -> String {
String::from(v.as_deref().unwrap_or(def))
}
fn fill_gap<T>(target: &mut Option<T::Owned>, other: &Option<T>)
where T: ToOwned
{
if let None = *target {
if let Some(v) = other {
*target = Some(v.to_owned());
}
}
}
fn lookup_yaml<'t>(data: &'t Yaml, key: &str) -> Option<&'t Yaml> {
if let Yaml::Hash(m) = data {
if let Some(v) = m.get(&Yaml::String(String::from(key))) {
return Some(&v);
}
}
None
}
fn lookup_yaml_str(data: &Yaml, key: &str) -> Option<String> {
lookup_yaml(data, key).and_then(|v| {
if let Yaml::String(s) = v {
Some(s.to_string())
} else { None }
})
}
fn lookup_yaml_u16(data: &Yaml, key: &str) -> Option<u16> {
lookup_yaml(data, key).and_then(|v| {
if let Yaml::Integer(n) = v {
if *n >= u16::MIN as i64 && *n <= u16::MAX as i64 {
return Some(*n as u16)
}
}
None
})
}
fn lookup_yaml_u32(data: &Yaml, key: &str) -> Option<u32> {
lookup_yaml(data, key).and_then(|v| {
if let Yaml::Integer(n) = v {
if *n >= u32::MIN as i64 && *n <= u32::MAX as i64 {
return Some(*n as u32)
}
}
None
})
}
fn lookup_yaml_f32(data: &Yaml, key: &str) -> Option<f32> {
lookup_yaml(data, key).and_then(|v| {
if let Yaml::Real(s) = v {
let n = f64::from_str(s);
if let Ok(n) = n {
if n >= f32::MIN as f64 && n <= f32::MAX as f64 {
return Some(n as f32)
}
}
}
None
})
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub host: Option<String>,
pub port: Option<u16>,
pub url: Option<String>,
}
#[derive(Debug, Eq, Clone, Serialize, Deserialize)]
pub struct MediaTypeMapping {
pub pattern: String,
pub media_type: String,
pub re: String,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct ClientConfig {
pub format: Option<String>,
pub timeout: Option<f32>,
pub retry: Option<u32>,
pub token: Option<String>,
#[serde(skip)]
pub types: DebugIgnore<Vec<MediaTypeMapping>>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct Config {
pub server: ServerConfig,
pub client: ClientConfig,
}
impl ServerConfig {
const ENV_PREFIX: &'static str = concatcp!(ENV_PREFIX, "SERVER_");
pub fn new() -> ServerConfig {
ServerConfig {
host: None,
port: None,
url: None,
}
}
pub fn from_env() -> ServerConfig {
ServerConfig {
host: std::env::var(concatcp!(ServerConfig::ENV_PREFIX, "HOST")).ok(),
port: std::env::var(concatcp!(ServerConfig::ENV_PREFIX, "PORT")).ok()
.and_then(|v| u16::from_str(&v).ok()),
url: std::env::var(concatcp!(ServerConfig::ENV_PREFIX, "URL")).ok(),
}
}
pub fn from_yaml(data: &Yaml) -> ServerConfig {
ServerConfig {
host: lookup_yaml_str(data, "host"),
port: lookup_yaml_u16(data, "port"),
url: lookup_yaml_str(data, "url"),
}
}
pub fn fill_gaps_with(&mut self, other: &ServerConfig) {
fill_gap(&mut self.host, &other.host);
fill_gap(&mut self.port, &other.port);
fill_gap(&mut self.url, &other.url);
}
pub fn get_api_url(&self) -> String {
if let Some(url) = &self.url {
return String::from(url)
} else {
return format!("http://{}:{}",
self.host.as_deref().unwrap_or(DEFAULT_HOST),
self.port.unwrap_or(DEFAULT_PORT));
}
}
}
lazy_static! {
static ref ESCAPE_RE: Regex = Regex::new(r"[.*+?^${}()|\[\]\\]").unwrap();
}
impl MediaTypeMapping {
pub fn new(pattern: &str, media_type: &str) -> MediaTypeMapping {
MediaTypeMapping {
pattern: pattern.to_lowercase(),
media_type: String::from(media_type),
re: format!("^(?i){}$",
ESCAPE_RE.replace_all(pattern, "\\$0")
.replace("\\*", ".*")
.replace("\\?", ".")),
}
}
pub fn from(key: &Yaml, value: &Yaml) -> Option<MediaTypeMapping> {
if let Yaml::String(k) = key {
if let Yaml::String(v) = value {
return Some(MediaTypeMapping::new(k, v))
}
}
None
}
pub fn is_match(&self, filename: &str) -> bool {
let re = Regex::new(&self.re).unwrap();
re.is_match(filename)
}
fn priority_value(&self) -> u8 {
if self.pattern.contains("*") { 2 }
else if self.pattern.contains("?") { 1 }
else { 0 }
}
}
impl PartialEq for MediaTypeMapping {
fn eq(&self, other: &Self) -> bool {
self.pattern.eq(&other.pattern)
}
}
impl Ord for MediaTypeMapping {
fn cmp(&self, other: &Self) -> Ordering {
let priority_ord = self.priority_value().cmp(&other.priority_value());
if !matches!(priority_ord, Ordering::Equal) {
priority_ord
} else {
let length_ord = self.pattern.len().cmp(&other.pattern.len()).reverse();
if !matches!(length_ord, Ordering::Equal) {
length_ord
} else {
self.pattern.cmp(&other.pattern)
}
}
}
}
impl PartialOrd for MediaTypeMapping {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl ClientConfig {
const ENV_PREFIX: &'static str = concatcp!(ENV_PREFIX, "CLIENT_");
fn new() -> ClientConfig {
ClientConfig {
format: None,
timeout: None,
retry: None,
token: None,
types: Vec::new().into(),
}
}
fn from_env() -> ClientConfig {
ClientConfig {
format: std::env::var(concatcp!(ClientConfig::ENV_PREFIX, "FORMAT")).ok(),
timeout: std::env::var(concatcp!(ClientConfig::ENV_PREFIX, "TIMEOUT")).ok()
.and_then(|v| f32::from_str(&v).ok()),
retry: std::env::var(concatcp!(ClientConfig::ENV_PREFIX, "RETRY")).ok()
.and_then(|v| u32::from_str(&v).ok()),
token: std::env::var(concatcp!(ClientConfig::ENV_PREFIX, "TOKEN")).ok(),
types: Vec::new().into(),
}
}
fn from_yaml(data: &Yaml) -> ClientConfig {
let mut types = Vec::new();
lookup_yaml(data, "types").map(|m| {
if let Yaml::Hash(map) = m {
for (k, v) in map {
if let Some(mtm) = MediaTypeMapping::from(&k, &v) {
types.push(mtm);
}
}
}
});
ClientConfig {
format: lookup_yaml_str(data, "format"),
timeout: lookup_yaml_f32(data, "timeout"),
retry: lookup_yaml_u32(data, "retry"),
token: lookup_yaml_str(data, "token"),
types: types.into(),
}
}
fn fill_gaps_with_media_types(&mut self, types: &Vec<MediaTypeMapping>) {
for mtm in types {
if !self.types.iter().any(|mtm2| mtm.pattern.eq(&mtm2.pattern)) {
self.types.push(mtm.clone());
}
}
}
fn fill_gaps_with(&mut self, other: &ClientConfig) {
fill_gap(&mut self.format, &other.format);
fill_gap(&mut self.timeout, &other.timeout);
fill_gap(&mut self.retry, &other.retry);
fill_gap(&mut self.token, &other.token);
self.fill_gaps_with_media_types(&other.types);
}
pub fn load_default_media_types(&mut self) {
self.fill_gaps_with_media_types(&default_media_types());
}
pub fn prepare_media_types_for_lookup(&mut self) {
self.types.sort_unstable();
}
pub fn get_format(&self) -> String { with_def(&self.format, DEFAULT_FORMAT) }
pub fn get_timeout(&self) -> f32 { self.timeout.unwrap_or(DEFAULT_TIMEOUT) }
pub fn get_retry(&self) -> u32 { self.retry.unwrap_or(DEFAULT_RETRY) }
pub fn lookup_media_type(&self, filename: &str) -> Option<String> {
self.types.iter()
.find(|mtm| mtm.is_match(filename))
.map(|mtm| mtm.media_type.clone())
}
}
impl Config {
pub fn new() -> Config {
Config {
server: ServerConfig::new(),
client: ClientConfig::new(),
}
}
pub fn from_env() -> Config {
Config {
server: ServerConfig::from_env(),
client: ClientConfig::from_env(),
}
}
pub fn from_yaml(data: &Yaml) -> Config {
let mut server: Option<ServerConfig> = None;
let mut client: Option<ClientConfig> = None;
match data {
Yaml::Hash(root) => {
if let Some(data) = root.get(&Yaml::String(String::from("server"))) {
server = Some(ServerConfig::from_yaml(data));
}
if let Some(data) = root.get(&Yaml::String(String::from("client"))) {
client = Some(ClientConfig::from_yaml(data));
}
},
_ => { }
}
Config {
server: server.unwrap_or_else(ServerConfig::new),
client: client.unwrap_or_else(ClientConfig::new),
}
}
pub fn fill_gaps_with(&mut self, other: &Config) {
self.server.fill_gaps_with(&other.server);
self.client.fill_gaps_with(&other.client);
}
fn load_config_file(&mut self, path: &Path) -> bool {
let text = read_to_string(path);
match text {
Ok(text) => {
match YamlLoader::load_from_str(&text) {
Ok(yaml) => {
for doc in yaml {
self.fill_gaps_with(&Config::from_yaml(&doc));
}
return true
},
Err(err) => {
eprintln!("Failed to parse JSON/YAML from: {:?}", path);
eprintln!("{:?}", err);
},
}
},
Err(err) => {
eprintln!("Failed to read text content from: {:?}", path);
eprintln!("{:?}", err);
},
}
false
}
fn load_config_file_any_ext(&mut self, path: &Path) {
let extensions = ["", "json", "yaml", "yml"];
for ext in extensions {
let path2 = path.with_extension(ext);
if path2.is_file() {
self.load_config_file(&path2);
break
}
}
}
pub fn load_unknown_config_files<T>(&mut self, paths: T) -> bool
where T: IntoIterator, T::Item: AsRef<Path>,
T::IntoIter: DoubleEndedIterator
{
for path in paths.into_iter().rev() {
if !self.load_config_file(path.as_ref()) {
return false
}
}
return true
}
pub fn load_known_config_files(&mut self) {
let base_dirs = BaseDirs::new();
let home_dir = base_dirs.map(|bd| bd.home_dir().to_owned());
let working_dir = current_dir().ok();
if let Some(working_dir) = &working_dir {
self.load_config_file_any_ext(&working_dir.join(CWD_CONFIG_FILE));
}
if let Some(home_dir) = &home_dir {
self.load_config_file_any_ext(&home_dir.join(USER_CONFIG_FILE));
}
if let Some(working_dir) = &working_dir {
self.load_config_file_any_ext(&working_dir.join(CWD_SERVER_CONFIG_FILE));
}
if let Some(home_dir) = &home_dir {
self.load_config_file_any_ext(&home_dir.join(USER_SERVER_CONFIG_FILE));
}
}
pub fn load_default_media_types(&mut self) {
self.client.load_default_media_types();
self.client.prepare_media_types_for_lookup();
}
}