parsers/parameters.go (210 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. */ package parsers import ( "encoding/json" "fmt" "github.com/apache/openwhisk-wskdeploy/utils" "github.com/apache/openwhisk-wskdeploy/wskderrors" "github.com/apache/openwhisk-wskdeploy/wskenv" "reflect" ) // TODO(): Support other valid Package Manifest types // TODO(): i.e., timestamp, version, string256, string64, string16 // TODO(): Support JSON schema validation for type: json // TODO(): Support OpenAPI schema validation const ( STRING string = "string" INTEGER string = "integer" FLOAT string = "float" BOOLEAN string = "boolean" JSON string = "json" SLICE string = "slice" ) var validParameterNameMap = map[string]string{ STRING: STRING, FLOAT: FLOAT, BOOLEAN: BOOLEAN, INTEGER: INTEGER, "int": INTEGER, "bool": BOOLEAN, "int8": INTEGER, "int16": INTEGER, "int32": INTEGER, "int64": INTEGER, "float32": FLOAT, "float64": FLOAT, JSON: JSON, "map": JSON, "slice": SLICE, } var typeDefaultValueMap = map[string]interface{}{ STRING: "", INTEGER: 0, FLOAT: 0.0, BOOLEAN: false, JSON: make(map[string]interface{}), // TODO() Support these types + their validation // timestamp // null // version // string256 // string64 // string16 // scalar-unit // schema // object } func isValidParameterType(typeName string) bool { _, isValid := typeDefaultValueMap[typeName] return isValid } // TODO(): throw errors func getTypeDefaultValue(typeName string) interface{} { if val, ok := typeDefaultValueMap[typeName]; ok { return val } else { // TODO() throw an error "type not found" InvalidParameterType } return nil } func IsTypeDefaultValue(typeName string, value interface{}) bool { defaultValue := getTypeDefaultValue(typeName) if defaultValue == nil { return false } else if defaultValue == value { return true } return false } /* ResolveParamTypeFromValue Resolves the Parameter's data type from its actual value. Inputs: - paramName: name of the parameter for error reporting - filepath: the path, including name, of the YAML file which contained the parameter for error reporting - value: the parameter value to resolve Returns: - (string) parameter type name as a string */ func ResolveParamTypeFromValue(paramName string, value interface{}, filePath string) (string, error) { // Note: 'string' is the default type if not specified and not resolvable. var paramType string = "string" var err error = nil if value != nil { actualType := reflect.TypeOf(value).Kind().String() // See if the actual type of the value is valid if normalizedTypeName, found := validParameterNameMap[actualType]; found { // use the full spec. name paramType = normalizedTypeName } else { // raise an error if parameter's value is not a known type err = wskderrors.NewInvalidParameterTypeError(filePath, paramName, actualType) } } return paramType, err } /* resolveSingleLineParameter assures that a Parameter's Type is correctly identified and set from its Value. Additionally, this function: - detects if the parameter value contains the name of a valid OpenWhisk parameter types. if so, the - param.Type is set to detected OpenWhisk parameter type. - param.Value is set to the zero (default) value for that OpenWhisk parameter type. Inputs: - filePath: the path, including name, of the YAML file which contained the parameter for error reporting - paramName: name of the parameter for error reporting - param: pointer to Parameter structure being resolved Returns: - (interface{}) the parameter's resolved value */ func resolveSingleLineParameter(filePath string, paramName string, param *Parameter) (interface{}, error) { var errorParser error if !param.multiline { // We need to identify parameter Type here for later validation param.Type, errorParser = ResolveParamTypeFromValue(paramName, param.Value, filePath) // In single-line format, the param's <value> can be a "Type name" and NOT an actual value. // if this is the case, we must detect it and set the value to the default for that type name. if param.Value != nil && param.Type == "string" { // The value is a <string>; now we must test if is the name of a known Type if isValidParameterType(param.Value.(string)) { // If the value is indeed the name of a Type, we must change BOTH its // Type to be that type and its value to that Type's default value param.Type = param.Value.(string) param.Value = getTypeDefaultValue(param.Type) //fmt.Printf("EXIT: Parameter [%s] type=[%v] value=[%v]\n", paramName, param.Type, param.Value) } } } else { // TODO() - move string to i18n return param.Value, wskderrors.NewYAMLParserErr(filePath, "Parameter ["+paramName+"] is not single-line format.") } return param.Value, errorParser } /* resolveMultiLineParameter assures that the values for Parameter Type and Value are properly set and are valid. Additionally, this function: - uses param.Default as param.Value if param.Value is not provided - uses the actual param.Value data type for param.type if param.Type is not provided Inputs: - filepath: the path, including name, of the YAML file which contained the parameter for error reporting - paramName: name of the parameter for error reporting - param: pointer to Parameter structure being resolved Returns: - (interface{}) the parameter's resolved value */ func resolveMultiLineParameter(filePath string, paramName string, param *Parameter) (interface{}, error) { var errorParser error if param.multiline { var valueType string // if we do not have a value, but have a default, use it for the value if param.Value == nil && param.Default != nil { param.Value = param.Default } // Note: if either the value or default is in conflict with the type then this is an error valueType, errorParser = ResolveParamTypeFromValue(paramName, param.Value, filePath) // if we have a declared parameter Type, assure that it is a known value if param.Type != "" { if !isValidParameterType(param.Type) { // TODO() - move string to i18n return param.Value, wskderrors.NewYAMLParserErr(filePath, "Parameter ["+paramName+"] has an invalid Type. ["+param.Type+"]") } } else { // if we do not have a value for the Parameter Type, use the Parameter Value's Type param.Type = valueType } // TODO{} if the declared and actual parameter type conflict, generate TypeMismatch error //if param.Type != valueType{ // errorParser = utils.NewParameterTypeMismatchError("", param.Type, valueType ) //} } else { // TODO() - move string to i18n return param.Value, wskderrors.NewYAMLParserErr(filePath, "Parameter ["+paramName+"] is not multiline format.") } return param.Value, errorParser } func interpolateJSON(data map[string]interface{}) map[string]interface{} { for key, value := range data { if reflect.TypeOf(value).Kind() == reflect.String { data[key] = wskenv.InterpolateStringWithEnvVar(value) } else if reflect.TypeOf(value).Kind() == reflect.Map { data[key] = interpolateJSON(value.(map[string]interface{})) } } return data } /* resolveJSONParameter assure JSON data is converted to a map[string]{interface*} type. This function handles the forms JSON data appears in: 1) a string containing JSON, which needs to be parsed into map[string]interface{} 2) is a map of JSON (but not a map[string]interface{} Inputs: - paramName: name of the parameter for error reporting - filePath: the path, including name, of the YAML file which contained the parameter for error reporting - param: pointer to Parameter structure being resolved - value: the current actual value of the parameter being resolved Returns: - (interface{}) the parameter's resolved value */ func resolveJSONParameter(filePath string, paramName string, param *Parameter, value interface{}) (interface{}, error) { var errorParser error // TODO() Is the "value" function parameter really needed with the current logic (use param.Value)? if param.Type == "json" { // Case 1: if user set parameter type to 'json' and the value's type is a 'string' if str, ok := value.(string); ok { var parsed interface{} errParser := json.Unmarshal([]byte(str), &parsed) if errParser == nil { //fmt.Printf("EXIT: Parameter [%s] type=[%v] value=[%v]\n", paramName, param.Type, parsed) return parsed, errParser } } // Case 2: value contains a map of JSON // We must make sure the map type is map[string]interface{}; otherwise we cannot // marshall it later on to serialize in the body of an HTTP request. if param.Value != nil && reflect.TypeOf(param.Value).Kind() == reflect.Map { if _, ok := param.Value.(map[interface{}]interface{}); ok { var temp map[string]interface{} = utils.ConvertInterfaceMap(param.Value.(map[interface{}]interface{})) temp = interpolateJSON(temp) //fmt.Printf("EXIT: Parameter [%s] type=[%v] value=[%v]\n", paramName, param.Type, temp) return temp, errorParser } } else { errorParser = wskderrors.NewParameterTypeMismatchError(filePath, paramName, JSON, param.Type) } } else { // TODO() - move string to i18n errorParser = wskderrors.NewYAMLParserErr(filePath, "Parameter ["+paramName+"] is not JSON format.") } return param.Value, errorParser } /* ResolveParameter assures that the Parameter structure's values are correctly filled out for further processing. This includes special processing for - single-line format parameters - deriving missing param.Type from param.Value - resolving case where param.Value contains a valid Parameter type name - multi-line format parameters: - assures that param.Value is set while taking into account param.Default - validating param.Type Note: parameter values may set later (overridden) by an (optional) Deployment file Inputs: - paramName: name of the parameter for error reporting - filepath: the path, including name, of the YAML file which contained the parameter for error reporting - param: pointer to Parameter structure being resolved Returns: - (interface{}) the parameter's resolved value */ func ResolveParameter(paramName string, param *Parameter, filePath string) (interface{}, error) { var errorParser error // default resolved parameter value to empty string var value interface{} = "" // Trace Parameter struct before any resolution //dumpParameter(paramName, param, "BEFORE") // Parameters can be single OR multi-line declarations which must be processed/validated differently // Regardless, the following functions will assure that param.Value and param.Type are correctly set if !param.multiline { value, errorParser = resolveSingleLineParameter(filePath, paramName, param) } else { value, errorParser = resolveMultiLineParameter(filePath, paramName, param) } // String value pre-processing (interpolation) // See if we have any Environment Variable replacement within the parameter's value // Make sure the parameter's value is a valid, non-empty string if param.Value != nil && param.Type == "string" { // perform $ notation replacement on string if any exist value = wskenv.InterpolateStringWithEnvVar(param.Value) } // JSON - Handle both cases, where value 1) is a string containing JSON, 2) is a map of JSON if param.Value != nil && param.Type == "json" { value, errorParser = resolveJSONParameter(filePath, paramName, param, value) } if param.Value != nil && param.Type == "slice" { value = wskenv.InterpolateStringWithEnvVar(param.Value) value = utils.ConvertInterfaceValue(value) } // Default value to zero value for the Type // Do NOT error/terminate as Value may be provided later by a Deployment file. if value == nil { value = getTypeDefaultValue(param.Type) // @TODO(): Need warning message here to warn of default usage, support for warnings (non-fatal) //msgs := []string{"Parameter [" + paramName + "] is not multiline format."} //return param.Value, utils.NewParserErr(filePath, nil, msgs) } // Trace Parameter struct after resolution //dumpParameter(paramName, param, "AFTER") //fmt.Printf("EXIT: Parameter [%s] type=[%v] value=[%v]\n", paramName, param.Type, value) return value, errorParser } // Provide custom Parameter marshalling and unmarshalling type ParsedParameter Parameter func (n *Parameter) UnmarshalYAML(unmarshal func(interface{}) error) error { var aux ParsedParameter // Attempt to unmarshal the multi-line schema if err := unmarshal(&aux); err == nil { n.multiline = true n.Type = aux.Type n.Description = aux.Description n.Value = aux.Value n.Required = aux.Required n.Default = aux.Default n.Status = aux.Status n.Schema = aux.Schema return nil } // If we did not find the multi-line schema, assume in-line (or single-line) schema var inline interface{} if err := unmarshal(&inline); err != nil { return err } n.Value = inline n.multiline = false return nil } func (n *Parameter) MarshalYAML() (interface{}, error) { if _, ok := n.Value.(string); len(n.Type) == 0 && len(n.Description) == 0 && ok { if !n.Required && len(n.Status) == 0 && n.Schema == nil { return n.Value.(string), nil } } return n, nil } // Provides debug/trace support for Parameter type func dumpParameter(paramName string, param *Parameter, separator string) { fmt.Printf("%s:\n", separator) fmt.Printf("\t%s: (%T)\n", paramName, param) if param != nil { fmt.Printf("\t\tParameter.Description: [%s]\n", param.Description) fmt.Printf("\t\tParameter.Type: [%s]\n", param.Type) fmt.Printf("\t\t--> Actual Type: [%T]\n", param.Value) fmt.Printf("\t\tParameter.Value: [%v]\n", param.Value) fmt.Printf("\t\tParameter.Default: [%v]\n", param.Default) } }