1use std::any::Any;
8
9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
11pub enum LogLevel {
12 Debug,
13 Info,
14 Warn,
15 Error,
16}
17
18pub trait Host {
20 fn log(&self, level: LogLevel, message: &str);
21 fn get_config(&self, path: &str) -> Option<String>;
22 fn report_progress(&self, message: &str);
23 fn process_run(&self, cmd: &crate::process::ProcessCommand) -> Result<crate::process::ProcessOutput, String>;
25 fn process_spawn(&self, cmd: &crate::process::ProcessCommand) -> Result<Box<dyn crate::process::ChildHandle>, String>;
27 fn http_send(&self, req: &crate::http::HttpRequestData) -> Result<crate::http::HttpResponseData, String>;
29 fn env_var(&self, name: &str) -> Option<String>;
32 fn env_vars(&self) -> Vec<(String, String)>;
34 fn random_bytes(&self, n: usize) -> Vec<u8>;
37 fn monotonic_nanos(&self) -> u64;
41 fn sleep_nanos(&self, nanos: u64);
45}
46
47pub struct Context<'a> {
49 host: &'a dyn Host,
50 working_dir: String,
51 step_name: String,
52 state: Option<&'a dyn Any>,
53 plugin_config: Option<&'a dyn Any>,
54}
55
56impl<'a> Context<'a> {
57 #[must_use]
58 pub fn new(host: &'a dyn Host, working_dir: String, step_name: String) -> Self {
59 Self {
60 host,
61 working_dir,
62 step_name,
63 state: None,
64 plugin_config: None,
65 }
66 }
67 #[must_use]
68 pub fn with_state(mut self, state: &'a dyn Any) -> Self {
69 self.state = Some(state);
70 self
71 }
72 #[must_use]
73 pub fn with_plugin_config(mut self, cfg: &'a dyn Any) -> Self {
74 self.plugin_config = Some(cfg);
75 self
76 }
77
78 pub fn log_debug(&self, msg: &str) {
79 self.host.log(LogLevel::Debug, msg);
80 }
81 pub fn log_info(&self, msg: &str) {
82 self.host.log(LogLevel::Info, msg);
83 }
84 pub fn log_warn(&self, msg: &str) {
85 self.host.log(LogLevel::Warn, msg);
86 }
87 pub fn log_error(&self, msg: &str) {
88 self.host.log(LogLevel::Error, msg);
89 }
90 pub fn progress(&self, msg: &str) {
91 self.host.report_progress(msg);
92 }
93
94 #[must_use]
95 pub fn working_dir(&self) -> &str {
96 &self.working_dir
97 }
98 #[must_use]
99 pub fn step_name(&self) -> &str {
100 &self.step_name
101 }
102
103 #[must_use]
105 pub fn get_config(&self, path: &str) -> Option<serde_json::Value> {
106 let raw = self.host.get_config(path)?;
107 serde_json::from_str(&raw).ok()
108 }
109 #[must_use]
111 pub fn get_config_as<T: serde::de::DeserializeOwned>(&self, path: &str) -> Option<T> {
112 let raw = self.host.get_config(path)?;
113 crate::config::from_json_value(&raw).ok()
114 }
115
116 #[must_use]
118 pub fn env(&self) -> crate::env::Env<'a> {
119 crate::env::Env::new(self.host)
120 }
121
122 #[must_use]
124 pub fn clock(&self) -> crate::clock::Clock<'a> {
125 crate::clock::Clock::new(self.host)
126 }
127
128 #[must_use]
130 pub fn random(&self) -> crate::random::Random<'a> {
131 crate::random::Random::new(self.host)
132 }
133
134 pub fn command(&self, program: impl Into<String>) -> crate::process::Command<'a> {
136 crate::process::Command::new(self.host, program)
137 }
138
139 #[must_use]
141 pub fn http(&self) -> crate::http::Http<'a> {
142 crate::http::Http::new(self.host)
143 }
144
145 #[must_use]
147 pub fn state<T: 'static>(&self) -> &T {
148 self
149 .state
150 .expect("this plugin declared no `state:` in moonlit_plugin!")
151 .downcast_ref::<T>()
152 .expect("state type mismatch")
153 }
154 #[must_use]
156 pub fn plugin_config<T: 'static>(&self) -> &T {
157 self
158 .plugin_config
159 .expect("this plugin declared no `config:` in moonlit_plugin!")
160 .downcast_ref::<T>()
161 .expect("plugin config type mismatch")
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::testing::MockHost;
169
170 #[test]
171 fn log_and_progress_route_to_host() {
172 let host = MockHost::new();
173 let ctx = Context::new(&host, "/w".into(), "compile".into());
174 ctx.log_info("hi");
175 ctx.log_error("bad");
176 ctx.progress("50%");
177 assert_eq!(ctx.working_dir(), "/w");
178 assert_eq!(ctx.step_name(), "compile");
179 assert_eq!(
180 host.logs(),
181 vec![(LogLevel::Info, "hi".to_string()), (LogLevel::Error, "bad".to_string()),]
182 );
183 assert_eq!(host.progress(), vec!["50%".to_string()]);
184 }
185
186 #[test]
187 fn debug_and_warn_logs_route_to_host() {
188 let host = MockHost::new();
189 let ctx = Context::new(&host, "/w".into(), "s".into());
190 ctx.log_debug("d");
191 ctx.log_warn("w");
192 assert_eq!(
193 host.logs(),
194 vec![(LogLevel::Debug, "d".to_string()), (LogLevel::Warn, "w".to_string())]
195 );
196 }
197
198 #[test]
199 fn get_config_parses_and_coerces() {
200 let host = MockHost::new().with_config("plugin:port", "\"8080\"");
201 let ctx = Context::new(&host, "/w".into(), "s".into());
202 assert_eq!(ctx.get_config("plugin:port"), Some(serde_json::json!("8080")));
203 assert_eq!(ctx.get_config_as::<i64>("plugin:port"), Some(8080));
204 assert_eq!(ctx.get_config_as::<i64>("missing"), None);
205 }
206
207 #[test]
208 fn state_downcasts() {
209 struct S {
210 n: u32,
211 }
212 let host = MockHost::new();
213 let s = S { n: 7 };
214 let ctx = Context::new(&host, "/w".into(), "s".into()).with_state(&s);
215 assert_eq!(ctx.state::<S>().n, 7);
216 }
217
218 #[test]
219 #[should_panic(expected = "this plugin declared no `state:` in moonlit_plugin!")]
220 fn state_panics_when_plugin_declared_none() {
221 struct S;
222 let host = MockHost::new();
223 let ctx = Context::new(&host, "/w".into(), "s".into());
224 let _ = ctx.state::<S>();
225 }
226
227 #[test]
228 #[should_panic(expected = "this plugin declared no `config:` in moonlit_plugin!")]
229 fn plugin_config_panics_when_plugin_declared_none() {
230 struct C;
231 let host = MockHost::new();
232 let ctx = Context::new(&host, "/w".into(), "s".into());
233 let _ = ctx.plugin_config::<C>();
234 }
235}