Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -96,6 +97,7 @@ func getOwnCommands() []ownCommand {
longDescription: "The \"show\" command prints the effective configuration of the selected profile or group.\n\nThe effective profile or group configuration is built by loading all includes, applying inheritance, mixins, templates and variables and parsing the result.",
action: showProfileOrGroup,
needConfiguration: true,
flags: map[string]string{"--json": "output the configuration in JSON format"},
},
{
name: "schedule",
Expand Down Expand Up @@ -235,6 +237,8 @@ func completeCommand(ctx commandContext) error {
func showProfileOrGroup(ctx commandContext) error {
c := ctx.config
flags := ctx.flags
args := ctx.request.arguments
jsonOutput := slices.Contains(args, "--json")

// Load global section
global, err := c.GetGlobalSection()
Expand Down Expand Up @@ -269,6 +273,10 @@ func showProfileOrGroup(ctx commandContext) error {
return fmt.Errorf("profile or group '%s': %w", flags.name, config.ErrNotFound)
}

if jsonOutput {
return showProfileOrGroupJSON(ctx, global, profileOrGroup)
}

// Show global
err = config.ShowStruct(ctx.terminal, global, constants.SectionConfigurationGlobal)
if err != nil {
Expand Down Expand Up @@ -297,6 +305,44 @@ func showProfileOrGroup(ctx commandContext) error {
return nil
}

func showProfileOrGroupJSON(ctx commandContext, global *config.Global, profileOrGroup config.Schedulable) error {
flags := ctx.flags

globalData, err := config.CollectStruct(global)
if err != nil {
return fmt.Errorf("cannot collect global section: %w", err)
}

profileData, err := config.CollectStruct(profileOrGroup)
if err != nil {
return fmt.Errorf("cannot collect %s '%s': %w", profileOrGroup.Kind(), flags.name, err)
}

schedules := slices.Collect(maps.Values(profileOrGroup.Schedules()))
slices.SortFunc(schedules, config.CompareSchedules)
var schedulesData []map[string]any
for _, schedule := range schedules {
data, err := config.CollectStruct(schedule.ScheduleConfig)
if err != nil {
continue
}
data["command"] = schedule.ScheduleOrigin().Command
schedulesData = append(schedulesData, data)
}

output := map[string]any{
"global": globalData,
profileOrGroup.Kind(): profileData,
}
if len(schedulesData) > 0 {
output["schedules"] = schedulesData
}

encoder := json.NewEncoder(ctx.terminal)
encoder.SetIndent("", " ")
return encoder.Encode(output)
}

func showSchedules(output io.Writer, schedules []*config.Schedule) {
slices.SortFunc(schedules, config.CompareSchedules)
for _, schedule := range schedules {
Expand Down
168 changes: 168 additions & 0 deletions config/collect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package config

import (
"fmt"
"reflect"
"slices"
"sort"

"github.com/creativeprojects/resticprofile/util"
)

// CollectStruct walks a struct using the same tag and visibility rules as ShowStruct,
// but returns a map[string]any suitable for JSON serialization.
func CollectStruct(orig any) (map[string]any, error) {
return collectStructValue(reflect.ValueOf(orig))
}

func collectStructValue(value reflect.Value) (map[string]any, error) {
v, isNil := util.UnpackValue(value)
if isNil {
return nil, nil
}
if v.Kind() != reflect.Struct {
return nil, fmt.Errorf("unsupported type %s, expected %s", v.Kind(), reflect.Struct)
}
result := make(map[string]any)
for i := 0; i < v.Type().NumField(); i++ {
ft, fv := v.Type().Field(i), v.Field(i)
key, ok := fieldShown(&ft)
if !ok {
continue
}
uv, isNil := util.UnpackValue(fv)
if isNil {
continue
}

switch uv.Kind() {
case reflect.Struct:
if getStringer(fv) != nil {
collectLeaf(result, key, fv)
}
if key == ",squash" {
if sub, err := collectStructValue(fv); err == nil {
for k, v := range sub {
result[k] = v
}
}
} else if sub, err := collectStructValue(fv); err == nil && len(sub) > 0 {
result[key] = sub
}

case reflect.Map:
if fv.Len() > 0 {
m := collectMapValue(fv)
if key == ",remain" {
for k, v := range m {
result[k] = v
}
} else {
result[key] = m
}
}

case reflect.Array, reflect.Slice:
if isSliceWithStruct(fv) {
if items := collectSliceValue(fv); len(items) > 0 {
result[key] = items
}
} else {
collectLeaf(result, key, fv)
}

default:
collectLeaf(result, key, fv)
}
}
return result, nil
}

func collectMapValue(v reflect.Value) map[string]any {
result := make(map[string]any)
keys := v.MapKeys()
sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
for _, key := range keys {
collectLeaf(result, key.String(), v.MapIndex(key))
}
return result
}

func collectSliceValue(v reflect.Value) (items []any) {
for i := 0; i < v.Len(); i++ {
elem := v.Index(i)
uv, isNil := util.UnpackValue(elem)
if isNil {
continue
}
switch uv.Kind() {
case reflect.Struct:
if getStringer(uv) != nil {
items = append(items, uv.Interface().(fmt.Stringer).String())
} else if sub, err := collectStructValue(elem); err == nil && len(sub) > 0 {
items = append(items, sub)
}
case reflect.Map:
if uv.Len() > 0 {
items = append(items, collectMapValue(uv))
}
case reflect.Slice, reflect.Array:
if nested := collectSliceValue(uv); len(nested) > 0 {
items = append(items, nested)
}
default:
if vals, ok := stringify(elem, false); ok && len(vals) > 0 {
items = append(items, vals[0])
}
}
}
return
}

func collectLeaf(result map[string]any, key string, v reflect.Value) {
if isNotShown(key, nil) {
return
}
uv, isNil := util.UnpackValue(v)
if isNil {
return
}
switch uv.Kind() {
case reflect.Struct:
if getStringer(uv) == nil {
if sub, err := collectStructValue(v); err == nil && len(sub) > 0 {
result[key] = sub
}
} else if vals, ok := stringify(v, false); ok && len(vals) > 0 {
result[key] = vals[0]
}

case reflect.Map:
if uv.Len() > 0 {
result[key] = collectMapValue(uv)
}

case reflect.Slice, reflect.Array:
if isSliceWithStruct(uv) {
if items := collectSliceValue(uv); len(items) > 0 {
result[key] = items
}
} else if uv.Len() > 0 {
result[key] = collectSliceValue(uv)
}

default:
if vals, ok := stringify(v, false); ok {
switch len(vals) {
case 0:
result[key] = true
case 1:
result[key] = vals[0]
default:
result[key] = vals
}
} else if slices.Contains(allowedEmptyValueArgs, key) {
result[key] = ""
}
}
}
Loading