Skip to main content

kagami/
config_file.rs

1use clap::ValueEnum;
2use log::{debug, trace};
3use semver::Version;
4use serde::{Deserialize, Serialize};
5use std::error::Error;
6use std::fmt::{Display, Formatter};
7
8#[derive(Clone, Deserialize, Debug)]
9#[serde(default)]
10pub struct Config {
11    pub targets: Vec<Target>,
12    pub output_format: Option<OutputFormat>,
13    pub log_level: Option<LogLevel>,
14}
15
16impl Config {
17    pub fn validate(&self) -> Result<(), Box<dyn Error>> {
18        debug!("Validating {} targets.", self.targets.len());
19
20        for target in &self.targets {
21            trace!("Validating target {:?}.", target);
22
23            let validate_result = target.validate();
24            validate_result?;
25        }
26
27        Ok(())
28    }
29}
30
31impl Default for Config {
32    fn default() -> Self {
33        Self {
34            targets: Vec::new(),
35            output_format: Some(OutputFormat::Text),
36            log_level: Some(LogLevel::Warn),
37        }
38    }
39}
40
41#[derive(Clone, Deserialize, Debug)]
42pub struct Target {
43    pub name: Option<String>,
44    pub version: String,
45    pub ecosystem: Option<String>,
46    pub advisory_type: AdvisoryType,
47    pub repository_path: Option<String>,
48}
49
50impl Target {
51    pub fn extract_repository_path(&self) -> Result<Option<[&str; 2]>, Box<dyn Error>> {
52        let path = match self
53            .repository_path
54            .as_ref()
55            .map(|s| s.split("/").map(|v| v.trim()).collect::<Vec<_>>())
56        {
57            Some(s) => s,
58            None => return Ok(None),
59        };
60
61        if path.len() != 2 {
62            return Err(Box::new(TargetParseError::RepositoryPathInvalid));
63        }
64
65        if path[0].is_empty() || path[1].is_empty() {
66            return Err(Box::new(TargetParseError::RepositoryPathInvalid));
67        }
68
69        Ok(Some([path[0], path[1]]))
70    }
71
72    pub fn validate(&self) -> Result<(), Box<dyn Error>> {
73        if self.advisory_type == AdvisoryType::GithubRepository && self.repository_path.is_none() {
74            return Err(Box::new(TargetParseError::RepositoryPathNotSpecified));
75        }
76        if self.advisory_type == AdvisoryType::Github && self.name.is_none() {
77            return Err(Box::new(TargetParseError::TargetNameNotSpecified));
78        }
79        if Version::parse(&self.version).is_err() {
80            return Err(Box::new(TargetParseError::VersionNotParsable));
81        }
82        if self.repository_path.is_some() {
83            match self.extract_repository_path() {
84                Ok(_) => {}
85                Err(e) => return Err(e),
86            };
87        }
88
89        Ok(())
90    }
91}
92
93#[derive(Debug)]
94pub enum TargetParseError {
95    RepositoryPathNotSpecified,
96    TargetNameNotSpecified,
97    VersionNotParsable,
98    RepositoryPathInvalid,
99}
100
101impl Display for TargetParseError {
102    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
103        let message = match self {
104            TargetParseError::RepositoryPathNotSpecified => {
105                "Please specify `repository_path` when the `advisory_type` is `github_repository`."
106            }
107            TargetParseError::TargetNameNotSpecified => {
108                "Please specify `name` when the `advisory_type` is `github`."
109            }
110            TargetParseError::VersionNotParsable => "`version` is not valid semver.",
111            TargetParseError::RepositoryPathInvalid => "`repository_path` is invalid.",
112        };
113        write!(f, "{}", message)
114    }
115}
116
117impl Error for TargetParseError {}
118
119#[derive(PartialEq, Copy, Clone, strum_macros::Display, Deserialize, Serialize, Debug)]
120#[serde(rename_all = "snake_case")]
121pub enum AdvisoryType {
122    Github,
123    GithubRepository,
124}
125
126#[derive(PartialEq, Copy, Clone, Deserialize, ValueEnum, Debug)]
127#[serde(rename_all = "snake_case")]
128pub enum OutputFormat {
129    Json,
130    Yaml,
131    Text,
132    SimpleMarkdownText,
133}
134
135#[derive(PartialEq, Copy, Clone, Deserialize, ValueEnum, Debug)]
136#[serde(rename_all = "snake_case")]
137pub enum LogLevel {
138    Error,
139    Warn,
140    Info,
141    Debug,
142    Trace,
143}