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 apis: &ManagedApis,
206 styles: &Styles,
207 ) -> anyhow::Result<(BlessedFiles, ErrorAccumulator)> {
208 let mut errors = ErrorAccumulator::new();
209 match self {
210 BlessedSource::Directory { local_directory } => {
211 eprintln!(
212 "{:>HEADER_WIDTH$} blessed OpenAPI documents from {:?}",
213 "Loading".style(styles.success_header),
214 local_directory,
215 );
216 let api_files: ApiSpecFilesBuilder<'_, BlessedApiSpecFile> =
217 walk_local_directory(local_directory, apis, &mut errors)?;
218 Ok((BlessedFiles::from(api_files), errors))
219 }
220 BlessedSource::GitRevisionMergeBase { revision, directory } => {
221 eprintln!(
222 "{:>HEADER_WIDTH$} blessed OpenAPI documents from git \
223 revision {:?} path {:?}",
224 "Loading".style(styles.success_header),
225 revision,
226 directory
227 );
228 Ok((
229 BlessedFiles::load_from_git_parent_branch(
230 revision,
231 directory,
232 apis,
233 &mut errors,
234 )?,
235 errors,
236 ))
237 }
238 }
239 }
240}
241
242#[derive(Debug)]
244pub enum GeneratedSource {
245 Generated,
247
248 Directory { local_directory: Utf8PathBuf },
252}
253
254impl GeneratedSource {
255 pub fn load(
257 &self,
258 apis: &ManagedApis,
259 styles: &Styles,
260 ) -> anyhow::Result<(GeneratedFiles, ErrorAccumulator)> {
261 let mut errors = ErrorAccumulator::new();
262 match self {
263 GeneratedSource::Generated => {
264 eprintln!(
265 "{:>HEADER_WIDTH$} OpenAPI documents from API \
266 definitions ... ",
267 GENERATING.style(styles.success_header)
268 );
269 Ok((GeneratedFiles::generate(apis, &mut errors)?, errors))
270 }
271 GeneratedSource::Directory { local_directory } => {
272 eprintln!(
273 "{:>HEADER_WIDTH$} \"generated\" OpenAPI documents from \
274 {:?} ... ",
275 "Loading".style(styles.success_header),
276 local_directory,
277 );
278 let api_files =
279 walk_local_directory(local_directory, apis, &mut errors)?;
280 Ok((GeneratedFiles::from(api_files), errors))
281 }
282 }
283 }
284}
285
286#[derive(Debug)]
288pub enum LocalSource {
289 Directory {
291 abs_dir: Utf8PathBuf,
293 rel_dir: Utf8PathBuf,
296 },
297}
298
299impl LocalSource {
300 pub fn load(
302 &self,
303 apis: &ManagedApis,
304 styles: &Styles,
305 ) -> anyhow::Result<(LocalFiles, ErrorAccumulator)> {
306 let mut errors = ErrorAccumulator::new();
307 match self {
308 LocalSource::Directory { abs_dir, .. } => {
309 eprintln!(
310 "{:>HEADER_WIDTH$} local OpenAPI documents from \
311 {:?} ... ",
312 "Loading".style(styles.success_header),
313 abs_dir,
314 );
315 Ok((
316 LocalFiles::load_from_directory(
317 abs_dir,
318 apis,
319 &mut errors,
320 )?,
321 errors,
322 ))
323 }
324 }
325 }
326}
327
328pub struct ErrorAccumulator {
330 errors: Vec<anyhow::Error>,
332 warnings: Vec<anyhow::Error>,
334}
335
336impl ErrorAccumulator {
337 pub fn new() -> ErrorAccumulator {
338 ErrorAccumulator { errors: Vec::new(), warnings: Vec::new() }
339 }
340
341 pub fn error(&mut self, error: anyhow::Error) {
343 self.errors.push(error);
344 }
345
346 pub fn warning(&mut self, error: anyhow::Error) {
348 self.warnings.push(error);
349 }
350
351 pub fn iter_errors(&self) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
352 self.errors.iter()
353 }
354
355 pub fn iter_warnings(
356 &self,
357 ) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
358 self.warnings.iter()
359 }
360}