Skip to main content

kagami/
config_file.rs

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