Skip to main content

xs/nu/
config.rs

1use nu_engine::eval_block_with_early_return;
2use nu_parser::parse;
3use nu_protocol::debugger::WithoutDebug;
4use nu_protocol::engine::{Closure, Stack, StateWorkingSet};
5use nu_protocol::shell_error::generic::GenericError;
6use nu_protocol::{PipelineData, ShellError, Span, Value};
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::Error;
11use crate::nu::util::value_to_json;
12use crate::store::TTL;
13
14/// Configuration parsed from a Nushell script.
15pub struct NuScriptConfig {
16    /// The main executable closure defined by the `run:` field in the script.
17    pub run_closure: Closure,
18    /// The full Nushell Value (typically a record) that the script evaluated to.
19    /// Callers can use this to extract other script-defined options.
20    pub full_config_value: Value,
21}
22
23impl NuScriptConfig {
24    /// Deserializes specific options from the `full_config_value`.
25    ///
26    /// The type `T` must implement `serde::Deserialize`.
27    /// This is a convenience for callers to extract custom fields from the script's
28    /// configuration record after `run` has been processed.
29    pub fn deserialize_options<T>(&self) -> Result<T, Error>
30    where
31        T: for<'de> serde::Deserialize<'de>,
32    {
33        let json_value = value_to_json(&self.full_config_value);
34        serde_json::from_value(json_value)
35            .map_err(|e| format!("Failed to deserialize script options: {e}").into())
36    }
37}
38
39/// Options for customizing the output frames
40#[derive(Clone, Debug, Serialize, Deserialize, Default)]
41pub struct ReturnOptions {
42    /// Custom suffix for output frames (default is ".out" for actors, ".recv" for commands)
43    pub suffix: Option<String>,
44    /// Optional time-to-live for the output frames
45    pub ttl: Option<TTL>,
46    /// Storage target for output: "cas" forces CAS storage, default stores records as meta
47    pub target: Option<String>,
48}
49
50/// Parse a script into a NuScriptConfig struct.
51///
52/// Parses and evaluates the script, then extracts the `run` closure and the full
53/// configuration value. VFS modules (registered via `*.nu` topics) are already
54/// available on the engine state before this function is called.
55pub fn parse_config(engine: &mut crate::nu::Engine, script: &str) -> Result<NuScriptConfig, Error> {
56    let mut working_set = StateWorkingSet::new(&engine.state);
57    let block = parse(&mut working_set, None, script.as_bytes(), false);
58
59    if let Some(err) = working_set.parse_errors.first() {
60        let shell_error = ShellError::Generic(GenericError::new(
61            "Parse error in script",
62            format!("{err:?}"),
63            err.span(),
64        ));
65        return Err(Error::from(nu_protocol::format_cli_error(
66            None,
67            &working_set,
68            &shell_error,
69            None,
70        )));
71    }
72
73    if let Some(err) = working_set.compile_errors.first() {
74        let shell_error = ShellError::Generic(GenericError::new_internal(
75            "Compile error in script",
76            format!("{err:?}"),
77        ));
78        return Err(Error::from(nu_protocol::format_cli_error(
79            None,
80            &working_set,
81            &shell_error,
82            None,
83        )));
84    }
85
86    engine.state.merge_delta(working_set.render())?;
87
88    let mut stack = Stack::new();
89    let eval_result = eval_block_with_early_return::<WithoutDebug>(
90        &engine.state,
91        &mut stack,
92        &block,
93        PipelineData::empty(),
94    )
95    .map_err(|err| {
96        let working_set = StateWorkingSet::new(&engine.state);
97        Error::from(nu_protocol::format_cli_error(
98            None,
99            &working_set,
100            &err,
101            None,
102        ))
103    })?;
104
105    let config_value = eval_result.body.into_value(Span::unknown())?;
106
107    let run_val = config_value
108        .get_data_by_key("run")
109        .ok_or_else(|| -> Error { "Script must define a 'run' closure.".into() })?;
110    let run_closure = run_val
111        .as_closure()
112        .map_err(|e| -> Error { format!("'run' field must be a closure: {e}").into() })?;
113
114    engine.state.merge_env(&mut stack)?;
115
116    Ok(NuScriptConfig {
117        run_closure: run_closure.clone(),
118        full_config_value: config_value,
119    })
120}