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

Add initial parsec-tool implementation #1

Merged
merged 1 commit into from
Aug 5, 2020

Conversation

joechrisellis
Copy link
Contributor

Hi guys, this is the initial parsec-tool implementation from issue 202 in parallaxsecond/parsec.

Any and all comments welcome and appreciated! :)

Copy link
Member

@ionut-arm ionut-arm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for seeding this!! I've left a few comments below.

Another question is how we test this. I would've said some pure CLI tests are in order - you could run them and redirect the output to two files (stdout and stderr) and them compare with some predefined values. Doesn't track the colours and it's a bit primitive. Alternatively, you can try and mock the client communication (like we do in the client tests) and use something like insta to check what you then print.

src/output.rs Outdated
@@ -0,0 +1,112 @@
// Copyright 2020 Contributors to the Parsec project.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you looked at libraries to handle the colour/style? Like mowl. Or did you want to avoid using log so as not to print anything but what you choose to, the way you want it to look?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a look around for some libraries to handle logging/style but I couldn't find anything that was as flexible as ansi_term, which is what I've opted for here. Logging felt a little bit extreme, since we're not really 'logging' as much as we are just writing to std{out,err}. Happy to change this if you think it's a better long term solution though!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this makes sense, it's not really logging in the end :) I think I was mostly bothered by the thought of having to set the colour for each line that we write, feels a bit extreme.

