Skip to content

Commit

Permalink
feature/timestamp (#2)
Browse files Browse the repository at this point in the history
* started implementation

* finished implementation and started adding tests

* fixed error string not used on output

* made things a bit simpler

* added unit tests

* added documentation for ts

* updated readme
  • Loading branch information
haondt committed Aug 21, 2023
1 parent 69fc347 commit 4054e3f
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 15 deletions.
13 changes: 12 additions & 1 deletion README.md
Expand Up @@ -19,14 +19,25 @@ Alternatively, you can clone the repository and build with Cargo.

## Usage

The basic usage is `medea [command] --options`. See `medea --help` or `medea [command] --help` for more details. Here are some example usages:
The basic usage is `medea [command] --options`. See `medea help` or `medea help [command] help` for more details. Here are some example usages:

```shell
# generate an HS256 hash
echo -n 'my data' | medea hash -a sha256 --hmac 'my secret'

# generate some uuids
medea uuid -uc 5

# convert timestamps
medea ts --format iso -z America/Los_Angeles 1678742400
```

## Tests

Run tests with

```shell
cargo test
```

## License
Expand Down
4 changes: 4 additions & 0 deletions medea/Cargo.toml
Expand Up @@ -8,14 +8,18 @@ edition = "2021"
[dependencies]
base16ct = { version = "0.2.0", features = ["alloc"] }
base64ct = { version = "1.6.0", features = ["alloc"] }
chrono = "0.4.26"
chrono-tz = "0.8.3"
clap = { version = "4.3.21", features = ["derive"] }
colored = "2.0.4"
digest = "0.10.7"
enum_dispatch = "0.3.12"
hmac = "0.12.1"
indoc = "2.0.3"
libc = "0.2.147"
mac_address = "1.1.5"
md-5 = "0.10.5"
regex = "1.9.3"
sha1 = "0.10.5"
sha2 = "0.10.7"
uuid = { version = "1.4.1", features = ["v4", "fast-rng", "v1", "std"] }
Expand Down
11 changes: 9 additions & 2 deletions medea/src/cli/args.rs
@@ -1,3 +1,5 @@
use std::io::{self, Read};

use clap::Parser;
use enum_dispatch::enum_dispatch;
use super::ArgsEnum;
Expand All @@ -15,10 +17,15 @@ pub struct BaseArgs {
pub command: ArgsEnum,
}

fn get_input_from_stdin() -> String {
let mut message = String::new();
let _ = io::stdin().read_to_string(&mut message);
return message;
}

pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let args = BaseArgs::parse();
let result = &args.command.run(&args)?;
let result = &args.command.run(&args, get_input_from_stdin)?;
if args.trim {
print!("{}", result);
} else {
Expand All @@ -30,5 +37,5 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {

#[enum_dispatch]
pub trait Runnable {
fn run(&self, base_args: &BaseArgs) -> Result<String, Box<dyn std::error::Error>>;
fn run(&self, base_args: &BaseArgs, get_input: impl Fn() -> String) -> Result<String, Box<dyn std::error::Error>>;
}
12 changes: 4 additions & 8 deletions medea/src/cli/commands/hash.rs
@@ -1,12 +1,9 @@
use std::{
error::Error,
io::{self, Read},
};
use std::error::Error;

use super::super::{BaseArgs, Runnable};
use base64ct::{Base64, Encoding};
use clap::{Parser, ValueEnum};
use digest::{OutputSizeUser};
use digest::OutputSizeUser;
use hmac::{Hmac, Mac};
use sha1::Sha1;

Expand Down Expand Up @@ -64,9 +61,8 @@ impl<T: Mac + OutputSizeUser + Clone> DynHmacDigest for T {


impl Runnable for HashArgs {
fn run(&self, _: &BaseArgs) -> Result<String, Box<dyn Error>> {
let mut message = String::new();
let _ = io::stdin().read_to_string(&mut message);
fn run(&self, _: &BaseArgs, get_input:impl Fn() -> String) -> Result<String,Box<dyn Error>> {
let message = get_input();
let data = message.as_bytes();
let res: Vec<u8>;

Expand Down
3 changes: 2 additions & 1 deletion medea/src/cli/commands/mod.rs
@@ -1,2 +1,3 @@
pub mod uuid;
pub mod hash;
pub mod hash;
pub mod timestamp;
237 changes: 237 additions & 0 deletions medea/src/cli/commands/timestamp.rs
@@ -0,0 +1,237 @@
use std::error::Error;

use super::super::{BaseArgs, Runnable};
use clap::{Parser, ValueEnum};

use chrono::{DateTime, TimeZone, Utc};
use chrono_tz::Tz;
use indoc::indoc;
use regex::Regex;

#[derive(Parser, Debug, Clone)]
#[command(
about = "Parse and convert timestamps",
after_help = "See `medea help timestamp` for details",
long_about = indoc!{"
Read a timestamp and convert it to the desired format.
Both iso8601 and unix (epoch) timestamps are supported
as input, and the type will be parsed automatically.
Omit the input to use the current time.
"},
after_long_help = indoc!{r#"
Examples:
# convert iso8601 to epoch
$ medea ts -f unix 2023-08-20T15:30:00Z
1692545400
# convert epoch to iso8601 with timezone
$ medea ts -f iso -z America/Los_Angeles 1678742400
2023-03-13T14:20:00-07:00
# get current time as epoch
$ medea ts -f unix
1692580941
"#}
)]
pub struct TimeStampArgs {

#[arg(
help = "Input timestamp",
long_help = indoc!{"
Input timestamp. Accepts unix (epoch)
or iso8601 format. If omitted, will
default to current time.
"},
required = false
)]
timestamp: Option<String>,

#[arg(short = 'z', long, long_help = "Timezone of the output")]
timezone: Option<String>,

#[arg(
short,
long,
default_value = "iso",
long_help = "Format for output",
)]
format: Format,
}

#[derive(ValueEnum, Debug, Clone)]
enum Format {
Iso,
Unix
}

#[derive(Debug)]
enum TimestampError {
ParseError(chrono::format::ParseError),
InvalidTimeZoneError(String),
}
impl From<chrono::format::ParseError> for TimestampError {
fn from(err: chrono::format::ParseError) -> Self {
TimestampError::ParseError(err)
}
}
impl std::fmt::Display for TimestampError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TimestampError::ParseError(e) => {
write!(f, "could not parse input as a naive datetime: {}", e)
}
TimestampError::InvalidTimeZoneError(s) => {
write!(f, "Invalid Timezone: {}", s)
}
}
}
}
impl Error for TimestampError {}

impl TimeStampArgs {
const NUMERIC_TIMESTAMP_PATTERN: &str = r"^[0-9]+$";

fn inner_run(
&self,
_: &BaseArgs,
_: impl Fn() -> String,
) -> Result<String, Box<dyn Error>> {
let ts = match &self.timestamp {
Some(input_string) => {
let regex = Regex::new(Self::NUMERIC_TIMESTAMP_PATTERN)?;
if regex.is_match(&input_string) {
let secs = input_string.parse::<i64>()?;
Utc.timestamp_opt(secs, 0).unwrap()
} else {
DateTime::parse_from_str(&input_string.as_str(), "%+")
.map_err(|e| TimestampError::ParseError(e))?.with_timezone(&Utc)
}

},
None => Utc::now()
};

let format_str = match self.format {
Format::Unix => "%s",
Format::Iso => "%+",
};

let output = match &self.timezone {
Some(t) => {
ts
.with_timezone(&t.parse::<Tz>().map_err(|e| TimestampError::InvalidTimeZoneError(e))?)
.format(format_str)
.to_string()
},
None => ts.format(format_str).to_string(),
};

return Ok(output);
}
}

impl Runnable for TimeStampArgs {
fn run(
&self,
base_args: &BaseArgs,
get_input: impl Fn() -> String,
) -> Result<String, Box<dyn std::error::Error>> {
self.inner_run(base_args, get_input)
}
}

#[cfg(test)]
mod tests {
use std::error::Error;

use crate::cli::{
args::{BaseArgs, Runnable},
ArgsEnum,
};

use super::TimeStampArgs;

fn base_args(tsa: TimeStampArgs) -> BaseArgs {
BaseArgs {
colors: false,
trim: false,
command: ArgsEnum::Timestamp(tsa),
}
}

fn spoof_input(input: String) -> Box<dyn Fn() -> String> {
return Box::new(move || -> String { return input.clone() });
}

fn run(sut: TimeStampArgs) -> Result<String, Box<dyn Error>> {
Ok(sut.run(&base_args(sut.clone()), spoof_input(String::new()))?)
}

#[test]
fn will_generate_timestamp() -> Result<(), Box<dyn Error>> {
let sut = TimeStampArgs {
timezone: None,
format: super::Format::Iso,
timestamp: None,
};

let ts = run(sut)?;
assert!(!ts.is_empty());
Ok(())
}

#[test]
fn will_convert_from_unix_time() -> Result<(), Box<dyn Error>> {
let sut = TimeStampArgs {
timezone: None,
format: super::Format::Iso,
timestamp: Some(String::from("1234567890")),
};

let ts = run(sut)?;
assert_eq!(ts, "2009-02-13T23:31:30+00:00");
Ok(())
}

#[test]
fn will_convert_to_unix_time() -> Result<(), Box<dyn Error>> {
let sut = TimeStampArgs {
timezone: None,
format: super::Format::Unix,
timestamp: Some(String::from("2009-02-13T23:31:30+03:00")),
};

let ts = run(sut)?;
assert_eq!(ts, "1234557090");
Ok(())
}

#[test]
fn will_convert_with_short_timezone() -> Result<(), Box<dyn Error>> {
let sut = TimeStampArgs {
timezone: Some(String::from("EST")),
format: super::Format::Iso,
timestamp: Some(String::from("2009-02-13T23:31:30+02:00")),
};

let ts = run(sut)?;
assert_eq!(ts, "2009-02-13T16:31:30-05:00");
Ok(())
}

#[test]
fn will_convert_with_long_timezone() -> Result<(), Box<dyn Error>> {
let sut = TimeStampArgs {
timezone: Some(String::from("America/Toronto")),
format: super::Format::Iso,
timestamp: Some(String::from("2009-02-13T23:31:30+02:00")),
};

let ts = run(sut)?;
assert_eq!(ts, "2009-02-13T16:31:30-05:00");
Ok(())
}


}
3 changes: 1 addition & 2 deletions medea/src/cli/commands/uuid.rs
Expand Up @@ -70,7 +70,7 @@ impl UuidArgs {
}

impl Runnable for UuidArgs {
fn run(&self, _: &BaseArgs) -> Result<String, Box<dyn Error>> {
fn run(&self, _: &BaseArgs, _:impl Fn() -> String) -> Result<String,Box<dyn Error>> {
if self.count == 0 { return Ok(String::new()); }

let mut s = self.get_uuid_string()?;
Expand All @@ -79,5 +79,4 @@ impl Runnable for UuidArgs {
}
return Ok(s);
}

}
5 changes: 4 additions & 1 deletion medea/src/cli/mod.rs
Expand Up @@ -8,12 +8,15 @@ use enum_dispatch::enum_dispatch;
use args::{Runnable, BaseArgs};
use commands::uuid::UuidArgs;
use commands::hash::HashArgs;
use commands::timestamp::TimeStampArgs;

#[derive(Parser, Debug)]
#[enum_dispatch(Runnable)]
#[enum_dispatch(Runnable,)]
pub enum ArgsEnum {
Uuid(UuidArgs),
Hash(HashArgs),
#[command(visible_alias="ts")]
Timestamp(TimeStampArgs),
}

pub use args::run;

0 comments on commit 4054e3f

Please sign in to comment.