Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/timestamp #2

Merged
merged 7 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;