http_nu/
engine.rs

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