...

Source file src/edge-infra.dev/pkg/tools/dlog/cmd/dlog.go

Documentation: edge-infra.dev/pkg/tools/dlog/cmd

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"path/filepath"
     9  	"slices"
    10  	"text/tabwriter"
    11  
    12  	"sigs.k8s.io/yaml"
    13  
    14  	"edge-infra.dev/pkg/lib/cli/rags"
    15  	"edge-infra.dev/pkg/lib/cli/sink"
    16  	"edge-infra.dev/pkg/tools/dlog"
    17  )
    18  
    19  // TODO: validate status inputs, use rags.StringEnum
    20  
    21  func New() *sink.Command {
    22  	var (
    23  		dir, status, out string
    24  		tags             []string
    25  	)
    26  
    27  	cmd := &sink.Command{
    28  		Use:   "dlog [command] [flags]",
    29  		Short: "Manage decision logs.",
    30  		Flags: []*rags.Rag{
    31  			dirFlag(&dir),
    32  			{
    33  				Name:  "status",
    34  				Short: "s",
    35  				Usage: "Filter by dlog status",
    36  				Value: &rags.String{Var: &status},
    37  			},
    38  			{
    39  				Name:  "tags",
    40  				Usage: "Comma separated list of tags to filter dlogs by. Can be provided multiple times.",
    41  				Value: &rags.StringSet{Var: &tags},
    42  			},
    43  			{
    44  				Name:  "output",
    45  				Usage: "Control output format. One of [plain, json, yaml]",
    46  				Value: &rags.StringEnum{
    47  					Var: &out, Valid: []string{"plain", "json", "yaml"},
    48  				},
    49  			},
    50  		},
    51  		Commands: []*sink.Command{
    52  			newCreateCmd(),
    53  			newEditCmd(),
    54  		},
    55  		Exec: func(_ context.Context, r sink.Run) error {
    56  			decisions, err := dlog.FromDir(dir)
    57  			if err != nil {
    58  				return err
    59  			}
    60  
    61  			decisions = filterDecisions(decisions, status, tags)
    62  
    63  			switch out {
    64  			case "json":
    65  				data, err := json.Marshal(dlogsToExternalFmt(dir, decisions))
    66  				if err != nil {
    67  					return err
    68  				}
    69  				out := new(bytes.Buffer)
    70  				if err := json.Indent(out, data, "", "\t"); err != nil {
    71  					return err
    72  				}
    73  				fmt.Fprintln(r.Out(), out)
    74  				return nil
    75  			case "yaml":
    76  				data, err := yaml.Marshal(dlogsToExternalFmt(dir, decisions))
    77  				if err != nil {
    78  					return err
    79  				}
    80  				fmt.Fprintln(r.Out(), string(data))
    81  				return nil
    82  			case "plain":
    83  				tw := tabwriter.NewWriter(r.Out(), 2, 0, 1, ' ', 0)
    84  				defer tw.Flush()
    85  
    86  				for _, d := range decisions {
    87  					fmt.Fprintf(tw, "%s\t%s\n", d.Number(), d.Title())
    88  					fmt.Fprintf(tw, "\t%s\t%s\t\n", d.Status, d.Date)
    89  					if len(d.Tags) > 0 {
    90  						fmt.Fprintf(tw, "\ttags:\t%s\n", d.Tags)
    91  					}
    92  					fmt.Fprintf(tw, "\tdeciders:\t%s\n", d.Deciders)
    93  					if len(d.Consulted) > 0 {
    94  						fmt.Fprintf(tw, "\tconsulted:\t%s\n", d.Consulted)
    95  					}
    96  					if len(d.Informed) > 0 {
    97  						fmt.Fprintf(tw, "\tinformed:\t%s\n", d.Informed)
    98  					}
    99  					fmt.Fprintln(tw)
   100  				}
   101  			}
   102  
   103  			return nil
   104  		},
   105  	}
   106  
   107  	return cmd
   108  }
   109  
   110  func dirFlag(dir *string) *rags.Rag {
   111  	return &rags.Rag{
   112  		Name:  "dir",
   113  		Short: "d",
   114  		Value: rags.NewValueDefault(dir, "docs/decisions"),
   115  		Usage: "Root directory for decision logs",
   116  	}
   117  }
   118  
   119  func templateFlag(template *string) *rags.Rag {
   120  	return &rags.Rag{
   121  		Name:  "template",
   122  		Short: "t",
   123  		Value: rags.NewValueDefault(template, "docs/decisions/template/template.md"),
   124  		Usage: "Path to decision log template",
   125  	}
   126  }
   127  
   128  func filterDecisions(dd []*dlog.Decision, status string, tags []string) []*dlog.Decision {
   129  	if status == "" && len(tags) == 0 {
   130  		return dd
   131  	}
   132  
   133  	n := 0
   134  	for _, d := range dd {
   135  		if !shouldSkip(d, status, tags) {
   136  			dd[n] = d
   137  			n++
   138  		}
   139  	}
   140  	dd = dd[:n]
   141  
   142  	return dd
   143  }
   144  
   145  func shouldSkip(d *dlog.Decision, status string, tags []string) bool {
   146  	if status != "" && d.Status != status {
   147  		return true
   148  	}
   149  	if len(tags) > 0 {
   150  		for _, t := range tags {
   151  			if !slices.Contains(d.Tags, t) {
   152  				return true
   153  			}
   154  		}
   155  	}
   156  	return false
   157  }
   158  
   159  // dlogsToExternalFmt takes an array of decisions and returns an array of wrapped
   160  // decisions that expose internal state for marshaling. This allows us to work
   161  // around the fact that you cannot conditional marshal/unmarshal data into a
   162  // struct such that:
   163  //
   164  //   - when we are editing frontmatter for a dlog markdown file, we do not double
   165  //     encode information like the title, the number, and the filepath (it wouldn't
   166  //     make sense )
   167  //   - when we are dumping data on dlogs for external consumers (eg JSON or YAML),
   168  //     we include important information for processing, such as the file
   169  func dlogsToExternalFmt(dir string, dd []*dlog.Decision) any {
   170  	type printedDlog struct {
   171  		*dlog.Decision
   172  		Number string `json:"number"`
   173  		File   string `json:"file"`
   174  		Title  string `json:"title"`
   175  	}
   176  	a := make([]printedDlog, len(dd))
   177  	for i, d := range dd {
   178  		a[i] = printedDlog{
   179  			Decision: d,
   180  			Number:   d.Number(),
   181  			File:     filepath.Join(dir, d.File()),
   182  			Title:    d.Title(),
   183  		}
   184  	}
   185  	return a
   186  }
   187  

View as plain text