Skip to content

Commit ae8c5df

Browse files
Null id validation (#566)
1 parent 47f0b2f commit ae8c5df

File tree

8 files changed

+369
-3
lines changed

8 files changed

+369
-3
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: Behind the scenes
2+
body: Null id validation
3+
time: 2025-11-26T12:59:22.922354+02:00

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.0
55
toolchain go1.24.1
66

77
require (
8+
github.com/go-playground/validator/v10 v10.28.0
89
github.com/hashicorp/terraform-plugin-docs v0.19.2
910
github.com/hashicorp/terraform-plugin-framework v1.8.0
1011
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
@@ -33,6 +34,9 @@ require (
3334
github.com/cloudflare/circl v1.3.7 // indirect
3435
github.com/davecgh/go-spew v1.1.1 // indirect
3536
github.com/fatih/color v1.16.0 // indirect
37+
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
38+
github.com/go-playground/locales v0.14.1 // indirect
39+
github.com/go-playground/universal-translator v0.18.1 // indirect
3640
github.com/golang/protobuf v1.5.4 // indirect
3741
github.com/google/go-cmp v0.6.0 // indirect
3842
github.com/google/uuid v1.6.0 // indirect
@@ -57,6 +61,7 @@ require (
5761
github.com/hashicorp/yamux v0.1.1 // indirect
5862
github.com/huandu/xstrings v1.3.3 // indirect
5963
github.com/imdario/mergo v0.3.15 // indirect
64+
github.com/leodido/go-urn v1.4.0 // indirect
6065
github.com/mattn/go-colorable v0.1.13 // indirect
6166
github.com/mattn/go-isatty v0.0.20 // indirect
6267
github.com/mattn/go-runewidth v0.0.9 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,22 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
4141
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
4242
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
4343
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
44+
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
45+
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
4446
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
4547
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
4648
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
4749
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
4850
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
4951
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
52+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
53+
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
54+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
55+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
56+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
57+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
58+
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
59+
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
5060
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
5161
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
5262
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@@ -137,6 +147,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
137147
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
138148
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
139149
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
150+
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
151+
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
140152
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
141153
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
142154
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=

pkg/dbt_cloud/model_notifications.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type ModelNotificationsResponse struct {
1414
}
1515

1616
type ModelNotifications struct {
17-
ID *int `json:"id,omitempty"`
17+
ID *int `json:"id,omitempty" validate:"required,ne=0"`
1818
CreatedAt string `json:"created_at,omitempty"`
1919
UpdatedAt string `json:"updated_at,omitempty"`
2020
AccountID int `json:"account_id,omitempty"`
@@ -52,6 +52,11 @@ func (c *Client) GetModelNotifications(environmentID string) (*ModelNotification
5252
return nil, err
5353
}
5454

55+
// Validate the response
56+
if err := ValidateResponse(&modelNotificationsResponse.Data, "ModelNotifications"); err != nil {
57+
return nil, err
58+
}
59+
5560
return &modelNotificationsResponse.Data, nil
5661
}
5762

@@ -129,5 +134,10 @@ func (c *Client) UpdateModelNotifications(
129134
return nil, err
130135
}
131136

137+
// Validate the response
138+
if err := ValidateResponse(&modelNotificationsResponse.Data, "ModelNotifications"); err != nil {
139+
return nil, err
140+
}
141+
132142
return &modelNotificationsResponse.Data, nil
133143
}

pkg/dbt_cloud/notification.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type NotificationResponse struct {
1414
}
1515

1616
type Notification struct {
17-
Id *int `json:"id,omitempty"`
17+
Id *int `json:"id,omitempty" validate:"required,ne=0"`
1818
AccountId int `json:"account_id"`
1919
UserId int `json:"user_id"`
2020
OnCancel []int `json:"on_cancel"`
@@ -54,6 +54,11 @@ func (c *Client) GetNotification(notificationID string) (*Notification, error) {
5454
return nil, err
5555
}
5656

57+
// Validate the response
58+
if err := ValidateResponse(&notificationResponse.Data, "Notification"); err != nil {
59+
return nil, err
60+
}
61+
5762
return &notificationResponse.Data, nil
5863
}
5964

@@ -108,6 +113,11 @@ func (c *Client) CreateNotification(
108113
return nil, err
109114
}
110115

116+
// Validate the response
117+
if err := ValidateResponse(&notificationResponse.Data, "Notification"); err != nil {
118+
return nil, err
119+
}
120+
111121
return &notificationResponse.Data, nil
112122
}
113123

@@ -145,5 +155,13 @@ func (c *Client) UpdateNotification(
145155
return nil, err
146156
}
147157

158+
// Skip validation for delete operations (STATE_DELETED) as the API may return nil ID
159+
// and we don't use the response data for deleted resources
160+
if notification.State != STATE_DELETED {
161+
if err := ValidateResponse(&notificationResponse.Data, "Notification"); err != nil {
162+
return nil, err
163+
}
164+
}
165+
148166
return &notificationResponse.Data, nil
149167
}

pkg/dbt_cloud/project.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
)
1010

