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 &temp_working_set,
89 &shell_error,
90 None,
91 )));
92 }
93 temp_engine_state.merge_delta(temp_working_set.render())?;
94
95 let mut temp_stack = Stack::new();
96 let eval_result = eval_block_with_early_return::<WithoutDebug>(
97 &temp_engine_state,
98 &mut temp_stack,
99 &temp_block,
100 PipelineData::empty(),
101 )
102 .map_err(|err| {
103 let working_set = nu_protocol::engine::StateWorkingSet::new(&temp_engine_state);
104 Error::from(nu_protocol::format_cli_error(&working_set, &err, None))
105 })?;
106 let val = eval_result.body.into_value(Span::unknown())?;
107
108 let modules = match val.get_data_by_key("modules") {
109 Some(mod_val) => {
110 let record = mod_val
111 .as_record()
112 .map_err(|_| -> Error { "modules field must be a record".into() })?;
113 record
114 .iter()
115 .map(|(name, content_val)| {
116 let content_str = content_val
117 .as_str()
118 .map_err(|_| -> Error {
119 format!(
120 "Module '{name}' content must be a string, got {typ:?}",
121 name = name,
122 typ = content_val.get_type()
123 )
124 .into()
125 })?
126 .to_string();
127 Ok((name.to_string(), content_str))
128 })
129 .collect::<Result<HashMap<String, String>, Error>>()?
130 }
131 None => HashMap::new(),
132 };
133 temp_engine_state.merge_env(&mut temp_stack)?; (val, modules)
135 };
136
137 if !modules_to_load.is_empty() {
139 for (name, content) in &modules_to_load {
140 tracing::debug!("Loading module '{}' into main engine", name);
141 engine
142 .add_module(name, content)
143 .map_err(|e| -> Error { format!("Failed to load module '{name}': {e}").into() })?;
144 }
145 }
146
147 let mut working_set = StateWorkingSet::new(&engine.state);
152 let block = parse(&mut working_set, None, script.as_bytes(), false);
153
154 if let Some(err) = working_set.parse_errors.first() {
155 let shell_error = ShellError::GenericError {
156 error: "Parse error in script (final pass)".into(),
157 msg: format!("{err:?}"),
158 span: Some(err.span()),
159 help: None,
160 inner: vec![],
161 };
162 return Err(Error::from(nu_protocol::format_cli_error(
163 &working_set,
164 &shell_error,
165 None,
166 )));
167 }
168
169 if let Some(err) = working_set.compile_errors.first() {
171 let shell_error = ShellError::GenericError {
172 error: "Compile error in script".into(),
173 msg: format!("{err:?}"),
174 span: None,
175 help: None,
176 inner: vec![],
177 };
178 return Err(Error::from(nu_protocol::format_cli_error(
179 &working_set,
180 &shell_error,
181 None,
182 )));
183 }
184
185 engine.state.merge_delta(working_set.render())?;
186
187 let mut stack = Stack::new();
188 let final_eval_result = eval_block_with_early_return::<WithoutDebug>(
189 &engine.state,
190 &mut stack,
191 &block,
192 PipelineData::empty(),
193 )
194 .map_err(|err| {
195 let working_set = nu_protocol::engine::StateWorkingSet::new(&engine.state); Error::from(nu_protocol::format_cli_error(&working_set, &err, None))
197 })?;
198
199 let final_config_value = final_eval_result.body.into_value(Span::unknown())?;
200
201 let run_val = final_config_value
202 .get_data_by_key("run")
203 .ok_or_else(|| -> Error { "Script must define a 'run' closure.".into() })?;
204 let run_closure = run_val
205 .as_closure()
206 .map_err(|e| -> Error { format!("'run' field must be a closure: {e}").into() })?;
207
208 engine.state.merge_env(&mut stack)?; Ok(NuScriptConfig {
211 run_closure: run_closure.clone(),
212 full_config_value: final_config_value,
213 })
214}
215
216pub fn parse_config_legacy(
219 engine: &mut crate::nu::Engine,
220 script: &str,
221) -> Result<CommonOptions, Error> {
222 let script_config = parse_config(engine, script)?;
224
225 let modules = match script_config.full_config_value.get_data_by_key("modules") {
227 Some(val) => {
228 let record = val
229 .as_record()
230 .map_err(|_| -> Error { "modules must be a record".into() })?;
231 record
232 .iter()
233 .map(|(name, content)| {
234 let content = content
235 .as_str()
236 .map_err(|_| -> Error {
237 format!("module '{name}' content must be a string").into()
238 })?
239 .to_string();
240 Ok((name.to_string(), content))
241 })
242 .collect::<Result<HashMap<_, _>, Error>>()?
243 }
244 None => HashMap::new(),
245 };
246
247 let return_options = if let Some(return_config) = script_config
249 .full_config_value
250 .get_data_by_key("return_options")
251 {
252 let record = return_config
253 .as_record()
254 .map_err(|_| -> Error { "return_options must be a record".into() })?;
255
256 let suffix = record
257 .get("suffix")
258 .map(|v| {
259 v.as_str()
260 .map_err(|_| -> Error { "suffix must be a string".into() })
261 .map(|s| s.to_string())
262 })
263 .transpose()?;
264
265 let ttl = record
266 .get("ttl")
267 .map(|v| serde_json::from_str(&value_to_json(v).to_string()))
268 .transpose()
269 .map_err(|e| -> Error { format!("invalid TTL: {e}").into() })?;
270
271 Some(ReturnOptions { suffix, ttl })
272 } else {
273 None
274 };
275
276 Ok(CommonOptions {
277 run: script_config.run_closure,
278 modules,
279 return_options,
280 })
281}