Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rs/nns/governance/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ DEPENDENCIES = [
MACRO_DEPENDENCIES = [
# Keep sorted.
"//rs/nervous_system/common/build_metadata",
"//rs/nns/governance/derive_self_describing",
"@crate_index//:async-trait",
"@crate_index//:rust_decimal_macros",
"@crate_index//:strum_macros",
Expand Down
1 change: 1 addition & 0 deletions rs/nns/governance/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ ic-nns-common = { path = "../common" }
ic-nns-constants = { path = "../constants" }
ic-nns-gtc-accounts = { path = "../gtc_accounts" }
ic-nns-governance-api = { path = "./api" }
ic-nns-governance-derive-self-describing = { path = "./derive_self_describing" }
ic-nns-governance-init = { path = "./init" }
ic-nns-handler-root-interface = { path = "../handlers/root/interface" }
ic-node-rewards-canister-api = { path = "../../node_rewards/canister/api" }
Expand Down
18 changes: 18 additions & 0 deletions rs/nns/governance/derive_self_describing/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
load("@rules_rust//rust:defs.bzl", "rust_proc_macro")

package(default_visibility = ["//visibility:public"])

DEPENDENCIES = [
# Keep sorted.
"@crate_index//:proc-macro2",
"@crate_index//:quote",
"@crate_index//:syn",
]

rust_proc_macro(
name = "derive_self_describing",
srcs = glob(["src/**/*.rs"]),
crate_name = "ic_nns_governance_derive_self_describing",
version = "0.9.0",
deps = DEPENDENCIES,
)
15 changes: 15 additions & 0 deletions rs/nns/governance/derive_self_describing/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "ic-nns-governance-derive-self-describing"
version.workspace = true
authors.workspace = true
edition.workspace = true
description.workspace = true
documentation.workspace = true

[lib]
proc-macro = true

[dependencies]
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true }
223 changes: 223 additions & 0 deletions rs/nns/governance/derive_self_describing/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
//! A derive macro for automatically implementing `From<T> for SelfDescribingValue`.
//!
//! This macro generates an implementation that converts a struct or enum into a `SelfDescribingValue`.
//!
//! For structs:
//! - Named fields: creates a map where each field name is a key
//! - Unit structs: NOT supported (compile error)
//! - Tuple structs: NOT supported (compile error)
//!
//! For enums:
//! - If all variants are unit types: uses `Text(VariantName)`
//! - Otherwise:
//! - Single-field tuple variants: map `{ "VariantName": inner_value }`
//! - Unit variants: map `{ "VariantName": [] }`
//! - Multi-field tuple variants: NOT supported (compile error)
//! - Named field variants: NOT supported (compile error)
//!
//! # Example
//!
//! ```ignore
//! use ic_nns_governance_derive_self_describing::SelfDescribing;
//!
//! #[derive(SelfDescribing)]
//! struct MyStruct {
//! name: String,
//! count: u64,
//! }
//!
//! #[derive(SelfDescribing)]
//! enum MyEnum {
//! VariantA(InnerA),
//! VariantB(InnerB),
//! }
//! ```

extern crate proc_macro;

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Data, DataEnum, DeriveInput, Fields, FieldsNamed, Ident, parse_macro_input};

/// Derives `From<T> for SelfDescribingValue` for a struct or enum.
///
/// For structs:
/// - Named fields: Creates a map with field names as keys
/// - Unit structs: Creates an empty array
/// - Tuple structs: NOT supported (compile error)
///
/// For enums:
/// - If all variants are unit types: uses `Text(VariantName)`
/// - Otherwise:
/// - Single-field tuple variants: Map with variant name as key and inner value as value
/// - Unit variants: Map with variant name as key and empty array as value
/// - Multi-field tuple variants: NOT supported (compile error)
/// - Named field variants: NOT supported (compile error)
#[proc_macro_derive(SelfDescribing)]
pub fn derive_self_describing(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;

let expanded = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(fields_named) => derive_struct_with_named_fields(name, fields_named),
Fields::Unnamed(_) => {
// We could have supported this with an array, but we don't expect it to be useful.
// It can be extended in the future if needed.
return syn::Error::new_spanned(
&input,
"SelfDescribing does not support tuple structs. Use a struct with named fields instead.",
)
.to_compile_error()
.into();
}
Fields::Unit => {
// We could have supported this with an empty array or map, but we don't expect
// it to be useful. It can be extended in the future if needed.
return syn::Error::new_spanned(
&input,
"SelfDescribing does not support unit structs. Use a struct with named fields instead.",
)
.to_compile_error()
.into();
}
},
Data::Enum(data_enum) => {
let all_unit = data_enum
.variants
.iter()
.all(|v| matches!(v.fields, Fields::Unit));

if all_unit {
derive_all_unit_enum(name, data_enum)
} else {
derive_mixed_enum(name, data_enum).unwrap_or_else(|err| err)
}
}
Data::Union(_) => {
return syn::Error::new_spanned(&input, "SelfDescribing does not support unions")
.to_compile_error()
.into();
}
};

TokenStream::from(expanded)
}

/// Generates `From` impl for a struct with named fields.
/// Creates a map where each field name is a key.
fn derive_struct_with_named_fields(name: &Ident, fields_named: &FieldsNamed) -> TokenStream2 {
let field_additions = fields_named.named.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_name_str = field_name.to_string();
quote! {
.add_field(#field_name_str, value.#field_name)
}
});

