Skip to main content

ready_set_sdk/
context.rs

1//! Per-invocation context populated from the dispatcher env contract.
2//!
3//! See
4//! [`docs/contracts/env-vars.md`](https://github.com/pulsearc-ai/ready-set/blob/main/docs/contracts/env-vars.md)
5//! for the source of truth.
6
7use std::env;
8use std::path::{Path, PathBuf};
9
10/// Requested log verbosity. Mirrors `READY_SET_LOG`.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum LogLevel {
13    /// Errors only.
14    Quiet,
15    /// Default.
16    #[default]
17    Normal,
18    /// Debug logging.
19    Verbose,
20}
21
22impl LogLevel {
23    fn parse(raw: Option<&str>) -> Self {
24        match raw.map(str::trim) {
25            Some("quiet") => Self::Quiet,
26            Some("verbose") => Self::Verbose,
27            // Unrecognized values fall back to the default per env-vars contract.
28            _ => Self::Normal,
29        }
30    }
31}
32
33/// Color preference. Mirrors `READY_SET_COLOR`.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum ColorMode {
36    /// Emit escape codes only when stdout is a TTY.
37    #[default]
38    Auto,
39    /// Always emit escape codes.
40    Always,
41    /// Never emit escape codes.
42    Never,
43}
44
45impl ColorMode {
46    fn parse(raw: Option<&str>) -> Self {
47        match raw.map(str::trim) {
48            Some("always") => Self::Always,
49            Some("never") => Self::Never,
50            _ => Self::Auto,
51        }
52    }
53}
54
55/// Per-invocation state, constructed from the dispatcher env contract.
56#[derive(Debug, Clone)]
57pub struct Context {
58    dispatcher_version: Option<semver::Version>,
59    project_root: Option<PathBuf>,
60    config_path: Option<PathBuf>,
61    output_mode: crate::output::OutputMode,
62    log_level: LogLevel,
63    color: ColorMode,
64}
65
66impl Context {
67    /// Construct a `Context` by reading the documented `READY_SET_*` env vars.
68    ///
69    /// Tolerates unset and unrecognized values per the env-vars contract.
70    #[must_use]
71    pub fn from_env() -> Self {
72        let dispatcher_version = env::var("READY_SET_DISPATCHER_VERSION")
73            .ok()
74            .and_then(|raw| semver::Version::parse(raw.trim()).ok());
75
76        let project_root = env::var_os("READY_SET_PROJECT_ROOT")
77            .filter(|s| !s.is_empty())
78            .map(PathBuf::from);
79
80        let config_path = env::var_os("READY_SET_CONFIG_PATH")
81            .filter(|s| !s.is_empty())
82            .map(PathBuf::from);
83
84        let output_mode =
85            crate::output::OutputMode::parse(env::var("READY_SET_OUTPUT").ok().as_deref());
86        let log_level = LogLevel::parse(env::var("READY_SET_LOG").ok().as_deref());
87        let color = ColorMode::parse(env::var("READY_SET_COLOR").ok().as_deref());
88
89        Self {
90            dispatcher_version,
91            project_root,
92            config_path,
93            output_mode,
94            log_level,
95            color,
96        }
97    }
98
99    /// Construct a default `Context` for tests or programmatic use.
100    #[must_use]
101    pub const fn default_for_tests() -> Self {
102        Self {
103            dispatcher_version: None,
104            project_root: None,
105            config_path: None,
106            output_mode: crate::output::OutputMode::Human,
107            log_level: LogLevel::Normal,
108            color: ColorMode::Auto,
109        }
110    }
111
112    /// Version of the dispatcher that invoked this plugin, if any.
113    #[must_use]
114    pub const fn dispatcher_version(&self) -> Option<&semver::Version> {
115        self.dispatcher_version.as_ref()
116    }
117
118    /// Resolved project root, if any.
119    #[must_use]
120    pub fn project_root(&self) -> Option<&Path> {
121        self.project_root.as_deref()
122    }
123
124    /// Resolved `.ready-set.toml` path, if any.
125    #[must_use]
126    pub fn config_path(&self) -> Option<&Path> {
127        self.config_path.as_deref()
128    }
129
130    /// Requested output mode.
131    #[must_use]
132    pub const fn output_mode(&self) -> crate::output::OutputMode {
133        self.output_mode
134    }
135
136    /// Requested log verbosity.
137    #[must_use]
138    pub const fn log_level(&self) -> LogLevel {
139        self.log_level
140    }
141
142    /// Color preference.
143    #[must_use]
144    pub const fn color(&self) -> ColorMode {
145        self.color
146    }
147
148    /// Return [`Self::project_root`] if known, otherwise `cwd`.
149    ///
150    /// # Errors
151    ///
152    /// Returns the underlying I/O error from `std::env::current_dir` if the
153    /// current working directory cannot be resolved.
154    pub fn project_root_or_cwd(&self) -> std::io::Result<PathBuf> {
155        if let Some(root) = &self.project_root {
156            return Ok(root.clone());
157        }
158        env::current_dir()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn parses_log_level_with_fallbacks() {
168        assert_eq!(LogLevel::parse(Some("quiet")), LogLevel::Quiet);
169        assert_eq!(LogLevel::parse(Some("verbose")), LogLevel::Verbose);
170        assert_eq!(LogLevel::parse(Some("normal")), LogLevel::Normal);
171        // Forward-compat: unknown values fall back, no panic.
172        assert_eq!(LogLevel::parse(Some("bogus")), LogLevel::Normal);
173        assert_eq!(LogLevel::parse(None), LogLevel::Normal);
174    }
175
176    #[test]
177    fn parses_color_mode_with_fallbacks() {
178        assert_eq!(ColorMode::parse(Some("always")), ColorMode::Always);
179        assert_eq!(ColorMode::parse(Some("never")), ColorMode::Never);
180        assert_eq!(ColorMode::parse(Some("auto")), ColorMode::Auto);
181        assert_eq!(ColorMode::parse(Some("rainbow")), ColorMode::Auto);
182        assert_eq!(ColorMode::parse(None), ColorMode::Auto);
183    }
184}