mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
CLI: Flatten config options output when using the "--json" flag #5220
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -192,6 +192,10 @@ If anything in this file conflicts with the `Makefile` or the Developer Guide, t
|
||||
|
||||
- Capture output with `RunWithTestContext`; usage and report values may be quoted and re‑ordered (e.g., set semantics). Use substring checks or regex for the final ", or <last>" rule from `CliUsageString`.
|
||||
- Prefer JSON output (`--json`) for stable machine assertions when commands offer it.
|
||||
- JSON shapes for `show` commands:
|
||||
- Most return a top‑level array of row objects (keys = snake_case columns).
|
||||
- `photoprism show config` returns `{ sections: [{ title, items[] }] }`.
|
||||
- `photoprism show config-options --json` and `photoprism show config-yaml --json` return a flat top‑level array (no `sections`).
|
||||
|
||||
### API Development & Config Options
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@ Configuration & Flags
|
||||
- If needed: `yaml:"-"` disables YAML processing; `flag:"-"` prevents `ApplyCliContext()` from assigning CLI values (flags/env variables) to a field, without affecting the flags in `internal/config/flags.go`.
|
||||
- Annotations may include edition tags like `tags:"plus,pro"` to control visibility (see `internal/config/options_report.go` logic).
|
||||
- Global flags/env: `internal/config/flags.go` (`EnvVars(...)`)
|
||||
- Available flags/env: `internal/config/cli_flags_report.go` + `internal/config/report_sections.go` → surfaced by `photoprism show config-options --md`
|
||||
- YAML options mapping: `internal/config/options_report.go` + `internal/config/report_sections.go` → surfaced by `photoprism show config-yaml --md`
|
||||
- Available flags/env: `internal/config/cli_flags_report.go` + `internal/config/report_sections.go` → surfaced by `photoprism show config-options --md/--json`
|
||||
- YAML options mapping: `internal/config/options_report.go` + `internal/config/report_sections.go` → surfaced by `photoprism show config-yaml --md/--json`
|
||||
- Report current values: `internal/config/report.go` → surfaced by `photoprism show config` (alias `photoprism config --md`).
|
||||
- Precedence: `defaults.yml` < CLI/env < `options.yml` (global options rule). See Agent Tips in `AGENTS.md`.
|
||||
- Getters are grouped by topic, e.g. DB in `internal/config/config_db.go`, server in `config_server.go`, TLS in `config_tls.go`, etc.
|
||||
|
||||
@@ -4,12 +4,6 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// JsonFlag enables machine-readable JSON output for cluster commands.
|
||||
var JsonFlag = &cli.BoolFlag{
|
||||
Name: "json",
|
||||
Usage: "print machine-readable JSON",
|
||||
}
|
||||
|
||||
// OffsetFlag for pagination offset (>= 0).
|
||||
var OffsetFlag = &cli.IntFlag{
|
||||
Name: "offset",
|
||||
|
||||
@@ -41,9 +41,9 @@ func showConfigAction(ctx *cli.Context) error {
|
||||
log.Debug(err)
|
||||
}
|
||||
|
||||
format, ferr := report.CliFormatStrict(ctx)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
format, formatErr := report.CliFormatStrict(ctx)
|
||||
if formatErr != nil {
|
||||
return formatErr
|
||||
}
|
||||
|
||||
if format == report.JSON {
|
||||
@@ -65,8 +65,10 @@ func showConfigAction(ctx *cli.Context) error {
|
||||
rows, cols := rep.Report(conf)
|
||||
opt := report.Options{Format: format, NoWrap: rep.NoWrap}
|
||||
result, _ := report.Render(rows, cols, opt)
|
||||
if opt.Format == report.Default {
|
||||
fmt.Printf("\n%s\n\n", strings.ToUpper(rep.Title))
|
||||
if opt.Format == report.Markdown {
|
||||
fmt.Printf("### %s\n\n", rep.Title)
|
||||
} else if opt.Format == report.Default {
|
||||
fmt.Printf("%s\n\n", strings.ToUpper(rep.Title))
|
||||
}
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -14,10 +13,11 @@ import (
|
||||
|
||||
// ShowConfigOptionsCommand configures the command name, flags, and action.
|
||||
var ShowConfigOptionsCommand = &cli.Command{
|
||||
Name: "config-options",
|
||||
Usage: "Displays supported environment variables and CLI flags",
|
||||
Flags: report.CliFlags,
|
||||
Action: showConfigOptionsAction,
|
||||
Name: "config-options",
|
||||
Usage: "Displays supported environment variables and CLI flags",
|
||||
Description: "For readability, standard and Markdown text output is divided into sections. The --json, --csv, and --tsv options return a flat list.",
|
||||
Flags: report.CliFlags,
|
||||
Action: showConfigOptionsAction,
|
||||
}
|
||||
|
||||
// showConfigOptionsAction displays supported environment variables and CLI flags.
|
||||
@@ -26,20 +26,20 @@ func showConfigOptionsAction(ctx *cli.Context) error {
|
||||
conf.SetLogLevel(logrus.FatalLevel)
|
||||
|
||||
rows, cols := config.Flags.Report()
|
||||
format, ferr := report.CliFormatStrict(ctx)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
format, formatErr := report.CliFormatStrict(ctx)
|
||||
if formatErr != nil {
|
||||
return formatErr
|
||||
}
|
||||
|
||||
// CSV/TSV exports use default single-table rendering
|
||||
if format == report.CSV || format == report.TSV {
|
||||
// CSV/TSV/JSON exports use default single-table rendering.
|
||||
if format == report.CSV || format == report.TSV || format == report.JSON {
|
||||
result, err := report.RenderFormat(rows, cols, format)
|
||||
fmt.Println(result)
|
||||
return err
|
||||
}
|
||||
|
||||
// JSON aggregation path
|
||||
if format == report.JSON {
|
||||
// JSON aggregation path (commented out because non-nested output is preferred for now).
|
||||
/* if format == report.JSON {
|
||||
type section struct {
|
||||
Title string `json:"title"`
|
||||
Info string `json:"info,omitempty"`
|
||||
@@ -72,7 +72,7 @@ func showConfigOptionsAction(ctx *cli.Context) error {
|
||||
b, _ := json.Marshal(map[string]interface{}{"sections": agg})
|
||||
fmt.Println(string(b))
|
||||
return nil
|
||||
}
|
||||
} */
|
||||
|
||||
markDown := ctx.Bool("md")
|
||||
sections := config.OptionsReportSections
|
||||
@@ -113,7 +113,7 @@ func showConfigOptionsAction(ctx *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// JSON handled earlier; Markdown and default render per section below
|
||||
// JSON handled earlier; Markdown and default render per section below.
|
||||
result, err := report.RenderFormat(secRows, cols, format)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -14,10 +13,11 @@ import (
|
||||
|
||||
// ShowConfigYamlCommand configures the command name, flags, and action.
|
||||
var ShowConfigYamlCommand = &cli.Command{
|
||||
Name: "config-yaml",
|
||||
Usage: "Displays supported YAML config options and CLI flags",
|
||||
Flags: report.CliFlags,
|
||||
Action: showConfigYamlAction,
|
||||
Name: "config-yaml",
|
||||
Usage: "Displays supported YAML config options and CLI flags",
|
||||
Description: "For readability, standard and Markdown text output is divided into sections. The --json, --csv, and --tsv options return a flat list.",
|
||||
Flags: report.CliFlags,
|
||||
Action: showConfigYamlAction,
|
||||
}
|
||||
|
||||
// showConfigYamlAction displays supported YAML config options and CLI flag.
|
||||
@@ -26,20 +26,20 @@ func showConfigYamlAction(ctx *cli.Context) error {
|
||||
conf.SetLogLevel(logrus.TraceLevel)
|
||||
|
||||
rows, cols := conf.Options().Report()
|
||||
format, ferr := report.CliFormatStrict(ctx)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
format, formatErr := report.CliFormatStrict(ctx)
|
||||
if formatErr != nil {
|
||||
return formatErr
|
||||
}
|
||||
|
||||
// CSV/TSV exports use default single-table rendering
|
||||
if format == report.CSV || format == report.TSV {
|
||||
// CSV/TSV/JSON exports use default single-table rendering.
|
||||
if format == report.CSV || format == report.TSV || format == report.JSON {
|
||||
result, err := report.RenderFormat(rows, cols, format)
|
||||
fmt.Println(result)
|
||||
return err
|
||||
}
|
||||
|
||||
// JSON aggregation path
|
||||
if format == report.JSON {
|
||||
// JSON aggregation path (commented out because non-nested output is preferred for now).
|
||||
/* if format == report.JSON {
|
||||
type section struct {
|
||||
Title string `json:"title"`
|
||||
Info string `json:"info,omitempty"`
|
||||
@@ -72,7 +72,7 @@ func showConfigYamlAction(ctx *cli.Context) error {
|
||||
b, _ := json.Marshal(map[string]interface{}{"sections": agg})
|
||||
fmt.Println(string(b))
|
||||
return nil
|
||||
}
|
||||
} */
|
||||
|
||||
markDown := ctx.Bool("md")
|
||||
sections := config.YamlReportSections
|
||||
@@ -113,7 +113,7 @@ func showConfigYamlAction(ctx *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// JSON handled earlier; Markdown and default render per section below
|
||||
// JSON handled earlier; Markdown and default render per section below.
|
||||
result, err := report.RenderFormat(secRows, cols, format)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -26,9 +26,9 @@ var ShowFileFormatsCommand = &cli.Command{
|
||||
// showFileFormatsAction displays supported media and sidecar file formats.
|
||||
func showFileFormatsAction(ctx *cli.Context) error {
|
||||
rows, cols := media.Report(fs.Extensions.Types(true), !ctx.Bool("short"), true, true)
|
||||
format, ferr := report.CliFormatStrict(ctx)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
format, formatErr := report.CliFormatStrict(ctx)
|
||||
if formatErr != nil {
|
||||
return formatErr
|
||||
}
|
||||
result, err := report.RenderFormat(rows, cols, format)
|
||||
fmt.Println(result)
|
||||
|
||||
@@ -8,11 +8,14 @@ import (
|
||||
|
||||
func TestShowThumbSizes_JSON(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowThumbSizesCommand, []string{"thumb-sizes", "--json"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var v []map[string]string
|
||||
if err := json.Unmarshal([]byte(out), &v); err != nil {
|
||||
|
||||
if err = json.Unmarshal([]byte(out), &v); err != nil {
|
||||
t.Fatalf("invalid json: %v\n%s", err, out)
|
||||
}
|
||||
if len(v) == 0 {
|
||||
@@ -28,11 +31,14 @@ func TestShowThumbSizes_JSON(t *testing.T) {
|
||||
|
||||
func TestShowSources_JSON(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowSourcesCommand, []string{"sources", "--json"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var v []map[string]string
|
||||
if err := json.Unmarshal([]byte(out), &v); err != nil {
|
||||
|
||||
if err = json.Unmarshal([]byte(out), &v); err != nil {
|
||||
t.Fatalf("invalid json: %v\n%s", err, out)
|
||||
}
|
||||
if len(v) == 0 {
|
||||
@@ -48,16 +54,20 @@ func TestShowSources_JSON(t *testing.T) {
|
||||
|
||||
func TestShowMetadata_JSON(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowMetadataCommand, []string{"metadata", "--json"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var v struct {
|
||||
Items []map[string]string `json:"items"`
|
||||
Docs []map[string]string `json:"docs"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &v); err != nil {
|
||||
|
||||
if err = json.Unmarshal([]byte(out), &v); err != nil {
|
||||
t.Fatalf("invalid json: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
if len(v.Items) == 0 {
|
||||
t.Fatalf("expected items")
|
||||
}
|
||||
@@ -65,18 +75,22 @@ func TestShowMetadata_JSON(t *testing.T) {
|
||||
|
||||
func TestShowConfig_JSON(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowConfigCommand, []string{"config", "--json"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var v struct {
|
||||
Sections []struct {
|
||||
Title string `json:"title"`
|
||||
Items []map[string]string `json:"items"`
|
||||
} `json:"sections"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &v); err != nil {
|
||||
|
||||
if err = json.Unmarshal([]byte(out), &v); err != nil {
|
||||
t.Fatalf("invalid json: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
if len(v.Sections) == 0 || len(v.Sections[0].Items) == 0 {
|
||||
t.Fatalf("expected sections with items")
|
||||
}
|
||||
@@ -84,47 +98,49 @@ func TestShowConfig_JSON(t *testing.T) {
|
||||
|
||||
func TestShowConfigOptions_JSON(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowConfigOptionsCommand, []string{"config-options", "--json"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var v struct {
|
||||
Sections []struct {
|
||||
Title string `json:"title"`
|
||||
Items []map[string]string `json:"items"`
|
||||
} `json:"sections"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &v); err != nil {
|
||||
|
||||
type options = []map[string]string
|
||||
var v = options{}
|
||||
|
||||
if err = json.Unmarshal([]byte(out), &v); err != nil {
|
||||
t.Fatalf("invalid json: %v\n%s", err, out)
|
||||
}
|
||||
if len(v.Sections) == 0 || len(v.Sections[0].Items) == 0 {
|
||||
|
||||
if len(v) == 0 {
|
||||
t.Fatalf("expected sections with items")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowConfigYaml_JSON(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowConfigYamlCommand, []string{"config-yaml", "--json"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var v struct {
|
||||
Sections []struct {
|
||||
Title string `json:"title"`
|
||||
Items []map[string]string `json:"items"`
|
||||
} `json:"sections"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &v); err != nil {
|
||||
|
||||
type options = []map[string]string
|
||||
var v = options{}
|
||||
|
||||
if err = json.Unmarshal([]byte(out), &v); err != nil {
|
||||
t.Fatalf("invalid json: %v\n%s", err, out)
|
||||
}
|
||||
if len(v.Sections) == 0 || len(v.Sections[0].Items) == 0 {
|
||||
|
||||
if len(v) == 0 {
|
||||
t.Fatalf("expected sections with items")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowFormatConflict_Error(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowSourcesCommand, []string{"sources", "--json", "--csv"})
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for conflicting flags, got nil; output=%s", out)
|
||||
}
|
||||
|
||||
// Expect an ExitCoder with code 2
|
||||
if ec, ok := err.(interface{ ExitCode() int }); ok {
|
||||
if ec.ExitCode() != 2 {
|
||||
@@ -154,11 +170,14 @@ func min(a, b int) int {
|
||||
|
||||
func TestShowFileFormats_JSON(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowFileFormatsCommand, []string{"file-formats", "--json"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var v []map[string]string
|
||||
if err := json.Unmarshal([]byte(out), &v); err != nil {
|
||||
|
||||
if err = json.Unmarshal([]byte(out), &v); err != nil {
|
||||
t.Fatalf("invalid json: %v\n%s", err, out)
|
||||
}
|
||||
if len(v) == 0 {
|
||||
@@ -178,11 +197,14 @@ func TestShowFileFormats_JSON(t *testing.T) {
|
||||
|
||||
func TestShowVideoSizes_JSON(t *testing.T) {
|
||||
out, err := RunWithTestContext(ShowVideoSizesCommand, []string{"video-sizes", "--json"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var v []map[string]string
|
||||
if err := json.Unmarshal([]byte(out), &v); err != nil {
|
||||
|
||||
if err = json.Unmarshal([]byte(out), &v); err != nil {
|
||||
t.Fatalf("invalid json: %v\n%s", err, out)
|
||||
}
|
||||
if len(v) == 0 {
|
||||
|
||||
@@ -38,9 +38,9 @@ func showMetadataAction(ctx *cli.Context) error {
|
||||
})
|
||||
|
||||
// Output overview of supported metadata tags.
|
||||
format, ferr := report.CliFormatStrict(ctx)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
format, formatErr := report.CliFormatStrict(ctx)
|
||||
if formatErr != nil {
|
||||
return formatErr
|
||||
}
|
||||
if format == report.JSON {
|
||||
resp := struct {
|
||||
|
||||
@@ -30,9 +30,9 @@ func showSearchFiltersAction(ctx *cli.Context) error {
|
||||
}
|
||||
})
|
||||
|
||||
format, ferr := report.CliFormatStrict(ctx)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
format, formatErr := report.CliFormatStrict(ctx)
|
||||
if formatErr != nil {
|
||||
return formatErr
|
||||
}
|
||||
result, err := report.RenderFormat(rows, cols, format)
|
||||
fmt.Println(result)
|
||||
|
||||
@@ -20,9 +20,9 @@ var ShowSourcesCommand = &cli.Command{
|
||||
// showSourcesAction displays supported metadata sources.
|
||||
func showSourcesAction(ctx *cli.Context) error {
|
||||
rows, cols := entity.SrcPriority.Report()
|
||||
format, ferr := report.CliFormatStrict(ctx)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
format, formatErr := report.CliFormatStrict(ctx)
|
||||
if formatErr != nil {
|
||||
return formatErr
|
||||
}
|
||||
result, err := report.RenderFormat(rows, cols, format)
|
||||
fmt.Println(result)
|
||||
|
||||
@@ -21,9 +21,9 @@ var ShowThumbSizesCommand = &cli.Command{
|
||||
// showThumbSizesAction displays supported standard thumbnail sizes.
|
||||
func showThumbSizesAction(ctx *cli.Context) error {
|
||||
rows, cols := thumb.Report(thumb.Sizes.All(), false)
|
||||
format, ferr := report.CliFormatStrict(ctx)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
format, formatErr := report.CliFormatStrict(ctx)
|
||||
if formatErr != nil {
|
||||
return formatErr
|
||||
}
|
||||
result, err := report.RenderFormat(rows, cols, format)
|
||||
fmt.Println(result)
|
||||
|
||||
@@ -20,9 +20,9 @@ var ShowVideoSizesCommand = &cli.Command{
|
||||
// showVideoSizesAction displays supported standard video sizes.
|
||||
func showVideoSizesAction(ctx *cli.Context) error {
|
||||
rows, cols := thumb.Report(thumb.VideoSizes, true)
|
||||
format, ferr := report.CliFormatStrict(ctx)
|
||||
if ferr != nil {
|
||||
return ferr
|
||||
format, formatErr := report.CliFormatStrict(ctx)
|
||||
if formatErr != nil {
|
||||
return formatErr
|
||||
}
|
||||
result, err := report.RenderFormat(rows, cols, format)
|
||||
fmt.Println(result)
|
||||
|
||||
@@ -50,16 +50,16 @@ var CliFlags = []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "md",
|
||||
Aliases: []string{"m"},
|
||||
Usage: "format as machine-readable Markdown",
|
||||
Usage: "print machine-readable Markdown",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "csv",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "export as semicolon separated values",
|
||||
Usage: "print semicolon separated values",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "tsv",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "export as tab separated values",
|
||||
Usage: "print tab separated values",
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user