Skip to content

Commit 73c9e78

Browse files
committed
rust: targeted attribute injection via additional_type_attributes/additional_member_attributes
1 parent 93a5f4e commit 73c9e78

5 files changed

Lines changed: 497 additions & 22 deletions

File tree

crates/guest-rust/macro/src/lib.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ impl Parse for Config {
127127
opts.additional_derive_ignore =
128128
list.into_iter().map(|i| i.value()).collect()
129129
}
130+
Opt::AdditionalTypeAttributes(list) => opts.additional_type_attributes = list,
131+
Opt::AdditionalMemberAttributes(list) => {
132+
opts.additional_member_attributes = list
133+
}
130134
Opt::With(with) => opts.with.extend(with),
131135
Opt::GenerateAll => {
132136
opts.generate_all = true;
@@ -237,7 +241,7 @@ fn parse_source(
237241
}
238242
}
239243
}
240-
pkgs.truncate(0);
244+
pkgs.clear();
241245
pkgs.push(resolve.push_str("macro-input", s)?);
242246
}
243247
Some(Source::Paths(p)) => parse(p)?,
@@ -312,6 +316,8 @@ mod kw {
312316
syn::custom_keyword!(export_prefix);
313317
syn::custom_keyword!(additional_derives);
314318
syn::custom_keyword!(additional_derives_ignore);
319+
syn::custom_keyword!(additional_type_attributes);
320+
syn::custom_keyword!(additional_member_attributes);
315321
syn::custom_keyword!(with);
316322
syn::custom_keyword!(generate_all);
317323
syn::custom_keyword!(type_section_suffix);
@@ -394,6 +400,8 @@ enum Opt {
394400
// Parse as paths so we can take the concrete types/macro names rather than raw strings
395401
AdditionalDerives(Vec<syn::Path>),
396402
AdditionalDerivesIgnore(Vec<syn::LitStr>),
403+
AdditionalTypeAttributes(Vec<(String, String)>),
404+
AdditionalMemberAttributes(Vec<(String, String)>),
397405
With(HashMap<String, WithOption>),
398406
GenerateAll,
399407
TypeSectionSuffix(syn::LitStr),
@@ -522,6 +530,26 @@ impl Parse for Opt {
522530
syn::bracketed!(contents in input);
523531
let list = Punctuated::<_, Token![,]>::parse_terminated(&contents)?;
524532
Ok(Opt::AdditionalDerivesIgnore(list.iter().cloned().collect()))
533+
} else if l.peek(kw::additional_type_attributes) {
534+
input.parse::<kw::additional_type_attributes>()?;
535+
input.parse::<Token![:]>()?;
536+
let contents;
537+
braced!(contents in input);
538+
let fields: Punctuated<_, Token![,]> =
539+
contents.parse_terminated(attr_map_field_parse, Token![,])?;
540+
Ok(Opt::AdditionalTypeAttributes(
541+
fields.into_iter().flatten().collect(),
542+
))
543+
} else if l.peek(kw::additional_member_attributes) {
544+
input.parse::<kw::additional_member_attributes>()?;
545+
input.parse::<Token![:]>()?;
546+
let contents;
547+
braced!(contents in input);
548+
let fields: Punctuated<_, Token![,]> =
549+
contents.parse_terminated(attr_map_field_parse, Token![,])?;
550+
Ok(Opt::AdditionalMemberAttributes(
551+
fields.into_iter().flatten().collect(),
552+
))
525553
} else if l.peek(kw::with) {
526554
input.parse::<kw::with>()?;
527555
input.parse::<Token![:]>()?;
@@ -601,6 +629,29 @@ impl Parse for Opt {
601629
}
602630
}
603631

632+
// Parse one `"selector": ["#[attr]", ...]` entry into a (selector, attribute) pair
633+
// per attribute.
634+
fn attr_map_field_parse(input: ParseStream<'_>) -> Result<Vec<(String, String)>> {
635+
let selector = input.parse::<syn::LitStr>()?;
636+
input.parse::<Token![:]>()?;
637+
let contents;
638+
let bracket = syn::bracketed!(contents in input);
639+
let attrs = Punctuated::<syn::LitStr, Token![,]>::parse_terminated(&contents)?;
640+
// An empty list would otherwise flatten away silently and escape the
641+
// unused-selector check in the generator.
642+
if attrs.is_empty() {
643+
return Err(Error::new(
644+
bracket.span.join(),
645+
"attribute list must not be empty",
646+
));
647+
}
648+
let selector = selector.value();
649+
Ok(attrs
650+
.into_iter()
651+
.map(|a| (selector.clone(), a.value()))
652+
.collect())
653+
}
654+
604655
fn with_field_parse(input: ParseStream<'_>) -> Result<(String, WithOption)> {
605656
let interface = input.parse::<syn::LitStr>()?.value();
606657
input.parse::<Token![:]>()?;

crates/guest-rust/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,31 @@ extern crate std;
682682
/// // By default this set is empty.
683683
/// additional_derives: [PartialEq, Eq, Hash, Clone],
684684
///
685+
/// // Extra attributes to emit on specific generated types (records, variants,
686+
/// // and enums), rather than on all types like `additional_derives`. A type is
687+
/// // selected by its bare kebab name, its package (`my:pkg`), its owning
688+
/// // interface (`my:pkg/types`), or its fully-qualified name; the qualified
689+
/// // forms written as in `with`, carrying the `@version` when versioned. An
690+
/// // injected `#[derive(...)]` folds into the generated derive (deduped); other
691+
/// // attributes are emitted verbatim, on every form including the borrowed one
692+
/// // under `Borrowing` (so owned-only derives fail there). See the CLI docs for
693+
/// // the full grammar.
694+
/// //
695+
/// // By default this map is empty.
696+
/// additional_type_attributes: {
697+
/// "my-record": [r#"#[derive(serde::Serialize)]"#], // one type, by bare name
698+
/// "my:pkg/types": [r#"#[derive(Clone)]"#], // every type in an interface
699+
/// },
700+
///
701+
/// // Like `additional_type_attributes`, but for generated record fields and
702+
/// // enum/variant cases, selected by `<type-selector>.member-name` (any type
703+
/// // selector above) or a bare `member-name`.
704+
/// //
705+
/// // By default this map is empty.
706+
/// additional_member_attributes: {
707+
/// "my-record.my-field": [r#"#[serde(rename = "mf")]"#],
708+
/// },
709+
///
685710
/// // When generating bindings for interfaces that are not defined in the
686711
/// // same package as `world`, this option can be used to either generate
687712
/// // those bindings or point to already generated bindings.

0 commit comments

Comments
 (0)