Repository: hetznercloud/hcloud-go (go)

Issue:
Implement updating a Floating IP
https://docs.hetzner.cloud/#resources-floating-ips-put

Pull request description:
Implement updating a Floating IP
Closes #34.

Patch:
diff --git a/README.md b/README.md
index 21cca0e..553e305 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,39 @@
 [![Build status](https://travis-ci.org/hetznercloud/hcloud-go.svg?branch=master)](https://travis-ci.org/hetznercloud/hcloud-go)
 [![GoDoc](https://godoc.org/github.com/hetznercloud/hcloud-go/hcloud?status.svg)](https://godoc.org/github.com/hetznercloud/hcloud-go/hcloud)
 
-**This library is still in development. It may and will change without notice.**
+Package hcloud is a library for the Hetzner Cloud API.
 
-We do not accept pull requests at the moment.
+The library’s documentation is available at [GoDoc](https://godoc.org/github.com/hetznercloud/hcloud-go/hcloud),
+the public API documentation is available at [docs.hetzner.cloud](https://docs.hetzner.cloud/).
+
+## Example
+
+```go
+package main
+
+import (
+    "context"
+    "fmt"
+    "log"
+
+    "github.com/hetznercloud/hcloud-go/hcloud"
+)
+
+func main() {
+    client := hcloud.NewClient(hcloud.WithToken("token"))
+
+    server, _, err := client.Server.GetByID(context.Background(), 1)
+    if err != nil {
+        log.Fatalf("error retrieving server: %s\n", err)
+    }
+    if server != nil {
+        fmt.Printf("server 1 is called %q\n", server.Name)
+    } else {
+        fmt.Println("server 1 not found")
+    }
+}
+```
+
+## License
+
+MIT license
diff --git a/hcloud/floating_ip.go b/hcloud/floating_ip.go
index 2746b98..34209d4 100644
--- a/hcloud/floating_ip.go
+++ b/hcloud/floating_ip.go
@@ -187,6 +187,35 @@ func (c *FloatingIPClient) Delete(ctx context.Context, floatingIP *FloatingIP) (
 	return c.client.Do(req, nil)
 }
 
+// FloatingIPUpdateOpts specifies options for updating a Floating IP.
+type FloatingIPUpdateOpts struct {
+	Description string
+}
+
+// Update updates a Floating IP.
+func (c *FloatingIPClient) Update(ctx context.Context, floatingIP *FloatingIP, opts FloatingIPUpdateOpts) (*FloatingIP, *Response, error) {
+	reqBody := schema.FloatingIPUpdateRequest{
+		Description: opts.Description,
+	}
+	reqBodyData, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	path := fmt.Sprintf("/floating_ips/%d", floatingIP.ID)
+	req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
+	if err != nil {
+		return nil, nil, err
+	}
+
+	respBody := schema.FloatingIPUpdateResponse{}
+	resp, err := c.client.Do(req, &respBody)
+	if err != nil {
+		return nil, resp, err
+	}
+	return FloatingIPFromSchema(respBody.FloatingIP), resp, nil
+}
+
 // Assign assigns a Floating IP to a server.
 func (c *FloatingIPClient) Assign(ctx context.Context, floatingIP *FloatingIP, server *Server) (*Action, *Response, error) {
 	reqBody := schema.FloatingIPActionAssignRequest{
diff --git a/hcloud/schema/floating_ip.go b/hcloud/schema/floating_ip.go
index c539838..fcdf9db 100644
--- a/hcloud/schema/floating_ip.go
+++ b/hcloud/schema/floating_ip.go
@@ -25,6 +25,16 @@ type FloatingIPGetResponse struct {
 	FloatingIP FloatingIP `json:"floating_ip"`
 }
 
+// FloatingIPUpdateRequest defines the schema of the request to update a Floating IP.
+type FloatingIPUpdateRequest struct {
+	Description string `json:"description,omitempty"`
+}
+
+// FloatingIPUpdateResponse defines the schema of the response when updating a Floating IP.
+type FloatingIPUpdateResponse struct {
+	FloatingIP FloatingIP `json:"floating_ip"`
+}
+
 // FloatingIPListResponse defines the schema of the response when
 // listing Floating IPs.
 type FloatingIPListResponse struct {
diff --git a/hcloud/schema/ssh_key.go b/hcloud/schema/ssh_key.go
index 4d9dc61..334d3bb 100644
--- a/hcloud/schema/ssh_key.go
+++ b/hcloud/schema/ssh_key.go
@@ -32,3 +32,13 @@ type SSHKeyListResponse struct {
 type SSHKeyGetResponse struct {
 	SSHKey SSHKey `json:"ssh_key"`
 }
+
+// SSHKeyUpdateRequest defines the schema of the request to update a SSH key.
+type SSHKeyUpdateRequest struct {
+	Name string `json:"name,omitempty"`
+}
+
+// SSHKeyUpdateResponse defines the schema of the response when updating a SSH key.
+type SSHKeyUpdateResponse struct {
+	SSHKey SSHKey `json:"ssh_key"`
+}
diff --git a/hcloud/ssh_key.go b/hcloud/ssh_key.go
index c360360..4dafbb8 100644
--- a/hcloud/ssh_key.go
+++ b/hcloud/ssh_key.go
@@ -171,3 +171,32 @@ func (c *SSHKeyClient) Delete(ctx context.Context, sshKey *SSHKey) (*Response, e
 	}
 	return c.client.Do(req, nil)
 }
+
+// SSHKeyUpdateOpts specifies options for updating a SSH key.
+type SSHKeyUpdateOpts struct {
+	Name string
+}
+
+// Update updates a SSH key.
+func (c *SSHKeyClient) Update(ctx context.Context, sshKey *SSHKey, opts SSHKeyUpdateOpts) (*SSHKey, *Response, error) {
+	reqBody := schema.SSHKeyUpdateRequest{
+		Name: opts.Name,
+	}
+	reqBodyData, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	path := fmt.Sprintf("/ssh_keys/%d", sshKey.ID)
+	req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
+	if err != nil {
+		return nil, nil, err
+	}
+
+	respBody := schema.SSHKeyUpdateResponse{}
+	resp, err := c.client.Do(req, &respBody)
+	if err != nil {
+		return nil, resp, err
+	}
+	return SSHKeyFromSchema(respBody.SSHKey), resp, nil
+}


Test patch:
diff --git a/hcloud/floating_ip_test.go b/hcloud/floating_ip_test.go
index 5ccd8b6..2345c98 100644
--- a/hcloud/floating_ip_test.go
+++ b/hcloud/floating_ip_test.go
@@ -148,6 +148,81 @@ func TestFloatingIPClientDelete(t *testing.T) {
 	}
 }
 
+func TestFloatingIPClientUpdate(t *testing.T) {
+	var (
+		ctx        = context.Background()
+		floatingIP = &FloatingIP{ID: 1}
+	)
+
+	t.Run("update description", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) {
+			if r.Method != "PUT" {
+				t.Error("expected PUT")
+			}
+			var reqBody schema.FloatingIPUpdateRequest
+			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+				t.Fatal(err)
+			}
+			if reqBody.Description != "test" {
+				t.Errorf("unexpected description: %v", reqBody.Description)
+			}
+			json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{
+				FloatingIP: schema.FloatingIP{
+					ID: 1,
+				},
+			})
+		})
+
+		opts := FloatingIPUpdateOpts{
+			Description: "test",
+		}
+		updatedFloatingIP, _, err := env.Client.FloatingIP.Update(ctx, floatingIP, opts)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if updatedFloatingIP.ID != 1 {
+			t.Errorf("unexpected Floating IP ID: %v", updatedFloatingIP.ID)
+		}
+	})
+
+	t.Run("no updates", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/floating_ips/1", func(w http.ResponseWriter, r *http.Request) {
+			if r.Method != "PUT" {
+				t.Error("expected PUT")
+			}
+			var reqBody schema.FloatingIPUpdateRequest
+			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+				t.Fatal(err)
+			}
+			if reqBody.Description != "" {
+				t.Errorf("unexpected no description, but got: %v", reqBody.Description)
+			}
+			json.NewEncoder(w).Encode(schema.FloatingIPUpdateResponse{
+				FloatingIP: schema.FloatingIP{
+					ID: 1,
+				},
+			})
+		})
+
+		opts := FloatingIPUpdateOpts{}
+		updatedFloatingIP, _, err := env.Client.FloatingIP.Update(ctx, floatingIP, opts)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if updatedFloatingIP.ID != 1 {
+			t.Errorf("unexpected Floating IP ID: %v", updatedFloatingIP.ID)
+		}
+	})
+}
+
 func TestFloatingIPClientAssign(t *testing.T) {
 	env := newTestEnv()
 	defer env.Teardown()
diff --git a/hcloud/ssh_key_test.go b/hcloud/ssh_key_test.go
index a4b73d8..c1248ab 100644
--- a/hcloud/ssh_key_test.go
+++ b/hcloud/ssh_key_test.go
@@ -280,3 +280,78 @@ func TestSSHKeyClientCreate(t *testing.T) {
 		t.Errorf("unexpected SSH key ID: %v", sshKey.ID)
 	}
 }
