fix(interface): preserve operation item uniqueness
Disambiguate generated argument types when distinct operation names normalize to the same PascalCase form, and add the requested Number round-trip and collision regression coverage.
This commit is contained in:
@@ -112,6 +112,36 @@ fn expand_interface(attribute: TokenStream2, item: ItemTrait) -> syn::Result<Tok
|
|||||||
operations.push(operation);
|
operations.push(operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PascalCase normalization is convenient for the public generated item,
|
||||||
|
// but distinct Rust names such as `item` and `item_` normalize identically.
|
||||||
|
// Add a stable source-name suffix to every member of a colliding group so
|
||||||
|
// otherwise-valid operations always produce distinct public Rust items.
|
||||||
|
let mut seen_argument_items = HashSet::new();
|
||||||
|
let mut colliding_argument_items = HashSet::new();
|
||||||
|
for operation in &operations {
|
||||||
|
let name = operation.arguments_ident.to_string();
|
||||||
|
if !seen_argument_items.insert(name.clone()) {
|
||||||
|
colliding_argument_items.insert(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for operation in &mut operations {
|
||||||
|
if colliding_argument_items.contains(&operation.arguments_ident.to_string()) {
|
||||||
|
let suffix = operation
|
||||||
|
.name
|
||||||
|
.as_bytes()
|
||||||
|
.iter()
|
||||||
|
.map(|byte| format!("{byte:02x}"))
|
||||||
|
.collect::<String>();
|
||||||
|
operation.arguments_ident = format_ident!(
|
||||||
|
"{}{}ArgumentsN{}",
|
||||||
|
trait_ident,
|
||||||
|
pascal_case(&operation.name),
|
||||||
|
suffix,
|
||||||
|
span = operation.ident.span()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let argument_items = operations.iter().map(argument_item);
|
let argument_items = operations.iter().map(argument_item);
|
||||||
let operation_declarations = operations.iter().map(operation_declaration);
|
let operation_declarations = operations.iter().map(operation_declaration);
|
||||||
let declaration_collectors = operations.iter().flat_map(operation_collectors);
|
let declaration_collectors = operations.iter().flat_map(operation_collectors);
|
||||||
|
|||||||
@@ -67,6 +67,17 @@ fn supported_values_round_trip() {
|
|||||||
assert!(fields.iter().all(|field| field.name != "priority"));
|
assert!(fields.iter().all(|field| field.name != "priority"));
|
||||||
assert_eq!(Request::decode(encoded).unwrap(), request);
|
assert_eq!(Request::decode(encoded).unwrap(), request);
|
||||||
|
|
||||||
|
let number = 42.5_f64;
|
||||||
|
assert_eq!(f64::type_expr(), TypeExpr::Number);
|
||||||
|
assert_eq!(f64::decode(number.encode().unwrap()).unwrap(), number);
|
||||||
|
|
||||||
|
let narrow_number = 1.5_f32;
|
||||||
|
assert_eq!(f32::type_expr(), TypeExpr::Number);
|
||||||
|
assert_eq!(
|
||||||
|
f32::decode(narrow_number.encode().unwrap()).unwrap(),
|
||||||
|
narrow_number
|
||||||
|
);
|
||||||
|
|
||||||
for color in [Color::Red, Color::Blue] {
|
for color in [Color::Red, Color::Blue] {
|
||||||
let encoded = color.clone().encode().unwrap();
|
let encoded = color.clone().encode().unwrap();
|
||||||
let decoded = Color::decode(encoded).unwrap();
|
let decoded = Color::decode(encoded).unwrap();
|
||||||
|
|||||||
@@ -216,3 +216,41 @@ fn typed_dispatch_validates_decodes_invokes_encodes_and_validates() {
|
|||||||
let unknown = CatalogDefinition::dispatch(&AcceptingCatalog, "missing", vec![]).unwrap_err();
|
let unknown = CatalogDefinition::dispatch(&AcceptingCatalog, "missing", vec![]).unwrap_err();
|
||||||
assert!(matches!(unknown, DispatchError::UnknownOperation(name) if name == "missing"));
|
assert!(matches!(unknown, DispatchError::UnknownOperation(name) if name == "missing"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Operations whose distinct source names normalize to the same PascalCase.
|
||||||
|
#[interface]
|
||||||
|
pub trait DistinctOperations {
|
||||||
|
/// Returns the first item marker.
|
||||||
|
fn item(&self) -> OperationResult<String>;
|
||||||
|
|
||||||
|
/// Returns the underscored item marker.
|
||||||
|
fn item_(&self) -> OperationResult<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DistinctImplementation;
|
||||||
|
|
||||||
|
impl DistinctOperations for DistinctImplementation {
|
||||||
|
fn item(&self) -> OperationResult<String> {
|
||||||
|
Ok("item".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item_(&self) -> OperationResult<String> {
|
||||||
|
Ok("item_".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn distinct_operation_names_always_generate_distinct_public_items() {
|
||||||
|
let descriptor = DistinctOperationsDefinition::descriptor();
|
||||||
|
assert_eq!(descriptor.operations[0].name, "item");
|
||||||
|
assert_eq!(descriptor.operations[1].name, "item_");
|
||||||
|
|
||||||
|
let first = DistinctOperationsDefinition::dispatch(&DistinctImplementation, "item", vec![])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let second = DistinctOperationsDefinition::dispatch(&DistinctImplementation, "item_", vec![])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(first, Value::String("item".into()));
|
||||||
|
assert_eq!(second, Value::String("item_".into()));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user