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)]
17pub enum Command {
18 HttpStart(u16),
20 Start,
22 Stop,
24 Select(String),
26 Get(String),
28 Exit,
30 Configure,
32 ConfigureBlock(Vec<(String, FieldValue)>),
34 Set { key: String, value: FieldValue },
36 List,
38 ListState,
40 Install(String),
42 Toggle(String),
44 Execute(PathBuf),
46 Help,
48 None,
50}
51
52fn strip_comments(line: &str) -> &str {
61 if let Some(i) = line.find("//") {
62 &line[..i]
63 } else if let Some(i) = line.find('#') {
64 &line[..i]
65 } else {
66 line
67 }
68}
69
70fn parse_value(raw: &str) -> FieldValue {
75 let v = raw.trim();
76
77 if (v.starts_with('"') && v.ends_with('"')) || (v.starts_with('\'') && v.ends_with('\'')) {
79 let inner = &v[1..v.len() - 1];
80 return FieldValue::String(inner.to_string());
81 }
82
83 match v.to_ascii_lowercase().as_str() {
85 "true" => return FieldValue::Bool(true),
86 "false" => return FieldValue::Bool(false),
87 _ => {}
88 }
89
90 if let Ok(i) = v.parse::<i64>() {
92 return FieldValue::Number(i);
93 }
94
95 FieldValue::String(v.to_string())
97}
98
99fn parse_kv(text: &str) -> Result<Option<(String, FieldValue)>> {
106 let t = text.trim();
107 if t.is_empty() {
108 return Ok(None);
109 }
110
111 if let Some((left, right)) = t.split_once('=') {
112 let key = left.trim();
113 let val = right.trim();
114
115 if key.is_empty() {
116 bail!("Invalid assignment (empty key): `{}`", text);
117 }
118
119 let val = val.trim_end_matches(';').trim();
121 Ok(Some((key.to_string(), parse_value(val))))
122 } else {
123 Ok(None)
125 }
126}
127
128fn collect_block<'a, I>(first_after_brace: &'a str, lines: &mut I) -> Result<String>
141where
142 I: Iterator<Item = &'a str>,
143{
144 let trimmed_after = first_after_brace.trim();
146
147 if let Some(inner) = trimmed_after.strip_suffix("}") {
148 return Ok(inner.trim().to_string());
150 }
151
152 let mut collected = String::new();
154 if !trimmed_after.is_empty() {
155 collected.push_str(trimmed_after);
156 collected.push('\n');
157 }
158
159 for next in lines {
161 let stripped = strip_comments(next).trim();
162 if let Some(inner) = stripped.strip_suffix('}') {
163 if !inner.trim().is_empty() {
164 collected.push_str(inner.trim());
165 }
166 return Ok(collected.trim().to_string());
167 } else if !stripped.is_empty() {
168 collected.push_str(stripped);
169 collected.push('\n');
170 }
171 }
172
173 bail!("Missing closing '}}' for block");
175}
176
177fn command_parser(script: &str) -> Result<Vec<Command>> {
186 let mut commands = Vec::new();
187
188 let mut lines = script.lines();
190
191 while let Some(raw_line) = lines.next() {
193 let no_comment = strip_comments(raw_line);
195 let trimmed = no_comment.trim();
196 if trimmed.is_empty() {
197 continue;
198 }
199
200 if trimmed.starts_with("configure") {
203 if let Some((_, after_brace)) = trimmed.split_once('{') {
205 let block_content = collect_block(after_brace, &mut lines)?;
207 let mut kvs: Vec<(String, FieldValue)> = Vec::new();
209
210 for chunk in block_content.split([';', '\n']) {
211 if let Some((k, v)) = parse_kv(chunk)? {
212 kvs.push((k, v));
213 }
214 }
215
216 commands.push(Command::ConfigureBlock(kvs));
217 continue;
218 } else {
219 commands.push(Command::Configure);
221 continue;
222 }
223 }
224
225 if let Some(tokens) = split(trimmed) {
228 let tokens: Vec<String> = tokens
229 .into_iter()
230 .filter(|token| {
231 let token = token.trim();
232 !token.is_empty() && token != "="
233 })
234 .collect();
235
236 if tokens.is_empty() {
237 continue;
238 }
239
240 let cmd = match tokens[0].as_str() {
241 "http" => Command::HttpStart(tokens[1].parse().unwrap_or(80)),
242 "start" => Command::Start,
243 "stop" => Command::Stop,
244 "select" => {
245 if tokens.len() > 1 {
246 Command::Select(tokens[1].clone())
247 } else {
248 Command::None
249 }
250 }
251 "help" => Command::Help,
252 "get" => {
253 if tokens.len() > 1 {
254 Command::Get(tokens[1].clone())
255 } else {
256 Command::None
257 }
258 }
259 "set" => {
260 if tokens.len() > 2 {
261 Command::Set {
262 key: tokens[1].clone(),
263 value: parse_value(tokens[2].as_str()),
264 }
265 } else {
266 Command::None
267 }
268 }
269 "list" => {
270 if tokens.len() > 1 && tokens[1].eq_ignore_ascii_case("state") {
271 Command::ListState
272 } else {
273 Command::List
274 }
275 }
276 "install" => {
277 if tokens.len() > 1 {
278 Command::Install(tokens[1].clone())
279 } else {
280 Command::None
281 }
282 }
283 "toggle" => {
284 if tokens.len() > 1 {
285 Command::Toggle(tokens[1].clone())
286 } else {
287 Command::None
288 }
289 }
290 "execute" => {
291 if tokens.len() > 1 {
292 Command::Execute(PathBuf::from(tokens[1].clone()))
293 } else {
294 Command::None
295 }
296 }
297 "exit" => Command::Exit,
298 "configure" => Command::Configure, _ => Command::None,
300 };
301
302 commands.push(cmd);
303 }
304 }
305
306 Ok(commands)
307}
308
309fn locate_ninja_cli() -> Result<PathBuf> {
321 let exe_path = env::current_exe()?;
322 if let Some(root) = exe_path.parent() {
323 let cli_path = if cfg!(windows) {
324 root.join("shurikenctl.exe")
325 } else {
326 root.join("shurikenctl")
327 };
328
329 if !cli_path.exists() {
330 return Err(Error::msg("No ninja CLI found"));
331 }
332
333 Ok(cli_path)
334 } else {
335 Err(Error::msg(
336 "No parent directory? where and how did you run this? please email me i'm genuinely curious. -- Hannan \"tunafysh\" Smani",
337 ))
338 }
339}
340
341pub struct DslContext {
345 pub manager: ShurikenManager,
347 pub selected: Arc<RwLock<Option<String>>>,
349}
350
351impl DslContext {
352 pub fn new(manager: ShurikenManager) -> Self {
360 Self {
361 manager,
362 selected: Arc::new(RwLock::new(None)),
363 }
364 }
365}
366
367pub async fn execute_commands(ctx: &DslContext, script: String) -> Result<Vec<String>> {
380 let parsed_commands = command_parser(script.as_str())?;
381
382 let mut output: Vec<String> = Vec::new();
383
384 for command in parsed_commands {
385 match command {
386 Command::HttpStart(port) => {
388 let path = locate_ninja_cli()?;
389 SubprocessCommand::new(path)
390 .arg("api")
391 .arg(port.to_string())
392 .stdout(Stdio::inherit())
393 .stderr(Stdio::inherit())
394 .stdin(Stdio::inherit())
395 .status()
396 .await?;
397 output.push(format!("HTTP server started on port {}", port));
398 }
399
400 Command::Select(name) => {
402 debug!("Shurikens: {:#?}", ctx.manager.shurikens.read().await);
403 if ctx.manager.shurikens.read().await.contains_key(&name) {
404 *ctx.selected.write().await = Some(name.clone());
405 output.push(format!("Selected shuriken '{}'", name));
406 } else {
407 output.push(format!("No such shuriken: {}", name));
408 }
409 }
410
411 Command::Configure => {
413 if let Some(name) = &*ctx.selected.read().await {
414 let mut shurikens = ctx.manager.shurikens.write().await;
415 if let Some(shuriken) = shurikens.get_mut(name) {
416 let path = &ctx.manager.root_path;
417 shuriken
418 .configure(
419 path,
420 &*ctx.manager.engine.lock().await,
421 Some(ctx.manager.clone()),
422 )
423 .await
424 .map_err(Error::msg)?;
425
426 output.push(format!(
427 "Generated configuration for shuriken {} successfully.",
428 &name
429 ));
430 }
431 }
432 }
433
434 Command::ConfigureBlock(kvs) => {
436 if let Some(shuriken_name) = &*ctx.selected.read().await {
437 let mut shurikens = ctx.manager.shurikens.write().await;
438 if let Some(shuriken) = shurikens.get_mut(shuriken_name)
439 && let Some(cfg) = &mut shuriken.config
440 {
441 let partial_options = cfg.options.get_or_insert_with(Default::default);
442 for (k, v) in kvs {
443 partial_options.insert(k.clone(), v.clone());
444 output.push(format!(
445 "Set {} = {} for {}",
446 k,
447 v.render(),
448 shuriken_name
449 ));
450 }
451 } else {
452 output.push("No selected shuriken or missing config while applying configure block.".to_string());
453 }
454 } else {
455 output.push("No shuriken selected — configure block ignored.".into());
456 }
457 }
458
459 Command::Help => {
460 output.push(
461 "Available commands:
462 http start <port> - Start the HTTP server
463 select <name> - Select a shuriken
464 configure - Generate configuration for the selected shuriken
465 configure { k = v } - Apply config assignments to the selected shuriken
466 set <key> <value> - Set a config key for the selected shuriken
467 get <key> - Get a config key's value
468 toggle <key> - Toggle a boolean config key
469 start - Start the selected shuriken
470 stop - Stop the selected shuriken
471 install <url | registry_entry | path> - Install a new shuriken from a file, url or a registry entry
472 list - List all shurikens
473 list state - List shurikens with their states
474 execute <script> - Run a Ninja script file
475 exit - Deselect current shuriken
476 help - Show this message"
477 .to_string(),
478 );
479 }
480
481 Command::Set { key, value } => {
483 if let Some(shuriken_name) = &*ctx.selected.read().await {
484 let mut shurikens = ctx.manager.shurikens.write().await;
485 if let Some(shuriken) = shurikens.get_mut(shuriken_name)
486 && let Some(cfg) = &mut shuriken.config
487 {
488 let cloned_value: FieldValue = value.clone();
489 if let Some(partial_options) = &mut cfg.options {
490 partial_options.insert(key.clone(), FieldValue::from(value.render()));
491 }
492
493 output.push(format!(
494 "Set {} = {} for {}",
495 key,
496 cloned_value.render(),
497 shuriken_name
498 ));
499 }
500 }
501 }
502
503 Command::Get(key) => {
504 if let Some(shuriken_name) = &*ctx.selected.read().await {
505 let shurikens = ctx.manager.shurikens.read().await;
506 if let Some(shuriken) = shurikens.get(shuriken_name)
507 && let Some(cfg) = &shuriken.config
508 && let Some(options) = &cfg.options
509 {
510 output.push(format!("{:?} = {:?}", key, options.get(&key)));
511 }
512 }
513 }
514
515 Command::Toggle(key) => {
516 if let Some(shuriken_name) = &*ctx.selected.read().await {
517 let mut shurikens = ctx.manager.shurikens.write().await;
518 if let Some(shuriken) = shurikens.get_mut(shuriken_name)
519 && let Some(cfg) = &mut shuriken.config
520 && let Some(options) = &mut cfg.options
521 && let Some(FieldValue::Bool(value)) = options.get_mut(&key)
522 {
523 *value = !*value;
524 output.push(format!("Toggled {} to {}", key, value));
525 }
526 }
527 }
528
529 Command::List => {
531 if let Either::Right(names) = ctx.manager.list(false).await? {
532 output.push(format!("Shurikens: {:?}", names))
533 }
534 }
535 Command::ListState => {
536 if let Either::Left(states) = ctx.manager.list(true).await? {
537 for (name, state) in states {
538 output.push(format!("{} -> {:?}", name, state));
539 }
540 }
541 }
542
543 Command::Start => {
544 if let Some(name) = &*ctx.selected.read().await {
545 match ctx.manager.start(name).await {
546 Ok(_) => output.push(format!("Started {}", name)),
547 Err(e) => output.push(format!("Error: {}", e)),
548 }
549 }
550 }
551 Command::Stop => {
552 if let Some(name) = &*ctx.selected.read().await {
553 match ctx.manager.stop(name).await {
554 Ok(_) => output.push(format!("Stopped {}", name)),
555 Err(e) => output.push(format!("Error: {}", e)),
556 }
557 }
558 }
559
560 Command::Execute(script_path) => {
561 let engine = NinjaEngine::new()
562 .await
563 .map_err(|e| io::Error::other(e.to_string()))?;
564 engine
565 .execute_file(&script_path, None, Some(ctx.manager.clone()))
566 .await
567 .map_err(|e| io::Error::other(e.to_string()))?;
568 }
569 Command::Install(file_path) => match ctx.manager.install(&file_path).await {
570 Ok(_) => output.push("Installed successfully".into()),
571 Err(e) => output.push(format!("Install failed: {}", e)),
572 },
573
574 Command::Exit => {
576 if ctx.selected.write().await.is_some() {
577 *ctx.selected.write().await = None;
578 output.push("Discarded current shuriken".into());
579 } else {
580 output.push("Cannot exit when there's no shuriken to discard.".into());
581 }
582 }
583
584 Command::None => {
586 output.push("Invalid or unsupported command.".to_string());
587 }
588 }
589 }
590
591 Ok(output)
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 #[test]
599 fn test_strip_comments() {
600 assert_eq!(strip_comments("code // comment"), "code ");
602
603 assert_eq!(strip_comments("code # comment"), "code ");
605
606 assert_eq!(strip_comments("code"), "code");
608
609 assert_eq!(strip_comments(""), "");
611
612 assert_eq!(strip_comments("// comment"), "");
614 assert_eq!(strip_comments("# comment"), "");
615 }
616
617 #[test]
618 fn test_parse_value() {
619 let val = parse_value("\"hello\"");
621 match val {
622 FieldValue::String(s) => assert_eq!(s, "hello"),
623 _ => panic!("Expected String"),
624 }
625
626 let val = parse_value("'world'");
628 match val {
629 FieldValue::String(s) => assert_eq!(s, "world"),
630 _ => panic!("Expected String"),
631 }
632
633 let val = parse_value("true");
635 match val {
636 FieldValue::Bool(b) => assert!(b),
637 _ => panic!("Expected Bool"),
638 }
639
640 let val = parse_value("false");
642 match val {
643 FieldValue::Bool(b) => assert!(!b),
644 _ => panic!("Expected Bool"),
645 }
646
647 let val = parse_value("42");
649 match val {
650 FieldValue::Number(n) => assert_eq!(n, 42),
651 _ => panic!("Expected Number"),
652 }
653
654 let val = parse_value("unquoted");
656 match val {
657 FieldValue::String(s) => assert_eq!(s, "unquoted"),
658 _ => panic!("Expected String"),
659 }
660 }
661
662 #[test]
663 fn test_parse_kv() {
664 let result = parse_kv("key = value").unwrap();
666 assert!(result.is_some());
667 let (k, v) = result.unwrap();
668 assert_eq!(k, "key");
669 match v {
670 FieldValue::String(s) => assert_eq!(s, "value"),
671 _ => panic!("Expected String"),
672 }
673
674 let result = parse_kv("key = value;").unwrap();
676 assert!(result.is_some());
677 let (k, _) = result.unwrap();
678 assert_eq!(k, "key");
679
680 let result = parse_kv("").unwrap();
682 assert!(result.is_none());
683
684 let result = parse_kv("= value");
686 assert!(result.is_err());
687
688 let result = parse_kv("standalone").unwrap();
690 assert!(result.is_none());
691 }
692
693 #[test]
694 fn test_collect_block_inline() {
695 let lines_vec: Vec<&str> = vec![];
696 let mut lines = lines_vec.iter().copied();
697
698 let result = collect_block("key = value }", &mut lines);
700 assert!(result.is_ok());
701 assert_eq!(result.unwrap(), "key = value");
702 }
703
704 #[test]
705 fn test_collect_block_multiline() {
706 let lines_vec = vec!["line2", "line3 }"];
707 let mut lines = lines_vec.iter().copied();
708
709 let result = collect_block("line1", &mut lines);
711 assert!(result.is_ok());
712 let content = result.unwrap();
713 assert!(content.contains("line1"));
714 assert!(content.contains("line2"));
715 assert!(content.contains("line3"));
716 }
717
718 #[test]
719 fn test_collect_block_missing_closing() {
720 let lines_vec = vec!["line2", "line3"];
721 let mut lines = lines_vec.iter().copied();
722
723 let result = collect_block("line1", &mut lines);
725 assert!(result.is_err());
726 }
727
728 #[test]
729 fn test_command_parser_simple() {
730 let script = "start\nstop\nlist";
732 let result = command_parser(script).unwrap();
733 assert_eq!(result.len(), 3);
734 }
735
736 #[test]
737 fn test_command_parser_with_comments() {
738 let script = "start // start the service\nstop # stop it";
740 let result = command_parser(script).unwrap();
741 assert_eq!(result.len(), 2);
742 }
743
744 #[test]
745 fn test_command_parser_configure_block() {
746 let script = "configure { key1 = value1; key2 = 42 }";
748 let result = command_parser(script).unwrap();
749 assert_eq!(result.len(), 1);
750 match &result[0] {
751 Command::ConfigureBlock(kvs) => {
752 assert_eq!(kvs.len(), 2);
753 assert_eq!(kvs[0].0, "key1");
754 }
755 _ => panic!("Expected ConfigureBlock"),
756 }
757 }
758
759 #[test]
760 fn test_command_parser_empty_lines() {
761 let script = "\n\nstart\n\n\nstop\n\n";
763 let result = command_parser(script).unwrap();
764 assert_eq!(result.len(), 2);
765 }
766
767 #[test]
768 fn test_command_parser_select() {
769 let script = "select myservice";
771 let result = command_parser(script).unwrap();
772 assert_eq!(result.len(), 1);
773 match &result[0] {
774 Command::Select(name) => assert_eq!(name, "myservice"),
775 _ => panic!("Expected Select"),
776 }
777 }
778
779 #[test]
780 fn test_command_parser_set() {
781 let script = "set key value";
783 let result = command_parser(script).unwrap();
784 assert_eq!(result.len(), 1);
785 match &result[0] {
786 Command::Set { key, .. } => assert_eq!(key, "key"),
787 _ => panic!("Expected Set"),
788 }
789 }
790}