Skip to main content

strut_core/
profile.rs

1use crate::profile::name::Name;
2use std::env;
3use std::fmt::{Display, Formatter};
4use std::ops::Deref;
5use std::sync::OnceLock;
6
7/// Implements the stack-allocated string for storing custom profile names.
8mod name;
9
10/// The string that is recognized as the [**production**](AppProfile::Prod)
11/// profile.
12pub const APP_PROFILE_PROD: &str = "prod";
13
14/// The string that is recognized as the [**development**](AppProfile::Dev)
15/// profile.
16pub const APP_PROFILE_DEV: &str = "dev";
17
18/// The string that is recognized as the [**test**](AppProfile::Test) profile.
19pub const APP_PROFILE_TEST: &str = "test";
20
21/// Represents the runtime profile of the application. The profile affects
22/// primarily which set of configuration files is applied, and the application
23/// is free to implement any profile-specific logic.
24///
25/// There are three **well-known profiles**:
26///
27/// - [**Production**](AppProfile::Prod) profile.
28/// - [**Development**](AppProfile::Dev) profile.
29/// - [**Test**](AppProfile::Test) profile.
30///
31/// Then, there are [**custom profiles**](AppProfile::Custom), which can take
32/// any lowercase ASCII-only [name](Name) within the limit of
33/// [`NAME_MAX_LEN`](name::NAME_MAX_LEN) characters. The custom profile names
34/// are always forced to lowercase.
35///
36/// This enumeration defines the
37/// [**active runtime profile**](AppProfile::active), which is lazily discerned
38/// from the environment on the first access, and is then statically stored for
39/// the whole runtime of the application. See the
40/// [`discern`](AppProfile::discern) method for details on how the active
41/// profile is chosen.
42///
43/// ## Usage
44///
45/// The intended way to match against the active profile is:
46///
47/// ```
48/// use strut_core::AppProfile;
49///
50/// match AppProfile::active() {
51///     AppProfile::Prod => println!("We are in prod"),
52///     AppProfile::Dev => println!("We are in dev"),
53///     AppProfile::Test => println!("We are in test"),
54///     AppProfile::Custom(name) => {
55///         match name.as_str() {
56///             "preprod" => println!("We are in preprod"),
57///             other => println!("We are in {}", other)
58///         };
59///     }
60/// };
61/// ```
62///
63/// ## Implicit detection
64///
65/// On the surface it looks like the three [`AppProfile`]s:
66/// [`prod`](AppProfile::Prod), [`dev`](AppProfile::Dev), and
67/// [`test`](AppProfile::Test) match quite nicely with the three out of four
68/// built-in
69/// [Cargo compilation profiles](https://doc.rust-lang.org/cargo/reference/profiles.html):
70/// `release`, `dev`, and `test` (leaving the fourth, `bench`, unmatched).
71///
72/// Having noted this similarity, a logical next step would be to use the Cargo
73/// compilation profile to automatically and implicitly infer the
74/// [active](AppProfile::active) [`AppProfile`] without requiring any custom
75/// environment variables to be set.
76///
77/// Unfortunately, in the current implementation of Cargo this is not feasible.
78///
79/// Firstly, [`AppProfile`] is a **runtime** construct, and the Cargo profiles
80/// exist only during **compilation**. Current implementation of Cargo provides
81/// no relevant runtime indicators: Are we running a unit test binary? A
82/// benchmark test? A compiled binary crate? We don’t know. That by itself could
83/// be a disqualifier, but we must also give a chance to the build scripts.
84///
85/// A build script is able to capture some of the compilation environment and
86/// pass it on to this crate’s compilation environment. We could then
87/// theoretically compile a different [`AppProfile`] depending on the
88/// compilation environment.
89///
90/// But that is not feasible either.
91///
92/// For one, there is no way to distinguish whether a test is being compiled:
93/// all test dependencies (including this crate) are compiled with the `dev` or
94/// `release` Cargo profile, not `test`.
95///
96/// We could technically infer (and pass on) whether the `release` profile is
97/// being used, but that alone tells us almost nothing. For example, there is
98/// nothing preventing the tests from compiling their dependencies with the
99/// `release` Cargo profile.
100///
101/// That all being said, we choose to not implement any “auto-detection” logic
102/// for the active profile, and instead rely on the special `APP_PROFILE`
103/// environment variable, falling back on the [`dev`](AppProfile::Dev) profile
104/// as the default.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
106pub enum AppProfile {
107    /// The **production** profile, used for live, customer-facing deployments.
108    /// Typically configured with optimized performance settings, reduced
109    /// logging verbosity, and real external services.
110    ///
111    /// If your application needs any prod-specific runtime configuration, it is
112    /// advised to run the compiled binary with the special environment variable:
113    ///
114    /// `APP_PROFILE=prod ./compiled_binary ...`
115    Prod,
116
117    /// The **development** profile, used during local or in-team development.
118    /// Commonly features hot-reloading, detailed logging, and integration with
119    /// mock or local services.
120    ///
121    /// This profile is the default if the special `APP_PROFILE` environment
122    /// variable is not set.
123    Dev,
124
125    /// The **test** profile, used in automated testing environments. Often
126    /// configured with in-memory databases, deterministic behavior, and fast
127    /// execution settings.
128    ///
129    /// If your application needs any test-specific runtime configuration, it is
130    /// advised to run Cargo with the special environment variable:
131    ///
132    /// `APP_PROFILE=test cargo test ...`
133    Test,
134
135    /// Any custom profile that a given application may choose to have.
136    ///
137    /// Examples from across the industry include `"preprod"`, `"qa"`, `"uat"`,
138    /// `"staging"`, `"sandbox"`, `"demo"`, `"canary"`, `"perf"`, `"local"`,
139    /// `"ci"`, `"nightly"`, `"hotfix"`, etc.
140    ///
141    /// The name of a custom profile is limited to
142    /// [`NAME_MAX_LEN`](name::NAME_MAX_LEN) ASCII lowercase characters. All
143    /// examples above fit into that limit.
144    ///
145    /// If your application needs any env-specific runtime configuration in a
146    /// custom environment, it is advised to run the compiled binary with the
147    /// special environment variable:
148    ///
149    /// `APP_PROFILE=preprod ./compiled_binary ...`
150    Custom(Name),
151}
152
153impl AppProfile {
154    /// Returns the active runtime [`AppProfile`], lazily
155    /// [discerned](AppProfile::discern).
156    pub fn active() -> &'static AppProfile {
157        static APP_PROFILE: OnceLock<AppProfile> = OnceLock::new();
158
159        APP_PROFILE.get_or_init(Self::discern)
160    }
161
162    /// Constructs a new [`AppProfile`] with the given name.
163    pub fn new(name: impl AsRef<str>) -> Self {
164        let name = Name::new(name);
165
166        match name.as_str() {
167            "prod" => Self::Prod,
168            "dev" => Self::Dev,
169            "test" => Self::Test,
170            _ => Self::Custom(name),
171        }
172    }
173
174    /// Reads the active runtime [`AppProfile`] from the `APP_PROFILE`
175    /// environment variable. If it is not set, delegates to
176    /// [`AppProfile::default`].
177    fn discern() -> Self {
178        // Detect if profile is set explicitly
179        if let Ok(profile) = env::var("APP_PROFILE") {
180            return Self::new(profile);
181        }
182
183        // Otherwise, return the default
184        Self::default()
185    }
186}
187
188impl AppProfile {
189    /// Reports whether the [`active`](AppProfile::active) profile is the
190    /// [**production**](AppProfile::Prod) profile.
191    pub fn active_is_prod() -> bool {
192        Self::active().is_prod()
193    }
194
195    /// Reports whether the [`active`](AppProfile::active) profile is the
196    /// [**development**](AppProfile::Dev) profile.
197    pub fn active_is_dev() -> bool {
198        Self::active().is_dev()
199    }
200
201    /// Reports whether the [`active`](AppProfile::active) profile is the
202    /// [**test**](AppProfile::Test) profile.
203    pub fn active_is_test() -> bool {
204        Self::active().is_test()
205    }
206
207    /// Reports whether the [`active`](AppProfile::active) profile is the
208    /// given profile name.
209    pub fn active_is(given: impl AsRef<str>) -> bool {
210        Self::active().is(given)
211    }
212}
213
214impl AppProfile {
215    /// Reports whether this [`AppProfile`] is the
216    /// [**production**](AppProfile::Prod) profile.
217    pub fn is_prod(&self) -> bool {
218        matches!(self, Self::Prod)
219    }
220
221    /// Reports whether this [`AppProfile`] is the
222    /// [**development**](AppProfile::Dev) profile.
223    pub fn is_dev(&self) -> bool {
224        matches!(self, Self::Dev)
225    }
226
227    /// Reports whether this [`AppProfile`] is the [**test**](AppProfile::Test)
228    /// profile.
229    pub fn is_test(&self) -> bool {
230        matches!(self, Self::Test)
231    }
232
233    /// Reports whether this [`AppProfile`] matches the given profile name.
234    ///
235    /// Before comparing, forces the given name into the same restrictions that
236    /// are [applied](Name::new) to a profile name normally.
237    pub fn is(&self, given: impl AsRef<str>) -> bool {
238        self.as_str() == Name::new(given).as_str()
239    }
240
241    /// Exposes a view on this [`AppProfile`] as a string slice.
242    pub fn as_str(&self) -> &str {
243        match self {
244            AppProfile::Prod => APP_PROFILE_PROD,
245            AppProfile::Dev => APP_PROFILE_DEV,
246            AppProfile::Test => APP_PROFILE_TEST,
247            AppProfile::Custom(name) => name.as_str(),
248        }
249    }
250}
251
252impl Display for AppProfile {
253    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
254        f.write_str(self.as_str())
255    }
256}
257
258impl AsRef<AppProfile> for AppProfile {
259    fn as_ref(&self) -> &AppProfile {
260        self
261    }
262}
263
264impl AsRef<str> for AppProfile {
265    fn as_ref(&self) -> &str {
266        self.as_str()
267    }
268}
269
270impl Deref for AppProfile {
271    type Target = str;
272
273    fn deref(&self) -> &Self::Target {
274        self.as_str()
275    }
276}
277
278impl From<&str> for AppProfile {
279    fn from(value: &str) -> Self {
280        Self::new(value)
281    }
282}
283
284impl Default for AppProfile {
285    /// Defines the default profile if it cannot be inferred from the special
286    /// `APP_PROFILE` environment variable.
287    fn default() -> Self {
288        Self::Dev
289    }
290}