Skip to main content

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    pub(crate) const fn runtime(text: &'static str) -> Self {
94        Self::from_static(text, StackKind::Runtime)
95    }
96
97    pub(crate) const fn framework(text: &'static str) -> Self {
98        Self::from_static(text, StackKind::Framework)
99    }
100
101    pub(crate) const fn tool(text: &'static str) -> Self {
102        Self::from_static(text, StackKind::Tool)
103    }
104
105    pub(crate) const fn database(text: &'static str) -> Self {
106        Self::from_static(text, StackKind::Database)
107    }
108
109    pub(crate) const fn service(text: &'static str) -> Self {
110        Self::from_static(text, StackKind::Service)
111    }
112
113    /// Return the label text.
114    #[must_use]
115    pub fn as_str(&self) -> &str {
116        &self.text
117    }
118
119    /// Return the label category.
120    #[must_use]
121    pub const fn kind(&self) -> StackKind {
122        self.kind
123    }
124
125    /// Convert the label into its text, dropping the kind.
126    ///
127    /// Static labels stay borrowed, so this does not allocate for built-in
128    /// detections.
129    #[must_use]
130    pub fn into_cow(self) -> Cow<'static, str> {
131        self.text
132    }
133}
134
135impl fmt::Display for StackLabel {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.write_str(&self.text)
138    }
139}
140
141impl AsRef<str> for StackLabel {
142    fn as_ref(&self) -> &str {
143        &self.text
144    }
145}
146
147impl PartialEq<str> for StackLabel {
148    fn eq(&self, other: &str) -> bool {
149        self.text == other
150    }
151}
152
153impl PartialEq<&str> for StackLabel {
154    fn eq(&self, other: &&str) -> bool {
155        self.text == *other
156    }
157}
158
159impl PartialEq<StackLabel> for str {
160    fn eq(&self, other: &StackLabel) -> bool {
161        self == other.text
162    }
163}
164
165impl PartialEq<StackLabel> for &str {
166    fn eq(&self, other: &StackLabel) -> bool {
167        *self == other.text
168    }
169}
170
171impl From<StackLabel> for Cow<'static, str> {
172    fn from(label: StackLabel) -> Self {
173        label.text
174    }
175}
176
177impl From<StackLabel> for String {
178    fn from(label: StackLabel) -> Self {
179        label.text.into_owned()
180    }
181}
182
183/// Process-like path inputs used to resolve a project root.
184///
185/// Build one with [`ProjectInput::new`] and the chainable setters. Project root
186/// resolution is best-effort and uses a stable fallback order:
187///
188/// 1. [`cwd`](Self::cwd)
189/// 2. parent of [`exe`](Self::exe)
190/// 3. parents of absolute paths in [`cmd`](Self::cmd)
191///
192/// Relative command-line arguments are ignored because they cannot be resolved
193/// safely without knowing the process working directory at the time the command
194/// started.
195///
196/// The home-directory ceiling is not part of the input. It is configured on
197/// [`StackDetector`](crate::StackDetector) or passed to
198/// [`resolve_project_root`](crate::resolve_project_root) directly.
199///
200/// # Examples
201///
202/// ```
203/// use std::path::Path;
204/// use what_stack::ProjectInput;
205///
206/// let input = ProjectInput::new()
207///     .cwd(Path::new("/workspace/api/src"))
208///     .exe(None);
209/// # let _ = input;
210/// ```
211#[derive(Debug, Clone, Copy, Default)]
212#[non_exhaustive]
213pub struct ProjectInput<'a> {
214    pub(crate) cwd: Option<&'a Path>,
215    pub(crate) exe: Option<&'a Path>,
216    pub(crate) cmd: &'a [OsString],
217}
218
219impl<'a> ProjectInput<'a> {
220    /// Create an empty input with no path hints.
221    #[must_use]
222    pub const fn new() -> Self {
223        Self {
224            cwd: None,
225            exe: None,
226            cmd: &[],
227        }
228    }
229
230    /// Set the process working directory.
231    ///
232    /// This is the most trustworthy project-root hint and is checked first.
233    /// Accepts a `&Path` or an `Option<&Path>`.
234    #[must_use]
235    pub fn cwd(mut self, cwd: impl Into<Option<&'a Path>>) -> Self {
236        self.cwd = cwd.into();
237        self
238    }
239
240    /// Set the process executable path.
241    ///
242    /// The parent directory is searched when the working directory is missing
243    /// or does not resolve to a project. Accepts a `&Path` or an
244    /// `Option<&Path>`.
245    #[must_use]
246    pub fn exe(mut self, exe: impl Into<Option<&'a Path>>) -> Self {
247        self.exe = exe.into();
248        self
249    }
250
251    /// Set the process command-line arguments.
252    ///
253    /// Only absolute path arguments are inspected, in their original order.
254    #[must_use]
255    pub const fn cmd(mut self, cmd: &'a [OsString]) -> Self {
256        self.cmd = cmd;
257        self
258    }
259}
260
261/// Inputs used by [`StackDetector`](crate::StackDetector) to resolve a stack.
262///
263/// Build one with [`StackInput::new`] and the chainable setters. The detector
264/// uses image, process, and project config metadata. Config detection is
265/// guarded to avoid false positives: a config label can win only when the
266/// process label is a [`StackKind::Runtime`] or [`StackKind::Tool`], or when the
267/// process is unknown but its executable path lies inside the project root. A
268/// known runtime or tool accepts only config labels from its own ecosystem.
269///
270/// # Examples
271///
272/// ```
273/// use std::path::Path;
274/// use what_stack::StackInput;
275///
276/// let input = StackInput::new("node")
277///     .exe_name("node.exe")
278///     .project_root(Path::new("/workspace/web"))
279///     .image(None);
280/// # let _ = input;
281/// ```
282#[derive(Debug, Clone, Copy, Default)]
283#[non_exhaustive]
284pub struct StackInput<'a> {
285    pub(crate) image: Option<&'a str>,
286    pub(crate) project_root: Option<&'a Path>,
287    pub(crate) process_name: &'a str,
288    pub(crate) exe_name: Option<&'a str>,
289    pub(crate) exe_path: Option<&'a Path>,
290}
291
292impl<'a> StackInput<'a> {
293    /// Create an input from a process name taken from the process table.
294    ///
295    /// Pass an empty string when no process name is available.
296    #[must_use]
297    pub const fn new(process_name: &'a str) -> Self {
298        Self {
299            image: None,
300            project_root: None,
301            process_name,
302            exe_name: None,
303            exe_path: None,
304        }
305    }
306
307    /// Set the container or artifact image name.
308    ///
309    /// Images are checked first because they usually identify services such as
310    /// databases more precisely than process names. Accepts a `&str` or an
311    /// `Option<&str>`.
312    #[must_use]
313    pub fn image(mut self, image: impl Into<Option<&'a str>>) -> Self {
314        self.image = image.into();
315        self
316    }
317
318    /// Set the project root directory.
319    ///
320    /// The directory itself is scanned; parent and child directories are not
321    /// searched by stack detection. Use
322    /// [`StackDetector::detect_project_root`](crate::StackDetector::detect_project_root)
323    /// first when starting from process paths. Accepts a `&Path` or an
324    /// `Option<&Path>`.
325    #[must_use]
326    pub fn project_root(mut self, project_root: impl Into<Option<&'a Path>>) -> Self {
327        self.project_root = project_root.into();
328        self
329    }
330
331    /// Set the executable file name taken from the executable path.
332    ///
333    /// This is used as a fallback when the process name is truncated or less
334    /// specific. Accepts a `&str` or an `Option<&str>`.
335    #[must_use]
336    pub fn exe_name(mut self, exe_name: impl Into<Option<&'a str>>) -> Self {
337        self.exe_name = exe_name.into();
338        self
339    }
340
341    /// Set the full executable path.
342    ///
343    /// The path is not read. It is used only to decide whether config detection
344    /// is allowed for an unknown process name. Accepts a `&Path` or an
345    /// `Option<&Path>`.
346    #[must_use]
347    pub fn exe_path(mut self, exe_path: impl Into<Option<&'a Path>>) -> Self {
348        self.exe_path = exe_path.into();
349        self
350    }
351}