use toml_edit::{DocumentMut, Item, Value};
use crate::naming::Transport;
use crate::scaffold::Transform;
use crate::version::framework_req;
pub(crate) struct Dep {
name: &'static str,
workspace_value: &'static str,
features: &'static [&'static str],
}
const fn nest_rs(features: &'static [&'static str]) -> Dep {
Dep {
name: "nest-rs",
workspace_value: "",
features,
}
}
impl Dep {
fn workspace_item(&self) -> Item {
if self.name.starts_with("nest-rs") {
parse_value(&format!("\"{}\"", framework_req()))
} else {
parse_value(self.workspace_value)
}
}
}
pub(super) const SEAORM: Dep = nest_rs(&["seaorm", "http"]);
pub(super) const RESOURCE: Dep = nest_rs(&["resource"]);
pub(super) const GRAPHQL: Dep = nest_rs(&["graphql"]);
pub(super) const WS: Dep = nest_rs(&["ws"]);
pub(super) const SCHEDULE: Dep = nest_rs(&["schedule"]);
pub(super) const REDIS: Dep = nest_rs(&["redis"]);
pub(super) const MCP: Dep = nest_rs(&["mcp"]);
pub(super) const AUTHN: Dep = nest_rs(&["authn"]);
pub(super) const AUTHZ: Dep = nest_rs(&["authz", "http"]);
const SEA_ORM: Dep = Dep {
name: "sea-orm",
workspace_value: "{ version = \"2.0\", default-features = false, features = [\"sqlx-postgres\", \"runtime-tokio-rustls\", \"macros\", \"with-uuid\", \"with-chrono\"] }",
features: &[],
};
const UUID: Dep = Dep {
name: "uuid",
workspace_value: "{ version = \"1.24\", features = [\"v7\", \"serde\"] }",
features: &[],
};
const SERDE: Dep = Dep {
name: "serde",
workspace_value: "{ version = \"1.0\", features = [\"derive\"] }",
features: &[],
};
const ASYNC_GRAPHQL: Dep = Dep {
name: "async-graphql",
workspace_value: "{ version = \"7.2\", features = [\"dataloader\"] }",
features: &[],
};
const TRACING: Dep = Dep {
name: "tracing",
workspace_value: "\"0.1\"",
features: &[],
};
const ANYHOW: Dep = Dep {
name: "anyhow",
workspace_value: "\"1.0\"",
features: &[],
};
const SEA_ORM_MIGRATION: Dep = Dep {
name: "sea-orm-migration",
workspace_value: "{ version = \"2.0\", features = [\"sqlx-postgres\", \"runtime-tokio-rustls\"] }",
features: &[],
};
const TRACING_SUBSCRIBER: Dep = Dep {
name: "tracing-subscriber",
workspace_value: "{ version = \"0.3\", features = [\"env-filter\"] }",
features: &[],
};
const TOKIO: Dep = Dep {
name: "tokio",
workspace_value: "{ version = \"1.53\", features = [\"macros\", \"rt-multi-thread\"] }",
features: &[],
};
pub fn resource_deps() -> Vec<&'static Dep> {
vec![&SEAORM, &RESOURCE, &AUTHZ, &SEA_ORM, &SERDE]
}
pub fn auth_deps() -> Vec<&'static Dep> {
vec![&AUTHN, &AUTHZ, &SERDE, &UUID]
}
pub fn migrations_deps() -> Vec<&'static Dep> {
vec![
&SEAORM,
&SEA_ORM,
&SEA_ORM_MIGRATION,
&ANYHOW,
&TOKIO,
&TRACING_SUBSCRIBER,
]
}
pub fn adapter_deps(transport: Transport) -> Vec<&'static Dep> {
match transport {
Transport::Http => vec![],
Transport::Graphql => vec![&GRAPHQL, &ASYNC_GRAPHQL],
Transport::Ws => vec![&WS, &TRACING],
Transport::Queue => vec![&REDIS, &ANYHOW, &TRACING],
Transport::Schedule => vec![&SCHEDULE, &ANYHOW, &TRACING],
Transport::Mcp => vec![&MCP],
}
}
pub fn app_host_deps(transport: Transport) -> Vec<&'static Dep> {
match transport {
Transport::Http | Transport::Ws | Transport::Mcp => vec![],
Transport::Graphql => vec![&GRAPHQL],
Transport::Queue => vec![&REDIS],
Transport::Schedule => vec![&SCHEDULE],
}
}
pub fn graphql_port_deps() -> Vec<&'static Dep> {
vec![&RESOURCE, &GRAPHQL]
}
pub fn ensure_workspace_deps(deps: Vec<&'static Dep>) -> Transform {
ensure_deps(deps, &["workspace", "dependencies"], Dep::workspace_item)
}
pub fn ensure_features_deps(deps: Vec<&'static Dep>) -> Transform {
ensure_deps(deps, &["dependencies"], |_| Item::Value(workspace_value()))
}
fn ensure_deps(
deps: Vec<&'static Dep>,
path: &'static [&'static str],
missing: fn(&Dep) -> Item,
) -> Transform {
Box::new(move |content: &str| {
let mut doc = content.parse::<DocumentMut>().ok()?;
let mut table = doc.as_table_mut() as &mut dyn toml_edit::TableLike;
for key in path {
table = table
.entry(key)
.or_insert(toml_edit::table())
.as_table_like_mut()?;
}
let mut changed = false;
for dep in &deps {
if table.get(dep.name).is_none() {
table.insert(dep.name, missing(dep));
changed = true;
}
changed |= enable_features(table.get_mut(dep.name)?, dep.features);
}
changed.then(|| doc.to_string())
})
}
fn enable_features(entry: &mut Item, wanted: &[&str]) -> bool {
if wanted.is_empty() {
return false;
}
if let Some(version) = entry.as_str() {
let mut table = toml_edit::InlineTable::new();
table.insert("version", Value::from(version));
*entry = Item::Value(Value::InlineTable(table));
}
let Some(table) = entry.as_table_like_mut() else {
return false;
};
let mut features = table
.get("features")
.and_then(Item::as_array)
.cloned()
.unwrap_or_default();
let mut changed = false;
for feature in wanted {
if !features.iter().any(|f| f.as_str() == Some(*feature)) {
features.push(*feature);
changed = true;
}
}
if changed {
table.insert("features", Item::Value(Value::Array(features)));
if let Some(inline) = entry.as_value_mut().and_then(Value::as_inline_table_mut) {
inline.fmt();
}
}
changed
}
fn parse_value(raw: &str) -> Item {
format!("x = {raw}\n")
.parse::<DocumentMut>()
.ok()
.and_then(|frag| frag.get("x").cloned())
.unwrap_or_else(|| Item::Value(Value::from(raw)))
}
fn workspace_value() -> Value {
let mut table = toml_edit::InlineTable::new();
table.insert("workspace", Value::from(true));
Value::InlineTable(table)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ensures_workspace_dep_idempotently() {
let src = "[workspace.dependencies]\nanyhow = \"1\"\n";
let t = ensure_workspace_deps(vec![&SEAORM]);
let out = t(src).expect("adds nest-rs");
assert!(out.contains("nest-rs"), "{out}");
assert!(out.contains("seaorm"), "{out}");
assert!(ensure_workspace_deps(vec![&SEAORM])(&out).is_none());
}
#[test]
fn ensures_features_dep_with_features() {
let src = "[dependencies]\nanyhow.workspace = true\n";
let out = ensure_features_deps(vec![&SEAORM])(src).expect("adds dep");
assert!(out.contains("nest-rs"));
assert!(out.contains("workspace = true"));
assert!(out.contains("\"http\""));
}
#[test]
fn enables_a_missing_feature_on_a_dependency_already_declared() {
let src = "[dependencies]\nnest-rs.workspace = true\n";
let out = ensure_features_deps(vec![&GRAPHQL])(src).expect("enables graphql");
assert!(out.contains("graphql"), "{out}");
assert!(
ensure_features_deps(vec![&GRAPHQL])(&out).is_none(),
"a second run is a no-op: {out}",
);
let doc = out.parse::<DocumentMut>().expect("still valid TOML");
assert_eq!(
doc["dependencies"]["nest-rs"]["workspace"].as_bool(),
Some(true),
"the existing keys survive: {out}",
);
}
#[test]
fn enabling_a_feature_keeps_the_ones_already_listed() {
let src = "[dependencies]\nnest-rs = { workspace = true, features = [\"http\"] }\n";
let out = ensure_features_deps(vec![&SEAORM, &GRAPHQL])(src).expect("adds graphql");
assert!(
out.contains("\"http\"") && out.contains("\"graphql\""),
"{out}"
);
}
#[test]
fn a_skeleton_that_names_a_crate_declares_it() {
const NAMED: &[(&str, &str)] = &[
("tracing::", "tracing"),
("anyhow::", "anyhow"),
("use anyhow", "anyhow"),
("serde::", "serde"),
("use serde", "serde"),
("async_graphql::", "async-graphql"),
("use async_graphql", "async-graphql"),
("rmcp::", "rmcp"),
];
for transport in Transport::ALL {
let declared: Vec<&str> = adapter_deps(transport).iter().map(|d| d.name).collect();
for crud_port in [false, true] {
let (handler, module) =
crate::commands::generate::adapter::templates_for(transport, crud_port);
let extra = if transport == Transport::Queue {
crate::templates::adapter::QUEUE_COMMAND
} else {
""
};
let src = format!("{handler}{module}{extra}");
for (token, krate) in NAMED {
if !src.contains(token) {
continue;
}
let reexported = src.contains(&format!("::{};", krate.replace('-', "_")));
assert!(
declared.contains(krate) || reexported,
"the {} skeleton writes `{token}` but `nestrs g {}` neither adds \
`{krate}` nor imports it through the framework — the first \
`cargo check` after generating fails",
transport.folder(),
transport.folder(),
);
}
}
}
}
fn rendered_handler(transport: Transport, crud_port: bool) -> String {
let names = crate::naming::Names::parse("posts");
let (handler, _) = crate::commands::generate::adapter::templates_for(transport, crud_port);
let mut r = crate::scaffold::Renderer::new(&names)
.with("handler", names.handler_for(transport))
.with("handler_mod", transport.handler_mod())
.with("tmodule", names.module_for(transport));
for (key, value) in crate::templates::crud_vars(crud_port, transport) {
r = r.with(key, value);
}
r.render(handler)
}
#[test]
fn no_crud_port_skeleton_calls_the_plain_features_count() {
for transport in Transport::ALL {
let rendered = rendered_handler(transport, true);
assert!(
!rendered.contains("svc.count()"),
"the {} adapter renders `svc.count()` over a resource port, which a \
CrudService does not have:
{rendered}",
transport.folder(),
);
}
}
#[test]
fn the_plain_feature_skeletons_still_delegate_to_the_port() {
for transport in [Transport::Http, Transport::Graphql, Transport::Ws] {
let rendered = rendered_handler(transport, false);
assert!(
rendered.contains("svc.count()"),
"the {} adapter over a `g feature` port should still show the delegation:
\
{rendered}",
transport.folder(),
);
}
}
#[test]
fn versions_are_major_minor() {
const EXACT: [&str; 2] = ["async-graphql", "async-graphql-poem"];
let repo = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let manifests = [
"Cargo.toml",
"demo/Cargo.toml",
"bench/sut/nestrs/Cargo.toml",
];
let mut checked = 0usize;
for rel in manifests {
let path = repo.join(rel);
let Ok(raw) = std::fs::read_to_string(&path) else {
continue; };
let doc = raw.parse::<DocumentMut>().expect("valid TOML");
let tables = [
doc.get("workspace").and_then(|w| w.get("dependencies")),
doc.get("dependencies"),
doc.get("dev-dependencies"),
doc.get("build-dependencies"),
];
for table in tables.into_iter().flatten() {
let Some(table) = table.as_table_like() else {
continue;
};
for (name, entry) in table.iter() {
if name.starts_with("nest-rs") || EXACT.contains(&name) {
continue;
}
let req = match entry.as_str() {
Some(literal) => literal,
None => match entry.get("version").and_then(Item::as_str) {
Some(version) => version,
None => continue,
},
};
checked += 1;
assert_eq!(
req.trim_start_matches('=').split('.').count(),
2,
"{rel}: `{name} = \"{req}\"` — third-party requirements are \
`major.minor`; a bare major accepts releases we never built \
against, a patch component rejects the fixes we want",
);
}
}
}
assert!(checked > 0, "no manifest was reachable to check");
}
#[test]
fn consumers_name_only_the_umbrella() {
let repo = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let manifests = [
"demo/Cargo.toml",
"demo/apps/api/Cargo.toml",
"demo/apps/assistant/Cargo.toml",
"demo/apps/auth/Cargo.toml",
"demo/apps/live/Cargo.toml",
"demo/apps/worker/Cargo.toml",
"demo/crates/features/Cargo.toml",
"demo/crates/migrations/Cargo.toml",
"demo/crates/seed/Cargo.toml",
"bench/sut/nestrs/Cargo.toml",
"crates/nest-rs-macro-hygiene/Cargo.toml",
];
let mut checked = 0usize;
for rel in manifests {
let path = repo.join(rel);
let Ok(raw) = std::fs::read_to_string(&path) else {
continue; };
let doc = raw.parse::<DocumentMut>().expect("valid TOML");
let tables = [
doc.get("workspace").and_then(|w| w.get("dependencies")),
doc.get("dependencies"),
doc.get("dev-dependencies"),
doc.get("build-dependencies"),
];
for table in tables.into_iter().flatten() {
let Some(table) = table.as_table_like() else {
continue;
};
for (name, _) in table.iter() {
if !name.starts_with("nest-rs") {
continue;
}
checked += 1;
assert_eq!(
name, "nest-rs",
"{rel}: declares `{name}`. A consumer names the umbrella and \
nothing else — enable the capability's feature on `nest-rs` \
instead. See *The umbrella is the front door* in CLAUDE.md",
);
}
}
}
assert!(checked > 0, "no consumer manifest was reachable to check");
}
#[test]
fn a_version_pinned_dependency_is_widened_to_carry_features() {
let src = "[dependencies]\nnest-rs = \"1.1\"\n";
let out = ensure_features_deps(vec![&GRAPHQL])(src).expect("widens the entry");
let doc = out.parse::<DocumentMut>().expect("still valid TOML");
assert_eq!(
doc["dependencies"]["nest-rs"]["version"].as_str(),
Some("1.1"),
"the pin survives: {out}",
);
assert!(out.contains("graphql"), "{out}");
}
}