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}