1use core::panic;
2use std::{env, path::PathBuf, str::FromStr};
3
4use proc_macro2::TokenStream;
5use quote::quote;
6use solana_sdk::pubkey::Pubkey;
7
8use crate::{
9 generator::{events, instructions, state},
10 idl::IdlJsonDefinition,
11 meta::Meta,
12};
13
14pub mod generator;
15pub mod idl;
16pub mod meta;
17
18#[derive(Default, PartialEq, Debug)]
19pub struct TypesAndAccountsConfig {
20 pub zero_copy: Vec<String>,
23 pub zero_copy_unsafe: Vec<String>,
28 pub repr_c: Vec<String>,
32 pub repr_packed: Vec<String>,
36}
37
38impl TypesAndAccountsConfig {
39 pub fn validate(&self) {
40 let mut duplicates = String::new();
41 for zero_copy in &self.zero_copy {
42 if self.zero_copy_unsafe.contains(zero_copy) {
43 duplicates.push_str(&format!("{}, ", zero_copy.clone()));
44 }
45 }
46
47 if duplicates.len() > 0 {
48 panic!(
49 "zero_copy and zero_copy_unsafe can not contain same identifiers. Duplicates: {}",
50 &duplicates[..duplicates.len() - 2]
51 )
52 }
53 }
54}
55
56#[derive(Default, PartialEq, Debug)]
57pub struct Args {
58 pub idl_path: PathBuf,
60 pub program_id: String,
62 pub skip_errors: bool,
64 pub skip_events: bool,
66 pub types_and_accounts_config: TypesAndAccountsConfig,
67}
68
69impl Args {
70 fn remove_whitespace(str: &str) -> String {
71 str.chars()
72 .filter(|c| !c.is_whitespace())
73 .collect::<String>()
74 }
75
76 fn parse_inside_parenthesis<'a, T: Iterator<Item = &'a str>>(
77 current: &str,
78 args: &mut T,
79 target: &mut Vec<String>,
80 name: &str,
81 ) {
82 match current.split("(").collect::<Vec<&str>>()[..] {
83 [_, val] => {
84 if val.ends_with(")") {
85 let val = &val[..val.len() - 1];
86 target.push(val.to_owned());
87 return;
88 } else {
89 target.push(val.to_owned());
90 }
91 }
92 _ => panic!("Invalid {} arg", name),
93 }
94
95 while let Some(arg) = args.next() {
96 if arg.ends_with(")") {
97 let val = &arg[..arg.len() - 1];
98 target.push(val.to_owned());
99 return;
100 } else {
101 target.push(arg.to_owned());
102 };
103 }
104 panic!("Invalid {} arg", name);
105 }
106
107 pub fn parse(args: String) -> Self {
108 let args_sanitized = args.replace('\"', "").replace('\n', " ");
109 let mut args = args_sanitized.split(",").map(|arg| arg.trim());
110
111 let mut idl_path: Option<PathBuf> = None;
112 let mut program_id: Option<String> = None;
113 let mut types_and_accounts_config = TypesAndAccountsConfig::default();
114 let mut skip_errors = false;
115 let mut skip_events = false;
116
117 while let Some(arg) = args.next() {
118 if arg.starts_with("idl_path") {
119 match Self::remove_whitespace(arg)
120 .split("=")
121 .collect::<Vec<&str>>()[..]
122 {
123 [_, path] => {
124 if !path.ends_with(".json") {
125 panic!("Idl file needs to be in JSON format")
126 }
127 let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
128 idl_path = Some(
129 PathBuf::from_str(&cargo_manifest_dir)
130 .expect("Invalid idl_path arg")
131 .join(path),
132 );
133 }
134 _ => panic!("Invalid idl_path arg"),
135 }
136 continue;
137 }
138 if arg.starts_with("program_id") {
139 match Self::remove_whitespace(arg)
140 .split("=")
141 .collect::<Vec<&str>>()[..]
142 {
143 [_, program_id_str] => {
144 Pubkey::from_str(program_id_str)
145 .expect("program_id is not valid public key");
146 program_id = Some(program_id_str.to_owned());
147 }
148 _ => panic!("Invalid program_id arg"),
149 }
150 continue;
151 }
152
153 match arg {
154 "skip_errors" => {
155 skip_errors = true;
156 }
157 "skip_events" => {
158 skip_events = true;
159 }
160 _ => {
161 if arg.starts_with("zero_copy_unsafe") {
162 Self::parse_inside_parenthesis(
163 arg,
164 &mut args,
165 &mut types_and_accounts_config.zero_copy_unsafe,
166 "zero_copy_unsafe",
167 );
168 } else if arg.starts_with("zero_copy") {
169 Self::parse_inside_parenthesis(
170 arg,
171 &mut args,
172 &mut types_and_accounts_config.zero_copy,
173 "zero_copy",
174 );
175 } else if arg.starts_with("repr_c") {
176 Self::parse_inside_parenthesis(
177 arg,
178 &mut args,
179 &mut types_and_accounts_config.repr_c,
180 "repr_c",
181 );
182 } else if arg.starts_with("repr_packed") {
183 Self::parse_inside_parenthesis(
184 arg,
185 &mut args,
186 &mut types_and_accounts_config.repr_packed,
187 "repr_packed",
188 );
189 } else {
190 panic!("Invalid arg");
191 }
192 }
193 }
194 }
195
196 if idl_path.is_none() {
197 panic!("Missing idl_path arg");
198 }
199
200 if program_id.is_none() {
201 panic!("Missing program_id arg");
202 }
203
204 types_and_accounts_config.validate();
205
206 Self {
207 program_id: program_id.unwrap(),
208 idl_path: idl_path.unwrap(),
209 skip_errors,
210 skip_events,
211 types_and_accounts_config,
212 }
213 }
214}
215
216pub fn generate(args: Args) -> TokenStream {
217 let idl = &IdlJsonDefinition::read_idl(&args.idl_path);
218 let meta = Meta::from(idl);
219 let program_id = args.program_id;
220
221 let types = if idl.types.len() > 0 {
222 let types = state::generate(
223 idl,
224 &idl.types,
225 &args.types_and_accounts_config,
226 &meta,
227 false,
228 );
229 quote! {
230 pub mod types {
231 #types
232 }
233 }
234 } else {
235 quote! {}
236 };
237 let accounts = if idl.accounts.len() > 0 {
238 let accounts = state::generate(
239 idl,
240 &idl.accounts,
241 &args.types_and_accounts_config,
242 &meta,
243 true,
244 );
245 quote! {
246 pub mod accounts {
247 #accounts
248 }
249 }
250 } else {
251 quote! {}
252 };
253 let instructions = if idl.instructions.len() > 0 {
254 let instructions = instructions::generate(idl);
255 quote! {
256 pub mod instructions {
257 #instructions
258 }
259 }
260 } else {
261 quote! {}
262 };
263
264 let events = if !args.skip_events && idl.events.len() > 0 {
265 let events = events::generate(idl, &meta);
266 quote! {
267 #events
268 }
269 } else {
270 quote! {}
271 };
272
273 quote! {
274 anchor_lang::declare_id!(#program_id);
275
276 #types
277
278 #accounts
279
280 #instructions
281
282 #events
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use std::{env, path::PathBuf};
289
290 use crate::{Args, TypesAndAccountsConfig};
291
292 #[test]
293 fn parse_args() {
294 let args = "idl_path = \"idl.json\", program_id =\n\"NativeLoader1111111111111111111111111111111\", skip_errors,\nzero_copy_unsafe(PerpMarket, Amm, PoolBalance, InsuranceClaim),\nrepr_c(PerpMarket)".to_owned();
295 let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
296 let parsed = Args::parse(args);
297 let should_be = Args {
298 idl_path: PathBuf::from(cargo_manifest_dir).join("idl.json"),
299 program_id: "NativeLoader1111111111111111111111111111111".to_owned(),
300 skip_errors: true,
301 skip_events: false,
302 types_and_accounts_config: TypesAndAccountsConfig {
303 zero_copy: vec![],
304 zero_copy_unsafe: vec![
305 "PerpMarket".to_owned(),
306 "Amm".to_owned(),
307 "PoolBalance".to_owned(),
308 "InsuranceClaim".to_owned(),
309 ],
310 repr_c: vec!["PerpMarket".to_owned()],
311 repr_packed: vec![],
312 },
313 };
314
315 assert_eq!(parsed, should_be);
316 }
317
318 #[test]
319 #[should_panic]
320 fn parse_args_panic_1() {
321 Args::parse("idl_path = \"idl.json\",\nzero_copy_unsafe(PerpMarket,)".to_owned());
322 }
323
324 #[test]
325 #[should_panic]
326 fn parse_args_panic_2() {
327 Args::parse("idl_path = \"idl.json\",\nzero_copy_unsafe(PerpMarket)".to_owned());
328 }
329
330 #[test]
331 #[should_panic]
332 fn parse_args_panic_duplicates() {
333 Args::parse(
334 "idl_path = idl.json,\nzero_copy(PerpMarket),\nzero_copy_unsafe(PerpMarket), program_id =\n\"NativeLoader1111111111111111111111111111111\"".to_string(),
335 );
336 }
337}