package cmd import ( "bytes" "context" "encoding/json" "fmt" "path/filepath" "slices" "text/tabwriter" "sigs.k8s.io/yaml" "edge-infra.dev/pkg/lib/cli/rags" "edge-infra.dev/pkg/lib/cli/sink" "edge-infra.dev/pkg/tools/dlog" ) // TODO: validate status inputs, use rags.StringEnum func New() *sink.Command { var ( dir, status, out string tags []string ) cmd := &sink.Command{ Use: "dlog [command] [flags]", Short: "Manage decision logs.", Flags: []*rags.Rag{ dirFlag(&dir), { Name: "status", Short: "s", Usage: "Filter by dlog status", Value: &rags.String{Var: &status}, }, { Name: "tags", Usage: "Comma separated list of tags to filter dlogs by. Can be provided multiple times.", Value: &rags.StringSet{Var: &tags}, }, { Name: "output", Usage: "Control output format. One of [plain, json, yaml]", Value: &rags.StringEnum{ Var: &out, Valid: []string{"plain", "json", "yaml"}, }, }, }, Commands: []*sink.Command{ newCreateCmd(), newEditCmd(), }, Exec: func(_ context.Context, r sink.Run) error { decisions, err := dlog.FromDir(dir) if err != nil { return err } decisions = filterDecisions(decisions, status, tags) switch out { case "json": data, err := json.Marshal(dlogsToExternalFmt(dir, decisions)) if err != nil { return err } out := new(bytes.Buffer) if err := json.Indent(out, data, "", "\t"); err != nil { return err } fmt.Fprintln(r.Out(), out) return nil case "yaml": data, err := yaml.Marshal(dlogsToExternalFmt(dir, decisions)) if err != nil { return err } fmt.Fprintln(r.Out(), string(data)) return nil case "plain": tw := tabwriter.NewWriter(r.Out(), 2, 0, 1, ' ', 0) defer tw.Flush() for _, d := range decisions { fmt.Fprintf(tw, "%s\t%s\n", d.Number(), d.Title()) fmt.Fprintf(tw, "\t%s\t%s\t\n", d.Status, d.Date) if len(d.Tags) > 0 { fmt.Fprintf(tw, "\ttags:\t%s\n", d.Tags) } fmt.Fprintf(tw, "\tdeciders:\t%s\n", d.Deciders) if len(d.Consulted) > 0 { fmt.Fprintf(tw, "\tconsulted:\t%s\n", d.Consulted) } if len(d.Informed) > 0 { fmt.Fprintf(tw, "\tinformed:\t%s\n", d.Informed) } fmt.Fprintln(tw) } } return nil }, } return cmd } func dirFlag(dir *string) *rags.Rag { return &rags.Rag{ Name: "dir", Short: "d", Value: rags.NewValueDefault(dir, "docs/decisions"), Usage: "Root directory for decision logs", } } func templateFlag(template *string) *rags.Rag { return &rags.Rag{ Name: "template", Short: "t", Value: rags.NewValueDefault(template, "docs/decisions/template/template.md"), Usage: "Path to decision log template", } } func filterDecisions(dd []*dlog.Decision, status string, tags []string) []*dlog.Decision { if status == "" && len(tags) == 0 { return dd } n := 0 for _, d := range dd { if !shouldSkip(d, status, tags) { dd[n] = d n++ } } dd = dd[:n] return dd } func shouldSkip(d *dlog.Decision, status string, tags []string) bool { if status != "" && d.Status != status { return true } if len(tags) > 0 { for _, t := range tags { if !slices.Contains(d.Tags, t) { return true } } } return false } // dlogsToExternalFmt takes an array of decisions and returns an array of wrapped // decisions that expose internal state for marshaling. This allows us to work // around the fact that you cannot conditional marshal/unmarshal data into a // struct such that: // // - when we are editing frontmatter for a dlog markdown file, we do not double // encode information like the title, the number, and the filepath (it wouldn't // make sense ) // - when we are dumping data on dlogs for external consumers (eg JSON or YAML), // we include important information for processing, such as the file func dlogsToExternalFmt(dir string, dd []*dlog.Decision) any { type printedDlog struct { *dlog.Decision Number string `json:"number"` File string `json:"file"` Title string `json:"title"` } a := make([]printedDlog, len(dd)) for i, d := range dd { a[i] = printedDlog{ Decision: d, Number: d.Number(), File: filepath.Join(dir, d.File()), Title: d.Title(), } } return a }