1use std::fs::File;
5use super::Result;
6use std::io::prelude::*;
7use serde_yaml::from_str;
8use std::env::current_dir;
9
10pub const CONFIG_FILE: &str = ".changelog.yml";
15
16const CONFIG_DEFAULT: &str = include_str!("assets/changelog.yml");
18
19pub const TEMPLATE_FILE: &str = ".changelog.hbs";
24
25const TEMPLATE_DEFAULT: &str = include_str!("assets/changelog.hbs");
27
28#[serde(default)]
32#[derive(Debug, Default, Deserialize)]
33pub struct Configuration {
34 pub conventions: Conventions,
36
37 pub output: OutputPreferences,
39}
40
41#[serde(default)]
43#[derive(Debug, Default, Deserialize, Eq, PartialEq)]
44pub struct Conventions {
45 pub scopes: Vec<Keyword>,
47
48 pub categories: Vec<Keyword>,
50}
51
52#[serde(default)]
54#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
55pub struct Keyword {
56 pub tag: String,
58
59 pub title: String,
61}
62
63#[serde(default)]
65#[derive(Debug, Default, Deserialize, Eq, PartialEq)]
66pub struct OutputPreferences {
67 pub json: bool,
69
70 pub template: Option<String>,
72
73 pub remote: Option<String>,
75
76 pub post_processors: Vec<PostProcessor>,
78}
79
80#[serde(default)]
82#[derive(Debug, Default, Deserialize, Eq, PartialEq)]
83pub struct PostProcessor {
84 pub lookup: String,
86
87 pub replace: String,
89}
90
91impl Configuration {
92 pub fn new() -> Self {
94 Self::from_file(None).unwrap_or_else(|_| Self::default())
95 }
96
97 pub fn from_file(file: Option<&str>) -> Result<Self> {
99 file.map(str::to_owned)
100 .or_else(|| find_file(CONFIG_FILE))
101 .map_or_else(|| Ok(String::from(CONFIG_DEFAULT)), |f| read_file(&f))
102 .and_then(|yml| Self::from_yaml(&yml))
103 }
104
105 pub fn from_yaml(yml: &str) -> Result<Self> {
107 from_str(yml).map_err(|e| format_err!("Configuration contains invalid YAML: {}", e))
108 }
109}
110
111impl Conventions {
112 pub fn scope_title(&self, scope: Option<String>) -> Option<&str> {
114 self.title(&self.scopes, scope)
115 }
116
117 pub fn category_title(&self, category: Option<String>) -> Option<&str> {
119 self.title(&self.categories, category)
120 }
121
122 pub fn category_titles(&self) -> Vec<&str> {
124 Self::titles(&self.categories)
125 }
126
127 pub fn scope_titles(&self) -> Vec<&str> {
129 Self::titles(&self.scopes)
130 }
131
132 fn title<'a>(&'a self, keywords: &'a [Keyword], tag: Option<String>) -> Option<&'a str> {
134 if keywords.is_empty() && tag.is_none() {
136 return Some("");
137 }
138
139 let given = tag.unwrap_or_default();
141 for kw in keywords {
142 if kw.tag == given {
143 return Some(&kw.title);
144 }
145 }
146
147 None
148 }
149
150 fn titles(keywords: &[Keyword]) -> Vec<&str> {
152 if keywords.is_empty() {
153 vec![""]
154 } else {
155 keywords.iter().map(|k| k.title.as_ref()).collect()
156 }
157 }
158}
159
160impl Keyword {
161 pub fn new<T: AsRef<str>>(tag: T, title: T) -> Self {
163 Keyword {
164 tag: tag.as_ref().to_owned(),
165 title: title.as_ref().to_owned(),
166 }
167 }
168}
169
170impl OutputPreferences {
171 pub fn new() -> Self {
173 Self::default()
174 }
175
176 pub fn get_template(&self) -> Result<String> {
178 self.template
179 .clone()
180 .or_else(|| find_file(TEMPLATE_FILE))
181 .map_or_else(|| Ok(String::from(TEMPLATE_DEFAULT)), |f| read_file(&f))
182 }
183}
184
185fn read_file(name: &str) -> Result<String> {
187 info!("Reading file '{}'", name);
189 let mut contents = String::new();
190
191 File::open(name)
192 .map_err(|e| format_err!("Cannot open file '{}' (Reason: {})", name, e))?
193 .read_to_string(&mut contents)
194 .map_err(|e| format_err!("Cannot read file '{}' (Reason: {})", name, e))?;
195
196 Ok(contents)
197}
198
199fn find_file(file: &str) -> Option<String> {
201 let mut cwd = current_dir().expect("Current directory is invalid");
203
204 while cwd.exists() {
206 cwd.push(file);
208
209 if cwd.is_file() {
211 return Some(cwd.to_string_lossy().to_string());
213 }
214
215 cwd.pop();
217
218 if cwd.parent().is_some() {
220 cwd.pop();
222 } else {
223 break;
225 }
226 }
227
228 None
230}
231
232#[cfg(test)]
233mod tests {
234 use super::Configuration;
235
236 #[test]
237 fn configuration_from_yaml() {
238 let project = include_str!("../.changelog.yml");
239 let no_category = r#"
240 conventions:
241 scopes: [{keyword:"a", title: "A"}]
242 "#;
243 let no_scope = r#"
244 conventions:
245 categories: [{keyword:"a", title: "A"}]
246 "#;
247 assert!(Configuration::from_yaml("").is_err());
248 assert!(Configuration::from_yaml(project).is_ok());
249 assert!(Configuration::from_yaml(no_scope).is_ok());
250 assert!(Configuration::from_yaml(no_category).is_ok());
251 }
252
253 #[test]
254 fn find_file() {
255 use super::find_file;
256 assert!(find_file("unknown").is_none());
257 assert!(find_file("Cargo.toml").is_some());
258 }
259}