diff --git a/Cargo.lock b/Cargo.lock index a21f536..8e571fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,27 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b476ce7103678b0c6d3d395dbbae31d48ff910bd28be979ba5d48c6351131d0d" +dependencies = [ + "memchr", +] + +[[package]] +name = "assert_cmd" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c88b9ca26f9c16ec830350d309397e74ee9abdfd8eb1f71cb6ecc71a3fc818da" +dependencies = [ + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -18,12 +40,63 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "escargot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74cf96bec282dcdb07099f7e31d9fed323bca9435a09aba7b6d99b7617bca96d" +dependencies = [ + "lazy_static", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2f02823cf78b754822df5f7f268fb59822e7296276d3e069d8e8cb26a14bd10" +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + [[package]] name = "nix" version = "0.18.0" @@ -46,12 +119,156 @@ dependencies = [ "winapi", ] +[[package]] +name = "predicates" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bfead12e90dccead362d62bb2c90a5f6fc4584963645bc7f71a735e0b0735a" +dependencies = [ + "difference", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178" + +[[package]] +name = "predicates-tree" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124" +dependencies = [ + "predicates-core", + "treeline", +] + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8963b85b8ce3074fecffde43b4b0dded83ce2f367dc8d363afc56679f3ee820b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cab7a364d15cde1e505267766a2d3c4e22a843e1a601f0fa7564c0f82ced11c" + [[package]] name = "rush" version = "0.1.0" dependencies = [ + "assert_cmd", + "escargot", "nix", "os_pipe", + "predicates-core", + "regex", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a230ea9107ca2220eea9d46de97eddcb04cd00e92d13dda78e478dd33fa82bd4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2e59c50ed8f6b050b071aa7b6865293957a9af6b58b94f97c1c9434ad440ea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "treeline" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7fec9b1..1d027e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,9 @@ edition = "2018" [dependencies] os_pipe = "0.9.2" nix = "0.18.0" +regex = "1.3.9" + +[dev-dependencies] +assert_cmd = "1.0.1" +escargot = "0.5.0" +predicates-core = "1.0.0" diff --git a/README.md b/README.md index 211edd0..874b43a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ I know that the name is overused, but it's too good to pass up. - [ ] Async execution `&` - [ ] Shell builtins - [ ] Normal built-ins - - [ ] `alias` `unalias` + - [x] `alias` `unalias` - [X] `cd` - [ ] etc - [ ] Special built-ins diff --git a/src/builtins.rs b/src/builtins.rs index ac86e6e..d8a9764 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -1,3 +1,5 @@ +use regex::Regex; +use std::collections::BTreeMap; use std::process::exit as exit_program; use std::env; use std::rc::Rc; @@ -7,6 +9,36 @@ use crate::helpers::Shell; // Unless specified otherwise, if provided multiple arguments while only // accepting one, these use the first argument. Dash does this as well. +pub fn alias( + // Aliases can be added and then printed in the same command + aliases: &mut BTreeMap, + args: Vec, +) -> bool { + if args.is_empty() { + for (lhs, rhs) in aliases { + println!("alias {}='{}'", lhs, rhs); + } + true + } else { + let mut success = true; + let assignment_re = Regex::new(r"^(\w+)=(.*)").unwrap(); + for arg in args { + if assignment_re.is_match(&arg) { + let caps = assignment_re.captures(&arg).unwrap(); + let lhs = &caps[1]; + let rhs = &caps[2]; + aliases.insert(lhs.to_string(), rhs.to_string()); + } else if aliases.contains_key(&arg) { + println!("alias {}='{}'", arg, aliases[&arg]); + } else { + eprintln!("rush: alias: {}: not found", arg); + success = false; + } + } + success + } +} + pub fn exit(args: Vec) -> bool { match args.get(0).map_or(Ok(0), |x| x.parse::()) { Ok(n) => { @@ -35,3 +67,23 @@ pub fn set(args: Vec, shell: &Rc>) -> bool { true } +pub fn unalias(aliases: &mut BTreeMap, args: Vec) -> bool { + if args.is_empty() { + eprintln!("unalias: usage: unalias [-a] name [name ...]"); + false + } else if args[0] == "-a" { + aliases.clear(); + true + } else { + let mut success = true; + for arg in args { + if aliases.contains_key(&arg) { + aliases.remove(&arg); + } else { + eprintln!("rush: unalias: {}: not found", arg); + success = false; + } + } + success + } +} diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..b064e26 --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,7 @@ +/// Alias of println! but only present in debug builds. +macro_rules! debug_println { + ($($t:tt)*) => { + #[cfg(debug_assertions)] // Only include when not built with `--release` flag + println!($($t)*); + } +} diff --git a/src/helpers.rs b/src/helpers.rs index dda25dc..5cbc492 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,6 +1,6 @@ use nix::unistd::Uid; use os_pipe::{dup_stderr, dup_stdin, dup_stdout, PipeReader, PipeWriter}; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::env; use std::fs::{self, File, OpenOptions}; use std::io::{self, BufRead, BufReader, Write}; @@ -38,6 +38,7 @@ pub struct Shell { positional: Vec, name: String, pub vars: HashMap, + pub aliases: BTreeMap, } impl Shell { @@ -62,6 +63,7 @@ impl Shell { positional: Vec::new(), name, vars: HashMap::new(), + aliases: BTreeMap::new(), } } diff --git a/src/lexer.rs b/src/lexer.rs index 4243ab2..c696c52 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -360,7 +360,7 @@ impl Lexer { } Some(_) => match self.read_until(false, false, false, Box::new(is_token_split)) { Ok(w) => { - println!("The words I got: {:?}", w); + debug_println!("The words I got: {:?}", w); match &w[..] { [Literal(s), ..] if s.ends_with('=') diff --git a/src/lib.rs b/src/lib.rs index ee7ee20..1ebc8e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +#[macro_use] +mod debug; + pub mod lexer; pub mod parser; pub mod runner; diff --git a/src/main.rs b/src/main.rs index d55de49..4c5615e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +#[macro_use] +mod debug; + use rush::lexer::Lexer; use rush::parser::Parser; use rush::runner::Runner; @@ -20,8 +23,7 @@ fn main() { let mut parser = Parser::new(lexer, Rc::clone(&shell)); match parser.get() { Ok(command) => { - #[cfg(debug_assertions)] // Only include when not built with `--release` flag - println!("\u{001b}[34m{:#?}\u{001b}[0m", command); + debug_println!("\u{001b}[34m{:#?}\u{001b}[0m", command); runner.execute(command, false); } diff --git a/src/parser.rs b/src/parser.rs index 1a8f9cc..27b2525 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -136,9 +136,33 @@ impl Parser let mut result = Vec::new(); let mut io = Io::new(); let mut map = HashMap::new(); + let mut env_finished = false; loop { + let mut will_finish_env = true; match self.lexer.peek() { + Some(Assign(_, _)) if !env_finished => { + if let Some(Assign(key, var)) = self.lexer.next() { + map.insert(key, self.expand_word(var)); + } + will_finish_env = false; + } + Some(Assign(_, _)) if env_finished => { + // Stick it back together as if it were a plain word + if let Some(Assign(lhs, mut rhs_expansions)) = self.lexer.next() { + if let [Literal(_)] = &rhs_expansions[..] { + result.push(format!( + "{}={}", + lhs, + rhs_expansions.pop().unwrap().get_name() + )) + } else { + let word = self.expand_word(rhs_expansions); + result.push(format!("{}={}", lhs, word)) + } + } + } + Some(Word(_)) => { if let Some(Word(mut expansions)) = self.lexer.next() { if let [Literal(_)] = &expansions[..] { @@ -151,11 +175,6 @@ impl Parser } } } - Some(Assign(_, _)) => { - if let Some(Assign(key, var)) = self.lexer.next() { - map.insert(key, self.expand_word(var)); - } - } Some(Op(Op::Less)) => { self.lexer.next(); io.set_stdin(self.token_to_fd(&io)?); @@ -181,6 +200,7 @@ impl Parser } _ => break, } + env_finished = env_finished || will_finish_env; } if result.is_empty() { if map.is_empty() { @@ -321,8 +341,7 @@ impl Parser // Though subshells typically seem to inherit everything I'm keeping in my // `shell` variable at the moment? if let Ok(command) = parser.get() { - #[cfg(debug_assertions)] // Only include when not built with `--release` flag - println!("\u{001b}[33m{:#?}\u{001b}[0m", command); + debug_println!("\u{001b}[33m{:#?}\u{001b}[0m", command); let mut output = Runner::new(Rc::clone(&parser.shell)).execute(command, true).unwrap(); output = output.replace(char::is_whitespace, " "); @@ -394,6 +413,7 @@ mod parser_tests { use crate::helpers::Shell; use crate::lexer::Lexer; use std::cell::RefCell; + use std::collections::HashMap; use std::rc::Rc; #[test] @@ -455,4 +475,27 @@ mod parser_tests { )); assert_eq!(expected, parser.get().unwrap()) } + + #[test] + fn test_cmd_with_equals_in_args() { + let shell = Rc::new(RefCell::new(Shell::new(None))); + let lexer = Lexer::new("LANG=en alias foo='bar' foo boo=\"far\"", Rc::clone(&shell)); + let mut parser = Parser::new(lexer, Rc::clone(&shell)); + let expected = Cmd::Simple({ + let mut s = Simple::new( + String::from("alias"), + vec![ + "foo=bar".to_string(), + "foo".to_string(), + "boo=far".to_string(), + ], + Io::new(), + ); + let mut env = HashMap::new(); + env.insert("LANG".to_string(), "en".to_string()); + s.add_env(env); + s + }); + assert_eq!(expected, parser.get().unwrap()) + } } diff --git a/src/runner.rs b/src/runner.rs index 2370230..35c7b96 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,5 +1,7 @@ use crate::builtins; use crate::helpers::{Fd, Shell}; +use crate::lexer::Lexer; +use crate::parser::Parser; use crate::parser::{Cmd, Simple}; use os_pipe::{pipe, PipeReader, PipeWriter}; use std::process::Command; @@ -65,8 +67,27 @@ impl Runner { // but it works for now. Once I figure out what's non-ideal // about it, I'll fix it fn visit(&self, node: Cmd, stdio: CmdMeta) -> bool { + self.expand_alias_then_visit(node, stdio, None) + } + + fn expand_alias_then_visit( + &self, + node: Cmd, + stdio: CmdMeta, + prev_alias: Option, + ) -> bool { match node { - Cmd::Simple(simple) => self.visit_simple(simple, stdio), + Cmd::Simple(simple) => { + if (prev_alias.as_ref() != Some(&simple.cmd)) + && self.shell.borrow().aliases.contains_key(&simple.cmd) + { + let aliased_cmd = simple.cmd.clone(); + let expanded = self.expand_alias(simple); + self.expand_alias_then_visit(expanded, stdio, Some(aliased_cmd)) + } else { + self.visit_simple(simple, stdio) + } + } Cmd::Pipeline(cmd0, cmd1) => self.visit_pipe(*cmd0, *cmd1, stdio), Cmd::And(cmd0, cmd1) => self.visit_and(*cmd0, *cmd1, stdio), Cmd::Or(cmd0, cmd1) => self.visit_or(*cmd0, *cmd1, stdio), @@ -75,6 +96,63 @@ impl Runner { } } + fn expand_alias(&self, cmd: Simple) -> Cmd { + let substitution = &self.shell.borrow().aliases[&cmd.cmd]; + let lexer = Lexer::new(substitution, Rc::clone(&self.shell)); + let mut parser = Parser::new(lexer, Rc::clone(&self.shell)); + + if let Ok(expanded) = parser.get() { + fn move_args(expanding: Cmd, parent: Simple) -> Cmd { + match expanding { + Cmd::Simple(mut new_simple) => { + new_simple.args.extend(parent.args); + Cmd::Simple(new_simple) + } + Cmd::Pipeline(lhs, rhs) => { + Cmd::Pipeline(lhs, Box::new(move_args(*rhs, parent))) + } + Cmd::And(lhs, rhs) => Cmd::And(lhs, Box::new(move_args(*rhs, parent))), + Cmd::Or(lhs, rhs) => Cmd::Or(lhs, Box::new(move_args(*rhs, parent))), + Cmd::Not(not) => Cmd::Not(Box::new(move_args(*not, parent))), + Cmd::Empty => Cmd::Empty, + } + } + + fn propagate_env(expanding: Cmd, parent: &Simple) -> Cmd { + match expanding { + Cmd::Simple(mut new_simple) => { + new_simple.env = parent.env.clone(); + Cmd::Simple(new_simple) + } + Cmd::Pipeline(lhs, rhs) => Cmd::Pipeline( + Box::new(propagate_env(*lhs, parent)), + Box::new(propagate_env(*rhs, parent)), + ), + Cmd::And(lhs, rhs) => Cmd::And( + Box::new(propagate_env(*lhs, parent)), + Box::new(propagate_env(*rhs, parent)), + ), + Cmd::Or(lhs, rhs) => Cmd::Or( + Box::new(propagate_env(*lhs, parent)), + Box::new(propagate_env(*rhs, parent)), + ), + Cmd::Not(not) => Cmd::Not(Box::new(propagate_env(*not, parent))), + Cmd::Empty => Cmd::Empty, + } + } + + move_args(propagate_env(expanded, &cmd), cmd) + } else { + let mut cmd = cmd; + cmd.cmd = if cmd.args.is_empty() { + "".to_string() + } else { + cmd.args.remove(0) + }; + Cmd::Simple(cmd) + } + } + fn visit_not(&self, cmd: Cmd, stdio: CmdMeta) -> bool { let result = self.visit(cmd, stdio); !result @@ -109,9 +187,12 @@ impl Runner { fn visit_simple(&self, mut simple: Simple, stdio: CmdMeta) -> bool { self.reconcile_io(&mut simple, stdio); match &simple.cmd[..] { + "alias" => builtins::alias(&mut self.shell.borrow_mut().aliases, simple.args), "exit" => builtins::exit(simple.args), "cd" => builtins::cd(simple.args), "set" => builtins::set(simple.args, &self.shell), + "unalias" => builtins::unalias(&mut self.shell.borrow_mut().aliases, simple.args), + command => { let mut cmd = Command::new(command); cmd.args(&simple.args); @@ -161,4 +242,3 @@ impl Runner { } } // How do I test this module? - diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 0000000..7b34ce7 --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,178 @@ +use assert_cmd::assert::IntoOutputPredicate; +use assert_cmd::cmd::Command; +use escargot::CargoBuild; +use predicates_core::Predicate; + +fn test_io( + input: I, + output: O, +) -> Result> +where + I: Into>, + O: IntoOutputPredicate

, + P: Predicate<[u8]>, +{ + let mut cmd = Command::from_std( + CargoBuild::new() + .bin(env!("CARGO_PKG_NAME")) + .release() + .run()? + .command(), + ); + let assert = cmd.write_stdin(input).assert(); + Ok(assert.stdout(output)) +} + +#[test] +fn alias_defines_and_prints_aliases() -> Result<(), Box> { + test_io( + "alias foo=echo +foo bar +alias +alias foo +alias boo='far' foo boo +alias +", + "$> $> bar +$> alias foo='echo' +$> alias foo='echo' +$> alias foo='echo' +alias boo='far' +$> alias boo='far' +alias foo='echo' +$> +", + )? + .success() + .code(0); + Ok(()) +} + +#[test] +fn aliases_do_not_self_recurse() -> Result<(), Box> { + test_io( + "alias echo='echo foo' +echo bar +", + "$> $> foo bar +$> +", + )? + .success() + .code(0); + Ok(()) +} + +#[test] +fn aliases_can_be_nested() -> Result<(), Box> { + test_io( + "alias foo='bar asdf && bar fdsa' +alias bar=echo +foo boo +", + "$> $> $> asdf +fdsa boo +$> +", + )? + .success() + .code(0); + Ok(()) +} + +#[test] +fn alias_can_be_empty() -> Result<(), Box> { + test_io( + "alias foo= +alias +foo +foo echo bar +", + "$> $> alias foo='' +$> $> bar +$> +", + )? + .success() + .code(0); + Ok(()) +} + +#[test] +fn echo_prints_argument() -> Result<(), Box> { + test_io( + "echo foo", + "$> foo +$> +", + )? + .success() + .code(0); + Ok(()) +} + +#[test] +fn empty_input_prints_newline() -> Result<(), Box> { + test_io( + "", "$> +", + )? + .success() + .code(0); + Ok(()) +} + +#[test] +fn exit_prints_nothing() -> Result<(), Box> { + test_io("exit", "$> ")?.success().code(0); + Ok(()) +} + +#[test] +fn unalias_removes_alias() -> Result<(), Box> { + test_io( + "alias foo='echo' bar='oche' +alias +unalias foo +alias +unalias bar +foo foo +", + "$> $> alias bar='oche' +alias foo='echo' +$> $> alias bar='oche' +$> $> $> +", + )? + .stderr( + "rush: foo: No such file or directory (os error 2) +", + ) + .success() + .code(0); + Ok(()) +} + +#[test] +fn unalias_dash_a_removes_all_aliases() -> Result<(), Box> { + test_io( + "alias foo='echo' bar='oche' +alias +unalias -a +alias +alias foo='echo' bar='oche' +alias +unalias -a asdf +alias +", + "$> $> alias bar='oche' +alias foo='echo' +$> $> $> $> alias bar='oche' +alias foo='echo' +$> $> $> +", + )? + .success() + .code(0); + Ok(()) +}