2017-06-26 23:46:34 +02:00
|
|
|
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
Implement mapper.Encode "for real"
This change implements `mapper.Encode` "for real" (that is, in a way
that isn't a complete embarrassment). It uses the obvious reflection
trickery to encode a tagged struct and its values as a JSON-like
in-memory map and collection of keyed values.
During this, I took the opportunity to also clean up a few other things
that had been bugging me. Namely, the presence of `mapper.Object` was
always error prone, since it isn't a true "typedef" in the sence that
it carries extra RTTI. Instead of doing that, let's just use the real
`map[string]interface{}` "JSON-map-like" object type. Even better, we
no longer require resource providers to deal with the mapper
infrastructure. Instead, the `Check` function can simply return an
array of errors. It's still best practice to return field-specific errors
to facilitate better diagnostics, but it's no longer required; and I've
added `resource.NewFieldError` to eliminate the need to import mapper.
As of this change, we can also consistently emit RPC structs with `lumi`
tags, rather than `lumi` tags on the way in and `json` on the way out.
This completes pulumi/lumi#183.
2017-06-06 02:43:52 +02:00
|
|
|
|
|
|
|
package mapper
|
|
|
|
|
|
|
|
import (
|
|
|
|
"reflect"
|
|
|
|
|
|
|
|
"github.com/pulumi/lumi/pkg/util/contract"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Encode encodes a strongly typed struct into a weakly typed JSON-like property bag.
|
|
|
|
func (md *mapper) Encode(source interface{}) (map[string]interface{}, MappingError) {
|
|
|
|
// Fetch the type and value; if it's a pointer, do a quick nil check, otherwise operate on its underlying type.
|
|
|
|
vsrc := reflect.ValueOf(source)
|
|
|
|
vsrcType := vsrc.Type()
|
|
|
|
if vsrcType.Kind() == reflect.Ptr {
|
|
|
|
if vsrc.IsNil() {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
vsrc = vsrc.Elem()
|
|
|
|
vsrcType = vsrc.Type()
|
|
|
|
}
|
|
|
|
contract.Assertf(vsrcType.Kind() == reflect.Struct,
|
|
|
|
"Source %v must be a struct type with `lumi:\"x\"` tags to direct encoding (kind %v)",
|
|
|
|
vsrcType, vsrcType.Kind())
|
|
|
|
|
|
|
|
// Fetch the source type, allocate a fresh object, and start encoding into it.
|
|
|
|
var errs []error
|
|
|
|
obj := make(map[string]interface{})
|
|
|
|
for _, fldtag := range md.structFieldsTags(vsrc.Type()) {
|
|
|
|
if !fldtag.Skip {
|
|
|
|
key := fldtag.Key
|
|
|
|
fld := vsrc.FieldByName(fldtag.Info.Name)
|
|
|
|
v, err := md.EncodeValue(fld.Interface())
|
|
|
|
if err != nil {
|
|
|
|
errs = append(errs, err.Failures()...)
|
|
|
|
} else if v == nil {
|
|
|
|
if !fldtag.Optional && !md.opts.IgnoreMissing {
|
|
|
|
// The field doesn't exist and yet it is required; issue an error.
|
|
|
|
errs = append(errs, NewMissingError(vsrcType, key))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
obj[key] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there are no errors, return nil; else manufacture a decode error object.
|
|
|
|
var err MappingError
|
|
|
|
if len(errs) > 0 {
|
|
|
|
err = NewMappingError(errs)
|
|
|
|
}
|
|
|
|
return obj, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// EncodeValue decodes primitive type fields. For fields of complex types, we use custom deserialization.
|
|
|
|
func (md *mapper) EncodeValue(v interface{}) (interface{}, MappingError) {
|
|
|
|
vsrc := reflect.ValueOf(v)
|
|
|
|
contract.Assert(vsrc.IsValid())
|
|
|
|
|
|
|
|
// Otherwise, try to map to the closest JSON-like destination type we can.
|
|
|
|
switch k := vsrc.Kind(); k {
|
|
|
|
// Primitive types:
|
|
|
|
case reflect.Bool:
|
|
|
|
return vsrc.Bool(), nil
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
|
|
return float64(vsrc.Int()), nil
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
|
|
return float64(vsrc.Uint()), nil
|
|
|
|
case reflect.Float32, reflect.Float64:
|
Tidy up more lint
This change fixes a few things:
* Most importantly, we need to place a leading "." in the paths
to Gometalinter, otherwise some sub-linters just silently skip
the directory altogether. errcheck is one such linter, which
is a very important one!
* Use an explicit Gometalinter.json file to configure the various
settings. This flips on a few additional linters that aren't
on by default (line line length checking). Sadly, a few that
I'd like to enable take waaaay too much time, so in the future
we may consider a nightly job (this includes code similarity,
unused parameters, unused functions, and others that generally
require global analysis).
* Now that we're running more, however, linting takes a while!
The core Lumi project now takes 26 seconds to lint on my laptop.
That's not terrible, but it's long enough that we don't want to
do the silly "run them twice" thing our Makefiles were previously
doing. Instead, we shall deploy some $$($${PIPESTATUS[1]}-1))-fu
to rely on the fact that grep returns 1 on "zero lines".
* Finally, fix the many issues that this turned up.
I think(?) we are done, except, of course, for needing to drive
down some of the cyclomatic complexity issues (which I'm possibly
going to punt on; see pulumi/lumi#259 for more details).
2017-06-22 21:09:46 +02:00
|
|
|
return vsrc.Float(), nil
|
Implement mapper.Encode "for real"
This change implements `mapper.Encode` "for real" (that is, in a way
that isn't a complete embarrassment). It uses the obvious reflection
trickery to encode a tagged struct and its values as a JSON-like
in-memory map and collection of keyed values.
During this, I took the opportunity to also clean up a few other things
that had been bugging me. Namely, the presence of `mapper.Object` was
always error prone, since it isn't a true "typedef" in the sence that
it carries extra RTTI. Instead of doing that, let's just use the real
`map[string]interface{}` "JSON-map-like" object type. Even better, we
no longer require resource providers to deal with the mapper
infrastructure. Instead, the `Check` function can simply return an
array of errors. It's still best practice to return field-specific errors
to facilitate better diagnostics, but it's no longer required; and I've
added `resource.NewFieldError` to eliminate the need to import mapper.
As of this change, we can also consistently emit RPC structs with `lumi`
tags, rather than `lumi` tags on the way in and `json` on the way out.
This completes pulumi/lumi#183.
2017-06-06 02:43:52 +02:00
|
|
|
case reflect.String:
|
|
|
|
return vsrc.String(), nil
|
|
|
|
|
|
|
|
// Pointers:
|
|
|
|
case reflect.Ptr:
|
|
|
|
if vsrc.IsNil() {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
return md.EncodeValue(vsrc.Elem().Interface())
|
|
|
|
|
|
|
|
// Slices and maps:
|
|
|
|
case reflect.Slice:
|
|
|
|
var slice []interface{}
|
|
|
|
var errs []error
|
|
|
|
for i := 0; i < vsrc.Len(); i++ {
|
|
|
|
ev := vsrc.Index(i).Interface()
|
|
|
|
if elem, err := md.EncodeValue(ev); err != nil {
|
|
|
|
errs = append(errs, err.Failures()...)
|
|
|
|
} else {
|
|
|
|
slice = append(slice, elem)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if errs == nil {
|
|
|
|
return slice, nil
|
|
|
|
}
|
|
|
|
return nil, NewMappingError(errs)
|
|
|
|
case reflect.Map:
|
|
|
|
keys := vsrc.MapKeys()
|
|
|
|
mmap := make(map[string]interface{})
|
|
|
|
var errs []error
|
|
|
|
for _, key := range keys {
|
|
|
|
contract.Assert(key.Kind() == reflect.String)
|
|
|
|
if val, err := md.EncodeValue(vsrc.MapIndex(key).Interface()); err != nil {
|
|
|
|
errs = append(errs, err.Failures()...)
|
|
|
|
} else {
|
|
|
|
mmap[key.String()] = val
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if errs == nil {
|
|
|
|
return mmap, nil
|
|
|
|
}
|
|
|
|
return nil, NewMappingError(errs)
|
|
|
|
|
|
|
|
// Structs and interface{}:
|
|
|
|
case reflect.Struct:
|
|
|
|
return md.Encode(vsrc.Interface())
|
|
|
|
case reflect.Interface:
|
|
|
|
return md.EncodeValue(vsrc.Elem().Interface())
|
|
|
|
default:
|
|
|
|
contract.Failf("Unrecognized field type '%v' during encoding", k)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, nil
|
|
|
|
}
|