Merge pull request #547 from samalba/cmd-doc
doc cmd: generate stdlib doc
This commit is contained in:
commit
92ceae9ce6
@ -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, " ")
|
||||
|
289
cmd/dagger/cmd/doc.go
Normal file
289
cmd/dagger/cmd/doc.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -46,6 +46,7 @@ func init() {
|
||||
input.Cmd,
|
||||
output.Cmd,
|
||||
versionCmd,
|
||||
docCmd,
|
||||
)
|
||||
|
||||
if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{}
|
||||
|
||||
|
1
go.mod
1
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
|
||||
|
Reference in New Issue
Block a user