#![allow(private_interfaces)]
use std::marker::PhantomData;
use std::string::String;
use std::sync::Arc;
use camino::Utf8PathBuf;
use facet::Facet;
use facet_reflect::ReflectError;
use crate::{
config_format::{ConfigFormat, ConfigFormatError},
help::HelpConfig,
layers::{
cli::{CliConfig, CliConfigBuilder},
env::{EnvConfig, EnvConfigBuilder},
file::FileConfig,
},
schema::{Schema, error::SchemaError},
};
pub fn builder<T>() -> Result<ConfigBuilder<T>, BuilderError>
where
T: Facet<'static>,
{
let schema = Schema::from_shape(T::SHAPE)?;
Ok(ConfigBuilder {
_phantom: PhantomData,
schema,
cli_config: None,
help_config: None,
env_config: None,
file_config: None,
})
}
pub struct ConfigBuilder<T> {
_phantom: PhantomData<T>,
schema: Schema,
cli_config: Option<CliConfig>,
help_config: Option<HelpConfig>,
env_config: Option<EnvConfig>,
file_config: Option<FileConfig>,
}
pub struct Config<T> {
pub schema: Schema,
pub cli_config: Option<CliConfig>,
pub help_config: Option<HelpConfig>,
pub env_config: Option<EnvConfig>,
pub file_config: Option<FileConfig>,
_phantom: PhantomData<T>,
}
impl<T> ConfigBuilder<T> {
pub fn cli<F>(mut self, f: F) -> Self
where
F: FnOnce(CliConfigBuilder) -> CliConfigBuilder,
{
self.cli_config = Some(f(CliConfigBuilder::new()).build());
self
}
pub fn help<F>(mut self, f: F) -> Self
where
F: FnOnce(HelpConfigBuilder) -> HelpConfigBuilder,
{
self.help_config = Some(f(HelpConfigBuilder::new()).build());
self
}
pub fn env<F>(mut self, f: F) -> Self
where
F: FnOnce(EnvConfigBuilder) -> EnvConfigBuilder,
{
self.env_config = Some(f(EnvConfigBuilder::new()).build());
self
}
pub fn file<F>(mut self, f: F) -> Self
where
F: FnOnce(FileConfigBuilder) -> FileConfigBuilder,
{
self.file_config = Some(f(FileConfigBuilder::new()).build());
self
}
pub fn build(self) -> Config<T> {
Config {
schema: self.schema,
cli_config: self.cli_config,
help_config: self.help_config,
env_config: self.env_config,
file_config: self.file_config,
_phantom: PhantomData,
}
}
}
#[derive(Debug, Default)]
pub struct HelpConfigBuilder {
config: HelpConfig,
}
impl HelpConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn program_name(mut self, name: impl Into<String>) -> Self {
self.config.program_name = Some(name.into());
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.config.version = Some(version.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.config.description = Some(description.into());
self
}
pub fn width(mut self, width: usize) -> Self {
self.config.width = width;
self
}
pub fn include_implementation_source_file(mut self, include: bool) -> Self {
self.config.include_implementation_source_file = include;
self
}
pub fn include_implementation_url<F>(mut self, render_url: F) -> Self
where
F: Fn(&str) -> String + Send + Sync + 'static,
{
self.config.implementation_url = Some(Arc::new(render_url));
self
}
pub fn include_implementation_git_url(
self,
owner_repo: impl Into<String>,
revision: impl Into<String>,
) -> Self {
let owner_repo = owner_repo.into();
let revision = revision.into();
self.include_implementation_url(move |source_file| {
let normalized = source_file.replace('\\', "/");
format!("https://github.com/{owner_repo}/blob/{revision}/{normalized}")
})
}
fn build(self) -> HelpConfig {
self.config
}
}
#[derive(Default)]
pub struct FileConfigBuilder {
config: FileConfig,
}
impl FileConfigBuilder {
pub fn new() -> Self {
Self {
config: FileConfig::default(),
}
}
pub fn default_paths<I, P>(mut self, paths: I) -> Self
where
I: IntoIterator<Item = P>,
P: Into<Utf8PathBuf>,
{
self.config.default_paths = paths.into_iter().map(|p| p.into()).collect();
self
}
pub fn format<F: ConfigFormat + 'static>(mut self, format: F) -> Self {
self.config.registry.register(format);
self
}
pub fn strict(mut self) -> Self {
self.config.strict = true;
self
}
pub fn content(mut self, content: impl Into<String>, filename: impl Into<String>) -> Self {
self.config.inline_content = Some((content.into(), filename.into()));
self
}
fn build(self) -> FileConfig {
self.config
}
}
#[derive(Facet)]
#[repr(u8)]
pub enum BuilderError {
SchemaError(#[facet(opaque)] SchemaError),
Alloc(#[facet(opaque)] ReflectError),
FileNotFound {
path: Utf8PathBuf,
},
FileRead(Utf8PathBuf, String),
FileParse(Utf8PathBuf, ConfigFormatError),
CliParse(String),
UnknownKey {
key: String,
source: &'static str,
suggestion: Option<String>,
},
MissingRequired(String),
}
impl std::fmt::Display for BuilderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuilderError::SchemaError(e) => write!(f, "{e}"),
BuilderError::Alloc(e) => write!(f, "allocation failed: {e}"),
BuilderError::FileNotFound { path } => {
write!(f, "config file not found: {path}")
}
BuilderError::FileRead(path, msg) => {
write!(f, "error reading {path}: {msg}")
}
BuilderError::FileParse(path, e) => {
write!(f, "error parsing {path}: {e}")
}
BuilderError::CliParse(msg) => write!(f, "{msg}"),
BuilderError::UnknownKey {
key,
source,
suggestion,
} => {
write!(f, "unknown configuration key '{key}' from {source}")?;
if let Some(suggestion) = suggestion {
write!(f, " (did you mean '{suggestion}'?)")?;
}
Ok(())
}
BuilderError::MissingRequired(field) => {
write!(f, "missing required configuration: {field}")
}
}
}
}
impl std::fmt::Debug for BuilderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl std::error::Error for BuilderError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
BuilderError::SchemaError(e) => Some(e),
BuilderError::Alloc(e) => Some(e),
BuilderError::FileParse(_, e) => Some(e),
_ => None,
}
}
}
impl From<SchemaError> for BuilderError {
fn from(e: SchemaError) -> Self {
BuilderError::SchemaError(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate as args;
use facet::Facet;
#[derive(Facet)]
struct TestConfig {
#[facet(args::config)]
config: TestConfigLayer,
}
#[derive(Facet)]
struct TestConfigLayer {
#[facet(args::named)]
port: u16,
#[facet(args::named)]
host: String,
}
#[test]
fn test_cli_config_builder() {
let config = CliConfigBuilder::new()
.args(["--port", "8080"])
.strict()
.build();
assert_eq!(config.resolve_args(), vec!["--port", "8080"]);
assert!(config.strict());
}
#[test]
fn test_env_config_builder() {
let config = EnvConfigBuilder::new().prefix("MYAPP").strict().build();
assert_eq!(config.prefix, "MYAPP");
assert!(config.strict);
}
#[test]
fn test_file_config_builder() {
let config = FileConfigBuilder::new()
.default_paths(["./config.json", "~/.config/app.json"])
.strict()
.build();
assert_eq!(config.explicit_path, None);
assert_eq!(config.default_paths.len(), 2);
assert!(config.strict);
}
}