1#![deny(
2 missing_docs,
3 missing_debug_implementations,
4 missing_copy_implementations,
5 trivial_casts,
6 trivial_numeric_casts,
7 unsafe_code,
8 unstable_features,
9 unused_import_braces,
10 unused_qualifications
11)]
12
13use anyhow::{bail, Context, Result};
16use chrono::naive::NaiveDate;
17use glob::glob;
18use indexmap::IndexMap;
19use log::warn;
20use serde::{Deserialize, Serialize};
21use std::collections::BTreeMap;
22use std::path::{Path, PathBuf};
23use std::str::FromStr;
24
25#[derive(Debug)]
27pub struct Repository {
28 path: PathBuf,
29 pub description: RootDescription,
31}
32
33impl Repository {
34 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
36 Ok(Repository {
37 path: path.as_ref().to_path_buf(),
38 description: RootDescription::load_from_file(
39 Self::initial_brevdash_toml_path(path),
40 )?,
41 })
42 }
43
44 pub fn create<P: AsRef<Path>>(
48 path: P,
49 description: RootDescription,
50 ) -> Result<Self> {
51 let r = Repository {
52 path: path.as_ref().to_path_buf(),
53 description,
54 };
55 r.store_description()?;
56 Ok(r)
57 }
58
59 pub fn store_description(&self) -> Result<()> {
61 self.description.store_to_file(self.brevdash_toml_path())
62 }
63
64 fn initial_brevdash_toml_path<P: AsRef<Path>>(p: P) -> PathBuf {
65 p.as_ref().join("brevdash.toml")
66 }
67
68 fn project_dir_path(&self, project_id: &str) -> PathBuf {
69 self.path.join(project_id)
70 }
71
72 fn project_toml_file_path(&self, project_id: &str) -> PathBuf {
73 self.project_dir_path(project_id).join("project.toml")
74 }
75
76 fn project_datapoint_directory_path(
77 &self,
78 project_id: &str,
79 date: NaiveDate,
80 ) -> PathBuf {
81 self.project_dir_path(project_id)
82 .join(date.format("%Y-%m-%d").to_string())
83 }
84
85 fn project_datapoint_toml_file_path(
86 &self,
87 project_id: &str,
88 date: NaiveDate,
89 ) -> PathBuf {
90 self.project_datapoint_directory_path(project_id, date)
91 .join("datapoint.toml")
92 }
93
94 pub fn project_datapoint_artifacts_directory_path(
96 &self,
97 project_id: &str,
98 date: NaiveDate,
99 ) -> PathBuf {
100 self.project_datapoint_directory_path(project_id, date)
101 .join("artifacts")
102 }
103
104 pub fn project_datapoint_characteristic_artifacts_directory_path(
106 &self,
107 project_id: &str,
108 date: NaiveDate,
109 characteristic_id: &str,
110 ) -> PathBuf {
111 self.project_datapoint_artifacts_directory_path(project_id, date)
112 .join(characteristic_id)
113 }
114
115 pub fn project_datapoint_characteristic_artifact_path(
117 &self,
118 project_id: &str,
119 date: NaiveDate,
120 characteristic_id: &str,
121 artifact_relative_path: &Path,
122 ) -> PathBuf {
123 self.project_datapoint_characteristic_artifacts_directory_path(
124 project_id,
125 date,
126 characteristic_id,
127 )
128 .join(artifact_relative_path)
129 }
130
131 fn brevdash_toml_path(&self) -> PathBuf {
132 self.path.join("brevdash.toml")
133 }
134
135 fn extract_project_id(
136 project_toml_file_path: &Path,
137 ) -> Result<String> {
138 let project_path =
139 project_toml_file_path.parent().with_context(|| {
140 format!(
141 "Couldn't extract parent directory of {:?}",
142 project_toml_file_path
143 )
144 })?;
145 let project_path_name_raw =
146 project_path.file_name().with_context(|| {
147 format!(
148 "Couldn't extract directory name of {:?}",
149 project_path
150 )
151 })?;
152 Ok(project_path_name_raw
153 .to_str()
154 .with_context(|| {
155 format!(
156 "Couldn't get project directory name, \
157 possibly invalid UTF-8: {:?}",
158 project_path_name_raw
159 )
160 })?
161 .to_string())
162 }
163
164 pub fn load_project_ids(&self) -> Result<Vec<String>> {
166 let pattern = format!("{}/*/project.toml", self.path.as_str()?);
167 let mut ids = Vec::new();
168 for entry in glob(&pattern).with_context(|| {
169 format!("Failed to read glob pattern {:?}", pattern)
170 })? {
171 match entry {
172 Ok(path) => match Self::extract_project_id(&path) {
173 Ok(id) => ids.push(id),
174 Err(e) => warn!("{:?}", e),
175 },
176 Err(e) => warn!("{:?}", e),
177 }
178 }
179 Ok(ids)
180 }
181
182 pub fn has_project(&self, project_id: &str) -> bool {
184 self.project_toml_file_path(project_id).exists()
185 }
186
187 pub fn store_project_description(
189 &self,
190 project_id: &str,
191 description: &ProjectDescription,
192 ) -> Result<()> {
193 let project_dir_path = self.project_dir_path(&project_id);
194 std::fs::create_dir_all(&project_dir_path).with_context(|| {
195 format!(
196 "Couldn't create project directory {:?}",
197 project_dir_path
198 )
199 })?;
200 description.store_to_file(self.project_toml_file_path(project_id))
201 }
202
203 pub fn load_project_description(
205 &self,
206 project_id: &str,
207 ) -> Result<ProjectDescription> {
208 ProjectDescription::load_from_file(
209 &self.project_toml_file_path(project_id),
210 )
211 }
212
213 pub fn load_project_descriptions(
215 &self,
216 ) -> Result<BTreeMap<String, ProjectDescription>> {
217 let mut descriptions = BTreeMap::new();
218 for project_id in self.load_project_ids()?.into_iter() {
219 let description =
220 self.load_project_description(&project_id)?;
221 descriptions.insert(project_id, description);
222 }
223 Ok(descriptions)
224 }
225
226 pub fn project_has_datapoint_date(
228 &self,
229 project_id: &str,
230 date: NaiveDate,
231 ) -> bool {
232 self.project_datapoint_toml_file_path(project_id, date)
233 .exists()
234 }
235
236 pub fn load_project_datapoint_dates(
238 &self,
239 project_id: &str,
240 ) -> Result<Vec<NaiveDate>> {
241 let pattern = format!(
242 "{}/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/datapoint.toml",
243 self.project_dir_path(project_id).as_str()?
244 );
245
246 let mut dates = Vec::new();
247
248 for entry in glob(&pattern).with_context(|| {
249 format!("Failed to read glob pattern {:?}", pattern)
250 })? {
251 match entry {
252 Ok(path) => {
253 let full_date_path =
254 path.parent().with_context(|| {
255 format!(
256 "Couldn't get parent of file {:?}",
257 path
258 )
259 })?;
260 let date_raw =
261 full_date_path.file_name().with_context(|| {
262 format!(
263 "Couldn't get file name of {:?}",
264 full_date_path
265 )
266 })?;
267
268 let date_str = date_raw
269 .to_str()
270 .with_context(|| {
271 format!(
272 "Couldn't get file path string, \
273 possibly invalid UTF-8: {:?}",
274 path
275 )
276 })?
277 .to_string();
278 let date =
279 NaiveDate::parse_from_str(&date_str, "%Y-%m-%d")?;
280 dates.push(date);
281 }
282 Err(e) => {
283 warn!("{:?}", e);
284 }
285 }
286 }
287 Ok(dates)
288 }
289
290 pub fn store_project_datapoint(
292 &self,
293 project_id: &str,
294 date: NaiveDate,
295 datapoint: &DataPoint,
296 ) -> Result<()> {
297 let project_toml_file_path =
298 self.project_toml_file_path(project_id);
299 if !project_toml_file_path.exists() {
300 bail!(
301 "Attempting to store datapoint for project \
302 {:?}, but no project.toml file is present",
303 project_id
304 );
305 }
306 let project_datapoint_directory_path =
307 self.project_datapoint_directory_path(project_id, date);
308 std::fs::create_dir_all(&project_datapoint_directory_path)
309 .with_context(|| {
310 format!(
311 "Couldn't create datapoint directory {:?}",
312 project_datapoint_directory_path
313 )
314 })?;
315 datapoint.store_to_file(
316 self.project_datapoint_toml_file_path(project_id, date),
317 )
318 }
319
320 pub fn load_project_datapoint(
322 &self,
323 project_id: &str,
324 date: NaiveDate,
325 ) -> Result<DataPoint> {
326 DataPoint::load_from_file(
327 &self.project_datapoint_toml_file_path(project_id, date),
328 )
329 }
330
331 pub fn load_project_datapoints(
333 &self,
334 project_id: &str,
335 ) -> Result<BTreeMap<NaiveDate, DataPoint>> {
336 let mut datapoints = BTreeMap::new();
337 for date in
338 self.load_project_datapoint_dates(project_id)?.into_iter()
339 {
340 let datapoint =
341 self.load_project_datapoint(project_id, date)?;
342 datapoints.insert(date, datapoint);
343 }
344 Ok(datapoints)
345 }
346}
347
348trait LoadFromFile: Sized {
349 fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self>;
350}
351
352impl<T: serde::de::DeserializeOwned> LoadFromFile for T {
353 fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
354 let path = path.as_ref();
355 let s = std::fs::read_to_string(path).with_context(|| {
356 format!("Couldn't open file {:?}", path.to_path_buf())
357 })?;
358
359 let t: T = toml::from_str(&s).with_context(|| {
360 format!("Couldn't read file: {:?}", path.to_path_buf())
361 })?;
362 Ok(t)
363 }
364}
365
366trait StoreToFile {
367 fn store_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()>;
368}
369
370impl<T: Serialize> StoreToFile for T {
371 fn store_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
372 let path = path.as_ref();
373 let s = toml::to_string(&self).with_context(|| {
374 format!(
375 "Couldn't serialize data for file {:?}",
376 path.to_path_buf()
377 )
378 })?;
379 std::fs::write(&path, s).with_context(|| {
380 format!("Couldn't write file {:?}", path.to_path_buf())
381 })?;
382 Ok(())
383 }
384}
385
386trait PathAsStr {
387 fn as_str(&self) -> Result<&str>;
388}
389
390impl<P: AsRef<Path>> PathAsStr for P {
391 fn as_str(&self) -> Result<&str> {
392 let p = self.as_ref();
393 p.to_str().with_context(|| {
394 format!("Couldn't get path, possibly invalid UTF-8: {:?}", p)
395 })
396 }
397}
398
399pub type DataPoint = BTreeMap<String, DataEntry>;
401
402#[derive(Clone, Debug, Serialize, Deserialize)]
404pub struct DataEntry {
405 pub value: DataValue,
407
408 #[serde(
410 default = "Default::default",
411 skip_serializing_if = "Vec::is_empty"
412 )]
413 pub artifacts: Vec<PathBuf>,
414}
415
416#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
418#[serde(untagged)]
419pub enum DataValue {
420 Boolean(bool),
422 Integer(i64),
424}
425
426impl FromStr for DataValue {
427 type Err = anyhow::Error;
428
429 fn from_str(s: &str) -> Result<Self, Self::Err> {
430 if let Ok(v) = s.parse::<bool>() {
431 Ok(DataValue::Boolean(v))
432 } else if let Ok(v) = s.parse::<i64>() {
433 Ok(DataValue::Integer(v))
434 } else {
435 bail!("Couldn't parse argument {:?}", s);
436 }
437 }
438}
439
440impl DataValue {
441 pub fn unwrap_boolean(&self) -> bool {
444 self.boolean().unwrap()
445 }
446
447 pub fn unwrap_integer(&self) -> i64 {
450 self.integer().unwrap()
451 }
452
453 pub fn boolean(&self) -> Option<bool> {
456 if let DataValue::Boolean(v) = *self {
457 Some(v)
458 } else {
459 None
460 }
461 }
462
463 pub fn integer(&self) -> Option<i64> {
466 if let DataValue::Integer(v) = *self {
467 Some(v)
468 } else {
469 None
470 }
471 }
472
473 pub fn datatype(&self) -> DataType {
475 match self {
476 DataValue::Integer(_) => DataType::Integer,
477 DataValue::Boolean(_) => DataType::Boolean,
478 }
479 }
480}
481
482#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
484#[serde(rename_all = "lowercase")]
485pub enum DataType {
486 Boolean,
488
489 Integer,
491}
492
493impl FromStr for DataType {
494 type Err = anyhow::Error;
495
496 fn from_str(s: &str) -> Result<Self, Self::Err> {
497 match s {
498 "boolean" => Ok(DataType::Boolean),
499 "integer" => Ok(DataType::Integer),
500 s => bail!("Couldn't parse argument {:?}", s),
501 }
502 }
503}
504
505#[derive(Clone, Debug, Serialize, Deserialize)]
507pub struct CharacteristicDescription {
508 pub datatype: DataType,
510
511 pub name: String,
513}
514
515#[derive(Clone, Debug, Serialize, Deserialize)]
517pub struct RootDescription {
518 pub name: String,
520
521 pub characteristics: IndexMap<String, CharacteristicDescription>,
523}
524
525#[derive(Clone, Debug, Serialize, Deserialize)]
527pub struct ProjectDescription {
528 pub name: String,
530
531 #[serde(
533 default = "Default::default",
534 skip_serializing_if = "String::is_empty"
535 )]
536 pub description: String,
537
538 #[serde(
540 default = "Default::default",
541 skip_serializing_if = "String::is_empty"
542 )]
543 pub website: String,
544
545 #[serde(
547 default = "Default::default",
548 skip_serializing_if = "String::is_empty"
549 )]
550 pub vcs: String,
551}