Skip to content

Commit

Permalink
change internal representation & Deserialize
Browse files Browse the repository at this point in the history
  • Loading branch information
aawsome committed Sep 13, 2024
1 parent 7bbb51a commit e9f386e
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 54 deletions.
3 changes: 3 additions & 0 deletions crates/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ pub enum CommandErrorKind {
NotAllowedWithAppendOnly(String),
/// Specify one of the keep-* options for forget! Please use keep-none to keep no snapshot.
NoKeepOption,
#[cfg(not(windows))]
/// {0:?}
FromParseError(#[from] shell_words::ParseError),
}

/// [`CryptoErrorKind`] describes the errors that can happen while dealing with Cryptographic functions
Expand Down
101 changes: 63 additions & 38 deletions crates/core/src/repository/command_input.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use std::{fmt::Display, ops::Deref, process::Command, str::FromStr};
use std::{fmt::Display, process::Command, str::FromStr};

use crate::RusticResult;
use crate::{error::RusticErrorKind, RusticError};
use log::{debug, trace, warn};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr, PickFirst};

/// A command to be called which can be given as CLI option as well as in config files
/// `CommandInput` implements Serialize/Deserialize as well as FromStr.
#[serde_as]
#[cfg_attr(feature = "merge", derive(merge::Merge))]
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommandInput(
// Note: we use CommandInputInternal here which itself impls FromStr in order to use serde_as PickFirst for CommandInput.
Expand All @@ -16,50 +17,37 @@ pub struct CommandInput(

impl From<Vec<String>> for CommandInput {
fn from(value: Vec<String>) -> Self {
Self(CommandInputInternal(value))
Self(CommandInputInternal::from_vec(value))

Check warning on line 20 in crates/core/src/repository/command_input.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository/command_input.rs#L19-L20

Added lines #L19 - L20 were not covered by tests
}
}

impl From<CommandInput> for Vec<String> {
fn from(value: CommandInput) -> Self {

Check warning on line 25 in crates/core/src/repository/command_input.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository/command_input.rs#L25

Added line #L25 was not covered by tests
value.0 .0
}
}

impl AsRef<[String]> for CommandInput {
fn as_ref(&self) -> &[String] {
&self.0 .0
}
}

impl Deref for CommandInput {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.0 .0
value.0.iter().cloned().collect()
}
}

impl CommandInput {
/// Returns if a command is set
#[must_use]
pub fn is_set(&self) -> bool {
!self.0 .0.is_empty()
self.0.command.is_some()
}

/// Returns the command if it is set
///
/// Panics if no command is set.
#[must_use]
pub fn command(&self) -> &str {
&self.0 .0[0]
&self.0.command.as_ref().unwrap()
}

/// Returns the command args if it is set
///
/// Panics if no command is set.
#[must_use]
pub fn args(&self) -> &[String] {
&self.0 .0[1..]
&self.0.args.0
}

/// Runs the command if it is set
Expand All @@ -82,7 +70,7 @@ impl CommandInput {
}

impl FromStr for CommandInput {
type Err = shell_words::ParseError;
type Err = RusticError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(CommandInputInternal::from_str(s)?))
}
Expand All @@ -94,31 +82,68 @@ impl Display for CommandInput {
}
}

#[cfg_attr(feature = "merge", derive(merge::Merge))]
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct CommandInputInternal(
#[cfg_attr(feature = "merge", merge(strategy = merge::vec::overwrite_empty))]
// the actual parts of the command
Vec<String>,
);
#[serde_as]
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
#[serde(default)]
struct CommandInputInternal {
command: Option<String>,
#[serde_as(as = "PickFirst<(DisplayFromStr,_)>")]
args: ArgInternal,
}