src/cli.rs Outdated Show resolved Hide resolved
src/command.rs Outdated
/// Gets the list of availble opcodes for a given provider and outputs them.
fn list_opcodes(
provider: &str,
_command_line_context: &CommandLineContext,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say, for all these functions, if you don't need this argument you shouldn't really pass it in/request it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, will remove.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I'm not so sure about this -- in the future it's likely that we'll want to show more/less information depending on the verbosity, which is encoded in the command line context. I think this is a good thing to keep around, even if we're not using it right now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

src/command.rs Outdated
print_colored!(Cyan, "0x{:02x} ({})", provider.id as u32, provider.id);
println!("]");

print_colored!(Yellow, "Description");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you keep this system for colouring output, you should probably write down somewhere what each colour represents (for example, why the details here are yellow - I would've assumed yellow is more of a warning colour).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not saying yellow is wrong, btw, just that it should be clear what the colours are used for

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea -- so far, the format is:

  • Yellow to represent field names -- i.e. 'description', 'uuid'.
  • Cyan for titles.
  • Blue for information.
  • Red for errors (and warnings?).
  • Green for success messages.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we agree on a list like this, would it make sense to make separate macros for each of these to handle both the colour and the printing inside the macro? So for example, instead of

print_colored!(Yellow, "Description");
println!(": {}", provider.description);

you'd get the same result via

print_field!("Description", "{}", provider.description);

Apologies if this is a bit over the top 😅

src/output.rs Outdated Show resolved Hide resolved
Copy link
Member

@hug-dev hug-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for setting up the bases of the Parsec Tool, that will be very helpful.

I left some comments below. Generally, I can see that you are using the BasicClient from the Parsec Client, which is the obvious, straightforward choice. I was thinking though if using the OperationClient could be a good idea as well. If we find ways to:

  • generate the correct commands, arguments and text from the Operation structures (maybe using procedural macros)
  • format and print the Result structures in the same structured way, with a color scheme (similar to what you have done)

then we can simply call the OperationClient to execute the operation and it would be very easy to add new operations to the tool in the future, with very little work.
What do you think? I am happy to merge this PR with your current idea but tackle this one in a next one.

Cargo.toml Outdated
Comment on lines 15 to 24
ansi_term = "0.12"
anyhow = "1.0.31"
atty = "0.2"
clap = "3.0.0-beta.1"
parsec-client = "0.7.1"
thiserror = "1.0"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please also add those dependencies in the README.md file? You can take example of our other repos.

src/main.rs Outdated
@@ -33,6 +33,26 @@

//! Parsec Tool: a tool to communicate with Parsec from the command-line

#[macro_use]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use:

use output::e_err;

instead of macro_use attribute? I think it is clearer to spot which macros come where as for static functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so -- the purpose of this line is to make explicitly make the output module available in the crate. If we want to use its macros, I think macro_use is necessary. 🙁

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like:

mod output;
use output::e_err;

?

src/main.rs Outdated
Comment on lines 37 to 42
mod output;

mod cli;
mod command;
mod common;
mod error;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to discussion, but I think it would be better to export those modules from a src/lib.rs file. That way documentation can be generated for those on docs.rs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you intend that documentation for users of the tool or for its developers?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For developpers only, the library crate exposed should probably not be used.

src/main.rs Outdated
use crate::command::dispatch_command;
use anyhow::{Context, Result};

fn run() -> Result<bool> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the bool result actually used? If not, this function can maybe return Result<()>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not yet. The idea was going to be:

  • Return Err on complete failure.
  • Return Ok(true) on success with partial failure.
  • Return Ok(false) on complete success.

But since we aren't using this right now, I agree -- better to just remove it.

src/cli.rs Outdated
Arg::with_name("provider")
.short('p')
.takes_value(true)
.required(true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the p argument be not required and if not passed, the list_opcodes command would execute on the default provider (the first one returned by list_providers)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, will change this.

src/cli.rs Outdated
} else {
unreachable!()
},
app_name: match matches.value_of("appname") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is appname one of the possible option? I don't see it in matches

src/cli.rs Outdated
use clap::{App, AppSettings, Arg};

#[derive(Debug)]
pub enum Action {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, Action should probably be replaced by NativeOperation to not have to duplicate similar information. This module would create the correct instance of NativeOperation from the command entered by the user.
Having an automatic translation (as a procedural macro maybe?) of each of the Operation into command line arguments for clap would be phenomenal.

Copy link
Member

@ionut-arm ionut-arm Jul 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having an automatic translation (as a procedural macro maybe?) of each of the Operation into command line arguments for clap would be phenomenal.

I don't think that's a good idea for some of the operations at least, especially the ones that take very rich data structures. Imagine having to write the whole KeyAttributes structure on the command line. It's probably more sensible to expect stuff like that to be written in a TOML file or something similar, and we parse it from there.

Hence, I'm not sure just automating the whole translation process makes that much sense, unless we can specify, for example, which parameters of a command should be taken from files, and how the file should be converted to the parameter in the first place. Even for "very simple" data like a file that you want to encrypt, it's probably more reasonable to let the user decide if they want to pipe the contents through the CLI or have an option to select the file based on its path (or potentially write a signature for it in a different file, for example, instead of just blindly writing stuff to stdout).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, I did not think about KeyAttributes, that would be very painful to write all down. Maybe more generally the CLI should limit itself to the highest abstraction level of the client? When we have features for parameters discovery and simple abstraction over complicated operations, then the CLI can have commands over that.

src/output.rs Outdated
// - Cyan should be used for section titles.
// - Yellow should represent field names -- i.e. UUIDs, versions, etc.
//
// By sticking to this guideline we ensure that the CLI stays consistent.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with Ionut that it would be nice to have macros mapping with each one of those colours to be consistent everywhere. Also, it would maybe be nice to not have the macro_export but let the module where they are needed import them directly like static functions.

src/output.rs Outdated

/// Write an info message to stdout.
#[macro_export]
macro_rules! info {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the verbosity level argument coming into play for the info, warn and err macros? Would it be possible to use a logger like env_var directly? I think it also handles colors.

@joechrisellis
Copy link
Contributor Author

joechrisellis commented Jul 30, 2020

Hi guys. I've addressed your comments as well as done some further work on this. The initial implementation I have now is quite different to what is shown in this patch. Do you want me to close this PR and open a new one or would you rather I just push the new stuff here?

@ionut-arm
Copy link
Member

Hi guys. I've addressed your comments as well as done some further work on this. The initial implementation I have now is quite different to what is shown in this patch. Do you want me to close this PR and open a new one or would you rather I just push the new stuff here?

Push it over this one, if someone asks why some things are the way they are, we can point them to this PR and it'll have the full convo.

@hug-dev
Copy link
Member

hug-dev commented Jul 30, 2020

Is everything changing in the new version? Would it make sense for you to first apply the changes requested here, merge this PR and then open a new one with the new changes?

Maybe it does not make sense if the comments we made are not relevent anymore.

(saying that so that it might be easier to review)

@joechrisellis
Copy link
Contributor Author

@hug-dev a lot has changed -- I think I will just re-push the code here just to keep the conversation going. 🙂

@joechrisellis joechrisellis force-pushed the master branch 4 times, most recently from 3ebef14 to 895402a Compare July 30, 2020 15:32
Copy link
Member

@ionut-arm ionut-arm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the hard work on rewriting this! I really like the new structuring, it's quite smooth 😎 I did have some comments on how the components link together

src/main.rs Outdated
Comment on lines 16 to 18
dispatch_subcommand(matches).context("Executing subcommand failed.")?;
Ok(())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can shorten this to

dispatch_subcommand(matches).context("Executing subcommand failed.")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good spot! I think I'll leave this as-is, though -- to me, the more explicit use of Ok(()) is more readable. If you have strong opinions in this I'm happy to change though. 😄

#[structopt(name = "list_providers")]
pub struct ListProvidersSubcommand {}

impl TryInto<NativeOperation> for ListProvidersSubcommand {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This'll be a bit of a churn, but instead of TryInto you should implement TryFrom (same for From and Into - you should generally implement only the first one directly). From the TryInto docs:

Library authors should usually not directly implement this trait, but should prefer implementing the TryFrom trait, which offers greater flexibility and provides an equivalent TryInto implementation for free, thanks to a blanket implementation in the standard library. For more information on this, see the documentation for Into.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is not to say that the pub trait ParsecToolSubcommand: StructOpt + TryInto<NativeOperation> should change, that one definitely makes sense)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, good spot!

/// effectively a `FromStr` implementation for AuthenticationData. Passing in `None` will return
/// AuthenticationData::None. Passing in `Some(s)` will give you an app identity whose secret is
/// built from the string `s`.
pub fn make_authentication_data(auth_secret: Option<String>) -> AuthenticationData {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be implemented as a method on ParsecToolApp - you're not expecting to have to parse some other value as an AuthenticationData entity, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite right! Done. :)


/// Runs the appropriate command given `matches`, which corresponds to the command-line invocation
/// from the user.
pub fn dispatch_subcommand(matches: ParsecToolApp) -> Result<(), ParsecToolError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the previous comment, this can be implemented as a method on Subcommand, or maybe even on ParsecToolApp, depending on how you want to call it in main().

We've generally avoided having "hanging" functions if we can implement them on some type where it makes sense for them to be attached. Sometimes that's not possible, though, either because the types are external so we literally cannot implement on them, or because we need multiple implementations for different things, so we then use functions.

/// - They implement `run`, which executes the subcommand.
pub trait ParsecToolSubcommand: StructOpt + TryInto<NativeOperation> {
/// Run the subcommand.
fn run(self, matches: ParsecToolApp) -> Result<(), ParsecToolError>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably split that ParsecToolApp into Subcommand and Parameters or Options or something like that, where you group all the other parts. And then in this method you pass a reference to that other struct, it feels fishy to give to a subcommand ownership over the structure that presumably holds it? I'm assuming this works because Subcommand has Copy on it

@joechrisellis joechrisellis force-pushed the master branch 2 times, most recently from 0874cdd to fa54c1d Compare August 3, 2020 16:49
Copy link
Member

@ionut-arm ionut-arm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 👌

@joechrisellis joechrisellis marked this pull request as ready for review August 4, 2020 10:39

/// Struct representing the command-line interface of parsec-tool.
#[derive(Debug, StructOpt)]
#[structopt(name=PROJECT_NAME, about=PROJECT_DESC, author=PROJECT_AUTHOR, version=PROJECT_VERSION)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does structopt not use these fields from the Cargo.toml by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, but doesn't seem to do it for the authors, so it makes sense to just leave them all there IMO. 🙂

src/lib.rs Outdated
#![deny(
nonstandard_style,
const_err,
// dead_code,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this one commented? And unused as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a debugging artifact, my bad.

src/cli/mod.rs Outdated
/// effectively a `FromStr` implementation for AuthenticationData. Passing in `None` will return
/// AuthenticationData::None. Passing in `Some(s)` will give you an app identity whose secret is
/// built from the string `s`.
pub fn make_authentication_data(&self) -> AuthenticationData {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the end this is similar to a getter, right? Doing also a conversion, maybe it could be named authentication_data directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, done.

//! Error definitions/handling.

use parsec_interface::operations::NativeResult;
use thiserror::Error;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's neat, the thiserror crate 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is! 😄

pub mod cli;
pub mod common;
pub mod error;
pub mod subcommands;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the tree structure you choosed, with subcommands as a module!

Comment on lines +25 to +34
impl TryFrom<ListOpcodesSubcommand> for NativeOperation {
type Error = ParsecToolError;

fn try_from(list_opcodes_subcommand: ListOpcodesSubcommand) -> Result<Self, Self::Error> {
// Trivially converted to a `NativeOperation`.
Ok(NativeOperation::ListOpcodes(list_opcodes::Operation {
provider_id: ProviderID::try_from(list_opcodes_subcommand.provider_opts.provider)?,
}))
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! It's a very good idea to use the OperationClient here. Also each command will make its own choice of either using the basic client or the operation client 👌

pub struct ParsecToolApp {
/// How verbose should we be?
#[structopt(short = "v", multiple = true, default_value = "0")]
pub verbosity: u8,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not seem to be used in the PR, are you going to implement it later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was thinking that verbosity would be useful to control the level of output for some of the more complex operations in the future. It's unused at the moment though. 🙂

Comment on lines 73 to 62
macro_rules! info {
($($arg:tt)*) => {
$crate::print_colored!(Blue, "[INFO] ");
println!($($arg)*);
};
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only keep one version of the macros, choosing only to emit logs to stdout or stderr. I would say that here we can simply log everything to stderr. If we want to have the choice, it might be best to have a configuration option to choose between stdout or stderr but for simplicity I think it's best to just log everything to stderr.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On that note, maybe we can delete those macros and directly use the one from log with the env_logger implementor? The verbosity flag could set the log level and I am pretty sure that this crate handles colouring as well 💯 It's also customizable to make it look nice for the command line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll get rid of the stdout stuff!

I think using the env_logger module is a good idea, but I think I'll implement this in a future commit if that's alright with you? It might require some further change throughout this repo. 😛

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure that's fine!


impl ParsecToolApp {
/// Run the requested subcommand.
pub fn dispatch_subcommand(&self) -> Result<(), ParsecToolError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking here but in a future version, if that is ok, Subcommand itself could implement the ParsecToolSubcommand trait so that you could call run on it directly from main without needing this method.

@hug-dev hug-dev merged commit 6c562b2 into parallaxsecond:master Aug 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants