1use std::collections::hash_map::HashMap;
8use std::fs::File;
9use std::io;
10use std::path::Path;
11
12use serde::Deserialize;
13use serde_json::Value;
14use thiserror::Error;
15
16use crate::handler::Handler;
17
18#[derive(Deserialize, Debug)]
20pub struct Config {
21 #[serde(default)]
22 pub(crate) post_paths: HashMap<String, Handler>,
23 secrets: Option<String>,
24}
25
26#[derive(Debug, Error)]
27pub enum ConfigError {
28 #[error("deserialization error: {}", source)]
29 Deserialize {
30 #[source]
31 source: serde_json::Error,
32 },
33 #[error("failed to read file: {}", source)]
34 Read {
35 #[source]
36 source: io::Error,
37 },
38}
39
40impl Config {
41 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
43 let fin = File::open(path.as_ref()).map_err(|source| {
44 ConfigError::Read {
45 source,
46 }
47 })?;
48 serde_json::from_reader(fin).map_err(|source| {
49 ConfigError::Deserialize {
50 source,
51 }
52 })
53 }
54
55 pub(crate) fn secrets(&self) -> Result<Value, ConfigError> {
56 if let Some(ref path) = self.secrets {
57 let fin = File::open(path).map_err(|source| {
58 ConfigError::Read {
59 source,
60 }
61 })?;
62 serde_json::from_reader(fin).map_err(|source| {
63 ConfigError::Deserialize {
64 source,
65 }
66 })
67 } else {
68 Ok(Value::Null)
69 }
70 }
71
72 #[cfg(test)]
73 pub(crate) fn secrets_path(&self) -> Option<&String> {
74 self.secrets.as_ref()
75 }
76}
77
78#[cfg(test)]
79mod test {
80 use std::fs::File;
81 use std::io::Write;
82
83 use serde_json::{json, Value};
84
85 use crate::config::{Config, ConfigError};
86 use crate::test_utils;
87
88 #[test]
89 fn test_config_empty_paths() {
90 let tempdir = test_utils::create_tempdir();
91 let path = test_utils::write_config(tempdir.path(), json!({}));
92 let config = Config::from_path(path).unwrap();
93
94 assert!(config.post_paths.is_empty());
95 assert_eq!(config.secrets, None);
96 assert_eq!(config.secrets().unwrap(), Value::Null);
97 }
98
99 #[test]
100 fn test_config_unreadable() {
101 let path = {
102 let tempdir = test_utils::create_tempdir();
103 test_utils::write_config(tempdir.path(), json!({}))
104 };
105 let err = Config::from_path(path).unwrap_err();
106
107 if let ConfigError::Read {
108 ..
109 } = err
110 {
111 } else {
113 panic!("unexpected error: {:?}", err);
114 }
115 }
116
117 #[test]
118 fn test_config_unparseable() {
119 let tempdir = test_utils::create_tempdir();
120 let path = tempdir.path().join("config.json");
121 {
122 let mut fout = File::create(&path).unwrap();
123 fout.write_all(b"not json\n").unwrap();
124 }
125 let err = Config::from_path(path).unwrap_err();
126
127 if let ConfigError::Deserialize {
128 ..
129 } = err
130 {
131 } else {
133 panic!("unexpected error: {:?}", err);
134 }
135 }
136
137 #[test]
138 fn test_secrets_unreadable() {
139 let config = {
140 let tempdir = test_utils::create_tempdir();
141 let (path, _) = test_utils::write_config_secrets(tempdir.path(), json!({}), json!({}));
142 Config::from_path(path).unwrap()
143 };
144 let err = config.secrets().unwrap_err();
145
146 if let ConfigError::Read {
147 ..
148 } = err
149 {
150 } else {
152 panic!("unexpected error: {:?}", err);
153 }
154 }
155
156 #[test]
157 fn test_secrets_unparseable() {
158 let tempdir = test_utils::create_tempdir();
159 let secrets_path = tempdir.path().join("secrets.json");
160 let path = test_utils::write_config(
161 tempdir.path(),
162 json!({
163 "secrets": secrets_path.to_str().unwrap(),
164 }),
165 );
166 {
167 let mut fout = File::create(&secrets_path).unwrap();
168 fout.write_all(b"not json\n").unwrap();
169 }
170 let config = Config::from_path(path).unwrap();
171 let err = config.secrets().unwrap_err();
172
173 if let ConfigError::Deserialize {
174 ..
175 } = err
176 {
177 } else {
179 panic!("unexpected error: {:?}", err);
180 }
181 }
182
183 #[test]
184 fn test_config_parse() {
185 let tempdir = test_utils::create_tempdir();
186 let path = test_utils::write_config(
187 tempdir.path(),
188 json!({
189 "post_paths": {
190 "hostname.example": {
191 "path": "path",
192 "filters": [
193 {
194 "kind": "blah",
195 },
196 ],
197 "header_name": "X-WebHook-Type",
198 },
199 },
200 }),
201 );
202 let config = Config::from_path(path).unwrap();
203
204 assert_eq!(config.post_paths.len(), 1);
205 }
206}