1use crate::{
7 apis::ManagedApis,
8 git::GitRevision,
9 output::{
10 Styles,
11 headers::{GENERATING, HEADER_WIDTH},
12 },
13 spec_files_blessed::BlessedFiles,
14 spec_files_generated::GeneratedFiles,
15 spec_files_local::{LocalFiles, walk_local_directory},
16};
17use anyhow::Context;
18use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
19use owo_colors::OwoColorize;
20
21#[derive(Clone, Debug)]
22pub struct Environment {
23 pub(crate) command: String,
25
26 pub(crate) repo_root: Utf8PathBuf,
28
29 pub(crate) default_openapi_dir: Utf8PathBuf,
31
32 pub(crate) default_git_branch: String,
34}
35
36impl Environment {
37 pub fn new(
48 command: String,
49 repo_root: Utf8PathBuf,
50 default_openapi_dir: Utf8PathBuf,
51 ) -> anyhow::Result<Self> {
52 if !repo_root.is_absolute() {
53 return Err(anyhow::anyhow!(
54 "repo_root must be an absolute path, found: {}",
55 repo_root
56 ));
57 }
58
59 if !is_normal_relative(&default_openapi_dir) {
60 return Err(anyhow::anyhow!(
61 "default_openapi_dir must be a relative path with \
62 normal components, found: {}",
63 default_openapi_dir
64 ));
65 }
66
67 Ok(Self {
68 repo_root,
69 default_openapi_dir,
70 default_git_branch: "origin/main".to_owned(),
71 command,
72 })
73 }
74
75 pub fn with_default_git_branch(mut self, branch: String) -> Self {
85 self.default_git_branch = branch;
86 self
87 }
88
89 pub(crate) fn resolve(
90 &self,
91 openapi_dir: Option<Utf8PathBuf>,
92 ) -> anyhow::Result<ResolvedEnv> {
93 let (abs_dir, rel_dir) = match &openapi_dir {
103 Some(provided_dir) => {
104 let abs_dir = camino::absolute_utf8(provided_dir)
106 .with_context(|| {
107 format!(
108 "error making provided OpenAPI directory \
109 absolute: {}",
110 provided_dir
111 )
112 })?;
113
114 let rel_dir = abs_dir
116 .strip_prefix(&self.repo_root)
117 .with_context(|| {
118 format!(
119 "provided OpenAPI directory {} is not a \
120 subdirectory of repository root {}",
121 abs_dir, self.repo_root
122 )
123 })?
124 .to_path_buf();
125
126 (abs_dir, rel_dir)
127 }
128 None => {
129 let rel_dir = self.default_openapi_dir.clone();
130 let abs_dir = self.repo_root.join(&rel_dir);
131 (abs_dir, rel_dir)
132 }
133 };
134
135 Ok(ResolvedEnv {
136 command: self.command.clone(),
137 repo_root: self.repo_root.clone(),
138 local_source: LocalSource::Directory { abs_dir, rel_dir },
139 default_git_branch: self.default_git_branch.clone(),
140 })
141 }
142}
143
144fn is_normal_relative(default_openapi_dir: &Utf8Path) -> bool {
145 default_openapi_dir
146 .components()
147 .all(|c| matches!(c, Utf8Component::Normal(_) | Utf8Component::CurDir))
148}
149
150#[derive(Debug)]
152pub(crate) struct ResolvedEnv {
153 pub(crate) command: String,
154 pub(crate) repo_root: Utf8PathBuf,
155 pub(crate) local_source: LocalSource,
156 pub(crate) default_git_branch: String,
157}
158
159impl ResolvedEnv {
160 pub(crate) fn openapi_abs_dir(&self) -> &Utf8Path {
161 match &self.local_source {
162 LocalSource::Directory { abs_dir, .. } => abs_dir,
163 }
164 }
165
166 pub(crate) fn openapi_rel_dir(&self) -> &Utf8Path {
167 match &self.local_source {
168 LocalSource::Directory { rel_dir, .. } => rel_dir,
169 }
170 }
171}
172
173#[derive(Debug)]
176pub enum BlessedSource {
177 GitRevisionMergeBase { revision: GitRevision, directory: Utf8PathBuf },
180
181 Directory { local_directory: Utf8PathBuf },
185}
186
187impl BlessedSource {
188 pub fn load(
190 &self,
191 apis: &ManagedApis,
192 styles: &Styles,
193 ) -> anyhow::Result<(BlessedFiles, ErrorAccumulator)> {
194 let mut errors = ErrorAccumulator::new();
195 match self {
196 BlessedSource::Directory { local_directory } => {
197 eprintln!(
198 "{:>HEADER_WIDTH$} blessed OpenAPI documents from {:?}",
199 "Loading".style(styles.success_header),
200 local_directory,
201 );
202 let api_files =
203 walk_local_directory(local_directory, apis, &mut errors)?;
204 Ok((BlessedFiles::from(api_files), errors))
205 }
206 BlessedSource::GitRevisionMergeBase { revision, directory } => {
207 eprintln!(
208 "{:>HEADER_WIDTH$} blessed OpenAPI documents from git \
209 revision {:?} path {:?}",
210 "Loading".style(styles.success_header),
211 revision,
212 directory
213 );
214 Ok((
215 BlessedFiles::load_from_git_parent_branch(
216 revision,
217 directory,
218 apis,
219 &mut errors,
220 )?,
221 errors,
222 ))
223 }
224 }
225 }
226}
227
228#[derive(Debug)]
230pub enum GeneratedSource {
231 Generated,
233
234 Directory { local_directory: Utf8PathBuf },
238}
239
240impl GeneratedSource {
241 pub fn load(
243 &self,
244 apis: &ManagedApis,
245 styles: &Styles,
246 ) -> anyhow::Result<(GeneratedFiles, ErrorAccumulator)> {
247 let mut errors = ErrorAccumulator::new();
248 match self {
249 GeneratedSource::Generated => {
250 eprintln!(
251 "{:>HEADER_WIDTH$} OpenAPI documents from API \
252 definitions ... ",
253 GENERATING.style(styles.success_header)
254 );
255 Ok((GeneratedFiles::generate(apis, &mut errors)?, errors))
256 }
257 GeneratedSource::Directory { local_directory } => {
258 eprintln!(
259 "{:>HEADER_WIDTH$} \"generated\" OpenAPI documents from \
260 {:?} ... ",
261 "Loading".style(styles.success_header),
262 local_directory,
263 );
264 let api_files =
265 walk_local_directory(local_directory, apis, &mut errors)?;
266 Ok((GeneratedFiles::from(api_files), errors))
267 }
268 }
269 }
270}
271
272#[derive(Debug)]
274pub enum LocalSource {
275 Directory {
277 abs_dir: Utf8PathBuf,
279 rel_dir: Utf8PathBuf,
282 },
283}
284
285impl LocalSource {
286 pub fn load(
288 &self,
289 apis: &ManagedApis,
290 styles: &Styles,
291 ) -> anyhow::Result<(LocalFiles, ErrorAccumulator)> {
292 let mut errors = ErrorAccumulator::new();
293 match self {
294 LocalSource::Directory { abs_dir, .. } => {
295 eprintln!(
296 "{:>HEADER_WIDTH$} local OpenAPI documents from \
297 {:?} ... ",
298 "Loading".style(styles.success_header),
299 abs_dir,
300 );
301 Ok((
302 LocalFiles::load_from_directory(
303 abs_dir,
304 apis,
305 &mut errors,
306 )?,
307 errors,
308 ))
309 }
310 }
311 }
312}
313
314pub struct ErrorAccumulator {
316 errors: Vec<anyhow::Error>,
318 warnings: Vec<anyhow::Error>,
320}
321
322impl ErrorAccumulator {
323 pub fn new() -> ErrorAccumulator {
324 ErrorAccumulator { errors: Vec::new(), warnings: Vec::new() }
325 }
326
327 pub fn error(&mut self, error: anyhow::Error) {
329 self.errors.push(error);
330 }
331
332 pub fn warning(&mut self, error: anyhow::Error) {
334 self.warnings.push(error);
335 }
336
337 pub fn iter_errors(&self) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
338 self.errors.iter()
339 }
340
341 pub fn iter_warnings(
342 &self,
343 ) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
344 self.warnings.iter()
345 }
346}