use crate::{CombineTypesVisitor, KorokVisitor};
use codama_attributes::{
AccountDirective, ArgumentDirective, Attributes, DefaultValueDirective, DiscriminatorDirective,
EnumDiscriminatorDirective, OptionalAccountStrategyDirective, ProgramDirective, TryFromFilter,
};
use codama_errors::CodamaResult;
use codama_koroks::FieldKorok;
use codama_nodes::{
CamelCaseString, DefaultValueStrategy, EnumVariantTypeNode, FieldDiscriminatorNode,
InstructionAccountNode, InstructionArgumentNode, InstructionNode,
InstructionOptionalAccountStrategy, NestedTypeNode, Node, NumberValueNode, ProgramNode,
StructFieldTypeNode, StructTypeNode, TypeNode,
};
use codama_syn_helpers::extensions::{ExprExtension, ToTokensExtension};
pub struct SetInstructionsVisitor {
combine_types: CombineTypesVisitor,
enum_name: String,
enum_discriminator: EnumDiscriminatorDirective,
enum_current_discriminator: usize,
}
impl Default for SetInstructionsVisitor {
fn default() -> Self {
Self {
combine_types: CombineTypesVisitor {
get_nammed_field: |korok, parent| {
if korok.attributes.has_codama_attribute("account") {
return None;
}
CombineTypesVisitor::get_strict_named_field(korok, parent)
},
..CombineTypesVisitor::strict()
},
enum_name: "".to_string(),
enum_discriminator: EnumDiscriminatorDirective::default(),
enum_current_discriminator: 0,
}
}
}
impl SetInstructionsVisitor {
pub fn new() -> Self {
Self::default()
}
}
impl KorokVisitor for SetInstructionsVisitor {
fn visit_struct(&mut self, korok: &mut codama_koroks::StructKorok) -> CodamaResult<()> {
if korok.node.is_some() {
return Ok(());
};
if !korok.attributes.has_codama_derive("CodamaInstruction") {
return Ok(());
};
self.combine_types.visit_struct(korok)?;
let (name, data) = parse_struct(korok)?;
let instruction = InstructionNode {
name,
optional_account_strategy: parse_optional_account_strategy(&korok.attributes),
accounts: parse_accounts(&korok.attributes, &korok.fields)?,
arguments: parse_arguments(&korok.attributes, &korok.fields, data, None)?,
discriminators: DiscriminatorDirective::nodes(&korok.attributes),
..InstructionNode::default()
};
korok.node = Some(ProgramDirective::apply(
&korok.attributes,
instruction.into(),
));
Ok(())
}
fn visit_enum(&mut self, korok: &mut codama_koroks::EnumKorok) -> CodamaResult<()> {
if korok.node.is_some() {
return Ok(());
};
if !korok.attributes.has_codama_derive("CodamaInstructions") {
return Ok(());
};
self.combine_types.visit_enum(korok)?;
let enum_discriminator = korok
.attributes
.get_last(EnumDiscriminatorDirective::filter)
.cloned()
.unwrap_or_else(|| EnumDiscriminatorDirective::from(&korok.node));
self.enum_name = korok.ast.ident.to_string();
self.enum_discriminator = enum_discriminator;
self.enum_current_discriminator = 0;
self.visit_children(korok)?;
let instructions = korok
.variants
.iter()
.filter_map(|variant| match &variant.node {
Some(Node::Instruction(instruction)) => Some(instruction.clone()),
_ => None,
})
.collect::<Vec<_>>();
let node: Node = ProgramNode {
instructions,
..ProgramNode::default()
}
.into();
korok.node = Some(ProgramDirective::apply(&korok.attributes, node));
Ok(())
}
fn visit_enum_variant(
&mut self,
korok: &mut codama_koroks::EnumVariantKorok,
) -> CodamaResult<()> {
let current_discriminator = match &korok.ast.discriminant {
Some((_, expr)) => expr.as_unsigned_integer()?,
_ => self.enum_current_discriminator,
};
self.enum_current_discriminator = current_discriminator + 1;
if korok.attributes.has_codama_attribute("skip") {
return Ok(());
};
let (name, data) = parse_enum_variant(korok, &self.enum_name)?;
let discriminator = InstructionArgumentNode {
default_value_strategy: Some(DefaultValueStrategy::Omitted),
default_value: Some(NumberValueNode::new(current_discriminator as u64).into()),
..InstructionArgumentNode::from(&self.enum_discriminator)
};
let mut discriminators = DiscriminatorDirective::nodes(&korok.attributes);
discriminators.insert(
0,
FieldDiscriminatorNode::new(discriminator.name.clone(), 0).into(),
);
korok.node = Some(
InstructionNode {
name,
optional_account_strategy: parse_optional_account_strategy(&korok.attributes),
accounts: parse_accounts(&korok.attributes, &korok.fields)?,
arguments: parse_arguments(
&korok.attributes,
&korok.fields,
data,
Some(discriminator),
)?,
discriminators,
..InstructionNode::default()
}
.into(),
);
Ok(())
}
}
fn parse_optional_account_strategy(attributes: &Attributes) -> InstructionOptionalAccountStrategy {
attributes
.get_last(OptionalAccountStrategyDirective::filter)
.map(|directive| directive.strategy)
.unwrap_or_default()
}
fn parse_accounts(
attributes: &Attributes,
fields: &[FieldKorok],
) -> CodamaResult<Vec<InstructionAccountNode>> {
let accounts_from_struct_attributes = attributes
.iter()
.filter_map(AccountDirective::filter)
.map(|attr| attr.to_instruction_account_node())
.collect::<CodamaResult<Vec<_>>>()?;
let accounts_from_fields = fields
.iter()
.filter_map(|field| field.attributes.get_last(AccountDirective::filter))
.map(|attr| attr.to_instruction_account_node())
.collect::<CodamaResult<Vec<_>>>()?;
Ok(accounts_from_struct_attributes
.into_iter()
.chain(accounts_from_fields)
.collect::<Vec<_>>())
}
fn parse_arguments(
attributes: &Attributes,
fields: &[FieldKorok],
data: StructTypeNode,
discriminator: Option<InstructionArgumentNode>,
) -> CodamaResult<Vec<InstructionArgumentNode>> {
let arguments_from_data = Vec::<InstructionArgumentNode>::from(data)
.into_iter()
.enumerate()
.map(|(i, argument)| {
if argument.default_value.is_some() {
return Ok(argument);
}
let field = fields
.iter()
.enumerate()
.find_map(|(fi, field)| match field.name() {
Some(name) if name == argument.name => Some(field),
None if fi == i => Some(field),
_ => None,
});
let Some(field) = field else {
return Ok(argument);
};
let Some(directive) = field.attributes.get_last(DefaultValueDirective::filter) else {
return Ok(argument);
};
Ok(InstructionArgumentNode {
default_value: Some(directive.node.try_resolved()?.clone()),
default_value_strategy: directive.default_value_strategy,
..argument
})
})
.collect::<CodamaResult<Vec<_>>>()?;
let (before, after): (Vec<_>, Vec<_>) = attributes
.get_all(ArgumentDirective::filter)
.into_iter()
.partition(|attr| !attr.after);
let before = before
.into_iter()
.map(|attr| attr.to_instruction_argument_node())
.collect::<CodamaResult<Vec<_>>>()?;
let after = after
.into_iter()
.map(|attr| attr.to_instruction_argument_node())
.collect::<CodamaResult<Vec<_>>>()?;
let mut arguments: Vec<InstructionArgumentNode> = before
.into_iter()
.chain(arguments_from_data)
.chain(after)
.collect();
if let Some(discriminator) = discriminator {
arguments.insert(0, discriminator);
}
Ok(arguments)
}
fn parse_struct(
korok: &codama_koroks::StructKorok,
) -> CodamaResult<(CamelCaseString, StructTypeNode)> {
if let Some(Node::DefinedType(node)) = &korok.node {
if let TypeNode::Struct(data) = node.r#type.clone() {
return Ok((node.name.clone(), data));
};
};
let message = format!(
"The \"{}\" struct could not be used as an Instruction because its type is not a `StructTypeNode`.",
korok.ast.ident,
);
Err(korok.ast.error(message).into())
}
fn parse_enum_variant(
korok: &codama_koroks::EnumVariantKorok,
enum_name: &str,
) -> CodamaResult<(CamelCaseString, StructTypeNode)> {
if let Some(node) = &korok.node {
if let Ok(node) = EnumVariantTypeNode::try_from(node.clone()) {
match node {
EnumVariantTypeNode::Struct(node) => {
if let NestedTypeNode::Value(data) = node.r#struct {
return Ok((node.name, data));
};
}
EnumVariantTypeNode::Empty(node) => {
return Ok((node.name, StructTypeNode::new(vec![])))
}
EnumVariantTypeNode::Tuple(node) => {
if let NestedTypeNode::Value(tuple) = node.tuple {
let fields = tuple
.items
.into_iter()
.enumerate()
.map(|(i, item)| {
let name = korok
.fields
.get(i)
.and_then(|f| f.name())
.unwrap_or_else(|| format!("arg{}", i).into());
StructFieldTypeNode::new(name, item)
})
.collect();
return Ok((node.name, StructTypeNode::new(fields)));
};
}
}
};
};
let message = format!(
"The \"{}\" variant of the \"{enum_name}\" enum could not be parsed as an instruction. \
Ensure all variant fields resolve to valid type nodes.",
korok.ast.ident
);
Err(korok.ast.error(message).into())
}