what_stack/types.rs
1//! Public input and output types shared by the detection APIs.
2
3use std::borrow::Cow;
4use std::ffi::OsString;
5use std::fmt;
6use std::path::Path;
7
8/// Broad category of a detected [`StackLabel`].
9///
10/// The kind is assigned where each detection rule is defined, so every label
11/// produced by this crate carries one. [`StackDetector`](crate::StackDetector)
12/// uses it to decide whether project config may refine a process label:
13///
14/// - [`Runtime`](Self::Runtime) and [`Tool`](Self::Tool) labels are generic
15/// hosts for project code, so a project config label (for example `Next.js`
16/// for a `node` process) may replace them.
17/// - [`Framework`](Self::Framework), [`Database`](Self::Database), and
18/// [`Service`](Self::Service) labels are final: a `postgres` or `rails`
19/// process keeps its own label even when started from a project directory.
20///
21/// The enum is `#[non_exhaustive]`; match it with a wildcard arm.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23#[non_exhaustive]
24pub enum StackKind {
25 /// Language runtime or process that executes project code, such as
26 /// `Node.js`, `Python`, `.NET`, `Go`, or the `Gunicorn` and `Uvicorn`
27 /// application servers.
28 Runtime,
29 /// Application or site framework, such as `Next.js`, `Django`, `Rails`,
30 /// or `Hugo`.
31 Framework,
32 /// Build tool, bundler, or dev server, such as `Vite`, `Webpack`, or
33 /// `Java (Maven)`.
34 Tool,
35 /// Database, cache, or search engine, such as `PostgreSQL`, `Redis`, or
36 /// `Elasticsearch`.
37 Database,
38 /// Network service that is not a data store, such as a web server, reverse
39 /// proxy, message broker, or cloud emulator (`Nginx`, `Traefik`,
40 /// `RabbitMQ`, `LocalStack`).
41 Service,
42}
43
44/// Human-readable application, framework, runtime, or service label.
45///
46/// A label pairs display text with a [`StackKind`]. Built-in labels borrow
47/// static strings, so producing them never allocates. Use
48/// [`as_str`](Self::as_str), [`AsRef<str>`], or [`Display`](fmt::Display) to
49/// read the text, and [`kind`](Self::kind) for the category.
50///
51/// Equality and hashing consider both text and kind. Comparisons with `str`
52/// and `&str` consider only the text, which keeps assertions short.
53///
54/// # Examples
55///
56/// ```
57/// use what_stack::{StackKind, StackLabel, detect_from_process};
58///
59/// let label: StackLabel = detect_from_process("postgres").expect("known process");
60/// assert_eq!(label, "PostgreSQL");
61/// assert_eq!(label.as_str(), "PostgreSQL");
62/// assert_eq!(label.kind(), StackKind::Database);
63/// assert_eq!(label.to_string(), "PostgreSQL");
64/// ```
65#[derive(Debug, Clone, PartialEq, Eq, Hash)]
66pub struct StackLabel {
67 text: Cow<'static, str>,
68 kind: StackKind,
69}
70
71impl StackLabel {
72 /// Create a label from owned or static text.
73 ///
74 /// Prefer [`from_static`](Self::from_static) for string literals; it is a
75 /// `const fn` and never allocates.
76 #[must_use]
77 pub fn new(text: impl Into<Cow<'static, str>>, kind: StackKind) -> Self {
78 Self {
79 text: text.into(),
80 kind,
81 }
82 }
83
84 /// Create a label from static text without allocating.
85 #[must_use]
86 pub const fn from_static(text: &'static str, kind: StackKind) -> Self {
87 Self {
88 text: Cow::Borrowed(text),
89 kind,
90 }
91 }
92
93 /// Return the label text.
94 #[must_use]
95 pub fn as_str(&self) -> &str {
96 &self.text
97 }
98
99 /// Return the label category.
100 #[must_use]
101 pub const fn kind(&self) -> StackKind {
102 self.kind
103 }
104
105 /// Convert the label into its text, dropping the kind.
106 ///
107 /// Static labels stay borrowed, so this does not allocate for built-in
108 /// detections.
109 #[must_use]
110 pub fn into_cow(self) -> Cow<'static, str> {
111 self.text
112 }
113}
114
115impl fmt::Display for StackLabel {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 f.write_str(&self.text)
118 }
119}
120
121impl AsRef<str> for StackLabel {
122 fn as_ref(&self) -> &str {
123 &self.text
124 }
125}
126
127impl PartialEq<str> for StackLabel {
128 fn eq(&self, other: &str) -> bool {
129 self.text == other
130 }
131}
132
133impl PartialEq<&str> for StackLabel {
134 fn eq(&self, other: &&str) -> bool {
135 self.text == *other
136 }
137}
138
139impl PartialEq<StackLabel> for str {
140 fn eq(&self, other: &StackLabel) -> bool {
141 self == other.text
142 }
143}
144
145impl PartialEq<StackLabel> for &str {
146 fn eq(&self, other: &StackLabel) -> bool {
147 *self == other.text
148 }
149}
150
151impl From<StackLabel> for Cow<'static, str> {
152 fn from(label: StackLabel) -> Self {
153 label.text
154 }
155}
156
157impl From<StackLabel> for String {
158 fn from(label: StackLabel) -> Self {
159 label.text.into_owned()
160 }
161}
162
163/// Process-like path inputs used to resolve a project root.
164///
165/// Build one with [`ProjectInput::new`] and the chainable setters. Project root
166/// resolution is best-effort and uses a stable fallback order:
167///
168/// 1. [`cwd`](Self::cwd)
169/// 2. parent of [`exe`](Self::exe), for executables that are not known
170/// runtimes or tools
171/// 3. parents of absolute paths in [`cmd`](Self::cmd)
172///
173/// Relative command-line arguments are ignored because they cannot be resolved
174/// safely without knowing the process working directory at the time the command
175/// started.
176///
177/// The home-directory ceiling is not part of the input. It is configured on
178/// [`StackDetector`](crate::StackDetector) or passed to
179/// [`resolve_project_root`](crate::resolve_project_root) directly.
180///
181/// # Examples
182///
183/// ```
184/// use std::path::Path;
185/// use what_stack::ProjectInput;
186///
187/// let input = ProjectInput::new()
188/// .cwd(Path::new("/workspace/api/src"))
189/// .exe(None);
190/// # let _ = input;
191/// ```
192#[derive(Debug, Clone, Copy, Default)]
193#[non_exhaustive]
194pub struct ProjectInput<'a> {
195 pub(crate) cwd: Option<&'a Path>,
196 pub(crate) exe: Option<&'a Path>,
197 pub(crate) cmd: &'a [OsString],
198}
199
200impl<'a> ProjectInput<'a> {
201 /// Create an empty input with no path hints.
202 #[must_use]
203 pub const fn new() -> Self {
204 Self {
205 cwd: None,
206 exe: None,
207 cmd: &[],
208 }
209 }
210
211 /// Set the process working directory.
212 ///
213 /// This is the most trustworthy project-root hint and is checked first.
214 /// Accepts a `&Path` or an `Option<&Path>`.
215 #[must_use]
216 pub fn cwd(mut self, cwd: impl Into<Option<&'a Path>>) -> Self {
217 self.cwd = cwd.into();
218 self
219 }
220
221 /// Set the process executable path.
222 ///
223 /// The parent directory is searched when the working directory is missing
224 /// or does not resolve to a project, unless the executable is a known
225 /// runtime or tool such as `node` or `python`. A root found this way is
226 /// ignored when it lies in a dot directory directly under the home
227 /// ceiling, such as `~/.nvm` or `~/.cargo`. Accepts a `&Path` or an
228 /// `Option<&Path>`.
229 #[must_use]
230 pub fn exe(mut self, exe: impl Into<Option<&'a Path>>) -> Self {
231 self.exe = exe.into();
232 self
233 }
234
235 /// Set the process command-line arguments.
236 ///
237 /// Only absolute path arguments are inspected, in their original order.
238 #[must_use]
239 pub const fn cmd(mut self, cmd: &'a [OsString]) -> Self {
240 self.cmd = cmd;
241 self
242 }
243}
244
245/// Inputs used by [`StackDetector`](crate::StackDetector) to resolve a stack.
246///
247/// Build one with [`StackInput::new`] and the chainable setters. The detector
248/// uses image, process, and project config metadata. Config detection is
249/// guarded to avoid false positives: a config label can win only when the
250/// process label is a [`StackKind::Runtime`] or [`StackKind::Tool`], or when the
251/// process is unknown but its executable belongs to the project (see
252/// [`StackDetector`](crate::StackDetector)). A known runtime or tool accepts
253/// only config labels from its own ecosystem.
254///
255/// # Examples
256///
257/// ```
258/// use std::path::Path;
259/// use what_stack::StackInput;
260///
261/// let input = StackInput::new("node")
262/// .exe_name("node.exe")
263/// .project_root(Path::new("/workspace/web"))
264/// .image(None);
265/// # let _ = input;
266/// ```
267#[derive(Debug, Clone, Copy, Default)]
268#[non_exhaustive]
269pub struct StackInput<'a> {
270 pub(crate) image: Option<&'a str>,
271 pub(crate) project_root: Option<&'a Path>,
272 pub(crate) process_name: &'a str,
273 pub(crate) exe_name: Option<&'a str>,
274 pub(crate) exe_path: Option<&'a Path>,
275}
276
277impl<'a> StackInput<'a> {
278 /// Create an input from a process name taken from the process table.
279 ///
280 /// Pass an empty string when no process name is available.
281 #[must_use]
282 pub const fn new(process_name: &'a str) -> Self {
283 Self {
284 image: None,
285 project_root: None,
286 process_name,
287 exe_name: None,
288 exe_path: None,
289 }
290 }
291
292 /// Set the container or artifact image name.
293 ///
294 /// Images are checked first because they usually identify services such as
295 /// databases more precisely than process names. Accepts a `&str` or an
296 /// `Option<&str>`.
297 #[must_use]
298 pub fn image(mut self, image: impl Into<Option<&'a str>>) -> Self {
299 self.image = image.into();
300 self
301 }
302
303 /// Set the project root directory.
304 ///
305 /// The directory itself is scanned; parent and child directories are not
306 /// searched by stack detection. Use
307 /// [`StackDetector::detect_project_root`](crate::StackDetector::detect_project_root)
308 /// first when starting from process paths. Accepts a `&Path` or an
309 /// `Option<&Path>`.
310 #[must_use]
311 pub fn project_root(mut self, project_root: impl Into<Option<&'a Path>>) -> Self {
312 self.project_root = project_root.into();
313 self
314 }
315
316 /// Set the executable file name taken from the executable path.
317 ///
318 /// This is used as a fallback when the process name is truncated or less
319 /// specific. Accepts a `&str` or an `Option<&str>`.
320 #[must_use]
321 pub fn exe_name(mut self, exe_name: impl Into<Option<&'a str>>) -> Self {
322 self.exe_name = exe_name.into();
323 self
324 }
325
326 /// Set the full executable path.
327 ///
328 /// The executable is not read. Its path is used only to decide whether
329 /// config detection is allowed for an unknown process name: it is when the
330 /// executable lies inside the project root, in a `go-build*` directory
331 /// from `go run`, or in the `target` directory of an enclosing Cargo
332 /// workspace. Accepts a `&Path` or an `Option<&Path>`.
333 #[must_use]
334 pub fn exe_path(mut self, exe_path: impl Into<Option<&'a Path>>) -> Self {
335 self.exe_path = exe_path.into();
336 self
337 }
338}