Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useJSONNumber to Plan and amend UnmarshalJSON behavior #113

Merged
merged 3 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package tfjson

import (
"bytes"
"encoding/json"
"errors"
"fmt"
Expand All @@ -29,6 +30,12 @@ const (

// Plan represents the entire contents of an output Terraform plan.
type Plan struct {
// useJSONNumber opts into the behavior of calling
// json.Decoder.UseNumber prior to decoding the plan, which turns
// numbers into json.Numbers instead of float64s. Set it using
// Plan.UseJSONNumber.
useJSONNumber bool

// The version of the plan format. This should always match the
// PlanFormatVersion constant in this package, or else an unmarshal
// will be unstable.
Expand Down Expand Up @@ -85,6 +92,14 @@ type ResourceAttribute struct {
Attribute []json.RawMessage `json:"attribute"`
}

// UseJSONNumber controls whether the Plan will be decoded using the
// json.Number behavior or the float64 behavior. When b is true, the Plan will
// represent numbers in PlanOutputs as json.Numbers. When b is false, the
// Plan will represent numbers in PlanOutputs as float64s.
func (p *Plan) UseJSONNumber(b bool) {
p.useJSONNumber = b
}

// Validate checks to ensure that the plan is present, and the
// version matches the version supported by this library.
func (p *Plan) Validate() error {
Expand Down Expand Up @@ -127,7 +142,11 @@ func (p *Plan) UnmarshalJSON(b []byte) error {
type rawPlan Plan
var plan rawPlan

err := json.Unmarshal(b, &plan)
dec := json.NewDecoder(bytes.NewReader(b))
if p.useJSONNumber {
dec.UseNumber()
}
err := dec.Decode(&plan)
if err != nil {
return err
}
Expand Down
56 changes: 56 additions & 0 deletions plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,59 @@ func TestPlan_movedBlock(t *testing.T) {
t.Fatalf("unexpected previous address %s, expected is random_id.test", plan.ResourceChanges[0].PreviousAddress)
}
}

func TestPlan_UnmarshalJSON(t *testing.T) {
t.Parallel()

b, err := os.ReadFile("testdata/numerics/plan.json")
if err != nil {
t.Fatal(err)
}

testCases := map[string]struct {
useJSONNumber bool
expected any
}{
"float64": {
expected: 1.23,
},
"json-number": {
useJSONNumber: true,
expected: json.Number("1.23"),
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

plan := &Plan{}

plan.UseJSONNumber(testCase.useJSONNumber)

err = plan.UnmarshalJSON(b)

if err != nil {
t.Fatal(err)
}

after, ok := plan.ResourceChanges[0].Change.After.(map[string]any)

if !ok {
t.Fatal("plan.ResourceChanges[0].Change.After cannot be asserted as map[string]any")
}

attr, ok := after["configurable_attribute"]

if !ok {
t.Fatal("configurable attribute not found")
}

if diff := cmp.Diff(attr, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}
1 change: 1 addition & 0 deletions testdata/numerics/plan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"format_version":"1.2","terraform_version":"1.6.5","planned_values":{"root_module":{"resources":[{"address":"example_resource.test","mode":"managed","type":"example_resource","name":"test","provider_name":"registry.terraform.io/hashicorp/example","schema_version":0,"values":{"configurable_attribute":1.23,"id":"one"},"sensitive_values":{}}]}},"resource_changes":[{"address":"example_resource.test","mode":"managed","type":"example_resource","name":"test","provider_name":"registry.terraform.io/hashicorp/example","change":{"actions":["create"],"before":null,"after":{"configurable_attribute":1.23,"id":"one"},"after_unknown":{},"before_sensitive":false,"after_sensitive":{}}}],"configuration":{"provider_config":{"example":{"name":"example","full_name":"registry.terraform.io/hashicorp/example"}},"root_module":{"resources":[{"address":"example_resource.test","mode":"managed","type":"example_resource","name":"test","provider_config_key":"example","expressions":{"configurable_attribute":{"constant_value":1.23},"id":{"constant_value":"one"}},"schema_version":0}]}},"timestamp":"2023-12-07T13:55:56Z"}