Repository: hetznercloud/hcloud-go
Pull request #23 (2018-01-09 12:16:53)

Expose API error details


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/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"`
+}


Test patch:
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) {