First draft

This commit is contained in:
Shreyas Goenka 2023-05-17 02:43:41 +02:00
parent 1a431dd26c
commit 9ef3547a3a
No known key found for this signature in database
GPG Key ID: 92A07DF49CCB0622
6 changed files with 237 additions and 125 deletions

View File

@ -1,21 +0,0 @@
{
"foo": {
"type": "string",
"default": "abc",
"validation": ["regex ^[abcd]*$"]
},
"bar": {
"type": "integer",
"default": 123,
"validation": ["greaterThan 5", "lessThan 10"]
},
"isAws": {
"type": "boolean",
"default": true
},
"project_name": {
"type": "string",
"default": "my_project",
"validation": ["startsWith my_"]
}
}

101
cmd/init/execute.go Normal file
View File

@ -0,0 +1,101 @@
package init
import (
"os"
"path/filepath"
"strings"
"text/template"
)
// Executes the template by appling config on it. Returns the materialized config
// as a string
func executeTemplate(config map[string]any, templateDefination string) (string, error) {
// configure template with helper functions
tmpl, err := template.New("foo").Funcs(HelperFuncs).Parse(templateDefination)
if err != nil {
return "", err
}
// execute template
result := strings.Builder{}
err = tmpl.Execute(&result, config)
if err != nil {
return "", err
}
return result.String(), nil
}
// TODO: allow skipping directories
func generateDirectory(config map[string]any, parentDir, nameTempate string) (string, error) {
dirName, err := executeTemplate(config, nameTempate)
if err != nil {
return "", err
}
err = os.Mkdir(filepath.Join(parentDir, dirName), 0755)
if err != nil {
return "", err
}
return dirName, nil
}
func generateFile(config map[string]any, parentDir, nameTempate, contentTemplate string) error {
// compute file content
fileContent, err := executeTemplate(config, contentTemplate)
// TODO: maybe we need string matching here to make this work
if err != nil && err == ErrSkipThisFile {
return nil
}
if err != nil {
return err
}
// create the file by executing the templatized file name
fileName, err := executeTemplate(config, nameTempate)
if err != nil {
return err
}
f, err := os.Create(filepath.Join(parentDir, fileName))
if err != nil {
return err
}
// write to file the computed content
_, err = f.Write([]byte(fileContent))
return err
}
func walkFileTree(config map[string]any, templatePath, instancePath string) error {
enteries, err := os.ReadDir(templatePath)
if err != nil {
return err
}
for _, entry := range enteries {
if entry.IsDir() {
// case: materialize a template directory
dirName, err := generateDirectory(config, instancePath, entry.Name())
if err != nil {
return err
}
// recusive generate files and directories inside inside our newly generated
// directory from the template defination
err = walkFileTree(config, filepath.Join(templatePath, entry.Name()), filepath.Join(instancePath, dirName))
if err != nil {
return err
}
} else {
// case: materialize a template file with it's contents
b, err := os.ReadFile(filepath.Join(templatePath, entry.Name()))
if err != nil {
return err
}
contentTemplate := string(b)
fileNameTemplate := entry.Name()
err = generateFile(config, instancePath, fileNameTemplate, contentTemplate)
if err != nil {
return err
}
}
}
return nil
}

View File

@ -1,74 +0,0 @@
package init
import (
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
)
// TODO: cleanup if template initialization fails
// TODO: need robust way to clean up half generated files
// TODO: define default files
// TODO: self reference for
func walkFileTree(config map[string]interface{}, templatePath string, instancePath string) error {
enteries, err := os.ReadDir(templatePath)
if err != nil {
return err
}
for _, entry := range enteries {
if entry.Name() == SchemaFileName {
continue
}
fileName := entry.Name()
tmpl, err := template.New("filename").Parse(fileName)
if err != nil {
return err
}
result := strings.Builder{}
err = tmpl.Execute(&result, config)
if err != nil {
return err
}
resolvedFileName := result.String()
fmt.Println(resolvedFileName)
if entry.IsDir() {
err := os.Mkdir(resolvedFileName, os.ModePerm)
if err != nil {
return err
}
err = walkFileTree(config, filepath.Join(templatePath, fileName), filepath.Join(instancePath, resolvedFileName))
if err != nil {
return err
}
} else {
f, err := os.Create(filepath.Join(instancePath, resolvedFileName))
if err != nil {
return err
}
b, err := os.ReadFile(filepath.Join(templatePath, fileName))
if err != nil {
return err
}
// TODO: Might be able to use ParseFiles or ParseFS. Might be more suited
contentTmpl, err := template.New("content").Funcs(HelperFuncs).Parse(string(b))
if err != nil {
return err
}
err = contentTmpl.Execute(f, config)
// Make this assertion more robust
if err != nil && strings.Contains(err.Error(), ErrSkipThisFile.Error()) {
err := os.Remove(filepath.Join(instancePath, resolvedFileName))
if err != nil {
return err
}
} else if err != nil {
return err
}
}
}
return nil
}

View File

