Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 215
Custom annotations for bundle-specific JSON schema fields#1957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
9b588c012c4373049e11b8016d410221c28a3fdc8d79a88e9c3d049fb076813073aecaa6c45d5d16496800164a8a579b1ce15107f9a5503716042b50171513d2bfb678f5969540505b8d3b30f743c4b58aed0e0a51948898a33fb36bc96641b31c097fb1ed89f3ec3d3231cffc619a73304918400376717d8ae4834d7484386c7d3835fb05File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| package main | ||
| import ( | ||
| "bytes" | ||
| "fmt" | ||
| "os" | ||
| "reflect" | ||
| "regexp" | ||
| "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 { | ||
pietern marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // 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 | ||
pietern marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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 { | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which types don't have this prefix? ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some primitive types like | ||
| 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 | ||
| } | ||
| missingAnnotations, err := convert.FromTyped(&d.missingAnnotations, dyn.NilValue) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| output, err := merge.Merge(existing, missingAnnotations) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| err = saveYamlWithStyle(outputPath, output) | ||
| 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, input dyn.Value) error { | ||
| style := map[string]yaml3.Style{} | ||
| file, _ := input.AsMap() | ||
| for _, v := range file.Keys() { | ||
| style[v.MustString()] = yaml3.LiteralStyle | ||
| } | ||
| saver := yamlsaver.NewSaverWithStyle(style) | ||
| err := saver.SaveAsYAML(file, outputPath, true) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return nil | ||
| } | ||
| 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 | ||
| } | ||
pietern marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this need the path to itself?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I passed this as argument to avoid hardcoding relative paths in Go (to keep an ability to execute both from
make schemaand from other places like Run from IDE). Other possible option to keep portability is to useembedbut I also need write operations so it seems like not an option in my case