Skip to content

feat(DCP-2580): unified output format flags#393

Merged
cemprolific merged 10 commits intomainfrom
DCP-2580-Global-Output-Flag
Apr 1, 2026
Merged

feat(DCP-2580): unified output format flags#393
cemprolific merged 10 commits intomainfrom
DCP-2580-Global-Output-Flag

Conversation

@cemprolific
Copy link
Copy Markdown
Contributor

@cemprolific cemprolific commented Mar 27, 2026

New shared formatting flags

  • Creates --table/-t, --csv/-c, --json/-j to use with list commands
  • ResolveFormat applies priority order: json > csv > table > auto
  • --non-interactive/-n is kept as a hidden alias for --table for backwards compatibility
  • Generic TableRenderer[T], CsvRenderer[T], and JSONRenderer[T] in ui/renderers.go replace per-resource renderer implementations

formatting aligns with gh cli usage

$ prolific study list --json
$ prolific study list -j

study list migrated

  • Replaces ad-hoc ListStrategy/ListRenderer/NonInteractiveRenderer/CsvRenderer in ui/study/list.go with the shared generic renderers
  • cmd/study/list.go now resolves the output format and owns the default fields (ID,Name,Status)
  • Added --json/-j output support (previously missing for studies)
  • Updated command examples to document all three output flags

New TUI view for submission list command

  • Same as the other commands, adds a TUI based list view for submission

ui package is only for shared ui functions

  • Moves all the command specific views functions into command packages.

Tests

  • Renderer tests moved from ui/study/list_test.go to ui/renderers_test.go and updated to use the generic renderers directly

@prolific-snyk
Copy link
Copy Markdown

prolific-snyk commented Mar 27, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

praise: Nice

Copy link
Copy Markdown
Contributor

@script-this script-this left a comment

Choose a reason for hiding this comment

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

Looking good 🙏 - just a couple of minor points to address.

@@ -1,214 +1 @@
package study_test
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Think this file can go completely now..?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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, " ")))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 {
Copy link
Copy Markdown
Contributor

@script-this script-this Mar 30, 2026

Choose a reason for hiding this comment

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

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.

@cemprolific cemprolific marked this pull request as ready for review March 30, 2026 12:29
@cemprolific cemprolific requested review from a team as code owners March 30, 2026 12:29
Copilot AI review requested due to automatic review settings March 30, 2026 12:29
@cemprolific cemprolific added team-dct dct-ready-for-review Triggers Slack notification when PR is ready for review labels Mar 30, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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/-n kept as a hidden alias for --table.
  • Introduced generic TableRenderer[T], CsvRenderer[T], and JSONRenderer[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.

Comment on lines +131 to +135
default:
r := &studyui.InteractiveRenderer{}
if err := r.Render(client, *studies, w); err != nil {
return fmt.Errorf("error: %s", err)
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +110
default:
r := &collectionui.InteractiveRenderer{}
if err := r.Render(c, *collections, w); err != nil {
return fmt.Errorf("error: %s", err)
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +37
// 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 ""
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +35
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)
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +62
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)
}
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +80
// 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)
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot generated this review using guidance from repository custom instructions.
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.")
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
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.")

Copilot uses AI. Check for mistakes.
fields := opts.Fields
if fields == "" {
fields = defaultListFields
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion (optional): Wondering if we can have a shared helper somehow that means we can drop the format switch from each list command.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@script-this
Copy link
Copy Markdown
Contributor

Couple of observations (may be intentional):

  1. Submission default case I think is table not interactive where as collection/study is interactive. May have been like that before but probably a good time to unify.
  2. Old code rendered record count for submissions, new code only show it for table case. Just wanted to check you were aware (likely result of standardisation).

@cemprolific
Copy link
Copy Markdown
Contributor Author

cemprolific commented Apr 1, 2026

Couple of observations (may be intentional):

  1. Submission default case I think is table not interactive where as collection/study is interactive. May have been like that before but probably a good time to unify.
  2. Old code rendered record count for submissions, new code only show it for table case. Just wanted to check you were aware (likely result of standardisation).
  • Created a TUI renderer for submissions.
  • Submissions count added into the csv output

@cemprolific cemprolific merged commit 18ced0b into main Apr 1, 2026
5 checks passed
@cemprolific cemprolific deleted the DCP-2580-Global-Output-Flag branch April 1, 2026 10:33
@script-this script-this linked an issue Apr 2, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dct-ready-for-review Triggers Slack notification when PR is ready for review team-dct

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Global Output Flag (structured JSON by default, GH CLI standard)

5 participants