1use crate::{
7 apis::ManagedApis,
8 git::GitRevision,
9 output::{
10 Styles,
11 headers::{GENERATING, HEADER_WIDTH},
12 },
13 spec_files_blessed::{BlessedApiSpecFile, BlessedFiles},
14 spec_files_generated::GeneratedFiles,
15 spec_files_generic::ApiSpecFilesBuilder,
16 spec_files_local::{LocalFiles, walk_local_directory},
17};
18use anyhow::Context;
19use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
20use owo_colors::OwoColorize;
21
22#[derive(Clone, Debug)]
29pub struct Environment {
30 pub(crate) command: String,
32
33 pub(crate) repo_root: Utf8PathBuf,
35
36 pub(crate) default_openapi_dir: Utf8PathBuf,
38
39 pub(crate) default_git_branch: String,
41}
42
43impl Environment {
44 pub fn new(
55 command: impl Into<String>,
56 repo_root: impl Into<Utf8PathBuf>,
57 default_openapi_dir: impl Into<Utf8PathBuf>,
58 ) -> anyhow::Result<Self> {
59 let command = command.into();
60 let repo_root = repo_root.into();
61 let default_openapi_dir = default_openapi_dir.into();
62
63 if !repo_root.is_absolute() {
64 return Err(anyhow::anyhow!(
65 "repo_root must be an absolute path, found: {}",
66 repo_root
67 ));
68 }
69
70 if !is_normal_relative(&default_openapi_dir) {
71 return Err(anyhow::anyhow!(
72 "default_openapi_dir must be a relative path with \
73 normal components, found: {}",
74 default_openapi_dir
75 ));
76 }
77
78 Ok(Self {
79 repo_root,
80 default_openapi_dir,
81 default_git_branch: "origin/main".to_owned(),
82 command,
83 })
84 }
85
86 pub fn with_default_git_branch(
96 mut self,
97 branch: impl Into<String>,
98 ) -> Self {
99 self.default_git_branch = branch.into();
100 self
101 }
102
103 pub(crate) fn resolve(
104 &self,
105 openapi_dir: Option<Utf8PathBuf>,
106 ) -> anyhow::Result<ResolvedEnv> {
107 let (abs_dir, rel_dir) = match &openapi_dir {
117 Some(provided_dir) => {
118 let abs_dir = camino::absolute_utf8(provided_dir)
120 .with_context(|| {
121 format!(
122 "error making provided OpenAPI directory \
123 absolute: {}",
124 provided_dir
125 )
126 })?;
127
128 let rel_dir = abs_dir
130 .strip_prefix(&self.repo_root)
131 .with_context(|| {
132 format!(
133 "provided OpenAPI directory {} is not a \
134 subdirectory of repository root {}",
135 abs_dir, self.repo_root
136 )
137 })?
138 .to_path_buf();
139
140 (abs_dir, rel_dir)
141 }
142 None => {
143 let rel_dir = self.default_openapi_dir.clone();
144 let abs_dir = self.repo_root.join(&rel_dir);
145 (abs_dir, rel_dir)
146 }
147 };
148
149 Ok(ResolvedEnv {
150 command: self.command.clone(),
151 repo_root: self.repo_root.clone(),
152 local_source: LocalSource::Directory { abs_dir, rel_dir },
153 default_git_branch: self.default_git_branch.clone(),
154 })
155 }
156}
157
158fn is_normal_relative(default_openapi_dir: &Utf8Path) -> bool {
159 default_openapi_dir
160 .components()
161 .all(|c| matches!(c, Utf8Component::Normal(_) | Utf8Component::CurDir))
162}
163
164#[derive(Debug)]
166pub(crate) struct ResolvedEnv {
167 pub(crate) command: String,
168 pub(crate) repo_root: Utf8PathBuf,
169 pub(crate) local_source: LocalSource,
170 pub(crate) default_git_branch: String,
171}
172
173impl ResolvedEnv {
174 pub(crate) fn openapi_abs_dir(&self) -> &Utf8Path {
175 match &self.local_source {
176 LocalSource::Directory { abs_dir, .. } => abs_dir,
177 }
178 }
179
180 pub(crate) fn openapi_rel_dir(&self) -> &Utf8Path {
181 match &self.local_source {
182 LocalSource::Directory { rel_dir, .. } => rel_dir,
183 }
184 }
185}
186
187#[derive(Debug)]
190pub enum BlessedSource {
191 GitRevisionMergeBase { revision: GitRevision, directory: Utf8PathBuf },
194
195 Directory { local_directory: Utf8PathBuf },
199}
200
201impl BlessedSource {
202 pub fn load(
204 &self,
205 repo_root: &Utf8Path,
206 apis: &ManagedApis,
207 styles: &Styles,
208 ) -> anyhow::Result<(BlessedFiles, ErrorAccumulator)> {
209 let mut errors = ErrorAccumulator::new();
210 match self {
211 BlessedSource::Directory { local_directory } => {
212 eprintln!(
213 "{:>HEADER_WIDTH$} blessed OpenAPI documents from {:?}",
214 "Loading".style(styles.success_header),
215 local_directory,
216 );
217 let api_files: ApiSpecFilesBuilder<'_, BlessedApiSpecFile> =
218 walk_local_directory(local_directory, apis, &mut errors)?;
219 Ok((BlessedFiles::from(api_files), errors))
220 }
221 BlessedSource::GitRevisionMergeBase { revision, directory } => {
222 eprintln!(
223 "{:>HEADER_WIDTH$} blessed OpenAPI documents from git \
224 revision {:?} path {:?}",
225 "Loading".style(styles.success_header),
226 revision,
227 directory
228 );
229 Ok((
230 BlessedFiles::load_from_git_parent_branch(
231 repo_root,
232 revision,
233 directory,
234 apis,
235 &mut errors,
236 )?,
237 errors,
238 ))
239 }
240 }
241 }
242}
243
244#[derive(Debug)]
246pub enum GeneratedSource {
247 Generated,
249
250 Directory { local_directory: Utf8PathBuf },
254}
255
256impl GeneratedSource {
257 pub fn load(
259 &self,
260 apis: &ManagedApis,
261 styles: &Styles,
262 ) -> anyhow::Result<(GeneratedFiles, ErrorAccumulator)> {
263 let mut errors = ErrorAccumulator::new();
264 match self {
265 GeneratedSource::Generated => {
266 eprintln!(
267 "{:>HEADER_WIDTH$} OpenAPI documents from API \
268 definitions ... ",
269 GENERATING.style(styles.success_header)
270 );
271 Ok((GeneratedFiles::generate(apis, &mut errors)?, errors))
272 }
273 GeneratedSource::Directory { local_directory } => {
274 eprintln!(
275 "{:>HEADER_WIDTH$} \"generated\" OpenAPI documents from \
276 {:?} ... ",
277 "Loading".style(styles.success_header),
278 local_directory,
279 );
280 let api_files =
281 walk_local_directory(local_directory, apis, &mut errors)?;
282 Ok((GeneratedFiles::from(api_files), errors))
283 }
284 }
285 }
286}
287
288#[derive(Debug)]
290pub enum LocalSource {
291 Directory {
293 abs_dir: Utf8PathBuf,
295 rel_dir: Utf8PathBuf,
298 },
299}
300
301impl LocalSource {
302 pub fn load(
304 &self,
305 apis: &ManagedApis,
306 styles: &Styles,
307 ) -> anyhow::Result<(LocalFiles, ErrorAccumulator)> {
308 let mut errors = ErrorAccumulator::new();
309 match self {
310 LocalSource::Directory { abs_dir, .. } => {
311 eprintln!(
312 "{:>HEADER_WIDTH$} local OpenAPI documents from \
313 {:?} ... ",
314 "Loading".style(styles.success_header),
315 abs_dir,
316 );
317 Ok((
318 LocalFiles::load_from_directory(
319 abs_dir,
320 apis,
321 &mut errors,
322 )?,
323 errors,
324 ))
325 }
326 }
327 }
328}
329
330pub struct ErrorAccumulator {
332 errors: Vec<anyhow::Error>,
334 warnings: Vec<anyhow::Error>,
336}
337
338impl ErrorAccumulator {
339 pub fn new() -> ErrorAccumulator {
340 ErrorAccumulator { errors: Vec::new(), warnings: Vec::new() }
341 }
342
343 pub fn error(&mut self, error: anyhow::Error) {
345 self.errors.push(error);
346 }
347
348 pub fn warning(&mut self, error: anyhow::Error) {
350 self.warnings.push(error);
351 }
352
353 pub fn iter_errors(&self) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
354 self.errors.iter()
355 }
356
357 pub fn iter_warnings(
358 &self,
359 ) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
360 self.warnings.iter()
361 }
362}