+
+func TestSSHKeyClientUpdate(t *testing.T) {
+	var (
+		ctx    = context.Background()
+		sshKey = &SSHKey{ID: 1}
+	)
+
+	t.Run("update name", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) {
+			if r.Method != "PUT" {
+				t.Error("expected PUT")
+			}
+			var reqBody schema.SSHKeyUpdateRequest
+			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+				t.Fatal(err)
+			}
+			if reqBody.Name != "test" {
+				t.Errorf("unexpected name: %v", reqBody.Name)
+			}
+			json.NewEncoder(w).Encode(schema.SSHKeyUpdateResponse{
+				SSHKey: schema.SSHKey{
+					ID: 1,
+				},
+			})
+		})
+
+		opts := SSHKeyUpdateOpts{
+			Name: "test",
+		}
+		updatedSSHKey, _, err := env.Client.SSHKey.Update(ctx, sshKey, opts)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if updatedSSHKey.ID != 1 {
+			t.Errorf("unexpected SSH key ID: %v", updatedSSHKey.ID)
+		}
+	})
+
+	t.Run("no updates", func(t *testing.T) {
+		env := newTestEnv()
+		defer env.Teardown()
+
+		env.Mux.HandleFunc("/ssh_keys/1", func(w http.ResponseWriter, r *http.Request) {
+			if r.Method != "PUT" {
+				t.Error("expected PUT")
+			}
+			var reqBody schema.SSHKeyUpdateRequest
+			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+				t.Fatal(err)
+			}
+			if reqBody.Name != "" {
+				t.Errorf("unexpected no name, but got: %v", reqBody.Name)
+			}
+			json.NewEncoder(w).Encode(schema.SSHKeyUpdateResponse{
+				SSHKey: schema.SSHKey{
+					ID: 1,
+				},
+			})
+		})
+
+		opts := SSHKeyUpdateOpts{}
+		updatedSSHKey, _, err := env.Client.SSHKey.Update(ctx, sshKey, opts)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if updatedSSHKey.ID != 1 {
+			t.Errorf("unexpected SSH key ID: %v", updatedSSHKey.ID)
+		}
+	})
+}