1use crate::{CapabilityId, Config, ConfigError, IdError};
3use std::collections::BTreeMap;
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
8#[non_exhaustive]
9pub enum Reason {
10 Missing,
12 Wrong,
14 Untested,
16}
17
18impl Reason {
19 pub fn as_str(self) -> &'static str {
21 match self {
22 Reason::Missing => "missing",
23 Reason::Wrong => "wrong",
24 Reason::Untested => "untested",
25 }
26 }
27}
28
29impl fmt::Display for Reason {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 f.write_str(self.as_str())
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37#[non_exhaustive]
38pub enum ParseError {
39 NoHeader,
41 MissingColumn(&'static str),
43 FieldCount {
45 line: usize,
46 want: usize,
47 got: usize,
48 },
49 BadId { line: usize, error: IdError },
51 BadConfig {
53 line: usize,
54 column: &'static str,
55 error: ConfigError,
56 },
57 BadEnabled { line: usize, value: String },
59 BadReason { line: usize, value: String },
61 ReasonMismatch { line: usize },
63}
64
65impl fmt::Display for ParseError {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 ParseError::NoHeader => write!(f, "matrix has no header line"),
69 ParseError::MissingColumn(c) => write!(f, "matrix header lacks column {c:?}"),
70 ParseError::FieldCount { line, want, got } => {
71 write!(f, "line {line}: {got} fields, header has {want}")
72 }
73 ParseError::BadId { line, error } => write!(f, "line {line}: subject: {error}"),
74 ParseError::BadConfig {
75 line,
76 column,
77 error,
78 } => write!(f, "line {line}: {column}: {error}"),
79 ParseError::BadEnabled { line, value } => {
80 write!(f, "line {line}: enabled is {value:?}, want yes or no")
81 }
82 ParseError::BadReason { line, value } => {
83 write!(f, "line {line}: reason {value:?} is unknown")
84 }
85 ParseError::ReasonMismatch { line } => {
86 write!(
87 f,
88 "line {line}: a no needs a reason and a yes must have none"
89 )
90 }
91 }
92 }
93}
94
95impl std::error::Error for ParseError {}
96
97pub(crate) const COLUMNS: [&str; 9] = [
99 "subject_kind",
100 "subject",
101 "backend",
102 "version",
103 "config",
104 "enabled",
105 "reason",
106 "metadata",
107 "note",
108];
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct Row {
113 pub subject_kind: String,
115 pub subject: CapabilityId,
117 pub backend: String,
119 pub version: String,
121 pub config: Config,
123 pub enabled: bool,
125 pub reason: Option<Reason>,
127 pub metadata: Config,
129 pub note: String,
131 pub extra: BTreeMap<String, String>,
133}
134
135impl Row {
136 pub(crate) fn parse_fields(
137 header: &[&str],
138 fields: &[&str],
139 line: usize,
140 ) -> Result<Row, ParseError> {
141 let idx = |name: &'static str| -> Result<usize, ParseError> {
142 header
143 .iter()
144 .position(|h| *h == name)
145 .ok_or(ParseError::MissingColumn(name))
146 };
147 let get = |name: &'static str| -> Result<&str, ParseError> { Ok(fields[idx(name)?]) };
148 let cfg = |name: &'static str| -> Result<Config, ParseError> {
149 Config::parse(get(name)?).map_err(|error| ParseError::BadConfig {
150 line,
151 column: name,
152 error,
153 })
154 };
155 let enabled = match get("enabled")? {
156 "yes" => true,
157 "no" => false,
158 other => {
159 return Err(ParseError::BadEnabled {
160 line,
161 value: other.to_string(),
162 });
163 }
164 };
165 let reason = match get("reason")? {
166 "" => None,
167 "missing" => Some(Reason::Missing),
168 "wrong" => Some(Reason::Wrong),
169 "untested" => Some(Reason::Untested),
170 other => {
171 return Err(ParseError::BadReason {
172 line,
173 value: other.to_string(),
174 });
175 }
176 };
177 if enabled == reason.is_some() {
178 return Err(ParseError::ReasonMismatch { line });
179 }
180 let extra = header
181 .iter()
182 .zip(fields)
183 .filter(|(h, _)| !COLUMNS.contains(h))
184 .map(|(h, v)| (h.to_string(), v.to_string()))
185 .collect();
186 Ok(Row {
187 subject_kind: get("subject_kind")?.to_string(),
188 subject: CapabilityId::new(get("subject")?)
189 .map_err(|error| ParseError::BadId { line, error })?,
190 backend: get("backend")?.to_string(),
191 version: get("version")?.to_string(),
192 config: cfg("config")?,
193 enabled,
194 reason,
195 metadata: cfg("metadata")?,
196 note: get("note")?.to_string(),
197 extra,
198 })
199 }
200
201 pub(crate) fn field(&self, column: &str) -> String {
202 match column {
203 "subject_kind" => self.subject_kind.clone(),
204 "subject" => self.subject.to_string(),
205 "backend" => self.backend.clone(),
206 "version" => self.version.clone(),
207 "config" => self.config.to_string(),
208 "enabled" => if self.enabled { "yes" } else { "no" }.to_string(),
209 "reason" => self
210 .reason
211 .map(|r| r.as_str().to_string())
212 .unwrap_or_default(),
213 "metadata" => self.metadata.to_string(),
214 "note" => self.note.clone(),
215 other => self.extra.get(other).cloned().unwrap_or_default(),
216 }
217 }
218}