1use crate::{
7 apis::ManagedApis,
8 output::{
9 Styles,
10 headers::{GENERATING, HEADER_WIDTH},
11 },
12 spec_files_blessed::{BlessedApiSpecFile, BlessedFiles},
13 spec_files_generated::GeneratedFiles,
14 spec_files_generic::ApiSpecFilesBuilder,
15 spec_files_local::{LocalFiles, walk_local_directory},
16 vcs::{RepoVcs, RepoVcsKind, VcsRevision},
17};
18use anyhow::Context;
19use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
20use owo_colors::OwoColorize;
21
22const DEFAULT_GIT_BRANCH: &str = "origin/main";
24
25const DEFAULT_JJ_REVSET: &str = "trunk()";
27
28#[derive(Clone, Debug)]
35pub struct Environment {
36 pub(crate) command: String,
38
39 pub(crate) repo_root: Utf8PathBuf,
41
42 pub(crate) default_openapi_dir: Utf8PathBuf,
44
45 pub(crate) default_git_branch: String,
48
49 pub(crate) default_jj_revset: String,
52
53 pub(crate) vcs: RepoVcs,
55}
56
57impl Environment {
58 pub fn new(
74 command: impl Into<String>,
75 repo_root: impl Into<Utf8PathBuf>,
76 default_openapi_dir: impl Into<Utf8PathBuf>,
77 ) -> anyhow::Result<Self> {
78 let command = command.into();
79 let repo_root = repo_root.into();
80 let default_openapi_dir = default_openapi_dir.into();
81
82 validate_paths(&repo_root, &default_openapi_dir)?;
83
84 let vcs = RepoVcs::detect(&repo_root)?;
85
86 Ok(Self {
87 repo_root,
88 default_openapi_dir,
89 default_git_branch: DEFAULT_GIT_BRANCH.to_owned(),
90 default_jj_revset: DEFAULT_JJ_REVSET.to_owned(),
91 command,
92 vcs,
93 })
94 }
95
96 pub fn with_default_git_branch(
107 mut self,
108 branch: impl Into<String>,
109 ) -> Self {
110 self.default_git_branch = branch.into();
111 self
112 }
113
114 pub fn with_default_jj_revset(mut self, revset: impl Into<String>) -> Self {
123 self.default_jj_revset = revset.into();
124 self
125 }
126
127 #[cfg(test)]
133 pub(crate) fn new_for_test(
134 command: impl Into<String>,
135 repo_root: impl Into<Utf8PathBuf>,
136 default_openapi_dir: impl Into<Utf8PathBuf>,
137 ) -> anyhow::Result<Self> {
138 let command = command.into();
139 let repo_root = repo_root.into();
140 let default_openapi_dir = default_openapi_dir.into();
141
142 validate_paths(&repo_root, &default_openapi_dir)?;
143
144 let vcs = RepoVcs::git()?;
145
146 Ok(Self {
147 repo_root,
148 default_openapi_dir,
149 default_git_branch: DEFAULT_GIT_BRANCH.to_owned(),
150 default_jj_revset: DEFAULT_JJ_REVSET.to_owned(),
151 command,
152 vcs,
153 })
154 }
155
156 pub(crate) fn resolve(
157 &self,
158 openapi_dir: Option<Utf8PathBuf>,
159 ) -> anyhow::Result<ResolvedEnv> {
160 let (abs_dir, rel_dir) = match &openapi_dir {
170 Some(provided_dir) => {
171 let abs_dir = camino::absolute_utf8(provided_dir)
173 .with_context(|| {
174 format!(
175 "error making provided OpenAPI directory \
176 absolute: {}",
177 provided_dir
178 )
179 })?;
180
181 let rel_dir = abs_dir
183 .strip_prefix(&self.repo_root)
184 .with_context(|| {
185 format!(
186 "provided OpenAPI directory {} is not a \
187 subdirectory of repository root {}",
188 abs_dir, self.repo_root
189 )
190 })?
191 .to_path_buf();
192
193 (abs_dir, rel_dir)
194 }
195 None => {
196 let rel_dir = self.default_openapi_dir.clone();
197 let abs_dir = self.repo_root.join(&rel_dir);
198 (abs_dir, rel_dir)
199 }
200 };
201
202 let default_blessed_branch = match self.vcs.kind() {
205 RepoVcsKind::Git => self.default_git_branch.clone(),
206 RepoVcsKind::Jj => self.default_jj_revset.clone(),
207 };
208
209 Ok(ResolvedEnv {
210 command: self.command.clone(),
211 repo_root: self.repo_root.clone(),
212 local_source: LocalSource::Directory { abs_dir, rel_dir },
213 default_blessed_branch,
214 vcs: self.vcs.clone(),
215 })
216 }
217}
218
219fn validate_paths(
222 repo_root: &Utf8Path,
223 default_openapi_dir: &Utf8Path,
224) -> anyhow::Result<()> {
225 if !repo_root.is_absolute() {
226 return Err(anyhow::anyhow!(
227 "repo_root must be an absolute path, found: {}",
228 repo_root
229 ));
230 }
231
232 if !is_normal_relative(default_openapi_dir) {
233 return Err(anyhow::anyhow!(
234 "default_openapi_dir must be a relative path with \
235 normal components, found: {}",
236 default_openapi_dir
237 ));
238 }
239
240 Ok(())
241}
242
243fn is_normal_relative(default_openapi_dir: &Utf8Path) -> bool {
244 default_openapi_dir
245 .components()
246 .all(|c| matches!(c, Utf8Component::Normal(_) | Utf8Component::CurDir))
247}
248
249#[derive(Debug)]
251pub(crate) struct ResolvedEnv {
252 pub(crate) command: String,
253 pub(crate) repo_root: Utf8PathBuf,
254 pub(crate) local_source: LocalSource,
255 pub(crate) default_blessed_branch: String,
256 pub(crate) vcs: RepoVcs,
257}
258
259impl ResolvedEnv {
260 pub(crate) fn openapi_abs_dir(&self) -> &Utf8Path {
261 match &self.local_source {
262 LocalSource::Directory { abs_dir, .. } => abs_dir,
263 }
264 }
265
266 pub(crate) fn openapi_rel_dir(&self) -> &Utf8Path {
267 match &self.local_source {
268 LocalSource::Directory { rel_dir, .. } => rel_dir,
269 }
270 }
271}
272
273#[derive(Debug, Eq, PartialEq)]
276pub enum BlessedSource {
277 VcsRevisionMergeBase { revision: VcsRevision, directory: Utf8PathBuf },
281
282 Directory { local_directory: Utf8PathBuf },
286}
287
288impl BlessedSource {
289 pub fn load(
291 &self,
292 repo_root: &Utf8Path,
293 apis: &ManagedApis,
294 styles: &Styles,
295 vcs: &RepoVcs,
296 ) -> anyhow::Result<(BlessedFiles, ErrorAccumulator)> {
297 let mut errors = ErrorAccumulator::new();
298 match self {
299 BlessedSource::Directory { local_directory } => {
300 eprintln!(
301 "{:>HEADER_WIDTH$} blessed OpenAPI documents from {:?}",
302 "Loading".style(styles.success_header),
303 local_directory,
304 );
305 let api_files: ApiSpecFilesBuilder<'_, BlessedApiSpecFile> =
306 walk_local_directory(
307 local_directory,
308 apis,
309 &mut errors,
310 repo_root,
311 vcs,
312 )?;
313 Ok((BlessedFiles::from(api_files), errors))
314 }
315 BlessedSource::VcsRevisionMergeBase { revision, directory } => {
316 eprintln!(
317 "{:>HEADER_WIDTH$} blessed OpenAPI documents from VCS \
318 revision {:?} path {:?}",
319 "Loading".style(styles.success_header),
320 revision,
321 directory
322 );
323 Ok((
324 BlessedFiles::load_from_vcs_parent_branch(
325 repo_root,
326 revision,
327 directory,
328 apis,
329 &mut errors,
330 vcs,
331 )?,
332 errors,
333 ))
334 }
335 }
336 }
337}
338
339#[derive(Debug)]
341pub enum GeneratedSource {
342 Generated,
344
345 Directory { local_directory: Utf8PathBuf },
349}
350
351impl GeneratedSource {
352 pub fn load(
354 &self,
355 apis: &ManagedApis,
356 styles: &Styles,
357 repo_root: &Utf8Path,
358 vcs: &RepoVcs,
359 ) -> anyhow::Result<(GeneratedFiles, ErrorAccumulator)> {
360 let mut errors = ErrorAccumulator::new();
361 match self {
362 GeneratedSource::Generated => {
363 eprintln!(
364 "{:>HEADER_WIDTH$} OpenAPI documents from API \
365 definitions ... ",
366 GENERATING.style(styles.success_header)
367 );
368 Ok((GeneratedFiles::generate(apis, &mut errors)?, errors))
369 }
370 GeneratedSource::Directory { local_directory } => {
371 eprintln!(
372 "{:>HEADER_WIDTH$} \"generated\" OpenAPI documents from \
373 {:?} ... ",
374 "Loading".style(styles.success_header),
375 local_directory,
376 );
377 let api_files = walk_local_directory(
378 local_directory,
379 apis,
380 &mut errors,
381 repo_root,
382 vcs,
383 )?;
384 Ok((GeneratedFiles::from(api_files), errors))
385 }
386 }
387 }
388}
389
390#[derive(Debug)]
392pub enum LocalSource {
393 Directory {
395 abs_dir: Utf8PathBuf,
397 rel_dir: Utf8PathBuf,
400 },
401}
402
403impl LocalSource {
404 pub fn load(
408 &self,
409 apis: &ManagedApis,
410 styles: &Styles,
411 repo_root: &Utf8Path,
412 vcs: &RepoVcs,
413 ) -> anyhow::Result<(LocalFiles, ErrorAccumulator)> {
414 let mut errors = ErrorAccumulator::new();
415
416 let any_uses_git_stub =
418 apis.iter_apis().any(|a| apis.uses_git_stub_storage(a));
419 if any_uses_git_stub && vcs.is_shallow_clone(repo_root) {
420 errors.error(anyhow::anyhow!(
421 "this repository is a shallow clone, but Git stub storage is \
422 enabled for some APIs. Git stubs cannot be resolved in a \
423 shallow clone because the referenced commits may not be \
424 available. To fix this, fetch complete history (e.g. \
425 `git fetch --unshallow`) or make a fresh clone without \
426 --depth."
427 ));
428 return Ok((LocalFiles::default(), errors));
429 }
430
431 match self {
432 LocalSource::Directory { abs_dir, .. } => {
433 eprintln!(
434 "{:>HEADER_WIDTH$} local OpenAPI documents from \
435 {:?} ... ",
436 "Loading".style(styles.success_header),
437 abs_dir,
438 );
439 Ok((
440 LocalFiles::load_from_directory(
441 abs_dir,
442 apis,
443 &mut errors,
444 repo_root,
445 vcs,
446 )?,
447 errors,
448 ))
449 }
450 }
451 }
452}
453
454pub struct ErrorAccumulator {
456 errors: Vec<anyhow::Error>,
458 warnings: Vec<anyhow::Error>,
460}
461
462impl ErrorAccumulator {
463 pub fn new() -> ErrorAccumulator {
464 ErrorAccumulator { errors: Vec::new(), warnings: Vec::new() }
465 }
466
467 pub fn error(&mut self, error: anyhow::Error) {
469 self.errors.push(error);
470 }
471
472 pub fn warning(&mut self, error: anyhow::Error) {
474 self.warnings.push(error);
475 }
476
477 pub fn iter_errors(&self) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
478 self.errors.iter()
479 }
480
481 pub fn iter_warnings(
482 &self,
483 ) -> impl Iterator<Item = &'_ anyhow::Error> + '_ {
484 self.warnings.iter()
485 }
486}