Skip to main content

near_sdk_abi/
lib.rs

1use anyhow::{anyhow, Result};
2use convert_case::{Case, Casing};
3use near_sdk_abi_impl::{generate_ext, read_abi};
4use quote::{format_ident, quote};
5use std::fs::File;
6use std::io::Write;
7use std::path::PathBuf;
8use std::{env, fs};
9
10pub use near_sdk_abi_macros::near_abi_ext;
11
12pub struct AbiFile {
13    /// Path to the ABI JSON file.
14    pub path: PathBuf,
15    /// Contract name to be used for the resulting trait name.
16    /// If missing will try to pull the name from ABI metadata and use `Ext<ContractName>`.
17    pub contract_name: Option<String>,
18    /// mod name to be used for the resulting ext mod.
19    /// If missing will be derived by applying snake case to the contract name, e.g. ext_status_message.
20    pub mod_name: Option<String>,
21}
22
23impl AbiFile {
24    pub fn new(path: impl Into<PathBuf>) -> Self {
25        AbiFile {
26            path: path.into(),
27            contract_name: None,
28            mod_name: None,
29        }
30    }
31}
32
33/// Configuration options for ABI code generation.
34#[derive(Default)]
35pub struct Generator {
36    out_dir: Option<PathBuf>,
37    abis: Vec<AbiFile>,
38}
39
40impl Generator {
41    pub fn new(out_dir: PathBuf) -> Self {
42        Generator {
43            out_dir: Some(out_dir),
44            abis: vec![],
45        }
46    }
47
48    pub fn file(mut self, abi_file: AbiFile) -> Self {
49        self.abis.push(abi_file);
50        self
51    }
52
53    pub fn generate(self) -> Result<()> {
54        let target: PathBuf = self.out_dir.map(Ok).unwrap_or_else(|| {
55            env::var_os("OUT_DIR")
56                .ok_or_else(|| anyhow!("OUT_DIR environment variable is not set"))
57                .map(Into::into)
58        })?;
59        fs::create_dir_all(&target)?;
60
61        for AbiFile {
62            path,
63            contract_name,
64            mod_name,
65        } in self.abis
66        {
67            let abi_path_no_ext = path.with_extension("");
68            let abi_filename = abi_path_no_ext
69                .file_name()
70                .ok_or_else(|| anyhow!("{:?} is not a valid ABI path", path.display()))?;
71            let rust_path = target.join(abi_filename).with_extension("rs");
72
73            let near_abi = read_abi(&path);
74
75            let contract_name = contract_name
76                .as_ref()
77                .map(|n| format_ident!("{}", n))
78                .or_else(|| {
79                    near_abi
80                        .metadata
81                        .name
82                        .clone()
83                        .map(|n| format_ident!("Ext{}", n.to_case(Case::UpperCamel)))
84                })
85                .ok_or_else(|| {
86                    anyhow!(
87                        "ABI file '{}' does not contain a contract name. Please supply the name via `file_with_name`.",
88                        path.display()
89                    )
90                })?;
91
92            let token_stream = generate_ext(
93                near_abi,
94                contract_name,
95                mod_name.map(|n| format_ident!("{}", n)),
96            );
97            let token_stream = quote! {
98                #![allow(unused_imports)]
99                use serde::{Deserialize, Serialize};
100                #token_stream
101            };
102            let syntax_tree = syn::parse_file(&token_stream.to_string()).unwrap();
103            let formatted = prettyplease::unparse(&syntax_tree);
104
105            let mut rust_file = File::create(rust_path)?;
106            write!(rust_file, "{}", formatted)?;
107        }
108
109        Ok(())
110    }
111}