Skip to main content

http_nu/
engine.rs

1use std::path::Path;
2use std::sync::{atomic::AtomicBool, Arc};
3
4use tokio_util::sync::CancellationToken;
5
6use nu_cli::{add_cli_context, gather_parent_env_vars};
7use nu_cmd_lang::create_default_context;
8use nu_command::add_shell_command_context;
9use nu_engine::eval_block_with_early_return;
10use nu_parser::parse;
11use nu_plugin_engine::{GetPlugin, PluginDeclaration};
12use nu_protocol::engine::Command;
13use nu_protocol::format_cli_error;
14use nu_protocol::{
15    debugger::WithoutDebug,
16    engine::{Closure, EngineState, Redirection, Stack, StateWorkingSet},
17    OutDest, PipelineData, PluginIdentity, RegisteredPlugin, ShellError, Signals, Span, Type,
18    Value,
19};
20
21use crate::commands::{
22    HighlightCommand, HighlightLangCommand, HighlightThemeCommand, MdCommand, MjCommand,
23    MjCompileCommand, MjRenderCommand, PrintCommand, ReverseProxyCommand, StaticCommand, ToSse,
24};
25use crate::logging::log_error;
26use crate::stdlib::load_http_nu_stdlib;
27use crate::Error;
28
29#[derive(Clone)]
30pub struct Engine {
31    pub state: EngineState,
32    pub closure: Option<Closure>,
33    /// Cancellation token triggered on engine reload
34    pub reload_token: CancellationToken,
35}
36
37impl Engine {
38    pub fn new() -> Result<Self, Error> {
39        let mut engine_state = create_default_context();
40
41        engine_state = add_shell_command_context(engine_state);
42        engine_state = add_cli_context(engine_state);
43        engine_state = nu_cmd_extra::extra::add_extra_command_context(engine_state);
44
45        load_http_nu_stdlib(&mut engine_state)?;
46
47        let init_cwd = std::env::current_dir()?;
48        gather_parent_env_vars(&mut engine_state, init_cwd.as_ref());
49
50        Ok(Self {
51            state: engine_state,
52            closure: None,
53            reload_token: CancellationToken::new(),
54        })
55    }
56
57    /// Sets `$env.HTTP_NU` with server configuration for stdlib modules
58    pub fn set_http_nu_env(&mut self, dev: bool) -> Result<(), Error> {
59        let mut stack = Stack::new();
60        let span = Span::unknown();
61        let record = Value::record(
62            nu_protocol::record! {
63                "dev" => Value::bool(dev, span),
64            },
65            span,
66        );
67        stack.add_env_var("HTTP_NU".to_string(), record);
68        self.state.merge_env(&mut stack)?;
69        Ok(())
70    }
71
72    pub fn add_commands(&mut self, commands: Vec<Box<dyn Command>>) -> Result<(), Error> {
73        let mut working_set = StateWorkingSet::new(&self.state);
74        for command in commands {
75            working_set.add_decl(command);
76        }
77        self.state.merge_delta(working_set.render())?;
78        Ok(())
79    }
80
81    /// Load a Nushell plugin from the given path
82    pub fn load_plugin(&mut self, path: &Path) -> Result<(), Error> {
83        // Canonicalize the path
84        let path = path.canonicalize().map_err(|e| {
85            Error::from(format!("Failed to canonicalize plugin path {path:?}: {e}"))
86        })?;
87
88        // Create the plugin identity
89        let identity = PluginIdentity::new(&path, None).map_err(|_| {
90            Error::from(format!(
91                "Invalid plugin path {path:?}: must be named nu_plugin_*"
92            ))
93        })?;
94
95        let mut working_set = StateWorkingSet::new(&self.state);
96
97        // Add plugin to working set and get handle
98        let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?;
99
100        // Merge working set to make plugin available
101        self.state.merge_delta(working_set.render())?;
102
103        // Spawn the plugin to get its signatures
104        let interface = plugin.clone().get_plugin(None)?;
105
106        // Set plugin metadata
107        plugin.set_metadata(Some(interface.get_metadata()?));
108
109        // Add command declarations from plugin signatures
110        let mut working_set = StateWorkingSet::new(&self.state);
111        for signature in interface.get_signature()? {
112            let decl = PluginDeclaration::new(plugin.clone(), signature);
113            working_set.add_decl(Box::new(decl));
114        }
115        self.state.merge_delta(working_set.render())?;
116
117        Ok(())
118    }
119
120    pub fn parse_closure(&mut self, script: &str) -> Result<(), Error> {
121        let mut working_set = StateWorkingSet::new(&self.state);
122        let block = parse(&mut working_set, None, script.as_bytes(), false);
123
124        // Handle parse errors
125        if let Some(err) = working_set.parse_errors.first() {
126            let shell_error = ShellError::GenericError {
127                error: "Parse error".into(),
128                msg: format!("{err:?}"),
129                span: Some(err.span()),
130                help: None,
131                inner: vec![],
132            };
133            return Err(Error::from(format_cli_error(
134                None,
135                &working_set,
136                &shell_error,
137                None,
138            )));
139        }
140
141        // Handle compile errors
142        if let Some(err) = working_set.compile_errors.first() {
143            let shell_error = ShellError::GenericError {
144                error: format!("Compile error {err}"),
145                msg: "".into(),
146                span: None,
147                help: None,
148                inner: vec![],
149            };
150            return Err(Error::from(format_cli_error(
151                None,
152                &working_set,
153                &shell_error,
154                None,
155            )));
156        }
157
158        self.state.merge_delta(working_set.render())?;
159
160        let mut stack = Stack::new();
161        let result = eval_block_with_early_return::<WithoutDebug>(
162            &self.state,
163            &mut stack,
164            &block,
165            PipelineData::empty(),
166        )
167        .map_err(|err| {
168            let working_set = StateWorkingSet::new(&self.state);
169            Error::from(format_cli_error(None, &working_set, &err, None))
170        })?;
171
172        let closure = result
173            .body
174            .into_value(Span::unknown())
175            .map_err(|err| {
176                let working_set = StateWorkingSet::new(&self.state);
177                Error::from(format_cli_error(None, &working_set, &err, None))
178            })?
179            .into_closure()
180            .map_err(|err| {
181                let working_set = StateWorkingSet::new(&self.state);
182                Error::from(format_cli_error(None, &working_set, &err, None))
183            })?;
184
185        // Verify closure accepts exactly one argument
186        let block = self.state.get_block(closure.block_id);
187        if block.signature.required_positional.len() != 1 {
188            return Err(format!(
189                "Closure must accept exactly one request argument, found {}",
190                block.signature.required_positional.len()
191            )
192            .into());
193        }
194
195        self.state.merge_env(&mut stack)?;
196
197        self.closure = Some(closure);
198        Ok(())
199    }
200
201    /// Sets the interrupt signal for the engine
202    pub fn set_signals(&mut self, interrupt: Arc<AtomicBool>) {
203        self.state.set_signals(Signals::new(interrupt));
204    }
205
206    /// Sets NU_LIB_DIRS const for module resolution
207    pub fn set_lib_dirs(&mut self, paths: &[std::path::PathBuf]) -> Result<(), Error> {
208        if paths.is_empty() {
209            return Ok(());
210        }
211        let span = Span::unknown();
212        let vals: Vec<Value> = paths
213            .iter()
214            .map(|p| Value::string(p.to_string_lossy(), span))
215            .collect();
216
217        let mut working_set = StateWorkingSet::new(&self.state);
218        let var_id = working_set.add_variable(
219            b"$NU_LIB_DIRS".into(),
220            span,
221            Type::List(Box::new(Type::String)),
222            false,
223        );
224        working_set.set_variable_const_val(var_id, Value::list(vals, span));
225        self.state.merge_delta(working_set.render())?;
226        Ok(())
227    }
228
229    /// Evaluate a script string and return the result value
230    pub fn eval(&self, script: &str) -> Result<Value, Error> {
231        let mut working_set = StateWorkingSet::new(&self.state);
232        let block = parse(&mut working_set, None, script.as_bytes(), false);
233
234        if let Some(err) = working_set.parse_errors.first() {
235            let shell_error = ShellError::GenericError {
236                error: "Parse error".into(),
237                msg: format!("{err:?}"),
238                span: Some(err.span()),
239                help: None,
240                inner: vec![],
241            };
242            return Err(Error::from(format_cli_error(
243                None,
244                &working_set,
245                &shell_error,
246                None,
247            )));
248        }
249
250        if let Some(err) = working_set.compile_errors.first() {
251            let shell_error = ShellError::GenericError {
252                error: format!("Compile error {err}"),
253                msg: "".into(),
254                span: None,
255                help: None,
256                inner: vec![],
257            };
258            return Err(Error::from(format_cli_error(
259                None,
260                &working_set,
261                &shell_error,
262                None,
263            )));
264        }
265
266        // Clone engine state and merge the parsed block
267        let mut engine_state = self.state.clone();
268        engine_state.merge_delta(working_set.render())?;
269
270        let mut stack = Stack::new();
271        let result = eval_block_with_early_return::<WithoutDebug>(
272            &engine_state,
273            &mut stack,
274            &block,
275            PipelineData::empty(),
276        )
277        .map_err(|err| {
278            let working_set = StateWorkingSet::new(&engine_state);
279            Error::from(format_cli_error(None, &working_set, &err, None))
280        })?;
281
282        result.body.into_value(Span::unknown()).map_err(|err| {
283            let working_set = StateWorkingSet::new(&engine_state);
284            Error::from(format_cli_error(None, &working_set, &err, None))
285        })
286    }
287
288    /// Run the parsed closure with input value and pipeline data
289    pub fn run_closure(
290        &self,
291        input: Value,
292        pipeline_data: PipelineData,
293    ) -> Result<PipelineData, Error> {
294        let closure = self.closure.as_ref().ok_or("Closure not parsed")?;
295
296        let mut stack = Stack::new().captures_to_stack(closure.captures.clone());
297        let mut stack =
298            stack.push_redirection(Some(Redirection::Pipe(OutDest::PipeSeparate)), None);
299        let block = self.state.get_block(closure.block_id);
300
301        stack.add_var(
302            block.signature.required_positional[0].var_id.unwrap(),
303            input,
304        );
305
306        eval_block_with_early_return::<WithoutDebug>(&self.state, &mut stack, block, pipeline_data)
307            .map(|exec_data| exec_data.body)
308            .map_err(|err| {
309                let working_set = StateWorkingSet::new(&self.state);
310                Error::from(format_cli_error(None, &working_set, &err, None))
311            })
312    }
313
314    /// Adds http-nu custom commands to the engine
315    pub fn add_custom_commands(&mut self) -> Result<(), Error> {
316        self.add_commands(vec![
317            Box::new(ReverseProxyCommand::new()),
318            Box::new(StaticCommand::new()),
319            Box::new(ToSse {}),
320            Box::new(MjCommand::new()),
321            Box::new(MjCompileCommand::new()),
322            Box::new(MjRenderCommand::new()),
323            Box::new(HighlightCommand::new()),
324            Box::new(HighlightThemeCommand::new()),
325            Box::new(HighlightLangCommand::new()),
326            Box::new(MdCommand::new()),
327            Box::new(PrintCommand::new()),
328        ])
329    }
330
331    /// Adds cross.stream store commands (.cat, .append, .cas, .last) to the engine
332    #[cfg(feature = "cross-stream")]
333    pub fn add_store_commands(&mut self, store: &xs::store::Store) -> Result<(), Error> {
334        self.add_commands(vec![
335            Box::new(xs::nu::commands::cat_stream_command::CatStreamCommand::new(
336                store.clone(),
337            )),
338            Box::new(xs::nu::commands::append_command::AppendCommand::new(
339                store.clone(),
340                serde_json::json!({}),
341            )),
342            Box::new(xs::nu::commands::cas_command::CasCommand::new(
343                store.clone(),
344            )),
345            Box::new(xs::nu::commands::last_stream_command::LastStreamCommand::new(store.clone())),
346            Box::new(xs::nu::commands::get_command::GetCommand::new(
347                store.clone(),
348            )),
349            Box::new(xs::nu::commands::remove_command::RemoveCommand::new(
350                store.clone(),
351            )),
352            Box::new(xs::nu::commands::scru128_command::Scru128Command::new()),
353        ])
354    }
355}
356
357/// Creates an engine from a script by cloning a base engine and parsing the closure.
358/// On error, prints to stderr and emits JSON to stdout, returning None.
359pub fn script_to_engine(base: &Engine, script: &str) -> Option<Engine> {
360    let mut engine = base.clone();
361    // Fresh cancellation token for this engine instance
362    engine.reload_token = CancellationToken::new();
363
364    if let Err(e) = engine.parse_closure(script) {
365        log_error(&nu_utils::strip_ansi_string_likely(e.to_string()));
366        return None;
367    }
368
369    Some(engine)
370}