use std::{
collections::{BTreeMap, BTreeSet},
error::Error as StdError,
fmt::{Debug, Display},
ops::Range,
path::Path,
};
use itertools::Itertools;
use miette::SourceSpan;
use ploidy_core::{codegen::Code, ir::View};
use semver::Version;
use serde::{Deserialize, de::IntoDeserializer};
use toml_edit::{Array, DocumentMut, InlineTable, Table, TableLike, value};
use super::{config::CodegenConfig, graph::CodegenGraph, naming::CargoFeature};
const PLOIDY_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Clone, Debug)]
pub struct CodegenCargoManifest<'a> {
graph: &'a CodegenGraph<'a>,
manifest: &'a CargoManifest,
}
impl<'a> CodegenCargoManifest<'a> {
#[inline]
pub fn new(graph: &'a CodegenGraph<'a>, manifest: &'a CargoManifest) -> Self {
Self { graph, manifest }
}
pub fn to_manifest(self) -> CargoManifest {
let features = {
let mut deps_by_feature = BTreeMap::new();
for schema in self.graph.schemas() {
let feature = match schema.resource().map(CargoFeature::from_name) {
Some(CargoFeature::Named(name)) => CargoFeature::Named(name),
_ => continue,
};
let entry: &mut BTreeSet<_> = deps_by_feature.entry(feature).or_default();
for dep in schema.dependencies().filter_map(|ty| {
match CargoFeature::from_name(ty.into_schema().ok()?.resource()?) {
CargoFeature::Named(name) => Some(CargoFeature::Named(name)),
CargoFeature::Default => None,
}
}) {
entry.insert(dep);
}
}
for op in self.graph.operations() {
let feature = match op.resource().map(CargoFeature::from_name) {
Some(CargoFeature::Named(name)) => CargoFeature::Named(name),
_ => continue,
};
let entry = deps_by_feature.entry(feature).or_default();
for dep in op.dependencies().filter_map(|ty| {
match CargoFeature::from_name(ty.into_schema().ok()?.resource()?) {
CargoFeature::Named(name) => Some(CargoFeature::Named(name)),
CargoFeature::Default => None,
}
}) {
entry.insert(dep);
}
}
let mut features: BTreeMap<_, _> = deps_by_feature
.iter()
.map(|(feature, deps)| {
(
feature.display().to_string(),
FeatureDependencies(
deps.iter()
.map(|dep| dep.display().to_string())
.collect_vec(),
),
)
})
.collect();
if features.is_empty() {
BTreeMap::new()
} else {
features.insert(
"default".to_owned(),
FeatureDependencies(
deps_by_feature
.keys()
.map(|feature| feature.display().to_string())
.collect_vec(),
),
);
features
}
};
self.manifest.clone().apply(CargoManifestDiff {
edition: Some(RustEdition::E2024),
dependencies: Some(BTreeMap::from_iter([
(
"ploidy-util".to_owned(),
Dependency::Simple(PLOIDY_VERSION.parse().unwrap()),
),
])),
features: Some(features),
..Default::default()
})
}
}
impl Code for CodegenCargoManifest<'_> {
fn path(&self) -> &str {
"Cargo.toml"
}
fn into_string(self) -> miette::Result<String> {
Ok(self.to_manifest().to_string())
}
}
#[derive(Clone, Debug)]
pub struct CargoManifest(DocumentMut);
impl CargoManifest {
pub fn new(name: &str, version: Version) -> Self {
let package = Table::from_iter([
("name", value(name)),
("version", value(version.to_string())),
("edition", value(RustEdition::E2024)),
]);
let manifest = Table::from_iter([("package", package)]);
Self(manifest.into())
}
pub fn from_disk(path: &Path) -> Result<Self, CargoManifestError> {
let contents = std::fs::read_to_string(path)?;
Self::parse(&contents)
}
pub fn parse(s: &str) -> Result<Self, CargoManifestError> {
Ok(Self(s.parse().map_err(
|source: toml_edit::TomlError| {
let span = source.span().map(SourceSpan::from);
SpannedError {
source: Box::new(source),
code: s.to_owned(),
span,
}
},
)?))
}
#[inline]
pub fn package(&self) -> Option<Package<'_>> {
let package = self.0.get("package")?.as_table_like()?;
let name = package.get("name")?;
let version = package.get("version")?;
Some(Package {
name: SpannedValue::new(name.as_str()?, &self.0, name.span()),
version: SpannedValue::new(version.as_str()?, &self.0, version.span()),
metadata: package
.get("metadata")
.and_then(|meta| Some((meta.as_table_like()?, meta.span())))
.map(|(meta, range)| SpannedValue::new(meta, &self.0, range)),
})
}
pub fn features(&self) -> BTreeMap<&str, Vec<&str>> {
self.0
.get("features")
.and_then(|features| features.as_table_like())
.into_iter()
.flat_map(|features| features.iter())
.map(|(name, item)| {
let deps = item
.as_array()
.into_iter()
.flat_map(|deps| deps.iter())
.filter_map(|dep| dep.as_str())
.collect_vec();
(name, deps)
})
.collect()
}
pub fn apply(mut self, diff: CargoManifestDiff) -> Self {
let package = &mut self.0["package"];
if let Some(name) = diff.name {
package["name"] = value(name);
}
if let Some(version) = diff.version {
package["version"] = value(version.to_string());
}
if let Some(edition) = diff.edition {
package["edition"] = value(edition);
}
if let Some(deps) = diff.dependencies.filter(|f| !f.is_empty()) {
let table = self.0["dependencies"].or_insert(Table::new().into());
for (name, dep) in deps {
dep.merge_into(&mut table[&name]);
}
}
if let Some(features) = diff.features.filter(|f| !f.is_empty()) {
let table = self.0["features"].or_insert(Table::new().into());
for (name, feature) in features {
feature.merge_into(&mut table[&name]);
}
}
self
}
}
impl Display for CargoManifest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Copy)]
pub struct Package<'a> {
name: SpannedValue<'a, &'a str>,
version: SpannedValue<'a, &'a str>,
metadata: Option<SpannedValue<'a, &'a dyn TableLike>>,
}
impl<'a> Package<'a> {
pub fn name(&self) -> &'a str {
self.name.value
}
pub fn version(&self) -> Result<Version, SpannedError<PackageError>> {
Version::parse(self.version.value).map_err(|err| SpannedError {
source: Box::new(PackageError::from(err)),
code: self.version.source.to_string(),
span: self.version.span,
})
}
pub fn config(&self) -> Result<Option<CodegenConfig>, SpannedError<PackageError>> {
let meta = match self.metadata {
Some(meta) => meta,
None => return Ok(None),
};
let table: Table = match meta.value.get("ploidy").and_then(|v| v.as_table_like()) {
Some(table) => table.iter().collect(),
None => return Ok(None),
};
let value: toml_edit::Value = table.into_inline_table().into();
let config =
CodegenConfig::deserialize(value.into_deserializer()).map_err(|err| SpannedError {
source: Box::new(PackageError::from(err)),
code: meta.source.to_string(),
span: meta.span,
})?;
Ok(Some(config))
}
}
impl Debug for Package<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Package")
.field("name", &self.name)
.field("version", &self.version)
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, Debug)]
struct SpannedValue<'a, T> {
source: &'a DocumentMut,
value: T,
span: Option<SourceSpan>,
}
impl<'a, T> SpannedValue<'a, T> {
fn new(value: T, source: &'a DocumentMut, range: Option<Range<usize>>) -> Self {
Self {
source,
value,
span: range.map(SourceSpan::from),
}
}
}
#[derive(Debug, miette::Diagnostic)]
pub struct SpannedError<E: StdError + Send + Sync + 'static> {
source: Box<E>,
#[source_code]
code: String,
#[label]
span: Option<SourceSpan>,
}
impl<E: StdError + Send + Sync + 'static> Display for SpannedError<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.source, f)
}
}
impl<E: StdError + Send + Sync + 'static> StdError for SpannedError<E> {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source.source()
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RustEdition {
E2021,
#[default]
E2024,
}
impl From<RustEdition> for toml_edit::Value {
fn from(edition: RustEdition) -> Self {
toml_edit::Value::from(match edition {
RustEdition::E2021 => "2021",
RustEdition::E2024 => "2024",
})
}
}
#[derive(Clone, Debug, Default)]
pub struct CargoManifestDiff {
pub name: Option<String>,
pub version: Option<Version>,
pub edition: Option<RustEdition>,
pub dependencies: Option<BTreeMap<String, Dependency>>,
pub features: Option<BTreeMap<String, FeatureDependencies>>,
}
#[derive(Clone, Debug)]
pub enum Dependency {
Simple(Version),
Detailed(DependencyDetail),
}
impl Dependency {
fn merge_into(self, entry: &mut toml_edit::Item) {
match self {
Dependency::Simple(version) => {
if let Some(table) = entry.as_table_like_mut() {
table.insert("version", value(version.to_string()));
} else {
*entry = value(version.to_string());
}
}
Dependency::Detailed(detail) => {
let table = match entry.as_table_like_mut() {
Some(table) => table,
None => {
*entry = InlineTable::new().into();
entry.as_table_like_mut().unwrap()
}
};
table.insert("version", value(detail.version.to_string()));
if let Some(path) = detail.path {
table.insert("path", value(path));
}
}
}
}
}
#[derive(Clone, Debug)]
pub struct DependencyDetail {
pub version: Version,
pub path: Option<String>,
}
#[derive(Clone, Debug)]
pub struct FeatureDependencies(Vec<String>);
impl FeatureDependencies {
fn merge_into(self, entry: &mut toml_edit::Item) {
match entry.as_array_mut() {
Some(array) => {
let existing: BTreeSet<_> = array.iter().filter_map(|dep| dep.as_str()).collect();
let new = self
.0
.into_iter()
.filter(|dep| !existing.contains(dep.as_str()))
.collect_vec();
array.extend(new);
}
None => {
*entry = Array::from_iter(self.0).into();
}
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum CargoManifestError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Parse(#[from] SpannedError<toml_edit::TomlError>),
}
#[derive(Debug, thiserror::Error)]
pub enum PackageError {
#[error(transparent)]
Deserialize(#[from] toml_edit::de::Error),
#[error(transparent)]
Semver(#[from] semver::Error),
}
#[cfg(test)]
mod tests {
use super::*;
use ploidy_core::{
arena::Arena,
ir::{RawGraph, Spec},
parse::Document,
};
use crate::{config::DateTimeFormat, tests::assert_matches};
fn default_manifest() -> CargoManifest {
CargoManifest::new("test-client", Version::new(0, 1, 0))
}
#[test]
fn test_new_manifest_has_package_name_version_and_edition() {
assert_eq!(
CargoManifest::new("my-crate", Version::new(1, 0, 0)).to_string(),
indoc::indoc! {r#"
[package]
name = "my-crate"
version = "1.0.0"
edition = "2024"
"#},
);
}
#[test]
fn test_package_returns_none_for_workspace() {
let manifest = CargoManifest::parse(indoc::indoc! {r#"
[workspace]
members = ["a"]
"#})
.unwrap();
assert!(manifest.package().is_none());
}
#[test]
fn test_apply_sets_name() {
let manifest = CargoManifest::new("old", Version::new(1, 0, 0)).apply(CargoManifestDiff {
name: Some("new".to_owned()),
..Default::default()
});
assert_eq!(manifest.package().unwrap().name.value, "new");
}
#[test]
fn test_apply_sets_version() {
let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0)).apply(CargoManifestDiff {
version: Some(Version::new(2, 0, 0)),
..Default::default()
});
assert_eq!(manifest.package().unwrap().version.value, "2.0.0");
}
#[test]
fn test_apply_sets_edition() {
let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0)).apply(CargoManifestDiff {
edition: Some(RustEdition::E2021),
..Default::default()
});
assert_eq!(
manifest.to_string(),
indoc::indoc! {r#"
[package]
name = "pkg"
version = "1.0.0"
edition = "2021"
"#},
);
}
#[test]
fn test_apply_sets_simple_dependency() {
let mut deps = BTreeMap::new();
deps.insert(
"serde".to_owned(),
Dependency::Simple(Version::new(1, 0, 0)),
);
let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0)).apply(CargoManifestDiff {
dependencies: Some(deps),
..Default::default()
});
assert_eq!(
manifest.to_string(),
indoc::indoc! {r#"
[package]
name = "pkg"
version = "1.0.0"
edition = "2024"
[dependencies]
serde = "1.0.0"
"#},
);
}
#[test]
fn test_apply_sets_detailed_dependency() {
let mut deps = BTreeMap::new();
deps.insert(
"ploidy-util".to_owned(),
Dependency::Detailed(DependencyDetail {
version: Version::new(0, 10, 0),
path: Some("../ploidy-util".to_owned()),
}),
);
let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0)).apply(CargoManifestDiff {
dependencies: Some(deps),
..Default::default()
});
assert_eq!(
manifest.to_string(),
indoc::indoc! {r#"
[package]
name = "pkg"
version = "1.0.0"
edition = "2024"
[dependencies]
ploidy-util = { version = "0.10.0", path = "../ploidy-util" }
"#},
);
}
#[test]
fn test_apply_preserves_existing_dependencies() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
paths: {}
"})
.unwrap();
let manifest = default_manifest().apply(CargoManifestDiff {
dependencies: Some({
let mut deps = BTreeMap::new();
deps.insert(
"serde".to_owned(),
Dependency::Simple(Version::new(1, 0, 0)),
);
deps
}),
..Default::default()
});
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &manifest).to_manifest();
assert_eq!(
manifest.to_string(),
indoc::formatdoc! {r#"
[package]
name = "test-client"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = "1.0.0"
ploidy-util = "{PLOIDY_VERSION}"
"#},
);
}
#[test]
fn test_apply_sets_features() {
let mut features = BTreeMap::new();
features.insert(
"default".to_owned(),
FeatureDependencies(vec!["customer".to_owned()]),
);
features.insert("customer".to_owned(), FeatureDependencies(vec![]));
let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0)).apply(CargoManifestDiff {
features: Some(features),
..Default::default()
});
let f = manifest.features();
assert_eq!(f["default"], vec!["customer"]);
assert_eq!(f["customer"], Vec::<String>::new());
}
#[test]
fn test_apply_preserves_untouched_fields() {
let manifest = CargoManifest::parse(indoc::indoc! {r#"
[package]
name = "pkg"
version = "1.0.0"
edition = "2021"
[profile.release]
lto = true
"#})
.unwrap()
.apply(CargoManifestDiff {
edition: Some(RustEdition::E2024),
..Default::default()
});
assert_eq!(
manifest.to_string(),
indoc::indoc! {r#"
[package]
name = "pkg"
version = "1.0.0"
edition = "2024"
[profile.release]
lto = true
"#},
);
}
#[test]
fn test_config_returns_none_when_absent() {
let manifest = CargoManifest::new("pkg", Version::new(1, 0, 0));
let pkg = manifest.package().unwrap();
assert_matches!(pkg.config(), Ok(None));
}
#[test]
fn test_config_deserializes_codegen_config() {
let manifest = CargoManifest::parse(indoc::indoc! {r#"
[package]
name = "pkg"
version = "1.0.0"
edition = "2024"
[package.metadata.ploidy]
date-time-format = "unix-seconds"
"#})
.unwrap();
let pkg = manifest.package().unwrap();
let config = pkg.config().unwrap().unwrap();
assert_eq!(config.date_time_format, DateTimeFormat::UnixSeconds);
}
#[test]
fn test_schema_with_x_resource_id_creates_feature() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Customer:
type: object
x-resourceId: customer
properties:
id:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
let keys = features.keys().copied().collect_vec();
assert_matches!(&*keys, ["customer", "default"]);
}
#[test]
fn test_operation_with_x_resource_name_creates_feature() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
paths:
/pets:
get:
operationId: listPets
x-resource-name: pets
responses:
'200':
description: OK
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
let keys = features.keys().copied().collect_vec();
assert_matches!(&*keys, ["default", "pets"]);
}
#[test]
fn test_unnamed_schema_creates_no_features() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Simple:
type: object
properties:
id:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
let keys = features.keys().copied().collect_vec();
assert_matches!(&*keys, []);
}
#[test]
fn test_schema_dependency_creates_feature_dependency() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Customer:
type: object
x-resourceId: customer
properties:
billing:
$ref: '#/components/schemas/BillingInfo'
BillingInfo:
type: object
x-resourceId: billing
properties:
card:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
assert_eq!(features["customer"], ["billing"]);
}
#[test]
fn test_transitive_schema_dependency_creates_feature_dependency() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Order:
type: object
x-resourceId: orders
properties:
customer:
$ref: '#/components/schemas/Customer'
Customer:
type: object
x-resourceId: customer
properties:
billing:
$ref: '#/components/schemas/BillingInfo'
BillingInfo:
type: object
x-resourceId: billing
properties:
card:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
assert_eq!(features["orders"], ["billing", "customer"]);
}
#[test]
fn test_unnamed_dependency_does_not_create_feature_dependency() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Customer:
type: object
x-resourceId: customer
properties:
address:
$ref: '#/components/schemas/Address'
Address:
type: object
properties:
street:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
assert_matches!(&*features["customer"], &[]);
}
#[test]
fn test_feature_does_not_depend_on_itself() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Node:
type: object
x-resourceId: nodes
properties:
children:
type: array
items:
$ref: '#/components/schemas/Node'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
assert_matches!(&*features["nodes"], []);
}
#[test]
fn test_operation_type_dependency_creates_feature_dependency() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
paths:
/orders:
get:
operationId: listOrders
x-resource-name: orders
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Order'
components:
schemas:
Order:
type: object
properties:
customer:
$ref: '#/components/schemas/Customer'
Customer:
type: object
x-resourceId: customer
properties:
id:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
assert_eq!(features["orders"], ["customer"]);
}
#[test]
fn test_operation_with_unnamed_type_dependency_does_not_create_full_dependency() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
paths:
/customers:
get:
operationId: listCustomers
x-resource-name: customer
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Customer'
components:
schemas:
Customer:
type: object
properties:
address:
$ref: '#/components/schemas/Address'
Address:
type: object
properties:
street:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
assert_matches!(&*features["customer"], []);
}
#[test]
fn test_diamond_dependency_deduplicates_feature() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
x-resourceId: a
properties:
b:
$ref: '#/components/schemas/B'
c:
$ref: '#/components/schemas/C'
B:
type: object
x-resourceId: b
properties:
d:
$ref: '#/components/schemas/D'
C:
type: object
x-resourceId: c
properties:
d:
$ref: '#/components/schemas/D'
D:
type: object
x-resourceId: d
properties:
value:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
assert_eq!(features["a"], ["b", "c", "d"]);
assert_eq!(features["b"], ["d"]);
assert_eq!(features["c"], ["d"]);
assert_matches!(&*features["d"], []);
}
#[test]
fn test_cycle_with_mixed_resources_does_not_create_feature_dependency() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
x-resourceId: a
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
properties:
c:
$ref: '#/components/schemas/C'
C:
type: object
x-resourceId: c
properties:
a:
$ref: '#/components/schemas/A'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
assert_eq!(features["a"], ["c"]);
assert_eq!(features["c"], ["a"]);
assert_eq!(features["default"], ["a", "c"]);
}
#[test]
fn test_cycle_with_all_named_resources_creates_mutual_dependencies() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
A:
type: object
x-resourceId: a
properties:
b:
$ref: '#/components/schemas/B'
B:
type: object
x-resourceId: b
properties:
c:
$ref: '#/components/schemas/C'
C:
type: object
x-resourceId: c
properties:
a:
$ref: '#/components/schemas/A'
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
assert_eq!(features["a"], ["b", "c"]);
assert_eq!(features["b"], ["a", "c"]);
assert_eq!(features["c"], ["a", "b"]);
assert_eq!(features["default"], ["a", "b", "c"]);
}
#[test]
fn test_default_feature_includes_all_other_features() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
paths:
/pets:
get:
operationId: listPets
x-resource-name: pets
responses:
'200':
description: OK
components:
schemas:
Customer:
type: object
x-resourceId: customer
properties:
id:
type: string
Order:
type: object
x-resourceId: orders
properties:
id:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
assert_eq!(features["default"], ["customer", "orders", "pets"]);
}
#[test]
fn test_default_feature_includes_all_named_features() {
let doc = Document::from_yaml(indoc::indoc! {"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
schemas:
Customer:
type: object
x-resourceId: customer
properties:
id:
type: string
"})
.unwrap();
let arena = Arena::new();
let spec = Spec::from_doc(&arena, &doc).unwrap();
let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
let features = manifest.features();
assert_eq!(features["default"], ["customer"]);
}
}