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}