1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#![forbid(unsafe_code)]
#![warn(missing_docs, unreachable_pub, unused_crate_dependencies)]
#![warn(clippy::all, clippy::cargo, clippy::nursery, clippy::pedantic)]
#![warn(clippy::unwrap_used)]

////////////////////////////////////////////////////////////////////////////////
pub extern crate anyhow;
pub extern crate clap;
pub extern crate tracing;
pub extern crate tracing_subscriber;

////////////////////////////////////////////////////////////////////////////////
#[cfg(feature = "macros")]
pub extern crate entrypoint_macros;

#[cfg(feature = "macros")]
pub mod macros {
    pub use crate::entrypoint_macros::entrypoint;
    pub use crate::entrypoint_macros::DotEnvDefault;
    pub use crate::entrypoint_macros::LoggerDefault;
}

////////////////////////////////////////////////////////////////////////////////
pub mod prelude {
    pub use crate::anyhow;
    pub use crate::anyhow::Context;

    pub use crate::clap;

    pub use crate::tracing;
    pub use crate::tracing::{debug, error, info, trace, warn};
    pub use crate::tracing_subscriber;

    pub use crate::DotEnvParser;
    pub use crate::Entrypoint;
    pub use crate::Logger;

    #[cfg(feature = "macros")]
    pub use crate::macros::*;
}

pub use crate::prelude::*;

////////////////////////////////////////////////////////////////////////////////
pub trait Entrypoint: clap::Parser + DotEnvParser + Logger {
    fn additional_configuration(self) -> anyhow::Result<Self> {
        Ok(self)
    }

    fn entrypoint<F, T>(self, function: F) -> anyhow::Result<T>
    where
        F: FnOnce(Self) -> anyhow::Result<T>,
    {
        let entrypoint = {
            {
                // use temp/local/default log subscriber until global is set by log_init()
                let _log = tracing::subscriber::set_default(self.log_subscriber().finish());

                self.process_dotenv_files()?.log_init()?
            }
            .additional_configuration()?
            .dump_env_vars()
        };

        info!("setup/config complete; executing entrypoint");
        function(entrypoint)
    }
}
impl<P: clap::Parser + DotEnvParser + Logger> Entrypoint for P {}

////////////////////////////////////////////////////////////////////////////////
pub trait Logger: clap::Parser {
    fn log_level(&self) -> tracing::Level {
        tracing_subscriber::fmt::Subscriber::DEFAULT_MAX_LEVEL
            .into_level()
            .expect("invalid DEFAULT_MAX_LEVEL")
    }

    fn log_subscriber(&self) -> tracing_subscriber::fmt::SubscriberBuilder {
        let format = tracing_subscriber::fmt::format();

        tracing_subscriber::fmt()
            .event_format(format)
            .with_max_level(self.log_level())
    }

    fn log_init(self) -> anyhow::Result<Self> {
        if self.log_subscriber().try_init().is_err() {
            warn!("tracing_subscriber::try_init() failed");
        }

        info!(
            "init log level: {}",
            tracing_subscriber::filter::LevelFilter::current()
                .into_level()
                .expect("invalid LevelFilter::current()")
        );

        Ok(self)
    }
}

////////////////////////////////////////////////////////////////////////////////
pub trait DotEnvParser: clap::Parser {
    /// user should/could override this
    /// order matters
    fn dotenv_files(&self) -> Option<Vec<std::path::PathBuf>> {
        info!("dotenv_files() default impl returns None");
        None
    }

    fn dotenv_can_override(&self) -> bool {
        false
    }

    /// #FIXME - doc
    /// warning: debug log_level can leak secrets
    fn dump_env_vars(self) -> Self {
        for (key, value) in std::env::vars() {
            debug!("{key}: {value}");
        }

        self
    }

    /// order matters - env, .env, passed paths
    /// don't override this
    fn process_dotenv_files(self) -> anyhow::Result<Self> {
        // do twice in case `dotenv_files()` is dependant on `.env` supplied variable
        for _ in 0..=1 {
            let processed_found_dotenv = {
                if self.dotenv_can_override() {
                    dotenvy::dotenv_override().map_or(Err(()), |file| {
                        info!("dotenv::from_filename_override({})", file.display());
                        Ok(())
                    })
                } else {
                    dotenvy::dotenv().map_or(Err(()), |file| {
                        info!("dotenv::from_filename({})", file.display());
                        Ok(())
                    })
                }
            };

            let processed_supplied_dotenv = self.dotenv_files().map_or(Err(()), |files| {
                for file in files {
                    if self.dotenv_can_override() {
                        info!("dotenv::from_filename_override({})", file.display());
                        dotenvy::from_filename_override(file).or(Err(()))?;
                    } else {
                        info!("dotenv::from_filename({})", file.display());
                        dotenvy::from_filename(file).or(Err(()))?;
                    }
                }
                Ok(())
            });

            if processed_found_dotenv.is_err() && processed_supplied_dotenv.is_err() {
                info!("no dotenv file(s) found/processed");
                break;
            }
        }

        Ok(self)
    }
}