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}