1use crate::{
7 apis::ManagedApis,
8 git::{GitRevision, is_shallow_clone},
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(
219 local_directory,
220 apis,
221 &mut errors,
222 repo_root,
223 )?;
224 Ok((BlessedFiles::from(api_files), errors))
225 }
226 BlessedSource::GitRevisionMergeBase { revision, directory } => {
227 eprintln!(
228 "{:>HEADER_WIDTH$} blessed OpenAPI documents from git \
229 revision {:?} path {:?}",
230 "Loading".style(styles.success_header),
231 revision,
232 directory
233 );
234 Ok((
235 BlessedFiles::load_from_git_parent_branch(
236 repo_root,
237 revision,
238 directory,
239 apis,
240 &mut errors,
241 )?,
242 errors,
243 ))
244 }
245 }
246 }
247}
248
249#[derive(Debug)]
251pub enum GeneratedSource {
252 Generated,
254
255 Directory { local_directory: Utf8PathBuf },
259}
260
261impl GeneratedSource {
262 pub fn load(
264 &self,
265 apis: &ManagedApis,
266 styles: &Styles,
267 repo_root: &Utf8Path,
268 ) -> anyhow::Result<(GeneratedFiles, ErrorAccumulator)> {
269 let mut errors = ErrorAccumulator::new();
270 match self {
271 GeneratedSource::Generated => {
272 eprintln!(
273 "{:>HEADER_WIDTH$} OpenAPI documents from API \
274 definitions ... ",
275 GENERATING.style(styles.success_header)
276 );
277 Ok((GeneratedFiles::generate(apis, &mut errors)?, errors))
278 }
279 GeneratedSource::Directory { local_directory } => {
280 eprintln!(
281 "{:>HEADER_WIDTH$} \"generated\" OpenAPI documents from \
282 {:?} ... ",
283 "Loading".style(styles.success_header),
284 local_directory,
285 );
286 let api_files = walk_local_directory(
287 local_directory,
288 apis,
289 &mut errors,
290 repo_root,
291 )?;
292 Ok((GeneratedFiles::from(api_files), errors))
293 }
294 }
295 }
296}
297
298#[derive(Debug)]
300pub enum LocalSource {
301 Directory {
303 abs_dir: Utf8PathBuf,
305 rel_dir: Utf8PathBuf,
308 },
309}
310
311impl LocalSource {
312 pub fn load(
316 &self,
317 apis: &ManagedApis,
318 styles: &Styles,
319 repo_root: &Utf8Path,
320 ) -> anyhow::Result<(LocalFiles, ErrorAccumulator)> {
321 let mut errors = ErrorAccumulator::new();
322
323 let any_uses_git_stub =
325 apis.iter_apis().any(|a| apis.uses_git_stub_storage(a));
326 if any_uses_git_stub && is_shallow_clone(repo_root) {
327 errors.error(anyhow::anyhow!(
328 "this repository is a shallow clone, but Git stub storage is \
329 enabled for some APIs. Git stubs cannot be resolved in a \
330 shallow clone because the referenced commits may not be \
331 available. To fix this, run `git fetch --unshallow` to \
332 fetch complete history, or make a fresh clone without --depth."
333 ));
334 return Ok((LocalFiles::default(), errors));
335 }
336
337 match self {
338 LocalSource::Directory { abs_dir, .. } => {
339 eprintln!(
340 "{:>HEADER_WIDTH$} local OpenAPI documents from \
341 {:?} ... ",
342 "Loading".style(styles.success_header),
343 abs_dir,
344 );
345 Ok((
346 LocalFiles::load_from_directory(
347 abs_dir,
348 apis,
349 &mut errors,
350 repo_root,
351 )?,
352 errors,
353 ))
354 }
355 }
356 }
357}
358
359pub struct ErrorAccumulator {
361 errors: Vec<anyhow::Error>,
363 warnings: Vec<anyhow::Error>,
365}
366
367impl ErrorAccumulator {
368 pub fn new() -> ErrorAccumulator {
369 ErrorAccumulator { errors: Vec::new(), warnings: Vec::new() }
370 }
371
372 pub fn error(&mut self, error: anyhow::Error) {
374 self.errors.push(error);
375 }
376
377 pub fn warning(&mut self, error: anyhow::Error) {
379 self.warnings.push(error);
380 }
381
382 pub fn iter_errors(&self) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
383 self.errors.iter()
384 }
385
386 pub fn iter_warnings(
387 &self,
388 ) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
389 self.warnings.iter()
390 }
391}