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