entrypoint/
lib.rs

1//! an (opinionated) app wrapper to eliminate main function boilerplate
2//!
3//! Eliminate boilerplate by smartly integrating:
4//! * [`anyhow`](https://crates.io/crates/anyhow): for easy error handling
5//! * [`clap`](https://crates.io/crates/clap): for easy CLI parsing
6//! * [`dotenv`](https://crates.io/crates/dotenv): for easy environment variable management
7//! * [`tracing`](https://crates.io/crates/tracing): for easy logging
8//!
9//! In lieu of `main()`, an [`entrypoint`] function is defined.
10//!
11//! Perfectly reasonable setup/config is done automagically.
12//! More explicitly, the [`entrypoint`](Entrypoint::entrypoint) function can be written as if:
13//! * [`anyhow::Error`] is ready to propogate
14//! * CLI have been parsed
15//! * `.dotenv` files have already been processed and populated into the environment
16//! * logging is ready to use
17//!
18//! Customization can be achieved by overriding various [trait](crate#traits) default implementations
19//! (or preferably/more-typically by using the provided [attribute macros](macros)).
20//!
21//! # Examples
22//! ```
23//! use entrypoint::prelude::*;
24//!
25//! #[derive(clap::Parser, DotEnvDefault, LoggerDefault, Debug)]
26//! #[log_format(pretty)]
27//! #[log_level(entrypoint::LevelFilter::DEBUG)]
28//! #[log_writer(std::io::stdout)]
29//! struct Args {}
30//!
31//! // this function replaces `main`
32//! #[entrypoint::entrypoint]
33//! fn main(args: Args) -> anyhow::Result<()> {
34//!     // tracing & parsed clap struct are ready-to-use
35//!     debug!("entrypoint input args: {:#?}", args);
36//!
37//!     // env vars already have values from dotenv file(s)
38//!     for (key, value) in std::env::vars() {
39//!         println!("{key}: {value}");
40//!     }
41//!
42//!     // easy error propagation w/ anyhow
43//!     Ok(())
44//! }
45//! ```
46//!
47//! # Feature Flags
48//! Name       | Description                     | Default?
49//! -----------|---------------------------------|---------
50//! [`macros`] | Enables optional utility macros | Yes
51//!
52
53pub extern crate anyhow;
54pub extern crate clap;
55pub extern crate tracing;
56pub extern crate tracing_subscriber;
57
58#[cfg(feature = "macros")]
59pub extern crate entrypoint_macros;
60
61/// re-export [`entrypoint_macros`](https://crates.io/crates/entrypoint_macros)
62#[cfg(feature = "macros")]
63pub mod macros {
64    pub use crate::entrypoint_macros::entrypoint;
65    pub use crate::entrypoint_macros::DotEnvDefault;
66    pub use crate::entrypoint_macros::LoggerDefault;
67}
68
69/// essential [traits](#traits) and re-exports
70pub mod prelude {
71    pub use crate::anyhow;
72    pub use crate::anyhow::Context;
73
74    pub use crate::clap;
75    pub use crate::clap::Parser;
76
77    pub use crate::tracing;
78    pub use crate::tracing::{
79        debug, enabled, error, event, info, instrument, trace, warn, Level, Subscriber,
80    };
81    pub use crate::tracing::{debug_span, error_span, info_span, span, trace_span, warn_span};
82
83    pub use crate::tracing_subscriber;
84    pub use crate::tracing_subscriber::filter::LevelFilter;
85    pub use crate::tracing_subscriber::fmt::{
86        format::{Compact, Format, Full, Json, Pretty},
87        FormatEvent, FormatFields, Layer, MakeWriter,
88    };
89    pub use crate::tracing_subscriber::prelude::*;
90    pub use crate::tracing_subscriber::registry::LookupSpan;
91    pub use crate::tracing_subscriber::reload;
92    pub use crate::tracing_subscriber::Registry;
93
94    pub use crate::Entrypoint;
95    pub use crate::{DotEnvParser, DotEnvParserConfig};
96    pub use crate::{Logger, LoggerConfig};
97
98    #[cfg(feature = "macros")]
99    pub use crate::macros::*;
100}
101
102pub use crate::prelude::*;
103
104/// blanket implementation to wrap a function with "`main()`" setup/initialization boilerplate
105///
106/// Refer to required [trait](crate#traits) bounds for more information and customization options.
107///
108/// # Examples
109/// **Don't copy this code example. Use the [`macros::entrypoint`] attribute macro instead.**
110/// ```
111/// # use entrypoint::prelude::*;
112/// # #[derive(clap::Parser, DotEnvDefault, LoggerDefault)]
113/// struct Args {}
114///
115/// // this function "replaces" `main()`
116/// fn entrypoint(args: Args) -> anyhow::Result<()> {
117///     Ok(())
118/// }
119///
120/// // execute entrypoint from main
121/// fn main() -> anyhow::Result<()> {
122///     <Args as clap::Parser>::parse().entrypoint(entrypoint)
123/// }
124/// ```
125pub trait Entrypoint: clap::Parser + DotEnvParserConfig + LoggerConfig {
126    /// run setup/configuration/initialization and execute supplied function
127    ///
128    /// Customize if/as needed with the other entrypoint [traits](crate#traits).
129    ///
130    /// # Errors
131    /// * failure processing [`dotenv`](DotEnvParserConfig) file(s)
132    /// * failure configuring [logging](LoggerConfig)
133    fn entrypoint<F, T>(self, function: F) -> anyhow::Result<T>
134    where
135        F: FnOnce(Self) -> anyhow::Result<T>,
136    {
137        let entrypoint = {
138            // use temp/local/default log subscriber until global is set by log_init()
139            let _log = tracing::subscriber::set_default(
140                Registry::default().with(self.default_log_layer()),
141            );
142
143            self.process_dotenv_files()?;
144
145            Self::parse() // parse again, dotenv might have defined some of the arg(env) fields
146                .process_dotenv_files()? // dotenv, again... same reason as above
147                .log_init(None)?
148        };
149        info!("setup/config complete; executing entrypoint function");
150
151        function(entrypoint)
152    }
153}
154impl<T: clap::Parser + DotEnvParserConfig + LoggerConfig> Entrypoint for T {}
155
156/// automatic [`tracing`] & [`tracing_subscriber`] configuration
157///
158/// Available configuration for the [`Logger`] trait.
159///
160/// Default implementations are what you'd expect.
161/// Use this [derive macro](macros::LoggerDefault) for typical use cases.
162///
163/// # Examples
164/// ```
165/// # use entrypoint::prelude::*;
166/// # #[derive(clap::Parser, DotEnvDefault)]
167/// #[derive(LoggerDefault)]
168/// #[log_format(full)]
169/// #[log_level(entrypoint::LevelFilter::DEBUG)]
170/// #[log_writer(std::io::stdout)]
171/// struct Args {}
172///
173/// #[entrypoint::entrypoint]
174/// fn main(args: Args) -> anyhow::Result<()> {
175///     // logs are ready to use
176///     info!("hello!");
177/// #   Ok(())
178/// }
179/// ```
180/// For advanced customization requirements, refer to [`LoggerConfig::bypass_log_init`].
181pub trait LoggerConfig: clap::Parser {
182    /// hook to disable/enable automatic initialization
183    ///
184    /// This disrupts automatic initialization so that completely custom [`Layer`]s can be provided to [`Logger::log_init`].
185    /// This is intended only for advanced use cases, such as:
186    /// 1. multiple [`Layer`]s are required
187    /// 2. a [reload handle](tracing_subscriber::reload::Handle) needs to be kept accessible
188    ///
189    /// Default behvaior ([`false`]) is to call [`Logger::log_init`] on startup and
190    /// register the default layer provided by [`LoggerConfig::default_log_layer`].
191    ///
192    /// Overriding this to [`true`] will **not** automatically call [`Logger::log_init`] on startup.
193    /// All other defaults provided by the [`LoggerConfig`] trait methods are ignored.
194    /// The application is then **required** to directly call [`Logger::log_init`] with explicitly provided layer(s).
195    ///
196    /// # Examples
197    /// ```
198    /// # use entrypoint::prelude::*;
199    /// # #[derive(clap::Parser, DotEnvDefault)]
200    /// struct Args {}
201    ///
202    /// impl entrypoint::LoggerConfig for Args {
203    ///     fn bypass_log_init(&self) -> bool { true }
204    /// }
205    ///
206    /// #[entrypoint::entrypoint]
207    /// fn main(args: Args) -> anyhow::Result<()> {
208    ///     // logging hasn't been configured yet
209    ///     assert!(!enabled!(entrypoint::Level::ERROR));
210    ///
211    ///     // must manually config/init logging
212    ///     let (layer, reload_handle) = reload::Layer::new(
213    ///         tracing_subscriber::fmt::Layer::default()
214    ///             .event_format(args.default_log_format())
215    ///             .with_writer(args.default_log_writer())
216    ///             .with_filter(args.default_log_level()),
217    ///     );
218    ///     let args = args.log_init(Some(vec![layer.boxed()]))?;
219    ///
220    ///     // OK... now logging should work
221    ///     assert!( enabled!(entrypoint::Level::ERROR));
222    ///     assert!(!enabled!(entrypoint::Level::TRACE));
223    ///
224    ///     // we've maintained direct access to the layer and reload handle
225    ///     let _ = reload_handle.modify(|layer| *layer.filter_mut() = entrypoint::LevelFilter::TRACE);
226    ///     assert!( enabled!(entrypoint::Level::TRACE));
227    /// #   Ok(())
228    /// }
229    /// ```
230    fn bypass_log_init(&self) -> bool {
231        false
232    }
233
234    /// define the default [`tracing_subscriber`] [`LevelFilter`]
235    ///
236    /// Defaults to [`DEFAULT_MAX_LEVEL`](tracing_subscriber::fmt::Subscriber::DEFAULT_MAX_LEVEL).
237    ///
238    /// This can be easily set with convenience [`macros`](macros::LoggerDefault#attributes).
239    ///
240    /// # Examples
241    /// ```
242    /// # use entrypoint::prelude::*;
243    /// # #[derive(clap::Parser)]
244    /// struct Args {
245    ///     /// allow user to pass in debug level
246    ///     #[arg(long)]
247    ///     default_log_level: LevelFilter,
248    /// }
249    ///
250    /// impl entrypoint::LoggerConfig for Args {
251    ///     fn default_log_level(&self) -> LevelFilter {
252    ///         self.default_log_level.clone()
253    ///     }
254    /// }
255    /// ```
256    fn default_log_level(&self) -> LevelFilter {
257        tracing_subscriber::fmt::Subscriber::DEFAULT_MAX_LEVEL
258    }
259
260    /// define the default [`tracing_subscriber`] [`Format`]
261    ///
262    /// Defaults to [`Format::default`].
263    ///
264    /// This can be easily set with convenience [`macros`](macros::LoggerDefault#attributes).
265    ///
266    /// # Examples
267    /// ```
268    /// # use entrypoint::prelude::*;
269    /// # #[derive(clap::Parser)]
270    /// # struct Args {}
271    /// impl entrypoint::LoggerConfig for Args {
272    ///     fn default_log_format<S,N>(&self) -> impl FormatEvent<S,N> + Send + Sync + 'static
273    ///     where
274    ///         S: Subscriber + for<'a> LookupSpan<'a>,
275    ///         N: for<'writer> FormatFields<'writer> + 'static,
276    ///     {
277    ///         Format::default().pretty()
278    ///     }
279    /// }
280    /// ```
281    fn default_log_format<S, N>(&self) -> impl FormatEvent<S, N> + Send + Sync + 'static
282    where
283        S: Subscriber + for<'a> LookupSpan<'a>,
284        N: for<'writer> FormatFields<'writer> + 'static,
285    {
286        Format::default()
287    }
288
289    /// define the default [`tracing_subscriber`] [`MakeWriter`]
290    ///
291    /// Defaults to [`std::io::stdout`].
292    ///
293    /// This can be easily set with convenience [`macros`](macros::LoggerDefault#attributes).
294    ///
295    /// # Examples
296    /// ```
297    /// # use entrypoint::prelude::*;
298    /// # #[derive(clap::Parser)]
299    /// # struct Args {}
300    /// impl entrypoint::LoggerConfig for Args {
301    ///     fn default_log_writer(&self) -> impl for<'writer> MakeWriter<'writer> + Send + Sync + 'static {
302    ///         std::io::stderr
303    ///     }
304    /// }
305    /// ```
306    fn default_log_writer(&self) -> impl for<'writer> MakeWriter<'writer> + Send + Sync + 'static {
307        std::io::stdout
308    }
309
310    /// define the default [`tracing_subscriber`] [`Layer`] to register
311    ///
312    /// This method uses the defaults defined by [`LoggerConfig`] methods and composes a default [`Layer`] to register.
313    ///
314    /// **You ***probably*** don't want to override this default implementation.**
315    /// 1. For standard customization, override these other trait methods:
316    ///    * [`LoggerConfig::default_log_level`]
317    ///    * [`LoggerConfig::default_log_format`]
318    ///    * [`LoggerConfig::default_log_writer`]
319    /// 2. Minor/static customization(s) ***can*** be achieved by overriding this method...
320    ///    though this might warrant moving to the 'advanced requirements' option below.
321    /// 3. Otherwise, for advanced requirements, refer to [`LoggerConfig::bypass_log_init`].
322    fn default_log_layer(
323        &self,
324    ) -> Box<dyn tracing_subscriber::Layer<Registry> + Send + Sync + 'static> {
325        let (layer, _) = reload::Layer::new(
326            tracing_subscriber::fmt::Layer::default()
327                .event_format(self.default_log_format())
328                .with_writer(self.default_log_writer())
329                .with_filter(self.default_log_level()),
330        );
331
332        layer.boxed()
333    }
334}
335
336/// blanket implementation for automatic [`tracing`] & [`tracing_subscriber`] initialization
337///
338/// Refer to [`LoggerConfig`] for configuration options.
339pub trait Logger: LoggerConfig {
340    /// register the supplied layers with the global tracing subscriber
341    ///
342    /// Default behvaior is to automatically (on startup) register the layer provided by [`LoggerConfig::default_log_layer`].
343    ///
344    /// This automatic setup/config can be disabled with [`LoggerConfig::bypass_log_init`].
345    /// When bypassed, **[`Logger::log_init`] must be manually/directly called from the application.**
346    /// This is an advanced use case. Refer to [`LoggerConfig::bypass_log_init`] for more details.
347    ///
348    /// # Errors
349    /// * [`tracing::subscriber::set_global_default`] was unsuccessful, likely because a global subscriber was already installed
350    fn log_init(
351        self,
352        layers: Option<Vec<Box<dyn tracing_subscriber::Layer<Registry> + Send + Sync + 'static>>>,
353    ) -> anyhow::Result<Self> {
354        let layers = match (self.bypass_log_init(), &layers) {
355            (false, Some(_)) => {
356                anyhow::bail!("bypass_log_init() is false, but layers were passed into log_init()");
357            }
358            (false, None) => Some(vec![self.default_log_layer()]),
359            (true, _) => layers,
360        };
361
362        if layers.is_some()
363            && tracing_subscriber::registry()
364                .with(layers)
365                .try_init()
366                .is_err()
367        {
368            anyhow::bail!("tracing::subscriber::set_global_default failed");
369        }
370
371        info!(
372            "log level: {}",
373            LevelFilter::current()
374                .into_level()
375                .expect("invalid LevelFilter::current()")
376        );
377
378        Ok(self)
379    }
380}
381impl<T: LoggerConfig> Logger for T {}
382
383/// automatic [`dotenv`](dotenvy) processing configuration
384///
385/// Available configuration for the [`DotEnvParser`] trait.
386///
387/// Default implementations are what you'd expect.
388/// Use this [derive macro](macros::DotEnvDefault) for typical use cases.
389///
390/// # Order Matters!
391/// Environment variables are processed/set in this order:
392/// 1. Preexisting variables already defined in environment.
393/// 2. The `.env` file, if present.
394/// 3. [`additional_dotenv_files`] supplied file(s) (sequentially, as supplied).
395///
396/// Keep in mind:
397/// * Depending on [`dotenv_can_override`], environment variable values may be the first *or* last processed/set.
398/// * [`additional_dotenv_files`] should be supplied in the order to be processed.
399///
400/// # Examples
401/// ```
402/// # use entrypoint::prelude::*;
403/// # #[derive(clap::Parser, LoggerDefault)]
404/// #[derive(DotEnvDefault)]
405/// struct Args {}
406///
407/// #[entrypoint::entrypoint]
408/// fn main(args: Args) -> anyhow::Result<()> {
409///     // .env variables should now be in the environment
410///     for (key, value) in std::env::vars() {
411///         println!("{key}: {value}");
412///     }
413/// #   Ok(())
414/// }
415/// ```
416/// [`additional_dotenv_files`]: DotEnvParserConfig#method.additional_dotenv_files
417/// [`dotenv_can_override`]: DotEnvParserConfig#method.dotenv_can_override
418pub trait DotEnvParserConfig: clap::Parser {
419    /// additional dotenv files to process
420    ///
421    /// Default behavior is to only use `.env` (i.e. no additional files).
422    /// This preserves the stock/default [`dotenvy`] behavior.
423    ///
424    /// **[Order Matters!](DotEnvParserConfig#order-matters)**
425    ///
426    /// # Examples
427    /// ```
428    /// # #[derive(clap::Parser)]
429    /// struct Args {
430    ///     /// allow user to pass in additional env files
431    ///     #[arg(long)]
432    ///     user_dotenv: Option<std::path::PathBuf>,
433    /// }
434    ///
435    /// impl entrypoint::DotEnvParserConfig for Args {
436    ///     fn additional_dotenv_files(&self) -> Option<Vec<std::path::PathBuf>> {
437    ///         self.user_dotenv.clone().map(|p| vec![p])
438    ///     }
439    /// }
440    /// ```
441    fn additional_dotenv_files(&self) -> Option<Vec<std::path::PathBuf>> {
442        None
443    }
444
445    /// whether successive dotenv files can override already defined environment variables
446    ///
447    /// Default behavior is to not override.
448    /// This preserves the stock/default [`dotenvy`] behavior.
449    ///
450    /// **[Order Matters!](DotEnvParserConfig#order-matters)**
451    ///
452    /// # Examples
453    /// ```
454    /// # #[derive(clap::Parser)]
455    /// # struct Args {}
456    /// impl entrypoint::DotEnvParserConfig for Args {
457    ///     fn dotenv_can_override(&self) -> bool { true }
458    /// }
459    /// ```
460    fn dotenv_can_override(&self) -> bool {
461        false
462    }
463}
464
465/// blanket implementation for automatic [`dotenv`](dotenvy) processing
466///
467/// Refer to [`DotEnvParserConfig`] for configuration options.
468pub trait DotEnvParser: DotEnvParserConfig {
469    /// process dotenv files and populate variables into the environment
470    ///
471    /// This will run automatically at startup.
472    ///
473    /// **[Order Matters!](DotEnvParserConfig#order-matters)**
474    ///
475    /// # Errors
476    /// * failure processing an [`DotEnvParserConfig::additional_dotenv_files`] supplied file
477    fn process_dotenv_files(self) -> anyhow::Result<Self> {
478        if self.dotenv_can_override() {
479            dotenvy::dotenv_override()
480                .map(|file| info!("dotenv::from_filename_override({})", file.display()))
481        } else {
482            dotenvy::dotenv().map(|file| info!("dotenv::from_filename({})", file.display()))
483        }
484        .map_err(|_| warn!("no .env file found"))
485        .unwrap_or(()); // suppress, no .env is a valid use case
486
487        self.additional_dotenv_files().map_or(Ok(()), |files| {
488            // try all, so any/all failures will be in the log
489            #[allow(clippy::manual_try_fold)]
490            files.into_iter().fold(Ok(()), |accum, file| {
491                let process = |res: Result<std::path::PathBuf, dotenvy::Error>, msg| {
492                    res.map(|_| info!(msg)).map_err(|e| {
493                        error!(msg);
494                        e
495                    })
496                };
497
498                if self.dotenv_can_override() {
499                    process(
500                        dotenvy::from_filename_override(file.clone()),
501                        format!("dotenv::from_filename_override({})", file.display()),
502                    )
503                } else {
504                    process(
505                        dotenvy::from_filename(file.clone()),
506                        format!("dotenv::from_filename({})", file.display()),
507                    )
508                }
509                .and(accum)
510            })
511        })?; // bail if any of the additional_dotenv_files failed
512
513        Ok(self)
514    }
515}
516impl<T: DotEnvParserConfig> DotEnvParser for T {}