1use crate::scripting::NinjaEngine;
2use crate::{common::types::FieldValue, manager::ShurikenManager};
3use anyhow::{Error, Result, bail};
4use either::Either;
5use log::debug;
6use shlex::split;
7use std::env;
8use std::sync::Arc;
9use std::{io, path::PathBuf, process::Stdio};
10use tokio::process::Command as SubprocessCommand;
11use tokio::sync::RwLock;
12
13#[derive(Debug, Clone)]
15pub enum Command {
16 HttpStart(u16),
17 Start,
18 Stop,
19 Select(String),
20 Get(String),
21 Exit,
22 Configure,
23 ConfigureBlock(Vec<(String, FieldValue)>),
24 Set { key: String, value: FieldValue },
25 List,
26 ListState,
27 Install(PathBuf),
28 Toggle(String),
29 Execute(PathBuf),
30 Help,
31 None,
32}
33
34fn strip_comments(line: &str) -> &str {
40 if let Some(i) = line.find("//") {
41 &line[..i]
42 } else if let Some(i) = line.find('#') {
43 &line[..i]
44 } else {
45 line
46 }
47}
48
49fn parse_value(raw: &str) -> FieldValue {
51 let v = raw.trim();
52
53 if (v.starts_with('"') && v.ends_with('"')) || (v.starts_with('\'') && v.ends_with('\'')) {
55 let inner = &v[1..v.len() - 1];
56 return FieldValue::String(inner.to_string());
57 }
58
59 match v.to_ascii_lowercase().as_str() {
61 "true" => return FieldValue::Bool(true),
62 "false" => return FieldValue::Bool(false),
63 _ => {}
64 }
65
66 if let Ok(i) = v.parse::<i64>() {
68 return FieldValue::Number(i);
69 }
70
71 FieldValue::String(v.to_string())
73}
74
75fn parse_kv(text: &str) -> Result<Option<(String, FieldValue)>> {
77 let t = text.trim();
78 if t.is_empty() {
79 return Ok(None);
80 }
81
82 if let Some((left, right)) = t.split_once('=') {
83 let key = left.trim();
84 let val = right.trim();
85
86 if key.is_empty() {
87 bail!("Invalid assignment (empty key): `{}`", text);
88 }
89
90 let val = val.trim_end_matches(';').trim();
92 Ok(Some((key.to_string(), parse_value(val))))
93 } else {
94 Ok(None)
96 }
97}
98
99fn collect_block<'a, I>(first_after_brace: &'a str, lines: &mut I) -> Result<String>
101where
102 I: Iterator<Item = &'a str>,
103{
104 let trimmed_after = first_after_brace.trim();
106
107 if let Some(inner) = trimmed_after.strip_suffix("}") {
108 return Ok(inner.trim().to_string());
110 }
111
112 let mut collected = String::new();
114 if !trimmed_after.is_empty() {
115 collected.push_str(trimmed_after);
116 collected.push('\n');
117 }
118
119 for next in lines {
121 let stripped = strip_comments(next).trim();
122 if let Some(inner) = stripped.strip_suffix('}') {
123 if !inner.trim().is_empty() {
124 collected.push_str(inner.trim());
125 }
126 return Ok(collected.trim().to_string());
127 } else if !stripped.is_empty() {
128 collected.push_str(stripped);
129 collected.push('\n');
130 }
131 }
132
133 bail!("Missing closing '}}' for block");
135}
136
137fn command_parser(script: &str) -> Result<Vec<Command>> {
139 let mut commands = Vec::new();
140
141 let mut lines = script.lines();
143
144 while let Some(raw_line) = lines.next() {
146 let no_comment = strip_comments(raw_line);
148 let trimmed = no_comment.trim();
149 if trimmed.is_empty() {
150 continue;
151 }
152
153 if trimmed.starts_with("configure") {
156 if let Some((_, after_brace)) = trimmed.split_once('{') {
158 let block_content = collect_block(after_brace, &mut lines)?;
160 let mut kvs: Vec<(String, FieldValue)> = Vec::new();
162
163 for chunk in block_content.split([';', '\n']) {
164 if let Some((k, v)) = parse_kv(chunk)? {
165 kvs.push((k, v));
166 }
167 }
168
169 commands.push(Command::ConfigureBlock(kvs));
170 continue;
171 } else {
172 commands.push(Command::Configure);
174 continue;
175 }
176 }
177
178 if let Some(tokens) = split(trimmed) {
181 let tokens: Vec<String> = tokens
182 .into_iter()
183 .filter(|token| {
184 let token = token.trim();
185 !token.is_empty() && token != "="
186 })
187 .collect();
188
189 if tokens.is_empty() {
190 continue;
191 }
192
193 let cmd = match tokens[0].as_str() {
194 "http" => Command::HttpStart(tokens[1].parse().unwrap_or(80)),
195 "start" => Command::Start,
196 "stop" => Command::Stop,
197 "select" => {
198 if tokens.len() > 1 {
199 Command::Select(tokens[1].clone())
200 } else {
201 Command::None
202 }
203 }
204 "help" => Command::Help,
205 "get" => {
206 if tokens.len() > 1 {
207 Command::Get(tokens[1].clone())
208 } else {
209 Command::None
210 }
211 }
212 "set" => {
213 if tokens.len() > 2 {
214 Command::Set {
215 key: tokens[1].clone(),
216 value: parse_value(tokens[2].as_str()),
217 }
218 } else {
219 Command::None
220 }
221 }
222 "list" => {
223 if tokens.len() > 1 && tokens[1].eq_ignore_ascii_case("state") {
224 Command::ListState
225 } else {
226 Command::List
227 }
228 }
229 "install" => {
230 if tokens.len() > 1 {
231 Command::Install(PathBuf::from(tokens[1].clone()))
232 } else {
233 Command::None
234 }
235 }
236 "toggle" => {
237 if tokens.len() > 1 {
238 Command::Toggle(tokens[1].clone())
239 } else {
240 Command::None
241 }
242 }
243 "execute" => {
244 if tokens.len() > 1 {
245 Command::Execute(PathBuf::from(tokens[1].clone()))
246 } else {
247 Command::None
248 }
249 }
250 "exit" => Command::Exit,
251 "configure" => Command::Configure, _ => Command::None,
253 };
254
255 commands.push(cmd);
256 }
257 }
258
259 Ok(commands)
260}
261
262fn locate_ninja_cli() -> Result<PathBuf> {
267 let exe_path = env::current_exe()?;
268 if let Some(root) = exe_path.parent() {
269 let cli_path = if cfg!(windows) {
270 root.join("shurikenctl.exe")
271 } else {
272 root.join("shurikenctl")
273 };
274
275 if !cli_path.exists() {
276 return Err(Error::msg("No ninja CLI found"));
277 }
278
279 Ok(cli_path)
280 } else {
281 Err(Error::msg(
282 "No parent directory? where and how did you run this? please email me i'm genuinely curious. -- Hannan \"tunafysh\" Smani",
283 ))
284 }
285}
286
287pub struct DslContext {
288 pub manager: ShurikenManager,
289 pub selected: Arc<RwLock<Option<String>>>,
290}
291
292impl DslContext {
293 pub fn new(manager: ShurikenManager) -> Self {
294 Self {
295 manager,
296 selected: Arc::new(RwLock::new(None)),
297 }
298 }
299}
300
301pub async fn execute_commands(ctx: &DslContext, script: String) -> Result<Vec<String>> {
302 let parsed_commands = command_parser(script.as_str())?;
303
304 let mut output: Vec<String> = Vec::new();
305
306 for command in parsed_commands {
307 match command {
308 Command::HttpStart(port) => {
310 let path = locate_ninja_cli()?;
311 SubprocessCommand::new(path)
312 .arg("api")
313 .arg(port.to_string())
314 .stdout(Stdio::inherit())
315 .stderr(Stdio::inherit())
316 .stdin(Stdio::inherit())
317 .status()
318 .await?;
319 output.push(format!("HTTP server started on port {}", port));
320 }
321
322 Command::Select(name) => {
324 debug!("Shurikens: {:#?}", ctx.manager.shurikens.read().await);
325 if ctx.manager.shurikens.read().await.contains_key(&name) {
326 *ctx.selected.write().await = Some(name.clone());
327 output.push(format!("Selected shuriken '{}'", name));
328 } else {
329 output.push(format!("No such shuriken: {}", name));
330 }
331 }
332
333 Command::Configure => {
335 if let Some(name) = &*ctx.selected.read().await {
336 let mut shurikens = ctx.manager.shurikens.write().await;
337 if let Some(shuriken) = shurikens.get_mut(name) {
338 let path = &ctx.manager.root_path;
339 shuriken
340 .configure(
341 path,
342 &*ctx.manager.engine.lock().await,
343 Some(ctx.manager.clone()),
344 )
345 .await
346 .map_err(Error::msg)?;
347
348 output.push(format!(
349 "Generated configuration for shuriken {} successfully.",
350 &name
351 ));
352 }
353 }
354 }
355
356 Command::ConfigureBlock(kvs) => {
358 if let Some(shuriken_name) = &*ctx.selected.read().await {
359 let mut shurikens = ctx.manager.shurikens.write().await;
360 if let Some(shuriken) = shurikens.get_mut(shuriken_name)
361 && let Some(cfg) = &mut shuriken.config
362 {
363 let partial_options = cfg.options.get_or_insert_with(Default::default);
364 for (k, v) in kvs {
365 partial_options.insert(k.clone(), v.clone());
366 output.push(format!(
367 "Set {} = {} for {}",
368 k,
369 v.render(),
370 shuriken_name
371 ));
372 }
373 } else {
374 output.push("No selected shuriken or missing config while applying configure block.".to_string());
375 }
376 } else {
377 output.push("No shuriken selected — configure block ignored.".into());
378 }
379 }
380
381 Command::Help => {
382 output.push(
383 "Available commands:
384 http start <port> - Start the HTTP server
385 select <name> - Select a shuriken
386 configure - Generate configuration for the selected shuriken
387 configure { k = v } - Apply config assignments to the selected shuriken
388 set <key> <value> - Set a config key for the selected shuriken
389 get <key> - Get a config key's value
390 toggle <key> - Toggle a boolean config key
391 start - Start the selected shuriken
392 stop - Stop the selected shuriken
393 install <path> - Install a new shuriken from a file
394 list - List all shurikens
395 list state - List shurikens with their states
396 execute <script> - Run a Ninja script file
397 exit - Deselect current shuriken
398 help - Show this message"
399 .to_string(),
400 );
401 }
402
403 Command::Set { key, value } => {
405 if let Some(shuriken_name) = &*ctx.selected.read().await {
406 let mut shurikens = ctx.manager.shurikens.write().await;
407 if let Some(shuriken) = shurikens.get_mut(shuriken_name)
408 && let Some(cfg) = &mut shuriken.config
409 {
410 let cloned_value: FieldValue = value.clone();
411 if let Some(partial_options) = &mut cfg.options {
412 partial_options.insert(key.clone(), FieldValue::from(value.render()));
413 }
414
415 output.push(format!(
416 "Set {} = {} for {}",
417 key,
418 cloned_value.render(),
419 shuriken_name
420 ));
421 }
422 }
423 }
424
425 Command::Get(key) => {
426 if let Some(shuriken_name) = &*ctx.selected.read().await {
427 let shurikens = ctx.manager.shurikens.read().await;
428 if let Some(shuriken) = shurikens.get(shuriken_name)
429 && let Some(cfg) = &shuriken.config
430 && let Some(options) = &cfg.options
431 {
432 output.push(format!("{:?} = {:?}", key, options.get(&key)));
433 }
434 }
435 }
436
437 Command::Toggle(key) => {
438 if let Some(shuriken_name) = &*ctx.selected.read().await {
439 let mut shurikens = ctx.manager.shurikens.write().await;
440 if let Some(shuriken) = shurikens.get_mut(shuriken_name)
441 && let Some(cfg) = &mut shuriken.config
442 && let Some(options) = &mut cfg.options
443 && let Some(FieldValue::Bool(value)) = options.get_mut(&key)
444 {
445 *value = !*value;
446 output.push(format!("Toggled {} to {}", key, value));
447 }
448 }
449 }
450
451 Command::List => {
453 if let Either::Right(names) = ctx.manager.list(false).await? {
454 output.push(format!("Shurikens: {:?}", names))
455 }
456 }
457 Command::ListState => {
458 if let Either::Left(states) = ctx.manager.list(true).await? {
459 for (name, state) in states {
460 output.push(format!("{} -> {:?}", name, state));
461 }
462 }
463 }
464
465 Command::Start => {
466 if let Some(name) = &*ctx.selected.read().await {
467 match ctx.manager.start(name).await {
468 Ok(_) => output.push(format!("Started {}", name)),
469 Err(e) => output.push(format!("Error: {}", e)),
470 }
471 }
472 }
473 Command::Stop => {
474 if let Some(name) = &*ctx.selected.read().await {
475 match ctx.manager.stop(name).await {
476 Ok(_) => output.push(format!("Stopped {}", name)),
477 Err(e) => output.push(format!("Error: {}", e)),
478 }
479 }
480 }
481
482 Command::Execute(script_path) => {
483 let engine = NinjaEngine::new()
484 .await
485 .map_err(|e| io::Error::other(e.to_string()))?;
486 engine
487 .execute_file(&script_path, None, Some(ctx.manager.clone()))
488 .await
489 .map_err(|e| io::Error::other(e.to_string()))?;
490 }
491 Command::Install(file_path) => match ctx.manager.install(&file_path).await {
492 Ok(_) => output.push("Installed successfully".into()),
493 Err(e) => output.push(format!("Install failed: {}", e)),
494 },
495
496 Command::Exit => {
498 if ctx.selected.write().await.is_some() {
499 *ctx.selected.write().await = None;
500 output.push("Discarded current shuriken".into());
501 } else {
502 output.push("Cannot exit when there's no shuriken to discard.".into());
503 }
504 }
505
506 Command::None => {
508 output.push("Invalid or unsupported command.".to_string());
509 }
510 }
511 }
512
513 Ok(output)
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_strip_comments() {
522 assert_eq!(strip_comments("code // comment"), "code ");
524
525 assert_eq!(strip_comments("code # comment"), "code ");
527
528 assert_eq!(strip_comments("code"), "code");
530
531 assert_eq!(strip_comments(""), "");
533
534 assert_eq!(strip_comments("// comment"), "");
536 assert_eq!(strip_comments("# comment"), "");
537 }
538
539 #[test]
540 fn test_parse_value() {
541 let val = parse_value("\"hello\"");
543 match val {
544 FieldValue::String(s) => assert_eq!(s, "hello"),
545 _ => panic!("Expected String"),
546 }
547
548 let val = parse_value("'world'");
550 match val {
551 FieldValue::String(s) => assert_eq!(s, "world"),
552 _ => panic!("Expected String"),
553 }
554
555 let val = parse_value("true");
557 match val {
558 FieldValue::Bool(b) => assert!(b),
559 _ => panic!("Expected Bool"),
560 }
561
562 let val = parse_value("false");
564 match val {
565 FieldValue::Bool(b) => assert!(!b),
566 _ => panic!("Expected Bool"),
567 }
568
569 let val = parse_value("42");
571 match val {
572 FieldValue::Number(n) => assert_eq!(n, 42),
573 _ => panic!("Expected Number"),
574 }
575
576 let val = parse_value("unquoted");
578 match val {
579 FieldValue::String(s) => assert_eq!(s, "unquoted"),
580 _ => panic!("Expected String"),
581 }
582 }
583
584 #[test]
585 fn test_parse_kv() {
586 let result = parse_kv("key = value").unwrap();
588 assert!(result.is_some());
589 let (k, v) = result.unwrap();
590 assert_eq!(k, "key");
591 match v {
592 FieldValue::String(s) => assert_eq!(s, "value"),
593 _ => panic!("Expected String"),
594 }
595
596 let result = parse_kv("key = value;").unwrap();
598 assert!(result.is_some());
599 let (k, _) = result.unwrap();
600 assert_eq!(k, "key");
601
602 let result = parse_kv("").unwrap();
604 assert!(result.is_none());
605
606 let result = parse_kv("= value");
608 assert!(result.is_err());
609
610 let result = parse_kv("standalone").unwrap();
612 assert!(result.is_none());
613 }
614
615 #[test]
616 fn test_collect_block_inline() {
617 let lines_vec: Vec<&str> = vec![];
618 let mut lines = lines_vec.iter().copied();
619
620 let result = collect_block("key = value }", &mut lines);
622 assert!(result.is_ok());
623 assert_eq!(result.unwrap(), "key = value");
624 }
625
626 #[test]
627 fn test_collect_block_multiline() {
628 let lines_vec = vec!["line2", "line3 }"];
629 let mut lines = lines_vec.iter().copied();
630
631 let result = collect_block("line1", &mut lines);
633 assert!(result.is_ok());
634 let content = result.unwrap();
635 assert!(content.contains("line1"));
636 assert!(content.contains("line2"));
637 assert!(content.contains("line3"));
638 }
639
640 #[test]
641 fn test_collect_block_missing_closing() {
642 let lines_vec = vec!["line2", "line3"];
643 let mut lines = lines_vec.iter().copied();
644
645 let result = collect_block("line1", &mut lines);
647 assert!(result.is_err());
648 }
649
650 #[test]
651 fn test_command_parser_simple() {
652 let script = "start\nstop\nlist";
654 let result = command_parser(script).unwrap();
655 assert_eq!(result.len(), 3);
656 }
657
658 #[test]
659 fn test_command_parser_with_comments() {
660 let script = "start // start the service\nstop # stop it";
662 let result = command_parser(script).unwrap();
663 assert_eq!(result.len(), 2);
664 }
665
666 #[test]
667 fn test_command_parser_configure_block() {
668 let script = "configure { key1 = value1; key2 = 42 }";
670 let result = command_parser(script).unwrap();
671 assert_eq!(result.len(), 1);
672 match &result[0] {
673 Command::ConfigureBlock(kvs) => {
674 assert_eq!(kvs.len(), 2);
675 assert_eq!(kvs[0].0, "key1");
676 }
677 _ => panic!("Expected ConfigureBlock"),
678 }
679 }
680
681 #[test]
682 fn test_command_parser_empty_lines() {
683 let script = "\n\nstart\n\n\nstop\n\n";
685 let result = command_parser(script).unwrap();
686 assert_eq!(result.len(), 2);
687 }
688
689 #[test]
690 fn test_command_parser_select() {
691 let script = "select myservice";
693 let result = command_parser(script).unwrap();
694 assert_eq!(result.len(), 1);
695 match &result[0] {
696 Command::Select(name) => assert_eq!(name, "myservice"),
697 _ => panic!("Expected Select"),
698 }
699 }
700
701 #[test]
702 fn test_command_parser_set() {
703 let script = "set key value";
705 let result = command_parser(script).unwrap();
706 assert_eq!(result.len(), 1);
707 match &result[0] {
708 Command::Set { key, .. } => assert_eq!(key, "key"),
709 _ => panic!("Expected Set"),
710 }
711 }
712}