quote! {
impl From<#name> for crate::pb::v1::SelfDescribingValue {
fn from(value: #name) -> Self {
crate::proposals::self_describing::ValueBuilder::new()
#(#field_additions)*
.build()
}
}
}
}

/// Generates `From` impl for an enum where all variants are unit types.
/// Uses `Text(VariantName)` for each variant.
fn derive_all_unit_enum(name: &Ident, data_enum: &DataEnum) -> TokenStream2 {
let match_arms = data_enum.variants.iter().map(|variant| {
let variant_name = &variant.ident;
let variant_name_str = variant_name.to_string();

quote! {
#name::#variant_name => {
crate::pb::v1::SelfDescribingValue::from(#variant_name_str)
}
}
});

quote! {
impl From<#name> for crate::pb::v1::SelfDescribingValue {
fn from(value: #name) -> Self {
match value {
#(#match_arms)*
}
}
}
}
}

/// Generates `From` impl for a mixed enum (not all unit variants).
/// - Single-field tuple variants: map with variant name as key, inner value as value
/// - Unit variants: map with variant name as key, empty array as value
///
/// Returns an error if the enum contains unsupported variants (multi-field tuples or named fields).
fn derive_mixed_enum(name: &Ident, data_enum: &DataEnum) -> Result<TokenStream2, TokenStream2> {
let match_arms: Vec<_> = data_enum
.variants
.iter()
.map(|variant| {
let variant_name = &variant.ident;
let variant_name_str = variant_name.to_string();

match &variant.fields {
Fields::Unnamed(fields) if fields.unnamed.len() > 1 => {
let field_count = fields.unnamed.len();
// We could have supported this with an array, but we don't expect it to
// be useful. It can be extended in the future if needed.
let error_msg = format!(
"SelfDescribing does not support enum variants with multiple tuple fields. \
Variant `{}` has {} fields.",
variant_name, field_count
);
Err(syn::Error::new_spanned(variant, error_msg).to_compile_error())
}
Fields::Unnamed(_) => {
// Single field: map with variant name as key, inner value as value
Ok(quote! {
#name::#variant_name(inner) => {
crate::proposals::self_describing::ValueBuilder::new()
.add_field(#variant_name_str, inner)
.build()
}
})
}
Fields::Unit => {
// Unit variant in mixed enum: map with variant name as key, empty array as value
Ok(quote! {
#name::#variant_name => {
crate::proposals::self_describing::ValueBuilder::new()
.add_empty_field(#variant_name_str)
.build()
}
})
}
Fields::Named(_) => {
// We could have supported this with a map, but we don't expect it to
// be useful. It can be extended in the future if needed.
let error_msg = format!(
"SelfDescribing does not support enum variants with named fields. \
Variant `{}` has named fields.",
variant_name
);
Err(syn::Error::new_spanned(variant, error_msg).to_compile_error())
}
}
})
.collect::<Result<_, _>>()?;

Ok(quote! {
impl From<#name> for crate::pb::v1::SelfDescribingValue {
fn from(value: #name) -> Self {
match value {
#(#match_arms)*
}
}
}
})
}
13 changes: 13 additions & 0 deletions rs/nns/governance/protobuf_generator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,19 @@ pub fn generate_prost_files(proto: ProtoPaths<'_>, out: &Path) {
"#[compare_default]",
);

let self_describing_types = vec![
"NetworkEconomics",
"NeuronsFundEconomics",
"NeuronsFundMatchedFundingCurveCoefficients",
"VotingPowerEconomics",
];
for type_name in self_describing_types {
config.type_attribute(
format!(".ic_nns_governance.pb.v1.{type_name}"),
"#[derive(ic_nns_governance_derive_self_describing::SelfDescribing)]",
);
}

// Add serde_bytes for efficiently parsing blobs.
let blob_fields = vec![
"NeuronStakeTransfer.from_subaccount",
Expand Down
7 changes: 6 additions & 1 deletion rs/nns/governance/src/gen/ic_nns_governance.pb.v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1919,7 +1919,9 @@ pub struct WaitForQuietState {
/// NetworkEconomics to 0.
#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, comparable::Comparable)]
#[self_describing]
#[derive(Clone, PartialEq, ::prost::Message)]
#[derive(
ic_nns_governance_derive_self_describing::SelfDescribing, Clone, PartialEq, ::prost::Message,
)]
pub struct NetworkEconomics {
/// The number of E8s (10E-8 of an ICP token) that a rejected
/// proposal will cost.
Expand Down Expand Up @@ -1979,6 +1981,7 @@ pub struct NetworkEconomics {
candid::Deserialize,
serde::Serialize,
comparable::Comparable,
ic_nns_governance_derive_self_describing::SelfDescribing,
Clone,
Copy,
PartialEq,
Expand Down Expand Up @@ -2024,6 +2027,7 @@ pub struct VotingPowerEconomics {
candid::Deserialize,
serde::Serialize,
comparable::Comparable,
ic_nns_governance_derive_self_describing::SelfDescribing,
Clone,
PartialEq,
::prost::Message,
Expand Down Expand Up @@ -2061,6 +2065,7 @@ pub struct NeuronsFundMatchedFundingCurveCoefficients {
candid::Deserialize,
serde::Serialize,
comparable::Comparable,
ic_nns_governance_derive_self_describing::SelfDescribing,
Clone,
PartialEq,
::prost::Message,
Expand Down
Loading
Loading