diff --git a/cmd/dagger/cmd/common/common.go b/cmd/dagger/cmd/common/common.go index a83560e8..712757f2 100644 --- a/cmd/dagger/cmd/common/common.go +++ b/cmd/dagger/cmd/common/common.go @@ -110,7 +110,7 @@ func FormatValue(val *compiler.Value) string { return "dagger.#Secret" } if val.IsConcreteR() != nil { - return val.Cue().IncompleteKind().String() + return val.IncompleteKindString() } // value representation in Cue valStr := fmt.Sprintf("%v", val.Cue()) @@ -121,7 +121,7 @@ func FormatValue(val *compiler.Value) string { // ValueDocString returns the value doc from the comment lines func ValueDocString(val *compiler.Value) string { docs := []string{} - for _, c := range val.Cue().Doc() { + for _, c := range val.Doc() { docs = append(docs, strings.TrimSpace(c.Text())) } doc := strings.Join(docs, " ") diff --git a/cmd/dagger/cmd/doc.go b/cmd/dagger/cmd/doc.go new file mode 100644 index 00000000..859ed897 --- /dev/null +++ b/cmd/dagger/cmd/doc.go @@ -0,0 +1,289 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "io/fs" + "os" + "strings" + "text/tabwriter" + "unicode/utf8" + + "cuelang.org/go/cue" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.dagger.io/dagger/cmd/dagger/cmd/common" + "go.dagger.io/dagger/cmd/dagger/logger" + "go.dagger.io/dagger/compiler" + "go.dagger.io/dagger/environment" + "go.dagger.io/dagger/stdlib" + "golang.org/x/term" +) + +const ( + textFormat = "txt" + markdownFormat = "md" + jsonFormat = "json" + textPadding = " " +) + +// types used for json generation + +type ValueJSON struct { + Name string + Type string + Description string +} + +type FieldJSON struct { + Name string + Description string + Inputs []ValueJSON + Outputs []ValueJSON +} + +type PackageJSON struct { + Name string + Description string + Fields []FieldJSON +} + +var docCmd = &cobra.Command{ + Use: "doc [PACKAGE | PATH]", + Short: "document a package", + Args: cobra.ExactArgs(1), + PreRun: func(cmd *cobra.Command, args []string) { + // Fix Viper bug for duplicate flags: + // https://github.com/spf13/viper/issues/233 + if err := viper.BindPFlags(cmd.Flags()); err != nil { + panic(err) + } + }, + Run: func(cmd *cobra.Command, args []string) { + lg := logger.New() + ctx := lg.WithContext(cmd.Context()) + + format := viper.GetString("output") + if format != textFormat && + format != markdownFormat && + format != jsonFormat { + lg.Fatal().Msg("output must be either `txt`, `md` or `json`") + } + + packageName := args[0] + + val, err := loadCode(packageName) + if err != nil { + lg.Fatal().Err(err).Msg("cannot compile code") + } + PrintDoc(ctx, packageName, val, format) + }, +} + +func init() { + docCmd.Flags().StringP("output", "o", textFormat, "Output format (txt|md)") + + if err := viper.BindPFlags(docCmd.Flags()); err != nil { + panic(err) + } +} + +func mdEscape(s string) string { + escape := []string{"|", "<", ">"} + for _, c := range escape { + s = strings.ReplaceAll(s, c, `\`+c) + } + return s +} + +func terminalTrim(msg string) string { + // If we're not running on a terminal, return the whole string + size, _, err := term.GetSize(1) + if err != nil { + return msg + } + + // Otherwise, trim to fit half the terminal + size /= 2 + for utf8.RuneCountInString(msg) > size { + msg = msg[0:len(msg)-4] + "…" + } + return msg +} + +func formatLabel(name string, val *compiler.Value) string { + label := val.Path().String() + return strings.TrimPrefix(label, name+".") +} + +func loadCode(packageName string) (*compiler.Value, error) { + sources := map[string]fs.FS{ + stdlib.Path: stdlib.FS, + } + + src, err := compiler.Build(sources, packageName) + if err != nil { + return nil, err + } + + return src, nil +} + +// printValuesText (text) formats an array of Values on stdout +func printValuesText(libName string, values []*compiler.Value) { + w := tabwriter.NewWriter(os.Stdout, 0, 4, len(textPadding), ' ', 0) + fmt.Printf("\n%sInputs:\n", textPadding) + for _, i := range values { + docStr := terminalTrim(common.ValueDocString(i)) + fmt.Fprintf(w, "\t\t%s\t%s\t%s\n", + formatLabel(libName, i), common.FormatValue(i), docStr) + } + w.Flush() +} + +// printValuesMarkdown (markdown) formats an array of Values on stdout +func printValuesMarkdown(libName string, values []*compiler.Value) { + w := tabwriter.NewWriter(os.Stdout, 0, 4, len(textPadding), ' ', 0) + fmt.Fprintf(w, "| Name\t| Type\t| Description \t|\n") + fmt.Fprintf(w, "| -------------\t|:-------------:\t|:-------------:\t|\n") + for _, i := range values { + fmt.Fprintf(w, "|*%s*\t|``%s``\t|%s\t|\n", + formatLabel(libName, i), + mdEscape(common.FormatValue(i)), + mdEscape(common.ValueDocString(i))) + } + fmt.Fprintln(w) + w.Flush() +} + +// printValuesJson fills a struct for json output +func valuesToJSON(libName string, values []*compiler.Value) []ValueJSON { + val := []ValueJSON{} + + for _, i := range values { + v := ValueJSON{} + v.Name = formatLabel(libName, i) + v.Type = common.FormatValue(i) + v.Description = common.ValueDocString(i) + val = append(val, v) + } + + return val +} + +func PrintDoc(ctx context.Context, packageName string, val *compiler.Value, format string) { + lg := log.Ctx(ctx) + + fields, err := val.Fields(cue.Definitions(true)) + if err != nil { + lg.Fatal().Err(err).Msg("cannot get fields") + } + + packageJSON := &PackageJSON{} + // Package Name + Description + switch format { + case textFormat: + fmt.Printf("Package %s\n", packageName) + fmt.Printf("\n%s\n", common.ValueDocString(val)) + case markdownFormat: + fmt.Printf("## Package %s\n", mdEscape(packageName)) + comment := common.ValueDocString(val) + if comment == "-" { + fmt.Println() + break + } + fmt.Printf("\n%s\n\n", mdEscape(comment)) + case jsonFormat: + packageJSON.Name = packageName + comment := common.ValueDocString(val) + if comment != "" { + packageJSON.Description = comment + } + } + + // Package Fields + for _, field := range fields { + fieldJSON := FieldJSON{} + + if !field.Selector.IsDefinition() { + // not a definition, skipping + continue + } + + name := field.Label() + v := field.Value + if v.Cue().IncompleteKind() != cue.StructKind { + // not a struct, skipping + continue + } + + // Field Name + Description + comment := common.ValueDocString(v) + switch format { + case textFormat: + fmt.Printf("\n%s\n\n%s%s\n", name, textPadding, comment) + case markdownFormat: + fmt.Printf("### %s\n\n", name) + if comment != "-" { + fmt.Printf("%s\n\n", mdEscape(comment)) + } + case jsonFormat: + fieldJSON.Name = name + comment := common.ValueDocString(val) + if comment != "" { + fieldJSON.Description = comment + } + } + + // Inputs + inp := environment.ScanInputs(ctx, v) + switch format { + case textFormat: + if len(inp) == 0 { + fmt.Printf("\n%sInputs: none\n", textPadding) + break + } + printValuesText(name, inp) + case markdownFormat: + fmt.Printf("#### %s Inputs\n\n", mdEscape(name)) + if len(inp) == 0 { + fmt.Printf("_No input._\n\n") + break + } + printValuesMarkdown(name, inp) + case jsonFormat: + fieldJSON.Inputs = valuesToJSON(name, inp) + } + + // Outputs + out := environment.ScanOutputs(ctx, v) + switch format { + case textFormat: + if len(out) == 0 { + fmt.Printf("\n%sOutputs: none\n", textPadding) + break + } + printValuesText(name, out) + case markdownFormat: + fmt.Printf("#### %s Outputs\n\n", mdEscape(name)) + if len(out) == 0 { + fmt.Printf("_No output._\n\n") + break + } + printValuesMarkdown(name, out) + case jsonFormat: + fieldJSON.Outputs = valuesToJSON(name, out) + packageJSON.Fields = append(packageJSON.Fields, fieldJSON) + } + } + + if format == jsonFormat { + data, err := json.MarshalIndent(packageJSON, "", " ") + if err != nil { + lg.Fatal().Err(err).Msg("json marshal") + } + fmt.Printf("%s\n", data) + } +} diff --git a/cmd/dagger/cmd/input/list.go b/cmd/dagger/cmd/input/list.go index a4c8130d..0d97d0f4 100644 --- a/cmd/dagger/cmd/input/list.go +++ b/cmd/dagger/cmd/input/list.go @@ -57,15 +57,6 @@ var listCmd = &cobra.Command{ for _, inp := range inputs { isConcrete := (inp.IsConcreteR() == nil) _, hasDefault := inp.Default() - // valStr := "-" - // if isConcrete { - // valStr, _ = inp.Cue().String() - // } - // if hasDefault { - // valStr = fmt.Sprintf("%s (default)", valStr) - // } - - // valStr = strings.ReplaceAll(valStr, "\n", "\\n") if !viper.GetBool("all") { // skip input that is not overridable diff --git a/cmd/dagger/cmd/root.go b/cmd/dagger/cmd/root.go index 19ab0229..a4d0a061 100644 --- a/cmd/dagger/cmd/root.go +++ b/cmd/dagger/cmd/root.go @@ -46,6 +46,7 @@ func init() { input.Cmd, output.Cmd, versionCmd, + docCmd, ) if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { diff --git a/compiler/value.go b/compiler/value.go index ded6e810..167f3861 100644 --- a/compiler/value.go +++ b/compiler/value.go @@ -5,6 +5,7 @@ import ( "strconv" "cuelang.org/go/cue" + "cuelang.org/go/cue/ast" cueformat "cuelang.org/go/cue/format" ) @@ -75,8 +76,8 @@ func (f Field) Label() string { // Proxy function to the underlying cue.Value // Field ordering is guaranteed to be stable. -func (v *Value) Fields() ([]Field, error) { - it, err := v.val.Fields() +func (v *Value) Fields(opts ...cue.Option) ([]Field, error) { + it, err := v.val.Fields(opts...) if err != nil { return nil, err } @@ -270,3 +271,11 @@ func (v *Value) Default() (*Value, bool) { val, hasDef := v.val.Default() return v.cc.Wrap(val), hasDef } + +func (v *Value) Doc() []*ast.CommentGroup { + return v.Cue().Doc() +} + +func (v *Value) IncompleteKindString() string { + return v.Cue().IncompleteKind().String() +} diff --git a/environment/environment.go b/environment/environment.go index a0aa24b7..260fd47d 100644 --- a/environment/environment.go +++ b/environment/environment.go @@ -320,7 +320,7 @@ func (e *Environment) ScanInputs(ctx context.Context, mergeUserInputs bool) ([]* } } - return scanInputs(ctx, src), nil + return ScanInputs(ctx, src), nil } func (e *Environment) ScanOutputs(ctx context.Context) ([]*compiler.Value, error) { @@ -340,5 +340,5 @@ func (e *Environment) ScanOutputs(ctx context.Context) ([]*compiler.Value, error } } - return scanOutputs(ctx, src), nil + return ScanOutputs(ctx, src), nil } diff --git a/environment/inputs_scan.go b/environment/inputs_scan.go index 0939a9ac..7b3a3648 100644 --- a/environment/inputs_scan.go +++ b/environment/inputs_scan.go @@ -42,7 +42,7 @@ func isReference(val cue.Value) bool { return isRef(val) } -func scanInputs(ctx context.Context, value *compiler.Value) []*compiler.Value { +func ScanInputs(ctx context.Context, value *compiler.Value) []*compiler.Value { lg := log.Ctx(ctx) inputs := []*compiler.Value{} @@ -67,7 +67,7 @@ func scanInputs(ctx context.Context, value *compiler.Value) []*compiler.Value { return inputs } -func scanOutputs(ctx context.Context, value *compiler.Value) []*compiler.Value { +func ScanOutputs(ctx context.Context, value *compiler.Value) []*compiler.Value { lg := log.Ctx(ctx) inputs := []*compiler.Value{} diff --git a/go.mod b/go.mod index 0ed3cba4..8352a529 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/uber/jaeger-lib v2.4.1+incompatible // indirect go.mozilla.org/sops/v3 v3.7.1 go.uber.org/atomic v1.7.0 // indirect + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c // indirect golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1