| /* |
| * 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) |
| } |
| } |