use crate::ast::{
BaseType, IdlAccountSnapshot, IdlArrayElementSnapshot, IdlDefinedInnerSnapshot,
IdlEnumVariantFieldSnapshot, IdlFieldSnapshot, IdlSnapshot, IdlTypeDefKindSnapshot,
IdlTypeDefSnapshot, IdlTypeSnapshot, IntegerKind, ResolvedField, ResolvedStructType,
SerializableStreamSpec,
};
use crate::identifiers::{typescript as ts_ident, IdentifierCase};
use crate::idl_models::{DeclaredModel, IdlModel};
use std::collections::{BTreeMap, BTreeSet, HashSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ResolvedTypeClaim {
Declare(String),
Shared(String),
}
impl ResolvedTypeClaim {
pub(crate) fn name(&self) -> &str {
match self {
Self::Declare(name) | Self::Shared(name) => name,
}
}
pub(crate) fn into_name(self) -> String {
match self {
Self::Declare(name) | Self::Shared(name) => name,
}
}
pub(crate) fn is_shared(&self) -> bool {
matches!(self, Self::Shared(_))
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct StackResolvedTypes {
declared: BTreeMap<String, String>,
reserved: HashSet<String>,
}
impl StackResolvedTypes {
pub(crate) fn reserve(&mut self, names: impl IntoIterator<Item = String>) {
self.reserved.extend(names);
}
pub(crate) fn is_declared(&self, name: &str) -> bool {
self.declared.contains_key(name)
}
pub(crate) fn is_taken(&self, name: &str) -> bool {
self.declared.contains_key(name) || self.reserved.contains(name)
}
pub(crate) fn declare(&mut self, name: &str, resolved: &ResolvedStructType) {
self.declared
.entry(name.to_string())
.or_insert_with(|| resolved_type_shape(resolved));
}
pub(crate) fn claim(
&self,
resolved: &ResolvedStructType,
chosen: String,
base_name: &str,
namespaces: &[String],
taken: &mut HashSet<String>,
) -> ResolvedTypeClaim {
let Some(declared) = self.declared.get(&chosen) else {
return ResolvedTypeClaim::Declare(chosen);
};
let shape = resolved_type_shape(resolved);
if *declared == shape {
return ResolvedTypeClaim::Shared(chosen);
}
let mut namespaces = namespaces
.iter()
.filter(|namespace| !namespace.is_empty())
.collect::<Vec<_>>();
namespaces.dedup();
let numbered_stem = namespaces
.last()
.map(|namespace| format!("{namespace}{base_name}"))
.unwrap_or_else(|| base_name.to_string());
let candidates = namespaces
.iter()
.map(|namespace| format!("{namespace}{base_name}"))
.chain((2usize..).map(|index| format!("{numbered_stem}{index}")));
for candidate in candidates {
if taken.contains(&candidate) || self.reserved.contains(&candidate) {
continue;
}
match self.declared.get(&candidate) {
Some(declared) if *declared == shape => {
taken.insert(candidate.clone());
return ResolvedTypeClaim::Shared(candidate);
}
Some(_) => continue,
None => {
taken.insert(candidate.clone());
return ResolvedTypeClaim::Declare(candidate);
}
}
}
unreachable!("the numbered candidates are unbounded")
}
}
pub(crate) fn entity_program_name<'a>(
entity: &'a SerializableStreamSpec,
idls: &'a [IdlSnapshot],
) -> Option<&'a str> {
entity
.program_id
.as_deref()
.and_then(|program_id| {
idls.iter()
.find(|idl| idl.program_id.as_deref() == Some(program_id))
})
.or(entity.idl.as_ref())
.map(|idl| idl.name.as_str())
}
pub(crate) fn entity_idl<'a>(
entity: &'a SerializableStreamSpec,
idls: &'a [IdlSnapshot],
) -> Option<&'a IdlSnapshot> {
entity
.program_id
.as_deref()
.and_then(|program_id| {
idls.iter()
.find(|idl| idl.program_id.as_deref() == Some(program_id))
})
.or(entity.idl.as_ref())
.or(match idls {
[only] => Some(only),
_ => None,
})
}
#[derive(Debug, Clone, Default)]
pub(crate) struct AccountModels {
by_program: BTreeMap<(String, String), String>,
models: BTreeMap<String, DeclaredModel>,
occupied: BTreeSet<String>,
programs: BTreeMap<String, Vec<ProgramAccountModel>>,
types: BTreeMap<String, Vec<ProgramTypeModel>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProgramAccountModel {
pub(crate) account: String,
pub(crate) model: Option<String>,
pub(crate) stable: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProgramTypeModel {
pub(crate) type_name: String,
pub(crate) model: String,
pub(crate) stable: String,
}
impl AccountModels {
pub(crate) fn declare_model(&mut self, name: &str, resolved: &ResolvedStructType) {
self.models
.entry(name.to_string())
.or_insert_with(|| DeclaredModel::Resolved(resolved.clone()));
}
pub(crate) fn declare_idl_model(&mut self, name: &str, model: &IdlModel, extra: Vec<String>) {
self.models
.entry(name.to_string())
.or_insert_with(|| DeclaredModel::Idl(model.clone()));
self.occupied.extend(extra);
}
pub(crate) fn model(&self, name: &str) -> Option<&DeclaredModel> {
self.models.get(name)
}
pub(crate) fn is_occupied(&self, name: &str) -> bool {
self.occupied.contains(name)
}
pub(crate) fn own(&self, program: &str, account: &str) -> Option<&String> {
self.by_program
.get(&(program.to_string(), account.to_string()))
.or_else(|| {
self.by_program
.iter()
.find(|((owner, name), _)| {
owner == program && name.eq_ignore_ascii_case(account)
})
.map(|(_, model)| model)
})
}
pub(crate) fn bind(&mut self, program: &str, account: &str, model: Option<&str>, stable: &str) {
if let Some(model) = model {
self.by_program.insert(
(program.to_string(), account.to_string()),
model.to_string(),
);
}
self.programs
.entry(program.to_string())
.or_default()
.push(ProgramAccountModel {
account: account.to_string(),
model: model.map(str::to_string),
stable: stable.to_string(),
});
}
pub(crate) fn bind_type(&mut self, program: &str, type_name: &str, model: &str, stable: &str) {
self.types
.entry(program.to_string())
.or_default()
.push(ProgramTypeModel {
type_name: type_name.to_string(),
model: model.to_string(),
stable: stable.to_string(),
});
}
pub(crate) fn program_accounts(&self, program: &str) -> &[ProgramAccountModel] {
self.programs
.get(program)
.map(Vec::as_slice)
.unwrap_or_default()
}
pub(crate) fn program_types(&self, program: &str) -> &[ProgramTypeModel] {
self.types
.get(program)
.map(Vec::as_slice)
.unwrap_or_default()
}
pub(crate) fn program_model_names(&self, program: &str) -> Vec<(String, String)> {
let mut seen = HashSet::new();
self.program_accounts(program)
.iter()
.filter_map(|account| Some((account.stable.clone(), account.model.clone()?)))
.chain(
self.program_types(program)
.iter()
.map(|model| (model.stable.clone(), model.model.clone())),
)
.filter(|(stable, _)| seen.insert(stable.clone()))
.collect()
}
pub(crate) fn record(&mut self, program: Option<&str>, account_type: &str, model: &str) {
if let Some(program) = program {
self.by_program
.entry((program.to_string(), account_type.to_string()))
.or_insert_with(|| model.to_string());
}
}
}
pub(crate) fn idl_account_field_snapshots(
idl: &IdlSnapshot,
account: &IdlAccountSnapshot,
) -> Option<Vec<IdlFieldSnapshot>> {
if !account.fields.is_empty() {
return Some(account.fields.clone());
}
let type_def = idl
.types
.iter()
.find(|type_def| type_def.name == account.name)
.or_else(|| {
idl.types
.iter()
.find(|type_def| type_def.name.eq_ignore_ascii_case(&account.name))
});
match type_def.map(|type_def| &type_def.type_def) {
None => Some(Vec::new()),
Some(IdlTypeDefKindSnapshot::Struct { fields, .. }) => Some(fields.clone()),
Some(IdlTypeDefKindSnapshot::TupleStruct { fields, .. }) => Some(
fields
.iter()
.enumerate()
.map(|(index, type_)| IdlFieldSnapshot {
name: format!("_{index}"),
type_: type_.clone(),
amount_hint: None,
})
.collect(),
),
Some(IdlTypeDefKindSnapshot::Enum { .. }) => None,
}
}
#[cfg(test)]
pub(crate) fn idl_account_model(
idl: &IdlSnapshot,
account: &IdlAccountSnapshot,
) -> Option<ResolvedStructType> {
Some(ResolvedStructType {
type_name: account.name.clone(),
fields: idl_account_field_snapshots(idl, account)?
.iter()
.map(resolved_idl_field)
.collect(),
is_instruction: false,
is_account: true,
is_event: false,
is_enum: false,
enum_variants: Vec::new(),
})
}
pub(crate) fn resolved_idl_field(field: &IdlFieldSnapshot) -> ResolvedField {
let shape = idl_field_shape(&field.type_);
ResolvedField {
field_name: field.name.clone(),
raw_name: Some(field.name.clone()),
canonical_name: Some(crate::ast::to_camel_case_owned(&field.name)),
field_type: shape.field_type,
base_type: shape.base_type,
integer_kind: shape.integer_kind,
is_optional: shape.is_optional,
is_array: shape.is_array,
}
}
struct IdlFieldShape {
field_type: String,
base_type: BaseType,
integer_kind: Option<IntegerKind>,
is_optional: bool,
is_array: bool,
}
fn simple_idl_base_type(name: &str) -> (BaseType, Option<IntegerKind>) {
if let Some(kind) = IntegerKind::from_rust_type(name) {
return (BaseType::Integer, Some(kind));
}
let base_type = match name {
"f32" | "f64" => BaseType::Float,
"bool" => BaseType::Boolean,
"string" | "String" => BaseType::String,
"publicKey" | "pubkey" | "Pubkey" => BaseType::Pubkey,
"bytes" => BaseType::Binary,
_ => BaseType::Object,
};
(base_type, None)
}
fn idl_field_shape(idl_type: &IdlTypeSnapshot) -> IdlFieldShape {
match idl_type {
IdlTypeSnapshot::Simple(name) => {
let (base_type, integer_kind) = simple_idl_base_type(name);
IdlFieldShape {
field_type: name.clone(),
base_type,
integer_kind,
is_optional: false,
is_array: false,
}
}
IdlTypeSnapshot::Option(option) => {
let inner = idl_field_shape(&option.option);
IdlFieldShape {
field_type: format!("Option<{}>", inner.field_type),
is_optional: true,
..inner
}
}
IdlTypeSnapshot::Vec(vec) => {
let inner = idl_field_shape(&vec.vec);
IdlFieldShape {
field_type: format!("Vec<{}>", inner.field_type),
is_array: true,
..inner
}
}
IdlTypeSnapshot::Array(array) if array.array.len() >= 2 => {
let element = match &array.array[0] {
IdlArrayElementSnapshot::Type(IdlTypeSnapshot::Simple(name))
| IdlArrayElementSnapshot::TypeName(name) => {
let (base_type, integer_kind) = simple_idl_base_type(name);
Some(IdlFieldShape {
field_type: name.clone(),
base_type,
integer_kind,
is_optional: false,
is_array: false,
})
}
IdlArrayElementSnapshot::Type(nested) => Some(idl_field_shape(nested)),
IdlArrayElementSnapshot::Size(_) => None,
};
match element {
Some(element) => IdlFieldShape {
field_type: format!("[{}]", element.field_type),
is_array: true,
..element
},
None => json_array_shape(),
}
}
IdlTypeSnapshot::Array(_) => json_array_shape(),
IdlTypeSnapshot::Defined(defined) => IdlFieldShape {
field_type: match &defined.defined {
IdlDefinedInnerSnapshot::Named { name } => name.clone(),
IdlDefinedInnerSnapshot::Simple(name) => name.clone(),
},
base_type: BaseType::Object,
integer_kind: None,
is_optional: false,
is_array: false,
},
IdlTypeSnapshot::HashMap(map) => IdlFieldShape {
field_type: format!(
"HashMap<{}, {}>",
idl_field_shape(&map.hash_map.0).field_type,
idl_field_shape(&map.hash_map.1).field_type
),
base_type: BaseType::Object,
integer_kind: None,
is_optional: false,
is_array: false,
},
IdlTypeSnapshot::Tuple(tuple) => IdlFieldShape {
field_type: format!(
"({})",
tuple
.tuple
.iter()
.map(|element| idl_field_shape(element).field_type)
.collect::<Vec<_>>()
.join(", ")
),
base_type: BaseType::Object,
integer_kind: None,
is_optional: false,
is_array: false,
},
}
}
fn json_array_shape() -> IdlFieldShape {
IdlFieldShape {
field_type: "Array".to_string(),
base_type: BaseType::Array,
integer_kind: None,
is_optional: false,
is_array: true,
}
}
pub(crate) fn stable_account_model_names(
idl: &IdlSnapshot,
pascal: impl Fn(&str) -> String,
reserved: &[&str],
) -> BTreeMap<String, String> {
let mut used = reserved
.iter()
.map(|name| name.to_string())
.collect::<HashSet<_>>();
let mut names = BTreeMap::new();
for account in &idl.accounts {
if names.contains_key(&account.name) {
continue;
}
let base = pascal(&account.name);
let name = std::iter::once(base.clone())
.chain(std::iter::once(format!("{base}Account")))
.chain((2usize..).map(|index| format!("{base}Account{index}")))
.find(|candidate| used.insert(candidate.clone()))
.expect("the numbered candidates are unbounded");
names.insert(account.name.clone(), name);
}
names
}
pub(crate) fn account_model_candidates(
stable: &str,
program_name: &str,
) -> impl Iterator<Item = String> {
let prefixed = format!(
"{}{stable}",
ts_ident::identifier_stem(program_name, IdentifierCase::Pascal)
);
let role = format!("{prefixed}Account");
[stable.to_string(), prefixed, role.clone()]
.into_iter()
.chain((2usize..).map(move |index| format!("{role}{index}")))
}
pub(crate) fn account_reader_programs(
idls: &[IdlSnapshot],
program_ids: &[String],
instructions: &[crate::ast::InstructionDef],
include_idl_only: bool,
has_read_layer: impl Fn(&str) -> bool,
) -> HashSet<String> {
if instructions.is_empty() && !include_idl_only {
return HashSet::new();
}
let default_program_id = program_ids.first().map(String::as_str);
idls.iter()
.enumerate()
.filter_map(|(index, idl)| {
let program_id = idl
.program_id
.as_deref()
.or_else(|| program_ids.get(index).map(String::as_str))?;
let emitted = include_idl_only
|| instructions.iter().any(|instruction| {
instruction.program_id.as_deref().or(default_program_id) == Some(program_id)
});
(emitted && has_read_layer(program_id)).then(|| idl.name.clone())
})
.collect()
}
pub(crate) fn resolved_type_namespaces(
program_name: Option<&str>,
entity_name: &str,
) -> Vec<String> {
program_name
.map(|name| ts_ident::identifier_stem(name, IdentifierCase::Pascal))
.into_iter()
.chain(std::iter::once(ts_ident::identifier_stem(
entity_name,
IdentifierCase::Pascal,
)))
.collect()
}
pub(crate) fn resolved_type_shape(resolved: &ResolvedStructType) -> String {
serde_json::to_string(&(resolved.is_enum, &resolved.enum_variants, &resolved.fields))
.expect("resolved types serialize")
}
pub(crate) struct ProgramTypeDefs<'a> {
first: BTreeMap<String, (&'a IdlTypeDefSnapshot, usize)>,
names: BTreeSet<String>,
programs: Vec<ProgramDefs<'a>>,
program_names: Vec<String>,
scope: Option<usize>,
}
struct ProgramDefs<'a> {
by_name: BTreeMap<&'a str, &'a IdlTypeDefSnapshot>,
lower: BTreeMap<String, &'a str>,
own: BTreeSet<&'a str>,
}
impl<'a> ProgramDefs<'a> {
fn get(&self, name: &str) -> Option<&'a IdlTypeDefSnapshot> {
self.by_name.get(name).copied().or_else(|| {
self.lower
.get(&name.to_lowercase())
.map(|spelling| self.by_name[spelling])
})
}
}
#[derive(Clone, Copy)]
pub(crate) struct ProgramTypeDef<'a> {
pub(crate) name: &'a str,
pub(crate) def: &'a IdlTypeDefSnapshot,
pub(crate) program: Option<usize>,
}
impl<'a> ProgramTypeDefs<'a> {
pub(crate) fn new(idls: &'a [IdlSnapshot]) -> Self {
let mut first = BTreeMap::new();
let mut names = BTreeSet::new();
for (program, idl) in idls.iter().enumerate() {
for def in &idl.types {
names.insert(def.name.clone());
first
.entry(def.name.to_lowercase())
.or_insert((def, program));
}
}
let mut programs = idls
.iter()
.map(|idl| {
let mut lower = BTreeMap::new();
for def in &idl.types {
lower
.entry(def.name.to_lowercase())
.or_insert(def.name.as_str());
}
ProgramDefs {
by_name: idl
.types
.iter()
.map(|def| (def.name.as_str(), def))
.collect(),
lower,
own: BTreeSet::new(),
}
})
.collect::<Vec<_>>();
for program in &mut programs {
let mut differs = BTreeMap::new();
program.own = program
.by_name
.values()
.filter(|def| {
definition_differs(def, program, &first, &mut differs, &mut Vec::new())
})
.map(|def| def.name.as_str())
.collect();
}
Self {
first,
names,
programs,
program_names: idls.iter().map(|idl| idl.name.clone()).collect(),
scope: None,
}
}
pub(crate) fn set_scope(&mut self, program: Option<usize>) {
self.scope = program;
}
pub(crate) fn program_name(&self, program: usize) -> &str {
&self.program_names[program]
}
pub(crate) fn names(&self) -> impl Iterator<Item = &String> {
self.names.iter()
}
pub(crate) fn lookup(&self, name: &str) -> Option<ProgramTypeDef<'a>> {
let scoped = self.scope.and_then(|program| {
let def = self.programs.get(program)?.get(name)?;
Some((program, def))
});
if let Some((program, def)) = scoped {
if self.programs[program].own.contains(def.name.as_str()) {
return Some(ProgramTypeDef {
name: &def.name,
def,
program: Some(program),
});
}
}
let key = scoped.map_or(name, |(_, def)| def.name.as_str());
let (def, _) = self.first.get(&key.to_lowercase())?;
Some(ProgramTypeDef {
name: &def.name,
def,
program: None,
})
}
pub(crate) fn conflicts(&self) -> Vec<(String, String, String)> {
self.programs
.iter()
.enumerate()
.flat_map(|(program, defs)| {
defs.own.iter().map(move |name| {
let (_, first_program) = self.first[&name.to_lowercase()];
(
name.to_string(),
self.program_names[first_program].clone(),
self.program_names[program].clone(),
)
})
})
.collect()
}
}
fn definition_differs<'a>(
own: &'a IdlTypeDefSnapshot,
program: &ProgramDefs<'a>,
first: &BTreeMap<String, (&'a IdlTypeDefSnapshot, usize)>,
differs: &mut BTreeMap<String, bool>,
visiting: &mut Vec<String>,
) -> bool {
if let Some(known) = differs.get(&own.name) {
return *known;
}
if visiting.contains(&own.name) {
return false;
}
let Some((first_def, _)) = first.get(&own.name.to_lowercase()) else {
return false;
};
visiting.push(own.name.clone());
let result = definition_shape(own) != definition_shape(first_def)
|| referenced_type_names(own).iter().any(|referenced| {
program.get(referenced).is_some_and(|referenced| {
definition_differs(referenced, program, first, differs, visiting)
})
});
visiting.pop();
differs.insert(own.name.clone(), result);
result
}
fn definition_shape(def: &IdlTypeDefSnapshot) -> serde_json::Value {
serde_json::json!({
"serialization": def.serialization,
"type": def.type_def,
})
}
fn referenced_type_names(def: &IdlTypeDefSnapshot) -> BTreeSet<String> {
fn words(text: &str, names: &mut BTreeSet<String>) {
names.extend(
text.split(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
.filter(|word| !word.is_empty())
.map(str::to_string),
);
}
fn walk(ty: &IdlTypeSnapshot, names: &mut BTreeSet<String>) {
match ty {
IdlTypeSnapshot::Simple(simple) => words(simple, names),
IdlTypeSnapshot::Array(array) => {
for element in &array.array {
match element {
IdlArrayElementSnapshot::Type(inner) => walk(inner, names),
IdlArrayElementSnapshot::TypeName(name) => words(name, names),
IdlArrayElementSnapshot::Size(_) => {}
}
}
}
IdlTypeSnapshot::Option(option) => walk(&option.option, names),
IdlTypeSnapshot::Vec(vec) => walk(&vec.vec, names),
IdlTypeSnapshot::HashMap(map) => {
walk(&map.hash_map.0, names);
walk(&map.hash_map.1, names);
}
IdlTypeSnapshot::Tuple(tuple) => {
for element in &tuple.tuple {
walk(element, names);
}
}
IdlTypeSnapshot::Defined(defined) => match &defined.defined {
IdlDefinedInnerSnapshot::Named { name } => {
names.insert(name.clone());
}
IdlDefinedInnerSnapshot::Simple(name) => {
names.insert(name.clone());
}
},
}
}
let mut names = BTreeSet::new();
match &def.type_def {
IdlTypeDefKindSnapshot::Struct { fields, .. } => {
for field in fields {
walk(&field.type_, &mut names);
}
}
IdlTypeDefKindSnapshot::TupleStruct { fields, .. } => {
for field in fields {
walk(field, &mut names);
}
}
IdlTypeDefKindSnapshot::Enum { variants, .. } => {
for field in variants.iter().flat_map(|variant| &variant.fields) {
match field {
IdlEnumVariantFieldSnapshot::Named(named) => walk(&named.type_, &mut names),
IdlEnumVariantFieldSnapshot::Tuple(tuple) => walk(tuple, &mut names),
}
}
}
}
names
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{BaseType, ResolvedField};
fn resolved(name: &str, fields: &[(&str, BaseType)]) -> ResolvedStructType {
ResolvedStructType {
type_name: name.to_string(),
fields: fields
.iter()
.map(|(field_name, base_type)| ResolvedField {
field_name: field_name.to_string(),
raw_name: None,
canonical_name: None,
field_type: format!("{base_type:?}"),
base_type: base_type.clone(),
integer_kind: None,
is_optional: false,
is_array: false,
})
.collect(),
is_instruction: false,
is_account: false,
is_event: false,
is_enum: false,
enum_variants: Vec::new(),
}
}
fn namespaces() -> Vec<String> {
vec!["Beta".to_string(), "Vault".to_string()]
}
#[test]
fn undeclared_names_are_kept() {
let types = StackResolvedTypes::default();
let header = resolved("header", &[("version", BaseType::Integer)]);
let mut taken = HashSet::from(["Header".to_string()]);
assert_eq!(
types.claim(
&header,
"Header".into(),
"Header",
&namespaces(),
&mut taken
),
ResolvedTypeClaim::Declare("Header".into())
);
}
#[test]
fn identical_definitions_are_shared_whatever_their_flags() {
let mut types = StackResolvedTypes::default();
types.declare(
"Header",
&resolved("header", &[("version", BaseType::Integer)]),
);
let mut account = resolved("Header", &[("version", BaseType::Integer)]);
account.is_account = true;
let mut taken = HashSet::from(["Header".to_string()]);
assert_eq!(
types.claim(
&account,
"Header".into(),
"Header",
&namespaces(),
&mut taken
),
ResolvedTypeClaim::Shared("Header".into())
);
}
#[test]
fn different_definitions_are_namespaced_then_numbered() {
let mut types = StackResolvedTypes::default();
types.declare(
"Header",
&resolved("header", &[("version", BaseType::Integer)]),
);
let other = resolved("header", &[("owner", BaseType::Pubkey)]);
let mut taken = HashSet::from(["Header".to_string()]);
let claim = types.claim(&other, "Header".into(), "Header", &namespaces(), &mut taken);
assert_eq!(claim, ResolvedTypeClaim::Declare("BetaHeader".into()));
assert!(taken.contains("BetaHeader"));
types.declare("BetaHeader", &other);
let mut taken = HashSet::from(["Header".to_string()]);
assert_eq!(
types.claim(&other, "Header".into(), "Header", &namespaces(), &mut taken),
ResolvedTypeClaim::Shared("BetaHeader".into())
);
let third = resolved("header", &[("flag", BaseType::Boolean)]);
types.reserve(["VaultHeader".to_string()]);
let mut taken = HashSet::from(["Header".to_string()]);
assert_eq!(
types.claim(&third, "Header".into(), "Header", &namespaces(), &mut taken),
ResolvedTypeClaim::Declare("VaultHeader2".into())
);
}
fn idl(name: &str, types: serde_json::Value) -> IdlSnapshot {
serde_json::from_value(serde_json::json!({
"name": name,
"version": "0.1.0",
"accounts": [],
"instructions": [],
"types": types,
"discriminant_size": 8
}))
.expect("test IDL")
}
fn program_of(defs: &ProgramTypeDefs<'_>, name: &str) -> Option<Option<usize>> {
defs.lookup(name).map(|found| found.program)
}
#[test]
fn program_type_defs_resolve_each_programs_own_different_definition() {
let struct_of =
|fields: serde_json::Value| serde_json::json!({ "kind": "struct", "fields": fields });
let inner_a = struct_of(serde_json::json!([{ "name": "a", "type": "u8" }]));
let inner_b = struct_of(serde_json::json!([{ "name": "a", "type": "u16" }]));
let outer =
struct_of(serde_json::json!([{ "name": "inner", "type": { "defined": "Inner" } }]));
let same = struct_of(serde_json::json!([{ "name": "x", "type": "bool" }]));
let idls = vec![
idl(
"alpha",
serde_json::json!([
{ "name": "Inner", "type": inner_a },
{ "name": "Outer", "type": outer },
{ "name": "Same", "type": same }
]),
),
idl(
"beta",
serde_json::json!([
{ "name": "Inner", "docs": ["documented differently"], "type": inner_b },
{ "name": "Outer", "type": outer },
{ "name": "Same", "docs": ["documented differently"], "type": same }
]),
),
];
let mut defs = ProgramTypeDefs::new(&idls);
for scope in [None, Some(0)] {
defs.set_scope(scope);
assert_eq!(program_of(&defs, "Inner"), Some(None));
assert_eq!(program_of(&defs, "Outer"), Some(None));
}
defs.set_scope(Some(1));
assert_eq!(program_of(&defs, "Inner"), Some(Some(1)));
assert_eq!(program_of(&defs, "inner"), Some(Some(1)));
assert_eq!(program_of(&defs, "Outer"), Some(Some(1)));
assert_eq!(program_of(&defs, "Same"), Some(None));
assert_eq!(program_of(&defs, "Missing"), None);
assert_eq!(
defs.conflicts(),
vec![
("Inner".to_string(), "alpha".to_string(), "beta".to_string()),
("Outer".to_string(), "alpha".to_string(), "beta".to_string()),
]
);
}
#[test]
fn program_type_defs_match_names_case_insensitively_across_programs() {
let struct_of = |ty: &str| serde_json::json!({ "kind": "struct", "fields": [{ "name": "a", "type": ty }] });
let idls = vec![
idl(
"alpha",
serde_json::json!([{ "name": "Header", "type": struct_of("u8") }]),
),
idl(
"beta",
serde_json::json!([{ "name": "header", "type": struct_of("u16") }]),
),
idl(
"gamma",
serde_json::json!([{ "name": "HEADER", "type": struct_of("u8") }]),
),
];
let mut defs = ProgramTypeDefs::new(&idls);
defs.set_scope(Some(1));
for spelling in ["Header", "header", "HEADER"] {
let found = defs.lookup(spelling).unwrap();
assert_eq!((found.name, found.program), ("header", Some(1)));
}
defs.set_scope(Some(2));
let found = defs.lookup("header").unwrap();
assert_eq!((found.name, found.program), ("Header", None));
assert_eq!(
defs.conflicts(),
vec![(
"header".to_string(),
"alpha".to_string(),
"beta".to_string()
)]
);
}
}