Skip to main content

ready_set_sdk/
dispatch.rs

1//! Helpers for one plugin to dispatch to another via the `ready-set` core.
2//!
3//! All inter-plugin calls go through the dispatcher (not directly to a
4//! `ready-set-<name>` binary) so PATH semantics and built-in resolution stay
5//! consistent. The env contract is forwarded automatically.
6
7use std::ffi::OsString;
8use std::process::Command;
9
10use crate::context::{ColorMode, Context, LogLevel};
11use crate::error::{Error, Result};
12use crate::output::OutputMode;
13
14/// Where to find the dispatcher binary. Tests and integration scenarios may
15/// set the `READY_SET_BIN` env var to override the default `which`-based
16/// lookup.
17fn locate_dispatcher() -> Result<std::path::PathBuf> {
18    if let Some(explicit) = std::env::var_os("READY_SET_BIN") {
19        return Ok(std::path::PathBuf::from(explicit));
20    }
21    which::which("ready-set").map_err(|_| Error::MissingDependency {
22        name: "ready-set".to_string(),
23        hint: Some("install via `cargo install ready-set`".to_string()),
24    })
25}
26
27/// Outcome of a dispatched subcommand.
28#[derive(Debug)]
29#[non_exhaustive]
30pub enum DispatchOutcome {
31    /// stdout was streamed to the parent's stdout.
32    Streamed {
33        /// Process exit code reported by the child.
34        exit_code: i32,
35    },
36    /// stdout was captured for programmatic use.
37    Captured {
38        /// Captured stdout bytes.
39        stdout: Vec<u8>,
40        /// Process exit code reported by the child.
41        exit_code: i32,
42    },
43}
44
45/// Builder for a dispatched subcommand call.
46pub struct DispatchBuilder {
47    subcommand: String,
48    args: Vec<OsString>,
49    capture: bool,
50}
51
52impl DispatchBuilder {
53    /// Start a builder for `ready-set <subcommand>`.
54    #[must_use]
55    pub fn new(subcommand: impl Into<String>) -> Self {
56        Self {
57            subcommand: subcommand.into(),
58            args: Vec::new(),
59            capture: false,
60        }
61    }
62
63    /// Append one argument.
64    #[must_use]
65    pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
66        self.args.push(arg.into());
67        self
68    }
69
70    /// Append multiple arguments.
71    #[must_use]
72    pub fn args<I, S>(mut self, args: I) -> Self
73    where
74        I: IntoIterator<Item = S>,
75        S: Into<OsString>,
76    {
77        self.args.extend(args.into_iter().map(Into::into));
78        self
79    }
80
81    /// Capture stdout instead of streaming it.
82    #[must_use]
83    pub const fn capture(mut self, yes: bool) -> Self {
84        self.capture = yes;
85        self
86    }
87
88    /// Run the dispatched subcommand.
89    ///
90    /// # Errors
91    ///
92    /// Returns [`Error::MissingDependency`] if the dispatcher binary cannot
93    /// be located on PATH (override with `READY_SET_BIN`), or [`Error::Io`]
94    /// if spawning the child fails.
95    pub fn run(self, ctx: &Context) -> Result<DispatchOutcome> {
96        let bin = locate_dispatcher()?;
97        let mut cmd = Command::new(bin);
98        cmd.arg(&self.subcommand);
99        cmd.args(&self.args);
100
101        // Forward env contract verbatim so the child sees what we received.
102        export_contract(&mut cmd, ctx);
103
104        if self.capture {
105            cmd.stdout(std::process::Stdio::piped());
106            let output = cmd.output()?;
107            Ok(DispatchOutcome::Captured {
108                stdout: output.stdout,
109                exit_code: output.status.code().unwrap_or(-1),
110            })
111        } else {
112            let status = cmd.status()?;
113            Ok(DispatchOutcome::Streamed {
114                exit_code: status.code().unwrap_or(-1),
115            })
116        }
117    }
118}
119
120/// Export the dispatcher env contract onto `cmd` from `ctx`.
121pub fn export_contract(cmd: &mut Command, ctx: &Context) {
122    if let Some(version) = ctx.dispatcher_version() {
123        cmd.env("READY_SET_DISPATCHER_VERSION", version.to_string());
124    }
125    if let Some(root) = ctx.project_root() {
126        cmd.env("READY_SET_PROJECT_ROOT", root);
127    }
128    if let Some(cfg) = ctx.config_path() {
129        cmd.env("READY_SET_CONFIG_PATH", cfg);
130    }
131    cmd.env(
132        "READY_SET_OUTPUT",
133        match ctx.output_mode() {
134            OutputMode::Human => "human",
135            OutputMode::Json => "json",
136        },
137    );
138    cmd.env(
139        "READY_SET_LOG",
140        match ctx.log_level() {
141            LogLevel::Quiet => "quiet",
142            LogLevel::Normal => "normal",
143            LogLevel::Verbose => "verbose",
144        },
145    );
146    cmd.env(
147        "READY_SET_COLOR",
148        match ctx.color() {
149            ColorMode::Auto => "auto",
150            ColorMode::Always => "always",
151            ColorMode::Never => "never",
152        },
153    );
154}