Skip to content

Commit

Permalink
Merge pull request #32 from blockscout/ll/add-field-attributes
Browse files Browse the repository at this point in the history
add field attributes for internal structs
  • Loading branch information
sevenzing authored Aug 26, 2024
2 parents 9cc47aa + 6ae3fcc commit 41f15f5
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 5 deletions.
37 changes: 35 additions & 2 deletions actix-prost-build/src/conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use prost_reflect::{
MessageDescriptor,
};
use quote::quote;
use syn::{punctuated::Punctuated, Expr, Field, Fields, Lit, Meta, MetaNameValue, Token, Type};
use syn::{
punctuated::Punctuated, Attribute, Expr, Field, Fields, Lit, Meta, MetaNameValue, Token, Type,
};

#[derive(Debug)]
pub struct ExtraFieldOptions {
Expand All @@ -33,6 +35,7 @@ pub struct ConvertFieldOptions {
pub ty: Option<String>,
pub val_override: Option<String>,
pub required: bool,
pub attributes: Vec<String>,
}

#[derive(Default, Debug)]
Expand Down Expand Up @@ -122,6 +125,7 @@ impl From<(&FieldDescriptor, &ExtensionDescriptor)> for ConvertFieldOptions {
Some(v) => v.as_bool().unwrap(),
None => false,
},
attributes: get_repeated_string_field(ext_val, "attributes"),
}
}
}
Expand Down Expand Up @@ -324,8 +328,13 @@ impl ConversionsGenerator {
fields
.map(|f| {
let name = f.ident.clone().unwrap();
// Remove the r# prefix if it exists, for example r#type -> type
let name_str = name.to_string().trim_start_matches("r#").to_string();
let vis = &f.vis;
let convert_field = convert_options.fields.get(&name.to_string());
let convert_field = convert_options.fields.get(&name_str);
let attributes = convert_field
.map(|cf| cf.attributes.clone())
.unwrap_or_default();

// 1. Check if the field contains a nested message
// 2. Check if the field is an enum
Expand All @@ -335,8 +344,16 @@ impl ConversionsGenerator {
.or_else(|| Self::process_enum(m_type, f))
.unwrap_or_else(|| self.process_default(f, convert_field));

// Ensure that all attributes are valid and convert them into tokens
let field_attributes = attributes.iter().map(|attr_raw| {
let attr_token: TokenStream = attr_raw.parse().unwrap();
let attr: Attribute = syn::parse_quote!(#attr_token);
quote!(#attr)
});

(
quote! {
#(#field_attributes)*
#vis #name: #ty
},
quote! {
Expand Down Expand Up @@ -563,3 +580,19 @@ fn get_string_field(m: &DynamicMessage, name: &str) -> Option<String> {
Some(f)
}
}

fn get_repeated_string_field(m: &DynamicMessage, name: &str) -> Vec<String> {
m.get_field_by_name(name)
.map(|f| {
f.as_list()
.unwrap_or_else(|| panic!("field '{name}' is not list"))
.iter()
.map(|v| {
v.as_str()
.unwrap_or_else(|| panic!("field '{name}' is not list of strings"))
.to_string()
})
.collect()
})
.unwrap_or_default()
}
15 changes: 15 additions & 0 deletions tests/proto/conversions.proto
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,25 @@ message ConversionsRequest {
repeated string addresses = 3 [ (convert_options.convert) = { type : "std::collections::HashSet<ethers::types::Address>" } ];
NestedEnum nested_enum = 4;
Nested nested = 5 [ (convert_options.convert) = { required : true } ];

}

message ConversionsResponse {
string address = 1 [ (convert_options.convert) = { type : "ethers::types::Address" } ];
Nested nested = 2;
map<string, MapValue> map_field = 3;
Config config = 4;
}


enum ConfigType {
CONFIG_TYPE_UNSPECIFIED = 0;
CONFIG_TYPE_FOO = 1;
CONFIG_TYPE_BAR = 2;
}

message Config {
option (convert_options.derive) = { name: "serde::Deserialize" };

ConfigType type = 1 [ (convert_options.convert) = { attributes : ["#[serde(default)]"] } ];
}
1 change: 1 addition & 0 deletions tests/proto/convert_options.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ message ConvertOptions {
string type = 1;
string override = 2;
bool required = 3;
repeated string attributes = 4;
}

message ExtraFieldOptions {
Expand Down
15 changes: 12 additions & 3 deletions tests/src/conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ use std::{collections::HashMap, net::SocketAddr, sync::Arc};
use crate::{
proto::conversions::{
conversions_rpc_actix::route_conversions_rpc, conversions_rpc_server::ConversionsRpc,
ConversionsRequest, ConversionsRequestInternal, ConversionsResponse,
ConversionsResponseInternal, MapValue, Nested,
ConfigInternal, ConfigType, ConversionsRequest, ConversionsRequestInternal,
ConversionsResponse, ConversionsResponseInternal, MapValue, Nested,
},
test,
};
use actix_web::{App, HttpServer};
use convert_trait::TryConvert;
use ethers::types::Address;
use serde_json::Value;
use serde_json::{json, Value};
use tonic::{Request, Response, Status};

#[derive(Default)]
Expand All @@ -30,6 +30,7 @@ impl ConversionsRpc for ConversionsServer {
address: Address::from_low_u64_be(0),
nested: Some(internal_request.nested),
map_field: internal_request.map_field,
config: None,
};

let response = ConversionsResponse::try_convert(internal_response)
Expand Down Expand Up @@ -111,3 +112,11 @@ async fn conversions() {
assert_eq!(res.nested.unwrap().address, test_address);
assert_eq!(res.map_field.get("key").unwrap().address, test_address);
}

#[test]
fn default_on_internal() {
let config: ConfigInternal = serde_json::from_value(json!({})).unwrap();
assert_eq!(config.r#type, ConfigType::Unspecified);
let config: ConfigInternal = serde_json::from_value(json!({"type": "FOO"})).unwrap();
assert_eq!(config.r#type, ConfigType::Foo);
}
52 changes: 52 additions & 0 deletions tests/src/proto/conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,45 @@ pub struct ConversionsResponse {
pub nested: ::core::option::Option<Nested>,
#[prost(map = "string, message", tag = "3")]
pub map_field: ::std::collections::HashMap<::prost::alloc::string::String, MapValue>,
#[prost(message, optional, tag = "4")]
pub config: ::core::option::Option<Config>,
}
#[actix_prost_macros::serde]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Config {
#[prost(enumeration = "ConfigType", tag = "1")]
pub r#type: i32,
}
#[actix_prost_macros::serde]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum ConfigType {
Unspecified = 0,
Foo = 1,
Bar = 2,
}
impl ConfigType {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
ConfigType::Unspecified => "CONFIG_TYPE_UNSPECIFIED",
ConfigType::Foo => "CONFIG_TYPE_FOO",
ConfigType::Bar => "CONFIG_TYPE_BAR",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"CONFIG_TYPE_UNSPECIFIED" => Some(Self::Unspecified),
"CONFIG_TYPE_FOO" => Some(Self::Foo),
"CONFIG_TYPE_BAR" => Some(Self::Bar),
_ => None,
}
}
}
pub mod conversions_rpc_actix {
#![allow(unused_variables, dead_code, missing_docs)]
Expand Down Expand Up @@ -203,6 +242,17 @@ impl convert_trait::TryConvert<MapValueInternal> for MapValue {
})
}
}
#[derive(serde::Deserialize)]
#[derive(Clone, Debug)]
pub struct ConfigInternal {
#[serde(default)]
pub r#type: ConfigType,
}
impl convert_trait::TryConvert<ConfigInternal> for Config {
fn try_convert(from: ConfigInternal) -> Result<Self, String> {
Ok(Self { r#type: from.r#type.into() })
}
}
#[derive(Clone, Debug)]
pub struct ConversionsResponseInternal {
pub address: ethers::types::Address,
Expand All @@ -211,13 +261,15 @@ pub struct ConversionsResponseInternal {
::prost::alloc::string::String,
MapValueInternal,
>,
pub config: ::core::option::Option<ConfigInternal>,
}
impl convert_trait::TryConvert<ConversionsResponseInternal> for ConversionsResponse {
fn try_convert(from: ConversionsResponseInternal) -> Result<Self, String> {
Ok(Self {
address: convert_trait::TryConvert::try_convert(from.address)?,
nested: convert_trait::TryConvert::try_convert(from.nested)?,
map_field: convert_trait::TryConvert::try_convert(from.map_field)?,
config: convert_trait::TryConvert::try_convert(from.config)?,
})
}
}
Expand Down
2 changes: 2 additions & 0 deletions tests/src/proto/convert_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pub struct ConvertOptions {
pub r#override: ::prost::alloc::string::String,
#[prost(bool, tag = "3")]
pub required: bool,
#[prost(string, repeated, tag = "4")]
pub attributes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
#[actix_prost_macros::serde]
#[allow(clippy::derive_partial_eq_without_eq)]
Expand Down

0 comments on commit 41f15f5

Please sign in to comment.