pulumi/pkg/codegen/hcl2/model/type_object.go
2021-09-09 13:36:49 -10:00

302 lines
8.7 KiB
Go

// Copyright 2016-2020, 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 model
import (
"fmt"
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
// ObjectType represents schematized maps from strings to particular types.
type ObjectType struct {
// Properties records the types of the object's properties.
Properties map[string]Type
// Annotations records any annotations associated with the object type.
Annotations []interface{}
propertyUnion Type
s string
}
// NewObjectType creates a new object type with the given properties and annotations.
func NewObjectType(properties map[string]Type, annotations ...interface{}) *ObjectType {
return &ObjectType{Properties: properties, Annotations: annotations}
}
// SyntaxNode returns the syntax node for the type. This is always syntax.None.
func (*ObjectType) SyntaxNode() hclsyntax.Node {
return syntax.None
}
// Traverse attempts to traverse the optional type with the given traverser. The result type of
// traverse(object({K_0 = T_0, ..., K_N = T_N})) is T_i if the traverser is the string literal K_i. If the traverser is
// a string but not a literal, the result type is any.
func (t *ObjectType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) {
key, keyType := GetTraverserKey(traverser)
if !InputType(StringType).ConversionFrom(keyType).Exists() {
return DynamicType, hcl.Diagnostics{unsupportedObjectProperty(traverser.SourceRange())}
}
if key == cty.DynamicVal {
if t.propertyUnion == nil {
types := make([]Type, 0, len(t.Properties))
for _, t := range t.Properties {
types = append(types, t)
}
t.propertyUnion = NewUnionType(types...)
}
return t.propertyUnion, nil
}
keyString, err := convert.Convert(key, cty.String)
contract.Assert(err == nil)
propertyName := keyString.AsString()
propertyType, hasProperty := t.Properties[propertyName]
if !hasProperty {
return DynamicType, hcl.Diagnostics{unknownObjectProperty(propertyName, traverser.SourceRange())}
}
return propertyType, nil
}
// Equals returns true if this type has the same identity as the given type.
func (t *ObjectType) Equals(other Type) bool {
return t.equals(other, nil)
}
func (t *ObjectType) equals(other Type, seen map[Type]struct{}) bool {
if t == other {
return true
}
if seen != nil {
if _, ok := seen[t]; ok {
return true
}
} else {
seen = map[Type]struct{}{}
}
seen[t] = struct{}{}
otherObject, ok := other.(*ObjectType)
if !ok {
return false
}
if len(t.Properties) != len(otherObject.Properties) {
return false
}
for k, t := range t.Properties {
if u, ok := otherObject.Properties[k]; !ok || !t.equals(u, seen) {
return false
}
}
return true
}
// AssignableFrom returns true if this type is assignable from the indicated source type.
// An object({K_0 = T_0, ..., K_N = T_N}) is assignable from U = object({K_0 = U_0, ... K_M = U_M}), where T_I is
// assignable from U[K_I] for all I from 0 to N.
func (t *ObjectType) AssignableFrom(src Type) bool {
return assignableFrom(t, src, func() bool {
if src, ok := src.(*ObjectType); ok {
for key, t := range t.Properties {
src, ok := src.Properties[key]
if !ok {
src = NoneType
}
if !t.AssignableFrom(src) {
return false
}
}
return true
}
return false
})
}
type objectTypeUnifier struct {
properties map[string]Type
any bool
conversionKind ConversionKind
}
func (u *objectTypeUnifier) unify(t *ObjectType) {
if !u.any {
u.properties = map[string]Type{}
for k, t := range t.Properties {
u.properties[k] = t
}
u.any, u.conversionKind = true, SafeConversion
} else {
for key, pt := range u.properties {
if _, exists := t.Properties[key]; !exists {
u.properties[key] = NewOptionalType(pt)
}
}
for key, t := range t.Properties {
if pt, exists := u.properties[key]; exists {
unified, ck := pt.unify(t)
if ck < u.conversionKind {
u.conversionKind = ck
}
u.properties[key] = unified
} else {
u.properties[key] = NewOptionalType(t)
}
}
}
}
// ConversionFrom returns the kind of conversion (if any) that is possible from the source type to this type.
//
// An object({K_0 = T_0, ..., K_N = T_N}) is convertible from object({K_0 = U_0, ... K_M = U_M}) if all properties
// that exist in both types are convertible, and any keys that do not exist in the source type are optional in
// the destination type. If any of these conversions are unsafe, the whole conversion is unsafe; otherwise, the
// conversion is safe.
//
// An object({K_0 = T_0, ..., K_N = T_N}) is convertible from a map(U) if U is convertible to all of T_0 through T_N.
// This conversion is always unsafe, and may fail if the map does not contain an appropriate set of keys for the
// destination type.
func (t *ObjectType) ConversionFrom(src Type) ConversionKind {
kind, _ := t.conversionFrom(src, false, nil)
return kind
}
func (t *ObjectType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
switch src := src.(type) {
case *ObjectType:
if seen != nil {
if _, ok := seen[t]; ok {
return NoConversion, func() hcl.Diagnostics { return hcl.Diagnostics{invalidRecursiveType(t)} }
}
} else {
seen = map[Type]struct{}{}
}
seen[t] = struct{}{}
defer delete(seen, t)
if unifying {
var unifier objectTypeUnifier
unifier.unify(t)
unifier.unify(src)
return unifier.conversionKind, nil
}
conversionKind := SafeConversion
var diags lazyDiagnostics
for k, dst := range t.Properties {
src, ok := src.Properties[k]
if !ok {
src = NoneType
}
if ck, why := dst.conversionFrom(src, unifying, seen); ck < conversionKind {
conversionKind, diags = ck, why
if conversionKind == NoConversion {
break
}
}
}
return conversionKind, diags
case *MapType:
conversionKind := UnsafeConversion
var diags lazyDiagnostics
for _, dst := range t.Properties {
if ck, why := dst.conversionFrom(src.ElementType, unifying, seen); ck < conversionKind {
conversionKind, diags = ck, why
if conversionKind == NoConversion {
break
}
}
}
return conversionKind, diags
}
return NoConversion, func() hcl.Diagnostics { return hcl.Diagnostics{typeNotConvertible(t, src)} }
})
}
func (t *ObjectType) String() string {
return t.string(nil)
}
func (t *ObjectType) string(seen map[Type]struct{}) string {
if t.s != "" {
return t.s
}
if seen != nil {
if _, ok := seen[t]; ok {
return "..."
}
} else {
seen = map[Type]struct{}{}
}
seen[t] = struct{}{}
var properties []string
for k, v := range t.Properties {
properties = append(properties, fmt.Sprintf("%s = %s", k, v.string(seen)))
}
sort.Strings(properties)
annotations := ""
if len(t.Annotations) != 0 {
annotations = fmt.Sprintf(", annotated(%p)", t)
}
t.s = fmt.Sprintf("object({%s}%v)", strings.Join(properties, ", "), annotations)
return t.s
}
func (t *ObjectType) unify(other Type) (Type, ConversionKind) {
return unify(t, other, func() (Type, ConversionKind) {
switch other := other.(type) {
case *MapType:
// Prefer the map type, but unify the element type.
elementType, conversionKind := other.ElementType, SafeConversion
for _, t := range t.Properties {
element, ck := elementType.unify(t)
if ck < conversionKind {
conversionKind = ck
}
elementType = element
}
return NewMapType(elementType), conversionKind
case *ObjectType:
// If the other type is an object type, produce a new type whose properties are the union of the two types.
// The types of intersecting properties will be unified.
var unifier objectTypeUnifier
unifier.unify(t)
unifier.unify(other)
return NewObjectType(unifier.properties), unifier.conversionKind
default:
// Otherwise, prefer the object type.
kind, _ := t.conversionFrom(other, true, nil)
return t, kind
}
})
}
func (*ObjectType) isType() {}