Repository: hetznercloud/hcloud-go
Pull request #22 (2018-01-09 10:01:05)

Implement GetByIDOrName

thcyron: Wondering if we can rename `GetByIDOrName` to just `Get`?

Patch:
diff --git a/hcloud/error.go b/hcloud/error.go
index d1429ef..0961bac 100644
--- a/hcloud/error.go
+++ b/hcloud/error.go
@@ -11,18 +11,31 @@ const (
 	ErrorCodeLimitReached           = "limit_reached" // Rate limit reached
 	ErrorCodeUnknownError           = "unknown_error" // Unknown error
 	ErrorCodeNotFound               = "not_found"     // Resource not found
+	ErrorCodeInvalidInput           = "invalid_input" // Validation error
 )
 
 // Error is an error returned from the API.
 type Error struct {
 	Code    ErrorCode
 	Message string
+	Details interface{}
 }
 
 func (e Error) Error() string {
 	return fmt.Sprintf("%s (%s)", e.Message, e.Code)
 }
 
+// ErrorDetailsInvalidInput contains the details of an 'invalid_input' error.
+type ErrorDetailsInvalidInput struct {
+	Fields []ErrorDetailsInvalidInputField
+}
+
+// ErrorDetailsInvalidInputField contains the validation errors reported on a field.
+type ErrorDetailsInvalidInputField struct {
+	Name     string
+	Messages []string
+}
+
 // IsError returns whether err is an API error with the given error code.
 func IsError(err error, code ErrorCode) bool {
 	apiErr, ok := err.(Error)
diff --git a/hcloud/image.go b/hcloud/image.go
index 79da6b1..7eddc87 100644
--- a/hcloud/image.go
+++ b/hcloud/image.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"net/url"
+	"strconv"
 	"time"
 
 	"github.com/hetznercloud/hcloud-go/hcloud/schema"
@@ -92,6 +93,14 @@ func (c *ImageClient) GetByName(ctx context.Context, name string) (*Image, *Resp
 	return ImageFromSchema(body.Images[0]), resp, nil
 }
 
+// Get retrieves an image by its ID if the input can be parsed as an integer, otherwise it retrieves an image by its name.
+func (c *ImageClient) Get(ctx context.Context, idOrName string) (*Image, *Response, error) {
+	if id, err := strconv.Atoi(idOrName); err == nil {
+		return c.GetByID(ctx, int(id))
+	}
+	return c.GetByName(ctx, idOrName)
+}
+
 // ImageListOpts specifies options for listing images.
 type ImageListOpts struct {
 	ListOpts
diff --git a/hcloud/schema.go b/hcloud/schema.go
index fe55867..39f339b 100644
--- a/hcloud/schema.go
+++ b/hcloud/schema.go
@@ -234,8 +234,23 @@ func PaginationFromSchema(s schema.MetaPagination) Pagination {
 
 // ErrorFromSchema converts a schema.Error to an Error.
 func ErrorFromSchema(s schema.Error) Error {
-	return Error{
+	e := Error{
 		Code:    ErrorCode(s.Code),
 		Message: s.Message,
 	}
+
+	switch d := s.Details.(type) {
+	case schema.ErrorDetailsInvalidInput:
+		details := ErrorDetailsInvalidInput{
+			Fields: []ErrorDetailsInvalidInputField{},
+		}
+		for _, field := range d.Fields {
+			details.Fields = append(details.Fields, ErrorDetailsInvalidInputField{
+				Name:     field.Name,
+				Messages: field.Messages,
+			})
+		}
+		e.Details = details
+	}
+	return e
 }
diff --git a/hcloud/schema/error.go b/hcloud/schema/error.go
index 6b02067..6bcb6ec 100644
--- a/hcloud/schema/error.go
+++ b/hcloud/schema/error.go
@@ -1,12 +1,43 @@
 package schema
 
+import "encoding/json"
+
 // Error represents the schema of an error response.
 type Error struct {
-	Code    string `json:"code"`
-	Message string `json:"message"`
+	Code       string          `json:"code"`
+	Message    string          `json:"message"`
+	DetailsRaw json.RawMessage `json:"details"`
+	Details    interface{}
+}
+
+// UnmarshalJSON overrides default json unmarshalling.
+func (e *Error) UnmarshalJSON(data []byte) (err error) {
+	type Alias Error
+	alias := (*Alias)(e)
+	if err = json.Unmarshal(data, alias); err != nil {
+		return
+	}
+	switch e.Code {
+	case "invalid_input":
+		details := ErrorDetailsInvalidInput{}
+		if err = json.Unmarshal(e.DetailsRaw, &details); err != nil {
+			return
+		}
+		alias.Details = details
+	}
+	return
 }
 
 // ErrorResponse defines the schema of a response containing an error.
 type ErrorResponse struct {
 	Error Error `json:"error"`
 }
+
+// ErrorDetailsInvalidInput defines the schema of the Details field
+// of an error with code 'invalid_input'.
+type ErrorDetailsInvalidInput struct {
+	Fields []struct {
+		Name     string   `json:"name"`
+		Messages []string `json:"messages"`
+	} `json:"fields"`
+}
diff --git a/hcloud/server.go b/hcloud/server.go
index 8684825..664d3ea 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -126,6 +126,14 @@ func (c *ServerClient) GetByName(ctx context.Context, name string) (*Server, *Re
 	return ServerFromSchema(body.Servers[0]), resp, nil
 }
 
+// Get retrieves a server by its ID if the input can be parsed as an integer, otherwise it retrieves a server by its name.
+func (c *ServerClient) Get(ctx context.Context, idOrName string) (*Server, *Response, error) {
+	if id, err := strconv.Atoi(idOrName); err == nil {
+		return c.GetByID(ctx, int(id))
+	}
+	return c.GetByName(ctx, idOrName)
+}
+
 // ServerListOpts specifies options for listing servers.
 type ServerListOpts struct {
 	ListOpts
diff --git a/hcloud/ssh_key.go b/hcloud/ssh_key.go
index 29726df..c360360 100644
--- a/hcloud/ssh_key.go
+++ b/hcloud/ssh_key.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"net/url"
+	"strconv"
 
 	"github.com/hetznercloud/hcloud-go/hcloud/schema"
 )
@@ -62,6 +63,14 @@ func (c *SSHKeyClient) GetByName(ctx context.Context, name string) (*SSHKey, *Re
 	return SSHKeyFromSchema(body.SSHKeys[0]), resp, nil
 }
 
+// Get retrieves a SSH key by its ID if the input can be parsed as an integer, otherwise it retrieves a SSH key by its name.
+func (c *SSHKeyClient) Get(ctx context.Context, idOrName string) (*SSHKey, *Response, error) {
+	if id, err := strconv.Atoi(idOrName); err == nil {
+		return c.GetByID(ctx, int(id))
+	}
+	return c.GetByName(ctx, idOrName)
+}
+
 // SSHKeyListOpts specifies options for listing SSH keys.
 type SSHKeyListOpts struct {
 	ListOpts


Test patch:
diff --git a/hcloud/image_test.go b/hcloud/image_test.go
index aafc6cd..9b040c0 100644
--- a/hcloud/image_test.go
+++ b/hcloud/image_test.go
@@ -33,6 +33,19 @@ func TestImageClient(t *testing.T) {
 		if image.ID != 1 {
 			t.Errorf("unexpected image ID: %v", image.ID)
 		}
+
+		t.Run("via Get", func(t *testing.T) {
+			image, _, err := env.Client.Image.Get(ctx, "1")
+			if err != nil {
+				t.Fatal(err)
+			}
+			if image == nil {
+				t.Fatal("no image")
+			}
+			if image.ID != 1 {
+				t.Errorf("unexpected image ID: %v", image.ID)
+			}
+		})
 	})
 
 	t.Run("GetByID (not found)", func(t *testing.T) {
@@ -87,6 +100,19 @@ func TestImageClient(t *testing.T) {
 		if image.ID != 1 {
 			t.Errorf("unexpected image ID: %v", image.ID)
 		}
+
+		t.Run("via Get", func(t *testing.T) {
+			image, _, err := env.Client.Image.Get(ctx, "my image")
+			if err != nil {
+				t.Fatal(err)
+			}
+			if image == nil {
+				t.Fatal("no image")
+			}
+			if image.ID != 1 {
+				t.Errorf("unexpected image ID: %v", image.ID)
+			}
+		})
 	})
 
 	t.Run("GetByName (not found)", func(t *testing.T) {
diff --git a/hcloud/schema/error_test.go b/hcloud/schema/error_test.go
new file mode 100644
index 0000000..3871df8
--- /dev/null
+++ b/hcloud/schema/error_test.go
@@ -0,0 +1,54 @@
+package schema
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+func TestError(t *testing.T) {
+	t.Run("UnmarshalJSON", func(t *testing.T) {
+		data := []byte(`{
+			"code": "invalid_input",
+			"message": "invalid input",
+			"details": {
+				"fields": [
+					{
+						"name": "broken_field",
+						"messages": ["is required"]
+					}
+				]
+			}
+		}`)
+
+		e := &Error{}
+		err := json.Unmarshal(data, e)
+		if err != nil {
+			t.Fatalf("unexpected error: %v", err)
+		}
+		if e.Code != "invalid_input" {
+			t.Errorf("unexpected Code: %v", e.Code)
+		}
+		if e.Message != "invalid input" {
+			t.Errorf("unexpected Message: %v", e.Message)
+		}
+		if e.Details == nil {
+			t.Fatalf("unexpected Details: %v", e.Details)
+		}
+		d, ok := e.Details.(ErrorDetailsInvalidInput)
+		if !ok {
+			t.Fatalf("unexpected Details type (should be ErrorDetailsInvalidInput): %v", e.Details)
+		}
+		if len(d.Fields) != 1 {
+			t.Fatalf("unexpected Details.Fields length (should be 1): %v", d.Fields)
+		}
+		if d.Fields[0].Name != "broken_field" {
+			t.Errorf("unexpected Details.Fields[0].Name: %v", d.Fields[0].Name)
+		}
+		if len(d.Fields[0].Messages) != 1 {
+			t.Fatalf("unexpected Details.Fields[0].Messages length (should be 1): %v", d.Fields[0].Messages)
+		}
+		if d.Fields[0].Messages[0] != "is required" {
+			t.Errorf("unexpected Details.Fields[0].Messages[0]: %v", d.Fields[0].Messages[0])
+		}
+	})
+}
diff --git a/hcloud/schema_test.go b/hcloud/schema_test.go
index a96cdff..fcefa5f 100644
--- a/hcloud/schema_test.go
+++ b/hcloud/schema_test.go
@@ -548,23 +548,70 @@ func TestSSHKeyFromSchema(t *testing.T) {
 }
 
 func TestErrorFromSchema(t *testing.T) {
-	data := []byte(`{
-		"code": "service_error",
-		"message": "An error occured"
-	}`)
+	t.Run("service_error", func(t *testing.T) {
+		data := []byte(`{
+			"code": "service_error",
+			"message": "An error occured",
+			"details": {}
+		}`)
 
-	var s schema.Error
-	if err := json.Unmarshal(data, &s); err != nil {
-		t.Fatal(err)
-	}
-	err := ErrorFromSchema(s)
+		var s schema.Error
+		if err := json.Unmarshal(data, &s); err != nil {
+			t.Fatal(err)
+		}
+		err := ErrorFromSchema(s)
 
-	if err.Code != "service_error" {
-		t.Errorf("unexpected code: %v", err.Code)
-	}
-	if err.Message != "An error occured" {
-		t.Errorf("unexpected message: %v", err.Message)
-	}
+		if err.Code != "service_error" {
+			t.Errorf("unexpected code: %v", err.Code)
+		}
+		if err.Message != "An error occured" {
+			t.Errorf("unexpected message: %v", err.Message)
+		}
+	})
+
+	t.Run("invalid_input", func(t *testing.T) {
+		data := []byte(`{
+			"code": "invalid_input",
+			"message": "invalid input",
+			"details": {
+				"fields": [
+					{
+						"name": "broken_field",
+						"messages": ["is required"]
+					}
+				]
+			}
+		}`)
+
+		var s schema.Error
+		if err := json.Unmarshal(data, &s); err != nil {
+			t.Fatal(err)
+		}
+		err := ErrorFromSchema(s)
+
+		if err.Code != "invalid_input" {
+			t.Errorf("unexpected Code: %v", err.Code)
+		}
+		if err.Message != "invalid input" {
+			t.Errorf("unexpected Message: %v", err.Message)
+		}
+		if d, ok := err.Details.(ErrorDetailsInvalidInput); !ok {
+			t.Fatalf("unexpected Details type (should be ErrorDetailsInvalidInput): %v", err.Details)
+		} else {
+			if len(d.Fields) != 1 {
+				t.Fatalf("unexpected Details.Fields length (should be 1): %v", d.Fields)
+			}
+			if d.Fields[0].Name != "broken_field" {
+				t.Errorf("unexpected Details.Fields[0].Name: %v", d.Fields[0].Name)
+			}
+			if len(d.Fields[0].Messages) != 1 {
+				t.Fatalf("unexpected Details.Fields[0].Messages length (should be 1): %v", d.Fields[0].Messages)
+			}
+			if d.Fields[0].Messages[0] != "is required" {
+				t.Errorf("unexpected Details.Fields[0].Messages[0]: %v", d.Fields[0].Messages[0])
+			}
+		}
+	})
 }
 
 func TestPaginationFromSchema(t *testing.T) {
diff --git a/hcloud/server_test.go b/hcloud/server_test.go
index 78507eb..70667a0 100644
--- a/hcloud/server_test.go
+++ b/hcloud/server_test.go
@@ -20,8 +20,8 @@ func TestServerClientGetByID(t *testing.T) {
 			},
 		})
 	})
-
 	ctx := context.Background()
+
 	server, _, err := env.Client.Server.GetByID(ctx, 1)
 	if err != nil {
 		t.Fatal(err)
@@ -32,6 +32,19 @@ func TestServerClientGetByID(t *testing.T) {
 	if server.ID != 1 {
 		t.Errorf("unexpected server ID: %v", server.ID)
 	}
+
+	t.Run("called via Get", func(t *testing.T) {
+		server, _, err := env.Client.Server.Get(ctx, "1")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if server == nil {
+			t.Fatal("no server")
+		}
+		if server.ID != 1 {
+			t.Errorf("unexpected server ID: %v", server.ID)
+		}
+	})
 }
 
 func TestServerClientGetByIDNotFound(t *testing.T) {
@@ -75,8 +88,8 @@ func TestServerClientGetByName(t *testing.T) {
 			},
 		})
 	})
-
 	ctx := context.Background()
+
 	server, _, err := env.Client.Server.GetByName(ctx, "myserver")
 	if err != nil {
 		t.Fatal(err)
@@ -87,6 +100,19 @@ func TestServerClientGetByName(t *testing.T) {
 	if server.ID != 1 {
 		t.Errorf("unexpected server ID: %v", server.ID)
 	}
+
+	t.Run("via Get", func(t *testing.T) {
+		server, _, err := env.Client.Server.Get(ctx, "myserver")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if server == nil {
+			t.Fatal("no server")
+		}
+		if server.ID != 1 {
+			t.Errorf("unexpected server ID: %v", server.ID)
+		}
+	})
 }
 
 func TestServerClientGetByNameNotFound(t *testing.T) {
diff --git a/hcloud/ssh_key_test.go b/hcloud/ssh_key_test.go
index b464870..a4b73d8 100644
--- a/hcloud/ssh_key_test.go
+++ b/hcloud/ssh_key_test.go
@@ -36,6 +36,19 @@ func TestSSHKeyClientGetByID(t *testing.T) {
 	if sshKey.ID != 1 {
 		t.Errorf("unexpected SSH key ID: %v", sshKey.ID)
 	}
+
+	t.Run("via Get", func(t *testing.T) {
+		sshKey, _, err := env.Client.SSHKey.Get(ctx, "1")
+		if err != nil {
+			t.Fatalf("SSHKey.GetByID failed: %s", err)
+		}
+		if sshKey == nil {
+			t.Fatal("no SSH key")
+		}
+		if sshKey.ID != 1 {
+			t.Errorf("unexpected SSH key ID: %v", sshKey.ID)
+		}
+	})
 }
 
 func TestSSHKeyClientGetByIDNotFound(t *testing.T) {
@@ -91,6 +104,19 @@ func TestSSHKeyClientGetByName(t *testing.T) {
 	if sshKey.ID != 1 {
 		t.Errorf("unexpected SSH key ID: %v", sshKey.ID)
 	}
+
+	t.Run("via Get", func(t *testing.T) {
+		sshKey, _, err := env.Client.SSHKey.Get(ctx, "My Key")
+		if err != nil {
+			t.Fatalf("SSHKey.GetByID failed: %s", err)
+		}
+		if sshKey == nil {
+			t.Fatal("no SSH key")
+		}
+		if sshKey.ID != 1 {
+			t.Errorf("unexpected SSH key ID: %v", sshKey.ID)
+		}
+	})
 }
 
 func TestSSHKeyClientGetByNameNotFound(t *testing.T) {