alloy_sol_macro_input/
input.rs

1use ast::Spanned;
2use std::path::PathBuf;
3use syn::{
4    parse::{discouraged::Speculative, Parse, ParseStream},
5    Attribute, Error, Ident, LitStr, Result, Token,
6};
7
8/// Parsed input for `sol!`-like macro expanders. This enum represents a `Sol` file, a JSON ABI, or
9/// a Solidity type.
10#[derive(Clone, Debug)]
11pub enum SolInputKind {
12    /// Solidity type.
13    Type(ast::Type),
14    /// Solidity file or snippet.
15    Sol(ast::File),
16    /// JSON ABI file
17    #[cfg(feature = "json")]
18    Json(Ident, alloy_json_abi::ContractObject),
19}
20
21impl Parse for SolInputKind {
22    fn parse(input: ParseStream<'_>) -> Result<Self> {
23        let fork = input.fork();
24        match fork.parse() {
25            Ok(file) => {
26                input.advance_to(&fork);
27                Ok(Self::Sol(file))
28            }
29            Err(e) => match input.parse() {
30                Ok(ast::Type::Custom(_)) | Err(_) => Err(e),
31
32                Ok(ast::Type::Mapping(m)) => {
33                    Err(Error::new(m.span(), "mapping types are not yet supported"))
34                }
35
36                Ok(ty) => Ok(Self::Type(ty)),
37            },
38        }
39    }
40}
41
42/// Parsed input for `sol!`-like macro expanders. This struct represents a list
43/// of expandable items parsed from either solidity code snippets, or from a
44/// JSON abi.
45#[derive(Clone, Debug)]
46pub struct SolInput {
47    /// Attributes attached to the input, of the form `#[...]`.
48    pub attrs: Vec<Attribute>,
49    /// Path to the input, if any.
50    pub path: Option<PathBuf>,
51    /// The input kind.
52    pub kind: SolInputKind,
53}
54
55impl Parse for SolInput {
56    fn parse(input: ParseStream<'_>) -> Result<Self> {
57        let attrs = Attribute::parse_inner(input)?;
58
59        // Ignore outer attributes when peeking.
60        let fork = input.fork();
61        let _fork_outer = Attribute::parse_outer(&fork)?;
62
63        if fork.peek(LitStr) || (fork.peek(Ident) && fork.peek2(Token![,]) && fork.peek3(LitStr)) {
64            Self::parse_abigen(attrs, input)
65        } else {
66            input.parse().map(|kind| Self { attrs, path: None, kind })
67        }
68    }
69}
70
71impl SolInput {
72    /// `abigen`-like syntax: `sol!(name, "path/to/file")`
73    fn parse_abigen(mut attrs: Vec<Attribute>, input: ParseStream<'_>) -> Result<Self> {
74        attrs.extend(Attribute::parse_outer(input)?);
75
76        let name = input.parse::<Option<Ident>>()?;
77        if name.is_some() {
78            input.parse::<Token![,]>()?;
79        }
80        let lit = input.parse::<LitStr>()?;
81
82        let _ = input.parse::<Option<Token![,]>>()?;
83        if !input.is_empty() {
84            let msg = "unexpected token, expected end of input";
85            return Err(Error::new(input.span(), msg));
86        }
87
88        let mut value = lit.value();
89        let mut path = None;
90        let span = lit.span();
91
92        let is_path = {
93            let s = value.trim();
94            !(s.is_empty()
95                || (s.starts_with('{') && s.ends_with('}'))
96                || (s.starts_with('[') && s.ends_with(']')))
97        };
98        if is_path {
99            let mut p = PathBuf::from(value);
100            if p.is_relative() {
101                let dir = std::env::var_os("CARGO_MANIFEST_DIR")
102                    .map(PathBuf::from)
103                    .ok_or_else(|| Error::new(span, "failed to get manifest dir"))?;
104                p = dir.join(p);
105            }
106            p = dunce::canonicalize(&p)
107                .map_err(|e| Error::new(span, format!("failed to canonicalize path {p:?}: {e}")))?;
108            value = std::fs::read_to_string(&p)
109                .map_err(|e| Error::new(span, format!("failed to read file {p:?}: {e}")))?;
110            path = Some(p);
111        }
112
113        let s = value.trim();
114        if s.is_empty() {
115            let msg = if is_path { "file path is empty" } else { "empty input is not allowed" };
116            Err(Error::new(span, msg))
117        } else if (s.starts_with('{') && s.ends_with('}'))
118            || (s.starts_with('[') && s.ends_with(']'))
119        {
120            #[cfg(feature = "json")]
121            {
122                let json = serde_json::from_str(s)
123                    .map_err(|e| Error::new(span, format!("invalid JSON: {e}")))?;
124                let name = name.ok_or_else(|| Error::new(span, "need a name for JSON ABI"))?;
125                Ok(Self { attrs, path, kind: SolInputKind::Json(name, json) })
126            }
127            #[cfg(not(feature = "json"))]
128            {
129                let msg = "JSON support must be enabled with the \"json\" feature";
130                Err(Error::new(span, msg))
131            }
132        } else {
133            if let Some(name) = name {
134                let msg = "names are not allowed outside of JSON ABI, remove this name";
135                return Err(Error::new(name.span(), msg));
136            }
137            let kind = syn::parse_str(s).map_err(|e| {
138                let msg = format!("expected a valid JSON ABI string or Solidity string: {e}");
139                Error::new(span, msg)
140            })?;
141            Ok(Self { attrs, path, kind })
142        }
143    }
144}