Skip to content

Protocol Macro Naming Convention ​

How Serve finds the methods generated by other derives.

The Convention ​

Each derive macro generates a method on the struct with a name derived from the macro name:

DeriveGenerated method
OpenApifn openapi() -> impl Layer
Metricsfn metrics() -> impl Layer
HealthCheckfn health_check() -> impl Layer
HttpMountfn http_mount_router(self: Arc<Self>) -> Router
FooExtfn foo_ext() -> impl Layer

The rule: derive name in PascalCase → method name in snake_case. OpenApi → openapi. HealthCheck → health_check. No prefix, no suffix.

Why This Matters ​

Serve coordinates all derives by parsing the #[derive(...)] list at compile time and generating wiring code that calls these methods:

rust
#[derive(ServerCore, OpenApi, Metrics, Serve)]
struct MyServer;

// Serve generates:
impl MyServer {
    pub async fn serve(self) {
        self.into_service()
            .layer(Self::openapi())
            .layer(Self::metrics())
            .run()
            .await
    }
}

Serve doesn't import anything from the other macros' crates. It just emits calls to Self::openapi() and Self::metrics() by name. If those methods exist, it compiles. If they don't, you get a method not found error at the call site, which points you at the missing derive.

This is the entire coordination mechanism. No registry, no inventory crate, no ctor-style linker tricks.

What Breaks Without It ​

If a third-party extension doesn't follow the convention, composition silently does the wrong thing:

rust
// Extension generates the method with a different name:
impl MyServer {
    pub fn my_extension_layer() -> impl Layer { ... }  // wrong name
}

#[derive(ServerCore, MyExtension, Serve)]
struct MyServer;

// Serve generates:
//   .layer(Self::my_extension())  // compile error: method not found

The failure is a compile error, not a silent runtime skip - which is good. But the error message (method not found for MyServer) points at the generated call site, not at the extension author's mistake. The user sees a confusing error from code they didn't write.

If an extension generates the right name but the wrong signature (wrong return type), the error is also at the call site. Still a compile error, still confusing.

Alternative: Trait-Based Discovery ​

The obvious alternative is a marker trait:

rust
pub trait ServerExtension {
    fn layer() -> impl tower::Layer<...>;
}

// Each derive implements it:
impl ServerExtension for MyServer { ... }

Serve would then call <MyServer as ServerExtension>::layer() instead of Self::openapi().

The problem: a single type can only implement a trait once. Multiple extensions on the same struct each need to contribute their own layer, so the trait would need associated types, HLists, or some other machinery to compose them. It also requires every extension author to depend on server-less-core to get the trait definition.

The naming convention avoids all of this. Each extension generates its own distinct method. There's no shared trait, no version coupling across crates, no orphan rule complications.

Why Naming Convention Wins ​

Naming conventionTrait-based
Extension crate depsNoneMust depend on server-less-core
Multiple extensions on one typeWorks (distinct method names)Requires HList or macro indirection
Error locationCall site in generated codeSame
Error claritymethod not foundtrait not implemented
Typo detectionCompile errorCompile error
Cross-crateWorksWorks (but needs shared dep)

The tradeoff: convention-based systems are fragile against typos, while trait-based systems are fragile against composition complexity. A single shared trait can't represent N different extensions on the same type without significant machinery. A naming convention can.

The Real Risk ​

The actual failure mode is subtle naming: foo_extension vs fooextension vs foo_ext. The convention says "PascalCase derive name → snake_case method name" but there's no mechanical check that an extension author followed it. If they get it wrong, the user gets a confusing error.

This is mitigated by:

  1. This document. Extension authors have a clear, documented target.
  2. Compile errors are immediate. The failure can't silently produce a wrong binary.
  3. cargo expand is the diagnostic. Users can inspect generated code to see what methods exist.
  4. A proc macro helper (future). A #[server_less_extension] attribute macro for extension authors could verify the generated method name matches the convention and emit a better error if not:
rust
// Future helper for extension authors:
#[server_less_extension]
#[proc_macro_derive(MyExtension)]
pub fn derive_my_extension(input: TokenStream) -> TokenStream {
    // macro helper verifies the generated method is named `my_extension`
}

This would catch the mistake at the extension crate's build time rather than at the user's.

Raw Identifiers ​

Rust allows method names that are keywords via raw identifier syntax (r#). Server-less strips the r# prefix before applying naming conventions:

rust
fn r#type(&self, ...) -> ...
// → subcommand name: "type"  (not "r#type")
// → HTTP path: /types        (not /r#types)

The generated protocol name is always clean — users of the CLI or HTTP API never see the r# prefix.

Summary ​

The naming convention is simple and works across crate boundaries without shared dependencies. It's deliberately fragile in one direction (typos cause compile errors) and deliberately robust in the other (no hidden runtime state, no dependency coupling, no composition limits). The alternative - a trait-based system - inverts the tradeoff: less fragile to naming mistakes, but significantly more complex to compose multiple extensions on a single type.

For a library whose central goal is to minimize machinery, the naming convention is the right call.