2020-03-18 17:28:57 +01:00
|
|
|
// 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"
|
2021-03-17 14:20:05 +01:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
2020-03-18 17:28:57 +01:00
|
|
|
"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
|
2020-04-21 18:18:09 +02:00
|
|
|
// Annotations records any annotations associated with the object type.
|
|
|
|
Annotations []interface{}
|
2020-03-18 17:28:57 +01:00
|
|
|
|
|
|
|
propertyUnion Type
|
|
|
|
s string
|
|
|
|
}
|
|
|
|
|
2020-04-21 18:18:09 +02:00
|
|
|
// NewObjectType creates a new object type with the given properties and annotations.
|
|
|
|
func NewObjectType(properties map[string]Type, annotations ...interface{}) *ObjectType {
|
2020-05-22 11:47:34 +02:00
|
|
|
return &ObjectType{Properties: properties, Annotations: annotations}
|
2020-03-18 17:28:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2020-05-22 11:47:34 +02:00
|
|
|
// Equals returns true if this type has the same identity as the given type.
|
|
|
|
func (t *ObjectType) Equals(other Type) bool {
|
2020-09-19 02:29:21 +02:00
|
|
|
return t.equals(other, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *ObjectType) equals(other Type, seen map[Type]struct{}) bool {
|
2020-05-22 11:47:34 +02:00
|
|
|
if t == other {
|
|
|
|
return true
|
|
|
|
}
|
2020-09-19 02:29:21 +02:00
|
|
|
if seen != nil {
|
|
|
|
if _, ok := seen[t]; ok {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
seen = map[Type]struct{}{}
|
|
|
|
}
|
|
|
|
seen[t] = struct{}{}
|
2020-05-22 11:47:34 +02:00
|
|
|
|
|
|
|
otherObject, ok := other.(*ObjectType)
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if len(t.Properties) != len(otherObject.Properties) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for k, t := range t.Properties {
|
2020-09-19 02:29:21 +02:00
|
|
|
if u, ok := otherObject.Properties[k]; !ok || !t.equals(u, seen) {
|
2020-05-22 11:47:34 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2020-03-18 17:28:57 +01:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-22 11:47:34 +02:00
|
|
|
// 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.
|
2020-03-18 17:28:57 +01:00
|
|
|
func (t *ObjectType) ConversionFrom(src Type) ConversionKind {
|
2021-06-24 18:17:55 +02:00
|
|
|
kind, _ := t.conversionFrom(src, false, nil)
|
|
|
|
return kind
|
2020-03-18 17:28:57 +01:00
|
|
|
}
|
|
|
|
|
2021-07-16 18:56:26 +02:00
|
|
|
func (t *ObjectType) conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) {
|
|
|
|
return conversionFrom(t, src, unifying, seen, func() (ConversionKind, lazyDiagnostics) {
|
2020-03-18 17:28:57 +01:00
|
|
|
switch src := src.(type) {
|
|
|
|
case *ObjectType:
|
2021-02-04 15:44:20 +01:00
|
|
|
if seen != nil {
|
|
|
|
if _, ok := seen[t]; ok {
|
2021-07-16 18:56:26 +02:00
|
|
|
return NoConversion, func() hcl.Diagnostics { return hcl.Diagnostics{invalidRecursiveType(t)} }
|
2021-02-04 15:44:20 +01:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
seen = map[Type]struct{}{}
|
|
|
|
}
|
|
|
|
seen[t] = struct{}{}
|
2021-06-24 18:17:55 +02:00
|
|
|
defer delete(seen, t)
|
2021-02-04 15:44:20 +01:00
|
|
|
|
2020-03-18 17:28:57 +01:00
|
|
|
if unifying {
|
|
|
|
var unifier objectTypeUnifier
|
|
|
|
unifier.unify(t)
|
|
|
|
unifier.unify(src)
|
2021-06-24 18:17:55 +02:00
|
|
|
return unifier.conversionKind, nil
|
2020-03-18 17:28:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
conversionKind := SafeConversion
|
2021-07-16 18:56:26 +02:00
|
|
|
var diags lazyDiagnostics
|
2020-03-18 17:28:57 +01:00
|
|
|
for k, dst := range t.Properties {
|
|
|
|
src, ok := src.Properties[k]
|
|
|
|
if !ok {
|
|
|
|
src = NoneType
|
|
|
|
}
|
2021-06-24 18:17:55 +02:00
|
|
|
if ck, why := dst.conversionFrom(src, unifying, seen); ck < conversionKind {
|
|
|
|
conversionKind, diags = ck, why
|
|
|
|
if conversionKind == NoConversion {
|
|
|
|
break
|
|
|
|
}
|
2020-03-18 17:28:57 +01:00
|
|
|
}
|
|
|
|
}
|
2021-06-24 18:17:55 +02:00
|
|
|
return conversionKind, diags
|
2020-03-18 17:28:57 +01:00
|
|
|
case *MapType:
|
|
|
|
conversionKind := UnsafeConversion
|
2021-07-16 18:56:26 +02:00
|
|
|
var diags lazyDiagnostics
|
2020-03-18 17:28:57 +01:00
|
|
|
for _, dst := range t.Properties {
|
2021-06-24 18:17:55 +02:00
|
|
|
if ck, why := dst.conversionFrom(src.ElementType, unifying, seen); ck < conversionKind {
|
|
|
|
conversionKind, diags = ck, why
|
|
|
|
if conversionKind == NoConversion {
|
|
|
|
break
|
|
|
|
}
|
2020-03-18 17:28:57 +01:00
|
|
|
}
|
|
|
|
}
|
2021-06-24 18:17:55 +02:00
|
|
|
return conversionKind, diags
|
2020-03-18 17:28:57 +01:00
|
|
|
}
|
2021-07-16 18:56:26 +02:00
|
|
|
return NoConversion, func() hcl.Diagnostics { return hcl.Diagnostics{typeNotConvertible(t, src)} }
|
2020-03-18 17:28:57 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *ObjectType) String() string {
|
2021-02-04 15:44:20 +01:00
|
|
|
return t.string(nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *ObjectType) string(seen map[Type]struct{}) string {
|
|
|
|
if t.s != "" {
|
|
|
|
return t.s
|
|
|
|
}
|
2020-03-18 17:28:57 +01:00
|
|
|
|
2021-02-04 15:44:20 +01:00
|
|
|
if seen != nil {
|
|
|
|
if _, ok := seen[t]; ok {
|
|
|
|
return "..."
|
2020-05-11 20:17:36 +02:00
|
|
|
}
|
2021-02-04 15:44:20 +01:00
|
|
|
} 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)
|
2020-05-11 20:17:36 +02:00
|
|
|
|
2021-02-04 15:44:20 +01:00
|
|
|
annotations := ""
|
|
|
|
if len(t.Annotations) != 0 {
|
|
|
|
annotations = fmt.Sprintf(", annotated(%p)", t)
|
2020-03-18 17:28:57 +01:00
|
|
|
}
|
2021-02-04 15:44:20 +01:00
|
|
|
|
|
|
|
t.s = fmt.Sprintf("object({%s}%v)", strings.Join(properties, ", "), annotations)
|
2020-03-18 17:28:57 +01:00
|
|
|
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.
|
2021-06-24 18:17:55 +02:00
|
|
|
kind, _ := t.conversionFrom(other, true, nil)
|
|
|
|
return t, kind
|
2020-03-18 17:28:57 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*ObjectType) isType() {}
|