Skip to content

Commit

Permalink
feat: Validate handlers don't overlap
Browse files Browse the repository at this point in the history
  • Loading branch information
jawoznia committed Sep 6, 2024
1 parent 3105f0e commit 8d5cd4b
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 23 deletions.
5 changes: 3 additions & 2 deletions sylvia-derive/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,12 @@ impl<'a> ContractInput<'a> {
item,
generics,
custom,
error,
..
} = self;
let multitest_helpers = self.emit_multitest_helpers();
let messages = self.emit_messages();
let contract_api = Api::new(item, generics, custom).emit();
let contract_api = Api::new(item, generics, custom, error).emit();
let querier = self.emit_querier();
let executor = self.emit_executor();
let reply = self.emit_reply();
Expand Down Expand Up @@ -183,6 +184,6 @@ impl<'a> ContractInput<'a> {
fn emit_reply(&self) -> TokenStream {
let variants = MsgVariants::new(self.item.as_variants(), MsgType::Reply, &[], &None);

Reply::new(&variants).emit()
Reply::new(self.item, &self.generics, &variants).emit()
}
}
15 changes: 12 additions & 3 deletions sylvia-derive/src/contract/communication/api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::crate_module;
use crate::parser::variant_descs::AsVariantDescs;
use crate::parser::{Custom, MsgType};
use crate::parser::{ContractErrorAttr, Custom, MsgType};
use crate::types::msg_variant::MsgVariants;
use crate::utils::emit_bracketed_generics;
use proc_macro2::TokenStream;
Expand All @@ -16,10 +16,16 @@ pub struct Api<'a> {
sudo_variants: MsgVariants<'a, GenericParam>,
generics: &'a [&'a GenericParam],
custom: &'a Custom,
error: &'a ContractErrorAttr,
}

