-
Notifications
You must be signed in to change notification settings - Fork 19
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
Conversation
There was a problem hiding this 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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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/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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, will remove.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 😅
There was a problem hiding this 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
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" | ||
|
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. 🙁
There was a problem hiding this comment.
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
mod output; | ||
|
||
mod cli; | ||
mod command; | ||
mod common; | ||
mod error; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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<()>
.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
)?
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 forclap
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).
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
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. |
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) |
@hug-dev a lot has changed -- I think I will just re-push the code here just to keep the conversation going. 🙂 |
3ebef14
to
895402a
Compare
There was a problem hiding this 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
dispatch_subcommand(matches).context("Executing subcommand failed.")?; | ||
Ok(()) |
There was a problem hiding this comment.
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.")
There was a problem hiding this comment.
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. 😄
src/subcommands/list_providers.rs
Outdated
#[structopt(name = "list_providers")] | ||
pub struct ListProvidersSubcommand {} | ||
|
||
impl TryInto<NativeOperation> for ListProvidersSubcommand { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, good spot!
src/subcommands/common.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(auth_secret: Option<String>) -> AuthenticationData { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quite right! Done. :)
src/subcommands/mod.rs
Outdated
|
||
/// Runs the appropriate command given `matches`, which corresponds to the command-line invocation | ||
/// from the user. | ||
pub fn dispatch_subcommand(matches: ParsecToolApp) -> Result<(), ParsecToolError> { |
There was a problem hiding this comment.
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.
src/subcommands/mod.rs
Outdated
/// - They implement `run`, which executes the subcommand. | ||
pub trait ParsecToolSubcommand: StructOpt + TryInto<NativeOperation> { | ||
/// Run the subcommand. | ||
fn run(self, matches: ParsecToolApp) -> Result<(), ParsecToolError>; |
There was a problem hiding this comment.
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
0874cdd
to
fa54c1d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! 👌
|
||
/// 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)] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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!
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)?, | ||
})) | ||
} | ||
} |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. 🙂
src/util/output.rs
Outdated
macro_rules! info { | ||
($($arg:tt)*) => { | ||
$crate::print_colored!(Blue, "[INFO] "); | ||
println!($($arg)*); | ||
}; | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. 😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure that's fine!
Signed-off-by: Joe Ellis <[email protected]>
|
||
impl ParsecToolApp { | ||
/// Run the requested subcommand. | ||
pub fn dispatch_subcommand(&self) -> Result<(), ParsecToolError> { |
There was a problem hiding this comment.
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.
Hi guys, this is the initial parsec-tool implementation from issue 202 in parallaxsecond/parsec.
Any and all comments welcome and appreciated! :)