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