-
Notifications
You must be signed in to change notification settings - Fork 19
Add option for tabular output #1167
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
Conversation
9ef7b5d
to
d005c78
Compare
Currently all commands will display their results as pretty-printed JSON. Tabular output is convenient for easy comparison of a single field when multiple objects are returned, and more amenable to traditional shell processing. Add a new global `--format` flag to control output format, defaulting to the existing JSON output. As larger output items, such as instances, can easily overflow a typical terminal width, we allow users to specify which fields to print with `--format=table:field1,field2,...`. Non-scalar fields within a returned object will be printed in compact JSON format, e.g. `{"cpus":0,"memory":0,"storage":0}` for the `allocated` field on `oxide silo utilization list`. To determine the field names to be shown in the table header, we parse the schema for the return type as part of `OxideOverride`. This logic makes some assumptions about the shape of the data returned, and we need to ensure that it remains valid. Add a new return_types `xtask` job, which writes out all return types from the Oxide API to a file, against which we test the parsing logic.
d005c78
to
bc5cd9b
Compare
I manually verified this works against all |
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 it would be helpful to have an overview of how this works.
Obviously I had designed the overrides to enable this work, but this isn't how I expected it to be implemented.
xtask/src/main.rs
Outdated
} | ||
|
||
if return_types { | ||
print!("generating return types ... "); |
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 may require additional explanation
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 longer printed now that return types are generated in sdk
.
xtask/src/main.rs
Outdated
std::io::stdout().flush().unwrap(); | ||
let mut ret_types = BTreeSet::new(); | ||
|
||
for path in spec.paths.paths.values().cloned() { |
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 approach seems brittle. Did you consider using syn
to parse the generated sdk:
for types in mod builder {
look for an `fn send`
take its response type
}
You could make this part of the "generate SDK" potentially if you're worried about input and output getting out of sync.
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's a little more verbose to use syn
, but I agree that it's less likely to blow up in the future. I've also moved this into the sdk
step as suggested.
cli/src/oxide_override.rs
Outdated
let printable_fields = set_header_fields(&fields, available_fields, &mut table); | ||
|
||
let serde_json::Value::Object(obj) = | ||
serde_json::to_value(std::ops::Deref::deref(value)) |
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.
?
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.
We need a JSON object to access the fields expected from the schema.
Does that answer your question?
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.
Sorry: try value.as_ref()
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.
Ah ok, makes sense.
cli/src/oxide_override.rs
Outdated
} | ||
|
||
// Store our list of fields to print. | ||
std::mem::swap(&mut printable_fields, &mut fields); |
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 follow why you're doing this
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've refactored the table logic into a new TableFormatter
struct. I think this easier to follow.
Added a comment on this in 0dd9137
What was your plan? |
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.
$ cargo run -- --format table:t project list
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
Running `target/debug/oxide --format 'table:t' project list`
WARNING: 644 permissions on "/Users/ahl/.config/oxide/credentials.toml" may allow other users to access your login credentials.
WARNING: 't' is not a valid field
table formatting is not supported for this command
One of the things I'm struggling with here is how it this option will be used outside of a demo context. For example, --format table
doesn't seem very usable on its own, so I need to choose specific columns. But how do I know what those are? What's a typical workflow? Would we imagine people embedding these lists .. in scripts? What happens when a new and important field is added to the output.
|
||
let schemas = return_types.into_iter().map(|ty| { | ||
quote! { | ||
(stringify!(oxide::#ty), schemars::schema_for!(oxide::#ty)) |
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 stringify!
rather than making it a string here? I guess they're equivalent, but curious about the decision.
let schemas = return_types.into_iter().map(|ty| { | ||
quote! { | ||
(stringify!(oxide::#ty), schemars::schema_for!(oxide::#ty)) | ||
} | ||
}); | ||
|
||
quote! { | ||
pub fn schemas() -> Vec<(&'static str, schemars::schema::RootSchema)> { | ||
vec![ | ||
#(#schemas),* | ||
] | ||
} | ||
} |
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.
Also, I think you could inline this if you wanted:
quote! {
pub fn schemas() -> ... {
vec![
#( stringify!(oxide:: #return_types), schemars::schema_for!(oxide:: #return_types)),*
]
}
}
_ => None, | ||
}) { | ||
if let ReturnType::Type(_, ty) = &method.sig.output { | ||
if let Some(return_type) = extract_response_value_inner_type(ty) { |
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 this fails we ignore it? Is that intended?
/// Extract the Oxide success type returned by `send`. | ||
/// For example, `types::Vpc` in `Result<ResponseValue<types::Vpc>, 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.
I think more words here would be helpful
if available_fields.is_empty() { | ||
println_nopipe!("{TABLE_NOT_SUPPORTED}"); | ||
return; | ||
} |
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 what situation will this occur?
} | ||
}; | ||
|
||
// Check if we're receiving an array. |
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 seems to do the opposite i.e. it checks if we're not receiving an array.
} | ||
|
||
/// Find the fields present on a the object returned by `success_item`. | ||
fn success_item_fields(root_schema: &RootSchema) -> (Vec<String>, ReturnType) { |
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 concerned that this function seems fairly brittle. Is the idea that it will be mitigated by the tests?
include!("../tests/data/api_return_types.rs"); | ||
} | ||
for (type_name, schema) in schemas::schemas() { | ||
let fields = if type_name.ends_with("Page") { |
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 we articulate this heuristic?
use super::*; | ||
|
||
#[test] | ||
fn test_table_schema_parsing() { |
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.
My fear is that this test is going to break frequently as a result of unexpected schema data. Is that a rational fear or do you expect that this will be fairly robust?
@@ -0,0 +1,600 @@ | |||
// This Source Code Form is subject to the terms of the Mozilla Public |
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 would be easier to review this if we first move override to its own file.
Closing in favor of #1221 |
Currently all commands will display their results as pretty-printed JSON. Tabular output is convenient for easy comparison of a single field when multiple objects are returned, and more amenable to traditional shell processing.
Add a new global
--format
flag to control output format, defaulting to the existing JSON output. As larger output items, such as instances, can easily overflow a typical terminal width, we allow users to specify which fields to print with--format=table:field1,field2,...
.Non-scalar fields within a returned object will be printed in compact JSON format, e.g.
{"cpus":0,"memory":0,"storage":0}
for theallocated
field onoxide silo utilization list
.To determine the field names to be shown in the table header, we parse the schema for the return type as part of
OxideOverride
. This logic makes some assumptions about the shape of the data returned, and we need to ensure that it remains valid. Add a new return_typesxtask
job, which writes out all return types from the Oxide API to a file, against which we test the parsing logic.