doc generation: refactor to allow multi-stage processing

Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
Signed-off-by: slumbering <slumbering.pierrot@gmail.com>
This commit is contained in:
Andrea Luzzardi 2021-06-09 18:28:51 -07:00 committed by slumbering
parent b8f9c0ee67
commit 02b47157e1

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/fs" "io/fs"
"os" "os"
"path" "path"
@ -32,25 +31,217 @@ const (
textPadding = " " textPadding = " "
) )
// types used for json generation type Value struct {
type ValueJSON struct {
Name string Name string
Type string Type string
Description string Description string
} }
type FieldJSON struct { type Field struct {
Name string Name string
Description string Description string
Inputs []ValueJSON Inputs []Value
Outputs []ValueJSON Outputs []Value
} }
type PackageJSON struct { type Package struct {
Name string Name string
Description string Description string
Fields []FieldJSON Fields []Field
}
func Parse(ctx context.Context, packageName string, val *compiler.Value) *Package {
lg := log.Ctx(ctx)
parseValues := func(field string, values []*compiler.Value) []Value {
val := []Value{}
for _, i := range values {
v := Value{}
v.Name = strings.TrimPrefix(
i.Path().String(),
field+".",
)
v.Type = common.FormatValue(i)
v.Description = common.ValueDocOneLine(i)
val = append(val, v)
}
return val
}
fields, err := val.Fields(cue.Definitions(true))
if err != nil {
lg.Fatal().Err(err).Msg("cannot get fields")
}
pkg := &Package{}
// Package Name + Description
pkg.Name = packageName
pkg.Description = common.ValueDocFull(val)
// Package Fields
for _, f := range fields {
field := Field{}
if !f.Selector.IsDefinition() {
// not a definition, skipping
continue
}
name := f.Label()
v := f.Value
if v.Cue().IncompleteKind() != cue.StructKind {
// not a struct, skipping
continue
}
// Field Name + Description
field.Name = name
field.Description = common.ValueDocOneLine(v)
// Inputs
inp := environment.ScanInputs(ctx, v)
field.Inputs = parseValues(field.Name, inp)
// Outputs
out := environment.ScanOutputs(ctx, v)
field.Outputs = parseValues(field.Name, out)
pkg.Fields = append(pkg.Fields, field)
}
return pkg
}
func (p *Package) Format(f string) string {
switch f {
case textFormat:
return p.Text()
case jsonFormat:
return p.JSON()
case markdownFormat:
return p.Markdown()
default:
panic(f)
}
}
func (p *Package) JSON() string {
data, err := json.MarshalIndent(p, "", " ")
if err != nil {
panic(err)
}
return fmt.Sprintf("%s\n", data)
}
func (p *Package) Text() string {
w := &strings.Builder{}
fmt.Fprintf(w, "Package %s\n", p.Name)
fmt.Fprintf(w, "\n%s\n", p.Description)
printValuesText := func(values []Value) {
tw := tabwriter.NewWriter(w, 0, 4, len(textPadding), ' ', 0)
for _, i := range values {
fmt.Fprintf(tw, "\t\t%s\t%s\t%s\n",
i.Name, i.Type, terminalTrim(i.Description))
}
tw.Flush()
}
// Package Fields
for _, field := range p.Fields {
fmt.Fprintf(w, "\n%s\n\n%s%s\n", field.Name, textPadding, field.Description)
if len(field.Inputs) == 0 {
fmt.Fprintf(w, "\n%sInputs: none\n", textPadding)
} else {
fmt.Fprintf(w, "\n%sInputs:\n", textPadding)
printValuesText(field.Inputs)
}
if len(field.Outputs) == 0 {
fmt.Fprintf(w, "\n%sOutputs: none\n", textPadding)
} else {
fmt.Fprintf(w, "\n%sOutputs:\n", textPadding)
printValuesText(field.Outputs)
}
}
return w.String()
}
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 (p *Package) Markdown() string {
w := &strings.Builder{}
fmt.Fprintf(w, "---\nsidebar_label: %s\n---\n\n",
filepath.Base(p.Name),
)
fmt.Fprintf(w, "# %s\n", mdEscape(p.Name))
if p.Description != "-" {
fmt.Fprintf(w, "\n%s\n", mdEscape(p.Description))
}
printValuesMarkdown := func(values []Value) {
tw := tabwriter.NewWriter(w, 0, 4, len(textPadding), ' ', 0)
fmt.Fprintf(tw, "| Name\t| Type\t| Description \t|\n")
fmt.Fprintf(tw, "| -------------\t|:-------------:\t|:-------------:\t|\n")
for _, i := range values {
fmt.Fprintf(tw, "|*%s*\t| `%s`\t|%s\t|\n",
i.Name,
mdEscape(i.Type),
mdEscape(i.Description),
)
}
tw.Flush()
}
// Package Fields
for _, field := range p.Fields {
fmt.Fprintf(w, "\n## %s\n\n", field.Name)
if field.Description != "-" {
fmt.Fprintf(w, "%s\n\n", mdEscape(field.Description))
}
fmt.Fprintf(w, "### %s Inputs\n\n", mdEscape(field.Name))
if len(field.Inputs) == 0 {
fmt.Fprintf(w, "_No input._\n")
} else {
printValuesMarkdown(field.Inputs)
}
fmt.Fprintf(w, "\n### %s Outputs\n\n", mdEscape(field.Name))
if len(field.Outputs) == 0 {
fmt.Fprintf(w, "_No output._\n")
} else {
printValuesMarkdown(field.Outputs)
}
}
return w.String()
}
func mdEscape(s string) string {
escape := []string{"|", "<", ">"}
for _, c := range escape {
s = strings.ReplaceAll(s, c, `\`+c)
}
return s
} }
var docCmd = &cobra.Command{ var docCmd = &cobra.Command{
@ -94,7 +285,8 @@ var docCmd = &cobra.Command{
if err != nil { if err != nil {
lg.Fatal().Err(err).Msg("cannot compile code") lg.Fatal().Err(err).Msg("cannot compile code")
} }
PrintDoc(ctx, os.Stdout, packageName, val, format) p := Parse(ctx, packageName, val)
fmt.Printf("%s", p.Format(format))
}, },
} }
@ -107,34 +299,6 @@ func init() {
} }
} }
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) { func loadCode(packageName string) (*compiler.Value, error) {
sources := map[string]fs.FS{ sources := map[string]fs.FS{
stdlib.Path: stdlib.FS, stdlib.Path: stdlib.FS,
@ -148,165 +312,6 @@ func loadCode(packageName string) (*compiler.Value, error) {
return src, nil return src, nil
} }
// printValuesText (text) formats an array of Values on stdout
func printValuesText(iw io.Writer, libName string, values []*compiler.Value) {
w := tabwriter.NewWriter(iw, 0, 4, len(textPadding), ' ', 0)
for _, i := range values {
docStr := terminalTrim(common.ValueDocOneLine(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(iw io.Writer, libName string, values []*compiler.Value) {
w := tabwriter.NewWriter(iw, 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.ValueDocOneLine(i)))
}
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.ValueDocOneLine(i)
val = append(val, v)
}
return val
}
func PrintDoc(ctx context.Context, w io.Writer, 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.Fprintf(w, "Package %s\n", packageName)
fmt.Fprintf(w, "\n%s\n", common.ValueDocFull(val))
case markdownFormat:
fmt.Fprintf(w, "---\nsidebar_label: %s\n---\n\n",
filepath.Base(packageName),
)
fmt.Fprintf(w, "# %s\n", mdEscape(packageName))
comment := common.ValueDocFull(val)
if comment == "-" {
break
}
fmt.Fprintf(w, "\n%s\n", mdEscape(comment))
case jsonFormat:
packageJSON.Name = packageName
comment := common.ValueDocFull(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.ValueDocOneLine(v)
switch format {
case textFormat:
fmt.Fprintf(w, "\n%s\n\n%s%s\n", name, textPadding, comment)
case markdownFormat:
fmt.Fprintf(w, "\n## %s\n\n", name)
if comment != "-" {
fmt.Fprintf(w, "%s\n\n", mdEscape(comment))
}
case jsonFormat:
fieldJSON.Name = name
comment := common.ValueDocOneLine(val)
if comment != "-" {
fieldJSON.Description = comment
}
}
// Inputs
inp := environment.ScanInputs(ctx, v)
switch format {
case textFormat:
if len(inp) == 0 {
fmt.Fprintf(w, "\n%sInputs: none\n", textPadding)
break
}
fmt.Fprintf(w, "\n%sInputs:\n", textPadding)
printValuesText(w, name, inp)
case markdownFormat:
fmt.Fprintf(w, "### %s Inputs\n\n", mdEscape(name))
if len(inp) == 0 {
fmt.Fprintf(w, "_No input._\n")
break
}
printValuesMarkdown(w, name, inp)
case jsonFormat:
fieldJSON.Inputs = valuesToJSON(name, inp)
}
// Outputs
out := environment.ScanOutputs(ctx, v)
switch format {
case textFormat:
if len(out) == 0 {
fmt.Fprintf(w, "\n%sOutputs: none\n", textPadding)
break
}
fmt.Fprintf(w, "\n%sOutputs:\n", textPadding)
printValuesText(w, name, out)
case markdownFormat:
fmt.Fprintf(w, "\n### %s Outputs\n\n", mdEscape(name))
if len(out) == 0 {
fmt.Fprintf(w, "_No output._\n")
break
}
printValuesMarkdown(w, 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.Fprintf(w, "%s\n", data)
}
}
// walkStdlib generate whole docs from stdlib walk // walkStdlib generate whole docs from stdlib walk
func walkStdlib(ctx context.Context, output, format string) { func walkStdlib(ctx context.Context, output, format string) {
lg := log.Ctx(ctx) lg := log.Ctx(ctx)
@ -327,9 +332,9 @@ func walkStdlib(ctx context.Context, output, format string) {
return err return err
} }
pkg := fmt.Sprintf("dagger.io/%s", p) pkgName := fmt.Sprintf("dagger.io/%s", p)
lg.Info().Str("package", pkg).Str("format", format).Msg("generating doc") lg.Info().Str("package", pkgName).Str("format", format).Msg("generating doc")
val, err := loadCode(pkg) val, err := loadCode(pkgName)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "no CUE files") { if strings.Contains(err.Error(), "no CUE files") {
lg.Warn().Str("package", p).Err(err).Msg("ignoring") lg.Warn().Str("package", p).Err(err).Msg("ignoring")
@ -344,7 +349,8 @@ func walkStdlib(ctx context.Context, output, format string) {
} }
defer f.Close() defer f.Close()
PrintDoc(ctx, f, pkg, val, format) pkg := Parse(ctx, pkgName, val)
fmt.Fprintf(f, "%s", pkg.Format(format))
return nil return nil
}) })