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