1use shape_ast::error::{Result, ShapeError};
13use shape_value::ValueWord;
14use shape_value::content::{BorderStyle, ChartChannel, Color, ContentNode, NamedColor};
15
16pub fn call_content_method(
20 method_name: &str,
21 receiver: ValueWord,
22 args: Vec<ValueWord>,
23) -> Option<Result<ValueWord>> {
24 match method_name {
25 "fg" => Some(handle_fg(receiver, args)),
27 "bg" => Some(handle_bg(receiver, args)),
28 "bold" => Some(handle_bold(receiver, args)),
29 "italic" => Some(handle_italic(receiver, args)),
30 "underline" => Some(handle_underline(receiver, args)),
31 "dim" => Some(handle_dim(receiver, args)),
32 "border" => Some(handle_border(receiver, args)),
34 "max_rows" | "maxRows" => Some(handle_max_rows(receiver, args)),
35 "series" => Some(handle_series(receiver, args)),
37 "title" => Some(handle_title(receiver, args)),
38 "x_label" | "xLabel" => Some(handle_x_label(receiver, args)),
39 "y_label" | "yLabel" => Some(handle_y_label(receiver, args)),
40 _ => None,
41 }
42}
43
44fn parse_color(s: &str) -> Result<Color> {
46 match s.to_lowercase().as_str() {
47 "red" => Ok(Color::Named(NamedColor::Red)),
48 "green" => Ok(Color::Named(NamedColor::Green)),
49 "blue" => Ok(Color::Named(NamedColor::Blue)),
50 "yellow" => Ok(Color::Named(NamedColor::Yellow)),
51 "magenta" => Ok(Color::Named(NamedColor::Magenta)),
52 "cyan" => Ok(Color::Named(NamedColor::Cyan)),
53 "white" => Ok(Color::Named(NamedColor::White)),
54 "default" => Ok(Color::Named(NamedColor::Default)),
55 other => Err(ShapeError::RuntimeError {
56 message: format!(
57 "Unknown color '{}'. Expected: red, green, blue, yellow, magenta, cyan, white, default",
58 other
59 ),
60 location: None,
61 }),
62 }
63}
64
65fn extract_content(receiver: &ValueWord) -> Result<ContentNode> {
67 receiver
68 .as_content()
69 .cloned()
70 .ok_or_else(|| ShapeError::RuntimeError {
71 message: "Expected a ContentNode receiver".to_string(),
72 location: None,
73 })
74}
75
76fn require_string_arg(args: &[ValueWord], index: usize, label: &str) -> Result<String> {
78 args.get(index)
79 .and_then(|nb| nb.as_str().map(|s| s.to_string()))
80 .ok_or_else(|| ShapeError::RuntimeError {
81 message: format!("{} requires a string argument", label),
82 location: None,
83 })
84}
85
86fn handle_fg(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
87 let node = extract_content(&receiver)?;
88 let color_name = require_string_arg(&args, 0, "fg")?;
89 let color = parse_color(&color_name)?;
90 Ok(ValueWord::from_content(node.with_fg(color)))
91}
92
93fn handle_bg(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
94 let node = extract_content(&receiver)?;
95 let color_name = require_string_arg(&args, 0, "bg")?;
96 let color = parse_color(&color_name)?;
97 Ok(ValueWord::from_content(node.with_bg(color)))
98}
99
100fn handle_bold(receiver: ValueWord, _args: Vec<ValueWord>) -> Result<ValueWord> {
101 let node = extract_content(&receiver)?;
102 Ok(ValueWord::from_content(node.with_bold()))
103}
104
105fn handle_italic(receiver: ValueWord, _args: Vec<ValueWord>) -> Result<ValueWord> {
106 let node = extract_content(&receiver)?;
107 Ok(ValueWord::from_content(node.with_italic()))
108}
109
110fn handle_underline(receiver: ValueWord, _args: Vec<ValueWord>) -> Result<ValueWord> {
111 let node = extract_content(&receiver)?;
112 Ok(ValueWord::from_content(node.with_underline()))
113}
114
115fn handle_dim(receiver: ValueWord, _args: Vec<ValueWord>) -> Result<ValueWord> {
116 let node = extract_content(&receiver)?;
117 Ok(ValueWord::from_content(node.with_dim()))
118}
119
120fn handle_border(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
121 let node = extract_content(&receiver)?;
122 let style_name = require_string_arg(&args, 0, "border")?;
123 let border = match style_name.to_lowercase().as_str() {
124 "rounded" => BorderStyle::Rounded,
125 "sharp" => BorderStyle::Sharp,
126 "heavy" => BorderStyle::Heavy,
127 "double" => BorderStyle::Double,
128 "minimal" => BorderStyle::Minimal,
129 "none" => BorderStyle::None,
130 other => {
131 return Err(ShapeError::RuntimeError {
132 message: format!(
133 "Unknown border style '{}'. Expected: rounded, sharp, heavy, double, minimal, none",
134 other
135 ),
136 location: None,
137 });
138 }
139 };
140 match node {
141 ContentNode::Table(mut table) => {
142 table.border = border;
143 Ok(ValueWord::from_content(ContentNode::Table(table)))
144 }
145 other => Ok(ValueWord::from_content(other)),
146 }
147}
148
149fn handle_max_rows(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
150 let node = extract_content(&receiver)?;
151 let n = args
152 .first()
153 .and_then(|nb| nb.as_number_coerce())
154 .ok_or_else(|| ShapeError::RuntimeError {
155 message: "max_rows requires a numeric argument".to_string(),
156 location: None,
157 })? as usize;
158 match node {
159 ContentNode::Table(mut table) => {
160 table.max_rows = Some(n);
161 Ok(ValueWord::from_content(ContentNode::Table(table)))
162 }
163 other => Ok(ValueWord::from_content(other)),
164 }
165}
166
167fn handle_series(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
168 let node = extract_content(&receiver)?;
169 let label = require_string_arg(&args, 0, "series")?;
170 let mut x_values = Vec::new();
172 let mut y_values = Vec::new();
173 if let Some(view) = args.get(1).and_then(|nb| nb.as_any_array()) {
174 let arr = view.to_generic();
175 for item in arr.iter() {
176 if let Some(inner) = item.as_any_array() {
177 let inner = inner.to_generic();
178 if inner.len() >= 2 {
179 if let (Some(x), Some(y)) =
180 (inner[0].as_number_coerce(), inner[1].as_number_coerce())
181 {
182 x_values.push(x);
183 y_values.push(y);
184 }
185 }
186 }
187 }
188 }
189 match node {
190 ContentNode::Chart(mut spec) => {
191 if spec.channel("x").is_none() && !x_values.is_empty() {
193 spec.channels.push(ChartChannel {
194 name: "x".to_string(),
195 label: "x".to_string(),
196 values: x_values,
197 color: None,
198 });
199 }
200 spec.channels.push(ChartChannel {
201 name: "y".to_string(),
202 label,
203 values: y_values,
204 color: None,
205 });
206 Ok(ValueWord::from_content(ContentNode::Chart(spec)))
207 }
208 other => Ok(ValueWord::from_content(other)),
209 }
210}
211
212fn handle_title(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
213 let node = extract_content(&receiver)?;
214 let title = require_string_arg(&args, 0, "title")?;
215 match node {
216 ContentNode::Chart(mut spec) => {
217 spec.title = Some(title);
218 Ok(ValueWord::from_content(ContentNode::Chart(spec)))
219 }
220 other => Ok(ValueWord::from_content(other)),
221 }
222}
223
224fn handle_x_label(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
225 let node = extract_content(&receiver)?;
226 let label = require_string_arg(&args, 0, "x_label")?;
227 match node {
228 ContentNode::Chart(mut spec) => {
229 spec.x_label = Some(label);
230 Ok(ValueWord::from_content(ContentNode::Chart(spec)))
231 }
232 other => Ok(ValueWord::from_content(other)),
233 }
234}
235
236fn handle_y_label(receiver: ValueWord, args: Vec<ValueWord>) -> Result<ValueWord> {
237 let node = extract_content(&receiver)?;
238 let label = require_string_arg(&args, 0, "y_label")?;
239 match node {
240 ContentNode::Chart(mut spec) => {
241 spec.y_label = Some(label);
242 Ok(ValueWord::from_content(ContentNode::Chart(spec)))
243 }
244 other => Ok(ValueWord::from_content(other)),
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use shape_value::content::ContentTable;
252 use std::sync::Arc;
253
254 fn nb_str(s: &str) -> ValueWord {
255 ValueWord::from_string(Arc::new(s.to_string()))
256 }
257
258 #[test]
259 fn test_call_content_method_lookup() {
260 let node = ValueWord::from_content(ContentNode::plain("hello"));
261 assert!(call_content_method("bold", node.clone(), vec![]).is_some());
262 assert!(call_content_method("italic", node.clone(), vec![]).is_some());
263 assert!(call_content_method("underline", node.clone(), vec![]).is_some());
264 assert!(call_content_method("dim", node.clone(), vec![]).is_some());
265 assert!(call_content_method("unknown", node, vec![]).is_none());
266 }
267
268 #[test]
269 fn test_fg_method() {
270 let node = ValueWord::from_content(ContentNode::plain("text"));
271 let result = handle_fg(node, vec![nb_str("red")]).unwrap();
272 let content = result.as_content().unwrap();
273 match content {
274 ContentNode::Text(st) => {
275 assert_eq!(st.spans[0].style.fg, Some(Color::Named(NamedColor::Red)));
276 }
277 _ => panic!("expected Text"),
278 }
279 }
280
281 #[test]
282 fn test_bg_method() {
283 let node = ValueWord::from_content(ContentNode::plain("text"));
284 let result = handle_bg(node, vec![nb_str("blue")]).unwrap();
285 let content = result.as_content().unwrap();
286 match content {
287 ContentNode::Text(st) => {
288 assert_eq!(st.spans[0].style.bg, Some(Color::Named(NamedColor::Blue)));
289 }
290 _ => panic!("expected Text"),
291 }
292 }
293
294 #[test]
295 fn test_bold_method() {
296 let node = ValueWord::from_content(ContentNode::plain("text"));
297 let result = handle_bold(node, vec![]).unwrap();
298 let content = result.as_content().unwrap();
299 match content {
300 ContentNode::Text(st) => assert!(st.spans[0].style.bold),
301 _ => panic!("expected Text"),
302 }
303 }
304
305 #[test]
306 fn test_italic_method() {
307 let node = ValueWord::from_content(ContentNode::plain("text"));
308 let result = handle_italic(node, vec![]).unwrap();
309 let content = result.as_content().unwrap();
310 match content {
311 ContentNode::Text(st) => assert!(st.spans[0].style.italic),
312 _ => panic!("expected Text"),
313 }
314 }
315
316 #[test]
317 fn test_underline_method() {
318 let node = ValueWord::from_content(ContentNode::plain("text"));
319 let result = handle_underline(node, vec![]).unwrap();
320 let content = result.as_content().unwrap();
321 match content {
322 ContentNode::Text(st) => assert!(st.spans[0].style.underline),
323 _ => panic!("expected Text"),
324 }
325 }
326
327 #[test]
328 fn test_dim_method() {
329 let node = ValueWord::from_content(ContentNode::plain("text"));
330 let result = handle_dim(node, vec![]).unwrap();
331 let content = result.as_content().unwrap();
332 match content {
333 ContentNode::Text(st) => assert!(st.spans[0].style.dim),
334 _ => panic!("expected Text"),
335 }
336 }
337
338 #[test]
339 fn test_border_method_on_table() {
340 let table = ContentNode::Table(ContentTable {
341 headers: vec!["A".into()],
342 rows: vec![vec![ContentNode::plain("1")]],
343 border: BorderStyle::Rounded,
344 max_rows: None,
345 column_types: None,
346 total_rows: None,
347 sortable: false,
348 });
349 let node = ValueWord::from_content(table);
350 let result = handle_border(node, vec![nb_str("heavy")]).unwrap();
351 let content = result.as_content().unwrap();
352 match content {
353 ContentNode::Table(t) => assert_eq!(t.border, BorderStyle::Heavy),
354 _ => panic!("expected Table"),
355 }
356 }
357
358 #[test]
359 fn test_border_method_on_non_table() {
360 let node = ValueWord::from_content(ContentNode::plain("text"));
361 let result = handle_border(node, vec![nb_str("sharp")]).unwrap();
362 let content = result.as_content().unwrap();
363 match content {
364 ContentNode::Text(st) => assert_eq!(st.spans[0].text, "text"),
365 _ => panic!("expected Text passthrough"),
366 }
367 }
368
369 #[test]
370 fn test_max_rows_method() {
371 let table = ContentNode::Table(ContentTable {
372 headers: vec!["X".into()],
373 rows: vec![
374 vec![ContentNode::plain("1")],
375 vec![ContentNode::plain("2")],
376 vec![ContentNode::plain("3")],
377 ],
378 border: BorderStyle::default(),
379 max_rows: None,
380 column_types: None,
381 total_rows: None,
382 sortable: false,
383 });
384 let node = ValueWord::from_content(table);
385 let result = handle_max_rows(node, vec![ValueWord::from_i64(2)]).unwrap();
386 let content = result.as_content().unwrap();
387 match content {
388 ContentNode::Table(t) => assert_eq!(t.max_rows, Some(2)),
389 _ => panic!("expected Table"),
390 }
391 }
392
393 #[test]
394 fn test_parse_color_valid() {
395 assert_eq!(parse_color("red").unwrap(), Color::Named(NamedColor::Red));
396 assert_eq!(
397 parse_color("GREEN").unwrap(),
398 Color::Named(NamedColor::Green)
399 );
400 assert_eq!(parse_color("Blue").unwrap(), Color::Named(NamedColor::Blue));
401 }
402
403 #[test]
404 fn test_parse_color_invalid() {
405 assert!(parse_color("purple").is_err());
406 }
407
408 #[test]
409 fn test_fg_invalid_color() {
410 let node = ValueWord::from_content(ContentNode::plain("text"));
411 let result = handle_fg(node, vec![nb_str("purple")]);
412 assert!(result.is_err());
413 }
414
415 #[test]
416 fn test_style_chaining_via_methods() {
417 let node = ValueWord::from_content(ContentNode::plain("text"));
418 let bold_result = handle_bold(node, vec![]).unwrap();
419 let fg_result = handle_fg(bold_result, vec![nb_str("cyan")]).unwrap();
420 let content = fg_result.as_content().unwrap();
421 match content {
422 ContentNode::Text(st) => {
423 assert!(st.spans[0].style.bold);
424 assert_eq!(st.spans[0].style.fg, Some(Color::Named(NamedColor::Cyan)));
425 }
426 _ => panic!("expected Text"),
427 }
428 }
429
430 #[test]
431 fn test_chart_title_method() {
432 use shape_value::content::{ChartSpec, ChartType};
433 let chart = ContentNode::Chart(ChartSpec {
434 chart_type: ChartType::Line,
435 channels: vec![],
436 x_categories: None,
437 title: None,
438 x_label: None,
439 y_label: None,
440 width: None,
441 height: None,
442 echarts_options: None,
443 interactive: true,
444 });
445 let node = ValueWord::from_content(chart);
446 let result = handle_title(node, vec![nb_str("Revenue")]).unwrap();
447 let content = result.as_content().unwrap();
448 match content {
449 ContentNode::Chart(spec) => assert_eq!(spec.title.as_deref(), Some("Revenue")),
450 _ => panic!("expected Chart"),
451 }
452 }
453
454 #[test]
455 fn test_chart_x_label_method() {
456 use shape_value::content::{ChartSpec, ChartType};
457 let chart = ContentNode::Chart(ChartSpec {
458 chart_type: ChartType::Bar,
459 channels: vec![],
460 x_categories: None,
461 title: None,
462 x_label: None,
463 y_label: None,
464 width: None,
465 height: None,
466 echarts_options: None,
467 interactive: true,
468 });
469 let node = ValueWord::from_content(chart);
470 let result = handle_x_label(node, vec![nb_str("Time")]).unwrap();
471 let content = result.as_content().unwrap();
472 match content {
473 ContentNode::Chart(spec) => assert_eq!(spec.x_label.as_deref(), Some("Time")),
474 _ => panic!("expected Chart"),
475 }
476 }
477
478 #[test]
479 fn test_chart_y_label_method() {
480 use shape_value::content::{ChartSpec, ChartType};
481 let chart = ContentNode::Chart(ChartSpec {
482 chart_type: ChartType::Line,
483 channels: vec![],
484 x_categories: None,
485 title: None,
486 x_label: None,
487 y_label: None,
488 width: None,
489 height: None,
490 echarts_options: None,
491 interactive: true,
492 });
493 let node = ValueWord::from_content(chart);
494 let result = handle_y_label(node, vec![nb_str("Value")]).unwrap();
495 let content = result.as_content().unwrap();
496 match content {
497 ContentNode::Chart(spec) => assert_eq!(spec.y_label.as_deref(), Some("Value")),
498 _ => panic!("expected Chart"),
499 }
500 }
501
502 #[test]
503 fn test_chart_series_method() {
504 use shape_value::content::{ChartSpec, ChartType};
505 let chart = ContentNode::Chart(ChartSpec {
506 chart_type: ChartType::Line,
507 channels: vec![],
508 x_categories: None,
509 title: None,
510 x_label: None,
511 y_label: None,
512 width: None,
513 height: None,
514 echarts_options: None,
515 interactive: true,
516 });
517 let node = ValueWord::from_content(chart);
518 let data_points = ValueWord::from_array(Arc::new(vec![
519 ValueWord::from_array(Arc::new(vec![
520 ValueWord::from_f64(1.0),
521 ValueWord::from_f64(10.0),
522 ])),
523 ValueWord::from_array(Arc::new(vec![
524 ValueWord::from_f64(2.0),
525 ValueWord::from_f64(20.0),
526 ])),
527 ]));
528 let result = handle_series(node, vec![nb_str("Sales"), data_points]).unwrap();
529 let content = result.as_content().unwrap();
530 match content {
531 ContentNode::Chart(spec) => {
532 assert_eq!(spec.channels.len(), 2);
534 assert_eq!(spec.channel("x").unwrap().values, vec![1.0, 2.0]);
535 let y = spec.channels_by_name("y");
536 assert_eq!(y.len(), 1);
537 assert_eq!(y[0].label, "Sales");
538 assert_eq!(y[0].values, vec![10.0, 20.0]);
539 }
540 _ => panic!("expected Chart"),
541 }
542 }
543
544 #[test]
545 fn test_chart_method_lookup() {
546 let node = ValueWord::from_content(ContentNode::plain("text"));
547 assert!(call_content_method("title", node.clone(), vec![nb_str("t")]).is_some());
548 assert!(call_content_method("series", node.clone(), vec![]).is_some());
549 assert!(call_content_method("xLabel", node.clone(), vec![nb_str("x")]).is_some());
550 assert!(call_content_method("yLabel", node, vec![nb_str("y")]).is_some());
551 }
552}