Skip to content

Commit

Permalink
Merge pull request #252 from detvdl/feat/read_data_support
Browse files Browse the repository at this point in the history
feat: add support for read_data
  • Loading branch information
DRuggeri authored Mar 1, 2024
2 parents 73d19cd + 802e293 commit 8c887f3
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 12 deletions.
1 change: 1 addition & 0 deletions docs/resources/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ resource "restapi_object" "Foo2" {
- `id_attribute` (String) Defaults to `id_attribute` set on the provider. Allows per-resource override of `id_attribute` (see `id_attribute` provider config documentation)
- `object_id` (String) Defaults to the id learned by the provider during normal operations and `id_attribute`. Allows you to set the id manually. This is used in conjunction with the `*_path` attributes.
- `query_string` (String) Query string to be included in the path
- `read_data` (String) Valid JSON object to pass during read requests.
- `read_method` (String) Defaults to `read_method` set on the provider. Allows per-resource override of `read_method` (see `read_method` provider config documentation)
- `read_path` (String) Defaults to `path/{id}`. The API path that represents where to READ (GET) objects of this type on the API server. The string `{id}` will be replaced with the terraform ID of the object.
- `read_search` (Map of String) Custom search for `read_path`. This map will take `search_key`, `search_value`, `results_key` and `query_string` (see datasource config documentation)
Expand Down
12 changes: 9 additions & 3 deletions restapi/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type apiClientOpt struct {
idAttribute string
createMethod string
readMethod string
readData string
updateMethod string
updateData string
destroyMethod string
Expand Down Expand Up @@ -63,6 +64,7 @@ type APIClient struct {
idAttribute string
createMethod string
readMethod string
readData string
updateMethod string
updateData string
destroyMethod string
Expand All @@ -76,7 +78,7 @@ type APIClient struct {
oauthConfig *clientcredentials.Config
}

//NewAPIClient makes a new api client for RESTful calls
// NewAPIClient makes a new api client for RESTful calls
func NewAPIClient(opt *apiClientOpt) (*APIClient, error) {
if opt.debug {
log.Printf("api_client.go: Constructing debug api_client\n")
Expand Down Expand Up @@ -162,6 +164,7 @@ func NewAPIClient(opt *apiClientOpt) (*APIClient, error) {
idAttribute: opt.idAttribute,
createMethod: opt.createMethod,
readMethod: opt.readMethod,
readData: opt.readData,
updateMethod: opt.updateMethod,
updateData: opt.updateData,
destroyMethod: opt.destroyMethod,
Expand Down Expand Up @@ -210,8 +213,11 @@ func (client *APIClient) toString() string {
return buffer.String()
}

/* Helper function that handles sending/receiving and handling
of HTTP data in and out. */
/*
Helper function that handles sending/receiving and handling
of HTTP data in and out.
*/
func (client *APIClient) sendRequest(method string, path string, data string) (string, error) {
fullURI := client.uri + path
var req *http.Request
Expand Down
29 changes: 28 additions & 1 deletion restapi/api_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type apiObjectOpts struct {
putPath string
createMethod string
readMethod string
readData string
updateMethod string
updateData string
destroyMethod string
Expand Down Expand Up @@ -52,6 +53,7 @@ type APIObject struct {

/* Set internally */
data map[string]interface{} /* Data as managed by the user */
readData map[string]interface{} /* Read data as managed by the user */
updateData map[string]interface{} /* Update data as managed by the user */
destroyData map[string]interface{} /* Destroy data as managed by the user */
apiData map[string]interface{} /* Data as available from the API */
Expand Down Expand Up @@ -79,6 +81,9 @@ func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) {
if opts.readMethod == "" {
opts.readMethod = iClient.readMethod
}
if opts.readData == "" {
opts.readData = iClient.readData
}
if opts.updateMethod == "" {
opts.updateMethod = iClient.updateMethod
}
Expand Down Expand Up @@ -124,6 +129,7 @@ func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) {
id: opts.id,
idAttribute: opts.idAttribute,
data: make(map[string]interface{}),
readData: make(map[string]interface{}),
updateData: make(map[string]interface{}),
destroyData: make(map[string]interface{}),
apiData: make(map[string]interface{}),
Expand Down Expand Up @@ -157,6 +163,17 @@ func NewAPIObject(iClient *APIClient, opts *apiObjectOpts) (*APIObject, error) {
}
}

if opts.readData != "" {
if opts.debug {
log.Printf("api_object.go: Parsing read data: '%s'", opts.readData)
}

err := json.Unmarshal([]byte(opts.readData), &obj.readData)
if err != nil {
return &obj, fmt.Errorf("api_object.go: error parsing read data provided: %v", err.Error())
}
}

if opts.updateData != "" {
if opts.debug {
log.Printf("api_object.go: Parsing update data: '%s'", opts.updateData)
Expand Down Expand Up @@ -202,6 +219,7 @@ func (obj *APIObject) toString() string {
buffer.WriteString(fmt.Sprintf("debug: %t\n", obj.debug))
buffer.WriteString(fmt.Sprintf("read_search: %s\n", spew.Sdump(obj.readSearch)))
buffer.WriteString(fmt.Sprintf("data: %s\n", spew.Sdump(obj.data)))
buffer.WriteString(fmt.Sprintf("read_data: %s\n", spew.Sdump(obj.readData)))
buffer.WriteString(fmt.Sprintf("update_data: %s\n", spew.Sdump(obj.updateData)))
buffer.WriteString(fmt.Sprintf("destroy_data: %s\n", spew.Sdump(obj.destroyData)))
buffer.WriteString(fmt.Sprintf("api_data: %s\n", spew.Sdump(obj.apiData)))
Expand Down Expand Up @@ -321,7 +339,16 @@ func (obj *APIObject) readObject() error {
getPath = fmt.Sprintf("%s?%s", obj.getPath, obj.queryString)
}

resultString, err := obj.apiClient.sendRequest(obj.readMethod, strings.Replace(getPath, "{id}", obj.id, -1), "")
b := []byte{}
readData, _ := json.Marshal(obj.readData)
if string(readData) != "" {
if obj.debug {
log.Printf("api_object.go: Using read data '%s'", string(readData))
}
b = readData
}

resultString, err := obj.apiClient.sendRequest(obj.readMethod, strings.Replace(getPath, "{id}", obj.id, -1), string(b))
if err != nil {
if strings.Contains(err.Error(), "unexpected response code '404'") {
log.Printf("api_object.go: 404 error while refreshing state for '%s' at path '%s'. Removing from state.", obj.id, obj.getPath)
Expand Down
18 changes: 18 additions & 0 deletions restapi/api_object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,24 @@ func TestAPIObject(t *testing.T) {
}
})

t.Run("read_object_with_read_data", func(t *testing.T) {
if testDebug {
log.Printf("api_object_test.go: Testing read_object() with read_data")
}
for testCase := range testingObjects {
t.Run(testCase, func(t *testing.T) {
if testDebug {
log.Printf("api_object_test.go: Getting data for '%s' test case from server\n", testCase)
}
testingObjects[testCase].readData["path"] = "/" + testCase
err := testingObjects[testCase].readObject()
if err != nil {
t.Fatalf("api_object_test.go: Failed to read data for test case '%s': %s", testCase, err)
}
})
}
})

/* Verify our copy_keys is happy by seeing if Thing made it into the data hash */
t.Run("copy_keys", func(t *testing.T) {
if testDebug {
Expand Down
42 changes: 34 additions & 8 deletions restapi/resource_api_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,23 @@ func resourceRestAPI() *schema.Resource {
ForceNew: true,
Description: "Any changes to these values will result in recreating the resource instead of updating.",
},
"read_data": {
Type: schema.TypeString,
Optional: true,
Description: "Valid JSON object to pass during read requests.",
Sensitive: isDataSensitive,
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
v := val.(string)
if v != "" {
data := make(map[string]interface{})
err := json.Unmarshal([]byte(v), &data)
if err != nil {
errs = append(errs, fmt.Errorf("read_data attribute is invalid JSON: %v", err))
}
}
return warns, errs
},
},
"update_data": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -183,10 +200,13 @@ func resourceRestAPI() *schema.Resource {
}
}

/* Since there is nothing in the ResourceData structure other
than the "id" passed on the command line, we have to use an opinionated
view of the API paths to figure out how to read that object
from the API */
/*
Since there is nothing in the ResourceData structure other
than the "id" passed on the command line, we have to use an opinionated
view of the API paths to figure out how to read that object
from the API
*/
func resourceRestAPIImport(d *schema.ResourceData, meta interface{}) (imported []*schema.ResourceData, err error) {
input := d.Id()

Expand Down Expand Up @@ -339,10 +359,13 @@ func resourceRestAPIExists(d *schema.ResourceData, meta interface{}) (exists boo
return exists, err
}

/* Simple helper routine to build an api_object struct
for the various calls terraform will use. Unfortunately,
terraform cannot just reuse objects, so each CRUD operation
results in a new object created */
/*
Simple helper routine to build an api_object struct
for the various calls terraform will use. Unfortunately,
terraform cannot just reuse objects, so each CRUD operation
results in a new object created
*/
func makeAPIObject(d *schema.ResourceData, meta interface{}) (*APIObject, error) {
opts, err := buildAPIObjectOpts(d)
if err != nil {
Expand Down Expand Up @@ -398,6 +421,9 @@ func buildAPIObjectOpts(d *schema.ResourceData) (*apiObjectOpts, error) {
if v, ok := d.GetOk("read_method"); ok {
opts.readMethod = v.(string)
}
if v, ok := d.GetOk("read_data"); ok {
opts.readData = v.(string)
}
if v, ok := d.GetOk("update_method"); ok {
opts.updateMethod = v.(string)
}
Expand Down

0 comments on commit 8c887f3

Please sign in to comment.