This change will vendor the new version of the exoscale-import post-processor component, but remove all of its code from Packer. After the v1.8.0 release this change should be removed entirely. This vendor process is being used as a workaround for decoupling the exoscale-import component without causing a breaking change in Packer. Users of Exoscale are encouraged to leverage `packer init` for installing the latest version of packer-plugin-exoscale.
358 lines
10 KiB
Go
358 lines
10 KiB
Go
package runtime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/deepmap/oapi-codegen/pkg/types"
|
|
"net/url"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func marshalDeepObject(in interface{}, path []string) ([]string, error) {
|
|
var result []string
|
|
|
|
switch t := in.(type) {
|
|
case []interface{}:
|
|
// For the array, we will use numerical subscripts of the form [x],
|
|
// in the same order as the array.
|
|
for i, iface := range t {
|
|
newPath := append(path, strconv.Itoa(i))
|
|
fields, err := marshalDeepObject(iface, newPath)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error traversing array")
|
|
}
|
|
result = append(result, fields...)
|
|
}
|
|
case map[string]interface{}:
|
|
// For a map, each key (field name) becomes a member of the path, and
|
|
// we recurse. First, sort the keys.
|
|
keys := make([]string, len(t))
|
|
i := 0
|
|
for k := range t {
|
|
keys[i] = k
|
|
i++
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
// Now, for each key, we recursively marshal it.
|
|
for _, k := range keys {
|
|
newPath := append(path, k)
|
|
fields, err := marshalDeepObject(t[k], newPath)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error traversing map")
|
|
}
|
|
result = append(result, fields...)
|
|
}
|
|
default:
|
|
// Now, for a concrete value, we will turn the path elements
|
|
// into a deepObject style set of subscripts. [a, b, c] turns into
|
|
// [a][b][c]
|
|
prefix := "[" + strings.Join(path, "][") + "]"
|
|
result = []string{
|
|
prefix + fmt.Sprintf("=%v", t),
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func MarshalDeepObject(i interface{}, paramName string) (string, error) {
|
|
// We're going to marshal to JSON and unmarshal into an interface{},
|
|
// which will use the json pkg to deal with all the field annotations. We
|
|
// can then walk the generic object structure to produce a deepObject. This
|
|
// isn't efficient and it would be more efficient to reflect on our own,
|
|
// but it's complicated, error-prone code.
|
|
buf, err := json.Marshal(i)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to marshal input to JSON")
|
|
}
|
|
var i2 interface{}
|
|
err = json.Unmarshal(buf, &i2)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to unmarshal JSON")
|
|
}
|
|
fields, err := marshalDeepObject(i2, nil)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "error traversing JSON structure")
|
|
}
|
|
|
|
// Prefix the param name to each subscripted field.
|
|
for i := range fields {
|
|
fields[i] = paramName + fields[i]
|
|
}
|
|
return strings.Join(fields, "&"), nil
|
|
}
|
|
|
|
type fieldOrValue struct {
|
|
fields map[string]fieldOrValue
|
|
value string
|
|
}
|
|
|
|
func (f *fieldOrValue) appendPathValue(path []string, value string) {
|
|
fieldName := path[0]
|
|
if len(path) == 1 {
|
|
f.fields[fieldName] = fieldOrValue{value: value}
|
|
return
|
|
}
|
|
|
|
pv, found := f.fields[fieldName]
|
|
if !found {
|
|
pv = fieldOrValue{
|
|
fields: make(map[string]fieldOrValue),
|
|
}
|
|
f.fields[fieldName] = pv
|
|
}
|
|
pv.appendPathValue(path[1:], value)
|
|
}
|
|
|
|
func makeFieldOrValue(paths [][]string, values []string) fieldOrValue {
|
|
|
|
f := fieldOrValue{
|
|
fields: make(map[string]fieldOrValue),
|
|
}
|
|
for i := range paths {
|
|
path := paths[i]
|
|
value := values[i]
|
|
f.appendPathValue(path, value)
|
|
}
|
|
return f
|
|
}
|
|
|
|
func UnmarshalDeepObject(dst interface{}, paramName string, params url.Values) error {
|
|
// Params are all the query args, so we need those that look like
|
|
// "paramName["...
|
|
var fieldNames []string
|
|
var fieldValues []string
|
|
searchStr := paramName + "["
|
|
for pName, pValues := range params {
|
|
if strings.HasPrefix(pName, searchStr) {
|
|
// trim the parameter name from the full name.
|
|
pName = pName[len(paramName):]
|
|
fieldNames = append(fieldNames, pName)
|
|
if len(pValues) != 1 {
|
|
return fmt.Errorf("%s has multiple values", pName)
|
|
}
|
|
fieldValues = append(fieldValues, pValues[0])
|
|
}
|
|
}
|
|
|
|
// Now, for each field, reconstruct its subscript path and value
|
|
paths := make([][]string, len(fieldNames))
|
|
for i, path := range fieldNames {
|
|
path = strings.TrimLeft(path, "[")
|
|
path = strings.TrimRight(path, "]")
|
|
paths[i] = strings.Split(path, "][")
|
|
}
|
|
|
|
fieldPaths := makeFieldOrValue(paths, fieldValues)
|
|
err := assignPathValues(dst, fieldPaths)
|
|
if err != nil {
|
|
return errors.Wrap(err, "error assigning value to destination")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// This returns a field name, either using the variable name, or the json
|
|
// annotation if that exists.
|
|
func getFieldName(f reflect.StructField) string {
|
|
n := f.Name
|
|
tag, found := f.Tag.Lookup("json")
|
|
if found {
|
|
// If we have a json field, and the first part of it before the
|
|
// first comma is non-empty, that's our field name.
|
|
parts := strings.Split(tag, ",")
|
|
if parts[0] != "" {
|
|
n = parts[0]
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// Create a map of field names that we'll see in the deepObject to reflect
|
|
// field indices on the given type.
|
|
func fieldIndicesByJsonTag(i interface{}) (map[string]int, error) {
|
|
t := reflect.TypeOf(i)
|
|
if t.Kind() != reflect.Struct {
|
|
return nil, errors.New("expected a struct as input")
|
|
}
|
|
|
|
n := t.NumField()
|
|
fieldMap := make(map[string]int)
|
|
for i := 0; i < n; i++ {
|
|
field := t.Field(i)
|
|
fieldName := getFieldName(field)
|
|
fieldMap[fieldName] = i
|
|
}
|
|
return fieldMap, nil
|
|
}
|
|
|
|
func assignPathValues(dst interface{}, pathValues fieldOrValue) error {
|
|
//t := reflect.TypeOf(dst)
|
|
v := reflect.ValueOf(dst)
|
|
|
|
iv := reflect.Indirect(v)
|
|
it := iv.Type()
|
|
|
|
switch it.Kind() {
|
|
case reflect.Slice:
|
|
sliceLength := len(pathValues.fields)
|
|
dstSlice := reflect.MakeSlice(it, sliceLength, sliceLength)
|
|
err := assignSlice(dstSlice, pathValues)
|
|
if err != nil {
|
|
return errors.Wrap(err, "error assigning slice")
|
|
}
|
|
iv.Set(dstSlice)
|
|
return nil
|
|
case reflect.Struct:
|
|
// Some special types we care about are structs. Handle them
|
|
// here. They may be redefined, so we need to do some hoop
|
|
// jumping. If the types are aliased, we need to type convert
|
|
// the pointer, then set the value of the dereference pointer.
|
|
|
|
// We check to see if the object implements the Binder interface first.
|
|
if dst, isBinder := v.Interface().(Binder); isBinder {
|
|
return dst.Bind(pathValues.value)
|
|
}
|
|
// Then check the legacy types
|
|
if it.ConvertibleTo(reflect.TypeOf(types.Date{})) {
|
|
var date types.Date
|
|
var err error
|
|
date.Time, err = time.Parse(types.DateFormat, pathValues.value)
|
|
if err != nil {
|
|
return errors.Wrap(err, "invalid date format")
|
|
}
|
|
dst := iv
|
|
if it != reflect.TypeOf(types.Date{}) {
|
|
// Types are aliased, convert the pointers.
|
|
ivPtr := iv.Addr()
|
|
aPtr := ivPtr.Convert(reflect.TypeOf(&types.Date{}))
|
|
dst = reflect.Indirect(aPtr)
|
|
}
|
|
dst.Set(reflect.ValueOf(date))
|
|
}
|
|
if it.ConvertibleTo(reflect.TypeOf(time.Time{})) {
|
|
var tm time.Time
|
|
var err error
|
|
tm, err = time.Parse(time.RFC3339Nano, pathValues.value)
|
|
if err != nil {
|
|
// Fall back to parsing it as a date.
|
|
tm, err = time.Parse(types.DateFormat, pathValues.value)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing tim as RFC3339 or 2006-01-02 time: %s", err)
|
|
}
|
|
return errors.Wrap(err, "invalid date format")
|
|
}
|
|
dst := iv
|
|
if it != reflect.TypeOf(time.Time{}) {
|
|
// Types are aliased, convert the pointers.
|
|
ivPtr := iv.Addr()
|
|
aPtr := ivPtr.Convert(reflect.TypeOf(&time.Time{}))
|
|
dst = reflect.Indirect(aPtr)
|
|
}
|
|
dst.Set(reflect.ValueOf(tm))
|
|
}
|
|
fieldMap, err := fieldIndicesByJsonTag(iv.Interface())
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed enumerating fields")
|
|
}
|
|
for _, fieldName := range sortedFieldOrValueKeys(pathValues.fields) {
|
|
fieldValue := pathValues.fields[fieldName]
|
|
fieldIndex, found := fieldMap[fieldName]
|
|
if !found {
|
|
return fmt.Errorf("field [%s] is not present in destination object", fieldName)
|
|
}
|
|
field := iv.Field(fieldIndex)
|
|
err = assignPathValues(field.Addr().Interface(), fieldValue)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error assigning field [%s]", fieldName)
|
|
}
|
|
}
|
|
return nil
|
|
case reflect.Ptr:
|
|
// If we have a pointer after redirecting, it means we're dealing with
|
|
// an optional field, such as *string, which was passed in as &foo. We
|
|
// will allocate it if necessary, and call ourselves with a different
|
|
// interface.
|
|
dstVal := reflect.New(it.Elem())
|
|
dstPtr := dstVal.Interface()
|
|
err := assignPathValues(dstPtr, pathValues)
|
|
iv.Set(dstVal)
|
|
return err
|
|
case reflect.Bool:
|
|
val, err := strconv.ParseBool(pathValues.value)
|
|
if err != nil {
|
|
return fmt.Errorf("expected a valid bool, got %s", pathValues.value)
|
|
}
|
|
iv.SetBool(val)
|
|
return nil
|
|
case reflect.Float32:
|
|
val, err := strconv.ParseFloat(pathValues.value, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("expected a valid float, got %s", pathValues.value)
|
|
}
|
|
iv.SetFloat(val)
|
|
return nil
|
|
case reflect.Float64:
|
|
val, err := strconv.ParseFloat(pathValues.value, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("expected a valid float, got %s", pathValues.value)
|
|
}
|
|
iv.SetFloat(val)
|
|
return nil
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
val, err := strconv.ParseInt(pathValues.value, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("expected a valid int, got %s", pathValues.value)
|
|
}
|
|
iv.SetInt(val)
|
|
return nil
|
|
case reflect.String:
|
|
iv.SetString(pathValues.value)
|
|
return nil
|
|
default:
|
|
return errors.New("unhandled type: " + it.String())
|
|
}
|
|
}
|
|
|
|
func assignSlice(dst reflect.Value, pathValues fieldOrValue) error {
|
|
// Gather up the values
|
|
nValues := len(pathValues.fields)
|
|
values := make([]string, nValues)
|
|
// We expect to have consecutive array indices in the map
|
|
for i := 0; i < nValues; i++ {
|
|
indexStr := strconv.Itoa(i)
|
|
fv, found := pathValues.fields[indexStr]
|
|
if !found {
|
|
return errors.New("array deepObjects must have consecutive indices")
|
|
}
|
|
values[i] = fv.value
|
|
}
|
|
|
|
// This could be cleaner, but we can call into assignPathValues to
|
|
// avoid recreating this logic.
|
|
for i := 0; i < nValues; i++ {
|
|
dstElem := dst.Index(i).Addr()
|
|
err := assignPathValues(dstElem.Interface(), fieldOrValue{value: values[i]})
|
|
if err != nil {
|
|
return errors.Wrap(err, "error binding array")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func sortedFieldOrValueKeys(m map[string]fieldOrValue) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|