ethers_core/macros/
ethers_crate.rs

1use cargo_metadata::MetadataCommand;
2use once_cell::sync::Lazy;
3use std::{
4    collections::HashMap,
5    env, fmt, fs,
6    path::{Path, PathBuf},
7};
8use strum::{EnumCount, EnumIter, EnumString, IntoEnumIterator, VariantNames};
9
10/// `ethers_crate => name`
11type CrateNames = HashMap<EthersCrate, &'static str>;
12
13const DIRS: [&str; 3] = ["benches", "examples", "tests"];
14
15/// Maps an [`EthersCrate`] to its path string.
16///
17/// See [`ProjectEnvironment`] for more information.
18///
19/// Note: this static variable cannot hold [`syn::Path`] because it is not [`Sync`], so the names
20/// must be parsed at every call.
21static ETHERS_CRATE_NAMES: Lazy<CrateNames> = Lazy::new(|| {
22    ProjectEnvironment::new_from_env()
23        .and_then(|x| x.determine_ethers_crates())
24        .unwrap_or_else(|| EthersCrate::ethers_path_names().collect())
25});
26
27/// Returns the `core` crate's [`Path`][syn::Path].
28#[inline]
29pub fn ethers_core_crate() -> syn::Path {
30    get_crate_path(EthersCrate::EthersCore)
31}
32
33/// Returns the `contract` crate's [`Path`][syn::Path].
34#[inline]
35pub fn ethers_contract_crate() -> syn::Path {
36    get_crate_path(EthersCrate::EthersContract)
37}
38
39/// Returns the `providers` crate's [`Path`][syn::Path].
40#[inline]
41pub fn ethers_providers_crate() -> syn::Path {
42    get_crate_path(EthersCrate::EthersProviders)
43}
44
45/// Returns an [`EthersCrate`]'s [`Path`][syn::Path] in the current project.
46#[inline(always)]
47pub fn get_crate_path(krate: EthersCrate) -> syn::Path {
48    krate.get_path()
49}
50
51/// Represents a generic Rust/Cargo project's environment.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct ProjectEnvironment {
54    manifest_dir: PathBuf,
55    crate_name: Option<String>,
56}
57
58impl ProjectEnvironment {
59    /// Creates a new instance using the given manifest dir and crate name.
60    pub fn new<T: Into<PathBuf>, U: Into<String>>(manifest_dir: T, crate_name: U) -> Self {
61        Self { manifest_dir: manifest_dir.into(), crate_name: Some(crate_name.into()) }
62    }
63
64    /// Creates a new instance using the the `CARGO_MANIFEST_DIR` and `CARGO_CRATE_NAME` environment
65    /// variables.
66    pub fn new_from_env() -> Option<Self> {
67        Some(Self {
68            manifest_dir: env::var_os("CARGO_MANIFEST_DIR")?.into(),
69            crate_name: env::var("CARGO_CRATE_NAME").ok(),
70        })
71    }
72
73    /// Determines the crate paths to use by looking at the [metadata][cargo_metadata] of the
74    /// project.
75    ///
76    /// The names will be:
77    /// - `ethers::*` if `ethers` is a dependency for all crates;
78    /// - for each `crate`:
79    ///   - `ethers_<crate>` if it is a dependency, otherwise `ethers::<crate>`.
80    #[inline]
81    pub fn determine_ethers_crates(&self) -> Option<CrateNames> {
82        let lock_file = self.manifest_dir.join("Cargo.lock");
83        let lock_file_existed = lock_file.exists();
84
85        let names = self.crate_names_from_metadata();
86
87        // remove the lock file created from running the command
88        if !lock_file_existed && lock_file.exists() {
89            let _ = std::fs::remove_file(lock_file);
90        }
91
92        names
93    }
94
95    #[inline]
96    fn crate_names_from_metadata(&self) -> Option<CrateNames> {
97        let metadata = MetadataCommand::new().current_dir(&self.manifest_dir).exec().ok()?;
98        let pkg = metadata.root_package()?;
99
100        // return ethers_* if the root package is an internal ethers crate since `ethers` is not
101        // available
102        if pkg.name.parse::<EthersCrate>().is_ok() || pkg.name == "ethers" {
103            return Some(EthersCrate::path_names().collect())
104        }
105
106        let mut names: CrateNames = EthersCrate::ethers_path_names().collect();
107        for dep in pkg.dependencies.iter() {
108            let name = dep.name.as_str();
109            if name.starts_with("ethers") {
110                if name == "ethers" {
111                    return None
112                } else if let Ok(dep) = name.parse::<EthersCrate>() {
113                    names.insert(dep, dep.path_name());
114                }
115            }
116        }
117        Some(names)
118    }
119
120    /// Returns whether the `crate` path identifier refers to the root package.
121    ///
122    /// This is false for integration tests, benches, and examples, as the `crate` keyword will not
123    /// refer to the root package.
124    ///
125    /// We can find this using some [environment variables set by Cargo during compilation][ref]:
126    /// - `CARGO_TARGET_TMPDIR` is only set when building integration test or benchmark code;
127    /// - When `CARGO_MANIFEST_DIR` contains `/benches/` or `/examples/`
128    /// - `CARGO_CRATE_NAME`, see `is_crate_name_in_dirs`.
129    ///
130    /// [ref]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
131    #[inline]
132    pub fn is_crate_root(&self) -> bool {
133        env::var_os("CARGO_TARGET_TMPDIR").is_none() &&
134            self.manifest_dir.components().all(|c| {
135                let s = c.as_os_str();
136                s != "examples" && s != "benches"
137            }) &&
138            !self.is_crate_name_in_dirs()
139    }
140
141    /// Returns whether `crate_name` is the name of a file or directory in the first level of
142    /// `manifest_dir/{benches,examples,tests}/`.
143    ///
144    /// # Example
145    ///
146    /// With this project structure:
147    ///
148    /// ```text
149    /// .
150    /// ├── Cargo.lock
151    /// ├── Cargo.toml
152    /// ├── src/
153    /// │   ...
154    /// ├── benches/
155    /// │   ├── large-input.rs
156    /// │   └── multi-file-bench/
157    /// │       ├── main.rs
158    /// │       └── bench_module.rs
159    /// ├── examples/
160    /// │   ├── simple.rs
161    /// │   └── multi-file-example/
162    /// │       ├── main.rs
163    /// │       └── ex_module.rs
164    /// └── tests/
165    ///     ├── some-integration-tests.rs
166    ///     └── multi-file-test/
167    ///         ├── main.rs
168    ///         └── test_module.rs
169    /// ```
170    ///
171    /// The resulting `CARGO_CRATE_NAME` values will be:
172    ///
173    /// |                  Path                  |          Value         |
174    /// |:-------------------------------------- | ----------------------:|
175    /// | benches/large-input.rs                 |            large-input |
176    /// | benches/multi-file-bench/\*\*/\*.rs    |       multi-file-bench |
177    /// | examples/simple.rs                     |                 simple |
178    /// | examples/multi-file-example/\*\*/\*.rs |     multi-file-example |
179    /// | tests/some-integration-tests.rs        | some-integration-tests |
180    /// | tests/multi-file-test/\*\*/\*.rs       |        multi-file-test |
181    #[inline]
182    pub fn is_crate_name_in_dirs(&self) -> bool {
183        let crate_name = match self.crate_name.as_ref() {
184            Some(name) => name,
185            None => return false,
186        };
187        let dirs = DIRS.map(|dir| self.manifest_dir.join(dir));
188        dirs.iter().any(|dir| {
189            fs::read_dir(dir)
190                .ok()
191                .and_then(|entries| {
192                    entries
193                        .filter_map(Result::ok)
194                        .find(|entry| file_stem_eq(entry.path(), crate_name))
195                })
196                .is_some()
197        })
198    }
199}
200
201/// An `ethers-rs` internal crate.
202#[derive(
203    Clone,
204    Copy,
205    Debug,
206    PartialEq,
207    Eq,
208    PartialOrd,
209    Ord,
210    Hash,
211    EnumCount,
212    EnumIter,
213    EnumString,
214    VariantNames,
215)]
216#[strum(serialize_all = "kebab-case")]
217pub enum EthersCrate {
218    EthersAddressbook,
219    EthersContract,
220    EthersContractAbigen,
221    EthersContractDerive,
222    EthersCore,
223    EthersEtherscan,
224    EthersMiddleware,
225    EthersProviders,
226    EthersSigners,
227    EthersSolc,
228}
229
230impl AsRef<str> for EthersCrate {
231    fn as_ref(&self) -> &str {
232        self.crate_name()
233    }
234}
235
236impl fmt::Display for EthersCrate {
237    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
238        f.pad(self.as_ref())
239    }
240}
241
242impl EthersCrate {
243    /// "`<self as kebab-case>`"
244    #[inline]
245    pub const fn crate_name(self) -> &'static str {
246        match self {
247            Self::EthersAddressbook => "ethers-addressbook",
248            Self::EthersContract => "ethers-contract",
249            Self::EthersContractAbigen => "ethers-contract-abigen",
250            Self::EthersContractDerive => "ethers-contract-derive",
251            Self::EthersCore => "ethers-core",
252            Self::EthersEtherscan => "ethers-etherscan",
253            Self::EthersMiddleware => "ethers-middleware",
254            Self::EthersProviders => "ethers-providers",
255            Self::EthersSigners => "ethers-signers",
256            Self::EthersSolc => "ethers-solc",
257        }
258    }
259
260    /// "`::<self as snake_case>`"
261    #[inline]
262    pub const fn path_name(self) -> &'static str {
263        match self {
264            Self::EthersAddressbook => "::ethers_addressbook",
265            Self::EthersContract => "::ethers_contract",
266            Self::EthersContractAbigen => "::ethers_contract_abigen",
267            Self::EthersContractDerive => "::ethers_contract_derive",
268            Self::EthersCore => "::ethers_core",
269            Self::EthersEtherscan => "::ethers_etherscan",
270            Self::EthersMiddleware => "::ethers_middleware",
271            Self::EthersProviders => "::ethers_providers",
272            Self::EthersSigners => "::ethers_signers",
273            Self::EthersSolc => "::ethers_solc",
274        }
275    }
276
277    /// "::ethers::`<self in ethers>`"
278    #[inline]
279    pub const fn ethers_path_name(self) -> &'static str {
280        match self {
281            // re-exported in ethers::contract
282            Self::EthersContractAbigen => "::ethers::contract", // partially
283            Self::EthersContractDerive => "::ethers::contract",
284
285            Self::EthersAddressbook => "::ethers::addressbook",
286            Self::EthersContract => "::ethers::contract",
287            Self::EthersCore => "::ethers::core",
288            Self::EthersEtherscan => "::ethers::etherscan",
289            Self::EthersMiddleware => "::ethers::middleware",
290            Self::EthersProviders => "::ethers::providers",
291            Self::EthersSigners => "::ethers::signers",
292            Self::EthersSolc => "::ethers::solc",
293        }
294    }
295
296    /// The path on the file system, from an `ethers-rs` root directory.
297    #[inline]
298    pub const fn fs_path(self) -> &'static str {
299        match self {
300            Self::EthersContractAbigen => "ethers-contract/ethers-contract-abigen",
301            Self::EthersContractDerive => "ethers-contract/ethers-contract-derive",
302            _ => self.crate_name(),
303        }
304    }
305
306    /// `<ethers_*>`
307    #[inline]
308    pub fn path_names() -> impl Iterator<Item = (Self, &'static str)> {
309        Self::iter().map(|x| (x, x.path_name()))
310    }
311
312    /// `<ethers::*>`
313    #[inline]
314    pub fn ethers_path_names() -> impl Iterator<Item = (Self, &'static str)> {
315        Self::iter().map(|x| (x, x.ethers_path_name()))
316    }
317
318    /// Returns the [`Path`][syn::Path] in the current project.
319    #[inline]
320    pub fn get_path(&self) -> syn::Path {
321        let name = ETHERS_CRATE_NAMES[self];
322        syn::parse_str(name).unwrap()
323    }
324}
325
326/// `path.file_stem() == s`
327#[inline]
328fn file_stem_eq<T: AsRef<Path>, U: AsRef<str>>(path: T, s: U) -> bool {
329    if let Some(stem) = path.as_ref().file_stem() {
330        if let Some(stem) = stem.to_str() {
331            return stem == s.as_ref()
332        }
333    }
334    false
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use rand::{
341        distributions::{Distribution, Standard},
342        thread_rng, Rng,
343    };
344    use std::{
345        collections::{BTreeMap, HashSet},
346        env,
347    };
348    use tempfile::TempDir;
349
350    impl Distribution<EthersCrate> for Standard {
351        fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> EthersCrate {
352            const RANGE: std::ops::Range<u8> = 0..EthersCrate::COUNT as u8;
353            // SAFETY: generates in the safe range
354            unsafe { std::mem::transmute(rng.gen_range(RANGE)) }
355        }
356    }
357
358    #[test]
359    #[ignore = "TODO: flaky and slow"]
360    fn test_names() {
361        fn assert_names(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
362            write_manifest(s, ethers, dependencies);
363
364            // speeds up consecutive runs by not having to re-create and delete the lockfile
365            // this is tested separately: test_lock_file
366            std::fs::write(s.manifest_dir.join("Cargo.lock"), "").unwrap();
367
368            let names = s
369                .determine_ethers_crates()
370                .unwrap_or_else(|| EthersCrate::ethers_path_names().collect());
371
372            let krate = s.crate_name.as_ref().and_then(|x| x.parse::<EthersCrate>().ok());
373            let is_internal = krate.is_some();
374            let expected: CrateNames = match (is_internal, ethers) {
375                // internal
376                (true, _) => EthersCrate::path_names().collect(),
377
378                // ethers
379                (_, true) => EthersCrate::ethers_path_names().collect(),
380
381                // no ethers
382                (_, false) => {
383                    let mut n: CrateNames = EthersCrate::ethers_path_names().collect();
384                    for &dep in dependencies {
385                        n.insert(dep, dep.path_name());
386                    }
387                    n
388                }
389            };
390
391            // don't use assert for a better custom message
392            if names != expected {
393                // BTreeMap sorts the keys
394                let names: BTreeMap<_, _> = names.into_iter().collect();
395                let expected: BTreeMap<_, _> = expected.into_iter().collect();
396                panic!("\nCase failed: (`{:?}`, `{ethers}`, `{dependencies:?}`)\nNames: {names:#?}\nExpected: {expected:#?}\n", s.crate_name);
397            }
398        }
399
400        fn gen_unique<const N: usize>() -> [EthersCrate; N] {
401            assert!(N < EthersCrate::COUNT);
402            let rng = &mut thread_rng();
403            let mut set = HashSet::with_capacity(N);
404            while set.len() < N {
405                set.insert(rng.gen());
406            }
407            let vec: Vec<_> = set.into_iter().collect();
408            vec.try_into().unwrap()
409        }
410
411        let (s, _dir) = test_project();
412        // crate_name        -> represents an external crate
413        // "ethers-contract" -> represents an internal crate
414        for name in [s.crate_name.as_ref().unwrap(), "ethers-contract"] {
415            let s = ProjectEnvironment::new(&s.manifest_dir, name);
416            // only ethers
417            assert_names(&s, true, &[]);
418
419            // only others
420            assert_names(&s, false, gen_unique::<3>().as_slice());
421
422            // ethers and others
423            assert_names(&s, true, gen_unique::<3>().as_slice());
424        }
425    }
426
427    #[test]
428    #[ignore = "TODO: flaky and slow"]
429    fn test_lock_file() {
430        let (s, _dir) = test_project();
431        write_manifest(&s, true, &[]);
432        let lock_file = s.manifest_dir.join("Cargo.lock");
433
434        assert!(!lock_file.exists());
435        s.determine_ethers_crates();
436        assert!(!lock_file.exists());
437
438        std::fs::write(&lock_file, "").unwrap();
439
440        assert!(lock_file.exists());
441        s.determine_ethers_crates();
442        assert!(lock_file.exists());
443        assert!(!std::fs::read(lock_file).unwrap().is_empty());
444    }
445
446    #[test]
447    fn test_is_crate_root() {
448        let (s, _dir) = test_project();
449        assert!(s.is_crate_root());
450
451        // `CARGO_MANIFEST_DIR`
452        // complex path has `/{dir_name}/` in the path
453        // name or path validity not checked
454        let s = ProjectEnvironment::new(
455            s.manifest_dir.join("examples/complex_examples"),
456            "complex-examples",
457        );
458        assert!(!s.is_crate_root());
459        let s = ProjectEnvironment::new(
460            s.manifest_dir.join("benches/complex_benches"),
461            "complex-benches",
462        );
463        assert!(!s.is_crate_root());
464    }
465
466    #[test]
467    fn test_is_crate_name_in_dirs() {
468        let (s, _dir) = test_project();
469        let root = &s.manifest_dir;
470
471        for dir_name in DIRS {
472            for ty in ["simple", "complex"] {
473                let s = ProjectEnvironment::new(root, format!("{ty}_{dir_name}"));
474                assert!(s.is_crate_name_in_dirs(), "{s:?}");
475            }
476        }
477
478        let s = ProjectEnvironment::new(root, "non_existant");
479        assert!(!s.is_crate_name_in_dirs());
480        let s = ProjectEnvironment::new(root.join("does-not-exist"), "foo_bar");
481        assert!(!s.is_crate_name_in_dirs());
482    }
483
484    #[test]
485    fn test_file_stem_eq() {
486        let path = Path::new("/tmp/foo.rs");
487        assert!(file_stem_eq(path, "foo"));
488        assert!(!file_stem_eq(path, "tmp"));
489        assert!(!file_stem_eq(path, "foo.rs"));
490        assert!(!file_stem_eq(path, "fo"));
491        assert!(!file_stem_eq(path, "f"));
492        assert!(!file_stem_eq(path, ""));
493
494        let path = Path::new("/tmp/foo/");
495        assert!(file_stem_eq(path, "foo"));
496        assert!(!file_stem_eq(path, "tmp"));
497        assert!(!file_stem_eq(path, "fo"));
498        assert!(!file_stem_eq(path, "f"));
499        assert!(!file_stem_eq(path, ""));
500    }
501
502    // utils
503
504    /// Creates:
505    ///
506    /// ```text
507    /// - new_dir
508    ///   - src
509    ///     - main.rs
510    ///   - {dir_name} for dir_name in DIRS
511    ///     - simple_{dir_name}.rs
512    ///     - complex_{dir_name}
513    ///       - src if not "tests"
514    ///         - main.rs
515    ///         - module.rs
516    /// ```
517    fn test_project() -> (ProjectEnvironment, TempDir) {
518        // change the prefix to one without the default `.` because it is not a valid crate name
519        let dir = tempfile::Builder::new().prefix("tmp").tempdir().unwrap();
520        let root = dir.path();
521        let name = root.file_name().unwrap().to_str().unwrap();
522
523        // No Cargo.toml, git
524        fs::create_dir_all(root).unwrap();
525        let src = root.join("src");
526        fs::create_dir(&src).unwrap();
527        fs::write(src.join("main.rs"), "fn main(){}").unwrap();
528
529        for dir_name in DIRS {
530            let new_dir = root.join(dir_name);
531            fs::create_dir(&new_dir).unwrap();
532
533            let simple = new_dir.join(format!("simple_{dir_name}.rs"));
534            fs::write(simple, "").unwrap();
535
536            let mut complex = new_dir.join(format!("complex_{dir_name}"));
537            if dir_name != "tests" {
538                fs::create_dir(&complex).unwrap();
539                fs::write(complex.join("Cargo.toml"), "").unwrap();
540                complex.push("src");
541            }
542            fs::create_dir(&complex).unwrap();
543            fs::write(complex.join("main.rs"), "").unwrap();
544            fs::write(complex.join("module.rs"), "").unwrap();
545        }
546
547        // create target dirs
548        let target = root.join("target");
549        fs::create_dir(&target).unwrap();
550        fs::create_dir_all(target.join("tmp")).unwrap();
551
552        (ProjectEnvironment::new(root, name), dir)
553    }
554
555    /// Writes a test manifest to `{root}/Cargo.toml`.
556    fn write_manifest(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
557        // use paths to avoid downloading dependencies
558        const ETHERS_CORE: &str = env!("CARGO_MANIFEST_DIR");
559        let ethers_root = Path::new(ETHERS_CORE).parent().unwrap();
560        let mut dependencies_toml =
561            String::with_capacity(150 * (ethers as usize + dependencies.len()));
562
563        if ethers {
564            let path = ethers_root.join("ethers");
565            let ethers = format!("ethers = {{ path = \"{}\" }}\n", path.display());
566            dependencies_toml.push_str(&ethers);
567        }
568
569        for dep in dependencies.iter() {
570            let path = ethers_root.join(dep.fs_path());
571            let dep = format!("{dep} = {{ path = \"{}\" }}\n", path.display());
572            dependencies_toml.push_str(&dep);
573        }
574
575        let contents = format!(
576            r#"
577[package]
578name = "{}"
579version = "0.0.0"
580edition = "2021"
581
582[dependencies]
583{dependencies_toml}
584"#,
585            s.crate_name.as_ref().unwrap()
586        );
587        fs::write(s.manifest_dir.join("Cargo.toml"), contents).unwrap();
588    }
589}