Merge pull request #547 from samalba/cmd-doc

doc cmd: generate stdlib doc
This commit is contained in:
Sam Alba 2021-06-04 09:26:19 +02:00 committed by GitHub
commit 92ceae9ce6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 308 additions and 17 deletions

View File

@ -110,7 +110,7 @@ func FormatValue(val *compiler.Value) string {
return "dagger.#Secret" return "dagger.#Secret"
} }
if val.IsConcreteR() != nil { if val.IsConcreteR() != nil {
return val.Cue().IncompleteKind().String() return val.IncompleteKindString()
} }
// value representation in Cue // value representation in Cue
valStr := fmt.Sprintf("%v", val.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 // ValueDocString returns the value doc from the comment lines
func ValueDocString(val *compiler.Value) string { func ValueDocString(val *compiler.Value) string {
docs := []string{} docs := []string{}
for _, c := range val.Cue().Doc() { for _, c := range val.Doc() {
docs = append(docs, strings.TrimSpace(c.Text())) docs = append(docs, strings.TrimSpace(c.Text()))
} }
doc := strings.Join(docs, " ") doc := strings.Join(docs, " ")

289
cmd/dagger/cmd/doc.go Normal file
View File

@ -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)
}
}

View File

@ -57,15 +57,6 @@ var listCmd = &cobra.Command{
for _, inp := range inputs { for _, inp := range inputs {
isConcrete := (inp.IsConcreteR() == nil) isConcrete := (inp.IsConcreteR() == nil)
_, hasDefault := inp.Default() _, 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") { if !viper.GetBool("all") {
// skip input that is not overridable // skip input that is not overridable

View File

@ -46,6 +46,7 @@ func init() {
input.Cmd, input.Cmd,
output.Cmd, output.Cmd,
versionCmd, versionCmd,
docCmd,
) )
if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {

View File

@ -5,6 +5,7 @@ import (
"strconv" "strconv"
"cuelang.org/go/cue" "cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
cueformat "cuelang.org/go/cue/format" cueformat "cuelang.org/go/cue/format"
) )
@ -75,8 +76,8 @@ func (f Field) Label() string {
// Proxy function to the underlying cue.Value // Proxy function to the underlying cue.Value
// Field ordering is guaranteed to be stable. // Field ordering is guaranteed to be stable.
func (v *Value) Fields() ([]Field, error) { func (v *Value) Fields(opts ...cue.Option) ([]Field, error) {
it, err := v.val.Fields() it, err := v.val.Fields(opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -270,3 +271,11 @@ func (v *Value) Default() (*Value, bool) {
val, hasDef := v.val.Default() val, hasDef := v.val.Default()
return v.cc.Wrap(val), hasDef 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()
}

View File

@ -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) { 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
} }

View File

@ -42,7 +42,7 @@ func isReference(val cue.Value) bool {
return isRef(val) 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) lg := log.Ctx(ctx)
inputs := []*compiler.Value{} inputs := []*compiler.Value{}
@ -67,7 +67,7 @@ func scanInputs(ctx context.Context, value *compiler.Value) []*compiler.Value {
return inputs 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) lg := log.Ctx(ctx)
inputs := []*compiler.Value{} inputs := []*compiler.Value{}

1
go.mod
View File

@ -30,6 +30,7 @@ require (
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
go.mozilla.org/sops/v3 v3.7.1 go.mozilla.org/sops/v3 v3.7.1
go.uber.org/atomic v1.7.0 // indirect 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/net v0.0.0-20210331212208-0fccb6fa2b5c // indirect
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1