Add ShowOption for configuring JSON decoding (#427)

* Adding ShowOption for JSON handling (#426)

* Temporarily pointing at terraform-json PR for dependent changes (#426)

* Ignoring fields that differ between TF versions (#426)

* Rename structs and func (#426)

* Removing redundant struct (#426)

* Update tfexec/options.go

Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com>

* Updating to use renamed struct (#426)

* Bumping terraform-json to v0.19.0 (#426)

---------

Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com>
diff --git a/go.mod b/go.mod
index c388769..750d326 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@
 	github.com/google/go-cmp v0.6.0
 	github.com/hashicorp/go-version v1.6.0
 	github.com/hashicorp/hc-install v0.6.2
-	github.com/hashicorp/terraform-json v0.18.0
+	github.com/hashicorp/terraform-json v0.19.0
 	github.com/zclconf/go-cty v1.14.1
 	github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b
 )
diff --git a/go.sum b/go.sum
index 3a91018..aa37568 100644
--- a/go.sum
+++ b/go.sum
@@ -41,8 +41,8 @@
 github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/hc-install v0.6.2 h1:V1k+Vraqz4olgZ9UzKiAcbman9i9scg9GgSt/U3mw/M=
 github.com/hashicorp/hc-install v0.6.2/go.mod h1:2JBpd+NCFKiHiu/yYCGaPyPHhZLxXTpz8oreHa/a3Ps=
-github.com/hashicorp/terraform-json v0.18.0 h1:pCjgJEqqDESv4y0Tzdqfxr/edOIGkjs8keY42xfNBwU=
-github.com/hashicorp/terraform-json v0.18.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
+github.com/hashicorp/terraform-json v0.19.0 h1:e9DBKC5sxDfiJT7Zoi+yRIwqLVtFur/fwK/FuE6AWsA=
+github.com/hashicorp/terraform-json v0.19.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
diff --git a/tfexec/internal/e2etest/cmp.go b/tfexec/internal/e2etest/cmp.go
index 15022e5..d1d84e6 100644
--- a/tfexec/internal/e2etest/cmp.go
+++ b/tfexec/internal/e2etest/cmp.go
@@ -20,8 +20,10 @@
 
 // diffPlan returns a human-readable report of the differences between two
 // plan values. It returns an empty string if the two values are equal.
-func diffPlan(expected *tfjson.Plan, actual *tfjson.Plan) string {
-	return cmp.Diff(expected, actual, cmpopts.IgnoreFields(tfjson.Plan{}, "TerraformVersion"))
+func diffPlan(expected *tfjson.Plan, actual *tfjson.Plan, opts ...cmp.Option) string {
+	opts = append(opts, cmpopts.IgnoreFields(tfjson.Plan{}, "TerraformVersion", "useJSONNumber"))
+
+	return cmp.Diff(expected, actual, opts...)
 }
 
 // diffSchema returns a human-readable report of the differences between two
diff --git a/tfexec/internal/e2etest/show_test.go b/tfexec/internal/e2etest/show_test.go
index 98bbd8a..c81d979 100644
--- a/tfexec/internal/e2etest/show_test.go
+++ b/tfexec/internal/e2etest/show_test.go
@@ -9,11 +9,13 @@
 	"errors"
 	"io/ioutil"
 	"os"
+	"path/filepath"
 	"runtime"
 	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/hashicorp/go-version"
 	tfjson "github.com/hashicorp/terraform-json"
 
@@ -764,6 +766,452 @@
 	})
 }
 
+func TestShowFloat64(t *testing.T) {
+	runTest(t, "bigint", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+		if tfv.LessThan(showMinVersion) {
+			t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
+		}
+
+		providerName := "registry.terraform.io/hashicorp/random"
+		if tfv.LessThan(providerAddressMinVersion) {
+			providerName = "random"
+		}
+
+		formatVersion := "0.1"
+		var sensitiveValues json.RawMessage
+
+		if tfv.Core().GreaterThanOrEqual(v1_0_1) {
+			formatVersion = "0.2"
+			sensitiveValues = json.RawMessage([]byte("{}"))
+		}
+		if tfv.Core().GreaterThanOrEqual(v1_1) {
+			formatVersion = "1.0"
+		}
+
+		expected := &tfjson.State{
+			FormatVersion: formatVersion,
+			// TerraformVersion is ignored to facilitate latest version testing
+			Values: &tfjson.StateValues{
+				RootModule: &tfjson.StateModule{
+					Resources: []*tfjson.StateResource{{
+						Address: "random_integer.bigint",
+						AttributeValues: map[string]interface{}{
+							"id":      "7227701560655103598",
+							"max":     float64(7227701560655103598),
+							"min":     float64(7227701560655103597),
+							"result":  float64(7227701560655103598),
+							"seed":    "12345",
+							"keepers": nil,
+						},
+						SensitiveValues: sensitiveValues,
+						Mode:            tfjson.ManagedResourceMode,
+						Type:            "random_integer",
+						Name:            "bigint",
+						ProviderName:    providerName,
+					}},
+				},
+			},
+		}
+
+		err := tf.Init(context.Background())
+		if err != nil {
+			t.Fatalf("error running Init in test directory: %s", err)
+		}
+
+		err = tf.Apply(context.Background())
+		if err != nil {
+			t.Fatalf("error running Apply in test directory: %s", err)
+		}
+
+		actual, err := tf.Show(context.Background(), tfexec.JSONNumber(false))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if diff := diffState(expected, actual); diff != "" {
+			t.Fatalf("mismatch (-want +got):\n%s", diff)
+		}
+	})
+}
+
+func TestShowStateFileBigInt(t *testing.T) {
+	runTest(t, "bigint", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+		if tfv.LessThan(showMinVersion) {
+			t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
+		}
+
+		providerName := "registry.terraform.io/hashicorp/random"
+		if tfv.LessThan(providerAddressMinVersion) {
+			providerName = "random"
+		}
+
+		formatVersion := "0.1"
+		var sensitiveValues json.RawMessage
+
+		if tfv.Core().GreaterThanOrEqual(v1_0_1) {
+			formatVersion = "0.2"
+			sensitiveValues = json.RawMessage([]byte("{}"))
+		}
+		if tfv.Core().GreaterThanOrEqual(v1_1) {
+			formatVersion = "1.0"
+		}
+
+		expected := &tfjson.State{
+			FormatVersion: formatVersion,
+			// TerraformVersion is ignored to facilitate latest version testing
+			Values: &tfjson.StateValues{
+				RootModule: &tfjson.StateModule{
+					Resources: []*tfjson.StateResource{{
+						Address: "random_integer.bigint",
+						AttributeValues: map[string]interface{}{
+							"id":      "7227701560655103598",
+							"max":     json.Number("7227701560655103598"),
+							"min":     json.Number("7227701560655103597"),
+							"result":  json.Number("7227701560655103598"),
+							"seed":    "12345",
+							"keepers": nil,
+						},
+						SensitiveValues: sensitiveValues,
+						Mode:            tfjson.ManagedResourceMode,
+						Type:            "random_integer",
+						Name:            "bigint",
+						ProviderName:    providerName,
+					}},
+				},
+			},
+		}
+
+		err := tf.Init(context.Background())
+		if err != nil {
+			t.Fatalf("error running Init in test directory: %s", err)
+		}
+
+		err = tf.Apply(context.Background())
+		if err != nil {
+			t.Fatalf("error running Apply in test directory: %s", err)
+		}
+
+		actual, err := tf.ShowStateFile(context.Background(), filepath.Join(tf.WorkingDir(), "terraform.tfstate"))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if diff := diffState(expected, actual); diff != "" {
+			t.Fatalf("mismatch (-want +got):\n%s", diff)
+		}
+	})
+}
+
+func TestShowStateFileFloat64(t *testing.T) {
+	runTest(t, "bigint", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+		if tfv.LessThan(showMinVersion) {
+			t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
+		}
+
+		providerName := "registry.terraform.io/hashicorp/random"
+		if tfv.LessThan(providerAddressMinVersion) {
+			providerName = "random"
+		}
+
+		formatVersion := "0.1"
+		var sensitiveValues json.RawMessage
+
+		if tfv.Core().GreaterThanOrEqual(v1_0_1) {
+			formatVersion = "0.2"
+			sensitiveValues = json.RawMessage([]byte("{}"))
+		}
+		if tfv.Core().GreaterThanOrEqual(v1_1) {
+			formatVersion = "1.0"
+		}
+
+		expected := &tfjson.State{
+			FormatVersion: formatVersion,
+			// TerraformVersion is ignored to facilitate latest version testing
+			Values: &tfjson.StateValues{
+				RootModule: &tfjson.StateModule{
+					Resources: []*tfjson.StateResource{{
+						Address: "random_integer.bigint",
+						AttributeValues: map[string]interface{}{
+							"id":      "7227701560655103598",
+							"max":     float64(7227701560655103598),
+							"min":     float64(7227701560655103598),
+							"result":  float64(7227701560655103598),
+							"seed":    "12345",
+							"keepers": nil,
+						},
+						SensitiveValues: sensitiveValues,
+						Mode:            tfjson.ManagedResourceMode,
+						Type:            "random_integer",
+						Name:            "bigint",
+						ProviderName:    providerName,
+					}},
+				},
+			},
+		}
+
+		err := tf.Init(context.Background())
+		if err != nil {
+			t.Fatalf("error running Init in test directory: %s", err)
+		}
+
+		err = tf.Apply(context.Background())
+		if err != nil {
+			t.Fatalf("error running Apply in test directory: %s", err)
+		}
+
+		actual, err := tf.ShowStateFile(context.Background(), filepath.Join(tf.WorkingDir(), "terraform.tfstate"), tfexec.JSONNumber(false))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if diff := diffState(expected, actual); diff != "" {
+			t.Fatalf("mismatch (-want +got):\n%s", diff)
+		}
+	})
+}
+
+func TestShowPlanFileBigInt(t *testing.T) {
+	runTest(t, "bigint", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+		if tfv.LessThan(showMinVersion) {
+			t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
+		}
+
+		providerName := "registry.terraform.io/hashicorp/random"
+		if tfv.LessThan(providerAddressMinVersion) {
+			providerName = "random"
+		}
+
+		var sensitiveValues json.RawMessage
+
+		if tfv.Core().GreaterThanOrEqual(v1_0_1) {
+			sensitiveValues = json.RawMessage([]byte("{}"))
+		}
+		if tfv.Core().GreaterThanOrEqual(v1_1) {
+		}
+
+		expected := &tfjson.Plan{
+			// TerraformVersion is ignored to facilitate latest version testing
+			PlannedValues: &tfjson.StateValues{
+				RootModule: &tfjson.StateModule{
+					Resources: []*tfjson.StateResource{{
+						Address: "random_integer.bigint",
+						AttributeValues: map[string]interface{}{
+							"keepers": nil,
+							"max":     json.Number("7227701560655103598"),
+							"min":     json.Number("7227701560655103597"),
+							"seed":    "12345",
+						},
+						SensitiveValues: sensitiveValues,
+						Mode:            tfjson.ManagedResourceMode,
+						Type:            "random_integer",
+						Name:            "bigint",
+						ProviderName:    providerName,
+					}},
+				},
+			},
+			ResourceChanges: []*tfjson.ResourceChange{{
+				Address:      "random_integer.bigint",
+				Mode:         tfjson.ManagedResourceMode,
+				Type:         "random_integer",
+				Name:         "bigint",
+				ProviderName: providerName,
+				Change: &tfjson.Change{
+					Actions: tfjson.Actions{tfjson.ActionCreate},
+					After: map[string]interface{}{
+						"keepers": nil,
+						"max":     json.Number("7227701560655103598"),
+						"min":     json.Number("7227701560655103597"),
+						"seed":    "12345",
+					},
+					AfterUnknown: map[string]interface{}{
+						"id":     true,
+						"result": true,
+					},
+				},
+			}},
+			Config: &tfjson.Config{
+				RootModule: &tfjson.ConfigModule{
+					Resources: []*tfjson.ConfigResource{{
+						Address:           "random_integer.bigint",
+						Mode:              tfjson.ManagedResourceMode,
+						Type:              "random_integer",
+						Name:              "bigint",
+						ProviderConfigKey: "random",
+						Expressions: map[string]*tfjson.Expression{
+							"max": {
+								ExpressionData: &tfjson.ExpressionData{
+									ConstantValue: float64(7227701560655104000),
+								},
+							},
+							"min": {
+								ExpressionData: &tfjson.ExpressionData{
+									ConstantValue: float64(7227701560655104000),
+								},
+							},
+							"seed": {
+								ExpressionData: &tfjson.ExpressionData{
+									ConstantValue: float64(12345),
+								},
+							},
+						},
+					}},
+				},
+			},
+		}
+
+		err := tf.Init(context.Background())
+		if err != nil {
+			t.Fatalf("error running Init in test directory: %s", err)
+		}
+
+		_, err = tf.Plan(context.Background(), tfexec.Out(filepath.Join(tf.WorkingDir(), "tfplan")))
+		if err != nil {
+			t.Fatalf("error running Plan in test directory: %s", err)
+		}
+
+		actual, err := tf.ShowPlanFile(context.Background(), filepath.Join(tf.WorkingDir(), "tfplan"), tfexec.JSONNumber(true))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		opts := []cmp.Option{
+			cmpopts.IgnoreFields(tfjson.Change{}, "BeforeSensitive"),
+			cmpopts.IgnoreFields(tfjson.Change{}, "AfterSensitive"),
+			cmpopts.IgnoreFields(tfjson.Config{}, "ProviderConfigs"),
+			cmpopts.IgnoreFields(tfjson.Plan{}, "FormatVersion"),
+			cmpopts.IgnoreFields(tfjson.Plan{}, "Timestamp"),
+		}
+
+		if diff := diffPlan(expected, actual, opts...); diff != "" {
+			t.Fatalf("mismatch (-want +got):\n%s", diff)
+		}
+	})
+}
+
+func TestShowPlanFileFloat64(t *testing.T) {
+	runTest(t, "bigint", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+		if tfv.LessThan(showMinVersion) {
+			t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
+		}
+
+		providerName := "registry.terraform.io/hashicorp/random"
+		if tfv.LessThan(providerAddressMinVersion) {
+			providerName = "random"
+		}
+
+		var sensitiveValues json.RawMessage
+
+		if tfv.Core().GreaterThanOrEqual(v1_0_1) {
+			sensitiveValues = json.RawMessage([]byte("{}"))
+		}
+		if tfv.Core().GreaterThanOrEqual(v1_1) {
+		}
+
+		expected := &tfjson.Plan{
+			// TerraformVersion is ignored to facilitate latest version testing
+			PlannedValues: &tfjson.StateValues{
+				RootModule: &tfjson.StateModule{
+					Resources: []*tfjson.StateResource{{
+						Address: "random_integer.bigint",
+						AttributeValues: map[string]interface{}{
+							"keepers": nil,
+							"max":     float64(7227701560655103598),
+							"min":     float64(7227701560655103597),
+							"seed":    "12345",
+						},
+						SensitiveValues: sensitiveValues,
+						Mode:            tfjson.ManagedResourceMode,
+						Type:            "random_integer",
+						Name:            "bigint",
+						ProviderName:    providerName,
+					}},
+				},
+			},
+			ResourceChanges: []*tfjson.ResourceChange{{
+				Address:      "random_integer.bigint",
+				Mode:         tfjson.ManagedResourceMode,
+				Type:         "random_integer",
+				Name:         "bigint",
+				ProviderName: providerName,
+				Change: &tfjson.Change{
+					Actions: tfjson.Actions{tfjson.ActionCreate},
+					After: map[string]interface{}{
+						"keepers": nil,
+						"max":     float64(7227701560655103598),
+						"min":     float64(7227701560655103597),
+						"seed":    "12345",
+					},
+					AfterUnknown: map[string]interface{}{
+						"id":     true,
+						"result": true,
+					},
+				},
+			}},
+			Config: &tfjson.Config{
+				ProviderConfigs: map[string]*tfjson.ProviderConfig{
+					"random": {
+						Name:              "random",
+						VersionConstraint: "3.1.3",
+					},
+				},
+				RootModule: &tfjson.ConfigModule{
+					Resources: []*tfjson.ConfigResource{{
+						Address:           "random_integer.bigint",
+						Mode:              tfjson.ManagedResourceMode,
+						Type:              "random_integer",
+						Name:              "bigint",
+						ProviderConfigKey: "random",
+						Expressions: map[string]*tfjson.Expression{
+							"max": {
+								ExpressionData: &tfjson.ExpressionData{
+									ConstantValue: float64(7227701560655104000),
+								},
+							},
+							"min": {
+								ExpressionData: &tfjson.ExpressionData{
+									ConstantValue: float64(7227701560655104000),
+								},
+							},
+							"seed": {
+								ExpressionData: &tfjson.ExpressionData{
+									ConstantValue: float64(12345),
+								},
+							},
+						},
+					}},
+				},
+			},
+		}
+
+		err := tf.Init(context.Background())
+		if err != nil {
+			t.Fatalf("error running Init in test directory: %s", err)
+		}
+
+		_, err = tf.Plan(context.Background(), tfexec.Out(filepath.Join(tf.WorkingDir(), "tfplan")))
+		if err != nil {
+			t.Fatalf("error running Plan in test directory: %s", err)
+		}
+
+		actual, err := tf.ShowPlanFile(context.Background(), filepath.Join(tf.WorkingDir(), "tfplan"))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		opts := []cmp.Option{
+			cmpopts.IgnoreFields(tfjson.Change{}, "BeforeSensitive"),
+			cmpopts.IgnoreFields(tfjson.Change{}, "AfterSensitive"),
+			cmpopts.IgnoreFields(tfjson.Config{}, "ProviderConfigs"),
+			cmpopts.IgnoreFields(tfjson.Plan{}, "FormatVersion"),
+			cmpopts.IgnoreFields(tfjson.Plan{}, "Timestamp"),
+		}
+
+		if diff := diffPlan(expected, actual, opts...); diff != "" {
+			t.Fatalf("mismatch (-want +got):\n%s", diff)
+		}
+	})
+}
+
 // Since our plan strings are not large, prefer simple cross-platform
 // normalization handling over pulling in a dependency.
 func normalizePlanOutput(str string) string {
diff --git a/tfexec/options.go b/tfexec/options.go
index 5f04680..d783027 100644
--- a/tfexec/options.go
+++ b/tfexec/options.go
@@ -243,6 +243,15 @@
 	return &GraphPlanOption{file}
 }
 