@ -2,8 +2,6 @@ package init
import (
"errors"
"fmt"
"strings"
"text/template"
)
@ -13,19 +11,4 @@ var HelperFuncs = template.FuncMap{
"skipThisFile": func() error {
panic(ErrSkipThisFile)
},
"eqString": func(a string, b string) bool {
return a == b
},
"eqNumber": func(a float64, b int) bool {
return int(a) == b
},
"validationError": func(message string) error {
panic(fmt.Errorf(message))
},
"assertStartsWith": func(s string, substr string) error {
if !strings.HasPrefix(s, substr) {
panic(fmt.Errorf("%s does not start with %s.", s, substr))
}
return nil
},
}

View File

@ -3,24 +3,38 @@ package init
import (
"encoding/json"
"os"
"path/filepath"
"github.com/databricks/bricks/cmd/root"
"github.com/spf13/cobra"
)
const SchemaFileName = "config.json"
const ConfigFileName = "config.json"
const SchemaFileName = "schema.json"
const TemplateDirname = "template"
// root template defination at schema.json
// decide on semantics of defination later
// initCmd represents the fs command
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize Template",
Long: `Initialize bundle template`,
Long: `Initialize template`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
templateLocation := args[0]
// read the file containing schema for template input parameters
schemaBytes, err := os.ReadFile(filepath.Join(templateLocation, SchemaFileName))
if err != nil {
return err
}
schema := Schema{}
err = json.Unmarshal(schemaBytes, &schema)
if err != nil {
return err
}
// read user config to initalize the template with
var config map[string]interface{}
b, err := os.ReadFile(SchemaFileName)
b, err := os.ReadFile(ConfigFileName)
if err != nil {
return err
}
@ -28,15 +42,22 @@ var initCmd = &cobra.Command{
if err != nil {
return err
}
err = walkFileTree(config, ".", ".")
// cast any fields that are supported to be integers. The json unmarshalling
// for a generic map converts all numbers to floating point
err = schema.CastFloatToInt(config)
if err != nil {
err2 := os.RemoveAll("favela")
if err2 != nil {
return err2
}
return err
}
return nil
// validate config according to schema
err = schema.ValidateConfig(config)
if err != nil {
return err
}
// materialize the template
return walkFileTree(config, filepath.Join(args[0], TemplateDirname), ".")
},
}

102
cmd/init/schema.go Normal file
View File

@ -0,0 +1,102 @@
package init
import (
"fmt"
"reflect"
)
type Schema map[string]FieldInfo
type FieldType string
const (
FieldTypeString = FieldType("string")
FieldTypeInt = FieldType("integer")
FieldTypeFloat = FieldType("float")
FieldTypeBoolean = FieldType("boolean")
)
type FieldInfo struct {
Type FieldType `json:"type"`
Description string `json:"description"`
Validation string `json:"validate"`
}
// function to check whether a float value represents an integer
func isIntegerValue(v float64) bool {
return v == float64(int(v))
}
// cast value to integer for config values that are floats but are supposed to be
// integeres according to the schema
//
// Needed because the default json unmarshaller for maps converts all numbers to floats
func (schema Schema) CastFloatToInt(config map[string]any) error {
for k, v := range config {
// error because all config keys should be defined in schema too
if _, ok := schema[k]; !ok {
return fmt.Errorf("%s is not defined as an input parameter for the template", k)
}
// skip non integer fields
fieldInfo := schema[k]
if fieldInfo.Type != FieldTypeInt {
continue
}
// convert floating point type values to integer
valueType := reflect.TypeOf(v)
switch valueType.Kind() {
case reflect.Float32:
floatVal := v.(float32)
if !isIntegerValue(float64(floatVal)) {
return fmt.Errorf("expected %s to have integer value but it is %v", k, v)
}
config[k] = int(floatVal)
case reflect.Float64:
floatVal := v.(float64)
if !isIntegerValue(floatVal) {
return fmt.Errorf("expected %s to have integer value but it is %v", k, v)
}
config[k] = int(floatVal)
}
}
return nil
}
func validateType(v any, fieldType FieldType) error {
switch fieldType {
case FieldTypeString:
if _, ok := v.(string); !ok {
return fmt.Errorf("expected type string, but value is %#v", v)
}
case FieldTypeInt:
if _, ok := v.(int); !ok {
return fmt.Errorf("expected type integer, but value is %#v", v)
}
case FieldTypeFloat:
if _, ok := v.(float64); !ok {
return fmt.Errorf("expected type float, but value is %#v", v)
}
case FieldTypeBoolean:
if _, ok := v.(bool); !ok {
return fmt.Errorf("expected type boolean, but value is %#v", v)
}
}
return nil
}
// TODO: add validation check for regex for string types
func (schema Schema) ValidateConfig(config map[string]any) error {
for k, v := range config {
fieldMetadata, ok := schema[k]
if !ok {
return fmt.Errorf("%s is not defined as an input parameter for the template", k)
}
err := validateType(v, fieldMetadata.Type)
if err != nil {
return fmt.Errorf("incorrect type for %s. %w", k, err)
}
}
return nil
}