Merge pull request #344 from smashwilson/cdn-updates

Improve CDN service patch updates.
diff --git a/acceptance/rackspace/cdn/v1/service_test.go b/acceptance/rackspace/cdn/v1/service_test.go
index af73dcf..ddec667 100644
--- a/acceptance/rackspace/cdn/v1/service_test.go
+++ b/acceptance/rackspace/cdn/v1/service_test.go
@@ -60,17 +60,13 @@
 }
 
 func testServiceUpdate(t *testing.T, client *gophercloud.ServiceClient, id string) {
-	updateOpts := os.UpdateOpts{
-		os.UpdateOpt{
-			Op:   os.Add,
-			Path: "/domains/-",
-			Value: map[string]interface{}{
-				"domain":   "newDomain.com",
-				"protocol": "http",
-			},
+	opts := []os.Patch{
+		os.Append{
+			Value: os.Domain{Domain: "newDomain.com", Protocol: "http"},
 		},
 	}
-	loc, err := services.Update(client, id, updateOpts).Extract()
+
+	loc, err := services.Update(client, id, opts).Extract()
 	th.AssertNoErr(t, err)
 	t.Logf("Successfully updated service at location: %s", loc)
 }
diff --git a/openstack/cdn/v1/services/fixtures.go b/openstack/cdn/v1/services/fixtures.go
index ec1fd52..d9bc9f2 100644
--- a/openstack/cdn/v1/services/fixtures.go
+++ b/openstack/cdn/v1/services/fixtures.go
@@ -170,10 +170,10 @@
 // HandleCreateCDNServiceSuccessfully creates an HTTP handler at `/services` on the test handler mux
 // that responds with a `Create` response.
 func HandleCreateCDNServiceSuccessfully(t *testing.T) {
-  th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
-    th.TestMethod(t, r, "POST")
-    th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-    th.TestJSONRequest(t, r, `
+	th.Mux.HandleFunc("/services", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestJSONRequest(t, r, `
       {
         "name": "mywebsite.com",
         "domains": [
@@ -212,21 +212,21 @@
         "flavor_id": "cdn"
       }
    `)
-   w.Header().Add("Location", "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
-   w.WriteHeader(http.StatusAccepted)
-  })
+		w.Header().Add("Location", "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+		w.WriteHeader(http.StatusAccepted)
+	})
 }
 
 // HandleGetCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
 // that responds with a `Get` response.
 func HandleGetCDNServiceSuccessfully(t *testing.T) {
-  th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
-    th.TestMethod(t, r, "GET")
-    th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+	th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
-    w.Header().Set("Content-Type", "application/json")
-    w.WriteHeader(http.StatusOK)
-    fmt.Fprintf(w, `
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, `
     {
         "id": "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
         "name": "mywebsite.com",
@@ -299,44 +299,74 @@
         ]
     }
     `)
-  })
+	})
 }
 
 // HandleUpdateCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
 // that responds with a `Update` response.
 func HandleUpdateCDNServiceSuccessfully(t *testing.T) {
-  th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
-    th.TestMethod(t, r, "PATCH")
-    th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-    th.TestJSONRequest(t, r, `
+	th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PATCH")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestJSONRequest(t, r, `
       [
-        {
-            "op": "replace",
-            "path": "/origins/0",
-            "value": {
-                "origin": "44.33.22.11",
-                "port": 80,
-                "ssl": false
-            }
-        },
-        {
-            "op": "add",
-            "path": "/domains/0",
-            "value": {"domain": "added.mocksite4.com"}
-        }
+				{
+					"op": "add",
+					"path": "/domains/-",
+					"value": {"domain": "appended.mocksite4.com"}
+				},
+				{
+					"op": "add",
+					"path": "/domains/4",
+					"value": {"domain": "inserted.mocksite4.com"}
+				},
+				{
+					"op": "add",
+					"path": "/domains",
+					"value": [
+						{"domain": "bulkadded1.mocksite4.com"},
+						{"domain": "bulkadded2.mocksite4.com"}
+					]
+				},
+				{
+					"op": "replace",
+					"path": "/origins/2",
+					"value": {"origin": "44.33.22.11", "port": 80, "ssl": false}
+				},
+				{
+					"op": "replace",
+					"path": "/origins",
+					"value": [
+						{"origin": "44.33.22.11", "port": 80, "ssl": false},
+						{"origin": "55.44.33.22", "port": 443, "ssl": true}
+					]
+				},
+				{
+					"op": "remove",
+					"path": "/caching/8"
+				},
+				{
+					"op": "remove",
+					"path": "/caching"
+				},
+				{
+					"op": "replace",
+					"path": "/name",
+					"value": "differentServiceName"
+				}
     ]
    `)
-   w.Header().Add("Location", "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
-   w.WriteHeader(http.StatusAccepted)
-  })
+		w.Header().Add("Location", "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0")
+		w.WriteHeader(http.StatusAccepted)
+	})
 }
 
 // HandleDeleteCDNServiceSuccessfully creates an HTTP handler at `/services/{id}` on the test handler mux
 // that responds with a `Delete` response.
 func HandleDeleteCDNServiceSuccessfully(t *testing.T) {
-  th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
-    th.TestMethod(t, r, "DELETE")
-    th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
-    w.WriteHeader(http.StatusAccepted)
-  })
+	th.Mux.HandleFunc("/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusAccepted)
+	})
 }
diff --git a/openstack/cdn/v1/services/requests.go b/openstack/cdn/v1/services/requests.go
index 646f63e..801fd3b 100644
--- a/openstack/cdn/v1/services/requests.go
+++ b/openstack/cdn/v1/services/requests.go
@@ -209,75 +209,142 @@
 	return res
 }
 
-// UpdateOptsBuilder is the interface options structs have to satisfy in order
-// to be used in the main Update operation in this package. Since many
-// extensions decorate or modify the common logic, it is useful for them to
-// satisfy a basic interface in order for them to be used.
-type UpdateOptsBuilder interface {
-	ToCDNServiceUpdateMap() ([]map[string]interface{}, error)
+// Path is a JSON pointer location that indicates which service parameter is being added, replaced,
+// or removed.
+type Path struct {
+	baseElement string
 }
 
-// Op represents an update operation.
-type Op string
+func (p Path) renderRoot() string {
+	return "/" + p.baseElement
+}
+
+func (p Path) renderDash() string {
+	return fmt.Sprintf("/%s/-", p.baseElement)
+}
+
+func (p Path) renderIndex(index int64) string {
+	return fmt.Sprintf("/%s/%d", p.baseElement, index)
+}
 
 var (
-	// Add is a constant used for performing a "add" operation when updating.
-	Add Op = "add"
-	// Remove is a constant used for performing a "remove" operation when updating.
-	Remove Op = "remove"
-	// Replace is a constant used for performing a "replace" operation when updating.
-	Replace Op = "replace"
+	// PathDomains indicates that an update operation is to be performed on a Domain.
+	PathDomains = Path{baseElement: "domains"}
+
+	// PathOrigins indicates that an update operation is to be performed on an Origin.
+	PathOrigins = Path{baseElement: "origins"}
+
+	// PathCaching indicates that an update operation is to be performed on a CacheRule.
+	PathCaching = Path{baseElement: "caching"}
 )
 
-// UpdateOpts represents the attributes used when updating an existing CDN service.
-type UpdateOpts []UpdateOpt
-
-// UpdateOpt represents a single update to an existing service. Multiple updates
-// to a service can be submitted at the same time. See UpdateOpts.
-type UpdateOpt struct {
-	// Specifies the update operation to perform.
-	Op Op `json:"op"`
-	// Specifies the JSON Pointer location within the service's JSON representation
-	// of the service parameter being added, replaced or removed.
-	Path string `json:"path"`
-	// Specifies the actual value to be added or replaced. It is not required for
-	// the remove operation.
-	Value map[string]interface{} `json:"value,omitempty"`
+type value interface {
+	toPatchValue() interface{}
+	appropriatePath() Path
+	renderRootOr(func(p Path) string) string
 }
 
-// ToCDNServiceUpdateMap casts an UpdateOpts struct to a map.
-func (opts UpdateOpts) ToCDNServiceUpdateMap() ([]map[string]interface{}, error) {
-	s := make([]map[string]interface{}, len(opts))
+// Patch represents a single update to an existing Service. Multiple updates to a service can be
+// submitted at the same time.
+type Patch interface {
+	ToCDNServiceUpdateMap() map[string]interface{}
+}
 
-	for i, opt := range opts {
-		if opt.Op != Add && opt.Op != Remove && opt.Op != Replace {
-			return nil, fmt.Errorf("Invalid Op: %v", opt.Op)
-		}
-		if opt.Op == "" {
-			return nil, no("Op")
-		}
-		if opt.Path == "" {
-			return nil, no("Path")
-		}
-		if opt.Op != Remove && opt.Value == nil {
-			return nil, no("Value")
-		}
-		s[i] = map[string]interface{}{
-			"op":    opt.Op,
-			"path":  opt.Path,
-			"value": opt.Value,
-		}
+// Insertion is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to
+// a Service at a fixed index. Use an Append instead to append the new value to the end of its
+// collection. Pass it to the Update function as part of the Patch slice.
+type Insertion struct {
+	Index int64
+	Value value
+}
+
+// ToCDNServiceUpdateMap converts an Insertion into a request body fragment suitable for the
+// Update call.
+func (i Insertion) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "add",
+		"path":  i.Value.renderRootOr(func(p Path) string { return p.renderIndex(i.Index) }),
+		"value": i.Value.toPatchValue(),
 	}
-
-	return s, nil
 }
 
-// Update accepts a UpdateOpts struct and updates an existing CDN service using
-// the values provided. idOrURL can be either the service's URL or its ID. For
-// example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
+// Append is a Patch that requests the addition of a value (Domain, Origin, or CacheRule) to a
+// Service at the end of its respective collection. Use an Insertion instead to insert the value
+// at a fixed index within the collection. Pass this to the Update function as part of its
+// Patch slice.
+type Append struct {
+	Value value
+}
+
+// ToCDNServiceUpdateMap converts an Append into a request body fragment suitable for the
+// Update call.
+func (a Append) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "add",
+		"path":  a.Value.renderRootOr(func(p Path) string { return p.renderDash() }),
+		"value": a.Value.toPatchValue(),
+	}
+}
+
+// Replacement is a Patch that alters a specific service parameter (Domain, Origin, or CacheRule)
+// in-place by index. Pass it to the Update function as part of the Patch slice.
+type Replacement struct {
+	Value value
+	Index int64
+}
+
+// ToCDNServiceUpdateMap converts a Replacement into a request body fragment suitable for the
+// Update call.
+func (r Replacement) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "replace",
+		"path":  r.Value.renderRootOr(func(p Path) string { return p.renderIndex(r.Index) }),
+		"value": r.Value.toPatchValue(),
+	}
+}
+
+// NameReplacement specifically updates the Service name. Pass it to the Update function as part
+// of the Patch slice.
+type NameReplacement struct {
+	NewName string
+}
+
+// ToCDNServiceUpdateMap converts a NameReplacement into a request body fragment suitable for the
+// Update call.
+func (r NameReplacement) ToCDNServiceUpdateMap() map[string]interface{} {
+	return map[string]interface{}{
+		"op":    "replace",
+		"path":  "/name",
+		"value": r.NewName,
+	}
+}
+
+// Removal is a Patch that requests the removal of a service parameter (Domain, Origin, or
+// CacheRule) by index. Pass it to the Update function as part of the Patch slice.
+type Removal struct {
+	Path  Path
+	Index int64
+	All   bool
+}
+
+// ToCDNServiceUpdateMap converts a Removal into a request body fragment suitable for the
+// Update call.
+func (r Removal) ToCDNServiceUpdateMap() map[string]interface{} {
+	result := map[string]interface{}{"op": "remove"}
+	if r.All {
+		result["path"] = r.Path.renderRoot()
+	} else {
+		result["path"] = r.Path.renderIndex(r.Index)
+	}
+	return result
+}
+
+// Update accepts a slice of Patch operations (Insertion, Append, Replacement or Removal) and
+// updates an existing CDN service using the values provided. idOrURL can be either the service's
+// URL or its ID. For example, both "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0" and
 // "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
 // are valid options for idOrURL.
-func Update(c *gophercloud.ServiceClient, idOrURL string, opts UpdateOptsBuilder) UpdateResult {
+func Update(c *gophercloud.ServiceClient, idOrURL string, patches []Patch) UpdateResult {
 	var url string
 	if strings.Contains(idOrURL, "/") {
 		url = idOrURL
@@ -285,11 +352,9 @@
 		url = updateURL(c, idOrURL)
 	}
 
-	var res UpdateResult
-	reqBody, err := opts.ToCDNServiceUpdateMap()
-	if err != nil {
-		res.Err = err
-		return res
+	reqBody := make([]map[string]interface{}, len(patches))
+	for i, patch := range patches {
+		reqBody[i] = patch.ToCDNServiceUpdateMap()
 	}
 
 	resp, err := perigee.Request("PATCH", url, perigee.Options{
@@ -297,9 +362,10 @@
 		ReqBody:     &reqBody,
 		OkCodes:     []int{202},
 	})
-	res.Header = resp.HttpResponse.Header
-	res.Err = err
-	return res
+	var result UpdateResult
+	result.Header = resp.HttpResponse.Header
+	result.Err = err
+	return result
 }
 
 // Delete accepts a service's ID or its URL and deletes the CDN service
diff --git a/openstack/cdn/v1/services/requests_test.go b/openstack/cdn/v1/services/requests_test.go
index ca7c269..2c11562 100644
--- a/openstack/cdn/v1/services/requests_test.go
+++ b/openstack/cdn/v1/services/requests_test.go
@@ -129,8 +129,8 @@
 					},
 				},
 				Restrictions: []Restriction{},
-				FlavorID: "europe",
-				Status:   "deployed",
+				FlavorID:     "europe",
+				Status:       "deployed",
 				Links: []gophercloud.Link{
 					gophercloud.Link{
 						Href: "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f1",
@@ -160,204 +160,199 @@
 }
 
 func TestCreate(t *testing.T) {
-  th.SetupHTTP()
-  defer th.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-  HandleCreateCDNServiceSuccessfully(t)
+	HandleCreateCDNServiceSuccessfully(t)
 
-  createOpts := CreateOpts{
-    Name: "mywebsite.com",
-    Domains: []Domain{
-      Domain{
-        Domain: "www.mywebsite.com",
-      },
-      Domain{
-        Domain: "blog.mywebsite.com",
-      },
-    },
-    Origins: []Origin{
-      Origin{
-        Origin: "mywebsite.com",
-        Port: 80,
-        SSL: false,
-      },
-    },
-    Restrictions: []Restriction{
-      Restriction{
-        Name: "website only",
-        Rules: []RestrictionRule{
-          RestrictionRule{
-            Name: "mywebsite.com",
-            Referrer: "www.mywebsite.com",
-          },
-        },
-      },
-    },
-    Caching: []CacheRule{
-      CacheRule{
-        Name: "default",
-        TTL: 3600,
-      },
-    },
-    FlavorID: "cdn",
-  }
+	createOpts := CreateOpts{
+		Name: "mywebsite.com",
+		Domains: []Domain{
+			Domain{
+				Domain: "www.mywebsite.com",
+			},
+			Domain{
+				Domain: "blog.mywebsite.com",
+			},
+		},
+		Origins: []Origin{
+			Origin{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Restrictions: []Restriction{
+			Restriction{
+				Name: "website only",
+				Rules: []RestrictionRule{
+					RestrictionRule{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		Caching: []CacheRule{
+			CacheRule{
+				Name: "default",
+				TTL:  3600,
+			},
+		},
+		FlavorID: "cdn",
+	}
 
-  expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
-  actual, err := Create(fake.ServiceClient(), createOpts).Extract()
-  th.AssertNoErr(t, err)
-  th.AssertEquals(t, expected, actual)
+	expected := "https://global.cdn.api.rackspacecloud.com/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+	actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, expected, actual)
 }
 
 func TestGet(t *testing.T) {
-  th.SetupHTTP()
-  defer th.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-  HandleGetCDNServiceSuccessfully(t)
+	HandleGetCDNServiceSuccessfully(t)
 
-  expected := &Service{
-    ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
-    Name: "mywebsite.com",
-    Domains: []Domain{
-      Domain{
-        Domain: "www.mywebsite.com",
-        Protocol: "http",
-      },
-    },
-    Origins: []Origin{
-      Origin{
-        Origin: "mywebsite.com",
-        Port:   80,
-        SSL:    false,
-      },
-    },
-    Caching: []CacheRule{
-      CacheRule{
-        Name: "default",
-        TTL:  3600,
-      },
-      CacheRule{
-        Name: "home",
-        TTL:  17200,
-        Rules: []TTLRule{
-          TTLRule{
-            Name:       "index",
-            RequestURL: "/index.htm",
-          },
-        },
-      },
-      CacheRule{
-        Name: "images",
-        TTL:  12800,
-        Rules: []TTLRule{
-          TTLRule{
-            Name:       "images",
-            RequestURL: "*.png",
-          },
-        },
-      },
-    },
-    Restrictions: []Restriction{
-      Restriction{
-        Name: "website only",
-        Rules: []RestrictionRule{
-          RestrictionRule{
-            Name:     "mywebsite.com",
-            Referrer: "www.mywebsite.com",
-          },
-        },
-      },
-    },
-    FlavorID: "cdn",
-    Status:   "deployed",
-    Errors:   []Error{},
-    Links: []gophercloud.Link{
-      gophercloud.Link{
-        Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
-        Rel:  "self",
-      },
-      gophercloud.Link{
-        Href: "blog.mywebsite.com.cdn1.raxcdn.com",
-        Rel:  "access_url",
-      },
-      gophercloud.Link{
-        Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
-        Rel:  "flavor",
-      },
-    },
-  }
+	expected := &Service{
+		ID:   "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+		Name: "mywebsite.com",
+		Domains: []Domain{
+			Domain{
+				Domain:   "www.mywebsite.com",
+				Protocol: "http",
+			},
+		},
+		Origins: []Origin{
+			Origin{
+				Origin: "mywebsite.com",
+				Port:   80,
+				SSL:    false,
+			},
+		},
+		Caching: []CacheRule{
+			CacheRule{
+				Name: "default",
+				TTL:  3600,
+			},
+			CacheRule{
+				Name: "home",
+				TTL:  17200,
+				Rules: []TTLRule{
+					TTLRule{
+						Name:       "index",
+						RequestURL: "/index.htm",
+					},
+				},
+			},
+			CacheRule{
+				Name: "images",
+				TTL:  12800,
+				Rules: []TTLRule{
+					TTLRule{
+						Name:       "images",
+						RequestURL: "*.png",
+					},
+				},
+			},
+		},
+		Restrictions: []Restriction{
+			Restriction{
+				Name: "website only",
+				Rules: []RestrictionRule{
+					RestrictionRule{
+						Name:     "mywebsite.com",
+						Referrer: "www.mywebsite.com",
+					},
+				},
+			},
+		},
+		FlavorID: "cdn",
+		Status:   "deployed",
+		Errors:   []Error{},
+		Links: []gophercloud.Link{
+			gophercloud.Link{
+				Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0",
+				Rel:  "self",
+			},
+			gophercloud.Link{
+				Href: "blog.mywebsite.com.cdn1.raxcdn.com",
+				Rel:  "access_url",
+			},
+			gophercloud.Link{
+				Href: "https://global.cdn.api.rackspacecloud.com/v1.0/110011/flavors/cdn",
+				Rel:  "flavor",
+			},
+		},
+	}
 
-
-  actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
-  th.AssertNoErr(t, err)
-  th.AssertDeepEquals(t, expected, actual)
+	actual, err := Get(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").Extract()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, expected, actual)
 }
 
 func TestSuccessfulUpdate(t *testing.T) {
-  th.SetupHTTP()
-  defer th.TeardownHTTP()
-
-  HandleUpdateCDNServiceSuccessfully(t)
-
-  expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
-  updateOpts := UpdateOpts{
-    UpdateOpt{
-      Op: Replace,
-      Path: "/origins/0",
-      Value: map[string]interface{}{
-        "origin": "44.33.22.11",
-        "port": 80,
-        "ssl": false,
-      },
-    },
-    UpdateOpt{
-      Op: Add,
-      Path: "/domains/0",
-      Value: map[string]interface{}{
-        "domain": "added.mocksite4.com",
-      },
-    },
-  }
-  actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", updateOpts).Extract()
-  th.AssertNoErr(t, err)
-  th.AssertEquals(t, expected, actual)
-}
-
-func TestUnsuccessfulUpdate(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 
 	HandleUpdateCDNServiceSuccessfully(t)
 
-	updateOpts := UpdateOpts{
-		UpdateOpt{
-			Op: "Foo",
-			Path: "/origins/0",
-			Value: map[string]interface{}{
-				"origin": "44.33.22.11",
-				"port": 80,
-				"ssl": false,
+	expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
+	ops := []Patch{
+		// Append a single Domain
+		Append{Value: Domain{Domain: "appended.mocksite4.com"}},
+		// Insert a single Domain
+		Insertion{
+			Index: 4,
+			Value: Domain{Domain: "inserted.mocksite4.com"},
+		},
+		// Bulk addition
+		Append{
+			Value: DomainList{
+				Domain{Domain: "bulkadded1.mocksite4.com"},
+				Domain{Domain: "bulkadded2.mocksite4.com"},
 			},
 		},
-		UpdateOpt{
-			Op: Add,
-			Path: "/domains/0",
-			Value: map[string]interface{}{
-				"domain": "added.mocksite4.com",
+		// Replace a single Origin
+		Replacement{
+			Index: 2,
+			Value: Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+		},
+		// Bulk replace Origins
+		Replacement{
+			Index: 0, // Ignored
+			Value: OriginList{
+				Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+				Origin{Origin: "55.44.33.22", Port: 443, SSL: true},
 			},
 		},
+		// Remove a single CacheRule
+		Removal{
+			Index: 8,
+			Path:  PathCaching,
+		},
+		// Bulk removal
+		Removal{
+			All:  true,
+			Path: PathCaching,
+		},
+		// Service name replacement
+		NameReplacement{
+			NewName: "differentServiceName",
+		},
 	}
-	_, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", updateOpts).Extract()
-	if err == nil {
-		t.Errorf("Expected error during TestUnsuccessfulUpdate but didn't get one.")
-	}
+
+	actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, expected, actual)
 }
 
 func TestDelete(t *testing.T) {
-  th.SetupHTTP()
-  defer th.TeardownHTTP()
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
 
-  HandleDeleteCDNServiceSuccessfully(t)
+	HandleDeleteCDNServiceSuccessfully(t)
 
-  err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
-  th.AssertNoErr(t, err)
+	err := Delete(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0").ExtractErr()
+	th.AssertNoErr(t, err)
 }
diff --git a/openstack/cdn/v1/services/results.go b/openstack/cdn/v1/services/results.go
index c64509b..33406c4 100644
--- a/openstack/cdn/v1/services/results.go
+++ b/openstack/cdn/v1/services/results.go
@@ -17,6 +17,42 @@
 	Protocol string `mapstructure:"protocol" json:"protocol,omitempty"`
 }
 
+func (d Domain) toPatchValue() interface{} {
+	r := make(map[string]interface{})
+	r["domain"] = d.Domain
+	if d.Protocol != "" {
+		r["protocol"] = d.Protocol
+	}
+	return r
+}
+
+func (d Domain) appropriatePath() Path {
+	return PathDomains
+}
+
+func (d Domain) renderRootOr(render func(p Path) string) string {
+	return render(d.appropriatePath())
+}
+
+// DomainList provides a useful way to perform bulk operations in a single Patch.
+type DomainList []Domain
+
+func (list DomainList) toPatchValue() interface{} {
+	r := make([]interface{}, len(list))
+	for i, domain := range list {
+		r[i] = domain.toPatchValue()
+	}
+	return r
+}
+
+func (list DomainList) appropriatePath() Path {
+	return PathDomains
+}
+
+func (list DomainList) renderRootOr(_ func(p Path) string) string {
+	return list.appropriatePath().renderRoot()
+}
+
 // OriginRule represents a rule that defines when an origin should be accessed.
 type OriginRule struct {
 	// Specifies the name of this rule.
@@ -39,6 +75,49 @@
 	Rules []OriginRule `mapstructure:"rules" json:"rules,omitempty"`
 }
 
+func (o Origin) toPatchValue() interface{} {
+	r := make(map[string]interface{})
+	r["origin"] = o.Origin
+	r["port"] = o.Port
+	r["ssl"] = o.SSL
+	if len(o.Rules) > 0 {
+		r["rules"] = make([]map[string]interface{}, len(o.Rules))
+		for index, rule := range o.Rules {
+			submap := r["rules"].([]map[string]interface{})[index]
+			submap["name"] = rule.Name
+			submap["request_url"] = rule.RequestURL
+		}
+	}
+	return r
+}
+
+func (o Origin) appropriatePath() Path {
+	return PathOrigins
+}
+
+func (o Origin) renderRootOr(render func(p Path) string) string {
+	return render(o.appropriatePath())
+}
+
+// OriginList provides a useful way to perform bulk operations in a single Patch.
+type OriginList []Origin
+
+func (list OriginList) toPatchValue() interface{} {
+	r := make([]interface{}, len(list))
+	for i, origin := range list {
+		r[i] = origin.toPatchValue()
+	}
+	return r
+}
+
+func (list OriginList) appropriatePath() Path {
+	return PathOrigins
+}
+
+func (list OriginList) renderRootOr(_ func(p Path) string) string {
+	return list.appropriatePath().renderRoot()
+}
+
 // TTLRule specifies a rule that determines if a TTL should be applied to an asset.
 type TTLRule struct {
 	// Specifies the name of this rule.
@@ -57,6 +136,46 @@
 	Rules []TTLRule `mapstructure:"rules" json:"rules,omitempty"`
 }
 
+func (c CacheRule) toPatchValue() interface{} {
+	r := make(map[string]interface{})
+	r["name"] = c.Name
+	r["ttl"] = c.TTL
+	r["rules"] = make([]map[string]interface{}, len(c.Rules))
+	for index, rule := range c.Rules {
+		submap := r["rules"].([]map[string]interface{})[index]
+		submap["name"] = rule.Name
+		submap["request_url"] = rule.RequestURL
+	}
+	return r
+}
+
+func (c CacheRule) appropriatePath() Path {
+	return PathCaching
+}
+
+func (c CacheRule) renderRootOr(render func(p Path) string) string {
+	return render(c.appropriatePath())
+}
+
+// CacheRuleList provides a useful way to perform bulk operations in a single Patch.
+type CacheRuleList []CacheRule
+
+func (list CacheRuleList) toPatchValue() interface{} {
+	r := make([]interface{}, len(list))
+	for i, rule := range list {
+		r[i] = rule.toPatchValue()
+	}
+	return r
+}
+
+func (list CacheRuleList) appropriatePath() Path {
+	return PathCaching
+}
+
+func (list CacheRuleList) renderRootOr(_ func(p Path) string) string {
+	return list.appropriatePath().renderRoot()
+}
+
 // RestrictionRule specifies a rule that determines if this restriction should be applied to an asset.
 type RestrictionRule struct {
 	// Specifies the name of this rule.
diff --git a/rackspace/cdn/v1/services/delegate.go b/rackspace/cdn/v1/services/delegate.go
index 10881eb..e3f1459 100644
--- a/rackspace/cdn/v1/services/delegate.go
+++ b/rackspace/cdn/v1/services/delegate.go
@@ -27,8 +27,8 @@
 
 // Update accepts a UpdateOpts struct and updates an existing CDN service using
 // the values provided.
-func Update(c *gophercloud.ServiceClient, id string, opts os.UpdateOptsBuilder) os.UpdateResult {
-	return os.Update(c, id, opts)
+func Update(c *gophercloud.ServiceClient, id string, patches []os.Patch) os.UpdateResult {
+	return os.Update(c, id, patches)
 }
 
 // Delete accepts a unique ID and deletes the CDN service associated with it.
diff --git a/rackspace/cdn/v1/services/delegate_test.go b/rackspace/cdn/v1/services/delegate_test.go
index f833cab..6c48365 100644
--- a/rackspace/cdn/v1/services/delegate_test.go
+++ b/rackspace/cdn/v1/services/delegate_test.go
@@ -299,59 +299,55 @@
 	os.HandleUpdateCDNServiceSuccessfully(t)
 
 	expected := "https://www.poppycdn.io/v1.0/services/96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0"
-	updateOpts := os.UpdateOpts{
-		os.UpdateOpt{
-			Op:   os.Replace,
-			Path: "/origins/0",
-			Value: map[string]interface{}{
-				"origin": "44.33.22.11",
-				"port":   80,
-				"ssl":    false,
+	ops := []os.Patch{
+		// Append a single Domain
+		os.Append{Value: os.Domain{Domain: "appended.mocksite4.com"}},
+		// Insert a single Domain
+		os.Insertion{
+			Index: 4,
+			Value: os.Domain{Domain: "inserted.mocksite4.com"},
+		},
+		// Bulk addition
+		os.Append{
+			Value: os.DomainList{
+				os.Domain{Domain: "bulkadded1.mocksite4.com"},
+				os.Domain{Domain: "bulkadded2.mocksite4.com"},
 			},
 		},
-		os.UpdateOpt{
-			Op:   os.Add,
-			Path: "/domains/0",
-			Value: map[string]interface{}{
-				"domain": "added.mocksite4.com",
+		// Replace a single Origin
+		os.Replacement{
+			Index: 2,
+			Value: os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+		},
+		// Bulk replace Origins
+		os.Replacement{
+			Index: 0, // Ignored
+			Value: os.OriginList{
+				os.Origin{Origin: "44.33.22.11", Port: 80, SSL: false},
+				os.Origin{Origin: "55.44.33.22", Port: 443, SSL: true},
 			},
 		},
+		// Remove a single CacheRule
+		os.Removal{
+			Index: 8,
+			Path:  os.PathCaching,
+		},
+		// Bulk removal
+		os.Removal{
+			All:  true,
+			Path: os.PathCaching,
+		},
+		// Service name replacement
+		os.NameReplacement{
+			NewName: "differentServiceName",
+		},
 	}
-	actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", updateOpts).Extract()
+
+	actual, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", ops).Extract()
 	th.AssertNoErr(t, err)
 	th.AssertEquals(t, expected, actual)
 }
 
-func TestUnsuccessfulUpdate(t *testing.T) {
-	th.SetupHTTP()
-	defer th.TeardownHTTP()
-
-	os.HandleUpdateCDNServiceSuccessfully(t)
-
-	updateOpts := os.UpdateOpts{
-		os.UpdateOpt{
-			Op: "Foo",
-			Path: "/origins/0",
-			Value: map[string]interface{}{
-				"origin": "44.33.22.11",
-				"port": 80,
-				"ssl": false,
-				},
-			},
-		os.UpdateOpt{
-			Op: os.Add,
-			Path: "/domains/0",
-			Value: map[string]interface{}{
-				"domain": "added.mocksite4.com",
-			},
-		},
-	}
-	_, err := Update(fake.ServiceClient(), "96737ae3-cfc1-4c72-be88-5d0e7cc9a3f0", updateOpts).Extract()
-	if err == nil {
-		t.Errorf("Expected error during TestUnsuccessfulUpdate but didn't get one.")
-	}
-}
-
 func TestDelete(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()