doc generation: refactor to allow multi-stage processing
Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
This commit is contained in:
parent
b8f9c0ee67
commit
ee8bcfafaa
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user