pulumi/pkg/util/mapper/mapper.go
2018-05-22 15:02:47 -07:00

200 lines
7 KiB
Go

// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed 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 mapper
import (
"reflect"
"strings"
"github.com/pulumi/pulumi/pkg/util/contract"
)
// Mapper can map from weakly typed JSON-like property bags to strongly typed structs, and vice versa.
type Mapper interface {
// Decode decodes a JSON-like object into the target pointer to a structure.
Decode(obj map[string]interface{}, target interface{}) MappingError
// DecodeField decodes a single JSON-like value (with a given type and name) into a target pointer to a structure.
DecodeValue(obj map[string]interface{}, ty reflect.Type, key string, target interface{}, optional bool) FieldError
// Encode encodes an object into a JSON-like in-memory object.
Encode(source interface{}) (map[string]interface{}, MappingError)
// EncodeValue encodes a value into its JSON-like in-memory value format.
EncodeValue(v interface{}) (interface{}, MappingError)
}
// New allocates a new mapper object with the given options.
func New(opts *Opts) Mapper {
var initOpts Opts
if opts != nil {
initOpts = *opts
}
if initOpts.CustomDecoders == nil {
initOpts.CustomDecoders = make(Decoders)
}
return &mapper{opts: initOpts}
}
// Opts controls the way mapping occurs; for default behavior, simply pass an empty struct.
type Opts struct {
Tags []string // the tag names to recognize (`json` and `pulumi` if unspecified).
OptionalTags []string // the tags to interpret to mean "optional" (`optional` if unspecified).
SkipTags []string // the tags to interpret to mean "skip" (`skip` if unspecified).
CustomDecoders Decoders // custom decoders.
IgnoreMissing bool // ignore missing required fields.
IgnoreUnrecognized bool // ignore unrecognized fields.
}
type mapper struct {
opts Opts
}
// Map decodes an entire map into a target object, using an anonymous decoder and tag-directed mappings.
func Map(obj map[string]interface{}, target interface{}) MappingError {
return New(nil).Decode(obj, target)
}
// MapI decodes an entire map into a target object, using an anonymous decoder and tag-directed mappings. This variant
// ignores any missing required fields in the payload in addition to any unrecognized fields.
func MapI(obj map[string]interface{}, target interface{}) MappingError {
return New(&Opts{
IgnoreMissing: true,
IgnoreUnrecognized: true,
}).Decode(obj, target)
}
// MapIM decodes an entire map into a target object, using an anonymous decoder and tag-directed mappings. This variant
// ignores any missing required fields in the payload.
func MapIM(obj map[string]interface{}, target interface{}) MappingError {
return New(&Opts{IgnoreMissing: true}).Decode(obj, target)
}
// MapIU decodes an entire map into a target object, using an anonymous decoder and tag-directed mappings. This variant
// ignores any unrecognized fields in the payload.
func MapIU(obj map[string]interface{}, target interface{}) MappingError {
return New(&Opts{IgnoreUnrecognized: true}).Decode(obj, target)
}
// Unmap translates an already mapped target object into a raw, unmapped form.
func Unmap(obj interface{}) (map[string]interface{}, error) {
return New(nil).Encode(obj)
}
// structFields digs into a type to fetch all fields, including its embedded structs, for a given type.
func structFields(t reflect.Type) []reflect.StructField {
contract.Assertf(t.Kind() == reflect.Struct,
"StructFields only valid on struct types; %v is not one (kind %v)", t, t.Kind())
var fldinfos []reflect.StructField
fldtypes := []reflect.Type{t}
for len(fldtypes) > 0 {
fldtype := fldtypes[0]
fldtypes = fldtypes[1:]
for i := 0; i < fldtype.NumField(); i++ {
if fldinfo := fldtype.Field(i); fldinfo.Anonymous {
// If an embedded struct, push it onto the queue to visit.
if fldinfo.Type.Kind() == reflect.Struct {
fldtypes = append(fldtypes, fldinfo.Type)
}
} else {
// Otherwise, we will go ahead and consider this field in our decoding.
fldinfos = append(fldinfos, fldinfo)
}
}
}
return fldinfos
}
// defaultTags fetches the mapper's tag names from the options, or supplies defaults if not present.
func (md *mapper) defaultTags() (tags []string, optionalTags []string, skipTags []string) {
if md.opts.Tags == nil {
tags = []string{"json", "pulumi"}
} else {
tags = md.opts.Tags
}
if md.opts.OptionalTags == nil {
optionalTags = []string{"omitempty", "optional"}
} else {
optionalTags = md.opts.OptionalTags
}
if md.opts.SkipTags == nil {
skipTags = []string{"skip"}
} else {
skipTags = md.opts.SkipTags
}
return tags, optionalTags, skipTags
}
// structFieldTags includes a field's information plus any parsed tags.
type structFieldTags struct {
Info reflect.StructField // the struct field info.
Optional bool // true if this can be missing.
Skip bool // true to skip a field.
Key string // the JSON key name.
}
// structFieldsTags digs into a type to fetch all fields, including embedded structs, plus any associated tags.
func (md *mapper) structFieldsTags(t reflect.Type) []structFieldTags {
// Fetch the tag names to use.
tags, optionalTags, skipTags := md.defaultTags()
// Now walk the field infos and parse the tags to create the requisite structures.
var fldtags []structFieldTags
for _, fldinfo := range structFields(t) {
for _, tagname := range tags {
if tag := fldinfo.Tag.Get(tagname); tag != "" {
var key string // the JSON key name.
var optional bool // true if this can be missing.
var skip bool // true if we should skip auto-marshaling.
// Decode the tag.
tagparts := strings.Split(tag, ",")
contract.Assertf(len(tagparts) > 0,
"Expected >0 tagparts on field %v.%v; got %v", t.Name(), fldinfo.Name, len(tagparts))
key = tagparts[0]
if key == "-" {
skip = true // a name of "-" means skip
}
for _, part := range tagparts[1:] {
var match bool
for _, optionalTag := range optionalTags {
if part == optionalTag {
optional = true
match = true
break
}
}
if !match {
for _, skipTag := range skipTags {
if part == skipTag {
skip = true
match = true
break
}
}
}
contract.Assertf(match, "Unrecognized tagpart on field %v.%v: %v", t.Name(), fldinfo.Name, part)
}
fldtags = append(fldtags, structFieldTags{
Key: key,
Optional: optional,
Skip: skip,
Info: fldinfo,
})
}
}
}
return fldtags
}