use std::fs::File;
use super::Result;
use std::io::prelude::*;
use serde_yaml::from_str;
use std::env::current_dir;
pub const CONFIG_FILE: &str = ".changelog.yml";
const CONFIG_DEFAULT: &str = include_str!("assets/changelog.yml");
pub const TEMPLATE_FILE: &str = ".changelog.hbs";
const TEMPLATE_DEFAULT: &str = include_str!("assets/changelog.hbs");
#[serde(default)]
#[derive(Debug, Default, Deserialize)]
pub struct Configuration {
pub conventions: Conventions,
pub output: OutputPreferences,
}
#[serde(default)]
#[derive(Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Conventions {
pub scopes: Vec<Keyword>,
pub categories: Vec<Keyword>,
}
#[serde(default)]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct Keyword {
pub tag: String,
pub title: String,
}
#[serde(default)]
#[derive(Debug, Default, Deserialize, Eq, PartialEq)]
pub struct OutputPreferences {
pub json: bool,
pub template: Option<String>,
pub remote: Option<String>,
pub post_processors: Vec<PostProcessor>,
}
#[serde(default)]
#[derive(Debug, Default, Deserialize, Eq, PartialEq)]
pub struct PostProcessor {
pub lookup: String,
pub replace: String,
}
impl Configuration {
pub fn new() -> Self {
Self::from_file(None).unwrap_or_else(|_| Self::default())
}
pub fn from_file(file: Option<&str>) -> Result<Self> {
file.map(str::to_owned)
.or_else(|| find_file(CONFIG_FILE))
.map_or_else(|| Ok(String::from(CONFIG_DEFAULT)), |f| read_file(&f))
.and_then(|yml| Self::from_yaml(&yml))
}
pub fn from_yaml(yml: &str) -> Result<Self> {
from_str(yml).map_err(|e| format_err!("Configuration contains invalid YAML: {}", e))
}
}
impl Conventions {
pub fn scope_title(&self, scope: Option<String>) -> Option<&str> {
self.title(&self.scopes, scope)
}
pub fn category_title(&self, category: Option<String>) -> Option<&str> {
self.title(&self.categories, category)
}
pub fn category_titles(&self) -> Vec<&str> {
Self::titles(&self.categories)
}
pub fn scope_titles(&self) -> Vec<&str> {
Self::titles(&self.scopes)
}
fn title<'a>(&'a self, keywords: &'a [Keyword], tag: Option<String>) -> Option<&'a str> {
if keywords.is_empty() && tag.is_none() {
return Some("");
}
let given = tag.unwrap_or_default();
for kw in keywords {
if kw.tag == given {
return Some(&kw.title);
}
}
None
}
fn titles(keywords: &[Keyword]) -> Vec<&str> {
if keywords.is_empty() {
vec![""]
} else {
keywords.iter().map(|k| k.title.as_ref()).collect()
}
}
}
impl Keyword {
pub fn new<T: AsRef<str>>(tag: T, title: T) -> Self {
Keyword {
tag: tag.as_ref().to_owned(),
title: title.as_ref().to_owned(),
}
}
}
impl OutputPreferences {
pub fn new() -> Self {
Self::default()
}
pub fn get_template(&self) -> Result<String> {
self.template
.clone()
.or_else(|| find_file(TEMPLATE_FILE))
.map_or_else(|| Ok(String::from(TEMPLATE_DEFAULT)), |f| read_file(&f))
}
}
fn read_file(name: &str) -> Result<String> {
info!("Reading file '{}'", name);
let mut contents = String::new();
File::open(name)
.map_err(|e| format_err!("Cannot open file '{}' (Reason: {})", name, e))?
.read_to_string(&mut contents)
.map_err(|e| format_err!("Cannot read file '{}' (Reason: {})", name, e))?;
Ok(contents)
}
fn find_file(file: &str) -> Option<String> {
let mut cwd = current_dir().expect("Current directory is invalid");
while cwd.exists() {
cwd.push(file);
if cwd.is_file() {
return Some(cwd.to_string_lossy().to_string());
}
cwd.pop();
if cwd.parent().is_some() {
cwd.pop();
} else {
break;
}
}
None
}
#[cfg(test)]
mod tests {
use super::Configuration;
#[test]
fn configuration_from_yaml() {
let project = include_str!("../.changelog.yml");
let no_category = r#"
conventions:
scopes: [{keyword:"a", title: "A"}]
"#;
let no_scope = r#"
conventions:
categories: [{keyword:"a", title: "A"}]
"#;
assert!(Configuration::from_yaml("").is_err());
assert!(Configuration::from_yaml(project).is_ok());
assert!(Configuration::from_yaml(no_scope).is_ok());
assert!(Configuration::from_yaml(no_category).is_ok());
}
#[test]
fn find_file() {
use super::find_file;
assert!(find_file("unknown").is_none());
assert!(find_file("Cargo.toml").is_some());
}
}