use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::fs;
use std::net::IpAddr;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use std::str::FromStr;
#[cfg(feature = "cli")]
use clap::Parser;
#[cfg(feature = "cli")]
use std::path::Path;
use crate::scheme::SchemeConfig;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LiteralOrEnv<T>(T, Option<String>);
impl<T> LiteralOrEnv<T> {
pub fn from_literal(value: T) -> Self {
Self(value, None)
}
#[allow(dead_code)]
pub fn inner(&self) -> &T {
&self.0
}
#[allow(dead_code)]
pub fn into_inner(self) -> T {
self.0
}
fn parse_env_var_syntax(s: &str) -> Option<String> {
if s.starts_with("${") && s.ends_with('}') {
Some(s[2..s.len() - 1].to_string())
} else if s.starts_with('$') && s.len() > 1 {
let var_name = &s[1..];
if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
Some(var_name.to_string())
} else {
None
}
} else {
None
}
}
}
impl<T> Deref for LiteralOrEnv<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for LiteralOrEnv<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'de, T> Deserialize<'de> for LiteralOrEnv<T>
where
T: FromStr,
T::Err: std::fmt::Display,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let (value, var_name) = if let Some(var_name) = Self::parse_env_var_syntax(&s) {
let value = std::env::var(&var_name).map_err(|_| {
serde::de::Error::custom(format!(
"Environment variable '{}' not found (referenced as '{}')",
var_name, s
))
})?;
(value, Some(var_name))
} else {
(s, None)
};
let parsed = value
.parse::<T>()
.map_err(|e| serde::de::Error::custom(format!("Failed to parse value: {}", e)))?;
Ok(LiteralOrEnv(parsed, var_name))
}
}
impl<T> serde::Serialize for LiteralOrEnv<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
impl<T> Display for LiteralOrEnv<T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.1.as_ref() {
None => self.0.fmt(f),
Some(var_name) => write!(f, "${}", var_name),
}
}
}
#[derive(Debug)]
#[cfg_attr(feature = "cli", derive(Parser))]
#[cfg_attr(feature = "cli", command(name = "x402-rs"))]
#[cfg_attr(feature = "cli", command(about = "x402 Facilitator HTTP server"))]
#[allow(dead_code)] pub struct CliArgs {
#[cfg_attr(
feature = "cli",
arg(long, short, env = "CONFIG", default_value = "config.json")
)]
pub config: PathBuf,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Config<TChainsConfig> {
#[serde(default = "config_defaults::default_port")]
port: u16,
#[serde(default = "config_defaults::default_host")]
host: IpAddr,
#[serde(default)]
chains: TChainsConfig,
#[serde(default)]
schemes: Vec<SchemeConfig>,
}
impl<TChainsConfig> Default for Config<TChainsConfig>
where
TChainsConfig: Default,
{
fn default() -> Self {
Config {
port: config_defaults::default_port(),
host: config_defaults::default_host(),
chains: TChainsConfig::default(),
schemes: Vec::new(),
}
}
}
pub mod config_defaults {
use std::env;
use std::net::IpAddr;
pub const DEFAULT_PORT: u16 = 8080;
pub const DEFAULT_HOST: &str = "0.0.0.0";
pub fn default_port() -> u16 {
env::var("PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_PORT)
}
pub fn default_host() -> IpAddr {
env::var("HOST")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(IpAddr::V4(DEFAULT_HOST.parse().unwrap()))
}
}
impl<TChainsConfig> Config<TChainsConfig> {
pub fn port(&self) -> u16 {
self.port
}
pub fn host(&self) -> IpAddr {
self.host
}
pub fn schemes(&self) -> &Vec<SchemeConfig> {
&self.schemes
}
pub fn chains(&self) -> &TChainsConfig {
&self.chains
}
}
impl<TChainsConfig> Config<TChainsConfig>
where
TChainsConfig: Default + for<'de> Deserialize<'de>,
{
#[cfg(feature = "cli")]
pub fn load() -> Result<Self, ConfigError> {
let cli_args = CliArgs::parse();
let config_path = Path::new(&cli_args.config)
.canonicalize()
.map_err(|e| ConfigError::FileRead(cli_args.config, e))?;
Self::load_from_path(config_path)
}
pub fn load_from_path(path: PathBuf) -> Result<Self, ConfigError> {
let content = fs::read_to_string(&path).map_err(|e| ConfigError::FileRead(path, e))?;
let config: Config<TChainsConfig> = serde_json::from_str(&content)?;
Ok(config)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("Failed to read config file at {0}: {1}")]
FileRead(PathBuf, std::io::Error),
#[error("Failed to parse config file: {0}")]
JsonParse(#[from] serde_json::Error),
}