impl FromStr for CommandInputInternal {
type Err = shell_words::ParseError;
#[cfg(not(windows))]
fn from_str(s: &str) -> Result<Self, Self::Err> {
let vec = shell_words::split(s)?;
Ok(Self(vec))
impl CommandInputInternal {
fn iter(&self) -> impl Iterator<Item = &String> {
self.command.iter().chain(self.args.0.iter())
}

fn from_vec(mut vec: Vec<String>) -> Self {
if vec.is_empty() {
Self::default()
} else {
let command = Some(vec.remove(0));
Self {
command,
args: ArgInternal(vec),

Check warning on line 106 in crates/core/src/repository/command_input.rs

View check run for this annotation

Codecov / codecov/patch

crates/core/src/repository/command_input.rs#L106

Added line #L106 was not covered by tests
}
}
}
#[cfg(windows)]
}

impl FromStr for CommandInputInternal {
type Err = RusticError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let vec = winsplit::split(s);
Ok(Self(vec))
Ok(Self::from_vec(split(s)?))
}
}

impl Display for CommandInputInternal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = shell_words::join(&self.0);
#[cfg(not(windows))]
let s = shell_words::join(self.iter());
#[cfg(windows)]
let s = self.iter().join(" ");

f.write_str(&s)
}
}

#[serde_as]
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
struct ArgInternal(Vec<String>);

impl FromStr for ArgInternal {
type Err = RusticError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(split(s)?))
}
}

// helper to split arguments
#[cfg(not(windows))]
fn split(s: &str) -> RusticResult<Vec<String>> {
Ok(shell_words::split(s).map_err(|err| RusticErrorKind::Command(err.into()))?)
}
#[cfg(windows)]
fn split(s: &str) -> RusticResult<Vec<String>> {
Ok(winsplit::split(s))
}
31 changes: 15 additions & 16 deletions crates/core/tests/command_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,10 @@ use tempfile::tempdir;
#[test]
fn from_str() -> Result<()> {
let cmd: CommandInput = "echo test".parse()?;
assert_eq!(*cmd, ["echo".to_string(), "test".to_string()]);
assert_eq!(cmd.command(), "echo");
assert_eq!(cmd.args(), ["test"]);

let cmd: CommandInput = r#"echo "test test" test"#.parse()?;
assert_eq!(
*cmd,
[
"echo".to_string(),
"test test".to_string(),
"test".to_string()
]
);
assert_eq!(cmd.command(), "echo");
assert_eq!(cmd.args(), ["test test", "test"]);
Ok(())
Expand All @@ -39,26 +30,33 @@ fn toml() -> Result<()> {
struct Test {
command1: CommandInput,
command2: CommandInput,
command3: CommandInput,
command4: CommandInput,
}

let test = toml::from_str::<Test>(
r#"
command1 = "echo test"
command2 = ["echo", "test test"]
command2 = {command = "echo", args = "test test"}
command3 = {command = "echo", args = "'test test'"}
command4 = {command = "echo", args = ["test test", "test"]}
"#,
)?;

assert_eq!(*test.command1, ["echo".to_string(), "test".to_string()]);
assert_eq!(
*test.command2,
["echo".to_string(), "test test".to_string()]
);
assert_eq!(test.command1.command(), "echo");
assert_eq!(test.command1.args(), ["test"]);
assert_eq!(test.command3.command(), "echo");
assert_eq!(test.command3.args(), ["test test"]);
assert_eq!(test.command4.command(), "echo");
assert_eq!(test.command4.args(), ["test test", "test"]);

let test_ser = toml::to_string(&test)?;
assert_eq!(
test_ser,
r#"command1 = "echo test"
command2 = "echo 'test test'"
command2 = "echo test test"
command3 = "echo 'test test'"
command4 = "echo 'test test' test"
"#
);
Ok(())
Expand All @@ -68,6 +66,7 @@ command2 = "echo 'test test'"
fn run() -> Result<()> {
// empty command
let command: CommandInput = "".parse()?;
dbg!(&command);
assert!(!command.is_set());
command.run("test", "empty")?;

Expand Down

0 comments on commit e9f386e

Please sign in to comment.