| package configschema |
| |
| import ( |
| "fmt" |
| |
| "github.com/zclconf/go-cty/cty" |
| "github.com/zclconf/go-cty/cty/convert" |
| ) |
| |
| // CoerceValue attempts to force the given value to conform to the type |
| // implied by the receiever. |
| // |
| // This is useful in situations where a configuration must be derived from |
| // an already-decoded value. It is always better to decode directly from |
| // configuration where possible since then source location information is |
| // still available to produce diagnostics, but in special situations this |
| // function allows a compatible result to be obtained even if the |
| // configuration objects are not available. |
| // |
| // If the given value cannot be converted to conform to the receiving schema |
| // then an error is returned describing one of possibly many problems. This |
| // error may be a cty.PathError indicating a position within the nested |
| // data structure where the problem applies. |
| func (b *Block) CoerceValue(in cty.Value) (cty.Value, error) { |
| var path cty.Path |
| return b.coerceValue(in, path) |
| } |
| |
| func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { |
| convType := b.specType() |
| impliedType := convType.WithoutOptionalAttributesDeep() |
| |
| switch { |
| case in.IsNull(): |
| return cty.NullVal(impliedType), nil |
| case !in.IsKnown(): |
| return cty.UnknownVal(impliedType), nil |
| } |
| |
| ty := in.Type() |
| if !ty.IsObjectType() { |
| return cty.UnknownVal(impliedType), path.NewErrorf("an object is required") |
| } |
| |
| for name := range ty.AttributeTypes() { |
| if _, defined := b.Attributes[name]; defined { |
| continue |
| } |
| if _, defined := b.BlockTypes[name]; defined { |
| continue |
| } |
| return cty.UnknownVal(impliedType), path.NewErrorf("unexpected attribute %q", name) |
| } |
| |
| attrs := make(map[string]cty.Value) |
| |
| for name, attrS := range b.Attributes { |
| attrType := impliedType.AttributeType(name) |
| attrConvType := convType.AttributeType(name) |
| |
| var val cty.Value |
| switch { |
| case ty.HasAttribute(name): |
| val = in.GetAttr(name) |
| case attrS.Computed || attrS.Optional: |
| val = cty.NullVal(attrType) |
| default: |
| return cty.UnknownVal(impliedType), path.NewErrorf("attribute %q is required", name) |
| } |
| |
| val, err := convert.Convert(val, attrConvType) |
| if err != nil { |
| return cty.UnknownVal(impliedType), append(path, cty.GetAttrStep{Name: name}).NewError(err) |
| } |
| attrs[name] = val |
| } |
| |
| for typeName, blockS := range b.BlockTypes { |
| switch blockS.Nesting { |
| |
| case NestingSingle, NestingGroup: |
| switch { |
| case ty.HasAttribute(typeName): |
| var err error |
| val := in.GetAttr(typeName) |
| attrs[typeName], err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName})) |
| if err != nil { |
| return cty.UnknownVal(impliedType), err |
| } |
| default: |
| attrs[typeName] = blockS.EmptyValue() |
| } |
| |
| case NestingList: |
| switch { |
| case ty.HasAttribute(typeName): |
| coll := in.GetAttr(typeName) |
| |
| switch { |
| case coll.IsNull(): |
| attrs[typeName] = cty.NullVal(cty.List(blockS.ImpliedType())) |
| continue |
| case !coll.IsKnown(): |
| attrs[typeName] = cty.UnknownVal(cty.List(blockS.ImpliedType())) |
| continue |
| } |
| |
| if !coll.CanIterateElements() { |
| return cty.UnknownVal(impliedType), path.NewErrorf("must be a list") |
| } |
| l := coll.LengthInt() |
| |
| if l == 0 { |
| attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType()) |
| continue |
| } |
| elems := make([]cty.Value, 0, l) |
| { |
| path = append(path, cty.GetAttrStep{Name: typeName}) |
| for it := coll.ElementIterator(); it.Next(); { |
| var err error |
| idx, val := it.Element() |
| val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx})) |
| if err != nil { |
| return cty.UnknownVal(impliedType), err |
| } |
| elems = append(elems, val) |
| } |
| } |
| attrs[typeName] = cty.ListVal(elems) |
| default: |
| attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType()) |
| } |
| |
| case NestingSet: |
| switch { |
| case ty.HasAttribute(typeName): |
| coll := in.GetAttr(typeName) |
| |
| switch { |
| case coll.IsNull(): |
| attrs[typeName] = cty.NullVal(cty.Set(blockS.ImpliedType())) |
| continue |
| case !coll.IsKnown(): |
| attrs[typeName] = cty.UnknownVal(cty.Set(blockS.ImpliedType())) |
| continue |
| } |
| |
| if !coll.CanIterateElements() { |
| return cty.UnknownVal(impliedType), path.NewErrorf("must be a set") |
| } |
| l := coll.LengthInt() |
| |
| if l == 0 { |
| attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType()) |
| continue |
| } |
| elems := make([]cty.Value, 0, l) |
| { |
| path = append(path, cty.GetAttrStep{Name: typeName}) |
| for it := coll.ElementIterator(); it.Next(); { |
| var err error |
| idx, val := it.Element() |
| val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx})) |
| if err != nil { |
| return cty.UnknownVal(impliedType), err |
| } |
| elems = append(elems, val) |
| } |
| } |
| attrs[typeName] = cty.SetVal(elems) |
| default: |
| attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType()) |
| } |
| |
| case NestingMap: |
| switch { |
| case ty.HasAttribute(typeName): |
| coll := in.GetAttr(typeName) |
| |
| switch { |
| case coll.IsNull(): |
| attrs[typeName] = cty.NullVal(cty.Map(blockS.ImpliedType())) |
| continue |
| case !coll.IsKnown(): |
| attrs[typeName] = cty.UnknownVal(cty.Map(blockS.ImpliedType())) |
| continue |
| } |
| |
| if !coll.CanIterateElements() { |
| return cty.UnknownVal(impliedType), path.NewErrorf("must be a map") |
| } |
| l := coll.LengthInt() |
| if l == 0 { |
| attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType()) |
| continue |
| } |
| elems := make(map[string]cty.Value) |
| { |
| path = append(path, cty.GetAttrStep{Name: typeName}) |
| for it := coll.ElementIterator(); it.Next(); { |
| var err error |
| key, val := it.Element() |
| if key.Type() != cty.String || key.IsNull() || !key.IsKnown() { |
| return cty.UnknownVal(impliedType), path.NewErrorf("must be a map") |
| } |
| val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: key})) |
| if err != nil { |
| return cty.UnknownVal(impliedType), err |
| } |
| elems[key.AsString()] = val |
| } |
| } |
| |
| // If the attribute values here contain any DynamicPseudoTypes, |
| // the concrete type must be an object. |
| useObject := false |
| switch { |
| case coll.Type().IsObjectType(): |
| useObject = true |
| default: |
| // It's possible that we were given a map, and need to coerce it to an object |
| ety := coll.Type().ElementType() |
| for _, v := range elems { |
| if !v.Type().Equals(ety) { |
| useObject = true |
| break |
| } |
| } |
| } |
| |
| if useObject { |
| attrs[typeName] = cty.ObjectVal(elems) |
| } else { |
| attrs[typeName] = cty.MapVal(elems) |
| } |
| default: |
| attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType()) |
| } |
| |
| default: |
| // should never happen because above is exhaustive |
| panic(fmt.Errorf("unsupported nesting mode %#v", blockS.Nesting)) |
| } |
| } |
| |
| return cty.ObjectVal(attrs), nil |
| } |