use core::{
fmt::{self, Display, Formatter},
str::FromStr,
};
use signet_constants::{
HostConstants, RollupConstants, SignetConstants, SignetEnvironmentConstants,
SignetSystemConstants,
};
use std::env::VarError;
use tracing_core::metadata::ParseLevelError;
use crate::utils::calc::SlotCalculator;
pub use init4_from_env_derive::FromEnv;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EnvItemInfo {
pub var: &'static str,
pub description: &'static str,
pub optional: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum FromEnvErr {
#[error("error reading variable {0}: {1}")]
EnvError(String, VarError),
#[error("environment variable {0} is empty")]
Empty(String),
#[error("failed to parse environment variable {0}: {1}")]
ParseError(String, #[source] Box<dyn core::error::Error + Send + Sync>),
}
impl FromEnvErr {
pub fn infallible_into(self) -> FromEnvErr {
match self {
Self::EnvError(s, e) => FromEnvErr::EnvError(s, e),
Self::Empty(s) => FromEnvErr::Empty(s),
Self::ParseError(..) => unreachable!(),
}
}
}
impl FromEnvErr {
pub fn env_err(var: &str, e: VarError) -> Self {
Self::EnvError(var.to_string(), e)
}
pub fn empty(var: &str) -> Self {
Self::Empty(var.to_string())
}
pub fn parse_error<E>(var: &str, err: E) -> Self
where
E: core::error::Error + Send + Sync + 'static,
{
Self::ParseError(var.to_string(), Box::new(err))
}
}
pub fn parse_env_if_present<T>(env_var: &str) -> Result<T, FromEnvErr>
where
T: FromStr,
T::Err: core::error::Error + Send + Sync + 'static,
{
let s = std::env::var(env_var).map_err(|e| FromEnvErr::env_err(env_var, e))?;
if s.is_empty() {
Err(FromEnvErr::empty(env_var))
} else {
s.parse()
.map_err(|error| FromEnvErr::ParseError(env_var.to_owned(), Box::new(error)))
}
}
pub trait FromEnv: fmt::Debug + Sized + 'static {
fn inventory() -> Vec<&'static EnvItemInfo>;
fn check_inventory() -> Result<(), Vec<&'static EnvItemInfo>> {
let mut missing = Vec::new();
for var in Self::inventory() {
match std::env::var(var.var) {
Ok(s) if s.is_empty() && !var.optional => missing.push(var),
Err(VarError::NotPresent) if !var.optional => missing.push(var),
_ => {}
}
}
if missing.is_empty() {
Ok(())
} else {
Err(missing)
}
}
fn from_env() -> Result<Self, FromEnvErr>;
}
impl<T> FromEnv for Option<T>
where
T: FromEnv,
{
fn inventory() -> Vec<&'static EnvItemInfo> {
T::inventory()
}
fn check_inventory() -> Result<(), Vec<&'static EnvItemInfo>> {
T::check_inventory()
}
fn from_env() -> Result<Self, FromEnvErr> {
match T::from_env() {
Ok(v) => Ok(Some(v)),
Err(FromEnvErr::Empty(_)) | Err(FromEnvErr::EnvError(_, _)) => Ok(None),
Err(e) => Err(e),
}
}
}
impl<T> FromEnv for Box<T>
where
T: FromEnv,
{
fn inventory() -> Vec<&'static EnvItemInfo> {
T::inventory()
}
fn check_inventory() -> Result<(), Vec<&'static EnvItemInfo>> {
T::check_inventory()
}
fn from_env() -> Result<Self, FromEnvErr> {
T::from_env().map(Box::new)
}
}
impl<T> FromEnv for std::sync::Arc<T>
where
T: FromEnv,
{
fn inventory() -> Vec<&'static EnvItemInfo> {
T::inventory()
}
fn check_inventory() -> Result<(), Vec<&'static EnvItemInfo>> {
T::check_inventory()
}
fn from_env() -> Result<Self, FromEnvErr> {
T::from_env().map(std::sync::Arc::new)
}
}
impl<T, U> FromEnv for std::borrow::Cow<'static, U>
where
T: FromEnv,
U: std::borrow::ToOwned<Owned = T> + core::fmt::Debug + ?Sized,
{
fn inventory() -> Vec<&'static EnvItemInfo> {
T::inventory()
}
fn check_inventory() -> Result<(), Vec<&'static EnvItemInfo>> {
T::check_inventory()
}
fn from_env() -> Result<Self, FromEnvErr> {
T::from_env().map(std::borrow::Cow::Owned)
}
}
pub trait FromEnvVar: core::fmt::Debug + Sized + 'static {
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr>;
fn from_env_var_or(env_var: &str, default: Self) -> Result<Self, FromEnvErr> {
match Self::from_env_var(env_var) {
Ok(v) => Ok(v),
Err(FromEnvErr::Empty(_)) | Err(FromEnvErr::EnvError(_, _)) => Ok(default),
Err(e) => Err(e),
}
}
fn from_env_var_or_else(
env_var: &str,
default: impl FnOnce() -> Self,
) -> Result<Self, FromEnvErr> {
match Self::from_env_var(env_var) {
Ok(v) => Ok(v),
Err(FromEnvErr::Empty(_)) | Err(FromEnvErr::EnvError(_, _)) => Ok(default()),
Err(e) => Err(e),
}
}
fn from_env_var_or_default(env_var: &str) -> Result<Self, FromEnvErr>
where
Self: Default,
{
Self::from_env_var_or_else(env_var, Self::default)
}
}
impl<T> FromEnvVar for Option<T>
where
T: FromEnvVar,
{
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr> {
match std::env::var(env_var) {
Ok(s) if s.is_empty() => Ok(None),
Ok(_) => T::from_env_var(env_var).map(Some),
Err(VarError::NotPresent) => Ok(None),
Err(error) => Err(FromEnvErr::parse_error(env_var, error)),
}
}
}
impl<T> FromEnvVar for Box<T>
where
T: FromEnvVar,
{
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr> {
T::from_env_var(env_var).map(Box::new)
}
}
impl<T> FromEnvVar for std::sync::Arc<T>
where
T: FromEnvVar,
{
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr> {
T::from_env_var(env_var).map(std::sync::Arc::new)
}
}
impl<T, U> FromEnvVar for std::borrow::Cow<'static, U>
where
T: FromEnvVar,
U: ToOwned<Owned = T> + core::fmt::Debug + ?Sized,
{
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr> {
T::from_env_var(env_var).map(std::borrow::Cow::Owned)
}
}
impl FromEnvVar for String {
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr> {
std::env::var(env_var).map_err(|_| FromEnvErr::empty(env_var))
}
}
impl FromEnvVar for std::time::Duration {
fn from_env_var(s: &str) -> Result<Self, FromEnvErr> {
u64::from_env_var(s).map(Self::from_millis)
}
}
impl<T> FromEnvVar for Vec<T>
where
T: From<String> + core::fmt::Debug + 'static,
{
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr> {
let s = std::env::var(env_var).map_err(|e| FromEnvErr::env_err(env_var, e))?;
if s.is_empty() {
return Ok(vec![]);
}
Ok(s.split(',')
.map(str::to_string)
.map(Into::into)
.collect::<Vec<_>>())
}
}
macro_rules! optional_with_default {
(
$(#[$meta:meta])*
$name:ident, $inner:ty, |$var:ident, $s:ident| $parse:expr
) => {
$(#[$meta])*
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Copy, Debug)]
pub struct $name<const DEFAULT: $inner>($inner);
impl<const DEFAULT: $inner> $name<DEFAULT> {
pub const fn into_inner(self) -> $inner {
self.0
}
}
impl<const DEFAULT: $inner> Display for $name<DEFAULT> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
<$inner as Display>::fmt(&self.0, f)
}
}
impl<const DEFAULT: $inner> Default for $name<DEFAULT> {
fn default() -> Self {
Self(DEFAULT)
}
}
impl<const DEFAULT: $inner> FromEnvVar for $name<DEFAULT> {
fn from_env_var($var: &str) -> Result<Self, FromEnvErr> {
match std::env::var($var) {
Ok($s) if $s.is_empty() => Ok(Self(DEFAULT)),
Ok($s) => $parse.map(Self),
Err(VarError::NotPresent) => Ok(Self(DEFAULT)),
Err(error) => Err(FromEnvErr::parse_error($var, error)),
}
}
}
};
}
optional_with_default! {
OptionalBoolWithDefault, bool, |env_var, _s| Ok(true)
}
fn parse_or_err<T: FromStr>(env_var: &str, s: &str) -> Result<T, FromEnvErr>
where
T::Err: core::error::Error + Send + Sync + 'static,
{
s.parse::<T>()
.map_err(|error| FromEnvErr::parse_error(env_var, error))
}
optional_with_default! {
OptionalU8WithDefault, u8, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalU16WithDefault, u16, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalU32WithDefault, u32, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalU64WithDefault, u64, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalU128WithDefault, u128, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalUsizeWithDefault, usize, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalI8WithDefault, i8, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalI16WithDefault, i16, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalI32WithDefault, i32, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalI64WithDefault, i64, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalI128WithDefault, i128, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalIsizeWithDefault, isize, |env_var, s| parse_or_err(env_var, &s)
}
optional_with_default! {
OptionalCharWithDefault, char, |env_var, s| parse_or_err(env_var, &s)
}
macro_rules! impl_for_parseable {
($($t:ty),*) => {
$(
impl FromEnvVar for $t {
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr> {
parse_env_if_present(env_var)
}
}
)*
}
}
impl_for_parseable!(
u8,
u16,
u32,
u64,
u128,
usize,
i8,
i16,
i32,
i64,
i128,
isize,
url::Url,
SignetConstants,
SignetEnvironmentConstants,
SignetSystemConstants,
HostConstants,
RollupConstants,
SlotCalculator
);
#[cfg(feature = "alloy")]
impl_for_parseable!(
alloy::primitives::Address,
alloy::primitives::Bytes,
alloy::primitives::U256
);
#[cfg(feature = "alloy")]
impl<const N: usize> FromEnvVar for alloy::primitives::FixedBytes<N> {
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr> {
parse_env_if_present(env_var)
}
}
impl FromEnvVar for bool {
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr> {
let s: String = std::env::var(env_var).map_err(|e| FromEnvErr::env_err(env_var, e))?;
Ok(!s.is_empty())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("failed to parse tracing level from environment variable")]
pub struct LevelParseError;
impl From<ParseLevelError> for LevelParseError {
fn from(_: ParseLevelError) -> Self {
LevelParseError
}
}
impl FromEnvVar for tracing::Level {
fn from_env_var(env_var: &str) -> Result<Self, FromEnvErr> {
let s: String = std::env::var(env_var).map_err(|e| FromEnvErr::env_err(env_var, e))?;
s.parse()
.map_err(LevelParseError::from)
.map_err(|error| FromEnvErr::parse_error(env_var, error))
}
}
impl FromEnv for SignetSystemConstants {
fn inventory() -> Vec<&'static EnvItemInfo> {
vec![&EnvItemInfo {
var: "CHAIN_NAME",
description: "The name of the chain",
optional: false,
}]
}
fn from_env() -> Result<Self, FromEnvErr> {
SignetSystemConstants::from_env_var("CHAIN_NAME")
}
}
#[cfg(feature = "cold-sql")]
mod cold_sql {
use super::{EnvItemInfo, FromEnv, FromEnvErr, FromEnvVar};
const URL_VAR: &str = "SIGNET_COLD_SQL_URL";
const MAX_CONNECTIONS_VAR: &str = "SIGNET_COLD_SQL_MAX_CONNECTIONS";
const MIN_CONNECTIONS_VAR: &str = "SIGNET_COLD_SQL_MIN_CONNECTIONS";
const ACQUIRE_TIMEOUT_SECS_VAR: &str = "SIGNET_COLD_SQL_ACQUIRE_TIMEOUT_SECS";
const MAX_LIFETIME_SECS_VAR: &str = "SIGNET_COLD_SQL_MAX_LIFETIME_SECS";
const IDLE_TIMEOUT_SECS_VAR: &str = "SIGNET_COLD_SQL_IDLE_TIMEOUT_SECS";
const DEFAULT_MAX_CONNECTIONS: u32 = 100;
const DEFAULT_MIN_CONNECTIONS: u32 = 5;
const DEFAULT_ACQUIRE_TIMEOUT_SECS: u64 = 5;
const DEFAULT_MAX_LIFETIME_SECS: u64 = 1800;
const DEFAULT_IDLE_TIMEOUT_SECS: u64 = 600;
impl FromEnv for signet_cold_sql::SqlConnector {
fn inventory() -> Vec<&'static EnvItemInfo> {
vec![
&EnvItemInfo {
var: URL_VAR,
description: "SQL connection URL for cold storage (postgres:// or sqlite:)",
optional: false,
},
&EnvItemInfo {
var: MAX_CONNECTIONS_VAR,
description: "Max SQL pool connections [default: 100]",
optional: true,
},
&EnvItemInfo {
var: MIN_CONNECTIONS_VAR,
description: "Min SQL pool idle connections [default: 5]",
optional: true,
},
&EnvItemInfo {
var: ACQUIRE_TIMEOUT_SECS_VAR,
description: "SQL pool acquire timeout in seconds [default: 5]",
optional: true,
},
&EnvItemInfo {
var: MAX_LIFETIME_SECS_VAR,
description: "SQL pool max connection lifetime in seconds [default: 1800]",
optional: true,
},
&EnvItemInfo {
var: IDLE_TIMEOUT_SECS_VAR,
description: "SQL pool idle timeout in seconds [default: 600]",
optional: true,
},
]
}
fn from_env() -> Result<Self, FromEnvErr> {
let url = String::from_env_var(URL_VAR)?;
let max_conns = u32::from_env_var_or(MAX_CONNECTIONS_VAR, DEFAULT_MAX_CONNECTIONS)?;
let min_conns = u32::from_env_var_or(MIN_CONNECTIONS_VAR, DEFAULT_MIN_CONNECTIONS)?;
let acquire_timeout =
u64::from_env_var_or(ACQUIRE_TIMEOUT_SECS_VAR, DEFAULT_ACQUIRE_TIMEOUT_SECS)?;
let max_lifetime =
u64::from_env_var_or(MAX_LIFETIME_SECS_VAR, DEFAULT_MAX_LIFETIME_SECS)?;
let idle_timeout =
u64::from_env_var_or(IDLE_TIMEOUT_SECS_VAR, DEFAULT_IDLE_TIMEOUT_SECS)?;
Ok(Self::new(url)
.with_max_connections(max_conns)
.with_min_connections(min_conns)
.with_acquire_timeout(std::time::Duration::from_secs(acquire_timeout))
.with_idle_timeout(Some(std::time::Duration::from_secs(idle_timeout)))
.with_max_lifetime(Some(std::time::Duration::from_secs(max_lifetime))))
}
}
}
#[cfg(test)]
mod test {
use std::{borrow::Cow, time::Duration};
use super::*;
fn set<T>(env: &str, val: &T)
where
T: ToString + ?Sized,
{
std::env::set_var(env, val.to_string());
}
fn test<T>(env: &str, val: T)
where
T: ToString + FromEnvVar + PartialEq + std::fmt::Debug,
{
set(env, &val);
let res = T::from_env_var(env).unwrap();
assert_eq!(res, val);
}
#[test]
fn test_primitives() {
test("U8", 42u8);
test("U16", 42u16);
test("U32", 42u32);
test("U64", 42u64);
test("U128", 42u128);
test("Usize", 42usize);
test("I8", 42i8);
test("I8-NEG", -42i16);
test("I16", 42i16);
test("I32", 42i32);
test("I64", 42i64);
test("I128", 42i128);
test("Isize", 42isize);
test("String", "hello".to_string());
test("Url", url::Url::parse("http://example.com").unwrap());
test("Level", tracing::Level::INFO);
}
#[test]
fn test_duration() {
let amnt = 42;
let val = Duration::from_millis(42);
set("Duration", &amnt);
let res = Duration::from_env_var("Duration").unwrap();
assert_eq!(res, val);
}
#[test]
fn test_a_few_errors() {
set("U8_", &30000u16);
let err = u8::from_env_var("U8_").unwrap_err();
assert!(matches!(err, FromEnvErr::ParseError(ref var, _) if var == "U8_"));
set("U8_", &"");
let err = u8::from_env_var("U8_").unwrap_err();
assert!(matches!(err, FromEnvErr::Empty(ref var) if var == "U8_"));
}
#[test]
fn is_cow_str_from_env_var() {
let s = "hello";
set("COW", s);
let res: Cow<'static, str> = Cow::from_env_var("COW").unwrap();
assert_eq!(res, Cow::<'static, str>::Owned(s.to_owned()));
}
}