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, Value,
16};
17
18use crate::commands::{
19    HighlightCommand, HighlightLangCommand, HighlightThemeCommand, MjCommand, MjCompileCommand,
20    MjRenderCommand, ResponseStartCommand, ReverseProxyCommand, StaticCommand, ToSse,
21};
22use crate::logging::log_error;
23use crate::stdlib::load_http_nu_stdlib;
24use crate::Error;
25
26#[derive(Clone)]
27pub struct Engine {
28    pub state: EngineState,
29    pub closure: Option<Closure>,
30}
31
32impl Engine {
33    pub fn new() -> Result<Self, Error> {
34        let mut engine_state = create_default_context();
35
36        engine_state = add_shell_command_context(engine_state);
37        engine_state = add_cli_context(engine_state);
38        engine_state = nu_cmd_extra::extra::add_extra_command_context(engine_state);
39
40        load_http_nu_stdlib(&mut engine_state)?;
41
42        let init_cwd = std::env::current_dir()?;
43        gather_parent_env_vars(&mut engine_state, init_cwd.as_ref());
44
45        Ok(Self {
46            state: engine_state,
47            closure: None,
48        })
49    }
50
51    pub fn add_commands(&mut self, commands: Vec<Box<dyn Command>>) -> Result<(), Error> {
52        let mut working_set = StateWorkingSet::new(&self.state);
53        for command in commands {
54            working_set.add_decl(command);
55        }
56        self.state.merge_delta(working_set.render())?;
57        Ok(())
58    }
59
60    /// Load a Nushell plugin from the given path
61    pub fn load_plugin(&mut self, path: &Path) -> Result<(), Error> {
62        // Canonicalize the path
63        let path = path.canonicalize().map_err(|e| {
64            Error::from(format!("Failed to canonicalize plugin path {path:?}: {e}"))
65        })?;
66
67        // Create the plugin identity
68        let identity = PluginIdentity::new(&path, None).map_err(|_| {
69            Error::from(format!(
70                "Invalid plugin path {path:?}: must be named nu_plugin_*"
71            ))
72        })?;
73
74        let mut working_set = StateWorkingSet::new(&self.state);
75
76        // Add plugin to working set and get handle
77        let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?;
78
79        // Merge working set to make plugin available
80        self.state.merge_delta(working_set.render())?;
81
82        // Spawn the plugin to get its signatures
83        let interface = plugin.clone().get_plugin(None)?;
84
85        // Set plugin metadata
86        plugin.set_metadata(Some(interface.get_metadata()?));
87
88        // Add command declarations from plugin signatures
89        let mut working_set = StateWorkingSet::new(&self.state);
90        for signature in interface.get_signature()? {
91            let decl = PluginDeclaration::new(plugin.clone(), signature);
92            working_set.add_decl(Box::new(decl));
93        }
94        self.state.merge_delta(working_set.render())?;
95
96        Ok(())
97    }
98
99    pub fn parse_closure(&mut self, script: &str) -> Result<(), Error> {
100        let mut working_set = StateWorkingSet::new(&self.state);
101        let block = parse(&mut working_set, None, script.as_bytes(), false);
102
103        // Handle parse errors
104        if let Some(err) = working_set.parse_errors.first() {
105            let shell_error = ShellError::GenericError {
106                error: "Parse error".into(),
107                msg: format!("{err:?}"),
108                span: Some(err.span()),
109                help: None,
110                inner: vec![],
111            };
112            return Err(Error::from(format_cli_error(
113                &working_set,
114                &shell_error,
115                None,
116            )));
117        }
118
119        // Handle compile errors
120        if let Some(err) = working_set.compile_errors.first() {
121            let shell_error = ShellError::GenericError {
122                error: format!("Compile error {err}"),
123                msg: "".into(),
124                span: None,
125                help: None,
126                inner: vec![],
127            };
128            return Err(Error::from(format_cli_error(
129                &working_set,
130                &shell_error,
131                None,
132            )));
133        }
134
135        self.state.merge_delta(working_set.render())?;
136
137        let mut stack = Stack::new();
138        let result = eval_block_with_early_return::<WithoutDebug>(
139            &self.state,
140            &mut stack,
141            &block,
142            PipelineData::empty(),
143        )
144        .map_err(|err| {
145            let working_set = StateWorkingSet::new(&self.state);
146            Error::from(format_cli_error(&working_set, &err, None))
147        })?;
148
149        let closure = result
150            .body
151            .into_value(Span::unknown())
152            .map_err(|err| {
153                let working_set = StateWorkingSet::new(&self.state);
154                Error::from(format_cli_error(&working_set, &err, None))
155            })?
156            .into_closure()
157            .map_err(|err| {
158                let working_set = StateWorkingSet::new(&self.state);
159                Error::from(format_cli_error(&working_set, &err, None))
160            })?;
161
162        // Verify closure accepts exactly one argument
163        let block = self.state.get_block(closure.block_id);
164        if block.signature.required_positional.len() != 1 {
165            return Err(format!(
166                "Closure must accept exactly one request argument, found {}",
167                block.signature.required_positional.len()
168            )
169            .into());
170        }
171
172        self.state.merge_env(&mut stack)?;
173
174        self.closure = Some(closure);
175        Ok(())
176    }
177
178    /// Sets the interrupt signal for the engine
179    pub fn set_signals(&mut self, interrupt: Arc<AtomicBool>) {
180        self.state.set_signals(Signals::new(interrupt));
181    }
182
183    /// Evaluate a script string and return the result value
184    pub fn eval(&self, script: &str) -> Result<Value, Error> {
185        let mut working_set = StateWorkingSet::new(&self.state);
186        let block = parse(&mut working_set, None, script.as_bytes(), false);
187
188        if let Some(err) = working_set.parse_errors.first() {
189            let shell_error = ShellError::GenericError {
190                error: "Parse error".into(),
191                msg: format!("{err:?}"),
192                span: Some(err.span()),
193                help: None,
194                inner: vec![],
195            };
196            return Err(Error::from(format_cli_error(
197                &working_set,
198                &shell_error,
199                None,
200            )));
201        }
202
203        if let Some(err) = working_set.compile_errors.first() {
204            let shell_error = ShellError::GenericError {
205                error: format!("Compile error {err}"),
206                msg: "".into(),
207                span: None,
208                help: None,
209                inner: vec![],
210            };
211            return Err(Error::from(format_cli_error(
212                &working_set,
213                &shell_error,
214                None,
215            )));
216        }
217
218        // Clone engine state and merge the parsed block
219        let mut engine_state = self.state.clone();
220        engine_state.merge_delta(working_set.render())?;
221
222        let mut stack = Stack::new();
223        let result = eval_block_with_early_return::<WithoutDebug>(
224            &engine_state,
225            &mut stack,
226            &block,
227            PipelineData::empty(),
228        )
229        .map_err(|err| {
230            let working_set = StateWorkingSet::new(&engine_state);
231            Error::from(format_cli_error(&working_set, &err, None))
232        })?;
233
234        result.body.into_value(Span::unknown()).map_err(|err| {
235            let working_set = StateWorkingSet::new(&engine_state);
236            Error::from(format_cli_error(&working_set, &err, None))
237        })
238    }
239
240    /// Run the parsed closure with input value and pipeline data
241    pub fn run_closure(
242        &self,
243        input: Value,
244        pipeline_data: PipelineData,
245    ) -> Result<PipelineData, Error> {
246        let closure = self.closure.as_ref().ok_or("Closure not parsed")?;
247
248        let mut stack = Stack::new().captures_to_stack(closure.captures.clone());
249        let mut stack =
250            stack.push_redirection(Some(Redirection::Pipe(OutDest::PipeSeparate)), None);
251        let block = self.state.get_block(closure.block_id);
252
253        stack.add_var(
254            block.signature.required_positional[0].var_id.unwrap(),
255            input,
256        );
257
258        eval_block_with_early_return::<WithoutDebug>(&self.state, &mut stack, block, pipeline_data)
259            .map(|exec_data| exec_data.body)
260            .map_err(|err| {
261                let working_set = StateWorkingSet::new(&self.state);
262                Error::from(format_cli_error(&working_set, &err, None))
263            })
264    }
265
266    /// Adds http-nu custom commands to the engine
267    pub fn add_custom_commands(&mut self) -> Result<(), Error> {
268        self.add_commands(vec![
269            Box::new(ResponseStartCommand::new()),
270            Box::new(ReverseProxyCommand::new()),
271            Box::new(StaticCommand::new()),
272            Box::new(ToSse {}),
273            Box::new(MjCommand::new()),
274            Box::new(MjCompileCommand::new()),
275            Box::new(MjRenderCommand::new()),
276            Box::new(HighlightCommand::new()),
277            Box::new(HighlightThemeCommand::new()),
278            Box::new(HighlightLangCommand::new()),
279        ])
280    }
281}
282
283/// Creates an engine from a script by cloning a base engine and parsing the closure.
284/// On error, prints to stderr and emits JSON to stdout, returning None.
285pub fn script_to_engine(base: &Engine, script: &str) -> Option<Engine> {
286    let mut engine = base.clone();
287
288    if let Err(e) = engine.parse_closure(script) {
289        log_error(&nu_utils::strip_ansi_string_likely(e.to_string()));
290        return None;
291    }
292
293    Some(engine)
294}