2023-05-17 09:53:13 +00:00
|
|
|
package template
|
2023-05-17 00:43:41 +00:00
|
|
|
|
|
|
|
import (
|
2023-05-21 20:37:38 +00:00
|
|
|
"encoding/json"
|
2023-05-17 00:43:41 +00:00
|
|
|
"fmt"
|
2023-05-21 20:37:38 +00:00
|
|
|
"os"
|
2023-05-17 00:43:41 +00:00
|
|
|
"reflect"
|
|
|
|
)
|
|
|
|
|
2023-05-22 09:06:10 +00:00
|
|
|
const LatestSchemaVersion = 0
|
|
|
|
|
2023-05-23 16:44:09 +00:00
|
|
|
// This is a JSON Schema compliant struct that we use to do validation checks on
|
|
|
|
// the provided configuration
|
2023-05-22 09:06:10 +00:00
|
|
|
type Schema struct {
|
|
|
|
// A list of properties that can be used in the config
|
2023-05-23 16:44:09 +00:00
|
|
|
Properties map[string]Property `json:"properties"`
|
2023-05-22 09:06:10 +00:00
|
|
|
}
|
2023-05-17 00:43:41 +00:00
|
|
|
|
2023-05-23 16:44:09 +00:00
|
|
|
type PropertyType string
|
2023-05-17 00:43:41 +00:00
|
|
|
|
|
|
|
const (
|
2023-05-23 16:44:09 +00:00
|
|
|
PropertyTypeString = PropertyType("string")
|
|
|
|
PropertyTypeInt = PropertyType("integer")
|
|
|
|
PropertyTypeNumber = PropertyType("number")
|
|
|
|
PropertyTypeBoolean = PropertyType("boolean")
|
2023-05-17 00:43:41 +00:00
|
|
|
)
|
|
|
|
|
2023-05-23 16:44:09 +00:00
|
|
|
type Property struct {
|
|
|
|
Type PropertyType `json:"type"`
|
|
|
|
Description string `json:"description"`
|
2023-05-17 00:43:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2023-05-23 16:44:09 +00:00
|
|
|
// integers according to the schema
|
2023-05-17 00:43:41 +00:00
|
|
|
//
|
|
|
|
// Needed because the default json unmarshaller for maps converts all numbers to floats
|
2023-05-22 09:06:10 +00:00
|
|
|
func castFloatToInt(config map[string]any, schema *Schema) error {
|
2023-05-17 00:43:41 +00:00
|
|
|
for k, v := range config {
|
|
|
|
// error because all config keys should be defined in schema too
|
2023-05-22 09:06:10 +00:00
|
|
|
if _, ok := schema.Properties[k]; !ok {
|
2023-05-17 00:43:41 +00:00
|
|
|
return fmt.Errorf("%s is not defined as an input parameter for the template", k)
|
|
|
|
}
|
|
|
|
|
|
|
|
// skip non integer fields
|
2023-05-22 09:06:10 +00:00
|
|
|
fieldInfo := schema.Properties[k]
|
2023-05-23 16:44:09 +00:00
|
|
|
if fieldInfo.Type != PropertyTypeInt {
|
2023-05-17 00:43:41 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-05-23 16:44:09 +00:00
|
|
|
func validateType(v any, fieldType PropertyType) error {
|
2023-05-19 13:39:55 +00:00
|
|
|
validateFunc, ok := validators[fieldType]
|
|
|
|
if !ok {
|
|
|
|
return nil
|
2023-05-17 00:43:41 +00:00
|
|
|
}
|
2023-05-19 13:39:55 +00:00
|
|
|
return validateFunc(v)
|
2023-05-17 00:43:41 +00:00
|
|
|
}
|
|
|
|
|
2023-05-22 09:06:10 +00:00
|
|
|
func (schema *Schema) ValidateConfig(config map[string]any) error {
|
2023-05-17 12:42:24 +00:00
|
|
|
// validate types defined in config
|
2023-05-17 00:43:41 +00:00
|
|
|
for k, v := range config {
|
2023-05-22 09:06:10 +00:00
|
|
|
fieldMetadata, ok := schema.Properties[k]
|
2023-05-17 00:43:41 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2023-05-17 13:00:27 +00:00
|
|
|
// assert all fields are defined in
|
2023-05-22 09:06:10 +00:00
|
|
|
for k := range schema.Properties {
|
2023-05-17 13:00:27 +00:00
|
|
|
if _, ok := config[k]; !ok {
|
|
|
|
return fmt.Errorf("input parameter %s is not defined in config", k)
|
|
|
|
}
|
|
|
|
}
|
2023-05-17 00:43:41 +00:00
|
|
|
return nil
|
|
|
|
}
|
2023-05-21 20:37:38 +00:00
|
|
|
|
2023-05-22 09:06:10 +00:00
|
|
|
func ReadSchema(path string) (*Schema, error) {
|
2023-05-21 20:37:38 +00:00
|
|
|
schemaBytes, err := os.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-05-22 09:06:10 +00:00
|
|
|
schema := &Schema{}
|
|
|
|
err = json.Unmarshal(schemaBytes, schema)
|
2023-05-21 20:37:38 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return schema, nil
|
|
|
|
}
|
|
|
|
|
2023-05-22 09:06:10 +00:00
|
|
|
func (schema *Schema) ReadConfig(path string) (map[string]any, error) {
|
2023-05-21 20:37:38 +00:00
|
|
|
var config map[string]any
|
|
|
|
b, err := os.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
err = json.Unmarshal(b, &config)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// cast any fields that are supposed to be integers. The json unmarshalling
|
|
|
|
// for a generic map converts all numbers to floating point
|
|
|
|
err = castFloatToInt(config, schema)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// validate config according to schema
|
|
|
|
err = schema.ValidateConfig(config)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return config, nil
|
|
|
|
}
|