ethers-rs/ethers-core/src/macros/ethers_crate.rs

593 lines
20 KiB
Rust

use cargo_metadata::MetadataCommand;
use once_cell::sync::Lazy;
use std::{
collections::HashMap,
env, fmt, fs,
path::{Path, PathBuf},
};
use strum::{EnumCount, EnumIter, EnumString, EnumVariantNames, IntoEnumIterator};
/// `ethers_crate => name`
type CrateNames = HashMap<EthersCrate, &'static str>;
const DIRS: [&str; 3] = ["benches", "examples", "tests"];
/// Maps an [`EthersCrate`] to its path string.
///
/// See [`ProjectEnvironment`] for more information.
///
/// Note: this static variable cannot hold [`syn::Path`] because it is not [`Sync`], so the names
/// must be parsed at every call.
static ETHERS_CRATE_NAMES: Lazy<CrateNames> = Lazy::new(|| {
ProjectEnvironment::new_from_env()
.and_then(|x| x.determine_ethers_crates())
.unwrap_or_else(|| EthersCrate::ethers_path_names().collect())
});
/// Returns the `core` crate's [`Path`][syn::Path].
#[inline]
pub fn ethers_core_crate() -> syn::Path {
get_crate_path(EthersCrate::EthersCore)
}
/// Returns the `contract` crate's [`Path`][syn::Path].
#[inline]
pub fn ethers_contract_crate() -> syn::Path {
get_crate_path(EthersCrate::EthersContract)
}
/// Returns the `providers` crate's [`Path`][syn::Path].
#[inline]
pub fn ethers_providers_crate() -> syn::Path {
get_crate_path(EthersCrate::EthersProviders)
}
/// Returns an [`EthersCrate`]'s [`Path`][syn::Path] in the current project.
#[inline(always)]
pub fn get_crate_path(krate: EthersCrate) -> syn::Path {
krate.get_path()
}
/// Represents a generic Rust/Cargo project's environment.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProjectEnvironment {
manifest_dir: PathBuf,
crate_name: Option<String>,
}
impl ProjectEnvironment {
/// Creates a new instance using the given manifest dir and crate name.
pub fn new<T: Into<PathBuf>, U: Into<String>>(manifest_dir: T, crate_name: U) -> Self {
Self { manifest_dir: manifest_dir.into(), crate_name: Some(crate_name.into()) }
}
/// Creates a new instance using the the `CARGO_MANIFEST_DIR` and `CARGO_CRATE_NAME` environment
/// variables.
pub fn new_from_env() -> Option<Self> {
Some(Self {
manifest_dir: env::var_os("CARGO_MANIFEST_DIR")?.into(),
crate_name: env::var("CARGO_CRATE_NAME").ok(),
})
}
/// Determines the crate paths to use by looking at the [metadata][cargo_metadata] of the
/// project.
///
/// The names will be:
/// - `ethers::*` if `ethers` is a dependency for all crates;
/// - for each `crate`:
/// - `ethers_<crate>` if it is a dependency, otherwise `ethers::<crate>`.
#[inline]
pub fn determine_ethers_crates(&self) -> Option<CrateNames> {
let lock_file = self.manifest_dir.join("Cargo.lock");
let lock_file_existed = lock_file.exists();
let names = self.crate_names_from_metadata();
// remove the lock file created from running the command
if !lock_file_existed && lock_file.exists() {
let _ = std::fs::remove_file(lock_file);
}
names
}
#[inline]
fn crate_names_from_metadata(&self) -> Option<CrateNames> {
let metadata = MetadataCommand::new().current_dir(&self.manifest_dir).exec().ok()?;
let pkg = metadata.root_package()?;
// return ethers_* if the root package is an internal ethers crate since `ethers` is not
// available
if pkg.name.parse::<EthersCrate>().is_ok() || pkg.name == "ethers" {
return Some(EthersCrate::path_names().collect())
}
let mut names: CrateNames = EthersCrate::ethers_path_names().collect();
for dep in pkg.dependencies.iter() {
let name = dep.name.as_str();
if name.starts_with("ethers") {
if name == "ethers" {
return None
} else if let Ok(dep) = name.parse::<EthersCrate>() {
names.insert(dep, dep.path_name());
}
}
}
Some(names)
}
/// Returns whether the `crate` path identifier refers to the root package.
///
/// This is false for integration tests, benches, and examples, as the `crate` keyword will not
/// refer to the root package.
///
/// We can find this using some [environment variables set by Cargo during compilation][ref]:
/// - `CARGO_TARGET_TMPDIR` is only set when building integration test or benchmark code;
/// - When `CARGO_MANIFEST_DIR` contains `/benches/` or `/examples/`
/// - `CARGO_CRATE_NAME`, see `is_crate_name_in_dirs`.
///
/// [ref]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
#[inline]
pub fn is_crate_root(&self) -> bool {
env::var_os("CARGO_TARGET_TMPDIR").is_none() &&
self.manifest_dir.components().all(|c| {
let s = c.as_os_str();
s != "examples" && s != "benches"
}) &&
!self.is_crate_name_in_dirs()
}
/// Returns whether `crate_name` is the name of a file or directory in the first level of
/// `manifest_dir/{benches,examples,tests}/`.
///
/// # Example
///
/// With this project structure:
///
/// ```text
/// .
/// ├── Cargo.lock
/// ├── Cargo.toml
/// ├── src/
/// │   ...
/// ├── benches/
/// │   ├── large-input.rs
/// │   └── multi-file-bench/
/// │   ├── main.rs
/// │   └── bench_module.rs
/// ├── examples/
/// │   ├── simple.rs
/// │   └── multi-file-example/
/// │   ├── main.rs
/// │   └── ex_module.rs
/// └── tests/
/// ├── some-integration-tests.rs
/// └── multi-file-test/
/// ├── main.rs
/// └── test_module.rs
/// ```
///
/// The resulting `CARGO_CRATE_NAME` values will be:
///
/// | Path | Value |
/// |:-------------------------------------- | ----------------------:|
/// | benches/large-input.rs | large-input |
/// | benches/multi-file-bench/\*\*/\*.rs | multi-file-bench |
/// | examples/simple.rs | simple |
/// | examples/multi-file-example/\*\*/\*.rs | multi-file-example |
/// | tests/some-integration-tests.rs | some-integration-tests |
/// | tests/multi-file-test/\*\*/\*.rs | multi-file-test |
#[inline]
pub fn is_crate_name_in_dirs(&self) -> bool {
let crate_name = match self.crate_name.as_ref() {
Some(name) => name,
None => return false,
};
let dirs = DIRS.map(|dir| self.manifest_dir.join(dir));
dirs.iter().any(|dir| {
fs::read_dir(dir)
.ok()
.and_then(|entries| {
entries
.filter_map(Result::ok)
.find(|entry| file_stem_eq(entry.path(), crate_name))
})
.is_some()
})
}
}
/// An `ethers-rs` internal crate.
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
EnumCount,
EnumIter,
EnumString,
EnumVariantNames,
)]
#[strum(serialize_all = "kebab-case")]
pub enum EthersCrate {
EthersAddressbook,
EthersContract,
EthersContractAbigen,
EthersContractDerive,
EthersCore,
EthersDeriveEip712,
EthersEtherscan,
EthersMiddleware,
EthersProviders,
EthersSigners,
EthersSolc,
}
impl AsRef<str> for EthersCrate {
fn as_ref(&self) -> &str {
self.crate_name()
}
}
impl fmt::Display for EthersCrate {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(self.as_ref())
}
}
impl EthersCrate {
/// "`<self as kebab-case>`"
#[inline]
pub const fn crate_name(self) -> &'static str {
match self {
Self::EthersAddressbook => "ethers-addressbook",
Self::EthersContract => "ethers-contract",
Self::EthersContractAbigen => "ethers-contract-abigen",
Self::EthersContractDerive => "ethers-contract-derive",
Self::EthersCore => "ethers-core",
Self::EthersDeriveEip712 => "ethers-derive-eip712",
Self::EthersEtherscan => "ethers-etherscan",
Self::EthersMiddleware => "ethers-middleware",
Self::EthersProviders => "ethers-providers",
Self::EthersSigners => "ethers-signers",
Self::EthersSolc => "ethers-solc",
}
}
/// "`::<self as snake_case>`"
#[inline]
pub const fn path_name(self) -> &'static str {
match self {
Self::EthersAddressbook => "::ethers_addressbook",
Self::EthersContract => "::ethers_contract",
Self::EthersContractAbigen => "::ethers_contract_abigen",
Self::EthersContractDerive => "::ethers_contract_derive",
Self::EthersCore => "::ethers_core",
Self::EthersDeriveEip712 => "::ethers_derive_eip712",
Self::EthersEtherscan => "::ethers_etherscan",
Self::EthersMiddleware => "::ethers_middleware",
Self::EthersProviders => "::ethers_providers",
Self::EthersSigners => "::ethers_signers",
Self::EthersSolc => "::ethers_solc",
}
}
/// "::ethers::`<self in ethers>`"
#[inline]
pub const fn ethers_path_name(self) -> &'static str {
match self {
// re-exported in ethers::contract
Self::EthersContractAbigen => "::ethers::contract", // partially
Self::EthersContractDerive => "::ethers::contract",
Self::EthersDeriveEip712 => "::ethers::contract",
Self::EthersAddressbook => "::ethers::addressbook",
Self::EthersContract => "::ethers::contract",
Self::EthersCore => "::ethers::core",
Self::EthersEtherscan => "::ethers::etherscan",
Self::EthersMiddleware => "::ethers::middleware",
Self::EthersProviders => "::ethers::providers",
Self::EthersSigners => "::ethers::signers",
Self::EthersSolc => "::ethers::solc",
}
}
/// The path on the file system, from an `ethers-rs` root directory.
#[inline]
pub const fn fs_path(self) -> &'static str {
match self {
Self::EthersContractAbigen => "ethers-contract/ethers-contract-abigen",
Self::EthersContractDerive => "ethers-contract/ethers-contract-derive",
Self::EthersDeriveEip712 => "ethers-core/ethers-derive-eip712",
_ => self.crate_name(),
}
}
/// `<ethers_*>`
#[inline]
pub fn path_names() -> impl Iterator<Item = (Self, &'static str)> {
Self::iter().map(|x| (x, x.path_name()))
}
/// `<ethers::*>`
#[inline]
pub fn ethers_path_names() -> impl Iterator<Item = (Self, &'static str)> {
Self::iter().map(|x| (x, x.ethers_path_name()))
}
/// Returns the [`Path`][syn::Path] in the current project.
#[inline]
pub fn get_path(&self) -> syn::Path {
let name = ETHERS_CRATE_NAMES[self];
syn::parse_str(name).unwrap()
}
}
/// `path.file_stem() == s`
#[inline]
fn file_stem_eq<T: AsRef<Path>, U: AsRef<str>>(path: T, s: U) -> bool {
if let Some(stem) = path.as_ref().file_stem() {
if let Some(stem) = stem.to_str() {
return stem == s.as_ref()
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{
distributions::{Distribution, Standard},
thread_rng, Rng,
};
use std::{
collections::{BTreeMap, HashSet},
env, fs,
};
use tempfile::TempDir;
impl Distribution<EthersCrate> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> EthersCrate {
const RANGE: std::ops::Range<u8> = 0..EthersCrate::COUNT as u8;
// SAFETY: generates in the safe range
unsafe { std::mem::transmute(rng.gen_range(RANGE)) }
}
}
#[test]
fn test_names() {
fn assert_names(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
write_manifest(s, ethers, dependencies);
// speeds up consecutive runs by not having to re-create and delete the lockfile
// this is tested separately: test_lock_file
std::fs::write(s.manifest_dir.join("Cargo.lock"), "").unwrap();
let names = s
.determine_ethers_crates()
.unwrap_or_else(|| EthersCrate::ethers_path_names().collect());
let krate = s.crate_name.as_ref().and_then(|x| x.parse::<EthersCrate>().ok());
let is_internal = krate.is_some();
let expected: CrateNames = match (is_internal, ethers) {
// internal
(true, _) => EthersCrate::path_names().collect(),
// ethers
(_, true) => EthersCrate::ethers_path_names().collect(),
// no ethers
(_, false) => {
let mut n: CrateNames = EthersCrate::ethers_path_names().collect();
for &dep in dependencies {
n.insert(dep, dep.path_name());
}
n
}
};
// don't use assert for a better custom message
if names != expected {
// BTreeMap sorts the keys
let names: BTreeMap<_, _> = names.into_iter().collect();
let expected: BTreeMap<_, _> = expected.into_iter().collect();
panic!("\nCase failed: (`{:?}`, `{ethers}`, `{dependencies:?}`)\nNames: {names:#?}\nExpected: {expected:#?}\n", s.crate_name);
}
}
fn gen_unique<const N: usize>() -> [EthersCrate; N] {
assert!(N < EthersCrate::COUNT);
let rng = &mut thread_rng();
let mut set = HashSet::with_capacity(N);
while set.len() < N {
set.insert(rng.gen());
}
let vec: Vec<_> = set.into_iter().collect();
vec.try_into().unwrap()
}
let (s, _dir) = test_project();
// crate_name -> represents an external crate
// "ethers-contract" -> represents an internal crate
for name in [s.crate_name.as_ref().unwrap(), "ethers-contract"] {
let s = ProjectEnvironment::new(&s.manifest_dir, name);
// only ethers
assert_names(&s, true, &[]);
// only others
assert_names(&s, false, gen_unique::<3>().as_slice());
// ethers and others
assert_names(&s, true, gen_unique::<3>().as_slice());
}
}
#[test]
fn test_lock_file() {
let (s, _dir) = test_project();
write_manifest(&s, true, &[]);
let lock_file = s.manifest_dir.join("Cargo.lock");
assert!(!lock_file.exists());
s.determine_ethers_crates();
assert!(!lock_file.exists());
std::fs::write(&lock_file, "").unwrap();
assert!(lock_file.exists());
s.determine_ethers_crates();
assert!(lock_file.exists());
assert!(!std::fs::read(lock_file).unwrap().is_empty());
}
#[test]
fn test_is_crate_root() {
let (s, _dir) = test_project();
assert!(s.is_crate_root());
// `CARGO_MANIFEST_DIR`
// complex path has `/{dir_name}/` in the path
// name or path validity not checked
let s = ProjectEnvironment::new(
s.manifest_dir.join("examples/complex_examples"),
"complex-examples",
);
assert!(!s.is_crate_root());
let s = ProjectEnvironment::new(
s.manifest_dir.join("benches/complex_benches"),
"complex-benches",
);
assert!(!s.is_crate_root());
}
#[test]
fn test_is_crate_name_in_dirs() {
let (s, _dir) = test_project();
let root = &s.manifest_dir;
for dir_name in DIRS {
for ty in ["simple", "complex"] {
let s = ProjectEnvironment::new(root, format!("{ty}_{dir_name}"));
assert!(s.is_crate_name_in_dirs(), "{s:?}");
}
}
let s = ProjectEnvironment::new(root, "non_existant");
assert!(!s.is_crate_name_in_dirs());
let s = ProjectEnvironment::new(root.join("does-not-exist"), "foo_bar");
assert!(!s.is_crate_name_in_dirs());
}
#[test]
fn test_file_stem_eq() {
let path = Path::new("/tmp/foo.rs");
assert!(file_stem_eq(path, "foo"));
assert!(!file_stem_eq(path, "tmp"));
assert!(!file_stem_eq(path, "foo.rs"));
assert!(!file_stem_eq(path, "fo"));
assert!(!file_stem_eq(path, "f"));
assert!(!file_stem_eq(path, ""));
let path = Path::new("/tmp/foo/");
assert!(file_stem_eq(path, "foo"));
assert!(!file_stem_eq(path, "tmp"));
assert!(!file_stem_eq(path, "fo"));
assert!(!file_stem_eq(path, "f"));
assert!(!file_stem_eq(path, ""));
}
// utils
/// Creates:
///
/// ```text
/// - new_dir
/// - src
/// - main.rs
/// - {dir_name} for dir_name in DIRS
/// - simple_{dir_name}.rs
/// - complex_{dir_name}
/// - src if not "tests"
/// - main.rs
/// - module.rs
/// ```
fn test_project() -> (ProjectEnvironment, TempDir) {
// change the prefix to one without the default `.` because it is not a valid crate name
let dir = tempfile::Builder::new().prefix("tmp").tempdir().unwrap();
let root = dir.path();
let name = root.file_name().unwrap().to_str().unwrap();
// No Cargo.toml, git
fs::create_dir_all(root).unwrap();
let src = root.join("src");
fs::create_dir(&src).unwrap();
fs::write(src.join("main.rs"), "fn main(){}").unwrap();
for dir_name in DIRS {
let new_dir = root.join(dir_name);
fs::create_dir(&new_dir).unwrap();
let simple = new_dir.join(format!("simple_{dir_name}.rs"));
fs::write(simple, "").unwrap();
let mut complex = new_dir.join(format!("complex_{dir_name}"));
if dir_name != "tests" {
fs::create_dir(&complex).unwrap();
fs::write(complex.join("Cargo.toml"), "").unwrap();
complex.push("src");
}
fs::create_dir(&complex).unwrap();
fs::write(complex.join("main.rs"), "").unwrap();
fs::write(complex.join("module.rs"), "").unwrap();
}
// create target dirs
let target = root.join("target");
fs::create_dir(&target).unwrap();
fs::create_dir_all(target.join("tmp")).unwrap();
(ProjectEnvironment::new(root, name), dir)
}
/// Writes a test manifest to `{root}/Cargo.toml`.
fn write_manifest(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
// use paths to avoid downloading dependencies
const ETHERS_CORE: &str = env!("CARGO_MANIFEST_DIR");
let ethers_root = Path::new(ETHERS_CORE).parent().unwrap();
let mut dependencies_toml =
String::with_capacity(150 * (ethers as usize + dependencies.len()));
if ethers {
let path = ethers_root.join("ethers");
let ethers = format!("ethers = {{ path = \"{}\" }}\n", path.display());
dependencies_toml.push_str(&ethers);
}
for dep in dependencies.iter() {
let path = ethers_root.join(dep.fs_path());
let dep = format!("{dep} = {{ path = \"{}\" }}\n", path.display());
dependencies_toml.push_str(&dep);
}
let contents = format!(
r#"
[package]
name = "{}"
version = "0.0.0"
edition = "2021"
[dependencies]
{dependencies_toml}
"#,
s.crate_name.as_ref().unwrap()
);
fs::write(s.manifest_dir.join("Cargo.toml"), contents).unwrap();
}
}