macro: Validate custom handler signatures and clarify dispatch errors#19
macro: Validate custom handler signatures and clarify dispatch errors#19
Conversation
Handler functions registered via `#[contract(encode_input = "…")]`, `decode_input`, or `decode_output` are spliced into the generated `data_driver` module and called directly by the dispatch match arms. A handler with the wrong shape (argument count, argument type, return type) used to slip past the macro and fail downstream with a cryptic type error against code the user didn't write. Add a compile-time validator that runs over each extracted handler and emits a clear `compile_error!` at the handler's definition naming the handler, the role, and the expected signature. The canonical per-role signature is now centralized in `data_driver::handler_signature` so the validator and the dispatch code that calls handlers can't drift apart.
The three data-driver dispatch sites (`encode_input_fn`,
`decode_input_fn`, `decode_output_fn`) used to return the same vague
`"custom handler required: {fn}"` when a method marked `#[contract(custom)]`
was dispatched without a matching handler. The user couldn't tell which
of the three roles they were missing a handler for, nor the signature
that role expects.
Replace each site with a role-tailored message that names the role
(`encode_input` / `decode_input` / `decode_output`) and the expected
handler signature in concrete types, so the reader can fix the handler
from the error alone. The message comes from
`data_driver::handler_signature_display`, the same source of truth the
compile-time handler validator uses.
HDauven
left a comment
There was a problem hiding this comment.
LGTM, but there's two edge cases I think we need to handle. I'll leave it up to you to decide whether they're follow-ups or not.
| /// equality. | ||
| fn types_match(expected: &Type, actual: &Type) -> bool { | ||
| match (expected, actual) { | ||
| (Type::Reference(e), Type::Reference(a)) => { |
There was a problem hiding this comment.
Not handling lifetime explicitly here means &'static str is accepted as matching &str, but the generated dispatcher cannot satisfy a 'static borrow. So this still allows for a downstream expansion failure after validation already said ok.
To fix that you'd need to make the rule as follows:
- No explicit lifetime: ok
- Lifetime is generic on the handler: ok
- Lifetime is
'static: reject
|
|
||
| #[test] | ||
| fn test_custom_handler_encode_input_short_error_path() { | ||
| // `use dusk_data_driver::Error;` → user writes `Error` bare. |
There was a problem hiding this comment.
I do not think the expansion works as intended here.
I tried the following minimal example:
use dusk_forge_contract::contract;
#[contract]
mod my_contract {
use dusk_data_driver::{Error, JsonValue};
pub struct MyContract {
value: u64,
}
impl MyContract {
pub const fn new() -> Self {
Self { value: 0 }
}
#[contract(custom)]
pub fn get_value(&self) -> u64 {
self.value
}
}
#[contract(decode_output = "get_value")]
fn decode_value(bytes: &[u8]) -> Result<JsonValue, Error> {
let _ = bytes;
unimplemented!()
}
}Which fails with:
error[E0425]: cannot find type `JsonValue` in this scope
error[E0425]: cannot find type `Error` in this scope
So these short-path forms are accepted by validation but fail on expansion once the handler is moved into the generated data_driver module.
Summary
Two coupled fixes to the
contract-macro/diagnostic surface around custom data-driver handlers. Supersedes task 189 (re-scope).1. Compile-time handler signature validation
Handler functions registered via
#[contract(encode_input = "…")],decode_input, ordecode_outputare spliced into the generateddata_drivermodule and called directly by the dispatch match arms. A handler with the wrong shape (argument count, argument type, return type) used to slip past the macro and fail downstream with a cryptic type error against code the user didn't write.Adds a validator that runs over each extracted handler and emits a clear
compile_error!at the handler's definition naming the handler, the role, and the expected signature. Types are compared structurally —Vec<u8>(afteruse alloc::vec::Vec) orError(afteruse dusk_data_driver::Error) are accepted, whilefoo::ErrorandMyErrorare rejected.2. Role-specific runtime errors at the three dispatch sites
The three data-driver dispatch sites (
encode_input_fn,decode_input_fn,decode_output_fn) used to return the same vague"custom handler required: {fn}"when a method marked#[contract(custom)]was dispatched without a matching handler. The user couldn't tell which of the three roles they were missing a handler for, nor the signature that role expects.Each site now emits a role-tailored message that names the role and the expected handler signature in concrete types, so the reader can fix the handler from the error alone.
Single source of truth
Both the validator and the dispatch error messages pull the canonical per-role signature from
data_driver::handler_signature, so the two can't drift apart.Tests
validate::custom_handler— positive regression per role plus negatives for wrong arg count, wrong arg type, wrong return type, missing return, self receiver,foo::Errorprefix,MyErrorlast segment,&mut strmutability.data_driverunit tests updated (not deleted) to assert on the new role-specific error content.Vec<u8>,Error,JsonValue,&'static str).test-contractintegration tests (2 handlers, encode_input + decode_output) still pass — no regression for existing users.