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}