http_nu/
engine.rs

1use nu_cli::{add_cli_context, gather_parent_env_vars};
2use nu_cmd_lang::create_default_context;
3use nu_command::add_shell_command_context;
4use nu_engine::eval_block_with_early_return;
5use nu_parser::parse;
6use nu_protocol::engine::Command;
7use nu_protocol::format_cli_error;
8use nu_protocol::{
9    debugger::WithoutDebug,
10    engine::{Closure, EngineState, Redirection, Stack, StateWorkingSet},
11    OutDest, PipelineData, ShellError, Signals, Span, Value,
12};
13use std::sync::{atomic::AtomicBool, Arc};
14
15use crate::commands::{MjCommand, ResponseStartCommand, ReverseProxyCommand, StaticCommand, ToSse};
16use crate::Error;
17
18#[derive(Clone)]
19pub struct Engine {
20    pub state: EngineState,
21    pub closure: Option<Closure>,
22}
23
24impl Engine {
25    pub fn new() -> Result<Self, Error> {
26        let mut engine_state = create_default_context();
27
28        engine_state = add_shell_command_context(engine_state);
29        engine_state = add_cli_context(engine_state);
30        engine_state = nu_cmd_extra::extra::add_extra_command_context(engine_state);
31
32        let init_cwd = std::env::current_dir()?;
33        gather_parent_env_vars(&mut engine_state, init_cwd.as_ref());
34
35        Ok(Self {
36            state: engine_state,
37            closure: None,
38        })
39    }
40
41    pub fn add_commands(&mut self, commands: Vec<Box<dyn Command>>) -> Result<(), Error> {
42        let mut working_set = StateWorkingSet::new(&self.state);
43        for command in commands {
44            working_set.add_decl(command);
45        }
46        self.state.merge_delta(working_set.render())?;
47        Ok(())
48    }
49
50    pub fn parse_closure(&mut self, script: &str) -> Result<(), Error> {
51        let mut working_set = StateWorkingSet::new(&self.state);
52        let block = parse(&mut working_set, None, script.as_bytes(), false);
53
54        // Handle parse errors
55        if let Some(err) = working_set.parse_errors.first() {
56            let shell_error = ShellError::GenericError {
57                error: "Parse error".into(),
58                msg: format!("{err:?}"),
59                span: Some(err.span()),
60                help: None,
61                inner: vec![],
62            };
63            return Err(Error::from(format_cli_error(
64                &working_set,
65                &shell_error,
66                None,
67            )));
68        }
69
70        // Handle compile errors
71        if let Some(err) = working_set.compile_errors.first() {
72            let shell_error = ShellError::GenericError {
73                error: format!("Compile error {err}"),
74                msg: "".into(),
75                span: None,
76                help: None,
77                inner: vec![],
78            };
79            return Err(Error::from(format_cli_error(
80                &working_set,
81                &shell_error,
82                None,
83            )));
84        }
85
86        self.state.merge_delta(working_set.render())?;
87
88        let mut stack = Stack::new();
89        let result = eval_block_with_early_return::<WithoutDebug>(
90            &self.state,
91            &mut stack,
92            &block,
93            PipelineData::empty(),
94        )
95        .map_err(|err| {
96            let working_set = StateWorkingSet::new(&self.state);
97            Error::from(format_cli_error(&working_set, &err, None))
98        })?;
99
100        let closure = result
101            .body
102            .into_value(Span::unknown())
103            .map_err(|err| {
104                let working_set = StateWorkingSet::new(&self.state);
105                Error::from(format_cli_error(&working_set, &err, None))
106            })?
107            .into_closure()
108            .map_err(|err| {
109                let working_set = StateWorkingSet::new(&self.state);
110                Error::from(format_cli_error(&working_set, &err, None))
111            })?;
112
113        // Verify closure accepts exactly one argument
114        let block = self.state.get_block(closure.block_id);
115        if block.signature.required_positional.len() != 1 {
116            return Err(format!(
117                "Closure must accept exactly one request argument, found {}",
118                block.signature.required_positional.len()
119            )
120            .into());
121        }
122
123        self.state.merge_env(&mut stack)?;
124
125        self.closure = Some(closure);
126        Ok(())
127    }
128
129    /// Sets the interrupt signal for the engine
130    pub fn set_signals(&mut self, interrupt: Arc<AtomicBool>) {
131        self.state.set_signals(Signals::new(interrupt));
132    }
133
134    pub fn eval(&self, input: Value, pipeline_data: PipelineData) -> Result<PipelineData, Error> {
135        let closure = self.closure.as_ref().ok_or("Closure not parsed")?;
136
137        let mut stack = Stack::new();
138        let mut stack =
139            stack.push_redirection(Some(Redirection::Pipe(OutDest::PipeSeparate)), None);
140        let block = self.state.get_block(closure.block_id);
141
142        stack.add_var(
143            block.signature.required_positional[0].var_id.unwrap(),
144            input,
145        );
146
147        eval_block_with_early_return::<WithoutDebug>(&self.state, &mut stack, block, pipeline_data)
148            .map(|exec_data| exec_data.body)
149            .map_err(|err| {
150                let working_set = StateWorkingSet::new(&self.state);
151                Error::from(format_cli_error(&working_set, &err, None))
152            })
153    }
154
155    /// Adds http-nu custom commands to the engine
156    pub fn add_custom_commands(&mut self) -> Result<(), Error> {
157        self.add_commands(vec![
158            Box::new(ResponseStartCommand::new()),
159            Box::new(ReverseProxyCommand::new()),
160            Box::new(StaticCommand::new()),
161            Box::new(ToSse {}),
162            Box::new(MjCommand::new()),
163        ])
164    }
165}
166
167/// Creates an engine from a script by cloning a base engine and parsing the closure.
168/// On error, prints to stderr and emits JSON to stdout, returning None.
169pub fn script_to_engine(base: &Engine, script: &str) -> Option<Engine> {
170    let mut engine = base.clone();
171
172    if let Err(e) = engine.parse_closure(script) {
173        let err_str = e.to_string();
174        eprintln!("{err_str}");
175        println!(
176            "{}",
177            serde_json::json!({
178                "stamp": scru128::new(),
179                "message": "error",
180                "error": nu_utils::strip_ansi_string_likely(err_str)
181            })
182        );
183        return None;
184    }
185
186    Some(engine)
187}