1use std::{
7 any::{Any, TypeId},
8 collections::{BTreeMap, BTreeSet, HashMap},
9 marker::PhantomData,
10 path::{Path, PathBuf},
11 sync::atomic::{AtomicU64, Ordering},
12};
13
14use anyhow::{Context, Result, bail};
15
16use crate::{
17 Api, GeneratedFile, GeneratedTree, SdkClientStyle, SdkSemantics, SecuritySchemeCatalog,
18 analyze_sdk_semantics,
19};
20
21#[derive(Clone, Debug, Default, PartialEq, Eq)]
24pub struct Common {
25 pub client_name: Option<String>,
26 pub client_style: Option<SdkClientStyle>,
27 pub package_version: Option<String>,
28 pub layout: Option<crate::SourceLayout>,
29}
30
31impl Common {
32 pub fn client_name(mut self, name: impl Into<String>) -> Self {
33 self.client_name = Some(name.into());
34 self
35 }
36 pub fn client_style(mut self, style: SdkClientStyle) -> Self {
37 self.client_style = Some(style);
38 self
39 }
40 pub fn package_version(mut self, version: impl Into<String>) -> Self {
41 self.package_version = Some(version.into());
42 self
43 }
44 pub fn layout(mut self, layout: crate::SourceLayout) -> Self {
45 self.layout = Some(layout);
46 self
47 }
48 pub fn overlay(&self, local: &Self) -> Self {
49 Self {
50 client_name: local
51 .client_name
52 .clone()
53 .or_else(|| self.client_name.clone()),
54 client_style: local.client_style.or(self.client_style),
55 layout: local.layout.clone().or_else(|| self.layout.clone()),
56 package_version: local
57 .package_version
58 .clone()
59 .or_else(|| self.package_version.clone()),
60 }
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
66pub struct InstanceId(u64);
67
68impl InstanceId {
69 fn fresh() -> Self {
70 static NEXT: AtomicU64 = AtomicU64::new(1);
71 Self(NEXT.fetch_add(1, Ordering::Relaxed))
72 }
73}
74
75#[derive(Debug)]
78pub struct Meta {
79 id: InstanceId,
80 label: Option<String>,
81}
82
83impl Default for Meta {
84 fn default() -> Self {
85 Self {
86 id: InstanceId::fresh(),
87 label: None,
88 }
89 }
90}
91
92impl Meta {
93 pub fn new() -> Self {
94 Self::default()
95 }
96 pub fn label(mut self, label: impl Into<String>) -> Self {
97 self.label = Some(label.into());
98 self
99 }
100 pub fn handle<C: Contract>(&self) -> Handle<C> {
101 Handle {
102 id: self.id,
103 marker: PhantomData,
104 }
105 }
106}
107
108pub trait Contract: Any + Send + Sync {
109 const NAME: &'static str;
110}
111
112pub struct Handle<C: Contract> {
113 id: InstanceId,
114 marker: PhantomData<fn() -> C>,
115}
116impl<C: Contract> Copy for Handle<C> {}
117impl<C: Contract> Clone for Handle<C> {
118 fn clone(&self) -> Self {
119 *self
120 }
121}
122
123#[derive(Clone, Copy)]
124pub struct Provision {
125 type_id: TypeId,
126 name: &'static str,
127}
128impl Provision {
129 pub fn of<C: Contract>() -> Self {
130 Self {
131 type_id: TypeId::of::<C>(),
132 name: C::NAME,
133 }
134 }
135}
136
137pub struct Requirement {
138 contract: Provision,
139 provider: Option<InstanceId>,
140 optional: bool,
141}
142impl Requirement {
143 pub fn on<C: Contract>(handle: Option<Handle<C>>) -> Self {
144 Self {
145 contract: Provision::of::<C>(),
146 provider: handle.map(|h| h.id),
147 optional: false,
148 }
149 }
150 pub fn optional(mut self) -> Self {
153 self.optional = true;
154 self
155 }
156}
157
158pub trait Language: Send + Sync + Sized + 'static {
159 const NAME: &'static str;
160 type Settings: Default + Send + Sync;
161 type Workspace: Default + Send;
162 fn finalize(_cx: &mut FinalizeContext<'_, Self>) -> Result<()> {
163 Ok(())
164 }
165 fn finalize_files(_tree: &mut GeneratedTree) -> Result<()> {
168 Ok(())
169 }
170 fn bundle_middleware(
173 _tree: &mut GeneratedTree,
174 _middleware: &[crate::customization::BundledMiddleware],
175 ) -> Result<()> {
176 bail!("{} does not support bundled runtime middleware", Self::NAME)
177 }
178}
179
180#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
186pub enum PluginPhase {
187 #[default]
188 Generate,
189 Post,
190}
191
192#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
198pub enum Enforce {
199 #[default]
200 Default,
201 Post,
202}
203
204impl Enforce {
205 pub const fn phase(self) -> PluginPhase {
206 match self {
207 Self::Default => PluginPhase::Generate,
208 Self::Post => PluginPhase::Post,
209 }
210 }
211}
212
213pub trait Plugin<L: Language>: Send + Sync + 'static {
214 fn kind(&self) -> &'static str;
215 fn meta(&self) -> &Meta;
216 fn phase(&self) -> PluginPhase {
219 self.enforce().phase()
220 }
221 fn enforce(&self) -> Enforce {
223 Enforce::Default
224 }
225 fn supports_native_input(&self) -> bool {
228 false
229 }
230 fn requires(&self) -> Vec<Requirement> {
231 Vec::new()
232 }
233 fn provides(&self) -> Vec<Provision> {
234 Vec::new()
235 }
236 fn generate(&self, cx: &mut PluginContext<'_, L>) -> Result<()>;
237}
238
239type Contracts = HashMap<(InstanceId, TypeId), Box<dyn Any + Send + Sync>>;
240type Bindings = HashMap<TypeId, Option<InstanceId>>;
241
242pub struct Inputs<'a> {
243 contracts: &'a Contracts,
244 bindings: &'a Bindings,
245}
246impl Inputs<'_> {
247 pub fn get<C: Contract>(&self) -> Result<&C> {
248 self.optional::<C>()?
249 .with_context(|| format!("no provider bound for {}", C::NAME))
250 }
251 pub fn optional<C: Contract>(&self) -> Result<Option<&C>> {
253 let provider = self
254 .bindings
255 .get(&TypeId::of::<C>())
256 .with_context(|| format!("undeclared contract read: {}", C::NAME))?;
257 provider
258 .map(|id| {
259 self.contracts
260 .get(&(id, TypeId::of::<C>()))
261 .and_then(|value| value.downcast_ref::<C>())
262 .with_context(|| format!("provider did not publish {}", C::NAME))
263 })
264 .transpose()
265 }
266}
267
268pub struct Emitter<'a> {
270 tree: &'a mut GeneratedTree,
271 owners: &'a mut BTreeMap<PathBuf, String>,
272 owner: String,
273}
274impl Emitter<'_> {
275 pub fn emit(&mut self, file: GeneratedFile) -> Result<()> {
276 self.insert(file, false)
277 }
278 pub fn emit_custom(&mut self, file: GeneratedFile) -> Result<()> {
279 self.insert(file, true)
280 }
281 fn insert(&mut self, file: GeneratedFile, custom: bool) -> Result<()> {
282 let path = checked_path(&file.path)?;
284 if let Some(previous) = self.owners.get(&path) {
285 bail!(
286 "{} and {} both emit {}",
287 previous,
288 self.owner,
289 path.display()
290 );
291 }
292 let file = GeneratedFile::new(&path, file.contents)?;
293 if custom {
294 self.tree.insert_custom(file)?;
295 } else {
296 self.tree.insert(file)?;
297 }
298 self.tree.set_owner(&path, self.owner.clone())?;
299 self.owners.insert(path, self.owner.clone());
300 Ok(())
301 }
302 pub fn append(&mut self, tree: GeneratedTree) -> Result<()> {
303 for (file, custom, owner) in tree.into_owned_files() {
304 let path = file.path.clone();
305 self.insert(file, custom)?;
306 if let Some(owner) = owner {
307 self.tree.set_owner(path, owner)?;
308 }
309 }
310 Ok(())
311 }
312
313 pub fn append_from(&mut self, tree: GeneratedTree, prefix: &Path) -> Result<()> {
315 for (file, custom, owner) in tree.into_owned_files() {
316 let relative = file.path.strip_prefix(prefix).with_context(|| {
317 format!(
318 "renderer output {} is outside {}",
319 file.path.display(),
320 prefix.display()
321 )
322 })?;
323 let relative = relative.to_owned();
324 self.insert(GeneratedFile::new(&relative, file.contents)?, custom)?;
325 if let Some(owner) = owner {
326 self.tree.set_owner(relative, owner)?;
327 }
328 }
329 Ok(())
330 }
331}
332
333pub struct PluginContext<'a, L: Language> {
334 pub api: &'a Api,
335 pub semantics: &'a SdkSemantics,
336 pub security_schemes: Option<&'a SecuritySchemeCatalog>,
337 pub common: &'a Common,
338 pub settings: &'a L::Settings,
339 pub inputs: Inputs<'a>,
340 pub workspace: &'a mut L::Workspace,
341 pub files: Emitter<'a>,
342 publications: &'a mut HashMap<TypeId, Box<dyn Any + Send + Sync>>,
343 declared: &'a [Provision],
344}
345impl<L: Language> PluginContext<'_, L> {
346 pub fn publish<C: Contract>(&mut self, value: C) -> Result<()> {
347 let key = TypeId::of::<C>();
348 if !self.declared.iter().any(|p| p.type_id == key) {
349 bail!("undeclared contract publication: {}", C::NAME);
350 }
351 if self.publications.contains_key(&key) {
352 bail!("{} published twice", C::NAME);
353 }
354 self.publications.insert(key, Box::new(value));
355 Ok(())
356 }
357}
358
359pub struct FinalizeContext<'a, L: Language> {
360 pub api: &'a Api,
361 pub common: &'a Common,
362 pub settings: &'a L::Settings,
363 pub workspace: &'a mut L::Workspace,
364 pub files: Emitter<'a>,
365}
366
367pub struct Package<L: Language> {
368 dir: String,
369 settings: L::Settings,
370 common: Common,
371 plugins: Vec<Box<dyn Plugin<L>>>,
372 customizations: Vec<crate::customization::CodeCustomization>,
373 middleware: Vec<crate::customization::BundledMiddleware>,
374 idempotency: crate::idempotency::IdempotencyConfig,
375}
376
377impl<L: Language> Package<L> {
378 pub fn new(dir: impl Into<String>) -> Self {
379 Self {
380 dir: dir.into(),
381 settings: Default::default(),
382 common: Default::default(),
383 plugins: vec![],
384 customizations: vec![],
385 middleware: vec![],
386 idempotency: Default::default(),
387 }
388 }
389 pub fn with(mut self, plugin: impl Plugin<L>) -> Self {
390 self.plugins.push(Box::new(plugin));
391 self
392 }
393 pub fn customize(mut self, code: crate::customization::CodeCustomization) -> Self {
395 self.customizations.push(code);
396 self
397 }
398 pub fn middleware(mut self, middleware: crate::customization::BundledMiddleware) -> Self {
400 self.middleware.push(middleware);
401 self
402 }
403 pub fn idempotency(mut self, config: crate::idempotency::IdempotencyConfig) -> Self {
405 self.idempotency = config;
406 self
407 }
408 pub fn common(mut self, common: Common) -> Self {
409 self.common = common;
410 self
411 }
412 pub fn settings(mut self, settings: L::Settings) -> Self {
413 self.settings = settings;
414 self
415 }
416 pub fn settings_mut(&mut self) -> &mut L::Settings {
417 &mut self.settings
418 }
419}
420
421struct Plan {
422 order: Vec<usize>,
423 phases: Vec<PluginPhase>,
424 bindings: Vec<Bindings>,
425 provisions: Vec<Vec<Provision>>,
426}
427
428impl<L: Language> Package<L> {
429 fn label(&self, index: usize) -> String {
430 let plugin = &self.plugins[index];
431 format!(
432 "{} ({})",
433 plugin.kind(),
434 plugin.meta().label.as_deref().unwrap_or(&self.dir)
435 )
436 }
437
438 fn resolve(&self) -> Result<Plan> {
439 checked_path(Path::new(&self.dir))?;
440 let mut middleware_paths = BTreeSet::new();
441 for middleware in &self.middleware {
442 middleware.validate()?;
443 if !middleware_paths.insert(checked_path(&middleware.path)?) {
444 bail!(
445 "duplicate bundled middleware path in {}: {}",
446 self.dir,
447 middleware.path.display()
448 );
449 }
450 }
451 let mut instances = BTreeMap::new();
452 let mut providers: HashMap<TypeId, Vec<usize>> = HashMap::new();
453 let mut phases = Vec::new();
454 let mut provisions = Vec::new();
455 for (index, plugin) in self.plugins.iter().enumerate() {
456 if instances.insert(plugin.meta().id, index).is_some() {
457 bail!("duplicate plugin instance: {}", self.label(index));
458 }
459 phases.push(plugin.phase());
460 let supplied = plugin.provides();
461 let mut seen = BTreeSet::new();
462 for contract in &supplied {
463 if !seen.insert(contract.type_id) {
464 bail!("{} declares {} twice", self.label(index), contract.name);
465 }
466 providers.entry(contract.type_id).or_default().push(index);
467 }
468 provisions.push(supplied);
469 }
470 let mut dependencies = vec![BTreeSet::new(); self.plugins.len()];
471 let mut bindings = Vec::new();
472 for (index, plugin) in self.plugins.iter().enumerate() {
473 let mut bound = HashMap::new();
474 for req in plugin.requires() {
475 let candidates = providers
476 .get(&req.contract.type_id)
477 .map(Vec::as_slice)
478 .unwrap_or(&[]);
479 let selected = if let Some(id) = req.provider {
480 let selected = *instances.get(&id).with_context(|| {
481 format!(
482 "{}: handle for {} is not registered in this package",
483 self.label(index),
484 req.contract.name
485 )
486 })?;
487 if !candidates.contains(&selected) {
488 bail!(
489 "{}: selected provider does not provide {}",
490 self.label(index),
491 req.contract.name
492 );
493 }
494 Some(selected)
495 } else {
496 match candidates {
497 [] if req.optional => None,
498 [] => bail!(
499 "{} requires {}, but no provider is registered",
500 self.label(index),
501 req.contract.name
502 ),
503 [one] => Some(*one),
504 many => bail!(
505 "{} needs one {} provider; found {}: {}. Select a provider handle explicitly",
506 self.label(index),
507 req.contract.name,
508 many.len(),
509 many.iter()
510 .map(|i| self.label(*i))
511 .collect::<Vec<_>>()
512 .join(", ")
513 ),
514 }
515 };
516 if bound
517 .insert(
518 req.contract.type_id,
519 selected.map(|i| self.plugins[i].meta().id),
520 )
521 .is_some()
522 {
523 bail!("{} requires {} twice", self.label(index), req.contract.name);
524 }
525 if let Some(selected) = selected {
526 if phases[index] == PluginPhase::Generate
527 && phases[selected] == PluginPhase::Post
528 {
529 bail!(
530 "{} requires {} from {}, but generation plugins cannot depend on post plugins",
531 self.label(index),
532 req.contract.name,
533 self.label(selected),
534 );
535 }
536 dependencies[index].insert(selected);
537 }
538 }
539 bindings.push(bound);
540 }
541 for (index, phase) in phases.iter().enumerate() {
545 if *phase == PluginPhase::Post {
546 dependencies[index].extend(phases.iter().enumerate().filter_map(
547 |(other, phase)| (*phase == PluginPhase::Generate).then_some(other),
548 ));
549 }
550 }
551 let mut order = Vec::new();
552 let mut completed = BTreeSet::new();
553 while order.len() < self.plugins.len() {
554 let next = (0..self.plugins.len())
555 .find(|i| !completed.contains(i) && dependencies[*i].is_subset(&completed));
556 let Some(next) = next else {
557 bail!(
558 "plugin dependency cycle in {}: {}",
559 self.dir,
560 (0..self.plugins.len())
561 .filter(|i| !completed.contains(i))
562 .map(|i| self.label(i))
563 .collect::<Vec<_>>()
564 .join(", ")
565 );
566 };
567 completed.insert(next);
568 order.push(next);
569 }
570 Ok(Plan {
571 order,
572 phases,
573 bindings,
574 provisions,
575 })
576 }
577
578 fn run(
579 &self,
580 api: &Api,
581 common: &Common,
582 catalog: Option<&SecuritySchemeCatalog>,
583 ) -> Result<GeneratedTree> {
584 let plan = self.resolve()?;
585 let common = common.overlay(&self.common);
586 let overridden = common.package_version.as_ref().map(|version| {
587 let mut copy = api.clone();
588 copy.version.clone_from(version);
589 copy
590 });
591 let api = overridden.as_ref().unwrap_or(api);
592 let prepared = crate::idempotency::prepare_api(api, &self.idempotency)?;
593 let api = &prepared;
594 let semantics = analyze_sdk_semantics(api, catalog);
595 let mut workspace = L::Workspace::default();
596 let mut tree = GeneratedTree::default();
597 let mut owners = BTreeMap::new();
598 let mut contracts = Contracts::new();
599 for phase in [PluginPhase::Generate, PluginPhase::Post] {
600 for &index in &plan.order {
601 if plan.phases[index] != phase {
602 continue;
603 }
604 let plugin = &self.plugins[index];
605 let mut publications = HashMap::new();
606 let mut cx = PluginContext {
607 api,
608 semantics: &semantics,
609 security_schemes: catalog,
610 common: &common,
611 settings: &self.settings,
612 inputs: Inputs {
613 contracts: &contracts,
614 bindings: &plan.bindings[index],
615 },
616 workspace: &mut workspace,
617 files: Emitter {
618 tree: &mut tree,
619 owners: &mut owners,
620 owner: self.label(index),
621 },
622 publications: &mut publications,
623 declared: &plan.provisions[index],
624 };
625 plugin
626 .generate(&mut cx)
627 .with_context(|| format!("generate {}", self.label(index)))?;
628 for provision in &plan.provisions[index] {
629 if !publications.contains_key(&provision.type_id) {
630 bail!(
631 "{} did not publish declared contract {}",
632 self.label(index),
633 provision.name
634 );
635 }
636 }
637 for (key, value) in publications {
638 contracts.insert((plugin.meta().id, key), value);
639 }
640 }
641 if phase == PluginPhase::Generate {
642 L::finalize(&mut FinalizeContext {
643 api,
644 common: &common,
645 settings: &self.settings,
646 workspace: &mut workspace,
647 files: Emitter {
648 tree: &mut tree,
649 owners: &mut owners,
650 owner: format!("{} finalizer", L::NAME),
651 },
652 })?;
653 }
654 }
655 if !self.middleware.is_empty() {
656 let mut staged = tree.clone();
657 let middleware = self
658 .middleware
659 .iter()
660 .map(|item| {
661 let mut item = item.clone();
662 item.path = checked_path(&item.path)?;
663 Ok(item)
664 })
665 .collect::<Result<Vec<_>>>()?;
666 L::bundle_middleware(&mut staged, &middleware).with_context(|| {
667 format!("bundle runtime middleware for {} ({})", self.dir, L::NAME)
668 })?;
669 for item in &middleware {
670 staged.set_owner(
671 &item.path,
672 format!("bundled-middleware:{}", item.path.display()),
673 )?;
674 }
675 tree = staged;
676 }
677 L::finalize_files(&mut tree)?;
678 crate::customization::apply_code_customizations(&mut tree, &self.customizations)?;
679 let mut output = GeneratedTree::default();
680 let dir = checked_path(Path::new(&self.dir))?;
681 for (file, custom, owner) in tree.into_owned_files() {
682 let file = GeneratedFile::new(dir.join(file.path), file.contents)?;
683 let path = file.path.clone();
684 if custom {
685 output.insert_custom(file)?;
686 } else {
687 output.insert(file)?;
688 }
689 if let Some(owner) = owner {
690 output.set_owner(path, format!("{}::{owner}", self.dir))?;
691 }
692 }
693 Ok(output)
694 }
695}
696
697trait ErasedPackage: Send + Sync {
699 fn directory(&self) -> &str;
700 fn validate(&self) -> Result<()>;
701 fn native_incompatibility(&self) -> Option<String>;
702 fn validate_native(&self) -> Result<()>;
703 fn generate(
704 &self,
705 api: &Api,
706 common: &Common,
707 catalog: Option<&SecuritySchemeCatalog>,
708 ) -> Result<GeneratedTree>;
709}
710impl<L: Language> ErasedPackage for Package<L> {
711 fn directory(&self) -> &str {
712 &self.dir
713 }
714 fn validate(&self) -> Result<()> {
715 self.resolve().map(|_| ())
716 }
717 fn native_incompatibility(&self) -> Option<String> {
718 let plugins: Vec<_> = self
719 .plugins
720 .iter()
721 .enumerate()
722 .filter(|(_, plugin)| !plugin.supports_native_input())
723 .map(|(index, _)| self.label(index))
724 .collect();
725 if plugins.is_empty() {
726 None
727 } else {
728 Some(format!(
729 "{} requires an HTTP API and cannot consume native input contracts",
730 plugins.join(", ")
731 ))
732 }
733 }
734 fn validate_native(&self) -> Result<()> {
735 if !self.middleware.is_empty()
736 || (self.idempotency.defaults.is_some() || !self.idempotency.operations.is_empty())
737 {
738 bail!(
739 "native package {} cannot use HTTP middleware or idempotency policy",
740 self.dir
741 );
742 }
743 self.validate()
744 }
745 fn generate(
746 &self,
747 api: &Api,
748 common: &Common,
749 catalog: Option<&SecuritySchemeCatalog>,
750 ) -> Result<GeneratedTree> {
751 self.run(api, common, catalog)
752 }
753}
754
755#[derive(Clone, Debug, PartialEq, Eq)]
757pub struct NativeSkip {
758 pub package: String,
759 pub reason: String,
760}
761#[derive(Debug)]
762pub struct NativeGeneration {
763 pub tree: GeneratedTree,
764 pub skipped: Vec<NativeSkip>,
765}
766
767#[derive(Default)]
768pub struct Packages {
769 packages: Vec<Box<dyn ErasedPackage>>,
770 common: Common,
771}
772impl Packages {
773 pub fn new() -> Self {
774 Self::default()
775 }
776 pub fn common(mut self, common: Common) -> Self {
777 self.common = common;
778 self
779 }
780 pub fn package<L: Language>(mut self, package: Package<L>) -> Self {
781 self.packages.push(Box::new(package));
782 self
783 }
784 pub fn is_empty(&self) -> bool {
785 self.packages.is_empty()
786 }
787 pub fn generate_native(&self) -> Result<GeneratedTree> {
791 let report = self.generate_native_report()?;
792 for skipped in &report.skipped {
793 eprintln!(
794 "warning: skipped native package {}: {}",
795 skipped.package, skipped.reason
796 );
797 }
798 Ok(report.tree)
799 }
800 pub fn generate_native_report(&self) -> Result<NativeGeneration> {
803 let mut skipped = Vec::new();
804 let mut active = Vec::new();
805 let mut dirs: Vec<PathBuf> = Vec::new();
806 for package in &self.packages {
807 let dir = checked_path(Path::new(package.directory()))?;
808 if let Some(other) = dirs
809 .iter()
810 .find(|other| dir.starts_with(other) || other.starts_with(&dir))
811 {
812 bail!(
813 "package directories overlap: {} and {}",
814 other.display(),
815 dir.display()
816 );
817 }
818 dirs.push(dir);
819 if let Some(reason) = package.native_incompatibility() {
820 skipped.push(NativeSkip {
821 package: package.directory().to_owned(),
822 reason,
823 });
824 } else {
825 package.validate_native()?;
826 active.push(package);
827 }
828 }
829 let mut tree = GeneratedTree::default();
830 for package in active {
831 tree.append(package.generate(&Api::default(), &self.common, None)?)?;
832 }
833 Ok(NativeGeneration { tree, skipped })
834 }
835 pub fn generate(
836 &self,
837 api: &Api,
838 catalog: Option<&SecuritySchemeCatalog>,
839 ) -> Result<GeneratedTree> {
840 for operation in &api.operations {
841 let parsed =
842 crate::HttpMethod::parse(operation.method.as_str()).map_err(anyhow::Error::msg)?;
843 if parsed != operation.method {
844 bail!("HTTP method variants must use their canonical representation");
845 }
846 }
847 let mut dirs: Vec<PathBuf> = Vec::new();
848 for package in &self.packages {
850 let dir = checked_path(Path::new(package.directory()))?;
851 if let Some(other) = dirs
852 .iter()
853 .find(|other| dir.starts_with(other) || other.starts_with(&dir))
854 {
855 bail!(
856 "package directories overlap: {} and {}",
857 other.display(),
858 dir.display()
859 );
860 }
861 dirs.push(dir);
862 package.validate()?;
863 }
864 let mut output = GeneratedTree::default();
865 for package in &self.packages {
866 output.append(package.generate(api, &self.common, catalog)?)?;
867 }
868 Ok(output)
869 }
870}
871
872fn checked_path(path: &Path) -> Result<PathBuf> {
873 GeneratedFile::new(path, "")?;
874 let normalized: PathBuf = path
875 .components()
876 .filter(|component| !matches!(component, std::path::Component::CurDir))
877 .collect();
878 if normalized.as_os_str().is_empty() || normalized == Path::new(".") {
879 bail!("output path cannot be empty or '.'");
880 }
881 Ok(normalized)
882}