1use std::{
2 collections::{BTreeMap, BTreeSet},
3 error::Error as StdError,
4 fmt::{Debug, Display},
5 ops::Range,
6 path::Path,
7};
8
9use itertools::Itertools;
10use miette::SourceSpan;
11use ploidy_core::{codegen::Code, ir::View};
12use semver::Version;
13use serde::{Deserialize, de::IntoDeserializer};
14use toml_edit::{Array, DocumentMut, InlineTable, Table, TableLike, value};
15
16use super::{config::CodegenConfig, graph::CodegenGraph, naming::AsFeatureName};
17
18const PLOIDY_VERSION: &str = env!("CARGO_PKG_VERSION");
19
20#[derive(Clone, Debug)]
21pub struct CodegenCargoManifest<'a> {
22 graph: &'a CodegenGraph<'a>,
23 manifest: &'a CargoManifest,
24}
25
26impl<'a> CodegenCargoManifest<'a> {
27 #[inline]
28 pub fn new(graph: &'a CodegenGraph<'a>, manifest: &'a CargoManifest) -> Self {
29 Self { graph, manifest }
30 }
31
32 pub fn to_manifest(self) -> CargoManifest {
33 let features = {
36 let mut deps_by_resource = BTreeMap::new();
37
38 for schema in self.graph.schemas() {
42 let Some(resource) = self.graph.resource_for(&schema).name() else {
43 continue;
44 };
45 let entry: &mut BTreeSet<_> = deps_by_resource.entry(resource).or_default();
46 entry.extend(
47 schema
48 .dependencies()
49 .filter_map(|ty| ty.into_schema().right())
50 .filter_map(|schema| self.graph.resource_for(&schema).name())
51 .filter(|dep| *dep != resource),
52 );
53 }
54
55 for op in self.graph.operations() {
59 let Some(resource) = self.graph.resource_for(&op).name() else {
60 continue;
61 };
62 let entry = deps_by_resource.entry(resource).or_default();
63 entry.extend(
64 op.dependencies()
65 .filter_map(|ty| ty.into_schema().right())
66 .filter_map(|schema| self.graph.resource_for(&schema).name())
67 .filter(|dep| *dep != resource),
68 );
69 }
70
71 let mut features = BTreeMap::new();
73 if !deps_by_resource.is_empty() {
74 features.extend(deps_by_resource.iter().map(|(resource, deps)| {
75 (
76 AsFeatureName(*resource).to_string(),
77 FeatureDependencies(
78 deps.iter()
79 .map(|resource| AsFeatureName(*resource).to_string())
80 .collect_vec(),
81 ),
82 )
83 }));
84 features.insert(
86 "default".to_owned(),
87 FeatureDependencies(
88 deps_by_resource
89 .keys()
90 .map(|resource| AsFeatureName(*resource).to_string())
91 .collect_vec(),
92 ),
93 );
94 }
95 features.insert(
98 "tracing".to_owned(),
99 FeatureDependencies(vec!["ploidy-util/tracing".to_owned()]),
100 );
101 features.insert(
102 "trace-context".to_owned(),
103 FeatureDependencies(vec![
104 "tracing".to_owned(),
105 "ploidy-util/trace-context".to_owned(),
106 ]),
107 );
108 features
109 };
110
111 self.manifest.clone().apply(CargoManifestDiff {
112 edition: Some(RustEdition::E2024),
114 dependencies: Some(BTreeMap::from_iter([
115 (
117 "ploidy-util".to_owned(),
118 Dependency::Simple(PLOIDY_VERSION.parse().unwrap()),
119 ),
120 ])),
121 features: Some(features),
122 ..Default::default()
123 })
124 }
125}
126
127impl Code for CodegenCargoManifest<'_> {
128 fn path(&self) -> &str {
129 "Cargo.toml"
130 }
131
132 fn into_string(self) -> miette::Result<String> {
133 Ok(self.to_manifest().to_string())
134 }
135}
136
137#[derive(Clone, Debug)]
139pub struct CargoManifest(DocumentMut);
140
141impl CargoManifest {
142 pub fn new(name: &str, version: Version) -> Self {
144 let package = Table::from_iter([
145 ("name", value(name)),
146 ("version", value(version.to_string())),
147 ("edition", value(RustEdition::E2024)),
148 ]);
149 let manifest = Table::from_iter([("package", package)]);
150 Self(manifest.into())
151 }
152
153 pub fn from_disk(path: &Path) -> Result<Self, CargoManifestError> {
155 let contents = std::fs::read_to_string(path)?;
156 Self::parse(&contents)
157 }
158
159 pub fn parse(s: &str) -> Result<Self, CargoManifestError> {
161 Ok(Self(s.parse().map_err(
162 |source: toml_edit::TomlError| {
163 let span = source.span().map(SourceSpan::from);
164 SpannedError {
165 source: Box::new(source),
166 code: s.to_owned(),
167 span,
168 }
169 },
170 )?))
171 }
172
173 #[inline]
176 pub fn package(&self) -> Option<Package<'_>> {
177 let package = self.0.get("package")?.as_table_like()?;
178 let name = package.get("name")?;
179 let version = package.get("version")?;
180 Some(Package {
181 name: SpannedValue::new(name.as_str()?, &self.0, name.span()),
182 version: SpannedValue::new(version.as_str()?, &self.0, version.span()),
183 metadata: package
184 .get("metadata")
185 .and_then(|meta| Some((meta.as_table_like()?, meta.span())))
186 .map(|(meta, range)| SpannedValue::new(meta, &self.0, range)),
187 })
188 }
189
190 pub fn features(&self) -> BTreeMap<&str, Vec<&str>> {
192 self.0
193 .get("features")
194 .and_then(|features| features.as_table_like())
195 .into_iter()
196 .flat_map(|features| features.iter())
197 .map(|(name, item)| {
198 let deps = item
199 .as_array()
200 .into_iter()
201 .flat_map(|deps| deps.iter())
202 .filter_map(|dep| dep.as_str())
203 .collect_vec();
204 (name, deps)
205 })
206 .collect()
207 }
208
209 pub fn apply(mut self, diff: CargoManifestDiff) -> Self {
211 let package = &mut self.0["package"];
212 if let Some(name) = diff.name {
213 package["name"] = value(name);
214 }
215 if let Some(version) = diff.version {
216 package["version"] = value(version.to_string());
217 }
218 if let Some(edition) = diff.edition {
219 package["edition"] = value(edition);
220 }
221 if let Some(deps) = diff.dependencies.filter(|f| !f.is_empty()) {
222 let table = self.0["dependencies"].or_insert(Table::new().into());
223 for (name, dep) in deps {
224 dep.merge_into(&mut table[&name]);
225 }
226 }
227 if let Some(features) = diff.features.filter(|f| !f.is_empty()) {
228 let table = self.0["features"].or_insert(Table::new().into());
229 for (name, feature) in features {
230 feature.merge_into(&mut table[&name]);
231 }
232 }
233 self
234 }
235}
236
237impl Display for CargoManifest {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 write!(f, "{}", self.0)
240 }
241}
242
243#[derive(Clone, Copy)]
245pub struct Package<'a> {
246 name: SpannedValue<'a, &'a str>,
247 version: SpannedValue<'a, &'a str>,
248 metadata: Option<SpannedValue<'a, &'a dyn TableLike>>,
249}
250
251impl<'a> Package<'a> {
252 pub fn name(&self) -> &'a str {
254 self.name.value
255 }
256
257 pub fn version(&self) -> Result<Version, SpannedError<PackageError>> {
259 Version::parse(self.version.value).map_err(|err| SpannedError {
260 source: Box::new(PackageError::from(err)),
261 code: self.version.source.to_string(),
262 span: self.version.span,
263 })
264 }
265
266 pub fn config(&self) -> Result<Option<CodegenConfig>, SpannedError<PackageError>> {
270 let meta = match self.metadata {
271 Some(meta) => meta,
272 None => return Ok(None),
273 };
274 let table: Table = match meta.value.get("ploidy").and_then(|v| v.as_table_like()) {
275 Some(table) => table.iter().collect(),
276 None => return Ok(None),
277 };
278 let value: toml_edit::Value = table.into_inline_table().into();
279 let config =
280 CodegenConfig::deserialize(value.into_deserializer()).map_err(|err| SpannedError {
281 source: Box::new(PackageError::from(err)),
282 code: meta.source.to_string(),
283 span: meta.span,
284 })?;
285 Ok(Some(config))
286 }
287}
288
289impl Debug for Package<'_> {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 f.debug_struct("Package")
292 .field("name", &self.name)
293 .field("version", &self.version)
294 .finish_non_exhaustive()
295 }
296}
297
298#[derive(Clone, Copy, Debug)]
300struct SpannedValue<'a, T> {
301 source: &'a DocumentMut,
302 value: T,
303 span: Option<SourceSpan>,
304}
305
306impl<'a, T> SpannedValue<'a, T> {
307 fn new(value: T, source: &'a DocumentMut, range: Option<Range<usize>>) -> Self {
308 Self {
309 source,
310 value,
311 span: range.map(SourceSpan::from),
312 }
313 }
314}
315
316#[derive(Debug, miette::Diagnostic)]
318pub struct SpannedError<E: StdError + Send + Sync + 'static> {
319 source: Box<E>,
320 #[source_code]
321 code: String,
322 #[label]
323 span: Option<SourceSpan>,
324}
325
326impl<E: StdError + Send + Sync + 'static> Display for SpannedError<E> {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 Display::fmt(&self.source, f)
329 }
330}
331
332impl<E: StdError + Send + Sync + 'static> StdError for SpannedError<E> {
333 fn source(&self) -> Option<&(dyn StdError + 'static)> {
334 self.source.source()
337 }
338}
339
340#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
342pub enum RustEdition {
343 E2021,
344 #[default]
345 E2024,
346}
347
348impl From<RustEdition> for toml_edit::Value {
349 fn from(edition: RustEdition) -> Self {
350 toml_edit::Value::from(match edition {
351 RustEdition::E2021 => "2021",
352 RustEdition::E2024 => "2024",
353 })
354 }
355}
356
357#[derive(Clone, Debug, Default)]
359pub struct CargoManifestDiff {
360 pub name: Option<String>,
361 pub version: Option<Version>,
362 pub edition: Option<RustEdition>,
363 pub dependencies: Option<BTreeMap<String, Dependency>>,
364 pub features: Option<BTreeMap<String, FeatureDependencies>>,
365}
366
367#[derive(Clone, Debug)]
369pub enum Dependency {
370 Simple(Version),
371 Detailed(DependencyDetail),
372}
373
374impl Dependency {
375 fn merge_into(self, entry: &mut toml_edit::Item) {
379 match self {
380 Dependency::Simple(version) => {
381 if let Some(table) = entry.as_table_like_mut() {
382 table.insert("version", value(version.to_string()));
383 } else {
384 *entry = value(version.to_string());
385 }
386 }
387 Dependency::Detailed(detail) => {
388 let table = match entry.as_table_like_mut() {
389 Some(table) => table,
390 None => {
391 *entry = InlineTable::new().into();
392 entry.as_table_like_mut().unwrap()
393 }
394 };
395 table.insert("version", value(detail.version.to_string()));
396 if let Some(path) = detail.path {
397 table.insert("path", value(path));
398 }
399 }
400 }
401 }
402}
403
404#[derive(Clone, Debug)]
405pub struct DependencyDetail {
406 pub version: Version,
407 pub path: Option<String>,
408}
409
410#[derive(Clone, Debug)]
412pub struct FeatureDependencies(Vec<String>);
413
414impl FeatureDependencies {
415 fn merge_into(self, entry: &mut toml_edit::Item) {
420 match entry.as_array_mut() {
421 Some(array) => {
422 let existing: BTreeSet<_> = array.iter().filter_map(|dep| dep.as_str()).collect();
423 let new = self
424 .0
425 .into_iter()
426 .filter(|dep| !existing.contains(dep.as_str()))
427 .collect_vec();
428 array.extend(new);
429 }
430 None => {
431 *entry = Array::from_iter(self.0).into();
432 }
433 }
434 }
435}
436
437#[derive(Debug, thiserror::Error)]
438pub enum CargoManifestError {
439 #[error(transparent)]
440 Io(#[from] std::io::Error),
441
442 #[error(transparent)]
443 Parse(#[from] SpannedError<toml_edit::TomlError>),
444}
445
446#[derive(Debug, thiserror::Error)]
447pub enum PackageError {
448 #[error(transparent)]
449 Deserialize(#[from] toml_edit::de::Error),
450
451 #[error(transparent)]
452 Semver(#[from] semver::Error),
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 use ploidy_core::{
460 arena::Arena,
461 ir::{RawGraph, Spec},
462 parse::Document,
463 };
464
465 use crate::{config::DateTimeFormat, tests::assert_matches};
466
467 fn default_manifest() -> CargoManifest {
468 CargoManifest::new("test-client", Version::new(0, 1, 0))
469 }
470
471 #[test]
474 fn test_new_manifest_has_package_name_version_and_edition() {
475 assert_eq!(
476 CargoManifest::new("my-crate", Version::new(1, 0, 0)).to_string(),
477 indoc::indoc! {r#"
478 [package]
479 name = "my-crate"
480 version = "1.0.0"
481 edition = "2024"
482 "#},
483 );
484 }
485
486 #[test]
487 fn test_package_returns_none_for_workspace() {
488 let manifest = CargoManifest::parse(indoc::indoc! {r#"
489 [workspace]
490 members = ["a"]
491 "#})
492 .unwrap();
493 assert!(manifest.package().is_none());
494 }
495
496 #[test]
497 fn test_apply_sets_name() {
498 let manifest = CargoManifest::new("old", Version::new(1, 0, 0)).apply(CargoManifestDiff {
499 name: Some("new".to_owned()),
500 ..Default::default()
501 });
502 assert_eq!(manifest.package().unwrap().name.value, "new");
503 }
504
505 #[test]
506 fn test_apply_sets_version() {
507 let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0)).apply(CargoManifestDiff {
508 version: Some(Version::new(2, 0, 0)),
509 ..Default::default()
510 });
511 assert_eq!(manifest.package().unwrap().version.value, "2.0.0");
512 }
513
514 #[test]
515 fn test_apply_sets_edition() {
516 let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0)).apply(CargoManifestDiff {
517 edition: Some(RustEdition::E2021),
518 ..Default::default()
519 });
520 assert_eq!(
521 manifest.to_string(),
522 indoc::indoc! {r#"
523 [package]
524 name = "pkg"
525 version = "1.0.0"
526 edition = "2021"
527 "#},
528 );
529 }
530
531 #[test]
532 fn test_apply_sets_simple_dependency() {
533 let mut deps = BTreeMap::new();
534 deps.insert(
535 "serde".to_owned(),
536 Dependency::Simple(Version::new(1, 0, 0)),
537 );
538 let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0)).apply(CargoManifestDiff {
539 dependencies: Some(deps),
540 ..Default::default()
541 });
542 assert_eq!(
543 manifest.to_string(),
544 indoc::indoc! {r#"
545 [package]
546 name = "pkg"
547 version = "1.0.0"
548 edition = "2024"
549
550 [dependencies]
551 serde = "1.0.0"
552 "#},
553 );
554 }
555
556 #[test]
557 fn test_apply_sets_detailed_dependency() {
558 let mut deps = BTreeMap::new();
559 deps.insert(
560 "ploidy-util".to_owned(),
561 Dependency::Detailed(DependencyDetail {
562 version: Version::new(0, 10, 0),
563 path: Some("../ploidy-util".to_owned()),
564 }),
565 );
566 let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0)).apply(CargoManifestDiff {
567 dependencies: Some(deps),
568 ..Default::default()
569 });
570 assert_eq!(
571 manifest.to_string(),
572 indoc::indoc! {r#"
573 [package]
574 name = "pkg"
575 version = "1.0.0"
576 edition = "2024"
577
578 [dependencies]
579 ploidy-util = { version = "0.10.0", path = "../ploidy-util" }
580 "#},
581 );
582 }
583
584 #[test]
585 fn test_apply_preserves_existing_dependencies() {
586 let doc = Document::from_yaml(indoc::indoc! {"
587 openapi: 3.0.0
588 info:
589 title: Test
590 version: 1.0.0
591 paths: {}
592 "})
593 .unwrap();
594
595 let manifest = default_manifest().apply(CargoManifestDiff {
596 dependencies: Some({
597 let mut deps = BTreeMap::new();
598 deps.insert(
599 "serde".to_owned(),
600 Dependency::Simple(Version::new(1, 0, 0)),
601 );
602 deps
603 }),
604 ..Default::default()
605 });
606
607 let arena = Arena::new();
608 let spec = Spec::from_doc(&arena, &doc).unwrap();
609 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
610 let manifest = CodegenCargoManifest::new(&graph, &manifest).to_manifest();
611
612 assert_eq!(
613 manifest.to_string(),
614 indoc::formatdoc! {r#"
615 [package]
616 name = "test-client"
617 version = "0.1.0"
618 edition = "2024"
619
620 [dependencies]
621 serde = "1.0.0"
622 ploidy-util = "{PLOIDY_VERSION}"
623
624 [features]
625 trace-context = ["tracing", "ploidy-util/trace-context"]
626 tracing = ["ploidy-util/tracing"]
627 "#},
628 );
629 }
630
631 #[test]
632 fn test_apply_sets_features() {
633 let mut features = BTreeMap::new();
634 features.insert(
635 "default".to_owned(),
636 FeatureDependencies(vec!["customer".to_owned()]),
637 );
638 features.insert("customer".to_owned(), FeatureDependencies(vec![]));
639 let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0)).apply(CargoManifestDiff {
640 features: Some(features),
641 ..Default::default()
642 });
643 let f = manifest.features();
644 assert_eq!(f["default"], vec!["customer"]);
645 assert_eq!(f["customer"], Vec::<String>::new());
646 }
647
648 #[test]
649 fn test_apply_preserves_untouched_fields() {
650 let manifest = CargoManifest::parse(indoc::indoc! {r#"
651 [package]
652 name = "pkg"
653 version = "1.0.0"
654 edition = "2021"
655
656 [profile.release]
657 lto = true
658 "#})
659 .unwrap()
660 .apply(CargoManifestDiff {
661 edition: Some(RustEdition::E2024),
662 ..Default::default()
663 });
664 assert_eq!(
665 manifest.to_string(),
666 indoc::indoc! {r#"
667 [package]
668 name = "pkg"
669 version = "1.0.0"
670 edition = "2024"
671
672 [profile.release]
673 lto = true
674 "#},
675 );
676 }
677
678 #[test]
679 fn test_config_returns_none_when_absent() {
680 let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0));
681 let pkg = manifest.package().unwrap();
682 assert_matches!(pkg.config(), Ok(None));
683 }
684
685 #[test]
686 fn test_config_deserializes_codegen_config() {
687 let manifest = CargoManifest::parse(indoc::indoc! {r#"
688 [package]
689 name = "pkg"
690 version = "1.0.0"
691 edition = "2024"
692
693 [package.metadata.ploidy]
694 date-time-format = "unix-seconds"
695 "#})
696 .unwrap();
697 let pkg = manifest.package().unwrap();
698 let config = pkg.config().unwrap().unwrap();
699 assert_eq!(config.date_time_format, DateTimeFormat::UnixSeconds);
700 }
701
702 #[test]
705 fn test_schema_with_x_resource_id_creates_feature() {
706 let doc = Document::from_yaml(indoc::indoc! {"
707 openapi: 3.0.0
708 info:
709 title: Test
710 version: 1.0.0
711 components:
712 schemas:
713 Customer:
714 type: object
715 x-resourceId: customer
716 properties:
717 id:
718 type: string
719 "})
720 .unwrap();
721
722 let arena = Arena::new();
723 let spec = Spec::from_doc(&arena, &doc).unwrap();
724 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
725 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
726
727 let features = manifest.features();
728 let keys = features.keys().copied().collect_vec();
729 assert_matches!(&*keys, ["customer", "default", "trace-context", "tracing"]);
730 }
731
732 #[test]
733 fn test_operation_with_x_resource_name_creates_feature() {
734 let doc = Document::from_yaml(indoc::indoc! {"
735 openapi: 3.0.0
736 info:
737 title: Test
738 version: 1.0.0
739 paths:
740 /pets:
741 get:
742 operationId: listPets
743 x-resource-name: pets
744 responses:
745 '200':
746 description: OK
747 "})
748 .unwrap();
749
750 let arena = Arena::new();
751 let spec = Spec::from_doc(&arena, &doc).unwrap();
752 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
753 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
754
755 let features = manifest.features();
756 let keys = features.keys().copied().collect_vec();
757 assert_matches!(&*keys, ["default", "pets", "trace-context", "tracing"]);
758 }
759
760 #[test]
761 fn test_resource_feature_names_deduplicate_numeric_case_collisions() {
762 let doc = Document::from_yaml(indoc::indoc! {"
763 openapi: 3.0.0
764 info:
765 title: Test
766 version: 1.0.0
767 paths:
768 /tokens:
769 get:
770 operationId: listTokens
771 x-resource-name: oauth_2_token
772 responses:
773 '200':
774 description: OK
775 components:
776 schemas:
777 OAuth2Token:
778 type: object
779 x-resourceId: oauth2Token
780 properties:
781 id:
782 type: string
783 "})
784 .unwrap();
785
786 let arena = Arena::new();
787 let spec = Spec::from_doc(&arena, &doc).unwrap();
788 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
789 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
790
791 let features = manifest.features();
792 let keys = features.keys().copied().collect_vec();
793 assert_matches!(
794 &*keys,
795 [
796 "default",
797 "oauth-2-token-2",
798 "oauth2-token",
799 "trace-context",
800 "tracing"
801 ]
802 );
803 assert_eq!(features["default"], ["oauth-2-token-2", "oauth2-token"]);
804 }
805
806 #[test]
807 fn test_resource_named_tracing_does_not_collide_with_tracing_feature() {
808 let doc = Document::from_yaml(indoc::indoc! {"
809 openapi: 3.0.0
810 info:
811 title: Test
812 version: 1.0.0
813 components:
814 schemas:
815 Trace:
816 type: object
817 x-resourceId: tracing
818 properties:
819 id:
820 type: string
821 "})
822 .unwrap();
823
824 let arena = Arena::new();
825 let spec = Spec::from_doc(&arena, &doc).unwrap();
826 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
827 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
828
829 let features = manifest.features();
832 let keys = features.keys().copied().collect_vec();
833 assert_matches!(&*keys, ["default", "trace-context", "tracing", "tracing-2"]);
834 assert_eq!(features["default"], ["tracing-2"]);
835 assert_eq!(features["tracing"], ["ploidy-util/tracing"]);
836 }
837
838 #[test]
839 fn test_unnamed_schema_creates_no_resource_features() {
840 let doc = Document::from_yaml(indoc::indoc! {"
841 openapi: 3.0.0
842 info:
843 title: Test
844 version: 1.0.0
845 components:
846 schemas:
847 Simple:
848 type: object
849 properties:
850 id:
851 type: string
852 "})
853 .unwrap();
854
855 let arena = Arena::new();
856 let spec = Spec::from_doc(&arena, &doc).unwrap();
857 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
858 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
859
860 let features = manifest.features();
861 let keys = features.keys().copied().collect_vec();
862 assert_matches!(&*keys, ["trace-context", "tracing"]);
863 }
864
865 #[test]
868 fn test_schema_dependency_creates_feature_dependency() {
869 let doc = Document::from_yaml(indoc::indoc! {"
870 openapi: 3.0.0
871 info:
872 title: Test
873 version: 1.0.0
874 components:
875 schemas:
876 Customer:
877 type: object
878 x-resourceId: customer
879 properties:
880 billing:
881 $ref: '#/components/schemas/BillingInfo'
882 BillingInfo:
883 type: object
884 x-resourceId: billing
885 properties:
886 card:
887 type: string
888 "})
889 .unwrap();
890
891 let arena = Arena::new();
892 let spec = Spec::from_doc(&arena, &doc).unwrap();
893 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
894 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
895
896 let features = manifest.features();
899 assert_eq!(features["customer"], ["billing"]);
900 }
901
902 #[test]
903 fn test_transitive_schema_dependency_creates_feature_dependency() {
904 let doc = Document::from_yaml(indoc::indoc! {"
905 openapi: 3.0.0
906 info:
907 title: Test
908 version: 1.0.0
909 components:
910 schemas:
911 Order:
912 type: object
913 x-resourceId: orders
914 properties:
915 customer:
916 $ref: '#/components/schemas/Customer'
917 Customer:
918 type: object
919 x-resourceId: customer
920 properties:
921 billing:
922 $ref: '#/components/schemas/BillingInfo'
923 BillingInfo:
924 type: object
925 x-resourceId: billing
926 properties:
927 card:
928 type: string
929 "})
930 .unwrap();
931
932 let arena = Arena::new();
933 let spec = Spec::from_doc(&arena, &doc).unwrap();
934 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
935 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
936
937 let features = manifest.features();
940 assert_eq!(features["orders"], ["billing", "customer"]);
941 }
942
943 #[test]
944 fn test_unnamed_dependency_does_not_create_feature_dependency() {
945 let doc = Document::from_yaml(indoc::indoc! {"
946 openapi: 3.0.0
947 info:
948 title: Test
949 version: 1.0.0
950 components:
951 schemas:
952 Customer:
953 type: object
954 x-resourceId: customer
955 properties:
956 address:
957 $ref: '#/components/schemas/Address'
958 Address:
959 type: object
960 properties:
961 street:
962 type: string
963 "})
964 .unwrap();
965
966 let arena = Arena::new();
967 let spec = Spec::from_doc(&arena, &doc).unwrap();
968 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
969 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
970
971 let features = manifest.features();
975 assert_matches!(&*features["customer"], &[]);
976 }
977
978 #[test]
979 fn test_feature_does_not_depend_on_itself() {
980 let doc = Document::from_yaml(indoc::indoc! {"
981 openapi: 3.0.0
982 info:
983 title: Test
984 version: 1.0.0
985 components:
986 schemas:
987 Node:
988 type: object
989 x-resourceId: nodes
990 properties:
991 children:
992 type: array
993 items:
994 $ref: '#/components/schemas/Node'
995 "})
996 .unwrap();
997
998 let arena = Arena::new();
999 let spec = Spec::from_doc(&arena, &doc).unwrap();
1000 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1001 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
1002
1003 let features = manifest.features();
1005 assert_matches!(&*features["nodes"], []);
1006 }
1007
1008 #[test]
1009 fn test_schema_dependency_on_own_resource_does_not_create_feature_dependency() {
1010 let doc = Document::from_yaml(indoc::indoc! {"
1011 openapi: 3.0.0
1012 info:
1013 title: Test
1014 version: 1.0.0
1015 components:
1016 schemas:
1017 Default:
1018 type: object
1019 x-resourceId: default
1020 properties:
1021 child:
1022 $ref: '#/components/schemas/DefaultChild'
1023 DefaultChild:
1024 type: object
1025 x-resourceId: default
1026 properties:
1027 id:
1028 type: string
1029 "})
1030 .unwrap();
1031
1032 let arena = Arena::new();
1033 let spec = Spec::from_doc(&arena, &doc).unwrap();
1034 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1035 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
1036
1037 let features = manifest.features();
1038 assert_matches!(&*features["default-2"], []);
1039 assert_matches!(&*features["default"], ["default-2"]);
1040 }
1041
1042 #[test]
1045 fn test_operation_type_dependency_creates_feature_dependency() {
1046 let doc = Document::from_yaml(indoc::indoc! {"
1047 openapi: 3.0.0
1048 info:
1049 title: Test
1050 version: 1.0.0
1051 paths:
1052 /orders:
1053 get:
1054 operationId: listOrders
1055 x-resource-name: orders
1056 responses:
1057 '200':
1058 description: OK
1059 content:
1060 application/json:
1061 schema:
1062 type: array
1063 items:
1064 $ref: '#/components/schemas/Order'
1065 components:
1066 schemas:
1067 Order:
1068 type: object
1069 properties:
1070 customer:
1071 $ref: '#/components/schemas/Customer'
1072 Customer:
1073 type: object
1074 x-resourceId: customer
1075 properties:
1076 id:
1077 type: string
1078 "})
1079 .unwrap();
1080
1081 let arena = Arena::new();
1082 let spec = Spec::from_doc(&arena, &doc).unwrap();
1083 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1084 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
1085
1086 let features = manifest.features();
1089 assert_eq!(features["orders"], ["customer"]);
1090 }
1091
1092 #[test]
1093 fn test_operation_with_unnamed_type_dependency_does_not_create_full_dependency() {
1094 let doc = Document::from_yaml(indoc::indoc! {"
1095 openapi: 3.0.0
1096 info:
1097 title: Test
1098 version: 1.0.0
1099 paths:
1100 /customers:
1101 get:
1102 operationId: listCustomers
1103 x-resource-name: customer
1104 responses:
1105 '200':
1106 description: OK
1107 content:
1108 application/json:
1109 schema:
1110 type: array
1111 items:
1112 $ref: '#/components/schemas/Customer'
1113 components:
1114 schemas:
1115 Customer:
1116 type: object
1117 properties:
1118 address:
1119 $ref: '#/components/schemas/Address'
1120 Address:
1121 type: object
1122 properties:
1123 street:
1124 type: string
1125 "})
1126 .unwrap();
1127
1128 let arena = Arena::new();
1129 let spec = Spec::from_doc(&arena, &doc).unwrap();
1130 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1131 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
1132
1133 let features = manifest.features();
1136 assert_matches!(&*features["customer"], []);
1137 }
1138
1139 #[test]
1140 fn test_operation_dependency_on_own_resource_does_not_create_feature_dependency() {
1141 let doc = Document::from_yaml(indoc::indoc! {"
1142 openapi: 3.0.0
1143 info:
1144 title: Test
1145 version: 1.0.0
1146 paths:
1147 /defaults:
1148 get:
1149 operationId: listDefaults
1150 x-resource-name: default
1151 responses:
1152 '200':
1153 description: OK
1154 content:
1155 application/json:
1156 schema:
1157 type: array
1158 items:
1159 $ref: '#/components/schemas/Default'
1160 components:
1161 schemas:
1162 Default:
1163 type: object
1164 x-resourceId: default
1165 properties:
1166 id:
1167 type: string
1168 "})
1169 .unwrap();
1170
1171 let arena = Arena::new();
1172 let spec = Spec::from_doc(&arena, &doc).unwrap();
1173 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1174 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
1175
1176 let features = manifest.features();
1177 assert_matches!(&*features["default-2"], []);
1178 assert_matches!(&*features["default"], ["default-2"]);
1179 }
1180
1181 #[test]
1184 fn test_diamond_dependency_deduplicates_feature() {
1185 let doc = Document::from_yaml(indoc::indoc! {"
1188 openapi: 3.0.0
1189 info:
1190 title: Test
1191 version: 1.0.0
1192 components:
1193 schemas:
1194 A:
1195 type: object
1196 x-resourceId: a
1197 properties:
1198 b:
1199 $ref: '#/components/schemas/B'
1200 c:
1201 $ref: '#/components/schemas/C'
1202 B:
1203 type: object
1204 x-resourceId: b
1205 properties:
1206 d:
1207 $ref: '#/components/schemas/D'
1208 C:
1209 type: object
1210 x-resourceId: c
1211 properties:
1212 d:
1213 $ref: '#/components/schemas/D'
1214 D:
1215 type: object
1216 x-resourceId: d
1217 properties:
1218 value:
1219 type: string
1220 "})
1221 .unwrap();
1222
1223 let arena = Arena::new();
1224 let spec = Spec::from_doc(&arena, &doc).unwrap();
1225 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1226 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
1227
1228 let features = manifest.features();
1229
1230 assert_eq!(features["a"], ["b", "c", "d"]);
1233
1234 assert_eq!(features["b"], ["d"]);
1236 assert_eq!(features["c"], ["d"]);
1237
1238 assert_matches!(&*features["d"], []);
1240 }
1241
1242 #[test]
1245 fn test_cycle_with_mixed_resources_does_not_create_feature_dependency() {
1246 let doc = Document::from_yaml(indoc::indoc! {"
1250 openapi: 3.0.0
1251 info:
1252 title: Test
1253 version: 1.0.0
1254 components:
1255 schemas:
1256 A:
1257 type: object
1258 x-resourceId: a
1259 properties:
1260 b:
1261 $ref: '#/components/schemas/B'
1262 B:
1263 type: object
1264 properties:
1265 c:
1266 $ref: '#/components/schemas/C'
1267 C:
1268 type: object
1269 x-resourceId: c
1270 properties:
1271 a:
1272 $ref: '#/components/schemas/A'
1273 "})
1274 .unwrap();
1275
1276 let arena = Arena::new();
1277 let spec = Spec::from_doc(&arena, &doc).unwrap();
1278 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1279 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
1280
1281 let features = manifest.features();
1282
1283 assert_eq!(features["a"], ["c"]);
1286
1287 assert_eq!(features["c"], ["a"]);
1289
1290 assert_eq!(features["default"], ["a", "c"]);
1292 }
1293
1294 #[test]
1295 fn test_cycle_with_all_named_resources_preserves_feature_members() {
1296 let doc = Document::from_yaml(indoc::indoc! {"
1299 openapi: 3.0.0
1300 info:
1301 title: Test
1302 version: 1.0.0
1303 components:
1304 schemas:
1305 A:
1306 type: object
1307 x-resourceId: a
1308 properties:
1309 b:
1310 $ref: '#/components/schemas/B'
1311 B:
1312 type: object
1313 x-resourceId: b
1314 properties:
1315 c:
1316 $ref: '#/components/schemas/C'
1317 C:
1318 type: object
1319 x-resourceId: c
1320 properties:
1321 a:
1322 $ref: '#/components/schemas/A'
1323 "})
1324 .unwrap();
1325
1326 let arena = Arena::new();
1327 let spec = Spec::from_doc(&arena, &doc).unwrap();
1328 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1329 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
1330
1331 let features = manifest.features();
1332
1333 assert_eq!(features["a"], ["b", "c"]);
1335
1336 assert_eq!(features["b"], ["a", "c"]);
1338
1339 assert_eq!(features["c"], ["a", "b"]);
1341
1342 assert_eq!(features["default"], ["a", "b", "c"]);
1344 }
1345
1346 #[test]
1349 fn test_default_feature_includes_all_other_features() {
1350 let doc = Document::from_yaml(indoc::indoc! {"
1351 openapi: 3.0.0
1352 info:
1353 title: Test
1354 version: 1.0.0
1355 paths:
1356 /pets:
1357 get:
1358 operationId: listPets
1359 x-resource-name: pets
1360 responses:
1361 '200':
1362 description: OK
1363 components:
1364 schemas:
1365 Customer:
1366 type: object
1367 x-resourceId: customer
1368 properties:
1369 id:
1370 type: string
1371 Order:
1372 type: object
1373 x-resourceId: orders
1374 properties:
1375 id:
1376 type: string
1377 "})
1378 .unwrap();
1379
1380 let arena = Arena::new();
1381 let spec = Spec::from_doc(&arena, &doc).unwrap();
1382 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1383 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
1384
1385 let features = manifest.features();
1388 assert_eq!(features["default"], ["customer", "orders", "pets"]);
1389 }
1390
1391 #[test]
1392 fn test_default_feature_includes_all_named_features() {
1393 let doc = Document::from_yaml(indoc::indoc! {"
1394 openapi: 3.0.0
1395 info:
1396 title: Test
1397 version: 1.0.0
1398 components:
1399 schemas:
1400 Customer:
1401 type: object
1402 x-resourceId: customer
1403 properties:
1404 id:
1405 type: string
1406 "})
1407 .unwrap();
1408
1409 let arena = Arena::new();
1410 let spec = Spec::from_doc(&arena, &doc).unwrap();
1411 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1412 let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
1413
1414 let features = manifest.features();
1416 assert_eq!(features["default"], ["customer"]);
1417 }
1418}