Skip to content

Commit 704d084

Browse files
committed
Update valueshim to handle DynamicPseudoType
1 parent 69d3178 commit 704d084

File tree

6 files changed

+172
-164
lines changed

6 files changed

+172
-164
lines changed

pkg/valueshim/convert.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2016-2025, Pulumi Corporation.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package valueshim
16+
17+
import (
18+
"encoding/json"
19+
20+
"github.com/hashicorp/go-cty/cty"
21+
ctyjson "github.com/hashicorp/go-cty/cty/json"
22+
ctymsgpack "github.com/hashicorp/go-cty/cty/msgpack"
23+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
24+
"github.com/hashicorp/terraform-plugin-go/tftypes"
25+
)
26+
27+
func toCtyType(t tftypes.Type) (cty.Type, error) {
28+
typeBytes, err := json.Marshal(t)
29+
if err != nil {
30+
return cty.NilType, err
31+
}
32+
return ctyjson.UnmarshalType(typeBytes)
33+
}
34+
35+
func toCtyValue(schemaType tftypes.Type, schemaCtyType cty.Type, value tftypes.Value) (cty.Value, error) {
36+
dv, err := tfprotov6.NewDynamicValue(schemaType, value)
37+
if err != nil {
38+
return cty.NilVal, err
39+
}
40+
return ctymsgpack.Unmarshal(dv.MsgPack, schemaCtyType)
41+
}

pkg/valueshim/cty.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package valueshim
1616

1717
import (
1818
"encoding/json"
19+
"fmt"
1920

2021
"github.com/hashicorp/go-cty/cty"
2122
ctyjson "github.com/hashicorp/go-cty/cty/json"
@@ -97,18 +98,24 @@ func (v ctyValueShim) Remove(key string) Value {
9798
}
9899
}
99100

