diff --git a/docs/data-sources/resource.md b/docs/data-sources/resource.md
index 622ed01..366175c 100644
--- a/docs/data-sources/resource.md
+++ b/docs/data-sources/resource.md
@@ -38,7 +38,7 @@ data "restful_resource" "test" {
### Read-Only
-- `output` (String) The response body after reading the resource.
+- `output` (Dynamic) The response body after reading the resource.
### Nested Schema for `precheck`
diff --git a/docs/resources/operation.md b/docs/resources/operation.md
index fdc5c38..68135a1 100644
--- a/docs/resources/operation.md
+++ b/docs/resources/operation.md
@@ -40,8 +40,8 @@ resource "restful_operation" "register_rp" {
### Optional
-- `body` (String) The payload for the `Create`/`Update` call.
-- `delete_body` (String) The payload for the `Delete` call.
+- `body` (Dynamic) The payload for the `Create`/`Update` call.
+- `delete_body` (Dynamic) The payload for the `Delete` call.
- `delete_method` (String) The method for the `Delete` call. Possible values are `POST`, `PUT`, `PATCH` and `DELETE`. If this is not specified, no `Delete` call will occur.
- `delete_path` (String) The path for the `Delete` call, relative to the `base_url` of the provider. The `path` is used instead if `delete_path` is absent.
- `header` (Map of String) The header parameters that are applied to each request. This overrides the `header` set in the provider block.
@@ -57,7 +57,7 @@ resource "restful_operation" "register_rp" {
### Read-Only
- `id` (String) The ID of the operation. Same as the `path`.
-- `output` (String) The response body.
+- `output` (Dynamic) The response body.
### Nested Schema for `poll`
diff --git a/docs/resources/resource.md b/docs/resources/resource.md
index aaeaa64..4935ac4 100644
--- a/docs/resources/resource.md
+++ b/docs/resources/resource.md
@@ -26,12 +26,12 @@ resource "restful_resource" "rg" {
pending = ["202", "200"]
}
}
- body = jsonencode({
+ body = {
location = "westus"
tags = {
foo = "bar"
}
- })
+ }
}
```
@@ -40,7 +40,7 @@ resource "restful_resource" "rg" {
### Required
-- `body` (String) The properties of the resource.
+- `body` (Dynamic) The properties of the resource.
- `path` (String) The path used to create the resource, relative to the `base_url` of the provider.
### Optional
@@ -74,7 +74,7 @@ resource "restful_resource" "rg" {
### Read-Only
- `id` (String) The ID of the Resource.
-- `output` (String) The response body after reading the resource.
+- `output` (Dynamic) The response body after reading the resource.
### Nested Schema for `poll_create`
diff --git a/examples/resources/restful_resource/resource.tf b/examples/resources/restful_resource/resource.tf
index b20e730..ba27104 100644
--- a/examples/resources/restful_resource/resource.tf
+++ b/examples/resources/restful_resource/resource.tf
@@ -11,10 +11,10 @@ resource "restful_resource" "rg" {
pending = ["202", "200"]
}
}
- body = jsonencode({
+ body = {
location = "westus"
tags = {
foo = "bar"
}
- })
+ }
}
diff --git a/examples/usecases/azure/route_table/main.tf b/examples/usecases/azure/route_table/main.tf
index 7f1fee9..5dee4af 100644
--- a/examples/usecases/azure/route_table/main.tf
+++ b/examples/usecases/azure/route_table/main.tf
@@ -49,12 +49,12 @@ resource "restful_resource" "rg" {
pending = ["202", "200"]
}
}
- body = jsonencode({
+ body = {
location = "westus"
tags = {
foo = "bar"
}
- })
+ }
}
locals {
@@ -80,9 +80,9 @@ resource "restful_resource" "table" {
query = {
api-version = ["2022-07-01"]
}
- body = jsonencode({
+ body = {
location = "westus"
- })
+ }
poll_create = local.poll
poll_delete = local.poll
}
@@ -101,12 +101,12 @@ resource "restful_resource" "route1" {
poll_update = local.poll
poll_delete = local.poll
- body = jsonencode({
+ body = {
properties = {
nextHopType = "VnetLocal"
addressPrefix = "10.1.0.0/16"
}
- })
+ }
}
resource "restful_resource" "route2" {
@@ -123,10 +123,10 @@ resource "restful_resource" "route2" {
poll_update = local.poll
poll_delete = local.poll
- body = jsonencode({
+ body = {
properties = {
nextHopType = "VnetLocal"
addressPrefix = "10.2.0.0/16"
}
- })
+ }
}
diff --git a/examples/usecases/azure/virtual_network/main.tf b/examples/usecases/azure/virtual_network/main.tf
index f831458..d058a4e 100644
--- a/examples/usecases/azure/virtual_network/main.tf
+++ b/examples/usecases/azure/virtual_network/main.tf
@@ -49,12 +49,12 @@ resource "restful_resource" "rg" {
pending = ["202", "200"]
}
}
- body = jsonencode({
+ body = {
location = "westus"
tags = {
foo = "bar"
}
- })
+ }
}
locals {
@@ -77,7 +77,7 @@ resource "restful_resource" "vnet" {
poll_create = local.vnet_poll
poll_update = local.vnet_poll
poll_delete = local.vnet_poll
- body = jsonencode({
+ body = {
location = "westus"
properties = {
addressSpace = {
@@ -92,5 +92,5 @@ resource "restful_resource" "vnet" {
}
]
}
- })
+ }
}
diff --git a/examples/usecases/feedly/main.tf b/examples/usecases/feedly/main.tf
index 41371c0..0916436 100644
--- a/examples/usecases/feedly/main.tf
+++ b/examples/usecases/feedly/main.tf
@@ -35,9 +35,9 @@ resource "restful_resource" "collection_go" {
update_method = "POST"
read_path = "$(path)/$(body.0.id)"
read_selector = "0"
- body = jsonencode({
+ body = {
label = "Go"
- })
+ }
}
resource "restful_resource" "feeds" {
@@ -47,7 +47,7 @@ resource "restful_resource" "feeds" {
create_selector = "#[feedId == \"${each.value}\"]"
read_path = "feeds/$(body.id)"
delete_path = "${restful_resource.collection_go.id}/feeds/$(body.id)"
- body = jsonencode({
+ body = {
id = each.value
- })
+ }
}
diff --git a/examples/usecases/msgraph/main.tf b/examples/usecases/msgraph/main.tf
index 65fa7b6..ea12497 100644
--- a/examples/usecases/msgraph/main.tf
+++ b/examples/usecases/msgraph/main.tf
@@ -36,7 +36,7 @@ provider "restful" {
resource "restful_resource" "group" {
path = "/groups"
read_path = "$(path)/$(body.id)"
- body = jsonencode({
+ body = {
description = "Self help community for library"
displayName = "Library Assist"
groupTypes = [
@@ -45,13 +45,13 @@ resource "restful_resource" "group" {
mailEnabled = true
mailNickname = "library"
securityEnabled = false
- })
+ }
}
resource "restful_resource" "user" {
path = "/users"
read_path = "$(path)/$(body.id)"
- body = jsonencode({
+ body = {
accountEnabled = true
mailNickname = "AdeleV"
displayName = "J.Doe"
@@ -59,7 +59,7 @@ resource "restful_resource" "user" {
passwordProfile = {
password = "SecretP@sswd99!"
}
- })
+ }
write_only_attrs = [
"mailNickname",
"accountEnabled",
diff --git a/examples/usecases/spotify/main.tf b/examples/usecases/spotify/main.tf
index 88f7fb3..56df659 100644
--- a/examples/usecases/spotify/main.tf
+++ b/examples/usecases/spotify/main.tf
@@ -26,12 +26,12 @@ data "restful_resource" "me" {
}
resource "restful_resource" "playlist" {
- path = "/users/${jsondecode(data.restful_resource.me.output).id}/playlists"
+ path = "/users/${data.restful_resource.me.output.id}/playlists"
read_path = "/playlists/$(body.id)"
delete_path = "/playlists/$(body.id)/followers"
- body = jsonencode({
+ body = {
name = "World Cup (by Terraform)"
- })
+ }
}
locals {
@@ -55,7 +55,7 @@ data "restful_resource" "track" {
resource "restful_operation" "add_tracks_to_playlist" {
path = "${restful_resource.playlist.id}/tracks"
method = "PUT"
- body = jsonencode({
- uris = [for d in data.restful_resource.track : jsondecode(d.output).tracks.items[0].uri]
- })
+ body = {
+ uris = [for d in data.restful_resource.track : d.output.tracks.items[0].uri]
+ }
}
diff --git a/examples/usecases/thingsboard/main.tf b/examples/usecases/thingsboard/main.tf
index 49454af..18bba31 100644
--- a/examples/usecases/thingsboard/main.tf
+++ b/examples/usecases/thingsboard/main.tf
@@ -50,10 +50,10 @@ data "restful_resource" "user" {
resource "restful_resource" "customer" {
path = "/customer"
read_path = "$(path)/$(body.id.id)"
- body = jsonencode({
+ body = {
title = "Example Company"
tenantId = {
- id = jsondecode(data.restful_resource.user.output).tenantId.id
+ id = data.restful_resource.user.output.tenantId.id
entityType = "TENANT"
}
country = "US"
@@ -64,15 +64,15 @@ resource "restful_resource" "customer" {
zip = "10004"
phone = "+1(415)777-7777"
email = "example@company.com"
- })
+ }
}
resource "restful_resource" "device_profile" {
path = "/deviceProfile"
read_path = "$(path)/$(body.id.id)"
- body = jsonencode({
+ body = {
tenantId = {
- id = jsondecode(data.restful_resource.user.output).tenantId.id
+ id = data.restful_resource.user.output.tenantId.id
entityType = "TENANT"
}
name = "My Profile"
@@ -99,28 +99,28 @@ resource "restful_resource" "device_profile" {
firmwareId = null
softwareId = null
default = false
- })
+ }
}
resource "restful_resource" "device" {
path = "/device"
read_path = "$(path)/$(body.id.id)"
- body = jsonencode({
+ body = {
tenantId = {
- id = jsondecode(data.restful_resource.user.output).tenantId.id
+ id = data.restful_resource.user.output.tenantId.id
entityType = "TENANT"
}
customerId = {
- id = jsondecode(restful_resource.customer.output).id.id
+ id = restful_resource.customer.output.id.id
entityType = "CUSTOMER"
}
name = "My Device"
label = "Room 123 Sensor"
deviceProfileId : {
- id = jsondecode(restful_resource.device_profile.output).id.id
+ id = restful_resource.device_profile.output.id.id
entityType = "DEVICE_PROFILE"
}
- })
+ }
}
data "restful_resource" "device_credential" {
@@ -137,7 +137,7 @@ locals {
resolveMultiple = false
singleEntity = {
entityType = "DEVICE"
- id = jsondecode(restful_resource.device.output).id.id
+ id = restful_resource.device.output.id.id
}
type = "singleEntity"
}
@@ -208,9 +208,9 @@ locals {
resource "restful_resource" "dashboard" {
path = "/dashboard"
read_path = "$(path)/$(body.id.id)"
- body = jsonencode({
+ body = {
tenantId = {
- id = jsondecode(data.restful_resource.user.output).tenantId.id
+ id = data.restful_resource.user.output.tenantId.id
entityType = "TENANT"
}
title = "My Dashboard"
@@ -256,10 +256,10 @@ resource "restful_resource" "dashboard" {
(local.my_device_widget.id) = local.my_device_widget
}
}
- })
+ }
}
output "device_token" {
- value = jsondecode(data.restful_resource.device_credential.output).credentialsId
+ value = data.restful_resource.device_credential.output.credentialsId
sensitive = true
}
diff --git a/go.mod b/go.mod
index c494a9c..5f586a8 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ toolchain go1.22.1
require (
github.com/evanphx/json-patch v0.5.2
github.com/go-resty/resty/v2 v2.10.0
- github.com/hashicorp/terraform-plugin-framework v1.7.0
+ github.com/hashicorp/terraform-plugin-framework v1.7.1-0.20240326130300-484f311c99cf
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
github.com/hashicorp/terraform-plugin-go v0.22.1
github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0
diff --git a/go.sum b/go.sum
index 630392c..4795bfd 100644
--- a/go.sum
+++ b/go.sum
@@ -76,8 +76,8 @@ github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8J
github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw=
github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U=
github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
-github.com/hashicorp/terraform-plugin-framework v1.7.0 h1:wOULbVmfONnJo9iq7/q+iBOBJul5vRovaYJIu2cY/Pw=
-github.com/hashicorp/terraform-plugin-framework v1.7.0/go.mod h1:jY9Id+3KbZ17OMpulgnWLSfwxNVYSoYBQFTgsx044CI=
+github.com/hashicorp/terraform-plugin-framework v1.7.1-0.20240326130300-484f311c99cf h1:pUx5HaXbPjLAhIO/vxisMrqDlalIUyQAxMDun0TKLBM=
+github.com/hashicorp/terraform-plugin-framework v1.7.1-0.20240326130300-484f311c99cf/go.mod h1:jY9Id+3KbZ17OMpulgnWLSfwxNVYSoYBQFTgsx044CI=
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc=
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg=
github.com/hashicorp/terraform-plugin-go v0.22.1 h1:iTS7WHNVrn7uhe3cojtvWWn83cm2Z6ryIUDTRO0EV7w=
diff --git a/internal/dynamic/dynamic.go b/internal/dynamic/dynamic.go
new file mode 100644
index 0000000..5cff53a
--- /dev/null
+++ b/internal/dynamic/dynamic.go
@@ -0,0 +1,408 @@
+package dynamic
+
+import (
+ "encoding/json"
+ "fmt"
+ "math/big"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+func ToJSON(d types.Dynamic) ([]byte, error) {
+ if d.IsNull() {
+ return nil, nil
+ }
+ return attrValueToJSON(d.UnderlyingValue())
+}
+
+func attrListToJSON(in []attr.Value) ([]json.RawMessage, error) {
+ l := []json.RawMessage{}
+ for _, v := range in {
+ vv, err := attrValueToJSON(v)
+ if err != nil {
+ return nil, err
+ }
+ l = append(l, json.RawMessage(vv))
+ }
+ return l, nil
+}
+
+func attrMapToJSON(in map[string]attr.Value) (map[string]json.RawMessage, error) {
+ m := map[string]json.RawMessage{}
+ for k, v := range in {
+ vv, err := attrValueToJSON(v)
+ if err != nil {
+ return nil, err
+ }
+ m[k] = json.RawMessage(vv)
+ }
+ return m, nil
+}
+
+func attrValueToJSON(val attr.Value) ([]byte, error) {
+ if val.IsNull() {
+ return json.Marshal(nil)
+ }
+ switch value := val.(type) {
+ case types.Bool:
+ return json.Marshal(value.ValueBool())
+ case types.String:
+ return json.Marshal(value.ValueString())
+ case types.Int64:
+ return json.Marshal(value.ValueInt64())
+ case types.Float64:
+ return json.Marshal(value.ValueFloat64())
+ case types.Number:
+ v, _ := value.ValueBigFloat().Float64()
+ return json.Marshal(v)
+ case types.List:
+ l, err := attrListToJSON(value.Elements())
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(l)
+ case types.Set:
+ l, err := attrListToJSON(value.Elements())
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(l)
+ case types.Tuple:
+ l, err := attrListToJSON(value.Elements())
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(l)
+ case types.Map:
+ m, err := attrMapToJSON(value.Elements())
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(m)
+ case types.Object:
+ m, err := attrMapToJSON(value.Attributes())
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(m)
+ default:
+ return nil, fmt.Errorf("Unhandled type: %T", value)
+ }
+}
+
+func FromJSON(b []byte, typ attr.Type) (types.Dynamic, error) {
+ v, err := attrValueFromJSON(b, typ)
+ if err != nil {
+ return types.Dynamic{}, err
+ }
+ return types.DynamicValue(v), nil
+}
+
+func attrListFromJSON(b []byte, etyp attr.Type) ([]attr.Value, error) {
+ var l []json.RawMessage
+ if err := json.Unmarshal(b, &l); err != nil {
+ return nil, err
+ }
+ vals := []attr.Value{}
+ for _, b := range l {
+ val, err := attrValueFromJSON(b, etyp)
+ if err != nil {
+ return nil, err
+ }
+ vals = append(vals, val)
+ }
+ return vals, nil
+}
+
+func attrValueFromJSON(b []byte, typ attr.Type) (attr.Value, error) {
+ switch typ := typ.(type) {
+ case basetypes.BoolType:
+ if b == nil || string(b) == "null" {
+ return types.BoolNull(), nil
+ }
+ var v bool
+ if err := json.Unmarshal(b, &v); err != nil {
+ return nil, err
+ }
+ return types.BoolValue(v), nil
+ case basetypes.StringType:
+ if b == nil || string(b) == "null" {
+ return types.StringNull(), nil
+ }
+ var v string
+ if err := json.Unmarshal(b, &v); err != nil {
+ return nil, err
+ }
+ return types.StringValue(v), nil
+ case basetypes.Int64Type:
+ if b == nil || string(b) == "null" {
+ return types.Int64Null(), nil
+ }
+ var v int64
+ if err := json.Unmarshal(b, &v); err != nil {
+ return nil, err
+ }
+ return types.Int64Value(v), nil
+ case basetypes.Float64Type:
+ if b == nil || string(b) == "null" {
+ return types.Float64Null(), nil
+ }
+ var v float64
+ if err := json.Unmarshal(b, &v); err != nil {
+ return nil, err
+ }
+ return types.Float64Value(v), nil
+ case basetypes.NumberType:
+ if b == nil || string(b) == "null" {
+ return types.NumberNull(), nil
+ }
+ var v float64
+ if err := json.Unmarshal(b, &v); err != nil {
+ return nil, err
+ }
+ return types.NumberValue(big.NewFloat(v)), nil
+ case basetypes.ListType:
+ if b == nil || string(b) == "null" {
+ return types.ListNull(typ.ElemType), nil
+ }
+ vals, err := attrListFromJSON(b, typ.ElemType)
+ if err != nil {
+ return nil, err
+ }
+ vv, diags := types.ListValue(typ.ElemType, vals)
+ if diags.HasError() {
+ diag := diags.Errors()[0]
+ return nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
+ }
+ return vv, nil
+ case basetypes.SetType:
+ if b == nil || string(b) == "null" {
+ return types.SetNull(typ.ElemType), nil
+ }
+ vals, err := attrListFromJSON(b, typ.ElemType)
+ if err != nil {
+ return nil, err
+ }
+ vv, diags := types.SetValue(typ.ElemType, vals)
+ if diags.HasError() {
+ diag := diags.Errors()[0]
+ return nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
+ }
+ return vv, nil
+ case basetypes.TupleType:
+ if b == nil || string(b) == "null" {
+ return types.TupleNull(typ.ElemTypes), nil
+ }
+ var l []json.RawMessage
+ if err := json.Unmarshal(b, &l); err != nil {
+ return nil, err
+ }
+ if len(l) != len(typ.ElemTypes) {
+ return nil, fmt.Errorf("tuple element size not match: json=%d, type=%d", len(l), len(typ.ElemTypes))
+ }
+ vals := []attr.Value{}
+ for i, b := range l {
+ val, err := attrValueFromJSON(b, typ.ElemTypes[i])
+ if err != nil {
+ return nil, err
+ }
+ vals = append(vals, val)
+ }
+ vv, diags := types.TupleValue(typ.ElemTypes, vals)
+ if diags.HasError() {
+ diag := diags.Errors()[0]
+ return nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
+ }
+ return vv, nil
+ case basetypes.MapType:
+ if b == nil || string(b) == "null" {
+ return types.MapNull(typ.ElemType), nil
+ }
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return nil, err
+ }
+ vals := map[string]attr.Value{}
+ for k, v := range m {
+ val, err := attrValueFromJSON(v, typ.ElemType)
+ if err != nil {
+ return nil, err
+ }
+ vals[k] = val
+ }
+ vv, diags := types.MapValue(typ.ElemType, vals)
+ if diags.HasError() {
+ diag := diags.Errors()[0]
+ return nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
+ }
+ return vv, nil
+ case basetypes.ObjectType:
+ if b == nil || string(b) == "null" {
+ return types.ObjectNull(typ.AttributeTypes()), nil
+ }
+ var m map[string]json.RawMessage
+ if err := json.Unmarshal(b, &m); err != nil {
+ return nil, err
+ }
+ vals := map[string]attr.Value{}
+ attrTypes := typ.AttributeTypes()
+
+ for k, attrType := range attrTypes {
+ val, err := attrValueFromJSON(m[k], attrType)
+ if err != nil {
+ return nil, err
+ }
+ vals[k] = val
+ }
+ vv, diags := types.ObjectValue(attrTypes, vals)
+ if diags.HasError() {
+ diag := diags.Errors()[0]
+ return nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
+ }
+ return vv, nil
+ case basetypes.DynamicType:
+ if b == nil || string(b) == "null" {
+ return types.DynamicNull(), nil
+ }
+ return FromJSONImplied(b)
+ default:
+ return nil, fmt.Errorf("Unhandled type: %T", typ)
+ }
+}
+
+// FromJSONImplied is similar to FromJSON, while it is for typeless case.
+// In which case, the following type conversion rules are applied (Go -> TF):
+// - bool: bool
+// - float64: number
+// - string: string
+// - []interface{}: tuple
+// - map[string]interface{}: object
+// - nil: null (dynamic)
+// Note the argument has to be a valid JSON byte. E.g. it returns error on nil (0-length bytes).
+func FromJSONImplied(b []byte) (types.Dynamic, error) {
+ _, v, err := attrValueFromJSONImplied(b)
+ if err != nil {
+ return types.Dynamic{}, err
+ }
+ return types.DynamicValue(v), nil
+}
+
+func attrValueFromJSONImplied(b []byte) (attr.Type, attr.Value, error) {
+ if string(b) == "null" {
+ return types.DynamicType, types.DynamicNull(), nil
+ }
+
+ var object map[string]json.RawMessage
+ if err := json.Unmarshal(b, &object); err == nil {
+ attrTypes := map[string]attr.Type{}
+ attrVals := map[string]attr.Value{}
+ for k, v := range object {
+ attrTypes[k], attrVals[k], err = attrValueFromJSONImplied(v)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+ typ := types.ObjectType{AttrTypes: attrTypes}
+ val, diags := types.ObjectValue(attrTypes, attrVals)
+ if diags.HasError() {
+ diag := diags.Errors()[0]
+ return nil, nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
+ }
+ return typ, val, nil
+ }
+
+ var array []json.RawMessage
+ if err := json.Unmarshal(b, &array); err == nil {
+ eTypes := []attr.Type{}
+ eVals := []attr.Value{}
+ for _, e := range array {
+ eType, eVal, err := attrValueFromJSONImplied(e)
+ if err != nil {
+ return nil, nil, err
+ }
+ eTypes = append(eTypes, eType)
+ eVals = append(eVals, eVal)
+ }
+ typ := types.TupleType{ElemTypes: eTypes}
+ val, diags := types.TupleValue(eTypes, eVals)
+ if diags.HasError() {
+ diag := diags.Errors()[0]
+ return nil, nil, fmt.Errorf("%s: %s", diag.Summary(), diag.Detail())
+ }
+ return typ, val, nil
+ }
+
+ // Primitives
+ var v interface{}
+ if err := json.Unmarshal(b, &v); err != nil {
+ return nil, nil, fmt.Errorf("failed to unmarshal %s: %v", string(b), err)
+ }
+
+ switch v := v.(type) {
+ case bool:
+ return types.BoolType, types.BoolValue(v), nil
+ case float64:
+ return types.NumberType, types.NumberValue(big.NewFloat(v)), nil
+ case string:
+ return types.StringType, types.StringValue(v), nil
+ case nil:
+ return types.DynamicType, types.DynamicNull(), nil
+ default:
+ return nil, nil, fmt.Errorf("Unhandled type: %T", v)
+ }
+}
+
+// IsFullyKnown returns true if `val` is known. If `val` is an aggregate type,
+// IsFullyKnown only returns true if all elements and attributes are known, as
+// well.
+func IsFullyKnown(val attr.Value) bool {
+ if val == nil {
+ return true
+ }
+ if val.IsUnknown() {
+ return false
+ }
+ switch v := val.(type) {
+ case types.Dynamic:
+ return IsFullyKnown(v.UnderlyingValue())
+ case types.List:
+ for _, e := range v.Elements() {
+ if !IsFullyKnown(e) {
+ return false
+ }
+ }
+ return true
+ case types.Set:
+ for _, e := range v.Elements() {
+ if !IsFullyKnown(e) {
+ return false
+ }
+ }
+ return true
+ case types.Tuple:
+ for _, e := range v.Elements() {
+ if !IsFullyKnown(e) {
+ return false
+ }
+ }
+ return true
+ case types.Map:
+ for _, e := range v.Elements() {
+ if !IsFullyKnown(e) {
+ return false
+ }
+ }
+ return true
+ case types.Object:
+ for _, e := range v.Attributes() {
+ if !IsFullyKnown(e) {
+ return false
+ }
+ }
+ return true
+ default:
+ return true
+ }
+}
diff --git a/internal/dynamic/dynamic_test.go b/internal/dynamic/dynamic_test.go
new file mode 100644
index 0000000..82e640c
--- /dev/null
+++ b/internal/dynamic/dynamic_test.go
@@ -0,0 +1,716 @@
+package dynamic
+
+import (
+ "context"
+ "math/big"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/stretchr/testify/require"
+)
+
+func TestToJSON(t *testing.T) {
+ input := types.DynamicValue(
+ types.ObjectValueMust(
+ map[string]attr.Type{
+ "bool": types.BoolType,
+ "bool_null": types.BoolType,
+ "string": types.StringType,
+ "string_null": types.StringType,
+ "int64": types.Int64Type,
+ "int64_null": types.Int64Type,
+ "float64": types.Float64Type,
+ "float64_null": types.Float64Type,
+ "number": types.NumberType,
+ "number_null": types.NumberType,
+ "list": types.ListType{
+ ElemType: types.BoolType,
+ },
+ "list_empty": types.ListType{
+ ElemType: types.BoolType,
+ },
+ "list_null": types.ListType{
+ ElemType: types.BoolType,
+ },
+ "set": types.SetType{
+ ElemType: types.BoolType,
+ },
+ "set_empty": types.SetType{
+ ElemType: types.BoolType,
+ },
+ "set_null": types.SetType{
+ ElemType: types.BoolType,
+ },
+ "tuple": types.TupleType{
+ ElemTypes: []attr.Type{
+ types.BoolType,
+ types.StringType,
+ },
+ },
+ "tuple_empty": types.TupleType{
+ ElemTypes: []attr.Type{},
+ },
+ "tuple_null": types.TupleType{
+ ElemTypes: []attr.Type{
+ types.BoolType,
+ types.StringType,
+ },
+ },
+ "map": types.MapType{
+ ElemType: types.BoolType,
+ },
+ "map_empty": types.MapType{
+ ElemType: types.BoolType,
+ },
+ "map_null": types.MapType{
+ ElemType: types.BoolType,
+ },
+ "object": types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "bool": types.BoolType,
+ "string": types.StringType,
+ },
+ },
+ "object_empty": types.ObjectType{
+ AttrTypes: map[string]attr.Type{},
+ },
+ "object_null": types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "bool": types.BoolType,
+ "string": types.StringType,
+ },
+ },
+ },
+ map[string]attr.Value{
+ "bool": types.BoolValue(true),
+ "bool_null": types.BoolNull(),
+ "string": types.StringValue("a"),
+ "string_null": types.StringNull(),
+ "int64": types.Int64Value(123),
+ "int64_null": types.Int64Null(),
+ "float64": types.Float64Value(1.23),
+ "float64_null": types.Float64Null(),
+ "number": types.NumberValue(big.NewFloat(1.23)),
+ "number_null": types.NumberNull(),
+ "list": types.ListValueMust(
+ types.BoolType,
+ []attr.Value{
+ types.BoolValue(true),
+ types.BoolValue(false),
+ },
+ ),
+ "list_empty": types.ListValueMust(
+ types.BoolType,
+ []attr.Value{},
+ ),
+ "list_null": types.ListNull(types.BoolType),
+ "set": types.SetValueMust(
+ types.BoolType,
+ []attr.Value{
+ types.BoolValue(true),
+ types.BoolValue(false),
+ },
+ ),
+ "set_empty": types.SetValueMust(
+ types.BoolType,
+ []attr.Value{},
+ ),
+ "set_null": types.SetNull(types.BoolType),
+ "tuple": types.TupleValueMust(
+ []attr.Type{
+ types.BoolType,
+ types.StringType,
+ },
+ []attr.Value{
+ types.BoolValue(true),
+ types.StringValue("a"),
+ },
+ ),
+ "tuple_empty": types.TupleValueMust(
+ []attr.Type{},
+ []attr.Value{},
+ ),
+ "tuple_null": types.TupleNull(
+ []attr.Type{
+ types.BoolType,
+ types.StringType,
+ },
+ ),
+ "map": types.MapValueMust(
+ types.BoolType,
+ map[string]attr.Value{
+ "a": types.BoolValue(true),
+ },
+ ),
+ "map_empty": types.MapValueMust(
+ types.BoolType,
+ map[string]attr.Value{},
+ ),
+ "map_null": types.MapNull(types.BoolType),
+ "object": types.ObjectValueMust(
+ map[string]attr.Type{
+ "bool": types.BoolType,
+ "string": types.StringType,
+ },
+ map[string]attr.Value{
+ "bool": types.BoolValue(true),
+ "string": types.StringValue("a"),
+ },
+ ),
+ "object_empty": types.ObjectValueMust(
+ map[string]attr.Type{},
+ map[string]attr.Value{},
+ ),
+ "object_null": types.ObjectNull(
+ map[string]attr.Type{
+ "bool": types.BoolType,
+ "string": types.StringType,
+ },
+ ),
+ },
+ ),
+ )
+
+ expect := `
+{
+ "bool": true,
+ "bool_null": null,
+ "string": "a",
+ "string_null": null,
+ "int64": 123,
+ "int64_null": null,
+ "float64": 1.23,
+ "float64_null": null,
+ "number": 1.23,
+ "number_null": null,
+ "list": [true, false],
+ "list_empty": [],
+ "list_null": null,
+ "set": [true, false],
+ "set_empty": [],
+ "set_null": null,
+ "tuple": [true, "a"],
+ "tuple_empty": [],
+ "tuple_null": null,
+ "map": {
+ "a": true
+ },
+ "map_empty": {},
+ "map_null": null,
+ "object": {
+ "bool": true,
+ "string": "a"
+ },
+ "object_empty": {},
+ "object_null": null
+}`
+
+ b, err := ToJSON(input)
+ require.NoError(t, err)
+ require.JSONEq(t, expect, string(b))
+}
+
+func TestFromJSON(t *testing.T) {
+ cases := []struct {
+ name string
+ input string
+ expect types.Dynamic
+ }{
+ {
+ name: "basic",
+ input: `
+{
+ "bool": true,
+ "bool_null": null,
+ "string": "a",
+ "string_null": null,
+ "int64": 123,
+ "int64_null": null,
+ "float64": 1.23,
+ "float64_null": null,
+ "number": 1.23,
+ "number_null": null,
+ "list": [true, false],
+ "list_empty": [],
+ "list_null": null,
+ "set": [true, false],
+ "set_empty": [],
+ "set_null": null,
+ "tuple": [true, "a"],
+ "tuple_empty": [],
+ "tuple_null": null,
+ "map": {
+ "a": true
+ },
+ "map_empty": {},
+ "map_null": null,
+ "object": {
+ "bool": true,
+ "string": "a"
+ },
+ "object_empty": {},
+ "object_null": null,
+ "dynamic": {
+ "foo": "bar"
+ },
+ "dynamic_null": null
+}`,
+ expect: types.DynamicValue(
+ types.ObjectValueMust(
+ map[string]attr.Type{
+ "bool": types.BoolType,
+ "bool_null": types.BoolType,
+ "string": types.StringType,
+ "string_null": types.StringType,
+ "int64": types.Int64Type,
+ "int64_null": types.Int64Type,
+ "float64": types.Float64Type,
+ "float64_null": types.Float64Type,
+ "number": types.NumberType,
+ "number_null": types.NumberType,
+ "list": types.ListType{
+ ElemType: types.BoolType,
+ },
+ "list_empty": types.ListType{
+ ElemType: types.BoolType,
+ },
+ "list_null": types.ListType{
+ ElemType: types.BoolType,
+ },
+ "set": types.SetType{
+ ElemType: types.BoolType,
+ },
+ "set_empty": types.SetType{
+ ElemType: types.BoolType,
+ },
+ "set_null": types.SetType{
+ ElemType: types.BoolType,
+ },
+ "tuple": types.TupleType{
+ ElemTypes: []attr.Type{
+ types.BoolType,
+ types.StringType,
+ },
+ },
+ "tuple_empty": types.TupleType{
+ ElemTypes: []attr.Type{},
+ },
+ "tuple_null": types.TupleType{
+ ElemTypes: []attr.Type{
+ types.BoolType,
+ types.StringType,
+ },
+ },
+ "map": types.MapType{
+ ElemType: types.BoolType,
+ },
+ "map_empty": types.MapType{
+ ElemType: types.BoolType,
+ },
+ "map_null": types.MapType{
+ ElemType: types.BoolType,
+ },
+ "object": types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "bool": types.BoolType,
+ "string": types.StringType,
+ },
+ },
+ "object_empty": types.ObjectType{
+ AttrTypes: map[string]attr.Type{},
+ },
+ "object_null": types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "bool": types.BoolType,
+ "string": types.StringType,
+ },
+ },
+ "dynamic": types.DynamicType,
+ "dynamic_null": types.DynamicType,
+ },
+ map[string]attr.Value{
+ "bool": types.BoolValue(true),
+ "bool_null": types.BoolNull(),
+ "string": types.StringValue("a"),
+ "string_null": types.StringNull(),
+ "int64": types.Int64Value(123),
+ "int64_null": types.Int64Null(),
+ "float64": types.Float64Value(1.23),
+ "float64_null": types.Float64Null(),
+ "number": types.NumberValue(big.NewFloat(1.23)),
+ "number_null": types.NumberNull(),
+ "list": types.ListValueMust(
+ types.BoolType,
+ []attr.Value{
+ types.BoolValue(true),
+ types.BoolValue(false),
+ },
+ ),
+ "list_empty": types.ListValueMust(
+ types.BoolType,
+ []attr.Value{},
+ ),
+ "list_null": types.ListNull(types.BoolType),
+ "set": types.SetValueMust(
+ types.BoolType,
+ []attr.Value{
+ types.BoolValue(true),
+ types.BoolValue(false),
+ },
+ ),
+ "set_empty": types.SetValueMust(
+ types.BoolType,
+ []attr.Value{},
+ ),
+ "set_null": types.SetNull(types.BoolType),
+ "tuple": types.TupleValueMust(
+ []attr.Type{
+ types.BoolType,
+ types.StringType,
+ },
+ []attr.Value{
+ types.BoolValue(true),
+ types.StringValue("a"),
+ },
+ ),
+ "tuple_empty": types.TupleValueMust(
+ []attr.Type{},
+ []attr.Value{},
+ ),
+ "tuple_null": types.TupleNull(
+ []attr.Type{
+ types.BoolType,
+ types.StringType,
+ },
+ ),
+ "map": types.MapValueMust(
+ types.BoolType,
+ map[string]attr.Value{
+ "a": types.BoolValue(true),
+ },
+ ),
+ "map_empty": types.MapValueMust(
+ types.BoolType,
+ map[string]attr.Value{},
+ ),
+ "map_null": types.MapNull(types.BoolType),
+ "object": types.ObjectValueMust(
+ map[string]attr.Type{
+ "bool": types.BoolType,
+ "string": types.StringType,
+ },
+ map[string]attr.Value{
+ "bool": types.BoolValue(true),
+ "string": types.StringValue("a"),
+ },
+ ),
+ "object_empty": types.ObjectValueMust(
+ map[string]attr.Type{},
+ map[string]attr.Value{},
+ ),
+ "object_null": types.ObjectNull(
+ map[string]attr.Type{
+ "bool": types.BoolType,
+ "string": types.StringType,
+ },
+ ),
+ "dynamic": types.DynamicValue(
+ types.ObjectValueMust(
+ map[string]attr.Type{
+ "foo": types.StringType,
+ },
+ map[string]attr.Value{
+ "foo": types.StringValue("bar"),
+ },
+ ),
+ ),
+ "dynamic_null": types.DynamicNull(),
+ },
+ ),
+ ),
+ },
+ {
+ name: "fields not defined in type is ignored",
+ input: `
+{
+ "str1": "a",
+ "str2": "b"
+}
+`,
+ expect: types.DynamicValue(
+ types.ObjectValueMust(
+ map[string]attr.Type{
+ "str1": types.StringType,
+ },
+ map[string]attr.Value{
+ "str1": types.StringValue("a"),
+ },
+ ),
+ ),
+ },
+ {
+ name: "fields defined in type not in JSON, set it as null",
+ input: `
+{
+ "str1": "a"
+}
+`,
+ expect: types.DynamicValue(
+ types.ObjectValueMust(
+ map[string]attr.Type{
+ "str1": types.StringType,
+ "str2": types.StringType,
+ },
+ map[string]attr.Value{
+ "str1": types.StringValue("a"),
+ "str2": types.StringNull(),
+ },
+ ),
+ ),
+ },
+ {
+ name: "tuple length changed",
+ input: `
+[{
+ "str1": "a"
+}]
+`,
+ expect: types.DynamicValue(
+ types.TupleValueMust(
+ []attr.Type{
+ types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "str1": types.StringType,
+ "str2": types.StringType,
+ },
+ },
+ },
+ []attr.Value{
+ types.ObjectValueMust(
+ map[string]attr.Type{
+ "str1": types.StringType,
+ "str2": types.StringType,
+ },
+ map[string]attr.Value{
+ "str1": types.StringValue("a"),
+ "str2": types.StringNull(),
+ },
+ ),
+ },
+ ),
+ ),
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ actual, err := FromJSON([]byte(tt.input), tt.expect.UnderlyingValue().Type(context.TODO()))
+ require.NoError(t, err)
+ require.Equal(t, tt.expect, actual)
+ })
+ }
+}
+
+func TestFromJSONImplied(t *testing.T) {
+ cases := []struct {
+ name string
+ input string
+ expect types.Dynamic
+ }{
+ {
+ name: "basic",
+ input: `
+{
+ "bool": true,
+ "bool_null": null,
+ "string": "a",
+ "string_null": null,
+ "int64": 123,
+ "int64_null": null,
+ "float64": 1.23,
+ "float64_null": null,
+ "number": 1.23,
+ "number_null": null,
+ "list": [true, false],
+ "list_empty": [],
+ "list_null": null,
+ "set": [true, false],
+ "set_empty": [],
+ "set_null": null,
+ "tuple": [true, "a"],
+ "tuple_empty": [],
+ "tuple_null": null,
+ "map": {
+ "a": true
+ },
+ "map_empty": {},
+ "map_null": null,
+ "object": {
+ "bool": true,
+ "string": "a"
+ },
+ "object_empty": {},
+ "object_null": null
+}`,
+ expect: types.DynamicValue(
+ types.ObjectValueMust(
+ map[string]attr.Type{
+ "bool": types.BoolType,
+ "bool_null": types.DynamicType,
+ "string": types.StringType,
+ "string_null": types.DynamicType,
+ "int64": types.NumberType,
+ "int64_null": types.DynamicType,
+ "float64": types.NumberType,
+ "float64_null": types.DynamicType,
+ "number": types.NumberType,
+ "number_null": types.DynamicType,
+ "list": types.TupleType{
+ ElemTypes: []attr.Type{
+ types.BoolType,
+ types.BoolType,
+ },
+ },
+ "list_empty": types.TupleType{
+ ElemTypes: []attr.Type{},
+ },
+ "list_null": types.DynamicType,
+ "set": types.TupleType{
+ ElemTypes: []attr.Type{
+ types.BoolType,
+ types.BoolType,
+ },
+ },
+ "set_empty": types.TupleType{
+ ElemTypes: []attr.Type{},
+ },
+ "set_null": types.DynamicType,
+ "tuple": types.TupleType{
+ ElemTypes: []attr.Type{
+ types.BoolType,
+ types.StringType,
+ },
+ },
+ "tuple_empty": types.TupleType{
+ ElemTypes: []attr.Type{},
+ },
+ "tuple_null": types.DynamicType,
+ "map": types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "a": types.BoolType,
+ },
+ },
+ "map_empty": types.ObjectType{
+ AttrTypes: map[string]attr.Type{},
+ },
+ "map_null": types.DynamicType,
+ "object": types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "bool": types.BoolType,
+ "string": types.StringType,
+ },
+ },
+ "object_empty": types.ObjectType{
+ AttrTypes: map[string]attr.Type{},
+ },
+ "object_null": types.DynamicType,
+ },
+ map[string]attr.Value{
+ "bool": types.BoolValue(true),
+ "bool_null": types.DynamicNull(),
+ "string": types.StringValue("a"),
+ "string_null": types.DynamicNull(),
+ "int64": types.NumberValue(big.NewFloat(123)),
+ "int64_null": types.DynamicNull(),
+ "float64": types.NumberValue(big.NewFloat(1.23)),
+ "float64_null": types.DynamicNull(),
+ "number": types.NumberValue(big.NewFloat(1.23)),
+ "number_null": types.DynamicNull(),
+ "list": types.TupleValueMust(
+ []attr.Type{
+ types.BoolType,
+ types.BoolType,
+ },
+ []attr.Value{
+ types.BoolValue(true),
+ types.BoolValue(false),
+ },
+ ),
+ "list_empty": types.TupleValueMust(
+ []attr.Type{},
+ []attr.Value{},
+ ),
+ "list_null": types.DynamicNull(),
+ "set": types.TupleValueMust(
+ []attr.Type{
+ types.BoolType,
+ types.BoolType,
+ },
+ []attr.Value{
+ types.BoolValue(true),
+ types.BoolValue(false),
+ },
+ ),
+ "set_empty": types.TupleValueMust(
+ []attr.Type{},
+ []attr.Value{},
+ ),
+ "set_null": types.DynamicNull(),
+ "tuple": types.TupleValueMust(
+ []attr.Type{
+ types.BoolType,
+ types.StringType,
+ },
+ []attr.Value{
+ types.BoolValue(true),
+ types.StringValue("a"),
+ },
+ ),
+ "tuple_empty": types.TupleValueMust(
+ []attr.Type{},
+ []attr.Value{},
+ ),
+ "tuple_null": types.DynamicNull(),
+ "map": types.ObjectValueMust(
+ map[string]attr.Type{
+ "a": types.BoolType,
+ },
+ map[string]attr.Value{
+ "a": types.BoolValue(true),
+ },
+ ),
+ "map_empty": types.ObjectValueMust(
+ map[string]attr.Type{},
+ map[string]attr.Value{},
+ ),
+ "map_null": types.DynamicNull(),
+ "object": types.ObjectValueMust(
+ map[string]attr.Type{
+ "bool": types.BoolType,
+ "string": types.StringType,
+ },
+ map[string]attr.Value{
+ "bool": types.BoolValue(true),
+ "string": types.StringValue("a"),
+ },
+ ),
+ "object_empty": types.ObjectValueMust(
+ map[string]attr.Type{},
+ map[string]attr.Value{},
+ ),
+ "object_null": types.DynamicNull(),
+ },
+ ),
+ ),
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ actual, err := FromJSONImplied([]byte(tt.input))
+ require.NoError(t, err)
+ require.Equal(t, tt.expect, actual)
+ })
+ }
+}
diff --git a/internal/provider/data_source.go b/internal/provider/data_source.go
index ec0bf73..f6c4ce1 100644
--- a/internal/provider/data_source.go
+++ b/internal/provider/data_source.go
@@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/magodo/terraform-provider-restful/internal/client"
+ "github.com/magodo/terraform-provider-restful/internal/dynamic"
)
type DataSource struct {
@@ -20,16 +21,16 @@ type DataSource struct {
var _ datasource.DataSource = &DataSource{}
type dataSourceData struct {
- ID types.String `tfsdk:"id"`
- Method types.String `tfsdk:"method"`
- Query types.Map `tfsdk:"query"`
- Header types.Map `tfsdk:"header"`
- Selector types.String `tfsdk:"selector"`
- OutputAttrs types.Set `tfsdk:"output_attrs"`
- AllowNotExist types.Bool `tfsdk:"allow_not_exist"`
- Precheck types.List `tfsdk:"precheck"`
- Retry types.Object `tfsdk:"retry"`
- Output types.String `tfsdk:"output"`
+ ID types.String `tfsdk:"id"`
+ Method types.String `tfsdk:"method"`
+ Query types.Map `tfsdk:"query"`
+ Header types.Map `tfsdk:"header"`
+ Selector types.String `tfsdk:"selector"`
+ OutputAttrs types.Set `tfsdk:"output_attrs"`
+ AllowNotExist types.Bool `tfsdk:"allow_not_exist"`
+ Precheck types.List `tfsdk:"precheck"`
+ Retry types.Object `tfsdk:"retry"`
+ Output types.Dynamic `tfsdk:"output"`
}
func (d *DataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
@@ -84,7 +85,7 @@ func (d *DataSource) Schema(ctx context.Context, req datasource.SchemaRequest, r
},
"precheck": precheckAttribute("Read", true, ""),
"retry": retryAttribute("Read"),
- "output": schema.StringAttribute{
+ "output": schema.DynamicAttribute{
Description: "The response body after reading the resource.",
MarkdownDescription: "The response body after reading the resource.",
Computed: true,
@@ -190,7 +191,6 @@ func (d *DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp
}
// Set output
- output := string(b)
if !config.OutputAttrs.IsNull() {
// Update the output to only contain the specified attributes.
var outputAttrs []string
@@ -199,7 +199,7 @@ func (d *DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp
if diags.HasError() {
return
}
- output, err = FilterAttrsInJSON(output, outputAttrs)
+ fb, err := FilterAttrsInJSON(string(b), outputAttrs)
if err != nil {
resp.Diagnostics.AddError(
"Filter `output` during Read",
@@ -207,9 +207,18 @@ func (d *DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp
)
return
}
+ b = []byte(fb)
}
- state.Output = types.StringValue(output)
+ output, err := dynamic.FromJSONImplied(b)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Evaluating `output` during Read",
+ err.Error(),
+ )
+ return
+ }
+ state.Output = output
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
diff --git a/internal/provider/data_source_jsonserver_test.go b/internal/provider/data_source_jsonserver_test.go
index baafa21..26a6000 100644
--- a/internal/provider/data_source_jsonserver_test.go
+++ b/internal/provider/data_source_jsonserver_test.go
@@ -20,7 +20,7 @@ func TestDataSource_JSONServer_Basic(t *testing.T) {
{
Config: d.dsBasic(),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(dsaddr, "output"),
+ resource.TestCheckResourceAttrSet(dsaddr, "output.%"),
),
},
},
@@ -39,7 +39,7 @@ func TestDataSource_JSONServer_WithSelector(t *testing.T) {
{
Config: d.dsWithSelector(),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(dsaddr, "output"),
+ resource.TestCheckResourceAttrSet(dsaddr, "output.%"),
),
},
},
@@ -58,7 +58,8 @@ func TestDataSource_JSONServer_WithOutputAttrs(t *testing.T) {
{
Config: d.dsWithOutputAttrs(),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrWith(dsaddr, "output", CheckJSONEqual("output", `{"foo": "bar", "obj": {"a": 1}}`)),
+ resource.TestCheckResourceAttr(dsaddr, "output.foo", "bar"),
+ resource.TestCheckResourceAttr(dsaddr, "output.obj.a", "1"),
),
},
},
@@ -76,7 +77,7 @@ func TestDataSource_JSONServer_NotExists(t *testing.T) {
Config: d.dsNotExist(),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(dsaddr, "id"),
- resource.TestCheckNoResourceAttr(dsaddr, "output"),
+ resource.TestCheckNoResourceAttr(dsaddr, "output.%"),
),
},
},
@@ -91,9 +92,9 @@ provider "restful" {
resource "restful_resource" "test" {
path = "/posts"
- body = jsonencode({
+ body = {
foo = "bar"
-})
+ }
read_path = "$(path)/$(body.id)"
}
@@ -112,9 +113,9 @@ provider "restful" {
resource "restful_resource" "test" {
path = "/posts"
- body = jsonencode({
+ body = {
foo = "bar"
-})
+ }
read_path = "$(path)/$(body.id)"
}
@@ -135,13 +136,13 @@ provider "restful" {
resource "restful_resource" "test" {
path = "/posts"
- body = jsonencode({
+ body = {
foo = "bar"
obj = {
a = 1
b = 2
}
-})
+ }
read_path = "$(path)/$(body.id)"
}
diff --git a/internal/provider/data_source_mtls_test.go b/internal/provider/data_source_mtls_test.go
index bd33776..129d993 100644
--- a/internal/provider/data_source_mtls_test.go
+++ b/internal/provider/data_source_mtls_test.go
@@ -25,7 +25,7 @@ func TestDataSourceMTLS(t *testing.T) {
serverTLSConfig, caCert, clientCert, clientKey, err := certSetup()
require.NoError(t, err)
- resp := "response"
+ resp := "{}"
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, resp)
}))
@@ -42,7 +42,7 @@ func TestDataSourceMTLS(t *testing.T) {
{
Config: mtlsConfig(server.URL, caCert, clientCert, clientKey),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttr(addr, "output", resp),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
},
diff --git a/internal/provider/migrate/operation_v0.go b/internal/provider/migrate/operation_v0.go
new file mode 100644
index 0000000..fab7b1d
--- /dev/null
+++ b/internal/provider/migrate/operation_v0.go
@@ -0,0 +1,80 @@
+package migrate
+
+import (
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+var OperationSchemaV0 = schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "path": schema.StringAttribute{
+ Required: true,
+ },
+ "method": schema.StringAttribute{
+ Required: true,
+ },
+ "body": schema.StringAttribute{
+ Optional: true,
+ },
+ "query": schema.MapAttribute{
+ ElementType: types.ListType{ElemType: types.StringType},
+ Optional: true,
+ },
+ "header": schema.MapAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ },
+
+ "precheck": precheckAttributeV0(true),
+ "poll": pollAttributeV0(),
+ "retry": retryAttributeV0(),
+
+ "delete_method": schema.StringAttribute{
+ Optional: true,
+ },
+
+ "delete_path": schema.StringAttribute{
+ Optional: true,
+ },
+
+ "delete_body": schema.StringAttribute{
+ Optional: true,
+ },
+
+ "precheck_delete": precheckAttributeV0(false),
+ "poll_delete": pollAttributeV0(),
+ "retry_delete": retryAttributeV0(),
+
+ "output_attrs": schema.SetAttribute{
+ Optional: true,
+ ElementType: types.StringType,
+ },
+
+ "output": schema.StringAttribute{
+ Computed: true,
+ },
+ },
+}
+
+type OperationDataV0 struct {
+ ID types.String `tfsdk:"id"`
+ Path types.String `tfsdk:"path"`
+ Method types.String `tfsdk:"method"`
+ Body types.String `tfsdk:"body"`
+ Query types.Map `tfsdk:"query"`
+ Header types.Map `tfsdk:"header"`
+ Precheck types.List `tfsdk:"precheck"`
+ Poll types.Object `tfsdk:"poll"`
+ Retry types.Object `tfsdk:"retry"`
+ DeleteMethod types.String `tfsdk:"delete_method"`
+ DeleteBody types.String `tfsdk:"delete_body"`
+ DeletePath types.String `tfsdk:"delete_path"`
+ PrecheckDelete types.List `tfsdk:"precheck_delete"`
+ PollDelete types.Object `tfsdk:"poll_delete"`
+ RetryDelete types.Object `tfsdk:"retry_delete"`
+ OutputAttrs types.Set `tfsdk:"output_attrs"`
+ Output types.String `tfsdk:"output"`
+}
diff --git a/internal/provider/migrate/resource_v0.go b/internal/provider/migrate/resource_v0.go
new file mode 100644
index 0000000..664e6ee
--- /dev/null
+++ b/internal/provider/migrate/resource_v0.go
@@ -0,0 +1,248 @@
+package migrate
+
+import (
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+func precheckAttributeV0(pathIsRequired bool) schema.ListNestedAttribute {
+ return schema.ListNestedAttribute{
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "mutex": schema.StringAttribute{
+ Optional: true,
+ },
+ "api": schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "status_locator": schema.StringAttribute{
+ Required: true,
+ },
+ "status": schema.SingleNestedAttribute{
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "success": schema.StringAttribute{
+ Required: true,
+ },
+ "pending": schema.ListAttribute{
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ },
+ },
+ "path": schema.StringAttribute{
+ Required: pathIsRequired,
+ Optional: !pathIsRequired,
+ },
+ "query": schema.MapAttribute{
+ ElementType: types.ListType{ElemType: types.StringType},
+ Optional: true,
+ },
+ "header": schema.MapAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ },
+ "default_delay_sec": schema.Int64Attribute{
+ Optional: true,
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func pollAttributeV0() schema.SingleNestedAttribute {
+ return schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "status_locator": schema.StringAttribute{
+ Required: true,
+ },
+ "status": schema.SingleNestedAttribute{
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "success": schema.StringAttribute{
+ Required: true,
+ },
+ "pending": schema.ListAttribute{
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ },
+ },
+ "url_locator": schema.StringAttribute{
+ Optional: true,
+ },
+ "header": schema.MapAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ },
+ "default_delay_sec": schema.Int64Attribute{
+ Optional: true,
+ Computed: true,
+ },
+ },
+ }
+}
+
+func retryAttributeV0() schema.SingleNestedAttribute {
+ return schema.SingleNestedAttribute{
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "status_locator": schema.StringAttribute{
+ Required: true,
+ },
+ "status": schema.SingleNestedAttribute{
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "success": schema.StringAttribute{
+ Required: true,
+ },
+ "pending": schema.ListAttribute{
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ },
+ },
+ "count": schema.Int64Attribute{
+ Optional: true,
+ },
+ "wait_in_sec": schema.Int64Attribute{
+ Optional: true,
+ },
+ "max_wait_in_sec": schema.Int64Attribute{
+ Optional: true,
+ },
+ },
+ }
+}
+
+var ResourceSchemaV0 = schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "path": schema.StringAttribute{
+ Required: true,
+ },
+
+ "create_selector": schema.StringAttribute{
+ Optional: true,
+ },
+ "read_selector": schema.StringAttribute{
+ Optional: true,
+ },
+
+ "read_path": schema.StringAttribute{
+ Optional: true,
+ },
+ "update_path": schema.StringAttribute{
+ Optional: true,
+ },
+ "delete_path": schema.StringAttribute{
+ Optional: true,
+ },
+
+ "body": schema.StringAttribute{
+ Required: true,
+ },
+
+ "poll_create": pollAttributeV0(),
+ "poll_update": pollAttributeV0(),
+ "poll_delete": pollAttributeV0(),
+
+ "precheck_create": precheckAttributeV0(true),
+ "precheck_update": precheckAttributeV0(false),
+ "precheck_delete": precheckAttributeV0(false),
+
+ "retry_create": retryAttributeV0(),
+ "retry_read": retryAttributeV0(),
+ "retry_update": retryAttributeV0(),
+ "retry_delete": retryAttributeV0(),
+
+ "create_method": schema.StringAttribute{
+ Optional: true,
+ },
+ "update_method": schema.StringAttribute{
+ Optional: true,
+ },
+ "delete_method": schema.StringAttribute{
+ Optional: true,
+ },
+ "write_only_attrs": schema.ListAttribute{
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ "merge_patch_disabled": schema.BoolAttribute{
+ Optional: true,
+ },
+ "query": schema.MapAttribute{
+ ElementType: types.ListType{ElemType: types.StringType},
+ Optional: true,
+ },
+ "header": schema.MapAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ },
+ "check_existance": schema.BoolAttribute{
+ Optional: true,
+ },
+ "force_new_attrs": schema.SetAttribute{
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ "output_attrs": schema.SetAttribute{
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ "output": schema.StringAttribute{
+ Computed: true,
+ },
+ },
+}
+
+type ResourceDataV0 struct {
+ ID types.String `tfsdk:"id"`
+
+ Path types.String `tfsdk:"path"`
+
+ CreateSelector types.String `tfsdk:"create_selector"`
+ ReadSelector types.String `tfsdk:"read_selector"`
+
+ ReadPath types.String `tfsdk:"read_path"`
+ UpdatePath types.String `tfsdk:"update_path"`
+ DeletePath types.String `tfsdk:"delete_path"`
+
+ CreateMethod types.String `tfsdk:"create_method"`
+ UpdateMethod types.String `tfsdk:"update_method"`
+ DeleteMethod types.String `tfsdk:"delete_method"`
+
+ PrecheckCreate types.List `tfsdk:"precheck_create"`
+ PrecheckUpdate types.List `tfsdk:"precheck_update"`
+ PrecheckDelete types.List `tfsdk:"precheck_delete"`
+
+ Body types.String `tfsdk:"body"`
+
+ PollCreate types.Object `tfsdk:"poll_create"`
+ PollUpdate types.Object `tfsdk:"poll_update"`
+ PollDelete types.Object `tfsdk:"poll_delete"`
+
+ RetryCreate types.Object `tfsdk:"retry_create"`
+ RetryRead types.Object `tfsdk:"retry_read"`
+ RetryUpdate types.Object `tfsdk:"retry_update"`
+ RetryDelete types.Object `tfsdk:"retry_delete"`
+
+ WriteOnlyAttributes types.List `tfsdk:"write_only_attrs"`
+ MergePatchDisabled types.Bool `tfsdk:"merge_patch_disabled"`
+ Query types.Map `tfsdk:"query"`
+ Header types.Map `tfsdk:"header"`
+
+ CheckExistance types.Bool `tfsdk:"check_existance"`
+ ForceNewAttrs types.Set `tfsdk:"force_new_attrs"`
+ OutputAttrs types.Set `tfsdk:"output_attrs"`
+
+ Output types.String `tfsdk:"output"`
+}
diff --git a/internal/provider/operation_jsonserver_test.go b/internal/provider/operation_jsonserver_test.go
index fcaded1..bcff632 100644
--- a/internal/provider/operation_jsonserver_test.go
+++ b/internal/provider/operation_jsonserver_test.go
@@ -36,7 +36,7 @@ func TestOperation_JSONServer_Basic(t *testing.T) {
{
Config: d.basic(),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
},
@@ -54,8 +54,8 @@ func TestOperation_JSONServer_withDelete(t *testing.T) {
{
Config: d.withDelete(true),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
- resource.TestCheckResourceAttr(addr, "output", `{"enabled":true}`),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
+ resource.TestCheckResourceAttr(addr, "output.enabled", `true`),
),
},
{
@@ -66,13 +66,38 @@ func TestOperation_JSONServer_withDelete(t *testing.T) {
{
Config: d.withDelete(false),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttr(resaddr, "output", `{"enabled":false}`),
+ resource.TestCheckResourceAttr(resaddr, "output.enabled", `false`),
),
},
},
})
}
+func TestOperation_JSONServer_MigrateV0ToV1(t *testing.T) {
+ d := newJsonServerOperation()
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { d.precheck(t) },
+ Steps: []resource.TestStep{
+ {
+ ProtoV6ProviderFactories: nil,
+ ExternalProviders: map[string]resource.ExternalProvider{
+ "restful": {
+ VersionConstraint: "= 0.13.2",
+ Source: "registry.terraform.io/magodo/restful",
+ },
+ },
+ Config: d.migrate_v0(),
+ },
+ {
+ ProtoV6ProviderFactories: acceptance.ProviderFactory(),
+ ExternalProviders: nil,
+ Config: d.migrate_v1(),
+ PlanOnly: true,
+ },
+ },
+ })
+}
+
func (d jsonServerOperation) basic() string {
return fmt.Sprintf(`
provider "restful" {
@@ -82,9 +107,9 @@ provider "restful" {
resource "restful_operation" "test" {
path = "posts"
method = "POST"
- body = jsonencode({
+ body = {
foo = "bar"
- })
+ }
}
`, d.url)
}
@@ -98,7 +123,7 @@ provider "restful" {
# This resource is used to check the state of the posts after the operation resource is deleted
resource "restful_resource" "test" {
path = "posts"
- body = jsonencode({})
+ body = {}
read_path = "$(path)/$(body.id)"
output_attrs = ["enabled"]
}
@@ -109,16 +134,48 @@ resource "restful_resource" "test" {
resource "restful_operation" "test" {
path = restful_resource.test.id
method = "PUT"
- body = jsonencode({
+ body = {
enabled = true
- })
+ }
delete_method = "PUT"
delete_path = restful_resource.test.id
- delete_body = jsonencode({
+ delete_body = {
enabled = false
- })
+ }
output_attrs = ["enabled"]
}`
}
return tpl
}
+
+func (d jsonServerOperation) migrate_v0() string {
+ return fmt.Sprintf(`
+provider "restful" {
+ base_url = %q
+}
+
+resource "restful_operation" "test" {
+ path = "posts"
+ method = "POST"
+ body = jsonencode({
+ foo = "bar"
+ })
+}
+`, d.url)
+}
+
+func (d jsonServerOperation) migrate_v1() string {
+ return fmt.Sprintf(`
+provider "restful" {
+ base_url = %q
+}
+
+resource "restful_operation" "test" {
+ path = "posts"
+ method = "POST"
+ body = {
+ foo = "bar"
+ }
+}
+`, d.url)
+}
diff --git a/internal/provider/operation_resource.go b/internal/provider/operation_resource.go
index 4ab31d3..0831ffd 100644
--- a/internal/provider/operation_resource.go
+++ b/internal/provider/operation_resource.go
@@ -19,6 +19,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/magodo/terraform-provider-restful/internal/buildpath"
"github.com/magodo/terraform-provider-restful/internal/client"
+ "github.com/magodo/terraform-provider-restful/internal/dynamic"
myvalidator "github.com/magodo/terraform-provider-restful/internal/validator"
)
@@ -27,25 +28,26 @@ type OperationResource struct {
}
var _ resource.Resource = &OperationResource{}
+var _ resource.ResourceWithUpgradeState = &OperationResource{}
type operationResourceData struct {
- ID types.String `tfsdk:"id"`
- Path types.String `tfsdk:"path"`
- Method types.String `tfsdk:"method"`
- Body types.String `tfsdk:"body"`
- Query types.Map `tfsdk:"query"`
- Header types.Map `tfsdk:"header"`
- Precheck types.List `tfsdk:"precheck"`
- Poll types.Object `tfsdk:"poll"`
- Retry types.Object `tfsdk:"retry"`
- DeleteMethod types.String `tfsdk:"delete_method"`
- DeleteBody types.String `tfsdk:"delete_body"`
- DeletePath types.String `tfsdk:"delete_path"`
- PrecheckDelete types.List `tfsdk:"precheck_delete"`
- PollDelete types.Object `tfsdk:"poll_delete"`
- RetryDelete types.Object `tfsdk:"retry_delete"`
- OutputAttrs types.Set `tfsdk:"output_attrs"`
- Output types.String `tfsdk:"output"`
+ ID types.String `tfsdk:"id"`
+ Path types.String `tfsdk:"path"`
+ Method types.String `tfsdk:"method"`
+ Body types.Dynamic `tfsdk:"body"`
+ Query types.Map `tfsdk:"query"`
+ Header types.Map `tfsdk:"header"`
+ Precheck types.List `tfsdk:"precheck"`
+ Poll types.Object `tfsdk:"poll"`
+ Retry types.Object `tfsdk:"retry"`
+ DeleteMethod types.String `tfsdk:"delete_method"`
+ DeleteBody types.Dynamic `tfsdk:"delete_body"`
+ DeletePath types.String `tfsdk:"delete_path"`
+ PrecheckDelete types.List `tfsdk:"precheck_delete"`
+ PollDelete types.Object `tfsdk:"poll_delete"`
+ RetryDelete types.Object `tfsdk:"retry_delete"`
+ OutputAttrs types.Set `tfsdk:"output_attrs"`
+ Output types.Dynamic `tfsdk:"output"`
}
func (r *OperationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
@@ -63,6 +65,7 @@ func (r *OperationResource) Schema(ctx context.Context, req resource.SchemaReque
resp.Schema = schema.Schema{
Description: "`restful_operation` represents a one-time API call operation.",
MarkdownDescription: "`restful_operation` represents a one-time API call operation.",
+ Version: 1,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The ID of the operation. Same as the `path`.",
@@ -88,13 +91,10 @@ func (r *OperationResource) Schema(ctx context.Context, req resource.SchemaReque
stringvalidator.OneOf("PUT", "POST", "PATCH", "DELETE"),
},
},
- "body": schema.StringAttribute{
+ "body": schema.DynamicAttribute{
Description: "The payload for the `Create`/`Update` call.",
MarkdownDescription: "The payload for the `Create`/`Update` call.",
Optional: true,
- Validators: []validator.String{
- myvalidator.StringIsJSON(),
- },
},
"query": schema.MapAttribute{
Description: "The query parameters that are applied to each request. This overrides the `query` set in the provider block.",
@@ -132,14 +132,10 @@ func (r *OperationResource) Schema(ctx context.Context, req resource.SchemaReque
},
},
- "delete_body": schema.StringAttribute{
+ "delete_body": schema.DynamicAttribute{
Description: "The payload for the `Delete` call.",
MarkdownDescription: "The payload for the `Delete` call.",
Optional: true,
- Validators: []validator.String{
- stringvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("delete_method")),
- myvalidator.StringIsJSON(),
- },
},
"precheck_delete": precheckDelete,
@@ -153,7 +149,7 @@ func (r *OperationResource) Schema(ctx context.Context, req resource.SchemaReque
ElementType: types.StringType,
},
- "output": schema.StringAttribute{
+ "output": schema.DynamicAttribute{
Description: "The response body.",
MarkdownDescription: "The response body.",
Computed: true,
@@ -205,7 +201,16 @@ func (r *OperationResource) createOrUpdate(ctx context.Context, tfplan tfsdk.Pla
}
defer unlockFunc()
- response, err := c.Operation(ctx, plan.Path.ValueString(), plan.Body.ValueString(), *opt)
+ b, err := dynamic.ToJSON(plan.Body)
+ if err != nil {
+ diagnostics.AddError(
+ "Error to marshal body",
+ err.Error(),
+ )
+ return
+ }
+
+ response, err := c.Operation(ctx, plan.Path.ValueString(), string(b), *opt)
if err != nil {
diagnostics.AddError(
"Error to call operation",
@@ -221,8 +226,6 @@ func (r *OperationResource) createOrUpdate(ctx context.Context, tfplan tfsdk.Pla
return
}
- b := response.Body()
-
resourceId := plan.Path.ValueString()
// For LRO, wait for completion
@@ -258,8 +261,7 @@ func (r *OperationResource) createOrUpdate(ctx context.Context, tfplan tfsdk.Pla
plan.ID = types.StringValue(resourceId)
// Set Output to state
- plan.Output = types.StringValue(string(b))
- output := string(b)
+ rb := response.Body()
if !plan.OutputAttrs.IsNull() {
// Update the output to only contain the specified attributes.
var outputAttrs []string
@@ -268,7 +270,7 @@ func (r *OperationResource) createOrUpdate(ctx context.Context, tfplan tfsdk.Pla
if diags.HasError() {
return
}
- output, err = FilterAttrsInJSON(output, outputAttrs)
+ fb, err := FilterAttrsInJSON(string(rb), outputAttrs)
if err != nil {
diagnostics.AddError(
"Filter `output` during operation",
@@ -276,8 +278,18 @@ func (r *OperationResource) createOrUpdate(ctx context.Context, tfplan tfsdk.Pla
)
return
}
+ rb = []byte(fb)
}
- plan.Output = types.StringValue(output)
+
+ output, err := dynamic.FromJSONImplied(rb)
+ if err != nil {
+ diagnostics.AddError(
+ "Evaluating `output` during Read",
+ err.Error(),
+ )
+ return
+ }
+ plan.Output = output
diags = state.Set(ctx, plan)
diagnostics.Append(diags...)
@@ -330,18 +342,34 @@ func (r *OperationResource) Delete(ctx context.Context, req resource.DeleteReque
path := state.ID.ValueString()
if !state.DeletePath.IsNull() {
- var err error
- path, err = buildpath.BuildPath(state.DeletePath.ValueString(), r.p.apiOpt.BaseURL.String(), state.Path.ValueString(), []byte(state.Output.ValueString()))
+ body, err := dynamic.ToJSON(state.Output)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ fmt.Sprintf("Failed to build the path for deleting the operation resource"),
+ fmt.Sprintf("Failed to marshal the output: %v", err),
+ )
+ return
+ }
+ path, err = buildpath.BuildPath(state.DeletePath.ValueString(), r.p.apiOpt.BaseURL.String(), state.Path.ValueString(), body)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Failed to build the path for deleting the operation resource"),
- fmt.Sprintf("Can't build path with `delete_path`: %q, `path`: %q, `body`: %q", state.DeletePath.ValueString(), state.Path.ValueString(), string(state.Output.ValueString())),
+ fmt.Sprintf("Can't build path with `delete_path`: %q, `path`: %q, `body`: %q, error: %v", state.DeletePath.ValueString(), state.Path.ValueString(), string(body), err),
)
return
}
}
- response, err := c.Operation(ctx, path, state.DeleteBody.ValueString(), *opt)
+ b, err := dynamic.ToJSON(state.DeleteBody)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error to marshal delete body",
+ err.Error(),
+ )
+ return
+ }
+
+ response, err := c.Operation(ctx, path, string(b), *opt)
if err != nil {
resp.Diagnostics.AddError(
"Delete: Error to call operation",
diff --git a/internal/provider/operation_resource_upgrader.go b/internal/provider/operation_resource_upgrader.go
new file mode 100644
index 0000000..2b23f82
--- /dev/null
+++ b/internal/provider/operation_resource_upgrader.go
@@ -0,0 +1,85 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/magodo/terraform-provider-restful/internal/dynamic"
+ "github.com/magodo/terraform-provider-restful/internal/provider/migrate"
+)
+
+func (r *OperationResource) UpgradeState(context.Context) map[int64]resource.StateUpgrader {
+ return map[int64]resource.StateUpgrader{
+ 0: {
+ PriorSchema: &migrate.OperationSchemaV0,
+ StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
+ var pd migrate.OperationDataV0
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &pd)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var err error
+
+ body := types.DynamicNull()
+ if !pd.Body.IsNull() {
+ body, err = dynamic.FromJSONImplied([]byte(pd.Body.ValueString()))
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Upgrade State Error",
+ fmt.Sprintf(`Converting "body": %v`, err),
+ )
+ }
+ }
+
+ deleteBody := types.DynamicNull()
+ if !pd.DeleteBody.IsNull() {
+ deleteBody, err = dynamic.FromJSONImplied([]byte(pd.DeleteBody.ValueString()))
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Upgrade State Error",
+ fmt.Sprintf(`Converting "delete_body": %v`, err),
+ )
+ }
+ }
+
+ output := types.DynamicNull()
+ if !pd.Output.IsNull() {
+ output, err = dynamic.FromJSONImplied([]byte(pd.Output.ValueString()))
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Upgrade State Error",
+ fmt.Sprintf(`Converting "output": %v`, err),
+ )
+ }
+ }
+
+ upgradedStateData := operationResourceData{
+ ID: pd.ID,
+ Path: pd.Path,
+ Method: pd.Method,
+ Body: body,
+ Query: pd.Query,
+ Header: pd.Header,
+ Precheck: pd.Precheck,
+ Poll: pd.Poll,
+ Retry: pd.Retry,
+ DeleteMethod: pd.DeleteMethod,
+ DeleteBody: deleteBody,
+ DeletePath: pd.DeletePath,
+ PrecheckDelete: pd.PrecheckDelete,
+ PollDelete: pd.PollDelete,
+ RetryDelete: pd.RetryDelete,
+ OutputAttrs: pd.OutputAttrs,
+ Output: output,
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateData)...)
+ },
+ },
+ }
+}
diff --git a/internal/provider/resource.go b/internal/provider/resource.go
index 48ea0cd..959d2c8 100644
--- a/internal/provider/resource.go
+++ b/internal/provider/resource.go
@@ -6,7 +6,6 @@ import (
"fmt"
"net/http"
"net/url"
- "strings"
jsonpatch "github.com/evanphx/json-patch"
"github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
@@ -24,18 +23,18 @@ import (
"github.com/magodo/terraform-provider-restful/internal/buildpath"
"github.com/magodo/terraform-provider-restful/internal/client"
"github.com/magodo/terraform-provider-restful/internal/defaults"
+ "github.com/magodo/terraform-provider-restful/internal/dynamic"
myvalidator "github.com/magodo/terraform-provider-restful/internal/validator"
"github.com/tidwall/gjson"
+ "github.com/tidwall/sjson"
)
-// Magic header used to indicate the value in the state is derived from import.
-const __IMPORT_HEADER__ = "__RESTFUL_PROVIDER__"
-
type Resource struct {
p *Provider
}
var _ resource.Resource = &Resource{}
+var _ resource.ResourceWithUpgradeState = &Resource{}
type resourceData struct {
ID types.String `tfsdk:"id"`
@@ -57,8 +56,7 @@ type resourceData struct {
PrecheckUpdate types.List `tfsdk:"precheck_update"`
PrecheckDelete types.List `tfsdk:"precheck_delete"`
- Body types.String `tfsdk:"body"`
- WriteOnlyAttributes types.List `tfsdk:"write_only_attrs"`
+ Body types.Dynamic `tfsdk:"body"`
PollCreate types.Object `tfsdk:"poll_create"`
PollUpdate types.Object `tfsdk:"poll_update"`
@@ -69,15 +67,16 @@ type resourceData struct {
RetryUpdate types.Object `tfsdk:"retry_update"`
RetryDelete types.Object `tfsdk:"retry_delete"`
- MergePatchDisabled types.Bool `tfsdk:"merge_patch_disabled"`
- Query types.Map `tfsdk:"query"`
- Header types.Map `tfsdk:"header"`
+ WriteOnlyAttributes types.List `tfsdk:"write_only_attrs"`
+ MergePatchDisabled types.Bool `tfsdk:"merge_patch_disabled"`
+ Query types.Map `tfsdk:"query"`
+ Header types.Map `tfsdk:"header"`
CheckExistance types.Bool `tfsdk:"check_existance"`
ForceNewAttrs types.Set `tfsdk:"force_new_attrs"`
OutputAttrs types.Set `tfsdk:"output_attrs"`
- Output types.String `tfsdk:"output"`
+ Output types.Dynamic `tfsdk:"output"`
}
type pollData struct {
@@ -334,6 +333,7 @@ func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp
resp.Schema = schema.Schema{
Description: "`restful_resource` manages a restful resource.",
MarkdownDescription: "`restful_resource` manages a restful resource.",
+ Version: 1,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The ID of the Resource.",
@@ -388,13 +388,10 @@ func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp
},
},
- "body": schema.StringAttribute{
+ "body": schema.DynamicAttribute{
Description: "The properties of the resource.",
MarkdownDescription: "The properties of the resource.",
Required: true,
- Validators: []validator.String{
- myvalidator.StringIsJSON(),
- },
},
"poll_create": pollAttribute("Create"),
@@ -410,12 +407,6 @@ func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp
"retry_update": retryAttribute("Update (i.e. PUT/PATCH/POST)"),
"retry_delete": retryAttribute("Delete (i.e. DELETE)"),
- "write_only_attrs": schema.ListAttribute{
- Description: "A list of paths (in gjson syntax) to the attributes that are only settable, but won't be read in GET response.",
- MarkdownDescription: "A list of paths (in [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)) to the attributes that are only settable, but won't be read in GET response.",
- Optional: true,
- ElementType: types.StringType,
- },
"create_method": schema.StringAttribute{
Description: "The method used to create the resource. Possible values are `PUT`, `POST` and `PATCH`. This overrides the `create_method` set in the provider block (defaults to POST).",
MarkdownDescription: "The method used to create the resource. Possible values are `PUT`, `POST` and `PATCH`. This overrides the `create_method` set in the provider block (defaults to POST).",
@@ -440,6 +431,12 @@ func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp
stringvalidator.OneOf("DELETE", "POST"),
},
},
+ "write_only_attrs": schema.ListAttribute{
+ Description: "A list of paths (in gjson syntax) to the attributes that are only settable, but won't be read in GET response.",
+ MarkdownDescription: "A list of paths (in [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)) to the attributes that are only settable, but won't be read in GET response.",
+ Optional: true,
+ ElementType: types.StringType,
+ },
"merge_patch_disabled": schema.BoolAttribute{
Description: "Whether to use a JSON Merge Patch as the request body in the PATCH update? This is only effective when `update_method` is set to `PATCH`. This overrides the `merge_patch_disabled` set in the provider block (defaults to `false`).",
MarkdownDescription: "Whether to use a JSON Merge Patch as the request body in the PATCH update? This is only effective when `update_method` is set to `PATCH`. This overrides the `merge_patch_disabled` set in the provider block (defaults to `false`).",
@@ -474,7 +471,7 @@ func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp
Optional: true,
ElementType: types.StringType,
},
- "output": schema.StringAttribute{
+ "output": schema.DynamicAttribute{
Description: "The response body after reading the resource.",
MarkdownDescription: "The response body after reading the resource.",
Computed: true,
@@ -490,13 +487,20 @@ func (r *Resource) ValidateConfig(ctx context.Context, req resource.ValidateConf
if diags.HasError() {
return
}
-
if !config.Body.IsUnknown() {
+ b, err := dynamic.ToJSON(config.Body)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Invalid configuration",
+ fmt.Sprintf("marshal body: %v", err),
+ )
+ return
+ }
if !config.WriteOnlyAttributes.IsUnknown() && !config.WriteOnlyAttributes.IsNull() {
for _, ie := range config.WriteOnlyAttributes.Elements() {
ie := ie.(types.String)
if !ie.IsUnknown() && !ie.IsNull() {
- if !gjson.Get(config.Body.ValueString(), ie.ValueString()).Exists() {
+ if !gjson.Get(string(b), ie.ValueString()).Exists() {
resp.Diagnostics.AddError(
"Invalid configuration",
fmt.Sprintf(`Invalid path in "write_only_attrs": %s`, ie.String()),
@@ -513,18 +517,27 @@ func (r *Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanReques
// If the entire plan is null, the resource is planned for destruction.
return
}
-
if req.State.Raw.IsNull() {
// If the entire state is null, the resource is planned for creation.
return
}
+
var plan resourceData
if diags := req.Plan.Get(ctx, &plan); diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}
+ var state resourceData
+ if diags := req.State.Get(ctx, &state); diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+
+ defer func() {
+ resp.Plan.Set(ctx, plan)
+ }()
- if !plan.ForceNewAttrs.IsUnknown() && !plan.Body.IsUnknown() {
+ if !plan.ForceNewAttrs.IsUnknown() && dynamic.IsFullyKnown(plan.Body) {
var forceNewAttrs []types.String
if diags := plan.ForceNewAttrs.ElementsAs(ctx, &forceNewAttrs, false); diags != nil {
resp.Diagnostics.Append(diags...)
@@ -545,16 +558,23 @@ func (r *Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanReques
return
}
- originJson := state.Body.ValueString()
- if originJson == "" {
- originJson = "{}"
+ originJson, err := dynamic.ToJSON(state.Body)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "ModifyPlan failed",
+ fmt.Sprintf("marshaling state body: %v", err),
+ )
}
- modifiedJson := plan.Body.ValueString()
- if modifiedJson == "" {
- modifiedJson = "{}"
+ modifiedJson, err := dynamic.ToJSON(plan.Body)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "ModifyPlan failed",
+ fmt.Sprintf("marshaling plan body: %v", err),
+ )
}
- patch, err := jsonpatch.CreateMergePatch([]byte(originJson), []byte(modifiedJson))
+
+ patch, err := jsonpatch.CreateMergePatch(originJson, modifiedJson)
if err != nil {
resp.Diagnostics.AddError("failed to create merge patch", err.Error())
return
@@ -637,7 +657,15 @@ func (r Resource) Create(ctx context.Context, req resource.CreateRequest, resp *
defer unlockFunc()
// Create the resource
- response, err := c.Create(ctx, plan.Path.ValueString(), plan.Body.ValueString(), *opt)
+ b, err := dynamic.ToJSON(plan.Body)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error to marshal body",
+ err.Error(),
+ )
+ return
+ }
+ response, err := c.Create(ctx, plan.Path.ValueString(), string(b), *opt)
if err != nil {
resp.Diagnostics.AddError(
"Error to call create",
@@ -653,7 +681,7 @@ func (r Resource) Create(ctx context.Context, req resource.CreateRequest, resp *
return
}
- b := response.Body()
+ b = response.Body()
if sel := plan.CreateSelector.ValueString(); sel != "" {
// Guaranteed by schema
@@ -735,7 +763,7 @@ func (r Resource) Create(ctx context.Context, req resource.CreateRequest, resp *
State: resp.State,
Diagnostics: resp.Diagnostics,
}
- r.Read(ctx, rreq, &rresp)
+ r.read(ctx, rreq, &rresp, false)
*resp = resource.CreateResponse{
State: rresp.State,
@@ -744,6 +772,10 @@ func (r Resource) Create(ctx context.Context, req resource.CreateRequest, resp *
}
func (r Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ r.read(ctx, req, resp, true)
+}
+
+func (r Resource) read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, updateBody bool) {
var state resourceData
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
@@ -791,45 +823,67 @@ func (r Resource) Read(ctx context.Context, req resource.ReadRequest, resp *reso
return
}
b = []byte(sb)
-
}
- var writeOnlyAttributes []string
- diags = state.WriteOnlyAttributes.ElementsAs(ctx, &writeOnlyAttributes, false)
- resp.Diagnostics.Append(diags...)
- if diags.HasError() {
- return
- }
+ if updateBody {
+ var writeOnlyAttributes []string
+ diags = state.WriteOnlyAttributes.ElementsAs(ctx, &writeOnlyAttributes, false)
+ resp.Diagnostics.Append(diags...)
+ if diags.HasError() {
+ return
+ }
- var body string
- if strings.HasPrefix(state.Body.ValueString(), __IMPORT_HEADER__) {
- // This branch is only invoked during `terraform import`.
- body, err = ModifyBodyForImport(strings.TrimPrefix(state.Body.ValueString(), __IMPORT_HEADER__), string(b))
- } else {
- body, err = ModifyBody(state.Body.ValueString(), string(b), writeOnlyAttributes)
- }
- if err != nil {
- resp.Diagnostics.AddError(
- "Modifying `body` during Read",
- err.Error(),
- )
- return
- }
+ // Update the read response by compensating the write only attributes from state
+ if len(writeOnlyAttributes) != 0 {
+ stateBody, err := dynamic.ToJSON(state.Body)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Read failure",
+ fmt.Sprintf("marshal state body: %v", err),
+ )
+ return
+ }
+ pb := string(b)
+ for _, path := range writeOnlyAttributes {
+ if gjson.Get(string(stateBody), path).Exists() && !gjson.Get(string(b), path).Exists() {
+ pb, err = sjson.Set(pb, path, gjson.Get(string(stateBody), path).Value())
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Read failure",
+ fmt.Sprintf("json set write only attr at path %q: %v", path, err),
+ )
+ return
+ }
+ }
+ }
+ b = []byte(pb)
+ }
- // Set body, which is modified during read.
- state.Body = types.StringValue(string(body))
+ var body types.Dynamic
+ if body, err = dynamic.FromJSON(b, state.Body.UnderlyingValue().Type(ctx)); err != nil {
+ // An error might occur here during refresh, when the type of the state doesn't match the remote,
+ // e.g. a tuple field has different number of elements.
+ // In this case, we fallback to the implied types, to make the refresh proceed and return a reasonable plan diff.
+ if body, err = dynamic.FromJSONImplied(b); err != nil {
+ resp.Diagnostics.AddError(
+ "Evaluating `body` during Read",
+ err.Error(),
+ )
+ return
+ }
+ }
+ state.Body = body
+ }
// Set output
- output := string(b)
if !state.OutputAttrs.IsNull() {
- // Update the output to only contain the specified attributes.
var outputAttrs []string
diags = state.OutputAttrs.ElementsAs(ctx, &outputAttrs, false)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
- output, err = FilterAttrsInJSON(output, outputAttrs)
+ fb, err := FilterAttrsInJSON(string(b), outputAttrs)
if err != nil {
resp.Diagnostics.AddError(
"Filter `output` during Read",
@@ -837,8 +891,18 @@ func (r Resource) Read(ctx context.Context, req resource.ReadRequest, resp *reso
)
return
}
+ b = []byte(fb)
+ }
+
+ output, err := dynamic.FromJSONImplied(b)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Evaluating `output` during Read",
+ err.Error(),
+ )
+ return
}
- state.Output = types.StringValue(output)
+ state.Output = output
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
@@ -870,8 +934,25 @@ func (r Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *
return
}
- // Invoke API to Update the resource only when there are changes in the body.
- if state.Body.ValueString() != plan.Body.ValueString() {
+ stateBody, err := dynamic.ToJSON(state.Body)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Update failure",
+ fmt.Sprintf("Error to marshal state body: %v", err),
+ )
+ return
+ }
+ planBody, err := dynamic.ToJSON(plan.Body)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Update failure",
+ fmt.Sprintf("Error to marshal plan body: %v", err),
+ )
+ return
+ }
+
+ // Invoke API to Update the resource only when there are changes in the body (regardless of the TF type diff).
+ if string(stateBody) != string(planBody) {
// Precheck
unlockFunc, diags := precheck(ctx, c, r.p.apiOpt, state.ID.ValueString(), opt.Header, opt.Query, plan.PrecheckUpdate)
if diags.HasError() {
@@ -880,9 +961,16 @@ func (r Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *
}
defer unlockFunc()
- body := plan.Body.ValueString()
if opt.Method == "PATCH" && !opt.MergePatchDisabled {
- b, err := jsonpatch.CreateMergePatch([]byte(state.Body.ValueString()), []byte(plan.Body.ValueString()))
+ stateBodyJSON, err := dynamic.ToJSON(state.Body)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Update failure",
+ fmt.Sprintf("Error to marshal state body: %v", err),
+ )
+ return
+ }
+ b, err := jsonpatch.CreateMergePatch(stateBodyJSON, planBody)
if err != nil {
resp.Diagnostics.AddError(
"Update failure",
@@ -890,23 +978,30 @@ func (r Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *
)
return
}
- body = string(b)
+ planBody = b
}
path := plan.ID.ValueString()
if !plan.UpdatePath.IsNull() {
- var err error
- path, err = buildpath.BuildPath(plan.UpdatePath.ValueString(), r.p.apiOpt.BaseURL.String(), plan.Path.ValueString(), []byte(state.Output.ValueString()))
+ output, err := dynamic.ToJSON(state.Output)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Failed to marshal json for `output`",
+ err.Error(),
+ )
+ return
+ }
+ path, err = buildpath.BuildPath(plan.UpdatePath.ValueString(), r.p.apiOpt.BaseURL.String(), plan.Path.ValueString(), output)
if err != nil {
resp.Diagnostics.AddError(
- fmt.Sprintf("Failed to build the path for updating the resource"),
- fmt.Sprintf("Can't build path with `update_path`: %q, `path`: %q, `body`: %q", plan.UpdatePath.ValueString(), plan.Path.ValueString(), string(state.Output.ValueString())),
+ "Failed to build the path for updating the resource",
+ fmt.Sprintf("Can't build path with `update_path`: %q, `path`: %q, `body`: %q", plan.UpdatePath.ValueString(), plan.Path.ValueString(), output),
)
return
}
}
- response, err := c.Update(ctx, path, body, *opt)
+ response, err := c.Update(ctx, path, planBody, *opt)
if err != nil {
resp.Diagnostics.AddError(
"Error to call update",
@@ -966,7 +1061,7 @@ func (r Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *
State: resp.State,
Diagnostics: resp.Diagnostics,
}
- r.Read(ctx, rreq, &rresp)
+ r.read(ctx, rreq, &rresp, false)
*resp = resource.UpdateResponse{
State: rresp.State,
@@ -1000,12 +1095,19 @@ func (r Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *
path := state.ID.ValueString()
if !state.DeletePath.IsNull() {
- var err error
- path, err = buildpath.BuildPath(state.DeletePath.ValueString(), r.p.apiOpt.BaseURL.String(), state.Path.ValueString(), []byte(state.Output.ValueString()))
+ output, err := dynamic.ToJSON(state.Output)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Failed to marshal json for `output`",
+ err.Error(),
+ )
+ return
+ }
+ path, err = buildpath.BuildPath(state.DeletePath.ValueString(), r.p.apiOpt.BaseURL.String(), state.Path.ValueString(), output)
if err != nil {
resp.Diagnostics.AddError(
- fmt.Sprintf("Failed to build the path for deleting the resource"),
- fmt.Sprintf("Can't build path with `delete_path`: %q, `path`: %q, `body`: %q", state.DeletePath.ValueString(), state.Path.ValueString(), string(state.Output.ValueString())),
+ "Failed to build the path for deleting the resource",
+ fmt.Sprintf("Can't build path with `delete_path`: %q, `path`: %q, `body`: %q", state.DeletePath.ValueString(), state.Path.ValueString(), output),
)
return
}
@@ -1070,25 +1172,15 @@ type importSpec struct {
// Path is the path used to create the resource.
Path string `json:"path"`
- // UpdatePath is the path used to update the resource
- UpdatePath *string `json:"update_path"`
-
- // DeletePath is the path used to delte the resource
- DeletePath *string `json:"delete_path"`
-
// Query is only required when it is mandatory for reading the resource.
Query url.Values `json:"query"`
// Header is only required when it is mandatory for reading the resource.
Header url.Values `json:"header"`
- CreateMethod *string `json:"create_method"`
- UpdateMethod *string `json:"update_method"`
- DeleteMethod *string `json:"delete_method"`
-
// Body represents the properties expected to be managed and tracked by Terraform. The value of these properties can be null as a place holder.
// When absent, all the response payload read wil be set to `body`.
- Body interface{}
+ Body json.RawMessage `json:"body"`
}
func (Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
@@ -1123,19 +1215,15 @@ func (Resource) ImportState(ctx context.Context, req resource.ImportStateRequest
return
}
- var body string
- if imp.Body != nil {
- b, err := json.Marshal(imp.Body)
- if err != nil {
- resp.Diagnostics.AddError(
- "Resource Import Error",
- fmt.Sprintf("failed to marshal id.body: %v", err),
- )
- return
- }
- body = string(b)
+ body, err := dynamic.FromJSONImplied(imp.Body)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Resource Import Error",
+ fmt.Sprintf("unmarshal `body`: %v", err),
+ )
+ return
}
- body = __IMPORT_HEADER__ + body
+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, idPath, imp.Id)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path, imp.Path)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, bodyPath, body)...)
diff --git a/internal/provider/resource_azure_test.go b/internal/provider/resource_azure_test.go
index dd6ef82..9e7f873 100644
--- a/internal/provider/resource_azure_test.go
+++ b/internal/provider/resource_azure_test.go
@@ -67,7 +67,7 @@ func TestResource_Azure_ResourceGroup(t *testing.T) {
{
Config: d.resourceGroup(),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -80,7 +80,7 @@ func TestResource_Azure_ResourceGroup(t *testing.T) {
{
Config: d.resourceGroup_complete(),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -88,7 +88,7 @@ func TestResource_Azure_ResourceGroup(t *testing.T) {
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"poll_delete", "create_method"},
- ImportStateIdFunc: d.resourceGroupImportStateIdFunc(addr),
+ ImportStateIdFunc: d.resourceGroupCompleteImportStateIdFunc(addr),
},
},
})
@@ -105,7 +105,7 @@ func TestResource_Azure_ResourceGroup_updatePath(t *testing.T) {
{
Config: d.resourceGroup_updatePath(),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -118,7 +118,7 @@ func TestResource_Azure_ResourceGroup_updatePath(t *testing.T) {
{
Config: d.resourceGroup_updatePath_complete(),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -126,7 +126,7 @@ func TestResource_Azure_ResourceGroup_updatePath(t *testing.T) {
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"poll_delete", "create_method", "update_path"},
- ImportStateIdFunc: d.resourceGroupUpdatePathImportStateIdFunc(addr),
+ ImportStateIdFunc: d.resourceGroupUpdatePathCompleteImportStateIdFunc(addr),
},
},
})
@@ -143,7 +143,7 @@ func TestResource_Azure_VirtualNetwork(t *testing.T) {
{
Config: d.vnet("foo"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -156,7 +156,7 @@ func TestResource_Azure_VirtualNetwork(t *testing.T) {
{
Config: d.vnet("bar"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -181,7 +181,7 @@ func TestResource_Azure_VirtualNetwork_Precheck(t *testing.T) {
{
Config: d.vnet_precheck("foo"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -194,7 +194,7 @@ func TestResource_Azure_VirtualNetwork_Precheck(t *testing.T) {
{
Config: d.vnet_precheck("bar"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -219,7 +219,7 @@ func TestResource_Azure_VirtualNetwork_SimplePoll(t *testing.T) {
{
Config: d.vnet_simple_poll("foo"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -232,7 +232,7 @@ func TestResource_Azure_VirtualNetwork_SimplePoll(t *testing.T) {
{
Config: d.vnet_simple_poll("bar"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -257,7 +257,7 @@ func TestResource_Azure_RouteTable_Precheck(t *testing.T) {
{
Config: d.routetable_precheck("foo"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -270,7 +270,7 @@ func TestResource_Azure_RouteTable_Precheck(t *testing.T) {
{
Config: d.routetable_precheck("bar"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -294,13 +294,13 @@ func TestOperationResource_Azure_Register_RP(t *testing.T) {
{
Config: d.unregisterRP("Microsoft.ProviderHub"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
Config: d.registerRP("Microsoft.ProviderHub"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
},
@@ -358,9 +358,9 @@ resource "restful_resource" "test" {
query = {
api-version = ["2020-06-01"]
}
- body = jsonencode({
+ body = {
location = "westeurope"
- })
+ }
create_method = "PUT"
@@ -396,12 +396,12 @@ resource "restful_resource" "test" {
query = {
api-version = ["2020-06-01"]
}
- body = jsonencode({
+ body = {
location = "westeurope"
tags = {
foo = "bar"
}
- })
+ }
create_method = "PUT"
@@ -438,9 +438,9 @@ resource "restful_resource" "test" {
query = {
api-version = ["2020-06-01"]
}
- body = jsonencode({
+ body = {
location = "westeurope"
- })
+ }
create_method = "PUT"
@@ -479,12 +479,12 @@ resource "restful_resource" "test" {
query = {
api-version = ["2020-06-01"]
}
- body = jsonencode({
+ body = {
location = "westeurope"
tags = {
foo = "bar"
}
- })
+ }
create_method = "PUT"
@@ -510,7 +510,21 @@ func (d azureData) resourceGroupImportStateIdFunc(addr string) func(s *terraform
"api-version": ["2020-06-01"]
},
"path": %[1]q,
-"create_method": "PUT",
+"body": {
+ "location": null
+}
+}`, s.RootModule().Resources[addr].Primary.Attributes["id"]), nil
+ }
+}
+
+func (d azureData) resourceGroupCompleteImportStateIdFunc(addr string) func(s *terraform.State) (string, error) {
+ return func(s *terraform.State) (string, error) {
+ return fmt.Sprintf(`{
+"id": %[1]q,
+"query": {
+ "api-version": ["2020-06-01"]
+},
+"path": %[1]q,
"body": {
"location": null,
"tags": null
@@ -526,7 +540,22 @@ func (d azureData) resourceGroupUpdatePathImportStateIdFunc(addr string) func(s
"query": {
"api-version": ["2020-06-01"]
},
-"create_method": "PUT",
+"path": %[1]q,
+"update_path": %[1]q,
+"body": {
+ "location": null
+}
+}`, s.RootModule().Resources[addr].Primary.Attributes["id"]), nil
+ }
+}
+
+func (d azureData) resourceGroupUpdatePathCompleteImportStateIdFunc(addr string) func(s *terraform.State) (string, error) {
+ return func(s *terraform.State) (string, error) {
+ return fmt.Sprintf(`{
+"id": %[1]q,
+"query": {
+ "api-version": ["2020-06-01"]
+},
"path": %[1]q,
"update_path": %[1]q,
"body": {
@@ -567,7 +596,6 @@ func (d azureData) routeImportStateIdFunc(addr string) func(s *terraform.State)
},
"path": %[1]q,
"body": {
- "location": null,
"properties": {
"addressPrefix": null,
"nextHopType": null
@@ -599,9 +627,9 @@ resource "restful_resource" "rg" {
query = {
api-version = ["2020-06-01"]
}
- body = jsonencode({
+ body = {
location = "westeurope"
- })
+ }
poll_delete = {
status_locator = "code"
@@ -641,7 +669,7 @@ resource "restful_resource" "test" {
poll_update = local.vnet_poll
poll_delete = local.vnet_poll
- body = jsonencode({
+ body = {
location = "westus"
properties = {
addressSpace = {
@@ -651,7 +679,7 @@ resource "restful_resource" "test" {
tags = {
foo = "%s"
}
- })
+ }
}
`, d.vnet_template(), d.rd, tag)
}
@@ -679,9 +707,9 @@ resource "restful_resource" "rg" {
query = {
api-version = ["2020-06-01"]
}
- body = jsonencode({
+ body = {
location = "westeurope"
- })
+ }
poll_delete = {
status_locator = "code"
@@ -732,7 +760,7 @@ resource "restful_resource" "test" {
poll_update = local.vnet_poll
poll_delete = local.vnet_poll
- body = jsonencode({
+ body = {
location = "westus"
properties = {
addressSpace = {
@@ -742,7 +770,7 @@ resource "restful_resource" "test" {
tags = {
foo = "%s"
}
- })
+ }
}
`, d.url, d.clientId, d.clientSecret, d.tenantId, d.subscriptionId, d.rd, d.rd, tag)
}
@@ -779,7 +807,7 @@ resource "restful_resource" "test" {
}
}
- body = jsonencode({
+ body = {
location = "westus"
properties = {
addressSpace = {
@@ -789,7 +817,7 @@ resource "restful_resource" "test" {
tags = {
foo = "%s"
}
- })
+ }
}
`, d.vnet_template(), d.rd, tag)
}
@@ -816,9 +844,9 @@ resource "restful_resource" "rg" {
query = {
api-version = ["2020-06-01"]
}
- body = jsonencode({
+ body = {
location = "westeurope"
- })
+ }
poll_delete = {
status_locator = "code"
@@ -854,12 +882,12 @@ resource "restful_resource" "table" {
query = {
api-version = ["2022-07-01"]
}
- body = jsonencode({
+ body = {
location = "westus"
tags = {
foo = "%s"
}
- })
+ }
poll_create = local.poll
poll_delete = local.poll
}
@@ -878,12 +906,12 @@ resource "restful_resource" "route1" {
poll_update = local.poll
poll_delete = local.poll
- body = jsonencode({
+ body = {
properties = {
nextHopType = "VnetLocal"
addressPrefix = "10.1.0.0/16"
}
- })
+ }
}
resource "restful_resource" "route2" {
@@ -900,12 +928,12 @@ resource "restful_resource" "route2" {
poll_update = local.poll
poll_delete = local.poll
- body = jsonencode({
+ body = {
properties = {
nextHopType = "VnetLocal"
addressPrefix = "10.2.0.0/16"
}
- })
+ }
}
`, d.url, d.clientId, d.clientSecret, d.tenantId, d.subscriptionId, d.rd, d.rd, tag)
}
diff --git a/internal/provider/resource_dead_simple_json_server_test.go b/internal/provider/resource_dead_simple_json_server_test.go
index 666ca3a..0e1f3db 100644
--- a/internal/provider/resource_dead_simple_json_server_test.go
+++ b/internal/provider/resource_dead_simple_json_server_test.go
@@ -53,7 +53,7 @@ func TestResource_DeadSimpleServer_ObjectArray(t *testing.T) {
{
Config: d.object_array(srv.URL, "foo"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.#"),
),
},
{
@@ -68,7 +68,7 @@ func TestResource_DeadSimpleServer_ObjectArray(t *testing.T) {
{
Config: d.object_array(srv.URL, "bar"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.#"),
),
},
{
@@ -123,7 +123,7 @@ func TestResource_DeadSimpleServer_CreateRetString(t *testing.T) {
{
Config: d.create_ret_string(srv.URL),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckNoResourceAttr(addr, "output.#"),
),
},
{
@@ -171,11 +171,11 @@ provider "restful" {
resource "restful_resource" "test" {
path = "test"
create_method = "PUT"
- body = jsonencode([
- {
- foo = %q
- }
-])
+ body = [
+ {
+ foo = %q
+ }
+ ]
}
`, url, v)
}
@@ -190,7 +190,7 @@ resource "restful_resource" "test" {
path = "test"
create_method = "PUT"
read_path = "$(path)/$(body)"
- body = "{}"
+ body = {}
}
`, url)
}
diff --git a/internal/provider/resource_jsonserver_test.go b/internal/provider/resource_jsonserver_test.go
index 0393e28..4984e26 100644
--- a/internal/provider/resource_jsonserver_test.go
+++ b/internal/provider/resource_jsonserver_test.go
@@ -43,7 +43,7 @@ func TestResource_JSONServer_Basic(t *testing.T) {
{
Config: d.basic("foo"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -58,7 +58,7 @@ func TestResource_JSONServer_Basic(t *testing.T) {
{
Config: d.basic("bar"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -85,7 +85,7 @@ func TestResource_JSONServer_PatchUpdate(t *testing.T) {
{
Config: d.patch("foo"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -100,7 +100,7 @@ func TestResource_JSONServer_PatchUpdate(t *testing.T) {
{
Config: d.patch("bar"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -127,7 +127,7 @@ func TestResource_JSONServer_FullPath(t *testing.T) {
{
Config: d.fullPath("foo"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -136,13 +136,13 @@ func TestResource_JSONServer_FullPath(t *testing.T) {
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"read_path", "update_path", "delete_path"},
ImportStateIdFunc: func(s *terraform.State) (string, error) {
- return fmt.Sprintf(`{"id": %q, "path": "posts", "update_path": "$(path)/$(body.id)", "delete_path": "$(path)/$(body.id)", "body": {"foo": null}}`, s.RootModule().Resources[addr].Primary.Attributes["id"]), nil
+ return fmt.Sprintf(`{"id": %q, "path": "posts", "body": {"foo": null}}`, s.RootModule().Resources[addr].Primary.Attributes["id"]), nil
},
},
{
Config: d.fullPath("bar"),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -151,7 +151,7 @@ func TestResource_JSONServer_FullPath(t *testing.T) {
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"read_path", "update_path", "delete_path"},
ImportStateIdFunc: func(s *terraform.State) (string, error) {
- return fmt.Sprintf(`{"id": %q, "path": "posts", "update_path": "$(path)/$(body.id)", "delete_path": "$(path)/$(body.id)", "body": {"foo": null}}`, s.RootModule().Resources[addr].Primary.Attributes["id"]), nil
+ return fmt.Sprintf(`{"id": %q, "path": "posts", "body": {"foo": null}}`, s.RootModule().Resources[addr].Primary.Attributes["id"]), nil
},
},
},
@@ -169,13 +169,41 @@ func TestResource_JSONServer_OutputAttrs(t *testing.T) {
{
Config: d.outputAttrs(),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrWith(addr, "output", CheckJSONEqual("output", `{"foo": "bar", "obj": {"a": 1}}`)),
+ resource.TestCheckResourceAttr(addr, "output.foo", "bar"),
+ resource.TestCheckResourceAttr(addr, "output.obj.a", "1"),
),
},
},
})
}
+func TestResource_JSONServer_MigrateV0ToV1(t *testing.T) {
+ addr := "restful_resource.test"
+ d := newJsonServerData()
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { d.precheck(t) },
+ CheckDestroy: d.CheckDestroy(addr),
+ Steps: []resource.TestStep{
+ {
+ ProtoV6ProviderFactories: nil,
+ ExternalProviders: map[string]resource.ExternalProvider{
+ "restful": {
+ VersionConstraint: "= 0.13.2",
+ Source: "registry.terraform.io/magodo/restful",
+ },
+ },
+ Config: d.migrate_v0(),
+ },
+ {
+ ProtoV6ProviderFactories: acceptance.ProviderFactory(),
+ ExternalProviders: nil,
+ Config: d.migrate_v1(),
+ PlanOnly: true,
+ },
+ },
+ })
+}
+
func (d jsonServerData) CheckDestroy(addr string) func(*terraform.State) error {
return func(s *terraform.State) error {
c, err := client.New(context.TODO(), d.url, nil)
@@ -207,9 +235,9 @@ provider "restful" {
resource "restful_resource" "test" {
path = "posts"
- body = jsonencode({
+ body = {
foo = %q
-})
+ }
read_path = "$(path)/$(body.id)"
}
`, d.url, v)
@@ -225,9 +253,9 @@ resource "restful_resource" "test" {
path = "posts"
read_path = "$(path)/$(body.id)"
update_method = "PATCH"
- body = jsonencode({
+ body = {
foo = %q
-})
+ }
}
`, d.url, v)
}
@@ -243,9 +271,9 @@ resource "restful_resource" "test" {
read_path = "$(path)/$(body.id)"
update_path = "$(path)/$(body.id)"
delete_path = "$(path)/$(body.id)"
- body = jsonencode({
+ body = {
foo = %q
-})
+ }
}
`, d.url, v)
@@ -259,15 +287,47 @@ provider "restful" {
resource "restful_resource" "test" {
path = "posts"
- body = jsonencode({
+ body = {
foo = "bar"
obj = {
a = 1
b = 2
}
-})
+ }
read_path = "$(path)/$(body.id)"
output_attrs = ["foo", "obj.a"]
}
`, d.url)
}
+
+func (d jsonServerData) migrate_v0() string {
+ return fmt.Sprintf(`
+provider "restful" {
+ base_url = %q
+}
+
+resource "restful_resource" "test" {
+ path = "posts"
+ body = jsonencode({
+ foo = "bar"
+ })
+ read_path = "$(path)/$(body.id)"
+}
+`, d.url)
+}
+
+func (d jsonServerData) migrate_v1() string {
+ return fmt.Sprintf(`
+provider "restful" {
+ base_url = %q
+}
+
+resource "restful_resource" "test" {
+ path = "posts"
+ body = {
+ foo = "bar"
+ }
+ read_path = "$(path)/$(body.id)"
+}
+`, d.url)
+}
diff --git a/internal/provider/resource_msgraph_test.go b/internal/provider/resource_msgraph_test.go
index 1f36036..18313f7 100644
--- a/internal/provider/resource_msgraph_test.go
+++ b/internal/provider/resource_msgraph_test.go
@@ -67,7 +67,7 @@ func TestResource_MsGraph_User(t *testing.T) {
{
Config: d.user(false),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -79,7 +79,7 @@ func TestResource_MsGraph_User(t *testing.T) {
{
Config: d.userUpdate(false),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -91,7 +91,7 @@ func TestResource_MsGraph_User(t *testing.T) {
{
Config: d.user(true),
Check: resource.ComposeTestCheckFunc(
- resource.TestCheckResourceAttrSet(addr, "output"),
+ resource.TestCheckResourceAttrSet(addr, "output.%"),
),
},
{
@@ -154,7 +154,7 @@ resource "restful_resource" "test" {
path = "/users"
read_path = "$(path)/$(body.id)"
merge_patch_disabled = %t
- body = jsonencode({
+ body = {
accountEnabled = true
mailNickname = "AdeleV"
passwordProfile = {
@@ -163,7 +163,7 @@ resource "restful_resource" "test" {
displayName = "J.Doe"
userPrincipalName = "%d@%s"
- })
+ }
write_only_attrs = [
"mailNickname",
"accountEnabled",
@@ -194,7 +194,7 @@ resource "restful_resource" "test" {
path = "/users"
read_path = "$(path)/$(body.id)"
merge_patch_disabled = %t
- body = jsonencode({
+ body = {
accountEnabled = false
mailNickname = "AdeleV"
passwordProfile = {
@@ -202,7 +202,7 @@ resource "restful_resource" "test" {
}
displayName = "J.Doe2"
userPrincipalName = "%d@%s"
- })
+ }
write_only_attrs = [
"mailNickname",
"accountEnabled",
diff --git a/internal/provider/resource_upgrader.go b/internal/provider/resource_upgrader.go
new file mode 100644
index 0000000..16ccb39
--- /dev/null
+++ b/internal/provider/resource_upgrader.go
@@ -0,0 +1,86 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/magodo/terraform-provider-restful/internal/dynamic"
+ "github.com/magodo/terraform-provider-restful/internal/provider/migrate"
+)
+
+func (r *Resource) UpgradeState(context.Context) map[int64]resource.StateUpgrader {
+ return map[int64]resource.StateUpgrader{
+ 0: {
+ PriorSchema: &migrate.ResourceSchemaV0,
+ StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
+ var pd migrate.ResourceDataV0
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &pd)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var err error
+
+ body := types.DynamicNull()
+ if !pd.Body.IsNull() {
+ body, err = dynamic.FromJSONImplied([]byte(pd.Body.ValueString()))
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Upgrade State Error",
+ fmt.Sprintf(`Converting "body": %v`, err),
+ )
+ }
+ }
+
+ output := types.DynamicNull()
+ if !output.IsNull() {
+ output, err = dynamic.FromJSONImplied([]byte(pd.Output.ValueString()))
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Upgrade State Error",
+ fmt.Sprintf(`Converting "output": %v`, err),
+ )
+ }
+ }
+
+ upgradedStateData := resourceData{
+ ID: pd.ID,
+ Path: pd.Path,
+ CreateSelector: pd.CreateSelector,
+ ReadSelector: pd.ReadSelector,
+ ReadPath: pd.ReadPath,
+ UpdatePath: pd.UpdatePath,
+ DeletePath: pd.DeletePath,
+ CreateMethod: pd.CreateMethod,
+ UpdateMethod: pd.UpdateMethod,
+ DeleteMethod: pd.DeleteMethod,
+ PrecheckCreate: pd.PrecheckCreate,
+ PrecheckUpdate: pd.PrecheckUpdate,
+ PrecheckDelete: pd.PrecheckDelete,
+ Body: body,
+ PollCreate: pd.PollCreate,
+ PollUpdate: pd.PollUpdate,
+ PollDelete: pd.PollDelete,
+ RetryCreate: pd.RetryCreate,
+ RetryRead: pd.RetryRead,
+ RetryUpdate: pd.RetryUpdate,
+ RetryDelete: pd.RetryDelete,
+ WriteOnlyAttributes: pd.WriteOnlyAttributes,
+ MergePatchDisabled: pd.MergePatchDisabled,
+ Query: pd.Query,
+ Header: pd.Header,
+ CheckExistance: pd.CheckExistance,
+ ForceNewAttrs: pd.ForceNewAttrs,
+ OutputAttrs: pd.OutputAttrs,
+ Output: output,
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateData)...)
+ },
+ },
+ }
+}