scripts/generate_schema_docs/main.go (160 lines of code) (raw):

// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Generates markdown files for schemas. // Meant to be run from the repo root like so: // go run ./scripts/generate_schema_docs package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "log" "os" "path/filepath" "regexp" "strings" "github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/hcl" "github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/policygen" "github.com/GoogleCloudPlatform/healthcare-data-protection-suite/internal/tfengine" ) const ( recipesDir = "./templates/tfengine/recipes" docsDir = "./docs" ) var schemaRE = regexp.MustCompile(`(?s)(?:schema = {)(.+?)(?:}\n\n)`) func main() { if err := run(); err != nil { log.Fatal(err) } } func run() error { if err := writeSchema([]byte(tfengine.Schema), filepath.Join(docsDir, "tfengine/schemas/config.md")); err != nil { return err } if err := writeSchema([]byte(policygen.Schema), filepath.Join(docsDir, "policygen/schemas/config.md")); err != nil { return err } if err := generateRecipeSchemaDocs(); err != nil { return err } return nil } func generateRecipeSchemaDocs() error { outputDir := filepath.Join(docsDir, "tfengine/schemas") fn := func(path string, info os.FileInfo, err error) error { if err != nil { return err } matches, err := findMatches(path) if err != nil { return err } if len(matches) == 0 { return nil } outPath := filepath.Join(outputDir, strings.Replace(info.Name(), ".hcl", ".md", 1)) return writeSchema(matches[1], outPath) } return filepath.Walk(recipesDir, fn) } // findMatches extracts the schema from an HCL recipe. // Matches will be empty if there is no schema or the file is not HCL. func findMatches(path string) ([][]byte, error) { if filepath.Ext(path) != ".hcl" { return nil, nil } b, err := ioutil.ReadFile(path) if err != nil { return nil, err } matches := schemaRE.FindSubmatch(b) if l := len(matches); l != 0 && l != 2 { return nil, fmt.Errorf("unexpected number of matches: got %q, want 0 or 2", len(matches)) } return matches, nil } func schemaFromHCL(b []byte) (*schema, error) { sj, err := hcl.ToJSON(b) if err != nil { return nil, err } s := new(schema) if err := json.Unmarshal(sj, s); err != nil { return nil, err } massageSchema(s) return s, nil } // massageSchema prepares the schema for templating. func massageSchema(s *schema) { props := s.Properties s.Properties = make(map[string]*property, len(props)) addRequiredByParent(props, requiredToMap(s.Required)) flattenObjects(s, props, "", false) flattenObjects(s, s.PatternProperties, "", true) for _, prop := range s.Properties { prop.Description = strings.TrimSpace(lstrip(prop.Description)) prop.Pattern = strings.ReplaceAll(prop.Pattern, "|", `\|`) } } // addRequiredByParent traverses the schema and adds a requiredByParent // flag indicating that field is listed as required at the previous level func addRequiredByParent(props map[string]*property, requiredFieldsByParent map[string]bool) { for name, prop := range props { if _, ok := requiredFieldsByParent[name]; ok { prop.RequiredByParent = true } switch prop.Type { case "object": addRequiredByParent(prop.Properties, requiredToMap(prop.Required)) case "array": addRequiredByParent(prop.Items.Properties, requiredToMap(prop.Items.Required)) } } } // requiredToMap converts a required array to a map of booleans func requiredToMap(required []string) map[string]bool { requiredMap := make(map[string]bool, len(required)) for _, field := range required { requiredMap[field] = true } return requiredMap } // flattenObjects will add the properties of all objects to the top level schema. func flattenObjects(s *schema, props map[string]*property, prefix string, nameIsPattern bool) { for name, prop := range props { if nameIsPattern { prop.Pattern = name name = "*pattern*" } name = prefix + name s.Properties[name] = prop switch prop.Type { case "object": flattenObjects(s, prop.Properties, name+".", false) flattenObjects(s, prop.PatternProperties, name+".", true) case "array": prop.Type = fmt.Sprintf("array(%s)", prop.Items.Type) flattenObjects(s, prop.Items.Properties, name+".", false) flattenObjects(s, prop.Items.PatternProperties, name+".", true) } } } // lstrip trims left space from all lines. func lstrip(s string) string { var b strings.Builder for _, line := range strings.Split(s, "\n\n") { b.WriteString(strings.TrimLeft(line, " ")) b.WriteString("<br><br>") } formattedBreaklines := b.String() formattedBreaklines = strings.TrimRight(formattedBreaklines, "<br>") var result string for _, line := range strings.Split(formattedBreaklines, "\n") { result += strings.TrimLeft(line, " ") result += " " } return result } func writeSchema(b []byte, outPath string) error { s, err := schemaFromHCL(b) if err != nil { return err } buf := new(bytes.Buffer) if err := tmpl.Execute(buf, s); err != nil { return err } if err := ioutil.WriteFile(outPath, buf.Bytes(), 0755); err != nil { return fmt.Errorf("write %q: %v", outPath, err) } return nil }