Conversation
There was a problem hiding this comment.
Top level overview; attaching to an arbitrary file so we can discuss inline.
I think this gives the correct end result, but I worry about the ergonomics of it. Having to specify the field, type and operation is a problem and will be error prone. I'll leaving some thoughts on possible things to explore.
At a higher level, I want to move away from TryFrom for something this specific. We really should use a dedicated GrpcDecode trait. This would be a rather large diff so I would suggest we do this in tandem with #1742. This allows us to have the current implementation, where we can then slowly move over to the #1742 piece by piece without requiring all things to change at once. So as we implement a method for #1742, we also implement the GrpcDecode parts that we need.
There was a problem hiding this comment.
Latest changes use the proc macro approach with the decoder struct you also mentioned. I have kept the TryFrom instead of introducing another trait, however. Just because there are already a lot of changes for now.
crates/proto/src/domain/block.rs
Outdated
| let prev_block_commitment = value | ||
| .prev_block_commitment | ||
| .try_convert_field::<proto::blockchain::BlockHeader>("prev_block_commitment")?; | ||
| let chain_commitment = value | ||
| .chain_commitment | ||
| .try_convert_field::<proto::blockchain::BlockHeader>("chain_commitment")?; | ||
| let account_root = value | ||
| .account_root | ||
| .try_convert_field::<proto::blockchain::BlockHeader>("account_root")?; | ||
| let nullifier_root = value | ||
| .nullifier_root | ||
| .try_convert_field::<proto::blockchain::BlockHeader>("nullifier_root")?; | ||
| let note_root = value | ||
| .note_root | ||
| .try_convert_field::<proto::blockchain::BlockHeader>("note_root")?; | ||
| let tx_commitment = value | ||
| .tx_commitment | ||
| .try_convert_field::<proto::blockchain::BlockHeader>("tx_commitment")?; | ||
| let tx_kernel_commitment = value | ||
| .tx_kernel_commitment | ||
| .try_convert_field::<proto::blockchain::BlockHeader>("tx_kernel_commitment")?; | ||
| let validator_key = value | ||
| .validator_key | ||
| .try_convert_field::<proto::blockchain::BlockHeader>("validator_key")?; | ||
| let fee_parameters = value | ||
| .fee_parameters | ||
| .try_convert_field::<proto::blockchain::BlockHeader>("fee_parameters")?; |
There was a problem hiding this comment.
serde solves the redundancy of specifying the parent type (proto::blockchain::BlockHeader) by having dedicated sub-decoders. Something like this (I forget the exact details):
/// This gives a single place to inject the parent struct name.
struct GrpcStructDecoder<T: GrpcMessage>;
impl GrpcStructDecoder<T: GrpcMessage> {
fn decode_field<U: GrpcDecode, V>(name: &'static str, value: U) -> Result<V> {
value.decode().context(name)?
}
}
let mut decoder = value.decode_struct();
let account_root = decoder.decode_field("account_root", value.account_root)?;and they have similar helper structs for arrays (which inject indices), and enums etc.
This still requires manually specifying the field name which is a bummer, but I wonder if we can't write a proc-macro that injects that intelligently for us to give something along
#[GrpcDecode::struct]
impl GrpcDecode<BlockHeader> for proto::blockchain::BlockHeader {
fn decode(self) -> Result<BlockHeader, GrpcDecodeError> {
let prev_block_commitment = self.prev_block_commitment.decode()?;
let chain_commitment = self.chain_commitment.decode()?;
let account_root = self.account_root.decode()?;
let nullifier_root = self.nullifier_root.decode()?;
...
Ok(BlockHeader::new(...))
}
}There was a problem hiding this comment.
Have done the decoder + proc macro. But kept the TryFrom for now
Resolves #1528
Refactors
ConversionErrorfrom a public enum to an opaque struct, and introduces layered abstractions to progressively eliminate proto-to-domain conversion boilerplate.ConversionErrorstructConverts
ConversionErrorto an opaque struct with a field path stack and a boxed error source. TheDisplayimpl renders dotted paths like"header.account_root: value is not in range 0..MODULUS".Adds
ConversionResultExtfor ergonomic.context("field_name")chaining onResult<T, ConversionError>.DecodeBytesExtExtension trait on
Deserializabletypes to replace the repeated pattern:with:
GrpcStructDecoder+GrpcDecodeExtReplaces the
TryConvertFieldExttrait (which required specifying the parent proto type on every field) with a serde-style struct decoder. The parent type is specified once via.decoder(), then each field is decoded withdecoder.decode_field("name", value.field):decode_fieldhandlesOptionunwrapping,TryIntoconversion, and field path context in a single call.#[grpc_decode]proc macroAttribute macro that eliminates manual field name strings by rewriting
.decode()shorthand from the AST:The macro rewrites
value.field.decode()intodecoder.decode_field("field", value.field)and injectslet decoder = value.decoder();at the top. It also handles nested scopes:.map(|entry| entry.key.decode()?))Other changes
missing_fieldtype parameters throughoutread_account_idgeneric over parent proto message typesstringify!macros with string literalsprostfrommiden-node-proto