1use std::collections::HashMap;
2
3use nu_engine::eval_block_with_early_return;
4use nu_parser::parse;
5use nu_protocol::debugger::WithoutDebug;
6use nu_protocol::engine::{Closure, Stack, StateWorkingSet};
7use nu_protocol::{PipelineData, ShellError, Span, Value};
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::Error;
12use crate::nu::util::value_to_json;
13use crate::store::TTL;
14
15pub struct NuScriptConfig {
17 pub run_closure: Closure,
19 pub full_config_value: Value,
22}
23
24impl NuScriptConfig {
25 pub fn deserialize_options<T>(&self) -> Result<T, Error>
31 where
32 T: for<'de> serde::Deserialize<'de>,
33 {
34 let json_value = value_to_json(&self.full_config_value);
35 serde_json::from_value(json_value)
36 .map_err(|e| format!("Failed to deserialize script options: {e}").into())
37 }
38}
39
40#[derive(Clone, Debug, Serialize, Deserialize, Default)]
42pub struct ReturnOptions {
43 pub suffix: Option<String>,
45 pub ttl: Option<TTL>,
47}
48
49pub struct CommonOptions {
52 pub run: Closure,
54 pub modules: HashMap<String, String>,
56 pub return_options: Option<ReturnOptions>,
58}
59
60pub fn parse_config(engine: &mut crate::nu::Engine, script: &str) -> Result<NuScriptConfig, Error> {
71 let (_initial_config_value, modules_to_load) = {
74 let mut temp_engine_state = engine.state.clone(); let mut temp_working_set = StateWorkingSet::new(&temp_engine_state);
76 let temp_block = parse(&mut temp_working_set, None, script.as_bytes(), false);
77
78 if let Some(err) = temp_working_set.parse_errors.first() {
80 let shell_error = ShellError::GenericError {
81 error: "Parse error in script (initial pass)".into(),
82 msg: format!("{err:?}"),
83 span: Some(err.span()),
84 help: None,
85 inner: vec![],
86 };
87 return Err(Error::from(nu_protocol::format_cli_error(
88 None,
89 &temp_working_set,
90 &shell_error,
91 None,
92 )));
93 }
94 temp_engine_state.merge_delta(temp_working_set.render())?;
95
96 let mut temp_stack = Stack::new();
97 let eval_result = eval_block_with_early_return::<WithoutDebug>(
98 &temp_engine_state,
99 &mut temp_stack,
100 &temp_block,
101 PipelineData::empty(),
102 )
103 .map_err(|err| {
104 let working_set = nu_protocol::engine::StateWorkingSet::new(&temp_engine_state);
105 Error::from(nu_protocol::format_cli_error(
106 None,
107 &working_set,
108 &err,
109 None,
110 ))
111 })?;
112 let val = eval_result.body.into_value(Span::unknown())?;
113
114 let modules = match val.get_data_by_key("modules") {
115 Some(mod_val) => {
116 let record = mod_val
117 .as_record()
118 .map_err(|_| -> Error { "modules field must be a record".into() })?;
119 record
120 .iter()
121 .map(|(name, content_val)| {
122 let content_str = content_val
123 .as_str()
124 .map_err(|_| -> Error {
125 format!(
126 "Module '{name}' content must be a string, got {typ:?}",
127 name = name,
128 typ = content_val.get_type()
129 )
130 .into()
131 })?
132 .to_string();
133 Ok((name.to_string(), content_str))
134 })
135 .collect::<Result<HashMap<String, String>, Error>>()?
136 }
137 None => HashMap::new(),
138 };
139 temp_engine_state.merge_env(&mut temp_stack)?; (val, modules)
141 };
142
143 if !modules_to_load.is_empty() {
145 for (name, content) in &modules_to_load {
146 tracing::debug!("Loading module '{}' into main engine", name);
147 engine
148 .add_module(name, content)
149 .map_err(|e| -> Error { format!("Failed to load module '{name}': {e}").into() })?;
150 }
151 }
152
153 let mut working_set = StateWorkingSet::new(&engine.state);
158 let block = parse(&mut working_set, None, script.as_bytes(), false);
159
160 if let Some(err) = working_set.parse_errors.first() {
161 let shell_error = ShellError::GenericError {
162 error: "Parse error in script (final pass)".into(),
163 msg: format!("{err:?}"),
164 span: Some(err.span()),
165 help: None,
166 inner: vec![],
167 };
168 return Err(Error::from(nu_protocol::format_cli_error(
169 None,
170 &working_set,
171 &shell_error,
172 None,
173 )));
174 }
175
176 if let Some(err) = working_set.compile_errors.first() {
178 let shell_error = ShellError::GenericError {
179 error: "Compile error in script".into(),
180 msg: format!("{err:?}"),
181 span: None,
182 help: None,
183 inner: vec![],
184 };
185 return Err(Error::from(nu_protocol::format_cli_error(
186 None,
187 &working_set,
188 &shell_error,
189 None,
190 )));
191 }
192
193 engine.state.merge_delta(working_set.render())?;
194
195 let mut stack = Stack::new();
196 let final_eval_result = eval_block_with_early_return::<WithoutDebug>(
197 &engine.state,
198 &mut stack,
199 &block,
200 PipelineData::empty(),
201 )
202 .map_err(|err| {
203 let working_set = nu_protocol::engine::StateWorkingSet::new(&engine.state); Error::from(nu_protocol::format_cli_error(
205 None,
206 &working_set,
207 &err,
208 None,
209 ))
210 })?;
211
212 let final_config_value = final_eval_result.body.into_value(Span::unknown())?;
213
214 let run_val = final_config_value
215 .get_data_by_key("run")
216 .ok_or_else(|| -> Error { "Script must define a 'run' closure.".into() })?;
217 let run_closure = run_val
218 .as_closure()
219 .map_err(|e| -> Error { format!("'run' field must be a closure: {e}").into() })?;
220
221 engine.state.merge_env(&mut stack)?; Ok(NuScriptConfig {
224 run_closure: run_closure.clone(),
225 full_config_value: final_config_value,
226 })
227}
228
229pub fn parse_config_legacy(
232 engine: &mut crate::nu::Engine,
233 script: &str,
234) -> Result<CommonOptions, Error> {
235 let script_config = parse_config(engine, script)?;
237
238 let modules = match script_config.full_config_value.get_data_by_key("modules") {
240 Some(val) => {
241 let record = val
242 .as_record()
243 .map_err(|_| -> Error { "modules must be a record".into() })?;
244 record
245 .iter()
246 .map(|(name, content)| {
247 let content = content
248 .as_str()
249 .map_err(|_| -> Error {
250 format!("module '{name}' content must be a string").into()
251 })?
252 .to_string();
253 Ok((name.to_string(), content))
254 })
255 .collect::<Result<HashMap<_, _>, Error>>()?
256 }
257 None => HashMap::new(),
258 };
259
260 let return_options = if let Some(return_config) = script_config
262 .full_config_value
263 .get_data_by_key("return_options")
264 {
265 let record = return_config
266 .as_record()
267 .map_err(|_| -> Error { "return_options must be a record".into() })?;
268
269 let suffix = record
270 .get("suffix")
271 .map(|v| {
272 v.as_str()
273 .map_err(|_| -> Error { "suffix must be a string".into() })
274 .map(|s| s.to_string())
275 })
276 .transpose()?;
277
278 let ttl = record
279 .get("ttl")
280 .map(|v| serde_json::from_str(&value_to_json(v).to_string()))
281 .transpose()
282 .map_err(|e| -> Error { format!("invalid TTL: {e}").into() })?;
283
284 Some(ReturnOptions { suffix, ttl })
285 } else {
286 None
287 };
288
289 Ok(CommonOptions {
290 run: script_config.run_closure,
291 modules,
292 return_options,
293 })
294}