use std::path::PathBuf;
use super::auth;
use super::cargo::{
adapter_deps, app_host_deps, auth_deps, ensure_features_deps, ensure_workspace_deps,
graphql_authz_deps, graphql_port_deps,
};
use super::support::{finish, resolve_start, wire_into_app};
use crate::context::{Context, NestrsWorkspace};
use crate::error::{CliError, CliResult};
use crate::naming::{Names, Transport, command_file};
use crate::scaffold::{
Renderer, Scaffold, ensure_expose_graphql, ensure_lines, ensure_module_imports,
};
use crate::templates::adapter;
pub struct AdapterOptions {
pub name: String,
pub path: Option<PathBuf>,
pub dry_run: bool,
}
pub fn run(transport: Transport, opts: AdapterOptions) -> CliResult<()> {
let ctx = Context::detect(&resolve_start(opts.path))?;
let ws = ctx.workspace.clone().ok_or(CliError::NotNestrsWorkspace)?;
crate::naming::validate_feature_name(&opts.name).map_err(CliError::InvalidFeatureName)?;
let names = Names::parse(&opts.name);
if !ws.feature_exists(&names.snake) {
return Err(CliError::FeatureNotFound {
name: names.snake.clone(),
});
}
let feature_root = ws.feature_root(&names.snake);
let dir = feature_root.join(transport.folder());
if dir.exists() {
return Err(CliError::AdapterExists {
transport: transport.folder(),
name: names.snake.clone(),
path: dir,
});
}
let handler = names.handler_for(transport);
let tmodule = names.module_for(transport);
let is_graphql = transport == Transport::Graphql;
let crud_port = is_crud_port(&ws, &names.snake);
let mut r = Renderer::new(&names)
.with("handler", handler.clone())
.with("handler_mod", transport.handler_mod())
.with("tmodule", tmodule.clone());
for (key, value) in crate::templates::crud_vars(crud_port, transport) {
r = r.with(key, value);
}
let (handler_tmpl, module_tmpl) = templates_for(transport, crud_port);
let mut s = Scaffold::new();
s.create(dir.join(transport.handler_file()), r.render(handler_tmpl));
let mut module_rs = r.render(module_tmpl);
let authz_bridge = match (crud_port, transport) {
(true, Transport::Graphql) => {
Some(("crate::authz::AuthzGraphqlModule", "AuthzGraphqlModule"))
}
(true, Transport::Http) => Some(("crate::authz::AuthzHttpModule", "AuthzHttpModule")),
_ => None,
};
if let Some(bridge) = authz_bridge
&& let Some(wired) = ensure_module_imports(&[bridge])(&module_rs)
{
module_rs = wired;
}
s.create(dir.join("module.rs"), module_rs);
s.create(dir.join("mod.rs"), r.render(adapter::MOD));
let scaffolded_auth = is_graphql && crud_port && !auth::exists(&ws);
let graphql_authz =
is_graphql && (crud_port || auth::exists(&ws)) && !auth::graphql_exists(&ws);
if graphql_authz {
auth::queue_graphql(&mut s, &ws);
}
if scaffolded_auth {
auth::queue(&mut s, &ws, auth::graphql_decls());
} else if graphql_authz {
s.edit(
ws.features_root().join("authz/mod.rs"),
ensure_lines(auth::graphql_decls()),
);
}
let mut port_lines = Vec::new();
if transport == Transport::Queue {
s.create(
feature_root.join(command_file(&opts.name, 1)),
r.render(adapter::QUEUE_COMMAND),
);
port_lines.push("mod command;".to_string());
port_lines.push(format!(
"pub use command::{{{}, {}}};",
names.command(),
names.queue_name()
));
}
let mut deps = adapter_deps(transport);
if graphql_authz {
deps.extend(graphql_authz_deps());
}
if is_graphql && crud_port {
deps.extend(graphql_port_deps());
let entity = feature_root.join("entity.rs");
if entity.is_file() {
s.edit(entity, ensure_expose_graphql());
}
}
if scaffolded_auth {
deps.extend(auth_deps());
}
if !deps.is_empty() {
s.edit(
ws.root.join("Cargo.toml"),
ensure_workspace_deps(deps.clone()),
);
s.edit(ws.features_cargo(), ensure_features_deps(deps));
}
if scaffolded_auth {
s.edit(ws.features_lib(), ensure_lines(auth::lib_decls()));
}
port_lines.push(format!("pub mod {};", transport.folder()));
port_lines.push(format!(
"pub use {}::{{{}, {}}};",
transport.folder(),
handler,
tmodule
));
s.edit(feature_root.join("mod.rs"), ensure_lines(port_lines));
let use_path = format!("features::{}::{}", names.snake, tmodule);
let mut imports = vec![(use_path.as_str(), tmodule.as_str())];
if graphql_authz {
imports.push(("features::authz::AuthzGraphqlModule", "AuthzGraphqlModule"));
}
if scaffolded_auth {
imports.extend(auth::APP_IMPORTS);
}
let wired_app = wire_into_app(&ctx, &mut s, &imports, None);
if wired_app.is_some()
&& let Some(app) = ctx.current_app.as_ref()
{
s.edit(
app.join("Cargo.toml"),
ensure_features_deps(app_host_deps(transport)),
);
}
finish(
s,
opts.dry_run,
&ws.root,
&format!("the {} adapter for `{}`", transport.folder(), names.snake),
)?;
print_next_steps(
&ctx,
transport,
&names,
&tmodule,
wired_app.is_some(),
graphql_authz,
);
Ok(())
}
fn is_crud_port(ws: &NestrsWorkspace, snake: &str) -> bool {
std::fs::read_to_string(ws.feature_root(snake).join("service.rs"))
.is_ok_and(|src| src.contains("impl CrudService for"))
}
pub(crate) fn templates_for(transport: Transport, crud_port: bool) -> (&'static str, &'static str) {
match (transport, crud_port) {
(Transport::Graphql, true) => (adapter::GRAPHQL_RESOLVER_CRUD, adapter::MODULE),
(Transport::Http, true) => (crate::templates::resource::HTTP_CONTROLLER, adapter::MODULE),
(Transport::Queue, _) => (adapter::QUEUE_PROCESSOR, adapter::MODULE),
(Transport::Http, false) => (adapter::HTTP_CONTROLLER, adapter::MODULE),
(Transport::Graphql, false) => (adapter::GRAPHQL_RESOLVER, adapter::MODULE),
(Transport::Ws, _) => (adapter::WS_GATEWAY, adapter::WS_MODULE),
(Transport::Schedule, _) => (adapter::SCHEDULE_TASKS, adapter::MODULE),
(Transport::Mcp, _) => (adapter::MCP_TOOL, adapter::MODULE),
}
}
fn host_module(transport: Transport) -> &'static str {
match transport {
Transport::Http | Transport::Ws => "HttpModule",
Transport::Graphql => "GraphqlModule",
Transport::Queue => "QueueWorkerModule",
Transport::Schedule => "ScheduleModule",
Transport::Mcp => "HttpModule",
}
}
fn print_next_steps(
ctx: &Context,
transport: Transport,
names: &Names,
tmodule: &str,
wired: bool,
graphql_authz: bool,
) {
println!();
println!("Next steps:");
if wired {
println!(" {tmodule} is wired into the current app.");
println!(
" Make sure the app imports {} so the adapter is served.",
host_module(transport)
);
} else if ctx.current_app.is_some() {
println!(
" Import `features::{}::{}` in this app (needs {}).",
names.snake,
tmodule,
host_module(transport)
);
} else {
println!(
" Import `features::{}::{}` in an app that has {}.",
names.snake,
tmodule,
host_module(transport)
);
}
if matches!(transport, Transport::Mcp) {
println!(
" Security: the MCP endpoint denies all requests until you bind an \
McpOperationGuard. Wire `McpAbilityBridge` (features::authz::mcp) so \
callers are authenticated and the ambient Ability is installed."
);
}
if graphql_authz {
println!();
println!("Also created the GraphQL authz adapter (crates/features/src/authz/graphql/):");
println!(" /graphql has no guard at the HTTP edge — authn and the ability run in band,");
println!(" per operation, through AuthzGraphqlModule. Every resolver serving rows");
println!(" imports it and declares #[authorize(Action, Entity)] or #[public].");
}
}