diff --git a/client/errors.go b/client/errors.go index 3c953930..1153e2c4 100644 --- a/client/errors.go +++ b/client/errors.go @@ -18,24 +18,59 @@ type DetailedError struct { // Title is a human-readable summary that explains the type of the problem. Title string `json:"title,omitempty"` // Details is an array of structured objects that give details about the error. - Details []struct { - Code string `json:"code"` - Description string `json:"description"` - Field string `json:"field"` - } `json:"type_detail,omitempty"` + Details []TypeDetail `json:"type_detail,omitempty"` +} + +type TypeDetail struct { + Code string `json:"code"` + Description string `json:"description"` + Field string `json:"field"` +} + +func (td TypeDetail) String() string { + response := "" + if td.Code != "" { + response += td.Code + // If any of the fields that could come next are present, add a space separator + if td.Field != "" || td.Description != "" { + response += " " + } + } + + if td.Field != "" { + response += td.Field + // If any of the fields that could come next are present, add a dash separator + if td.Description != "" { + response += " - " + } + } + + if td.Description != "" { + response += td.Description + } + + return response +} + +func (e DetailedError) detailsToString() string { + var response string + + for index, details := range e.Details { + response += details.String() + + // If we haven't reached the end of the list of error details, add a newline separator between each error + if index < len(e.Details)-1 { + response += "\n" + } + } + + return response } // Error returns a pretty-printed representation of the error func (e DetailedError) Error() string { if len(e.Details) > 0 { - var response string - for i, d := range e.Details { - response += d.Code + " - " + d.Description - if i > len(e.Details)-1 { - response += ", " - } - } - return response + return e.detailsToString() } return e.Message diff --git a/client/errors_test.go b/client/errors_test.go index 4f754d79..57aef1cc 100644 --- a/client/errors_test.go +++ b/client/errors_test.go @@ -33,4 +33,151 @@ func TestClient_ParseDetailedError(t *testing.T) { assert.Equal(t, de.Title, "The requested resource cannot be found.") assert.Equal(t, de.Message, "Dataset not found") }) + + t.Run("Creating a dataset without a name should return a validation error", func(t *testing.T) { + createDatasetRequest := &Dataset{} + _, err := c.Datasets.Create(ctx, createDatasetRequest) + require.Error(t, err) + assert.ErrorAs(t, err, &de) + assert.Equal(t, http.StatusUnprocessableEntity, de.Status) + assert.Equal(t, "https://api.honeycomb.io/problems/validation-failed", de.Type) + assert.Equal(t, "The provided input is invalid.", de.Title) + assert.Equal(t, "The provided input is invalid.", de.Message) + assert.Equal(t, 1, len(de.Details)) + assert.Equal(t, "missing", de.Details[0].Code) + assert.Equal(t, "name", de.Details[0].Field) + assert.Equal(t, "cannot be blank", de.Details[0].Description) + assert.Equal(t, "missing name - cannot be blank", de.detailsToString()) + }) +} + +func TestErrors_DetailedError_detailsToString(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + input DetailedError + expectedOutput string + }{ + { + name: "multiple details get separated by newline", + input: DetailedError{ + Details: []TypeDetail{ + { + Code: "test code1", + Field: "test_field1", + Description: "test description1", + }, + { + Code: "test code2", + Field: "test_field2", + Description: "test description2", + }, + }, + }, + expectedOutput: "test code1 test_field1 - test description1\ntest code2 test_field2 - test description2", + }, + { + name: "empty details returns empty string", + input: DetailedError{ + Details: []TypeDetail{}, + }, + expectedOutput: "", + }, + { + name: "one item in details has no newlines", + input: DetailedError{ + Details: []TypeDetail{ + { + Code: "test code", + Field: "test_field", + Description: "test description", + }, + }, + }, + expectedOutput: "test code test_field - test description", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + actualOutput := testCase.input.detailsToString() + assert.Equal(t, testCase.expectedOutput, actualOutput) + }) + } +} + +func TestErrors_TypeDetail_String(t *testing.T) { + testCases := []struct { + name string + input TypeDetail + expectedOutput string + }{ + { + name: "happy path: Code, Field, and Description present", + input: TypeDetail{ + Code: "test code", + Field: "test_field", + Description: "test description", + }, + expectedOutput: "test code test_field - test description", + }, + { + name: "all fields blank returns empty string", + input: TypeDetail{}, + expectedOutput: "", + }, + { + name: "empty Code", + input: TypeDetail{ + Field: "test_field", + Description: "test description", + }, + expectedOutput: "test_field - test description", + }, + { + name: "empty Code and Field", + input: TypeDetail{ + Description: "test description", + }, + expectedOutput: "test description", + }, + { + name: "empty Code and Description", + input: TypeDetail{ + Field: "test_field", + }, + expectedOutput: "test_field", + }, + { + name: "empty Field", + input: TypeDetail{ + Code: "test code", + Description: "test description", + }, + expectedOutput: "test code test description", + }, + { + name: "empty Field and Description", + input: TypeDetail{ + Code: "test code", + }, + expectedOutput: "test code", + }, + { + name: "empty Description", + input: TypeDetail{ + Code: "test code", + Field: "test_field", + }, + expectedOutput: "test code test_field", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + actualOutput := testCase.input.String() + assert.Equal(t, testCase.expectedOutput, actualOutput) + }) + } }