impl<'a> Api<'a> {
pub fn new(source: &'a ItemImpl, generics: &'a [&'a GenericParam], custom: &'a Custom) -> Self {
pub fn new(
source: &'a ItemImpl,
generics: &'a [&'a GenericParam],
custom: &'a Custom,
error: &'a ContractErrorAttr,
) -> Self {
let exec_variants = MsgVariants::new(
source.as_variants(),
MsgType::Exec,
Expand Down Expand Up @@ -64,6 +70,7 @@ impl<'a> Api<'a> {
sudo_variants,
generics,
custom,
error,
}
}

Expand All @@ -78,7 +85,7 @@ impl<'a> Api<'a> {
sudo_variants,
generics,
custom,
..
error,
} = self;

let where_clause = &source.generics.where_clause;
Expand All @@ -103,6 +110,7 @@ impl<'a> Api<'a> {
};
let custom_msg = custom.msg_or_default();
let custom_query = custom.query_or_default();
let error = &error.error;

quote! {
impl #bracket_generics #sylvia ::types::ContractApi for #contract_name #where_clause {
Expand All @@ -118,6 +126,7 @@ impl<'a> Api<'a> {
type Querier<'querier> = #sylvia ::types::BoundQuerier<'querier, #custom_query, Self >;
type CustomMsg = #custom_msg;
type CustomQuery = #custom_query;
type Error = #error;
}
}
}
Expand Down
193 changes: 179 additions & 14 deletions sylvia-derive/src/contract/communication/reply.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,153 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;

use convert_case::{Case, Casing};
use itertools::Itertools;
use proc_macro2::TokenStream;
use proc_macro_error::emit_error;
use quote::quote;
use syn::{GenericParam, Ident};
use syn::{parse_quote, GenericParam, Ident, ItemImpl};

use crate::types::msg_variant::MsgVariants;
use crate::crate_module;
use crate::parser::attributes::msg::ReplyOn;
use crate::parser::MsgType;
use crate::types::msg_variant::{MsgVariant, MsgVariants};

pub struct Reply<'a> {
source: &'a ItemImpl,
generics: &'a [&'a GenericParam],
variants: &'a MsgVariants<'a, GenericParam>,
reply_id_to_data: HashMap<Ident, Vec<(&'a Ident, ReplyOn)>>,
}

impl<'a> Reply<'a> {
pub fn new(variants: &'a MsgVariants<'a, GenericParam>) -> Self {
Self { variants }
pub fn new(
source: &'a ItemImpl,
generics: &'a [&'a GenericParam],
variants: &'a MsgVariants<'a, GenericParam>,
) -> Self {
let reply_data: Vec<_> = variants
.variants()
.flat_map(ReplyVariant::as_reply_data)
.collect();

let mut reply_id_to_data: HashMap<Ident, Vec<(&'a Ident, ReplyOn)>> = HashMap::new();

for (reply_id, function_name, reply_on) in reply_data {
match reply_id_to_data.get_key_value(&reply_id) {
Some((prev_reply_id, prev_value))
if prev_value
.iter()
.any(|(_, prev_reply_on)| {
prev_reply_on.excludes(&reply_on)
}) =>
{
prev_value.iter().for_each(|(prev_function_name, prev_reply_on)|{
emit_error!(reply_id.span(), "Duplicated reply handler.";
note = prev_function_name.span() => format!("Previous definition of handler={} for reply_on={}", prev_reply_id.to_string(), prev_reply_on)
)
})
}
_ =>{ let _ = reply_id_to_data.entry(reply_id).and_modify(|values| values.push((function_name, reply_on))).or_insert_with(|| vec![(function_name, reply_on)]);},
// None => reply_id_to_data.insert(reply_id, vec![(function_name, reply_on)]),
}
}
// let entry = reply_id_to_data.try_insert(reply_id, vec![(function_name, reply_on)]);
// match entry {
// Ok(_) => (),
// Err(err) => {
// if err
// .entry
// .get()
// .iter()
// .any(|(_, used_reply_on)| used_reply_on.excludes(reply_on))
// {
// emit_error!(reply_id.span(), "Duplicated reply handler.";
// note = prev_reply_id.span() => format!("Previous definition of handler={} for reply_on={}", prev_reply_id.to_string(), prev_reply_on)
// )
// }
// err.entry.get_mut().push((function_name, reply_on));
// }
// }
// match reply_id_to_data.get_key_value(&reply_id) {
// Some((&prev_reply_id, &(prev_function_name, prev_reply_on)))
// if prev_reply_on == reply_on =>
// {
// emit_error!(reply_id.span(), "Duplicated reply handler.";
// note = prev_reply_id.span() => format!("Previous definition of handler={} for reply_on={}", prev_reply_id.to_string(), prev_reply_on)
// )
// }
// _ => reply_id_to_data.insert(reply_id, (function_name, reply_on)),
// }
// }

Self {
source,
generics,
variants,
reply_id_to_data,
}
}

pub fn emit(&self) -> TokenStream {
let unique_handlers = self.emit_reply_ids();
let dispatch = self.emit_dispatch();

quote! {
#(#unique_handlers)*

#dispatch
}
}

/// Generates dispatch method that matches over every generated `ReplyId`
/// and dispatches depending on the [`ReplyOn`](crate::parser::attributes::msg::ReplyOn).
pub fn emit_dispatch(&self) -> TokenStream {
let sylvia = crate_module();
let Self {
source,
generics,
variants,
..
} = self;

let msg_ty = MsgType::Reply;
let contract = &source.self_ty;
let where_clause = &source.generics.where_clause;

let custom_query = parse_quote!( < #contract as #sylvia ::types::ContractApi>::CustomQuery);
let custom_msg = parse_quote!( < #contract as #sylvia ::types::ContractApi>::CustomMsg);
let error = parse_quote!( < #contract as #sylvia ::types::ContractApi>::Error);
let ctx_params = msg_ty.emit_ctx_params(&custom_query);
let ret_type = msg_ty.emit_result_type(&custom_msg, &error);
let reply_ids = variants.as_reply_ids();

quote! {
pub fn dispatch_reply < #(#generics),* >( #ctx_params , reply: #sylvia ::cw_std::Reply) -> #ret_type #where_clause {
let #sylvia ::cw_std::Reply {
id,
payload,
gas_used,
result,
} = reply;

// match id {
// }
Ok(Response::new())
}
}
}

/// Generates `ReplyId`s for every unique
/// [`reply_handler`](crate::parser::attributes::msg::MsgAttr::reply_handlers) and
/// [`function_name`](crate::types::msg_variant::MsgVariant::function_name) not anotated with
/// the `#[sv::msg(..)]` attribute with the `handlers` parameter.
fn emit_reply_ids(&self) -> impl Iterator<Item = TokenStream> + 'a {
self.variants
.as_reply_ids()
.enumerate()
.map(|(id, reply_id)| {
let id = id as u64;

quote! {
pub const #reply_id : u64 = #id ;
}
Expand All @@ -38,23 +156,70 @@ impl<'a> Reply<'a> {
}

trait ReplyVariants<'a> {
/// Maps [MsgVariants] to unique reply ids.
fn as_reply_ids(&'a self) -> impl Iterator<Item = Ident> + 'a;

fn emit_match_arms(&'a self) -> impl Iterator<Item = TokenStream> + 'a;
}

impl<'a> ReplyVariants<'a> for MsgVariants<'a, GenericParam> {
fn as_reply_ids(&'a self) -> impl Iterator<Item = Ident> + 'a {
self.variants()
.flat_map(|variant| {
if variant.msg_attr().handlers().is_empty() {
return vec![variant.function_name()];
}
variant.msg_attr().handlers().iter().collect()
})
.flat_map(ReplyVariant::as_handlers)
.unique()
.map(AsReplyId::as_reply_id)
}

/// Transform MsgVariant into (`reply_id`, (`function_name`, `handlers`, `reply_on`)).
///
fn emit_match_arms(&'a self) -> impl Iterator<Item = TokenStream> + 'a {
self.variants()
.flat_map(ReplyVariant::as_reply_data)
.map(|data| quote! {})
}
}

struct ReplyData<'a> {
pub reply_id: Ident,
pub function_name: &'a Ident,
pub reply_on: ReplyOn,
}

trait ReplyVariant<'a> {
fn as_handlers(&'a self) -> Vec<&'a Ident>;
fn as_reply_data(&self) -> Vec<(Ident, &Ident, ReplyOn)>;
}

impl<'a> ReplyVariant<'a> for MsgVariant<'a> {
fn as_handlers(&'a self) -> Vec<&'a Ident> {
if self.msg_attr().handlers().is_empty() {
return vec![self.function_name()];
}
self.msg_attr().handlers().iter().collect()
}

fn as_reply_data(&self) -> Vec<(Ident, &Ident, ReplyOn)> {
self.as_handlers()
.iter()
.map(|handler| {
let reply_id =
format! {"{}_REPLY_ID", handler.to_string().to_case(Case::UpperSnake)};
Ident::new(&reply_id, handler.span())
(
handler.as_reply_id(),
self.function_name(),
self.msg_attr().reply_on(),
)
})
.collect()
}
}

/// Maps self to a reply id
trait AsReplyId {
fn as_reply_id(&self) -> Ident;
}

impl AsReplyId for Ident {
fn as_reply_id(&self) -> Ident {
let reply_id = format! {"{}_REPLY_ID", self.to_string().to_case(Case::UpperSnake)};
Ident::new(&reply_id, self.span())
}
}
34 changes: 31 additions & 3 deletions sylvia-derive/src/parser/attributes/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,24 @@ impl Parse for ArgumentParser {
}

/// Representation of `reply_on` parameter in `#[sv::msg(reply(...))]` attribute.
#[derive(Debug, Default, Clone)]
#[derive(Copy, Debug, Default, Clone, PartialEq)]
pub enum ReplyOn {
Success,
Failure,
#[default]
Always,
}

impl std::fmt::Display for ReplyOn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReplyOn::Success => f.write_str("success"),
ReplyOn::Failure => f.write_str("failure"),
ReplyOn::Always => f.write_str("always"),
}
}
}

impl ReplyOn {
pub fn new(reply_on: Ident) -> Result<Self> {
match reply_on.to_string().as_str() {
Expand All @@ -87,6 +97,20 @@ impl ReplyOn {
)),
}
}

/// Checks if two [ReplyOn] values are unique and if [ReplyOn::Always] is not one of them as it is exclusive
/// and cannot be paired with any other value.
pub fn excludes(&self, other: &Self) -> bool {
let are_equal = self == other;
let is_any_always = self == &ReplyOn::Always || other == &ReplyOn::Always;

println!(
"L: {}, R: {}, are_equal: {}, is_any_always: {}",
self, other, are_equal, is_any_always
);

are_equal || is_any_always
}
}

/// Parsed representation of `#[sv::msg(...)]` attribute.
Expand All @@ -95,7 +119,7 @@ pub struct MsgAttr {
msg_type: MsgType,
query_resp_type: Option<Ident>,
reply_handlers: Vec<Ident>,
_reply_on: ReplyOn,
reply_on: ReplyOn,
}

impl MsgAttr {
Expand All @@ -117,6 +141,10 @@ impl MsgAttr {
pub fn handlers(&self) -> &[Ident] {
&self.reply_handlers
}

pub fn reply_on(&self) -> ReplyOn {
self.reply_on
}
}

impl PartialEq<MsgType> for MsgAttr {
Expand All @@ -139,7 +167,7 @@ impl Parse for MsgAttr {
msg_type,
query_resp_type,
reply_handlers,
_reply_on: reply_on.unwrap_or_default(),
reply_on: reply_on.unwrap_or_default(),
})
}
}
Loading

0 comments on commit 8d5cd4b

Please sign in to comment.