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 {}