1111
type Project struct {
12-
ID *int `json:"id,omitempty"`
12+
ID *int `json:"id,omitempty" validate:"required,ne=0"`
1313
Name string `json:"name"`
1414
Description string `json:"description"`
1515
DbtProjectSubdirectory *string `json:"dbt_project_subdirectory,omitempty"`
@@ -149,6 +149,11 @@ func (c *Client) GetProject(projectID string) (*Project, error) {
149149
return nil, err
150150
}
151151

152+
// Validate the response
153+
if err := ValidateResponse(&projectResponse.Data, "Project"); err != nil {
154+
return nil, err
155+
}
156+
152157
return &projectResponse.Data, nil
153158
}
154159

@@ -199,6 +204,11 @@ func (c *Client) CreateProject(
199204
return nil, err
200205
}
201206

207+
// Validate the response
208+
if err := ValidateResponse(&projectResponse.Data, "Project"); err != nil {
209+
return nil, err
210+
}
211+
202212
return &projectResponse.Data, nil
203213
}
204214

@@ -240,6 +250,14 @@ func (c *Client) UpdateProject(projectID string, project Project) (*Project, err
240250
return nil, err
241251
}
242252

253+
// Skip validation for delete operations (STATE_DELETED) as the API may return nil ID
254+
// and we don't use the response data for deleted resources
255+
if project.State != STATE_DELETED {
256+
if err := ValidateResponse(&projectResponse.Data, "Project"); err != nil {
257+
return nil, err
258+
}
259+
}
260+
243261
return &projectResponse.Data, nil
244262
}
245263

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package dbt_cloud
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/go-playground/validator/v10"
8+
)
9+
10+
// ResponseValidator is a singleton instance of the validator
11+
var validate *validator.Validate
12+
13+
func init() {
14+
validate = validator.New(validator.WithRequiredStructEnabled())
15+
}
16+
17+
// ValidationError wraps validator errors with the API response for debugging
18+
type ValidationError struct {
19+
Message string
20+
FieldErrors []FieldError
21+
Response interface{}
22+
}
23+
24+
type FieldError struct {
25+
Field string
26+
Tag string
27+
Value interface{}
28+
Message string
29+
}
30+
31+
func (e *ValidationError) Error() string {
32+
responseJSON, _ := json.MarshalIndent(e.Response, "", " ")
33+
34+
msg := fmt.Sprintf("API response validation failed:\n%s\n\nFields with errors:\n", e.Message)
35+
for _, fe := range e.FieldErrors {
36+
msg += fmt.Sprintf(" - %s: %s (value: %v)\n", fe.Field, fe.Message, fe.Value)
37+
}
38+
msg += fmt.Sprintf("\nFull API response:\n%s", string(responseJSON))
39+
40+
return msg
41+
}
42+
43+
// ValidateResponse validates a struct using validator tags
44+
func ValidateResponse(response interface{}, resourceType string) error {
45+
err := validate.Struct(response)
46+
if err == nil {
47+
return nil
48+
}
49+
50+
// Convert validation errors to our custom format
51+
validationErrs, ok := err.(validator.ValidationErrors)
52+
if !ok {
53+
// Not a validation error, return as is
54+
return err
55+
}
56+
57+
fieldErrors := make([]FieldError, 0, len(validationErrs))
58+
for _, e := range validationErrs {
59+
fieldErrors = append(fieldErrors, FieldError{
60+
Field: e.Field(),
61+
Tag: e.Tag(),
62+
Value: e.Value(),
63+
Message: getErrorMessage(e),
64+
})
65+
}
66+
67+
return &ValidationError{
68+
Message: fmt.Sprintf("Validation failed for %s", resourceType),
69+
FieldErrors: fieldErrors,
70+
Response: response,
71+
}
72+
}
73+
74+
// getErrorMessage returns a human-readable error message for a validation error
75+
func getErrorMessage(e validator.FieldError) string {
76+
switch e.Tag() {
77+
case "required":
78+
return "field is required but was nil or empty"
79+
case "ne":
80+
return fmt.Sprintf("must not equal %s", e.Param())
81+
default:
82+
return fmt.Sprintf("failed validation tag '%s'", e.Tag())
83+
}
84+
}

0 commit comments

Comments
 (0)