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 |
ahl
left a comment
There was a problem hiding this comment.
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.
this may require additional explanation
There was a problem hiding this comment.
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.
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.
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.
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.
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.
I don't follow why you're doing this
There was a problem hiding this comment.
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? |
ahl
left a comment
There was a problem hiding this comment.
$ 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 commandOne 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.
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.
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.
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.
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.
in what situation will this occur?
| } | ||
| }; | ||
|
|
||
| // Check if we're receiving an array. |
There was a problem hiding this comment.
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.
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.
can we articulate this heuristic?
| use super::*; | ||
|
|
||
| #[test] | ||
| fn test_table_schema_parsing() { |
There was a problem hiding this comment.
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.
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
--formatflag 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 theallocatedfield 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_typesxtaskjob, which writes out all return types from the Oxide API to a file, against which we test the parsing logic.