100-
func (v ctyValueShim) Marshal() (json.RawMessage, error) {
101+
func (v ctyValueShim) Marshal(schemaType Type) (json.RawMessage, error) {
101102
vv := v.val()
102-
raw, err := ctyjson.Marshal(vv, vv.Type())
103+
tt, ok := schemaType.(ctyTypeShim)
104+
if !ok {
105+
return nil, fmt.Errorf("Cannot marshal to RawState: "+
106+
"expected schemaType to be of type ctyTypeShim, got %#T",
107+
schemaType)
108+
}
109+
raw, err := ctyjson.Marshal(vv, tt.ty())
103110
if err != nil {
104-
return nil, err
111+
return nil, fmt.Errorf("Cannot marshal to RawState: %w", err)
105112
}
106113
return json.RawMessage(raw), nil
107114
}
108115

109116
type ctyTypeShim cty.Type
110117

111-
var _ Type = (*ctyTypeShim)(nil)
118+
var _ Type = ctyTypeShim{}
112119

113120
func (t ctyTypeShim) ty() cty.Type {
114121
return cty.Type(t)

pkg/valueshim/cty_test.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,10 @@ func Test_HCtyValue_Marshal(t *testing.T) {
109109
tupType := cty.Tuple([]cty.Type{cty.String, cty.Number})
110110

111111
type testCase struct {
112-
v cty.Value
113-
expect autogold.Value
112+
v cty.Value
113+
expect autogold.Value
114+
schemaType cty.Type
115+
hasSchemaType bool
114116
}
115117

116118
testCases := []testCase{
@@ -210,10 +212,51 @@ func Test_HCtyValue_Marshal(t *testing.T) {
210212
v: cty.TupleVal([]cty.Value{ok, n42}),
211213
expect: autogold.Expect(`["OK",42]`),
212214
},
215+
{
216+
v: cty.NullVal(cty.String),
217+
schemaType: cty.DynamicPseudoType,
218+
hasSchemaType: true,
219+
expect: autogold.Expect(`{"value":null,"type":"string"}`),
220+
},
221+
{
222+
v: cty.StringVal("foo"),
223+
schemaType: cty.DynamicPseudoType,
224+
hasSchemaType: true,
225+
expect: autogold.Expect(`{"value":"foo","type":"string"}`),
226+
},
227+
{
228+
v: cty.NumberIntVal(42),
229+
schemaType: cty.DynamicPseudoType,
230+
hasSchemaType: true,
231+
expect: autogold.Expect(`{"value":42,"type":"number"}`),
232+
},
233+
{
234+
v: cty.BoolVal(true),
235+
schemaType: cty.DynamicPseudoType,
236+
hasSchemaType: true,
237+
expect: autogold.Expect(`{"value":true,"type":"bool"}`),
238+
},
239+
{
240+
v: cty.ListVal([]cty.Value{cty.StringVal("A")}),
241+
schemaType: cty.DynamicPseudoType,
242+
hasSchemaType: true,
243+
expect: autogold.Expect(`{"value":["A"],"type":["list","string"]}`),
244+
},
245+
{
246+
v: cty.MapVal(map[string]cty.Value{"x": ok, "y": ok2}),
247+
schemaType: cty.DynamicPseudoType,
248+
hasSchemaType: true,
249+
expect: autogold.Expect(`{"value":{"x":"OK","y":"OK2"},"type":["map","string"]}`),
250+
},
213251
}
214252

215253
for _, tc := range testCases {
216-
raw, err := valueshim.FromHCtyValue(tc.v).Marshal()
254+
ty := tc.schemaType
255+
if !tc.hasSchemaType {
256+
ty = tc.v.Type()
257+
}
258+
vv := valueshim.FromHCtyValue(tc.v)
259+
raw, err := vv.Marshal(valueshim.FromHCtyType(ty))
217260
require.NoError(t, err)
218261
tc.expect.Equal(t, string(raw))
219262
}

pkg/valueshim/shim.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,16 @@ type Value interface {
2828
AsValueMap() map[string]Value
2929

3030
// Marshals into the "raw state" JSON representation.
31-
Marshal() (json.RawMessage, error)
31+
//
32+
// This is the representation expected on the TF protocol UpgradeResourceState method.
33+
//
34+
// For correctly encoding DynamicPseudoType values to {"type": "...", "value": "..."} structures, the
35+
// schemaType is needed. This encoding will be used when the schema a type is a DynamicPseudoType but
36+
// the value type is a concrete type.
37+
//
38+
// In situations where the DynamicPseudoType encoding is not needed, you can also call Marshal with
39+
// value.Type() to assume the intrinsic type of the value.
40+
Marshal(schemaType Type) (json.RawMessage, error)
3241

3342
// Removes a top-level property from an Object.
3443
Remove(key string) Value

pkg/valueshim/tfvalue.go

Lines changed: 15 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package valueshim
1616

1717
import (
1818
"encoding/json"
19+
"errors"
20+
"fmt"
1921
"math/big"
2022

2123
"github.com/hashicorp/terraform-plugin-go/tftypes"
@@ -74,12 +76,20 @@ func (v tValueShim) AsValueMap() map[string]Value {
7476
return res
7577
}
7678

77-
func (v tValueShim) Marshal() (json.RawMessage, error) {
78-
inmem, err := jsonMarshal(v.val(), tftypes.NewAttributePath())
79+
func (v tValueShim) Marshal(schemaType Type) (json.RawMessage, error) {
80+
tt, ok := schemaType.(tTypeShim)
81+
if !ok {
82+
return nil, errors.New("Cannot marshal to RawState: expected schemaType to be of type tTypeShim")
83+
}
84+
ctyType, err := toCtyType(tt.ty())
85+
if err != nil {
86+
return nil, fmt.Errorf("Cannot marshal to RawState. Error converting to cty.Type: %w", err)
87+
}
88+
cty, err := toCtyValue(tt.ty(), ctyType, v.val())
7989
if err != nil {
80-
return nil, err
90+
return nil, fmt.Errorf("Cannot marshal to RawState. Error converting to cty.Value: %w", err)
8191
}
82-
return json.Marshal(inmem)
92+
return FromHCtyValue(cty).Marshal(FromHCtyType(ctyType))
8393
}
8494

8595
func (v tValueShim) Remove(prop string) Value {
@@ -139,159 +149,11 @@ func (v tValueShim) StringValue() string {
139149
return result
140150
}
141151

142-
func jsonMarshal(v tftypes.Value, p *tftypes.AttributePath) (interface{}, error) {
143-
if v.IsNull() {
144-
return nil, nil
145-
}
146-
if !v.IsKnown() {
147-
return nil, p.NewErrorf("unknown values cannot be serialized to JSON")
148-
}
149-
typ := v.Type()
150-
switch {
151-
case typ.Is(tftypes.String):
152-
return jsonMarshalString(v, p)
153-
case typ.Is(tftypes.Number):
154-
return jsonMarshalNumber(v, p)
155-
case typ.Is(tftypes.Bool):
156-
return jsonMarshalBool(v, p)
157-
case typ.Is(tftypes.List{}):
158-
return jsonMarshalList(v, p)
159-
case typ.Is(tftypes.Set{}):
160-
return jsonMarshalSet(v, p)
161-
case typ.Is(tftypes.Map{}):
162-
return jsonMarshalMap(v, p)
163-
case typ.Is(tftypes.Tuple{}):
164-
return jsonMarshalTuple(v, p)
165-
case typ.Is(tftypes.Object{}):
166-
return jsonMarshalObject(v, p)
167-
}
168-
169-
return nil, p.NewErrorf("unknown type %s", typ)
170-
}
171-
172-
func jsonMarshalString(v tftypes.Value, p *tftypes.AttributePath) (interface{}, error) {
173-
var stringValue string
174-
err := v.As(&stringValue)
175-
if err != nil {
176-
return nil, p.NewError(err)
177-
}
178-
return stringValue, nil
179-
}
180-
181-
func jsonMarshalNumber(v tftypes.Value, p *tftypes.AttributePath) (interface{}, error) {
182-
var n big.Float
183-
err := v.As(&n)
184-
if err != nil {
185-
return nil, p.NewError(err)
186-
}
187-
return json.Number(n.Text('f', -1)), nil
188-
}
189-
190-
func jsonMarshalBool(v tftypes.Value, p *tftypes.AttributePath) (interface{}, error) {
191-
var b bool
192-
err := v.As(&b)
193-
if err != nil {
194-
return nil, p.NewError(err)
195-
}
196-
return b, nil
197-
}
198-
199-
func jsonMarshalList(v tftypes.Value, p *tftypes.AttributePath) (interface{}, error) {
200-
var vs []tftypes.Value
201-
err := v.As(&vs)
202-
if err != nil {
203-
return nil, p.NewError(err)
204-
}
205-
res := make([]any, len(vs))
206-
for i, v := range vs {
207-
ep := p.WithElementKeyInt(i)
208-
e, err := jsonMarshal(v, ep)
209-
if err != nil {
210-
return nil, ep.NewError(err)
211-
}
212-
res[i] = e
213-
}
214-
return res, nil
215-
}
216-
217-
// Important to preserve original order of tftypes.Value here.
218-
func jsonMarshalSet(v tftypes.Value, p *tftypes.AttributePath) (interface{}, error) {
219-
var vs []tftypes.Value
220-
err := v.As(&vs)
221-
if err != nil {
222-
return nil, p.NewError(err)
223-
}
224-
res := make([]any, len(vs))
225-
for i, v := range vs {
226-
ep := p.WithElementKeyValue(v)
227-
e, err := jsonMarshal(v, ep)
228-
if err != nil {
229-
return nil, ep.NewError(err)
230-
}
231-
res[i] = e
232-
}
233-
return res, nil
234-
}
235-
236-
func jsonMarshalMap(v tftypes.Value, p *tftypes.AttributePath) (interface{}, error) {
237-
var vs map[string]tftypes.Value
238-
err := v.As(&vs)
239-
if err != nil {
240-
return nil, p.NewError(err)
241-
}
242-
res := make(map[string]any, len(vs))
243-
for k, v := range vs {
244-
ep := p.WithElementKeyValue(v)
245-
e, err := jsonMarshal(v, ep)
246-
if err != nil {
247-
return nil, ep.NewError(err)
248-
}
249-
res[k] = e
250-
}
251-
return res, nil
252-
}
253-
254-
func jsonMarshalTuple(v tftypes.Value, p *tftypes.AttributePath) (interface{}, error) {
255-
var vs []tftypes.Value
256-
err := v.As(&vs)
257-
if err != nil {
258-
return nil, p.NewError(err)
259-
}
260-
res := make([]any, len(vs))
261-
for i, v := range vs {
262-
ep := p.WithElementKeyInt(i)
263-
e, err := jsonMarshal(v, ep)
264-
if err != nil {
265-
return nil, ep.NewError(err)
266-
}
267-
res[i] = e
268-
}
269-
return res, nil
270-
}
271-
272-
func jsonMarshalObject(v tftypes.Value, p *tftypes.AttributePath) (interface{}, error) {
273-
var vs map[string]tftypes.Value
274-
err := v.As(&vs)
275-
if err != nil {
276-
return nil, p.NewError(err)
277-
}
278-
res := make(map[string]any, len(vs))
279-
for k, v := range vs {
280-
ep := p.WithAttributeName(k)
281-
e, err := jsonMarshal(v, ep)
282-
if err != nil {
283-
return nil, ep.NewError(err)
284-
}
285-
res[k] = e
286-
}
287-
return res, nil
288-
}
289-
290152
type tTypeShim struct {
291153
t tftypes.Type
292154
}
293155

294-
var _ Type = (*tTypeShim)(nil)
156+
var _ Type = tTypeShim{}
295157

296158
func (t tTypeShim) ty() tftypes.Type {
297159
return t.t

0 commit comments

Comments
 (0)