Repository: cloudflare/cloudflare-go
Pull request #1251 (2023-03-28 23:11:37)

zone: make settings more flexible
For the life of this library, the settings have always been under the `/zones/:zone_id/settings` route which was an aggregate of all the settings. This was designed as a way to aggregate all of the settings in one place and had its place however, now we are breaking up the zone settings to their true paths and need to make the `Get` and `Update` zone settings compatible with this approach.

This will enable teams to use their true paths instead of the prefixed or aggregate paths with no friction from the library (or upstreams like Terraform).

github-actions[bot]: Oops! It looks like no changelog entry is attached to this PR. Please include a release note as described in https://github.com/cloudflare/cloudflare-go/blob/master/docs/changelog-process.md.

Example: 

~~~
```release-note:TYPE
Release note
```
~~~

If you do not require a release note to be included, please add the `workflow/skip-changelog-entry` label.

Patch:
diff --git a/.changelog/1251.txt b/.changelog/1251.txt
new file mode 100644
index 000000000..b750d6ebf
--- /dev/null
+++ b/.changelog/1251.txt
@@ -0,0 +1,11 @@
+```release-note:breaking-change
+zone: `ZoneSingleSetting` has been renamed to `GetZoneSetting` and updated method signature inline with our expected conventions
+```
+
+```release-note:breaking-change
+zone: `UpdateZoneSingleSetting` has been renamed to `UpdateZoneSetting` and updated method signature inline with our expected conventions
+```
+
+```release-note:enhancement
+zone: `GetZoneSetting` and `UpdateZoneSetting` now allow configuring the path for where a setting resides instead of assuming `settings`
+```
diff --git a/errors.go b/errors.go
index 723c18bae..85fdc3efa 100644
--- a/errors.go
+++ b/errors.go
@@ -31,6 +31,7 @@ const (
 
 	errInvalidResourceContainerAccess        = "requested resource container (%q) is not supported for this endpoint"
 	errRequiredAccountLevelResourceContainer = "this endpoint requires using an account level resource container and identifiers"
+	errRequiredZoneLevelResourceContainer    = "this endpoint requires using a zone level resource container and identifiers"
 )
 
 var (
@@ -43,6 +44,7 @@ var (
 	ErrMissingResourceIdentifier              = errors.New(errMissingResourceIdentifier)
 
 	ErrRequiredAccountLevelResourceContainer = errors.New(errRequiredAccountLevelResourceContainer)
+	ErrRequiredZoneLevelResourceContainer    = errors.New(errRequiredZoneLevelResourceContainer)
 )
 
 type ErrorType string
diff --git a/zone.go b/zone.go
index ba544c789..59489f50c 100644
--- a/zone.go
+++ b/zone.go
@@ -14,6 +14,11 @@ import (
 	"golang.org/x/net/idna"
 )
 
+var (
+	// ErrMissingSettingName is for when setting name is required but missing.
+	ErrMissingSettingName = errors.New("zone setting name required but missing")
+)
+
 // Owner describes the resource owner.
 type Owner struct {
 	ID        string `json:"id"`
@@ -300,6 +305,17 @@ type zoneSubscriptionRatePlanPayload struct {
 	} `json:"rate_plan"`
 }
 
+type GetZoneSettingParams struct {
+	Name       string `json:"-"`
+	PathPrefix string `json:"-"`
+}
+
+type UpdateZoneSettingParams struct {
+	Name       string      `json:"-"`
+	PathPrefix string      `json:"-"`
+	Value      interface{} `json:"value"`
+}
+
 // CreateZone creates a zone on an account.
 //
 // Setting jumpstart to true will attempt to automatically scan for existing
@@ -897,11 +913,25 @@ func normalizeZoneName(name string) string {
 	return name
 }
 
-// ZoneSingleSetting returns information about specified setting to the specified zone.
+// GetZoneSetting returns information about specified setting to the specified
+// zone.
 //
 // API reference: https://api.cloudflare.com/#zone-settings-get-all-zone-settings
-func (api *API) ZoneSingleSetting(ctx context.Context, zoneID, settingName string) (ZoneSetting, error) {
-	uri := fmt.Sprintf("/zones/%s/settings/%s", zoneID, settingName)
+func (api *API) GetZoneSetting(ctx context.Context, rc *ResourceContainer, params GetZoneSettingParams) (ZoneSetting, error) {
+	if rc.Level != ZoneRouteLevel {
+		return ZoneSetting{}, ErrRequiredZoneLevelResourceContainer
+	}
+
+	if rc.Identifier == "" {
+		return ZoneSetting{}, ErrMissingName
+	}
+
+	pathPrefix := "settings"
+	if params.PathPrefix != "" {
+		pathPrefix = params.PathPrefix
+	}
+
+	uri := fmt.Sprintf("/zones/%s/%s/%s", rc.Identifier, pathPrefix, params.Name)
 	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
 	if err != nil {
 		return ZoneSetting{}, err
@@ -914,23 +944,36 @@ func (api *API) ZoneSingleSetting(ctx context.Context, zoneID, settingName strin
 	return r.Result, nil
 }
 
-// UpdateZoneSingleSetting updates the specified setting for a given zone.
+// UpdateZoneSetting updates the specified setting for a given zone.
 //
 // API reference: https://api.cloudflare.com/#zone-settings-edit-zone-settings-info
-func (api *API) UpdateZoneSingleSetting(ctx context.Context, zoneID, settingName string, setting ZoneSetting) (*ZoneSettingSingleResponse, error) {
-	uri := fmt.Sprintf("/zones/%s/settings/%s", zoneID, settingName)
-	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, setting)
+func (api *API) UpdateZoneSetting(ctx context.Context, rc *ResourceContainer, params UpdateZoneSettingParams) (ZoneSetting, error) {
+	if rc.Level != ZoneRouteLevel {
+		return ZoneSetting{}, ErrRequiredZoneLevelResourceContainer
+	}
+
+	if rc.Identifier == "" {
+		return ZoneSetting{}, ErrMissingName
+	}
+
+	pathPrefix := "settings"
+	if params.PathPrefix != "" {
+		pathPrefix = params.PathPrefix
+	}
+
+	uri := fmt.Sprintf("/zones/%s/%s/%s", rc.Identifier, pathPrefix, params.Name)
+	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
 	if err != nil {
-		return nil, err
+		return ZoneSetting{}, err
 	}
 
 	response := &ZoneSettingSingleResponse{}
 	err = json.Unmarshal(res, &response)
 	if err != nil {
-		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
+		return ZoneSetting{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
 	}
 
-	return response, nil
+	return response.Result, nil
 }
 
 // ZoneExport returns the text BIND config for the given zone


Test patch:
diff --git a/zone_test.go b/zone_test.go
index 93a2bb6fd..6dd5bc4fc 100644
--- a/zone_test.go
+++ b/zone_test.go
@@ -1510,3 +1510,103 @@ func TestUpdateZoneSSLSettings(t *testing.T) {
 		assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z")
 	}
 }
+
+func TestGetZoneSetting(t *testing.T) {
+	setup()
+	defer teardown()
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		_, _ = fmt.Fprintf(w, `{
+			"result": {
+				"id": "ssl",
+				"value": "off",
+				"editable": true,
+				"modified_on": "2014-01-01T05:20:00.12345Z"
+			}
+		}`)
+	}
+	mux.HandleFunc("/zones/foo/settings/ssl", handler)
+	s, err := client.GetZoneSetting(context.Background(), ZoneIdentifier("foo"), GetZoneSettingParams{Name: "ssl"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, s.ID, "ssl")
+		assert.Equal(t, s.Value, "off")
+		assert.Equal(t, s.Editable, true)
+		assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z")
+	}
+}
+
+func TestGetZoneSettingWithCustomPathPrefix(t *testing.T) {
+	setup()
+	defer teardown()
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		_, _ = fmt.Fprintf(w, `{
+			"result": {
+				"id": "ssl",
+				"value": "off",
+				"editable": true,
+				"modified_on": "2014-01-01T05:20:00.12345Z"
+			}
+		}`)
+	}
+	mux.HandleFunc("/zones/foo/my_custom_path/ssl", handler)
+	s, err := client.GetZoneSetting(context.Background(), ZoneIdentifier("foo"), GetZoneSettingParams{Name: "ssl", PathPrefix: "my_custom_path"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, s.ID, "ssl")
+		assert.Equal(t, s.Value, "off")
+		assert.Equal(t, s.Editable, true)
+		assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z")
+	}
+}
+
+func TestUpdateZoneSetting(t *testing.T) {
+	setup()
+	defer teardown()
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		_, _ = fmt.Fprintf(w, `{
+			"result": {
+				"id": "ssl",
+				"value": "off",
+				"editable": true,
+				"modified_on": "2014-01-01T05:20:00.12345Z"
+			}
+		}`)
+	}
+	mux.HandleFunc("/zones/foo/settings/ssl", handler)
+	s, err := client.UpdateZoneSetting(context.Background(), ZoneIdentifier("foo"), UpdateZoneSettingParams{Name: "ssl", Value: "off"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, s.ID, "ssl")
+		assert.Equal(t, s.Value, "off")
+		assert.Equal(t, s.Editable, true)
+		assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z")
+	}
+}
+
+func TestUpdateZoneSettingWithCustomPathPrefix(t *testing.T) {
+	setup()
+	defer teardown()
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		_, _ = fmt.Fprintf(w, `{
+			"result": {
+				"id": "ssl",
+				"value": "off",
+				"editable": true,
+				"modified_on": "2014-01-01T05:20:00.12345Z"
+			}
+		}`)
+	}
+	mux.HandleFunc("/zones/foo/my_custom_path/ssl", handler)
+	s, err := client.UpdateZoneSetting(context.Background(), ZoneIdentifier("foo"), UpdateZoneSettingParams{Name: "ssl", PathPrefix: "my_custom_path", Value: "off"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, s.ID, "ssl")
+		assert.Equal(t, s.Value, "off")
+		assert.Equal(t, s.Editable, true)
+		assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z")
+	}
+}