+type UseJSONNumberOption struct {
+	useJSONNumber bool
+}
+
+// JSONNumber determines how numerical values are handled during JSON decoding.
+func JSONNumber(useJSONNumber bool) *UseJSONNumberOption {
+	return &UseJSONNumberOption{useJSONNumber}
+}
+
 type PlatformOption struct {
 	platform string
 }
diff --git a/tfexec/show.go b/tfexec/show.go
index 8bf0779..5854af1 100644
--- a/tfexec/show.go
+++ b/tfexec/show.go
@@ -14,6 +14,7 @@
 
 type showConfig struct {
 	reattachInfo ReattachInfo
+	jsonNumber   *UseJSONNumberOption
 }
 
 var defaultShowOptions = showConfig{}
@@ -26,6 +27,10 @@
 	conf.reattachInfo = opt.info
 }
 
+func (opt *UseJSONNumberOption) configureShow(conf *showConfig) {
+	conf.jsonNumber = opt
+}
+
 // Show reads the default state path and outputs the state.
 // To read a state or plan file, ShowState or ShowPlan must be used instead.
 func (tf *Terraform) Show(ctx context.Context, opts ...ShowOption) (*tfjson.State, error) {
@@ -53,6 +58,11 @@
 
 	var ret tfjson.State
 	ret.UseJSONNumber(true)
+
+	if c.jsonNumber != nil {
+		ret.UseJSONNumber(c.jsonNumber.useJSONNumber)
+	}
+
 	err = tf.runTerraformCmdJSON(ctx, showCmd, &ret)
 	if err != nil {
 		return nil, err
@@ -96,6 +106,11 @@
 
 	var ret tfjson.State
 	ret.UseJSONNumber(true)
+
+	if c.jsonNumber != nil {
+		ret.UseJSONNumber(c.jsonNumber.useJSONNumber)
+	}
+
 	err = tf.runTerraformCmdJSON(ctx, showCmd, &ret)
 	if err != nil {
 		return nil, err
@@ -138,6 +153,11 @@
 	showCmd := tf.showCmd(ctx, true, mergeEnv, planPath)
 
 	var ret tfjson.Plan
+
+	if c.jsonNumber != nil {
+		ret.UseJSONNumber(c.jsonNumber.useJSONNumber)
+	}
+
 	err = tf.runTerraformCmdJSON(ctx, showCmd, &ret)
 	if err != nil {
 		return nil, err