Skip to content

Commit 2bc0a43

Browse files
t0yv0VenelinMartinov
authored andcommitted
Update valueshim to handle DynamicPseudoType
1 parent ec7b59b commit 2bc0a43

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
"math/big"
2021

2122
"github.com/hashicorp/go-cty/cty"
@@ -103,18 +104,24 @@ func (v ctyValueShim) Remove(key string) Value {
103104
}
104105
}
105106

106-
func (v ctyValueShim) Marshal() (json.RawMessage, error) {
107+
func (v ctyValueShim) Marshal(schemaType Type) (json.RawMessage, error) {
107108
vv := v.val()
108-
raw, err := ctyjson.Marshal(vv, vv.Type())
109+
tt, ok := schemaType.(ctyTypeShim)
110+
if !ok {
111+
return nil, fmt.Errorf("Cannot marshal to RawState: "+
112+
"expected schemaType to be of type ctyTypeShim, got %#T",
113+
schemaType)
114+
}
115+
raw, err := ctyjson.Marshal(vv, tt.ty())
109116
if err != nil {
110-
return nil, err
117+
return nil, fmt.Errorf("Cannot marshal to RawState: %w", err)
111118
}
112119
return json.RawMessage(raw), nil
113120
}
114121

115122
type ctyTypeShim cty.Type
116123

117-
var _ Type = (*ctyTypeShim)(nil)
124+
var _ Type = ctyTypeShim{}
118125

119126
func (t ctyTypeShim) ty() cty.Type {
120127
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
@@ -29,7 +29,16 @@ type Value interface {
2929
AsValueMap() map[string]Value
3030

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

3443
// Removes a top-level property from an Object.
3544
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 {
@@ -144,159 +154,11 @@ func (v tValueShim) StringValue() string {
144154
return result
145155
}
146156

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

299-
var _ Type = (*tTypeShim)(nil)
161+
var _ Type = tTypeShim{}
300162

301163
func (t tTypeShim) ty() tftypes.Type {
302164
return t.t

0 commit comments

Comments
 (0)