diff --git a/plan.go b/plan.go index b6583c0..04a71ca 100644 --- a/plan.go +++ b/plan.go @@ -4,6 +4,7 @@ package tfjson import ( + "bytes" "encoding/json" "errors" "fmt" @@ -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. @@ -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 { @@ -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 } diff --git a/plan_test.go b/plan_test.go index 2f9b37c..1f59fc1 100644 --- a/plan_test.go +++ b/plan_test.go @@ -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) + } + }) + } +} diff --git a/testdata/numerics/plan.json b/testdata/numerics/plan.json new file mode 100644 index 0000000..ea5b5fd --- /dev/null +++ b/testdata/numerics/plan.json @@ -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"}