fix: harden API macro expansion hygiene

This commit is contained in:
2026-09-17 23:21:18 +09:00
parent 9590cf332d
commit 633f43530c
8 changed files with 207 additions and 35 deletions
+2
View File
@@ -58,6 +58,8 @@
//! explicit variants for future transport work; accepting one requires a deliberate macro and
//! adapter change rather than silently treating it as JSON.
extern crate self as api_macros;
pub use api_macros_impl::api;
/// A transport-neutral HTTP method.
+8
View File
@@ -35,6 +35,12 @@ pub trait WrapperApi {
async fn search(&self) -> SearchResult<Widget>;
}
#[api]
pub trait RawIdentifierApi {
#[get("/type", operation_id = "raw.type")]
async fn r#type(&self) -> Widget;
}
fn assert_operation_types<O>()
where
O: Operation<
@@ -52,6 +58,8 @@ fn expansion_exposes_deterministic_metadata_and_type_connections() {
fn assert_named_wrapper<O: Operation<ResponseBody = SearchResult<Widget>>>() {}
assert_named_wrapper::<wrapper_api_operations::Search>();
let raw = <raw_identifier_api_operations::Type as Operation>::METADATA;
assert_eq!(raw.operation_id, "raw.type");
let operations = <WidgetApiMetadata as ApiContract>::OPERATIONS;
assert_eq!(operations.len(), 3);
@@ -0,0 +1,66 @@
use std::{fs, path::PathBuf, process::Command};
#[test]
fn macro_expansion_resolves_a_renamed_support_dependency() {
let fixture = std::env::temp_dir().join(format!(
"api-macros-renamed-dependency-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&fixture);
fs::create_dir_all(fixture.join("src")).expect("create fixture source directory");
let package_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let package_path = package_path
.to_str()
.expect("package path is UTF-8")
.replace('\\', "\\\\");
fs::write(
fixture.join("Cargo.toml"),
format!(
r#"[package]
name = "renamed-api-consumer"
version = "0.0.0"
edition = "2024"
[workspace]
[dependencies]
yoi-api = {{ package = "api-macros", path = "{package_path}" }}
"#,
),
)
.expect("write fixture manifest");
fs::write(
fixture.join("src/lib.rs"),
r#"use yoi_api::{ApiContract, api};
pub struct Output;
#[api]
pub trait RenamedApi {
#[get("/output", operation_id = "renamed.output")]
async fn output(&self) -> Output;
}
pub fn operation_count() -> usize {
<RenamedApiMetadata as ApiContract>::OPERATIONS.len()
}
"#,
)
.expect("write fixture source");
let output = Command::new(env!("CARGO"))
.args(["check", "--quiet", "--offline"])
.current_dir(&fixture)
.env("CARGO_TARGET_DIR", fixture.join("target"))
.output()
.expect("run cargo check for renamed dependency fixture");
let _ = fs::remove_dir_all(&fixture);
assert!(
output.status.success(),
"renamed dependency fixture failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
@@ -0,0 +1,14 @@
use api_macros::api;
pub struct Response;
#[api]
pub trait MarkerCollision {
#[get("/first")]
async fn foo_bar(&self) -> Response;
#[get("/second")]
async fn foo__bar(&self) -> Response;
}
fn main() {}
@@ -0,0 +1,11 @@
error: generated operation marker `FooBar` collides with method `foo_bar`
--> tests/ui/marker_collision.rs:11:14
|
11 | async fn foo__bar(&self) -> Response;
| ^^^^^^^^
error: first method generating marker `FooBar`
--> tests/ui/marker_collision.rs:8:14
|
8 | async fn foo_bar(&self) -> Response;
| ^^^^^^^