feat(DCP-2580): unified output format flags#393
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
| } | ||
|
|
||
| // AddOutputFlags registers --json / -j, --table / -t, --csv / -c on the given command. | ||
| // --non-interactive / -n is registered as a hidden alias for --table for backwards compatibility. |
script-this
left a comment
There was a problem hiding this comment.
Looking good 🙏 - just a couple of minor points to address.
ui/study/list_test.go
Outdated
| @@ -1,214 +1 @@ | |||
| package study_test | |||
There was a problem hiding this comment.
Think this file can go completely now..?
There was a problem hiding this comment.
There is a bit of inconsistency in the location of command specific renderers. Most of the commands have renderers in cmd/ dir, but there are also command specific renderers in the ui package. Does it make sense to move command specific renderers to their own packages and keep the ui only for shared components?
There was a problem hiding this comment.
Good question, have no strong opinions here other than what makes the most sense.
ui/renderers.go
Outdated
|
|
||
| for _, item := range items { | ||
| for _, field := range fieldList { | ||
| fmt.Fprintf(tw, "%v\t", reflect.ValueOf(item).FieldByName(strings.Trim(field, " "))) |
There was a problem hiding this comment.
What happens if field doesn't exist here..?
Might be good to add some validation.
| type CsvRenderer[T any] struct{} | ||
|
|
||
| // Render writes items as CSV to w. Values containing commas are wrapped in double quotes. | ||
| func (r CsvRenderer[T]) Render(items []T, fields string, w io.Writer) error { |
There was a problem hiding this comment.
Think we should probably be using Go's encoding/csv here so it's catches all the edge cases around quoting, escaping and general compliance.
There was a problem hiding this comment.
Pull request overview
This PR standardizes output formatting for list commands across the CLI by introducing shared --table/-t, --csv/-c, and --json/-j flags and consolidating rendering into generic UI renderers.
Changes:
- Added shared output-format flags (
--table/--csv/--json) with--non-interactive/-nkept as a hidden alias for--table. - Introduced generic
TableRenderer[T],CsvRenderer[T], andJSONRenderer[T]and migrated list commands to use them. - Moved/updated renderer tests to target the new shared renderers and adjusted CSV expectations.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
cmd/shared/list_format_flags.go |
Adds shared output flags and a format resolver used by list commands. |
ui/renderers.go |
Implements shared generic table/CSV/JSON renderers using reflection. |
ui/renderers_test.go |
Adds tests for shared renderers (currently CSV + table). |
cmd/study/list.go |
Migrates study listing to shared output flags/renderers and supports JSON output. |
cmd/study/list_test.go |
Updates CSV output expectation to match encoding/csv output. |
cmd/collection/list.go |
Migrates collection listing to shared output flags/renderers and supports JSON output. |
cmd/submission/list.go |
Migrates submission listing to shared output flags/renderers and adds JSON output support. |
ui/study/list.go |
Removes per-resource non-interactive/CSV rendering, leaving interactive renderer. |
ui/collection/list.go |
Removes per-resource non-interactive/CSV/JSON rendering, leaving interactive renderer. |
ui/study/list_test.go |
Removes study-specific renderer tests (superseded by shared renderer tests). |
ui/collection/list_test.go |
Removes collection-specific renderer tests (superseded by shared renderer tests). |
ui/submission/list.go |
Removes submission-specific renderers (now handled in cmd + shared renderers). |
ui/submission/list_test.go |
Removes submission-specific renderer tests. |
| default: | ||
| r := &studyui.InteractiveRenderer{} | ||
| if err := r.Render(client, *studies, w); err != nil { | ||
| return fmt.Errorf("error: %s", err) | ||
| } |
There was a problem hiding this comment.
The default (auto) format path always launches the Bubble Tea interactive UI. This conflicts with the documented intent of auto mode (TUI if TTY, else table) and can break scripted usage when stdout is piped/non-TTY. Consider detecting whether output is a terminal and falling back to table output (or requiring an explicit format) when not on a TTY.
| default: | ||
| r := &collectionui.InteractiveRenderer{} | ||
| if err := r.Render(c, *collections, w); err != nil { | ||
| return fmt.Errorf("error: %s", err) | ||
| } |
There was a problem hiding this comment.
The default (auto) format path always launches the Bubble Tea interactive UI. This conflicts with the documented intent of auto mode (TUI if TTY, else table) and can break scripted usage when stdout is piped/non-TTY. Consider detecting whether output is a terminal and falling back to table output when not on a TTY.
| // ResolveFormat returns the resolved format string based on the flags set. | ||
| // Priority: json > csv > table. Returns "" to indicate auto (TUI if TTY, else table). | ||
| func ResolveFormat(opts OutputOptions) string { | ||
| switch { | ||
| case opts.Json: | ||
| return "json" | ||
| case opts.Csv: | ||
| return "csv" | ||
| case opts.Table: | ||
| return "table" | ||
| default: | ||
| return "" | ||
| } |
There was a problem hiding this comment.
ResolveFormat's docstring says the default ("auto") behavior is "TUI if TTY, else table", but ResolveFormat only returns "" and does not (and cannot, as written) inspect TTY state. Either implement the TTY-based auto-resolution in shared (e.g., by passing an *os.File / io.Writer context) or adjust the comment so callers don't assume this behavior is already handled here.
| for _, item := range items { | ||
| for _, field := range fieldList { | ||
| v := reflect.ValueOf(item).FieldByName(strings.Trim(field, " ")) | ||
| if !v.IsValid() { | ||
| fmt.Fprintf(tw, "\t") | ||
| continue | ||
| } | ||
| fmt.Fprintf(tw, "%v\t", v) | ||
| } |
There was a problem hiding this comment.
TableRenderer uses reflect.ValueOf(item).FieldByName(...) directly. If callers pass pointers (e.g., []*model.Study) or a non-struct type, FieldByName will panic. Since this is a generic, reusable renderer, consider using reflect.Indirect and validating Kind()==Struct (returning a clear error instead of panicking) to make it safe for broader use.
| for _, item := range items { | ||
| row := make([]string, len(fieldList)) | ||
| for i, field := range fieldList { | ||
| v := reflect.ValueOf(item).FieldByName(field) | ||
| if v.IsValid() { | ||
| row[i] = fmt.Sprintf("%v", v) | ||
| } | ||
| } |
There was a problem hiding this comment.
CsvRenderer uses reflect.ValueOf(item).FieldByName(field) directly. If callers pass pointers (e.g., []*model.Collection) or a non-struct type, this will panic at runtime. Consider using reflect.Indirect and validating the value is a struct (and returning an error) before accessing fields.
| // JSONRenderer renders a slice of items as indented JSON. | ||
| type JSONRenderer[T any] struct{} | ||
|
|
||
| // Render writes items as indented JSON to w. | ||
| func (r JSONRenderer[T]) Render(items []T, w io.Writer) error { | ||
| encoder := json.NewEncoder(w) | ||
| encoder.SetIndent("", " ") | ||
| return encoder.Encode(items) | ||
| } |
There was a problem hiding this comment.
JSONRenderer is new shared functionality used by list commands, but ui/renderers_test.go currently only exercises the CSV and table renderers. Add a unit test that renders JSON and verifies the output is valid JSON (and matches expected structure) to prevent regressions.
cmd/study/list.go
Outdated
| flags.BoolVarP(&opts.NonInteractive, "non-interactive", "n", false, "Render the list details straight to the terminal.") | ||
| flags.BoolVarP(&opts.Csv, "csv", "c", false, "Render the list details in a CSV format.") | ||
| flags.BoolVarP(&opts.Underpaying, "underpaying", "u", false, "Filter by underpaying studies.") | ||
| flags.StringVarP(&opts.Fields, "fields", "f", "", "Comma separated list of fields you want to display in non-interactive/csv mode.") |
There was a problem hiding this comment.
The --fields flag help text still references "non-interactive" mode, but this command now uses the unified output flags (--table/--csv/--json) and the non-interactive flag is only a hidden alias for --table. Update the help text to avoid confusing users (e.g., reference table/CSV output instead).
| flags.StringVarP(&opts.Fields, "fields", "f", "", "Comma separated list of fields you want to display in non-interactive/csv mode.") | |
| flags.StringVarP(&opts.Fields, "fields", "f", "", "Comma separated list of fields to display for table or CSV output.") |
| fields := opts.Fields | ||
| if fields == "" { | ||
| fields = defaultListFields | ||
| } |
There was a problem hiding this comment.
Suggestion (optional): Wondering if we can have a shared helper somehow that means we can drop the format switch from each list command.
There was a problem hiding this comment.
I thought the same. However, if we want to customise the renderer for a specific command, this approach gives us the flexibility to use a custom one. So, let's leave it as is for now. We can make a small refactoring later if we want to generalise it.
|
Couple of observations (may be intentional):
|
|
New shared formatting flags
--table/-t,--csv/-c,--json/-jto use withlistcommands--non-interactive/-nis kept as a hidden alias for--tablefor backwards compatibilityTableRenderer[T],CsvRenderer[T], andJSONRenderer[T]in ui/renderers.go replace per-resource renderer implementationsformatting aligns with gh cli usage
study list migrated
cmd/study/list.gonow resolves the output format and owns the default fields (ID,Name,Status)--json/-joutput support (previously missing for studies)New TUI view for submission list command
submissionui package is only for shared ui functions
Tests