mirror of https://github.com/databricks/cli.git
258 lines
6.6 KiB
Go
258 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
yaml3 "gopkg.in/yaml.v3"
|
|
|
|
"github.com/databricks/cli/libs/dyn"
|
|
"github.com/databricks/cli/libs/dyn/convert"
|
|
"github.com/databricks/cli/libs/dyn/merge"
|
|
"github.com/databricks/cli/libs/dyn/yamlloader"
|
|
"github.com/databricks/cli/libs/dyn/yamlsaver"
|
|
"github.com/databricks/cli/libs/jsonschema"
|
|
)
|
|
|
|
type annotation struct {
|
|
Description string `json:"description,omitempty"`
|
|
MarkdownDescription string `json:"markdown_description,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
Default any `json:"default,omitempty"`
|
|
Enum []any `json:"enum,omitempty"`
|
|
}
|
|
|
|
type annotationHandler struct {
|
|
// Annotations read from all annotation files including all overrides
|
|
parsedAnnotations annotationFile
|
|
// Missing annotations for fields that are found in config that need to be added to the annotation file
|
|
missingAnnotations annotationFile
|
|
}
|
|
|
|
/**
|
|
* Parsed file with annotations, expected format:
|
|
* github.com/databricks/cli/bundle/config.Bundle:
|
|
* cluster_id:
|
|
* description: "Description"
|
|
*/
|
|
type annotationFile map[string]map[string]annotation
|
|
|
|
const Placeholder = "PLACEHOLDER"
|
|
|
|
// Adds annotations to the JSON schema reading from the annotation files.
|
|
// More details https://json-schema.org/understanding-json-schema/reference/annotations
|
|
func newAnnotationHandler(sources []string) (*annotationHandler, error) {
|
|
prev := dyn.NilValue
|
|
for _, path := range sources {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
generated, err := yamlloader.LoadYAML(path, bytes.NewBuffer(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
prev, err = merge.Merge(prev, generated)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var data annotationFile
|
|
|
|
err := convert.ToTyped(&data, prev)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
d := &annotationHandler{}
|
|
d.parsedAnnotations = data
|
|
d.missingAnnotations = annotationFile{}
|
|
return d, nil
|
|
}
|
|
|
|
func (d *annotationHandler) addAnnotations(typ reflect.Type, s jsonschema.Schema) jsonschema.Schema {
|
|
refPath := getPath(typ)
|
|
shouldHandle := strings.HasPrefix(refPath, "github.com")
|
|
if !shouldHandle {
|
|
return s
|
|
}
|
|
|
|
annotations := d.parsedAnnotations[refPath]
|
|
if annotations == nil {
|
|
annotations = map[string]annotation{}
|
|
}
|
|
|
|
rootTypeAnnotation, ok := annotations[RootTypeKey]
|
|
if ok {
|
|
assignAnnotation(&s, rootTypeAnnotation)
|
|
}
|
|
|
|
for k, v := range s.Properties {
|
|
item := annotations[k]
|
|
if item.Description == "" {
|
|
item.Description = Placeholder
|
|
|
|
emptyAnnotations := d.missingAnnotations[refPath]
|
|
if emptyAnnotations == nil {
|
|
emptyAnnotations = map[string]annotation{}
|
|
d.missingAnnotations[refPath] = emptyAnnotations
|
|
}
|
|
emptyAnnotations[k] = item
|
|
}
|
|
assignAnnotation(v, item)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Writes missing annotations with placeholder values back to the annotation file
|
|
func (d *annotationHandler) syncWithMissingAnnotations(outputPath string) error {
|
|
existingFile, err := os.ReadFile(outputPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
existing, err := yamlloader.LoadYAML("", bytes.NewBuffer(existingFile))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for k := range d.missingAnnotations {
|
|
if !isCliPath(k) {
|
|
delete(d.missingAnnotations, k)
|
|
fmt.Printf("Missing annotations for `%s` that are not in CLI package, try to fetch latest OpenAPI spec and regenerate annotations", k)
|
|
}
|
|
}
|
|
|
|
missingAnnotations, err := convert.FromTyped(d.missingAnnotations, dyn.NilValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
output, err := merge.Merge(existing, missingAnnotations)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var outputTyped annotationFile
|
|
err = convert.ToTyped(&outputTyped, output)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = saveYamlWithStyle(outputPath, outputTyped)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getPath(typ reflect.Type) string {
|
|
return typ.PkgPath() + "." + typ.Name()
|
|
}
|
|
|
|
func assignAnnotation(s *jsonschema.Schema, a annotation) {
|
|
if a.Description != Placeholder {
|
|
s.Description = a.Description
|
|
}
|
|
|
|
if a.Default != nil {
|
|
s.Default = a.Default
|
|
}
|
|
s.MarkdownDescription = convertLinksToAbsoluteUrl(a.MarkdownDescription)
|
|
s.Title = a.Title
|
|
s.Enum = a.Enum
|
|
}
|
|
|
|
func saveYamlWithStyle(outputPath string, annotations annotationFile) error {
|
|
annotationOrder := yamlsaver.NewOrder([]string{"description", "markdown_description", "title", "default", "enum"})
|
|
style := map[string]yaml3.Style{}
|
|
|
|
order := getAlphabeticalOrder(annotations)
|
|
dynMap := map[string]dyn.Value{}
|
|
for k, v := range annotations {
|
|
style[k] = yaml3.LiteralStyle
|
|
|
|
properties := map[string]dyn.Value{}
|
|
propertiesOrder := getAlphabeticalOrder(v)
|
|
for key, value := range v {
|
|
d, err := convert.FromTyped(value, dyn.NilValue)
|
|
if d.Kind() == dyn.KindNil || err != nil {
|
|
properties[key] = dyn.NewValue(map[string]dyn.Value{}, []dyn.Location{{Line: propertiesOrder.Get(key)}})
|
|
continue
|
|
}
|
|
val, err := yamlsaver.ConvertToMapValue(value, annotationOrder, []string{}, map[string]dyn.Value{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
properties[key] = val.WithLocations([]dyn.Location{{Line: propertiesOrder.Get(key)}})
|
|
}
|
|
|
|
dynMap[k] = dyn.NewValue(properties, []dyn.Location{{Line: order.Get(k)}})
|
|
}
|
|
|
|
saver := yamlsaver.NewSaverWithStyle(style)
|
|
err := saver.SaveAsYAML(dynMap, outputPath, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getAlphabeticalOrder[T any](mapping map[string]T) *yamlsaver.Order {
|
|
order := []string{}
|
|
for k := range mapping {
|
|
order = append(order, k)
|
|
}
|
|
slices.Sort(order)
|
|
return yamlsaver.NewOrder(order)
|
|
}
|
|
|
|
func convertLinksToAbsoluteUrl(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
base := "https://docs.databricks.com"
|
|
referencePage := "/dev-tools/bundles/reference.html"
|
|
|
|
// Regular expression to match Markdown-style links like [_](link)
|
|
re := regexp.MustCompile(`\[_\]\(([^)]+)\)`)
|
|
result := re.ReplaceAllStringFunc(s, func(match string) string {
|
|
matches := re.FindStringSubmatch(match)
|
|
if len(matches) < 2 {
|
|
return match
|
|
}
|
|
link := matches[1]
|
|
var text, absoluteURL string
|
|
|
|
if strings.HasPrefix(link, "#") {
|
|
text = strings.TrimPrefix(link, "#")
|
|
absoluteURL = fmt.Sprintf("%s%s%s", base, referencePage, link)
|
|
|
|
// Handle relative paths like /dev-tools/bundles/resources.html#dashboard
|
|
} else if strings.HasPrefix(link, "/") {
|
|
absoluteURL = strings.ReplaceAll(fmt.Sprintf("%s%s", base, link), ".md", ".html")
|
|
if strings.Contains(link, "#") {
|
|
parts := strings.Split(link, "#")
|
|
text = parts[1]
|
|
} else {
|
|
text = "link"
|
|
}
|
|
} else {
|
|
return match
|
|
}
|
|
|
|
return fmt.Sprintf("[%s](%s)", text, absoluteURL)
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
func isCliPath(path string) bool {
|
|
return !strings.HasPrefix(path, "github.com/databricks/databricks-sdk-go")
|
|
}
|