Skip to content

Commit 630979f

Browse files
tuxityclaude
andauthored
fix: handle boolean JSON values for int fields on Splunk Cloud (#228)
Splunk Cloud returns JSON booleans (false/true) for fields such as action.email.include.search that Splunk Enterprise returns as string-encoded integers ("0"/"1"). The existing `json:",string"` struct tags cause encoding/json to panic when it encounters a bare boolean. Introduce a FlexInt type whose UnmarshalJSON accepts string-encoded integers, bare integers, and booleans (false→0, true→1). Replace the seven affected int fields in SavedSearchObject with FlexInt and drop the ,string option from their json tags. partial fix for #130 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4a21e67 commit 630979f

3 files changed

Lines changed: 173 additions & 14 deletions

File tree

client/models/flex_int_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package models
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
)
7+
8+
func TestFlexInt_UnmarshalJSON(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
input string
12+
want FlexInt
13+
wantErr bool
14+
}{
15+
{"string 1", `"1"`, 1, false},
16+
{"string 0", `"0"`, 0, false},
17+
{"bare int 1", `1`, 1, false},
18+
{"bare int 0", `0`, 0, false},
19+
{"bool true", `true`, 1, false},
20+
{"bool false", `false`, 0, false},
21+
{"invalid string", `"abc"`, 0, true},
22+
{"null", `null`, 0, true},
23+
}
24+
for _, tt := range tests {
25+
t.Run(tt.name, func(t *testing.T) {
26+
var f FlexInt
27+
err := json.Unmarshal([]byte(tt.input), &f)
28+
if (err != nil) != tt.wantErr {
29+
t.Fatalf("UnmarshalJSON(%s) error = %v, wantErr %v", tt.input, err, tt.wantErr)
30+
}
31+
if !tt.wantErr && f != tt.want {
32+
t.Errorf("UnmarshalJSON(%s) = %d, want %d", tt.input, f, tt.want)
33+
}
34+
})
35+
}
36+
}
37+
38+
// TestSavedSearchObject_CloudBooleanFields verifies that SavedSearchObject
39+
// correctly deserialises the boolean values Splunk Cloud returns for
40+
// integer fields that Splunk Enterprise returns as string-encoded integers.
41+
// Regression test for https://github.com/splunk/terraform-provider-splunk/issues/130
42+
func TestSavedSearchObject_CloudBooleanFields(t *testing.T) {
43+
// Splunk Cloud returns booleans for these fields instead of "0"/"1" strings.
44+
cloudJSON := `{
45+
"action.email.include.results_link": false,
46+
"action.email.include.search": false,
47+
"action.email.include.trigger": true,
48+
"action.email.include.trigger_time": true,
49+
"action.email.include.view_link": false,
50+
"action.email.sendcsv": false,
51+
"action.snow_event.param.severity": false
52+
}`
53+
54+
var obj SavedSearchObject
55+
if err := json.Unmarshal([]byte(cloudJSON), &obj); err != nil {
56+
t.Fatalf("Unmarshal cloud response: %v", err)
57+
}
58+
59+
if obj.ActionEmailIncludeResultsLink != 0 {
60+
t.Errorf("ActionEmailIncludeResultsLink: got %d, want 0", obj.ActionEmailIncludeResultsLink)
61+
}
62+
if obj.ActionEmailIncludeSearch != 0 {
63+
t.Errorf("ActionEmailIncludeSearch: got %d, want 0", obj.ActionEmailIncludeSearch)
64+
}
65+
if obj.ActionEmailIncludeTrigger != 1 {
66+
t.Errorf("ActionEmailIncludeTrigger: got %d, want 1", obj.ActionEmailIncludeTrigger)
67+
}
68+
if obj.ActionEmailIncludeTriggerTime != 1 {
69+
t.Errorf("ActionEmailIncludeTriggerTime: got %d, want 1", obj.ActionEmailIncludeTriggerTime)
70+
}
71+
if obj.ActionEmailIncludeViewLink != 0 {
72+
t.Errorf("ActionEmailIncludeViewLink: got %d, want 0", obj.ActionEmailIncludeViewLink)
73+
}
74+
if obj.ActionEmailSendCSV != 0 {
75+
t.Errorf("ActionEmailSendCSV: got %d, want 0", obj.ActionEmailSendCSV)
76+
}
77+
if obj.ActionSnowEventParamSeverity != 0 {
78+
t.Errorf("ActionSnowEventParamSeverity: got %d, want 0", obj.ActionSnowEventParamSeverity)
79+
}
80+
}
81+
82+
// TestSavedSearchObject_EnterpriseStringFields verifies that SavedSearchObject
83+
// correctly deserialises the string-encoded integers Splunk Enterprise returns.
84+
func TestSavedSearchObject_EnterpriseStringFields(t *testing.T) {
85+
enterpriseJSON := `{
86+
"action.email.include.results_link": "1",
87+
"action.email.include.search": "1",
88+
"action.email.include.trigger": "1",
89+
"action.email.include.trigger_time": "1",
90+
"action.email.include.view_link": "1",
91+
"action.email.sendcsv": "0",
92+
"action.snow_event.param.severity": "3"
93+
}`
94+
95+
var obj SavedSearchObject
96+
if err := json.Unmarshal([]byte(enterpriseJSON), &obj); err != nil {
97+
t.Fatalf("Unmarshal enterprise response: %v", err)
98+
}
99+
100+
if obj.ActionEmailIncludeResultsLink != 1 {
101+
t.Errorf("ActionEmailIncludeResultsLink: got %d, want 1", obj.ActionEmailIncludeResultsLink)
102+
}
103+
if obj.ActionEmailIncludeSearch != 1 {
104+
t.Errorf("ActionEmailIncludeSearch: got %d, want 1", obj.ActionEmailIncludeSearch)
105+
}
106+
if obj.ActionEmailIncludeTrigger != 1 {
107+
t.Errorf("ActionEmailIncludeTrigger: got %d, want 1", obj.ActionEmailIncludeTrigger)
108+
}
109+
if obj.ActionEmailIncludeTriggerTime != 1 {
110+
t.Errorf("ActionEmailIncludeTriggerTime: got %d, want 1", obj.ActionEmailIncludeTriggerTime)
111+
}
112+
if obj.ActionEmailIncludeViewLink != 1 {
113+
t.Errorf("ActionEmailIncludeViewLink: got %d, want 1", obj.ActionEmailIncludeViewLink)
114+
}
115+
if obj.ActionEmailSendCSV != 0 {
116+
t.Errorf("ActionEmailSendCSV: got %d, want 0", obj.ActionEmailSendCSV)
117+
}
118+
if obj.ActionSnowEventParamSeverity != 3 {
119+
t.Errorf("ActionSnowEventParamSeverity: got %d, want 3", obj.ActionSnowEventParamSeverity)
120+
}
121+
}

client/models/saved_searches.go

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,43 @@
11
package models
22

3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
8+
// FlexInt unmarshals JSON values that Splunk returns as either a
9+
// string-encoded integer ("0", "1"), a bare integer (0, 1), or a
10+
// boolean (false, true). Splunk Enterprise returns string-encoded
11+
// integers; Splunk Cloud returns booleans for the same fields.
12+
// See: https://github.com/splunk/terraform-provider-splunk/issues/130
13+
type FlexInt int
14+
15+
func (f *FlexInt) UnmarshalJSON(data []byte) error {
16+
var raw interface{}
17+
if err := json.Unmarshal(data, &raw); err != nil {
18+
return err
19+
}
20+
switch v := raw.(type) {
21+
case float64:
22+
*f = FlexInt(int(v))
23+
case string:
24+
var i int
25+
if _, err := fmt.Sscanf(v, "%d", &i); err != nil {
26+
return fmt.Errorf("cannot parse %q as int: %w", v, err)
27+
}
28+
*f = FlexInt(i)
29+
case bool:
30+
if v {
31+
*f = 1
32+
} else {
33+
*f = 0
34+
}
35+
default:
36+
return fmt.Errorf("cannot unmarshal %T into FlexInt", raw)
37+
}
38+
return nil
39+
}
40+
341
type SavedSearchesResponse struct {
442
Entry []SavedSearchesEntry `json:"entry"`
543
Messages []ErrorMessage `json:"messages"`
@@ -22,11 +60,11 @@ type SavedSearchObject struct {
2260
ActionEmailFormat string `json:"action.email.format,omitempty" url:"action.email.format,omitempty"`
2361
ActionEmailFrom string `json:"action.email.from,omitempty" url:"action.email.from,omitempty"`
2462
ActionEmailHostname string `json:"action.email.hostname,omitempty" url:"action.email.hostname,omitempty"`
25-
ActionEmailIncludeResultsLink int `json:"action.email.include.results_link,string,omitempty" url:"action.email.include.results_link"`
26-
ActionEmailIncludeSearch int `json:"action.email.include.search,string,omitempty" url:"action.email.include.search"`
27-
ActionEmailIncludeTrigger int `json:"action.email.include.trigger,string,omitempty" url:"action.email.include.trigger"`
28-
ActionEmailIncludeTriggerTime int `json:"action.email.include.trigger_time,string,omitempty" url:"action.email.include.trigger_time"`
29-
ActionEmailIncludeViewLink int `json:"action.email.include.view_link,string,omitempty" url:"action.email.include.view_link"`
63+
ActionEmailIncludeResultsLink FlexInt `json:"action.email.include.results_link,omitempty" url:"action.email.include.results_link"`
64+
ActionEmailIncludeSearch FlexInt `json:"action.email.include.search,omitempty" url:"action.email.include.search"`
65+
ActionEmailIncludeTrigger FlexInt `json:"action.email.include.trigger,omitempty" url:"action.email.include.trigger"`
66+
ActionEmailIncludeTriggerTime FlexInt `json:"action.email.include.trigger_time,omitempty" url:"action.email.include.trigger_time"`
67+
ActionEmailIncludeViewLink FlexInt `json:"action.email.include.view_link,omitempty" url:"action.email.include.view_link"`
3068
ActionEmailInline bool `json:"action.email.inline" url:"action.email.inline"`
3169
ActionEmailMailserver string `json:"action.email.mailserver,omitempty" url:"action.email.mailserver,omitempty"`
3270
ActionEmailMaxResults int `json:"action.email.maxresults,omitempty" url:"action.email.maxresults,omitempty"`
@@ -41,7 +79,7 @@ type SavedSearchObject struct {
4179
ActionEmailReportPaperSize string `json:"action.email.reportPaperSize,omitempty" url:"action.email.reportPaperSize,omitempty"`
4280
ActionEmailReportServerEnabled bool `json:"action.email.reportServerEnabled" url:"action.email.reportServerEnabled"`
4381
ActionEmailReportServerURL string `json:"action.email.reportServerURL,omitempty" url:"action.email.reportServerURL,omitempty"`
44-
ActionEmailSendCSV int `json:"action.email.sendcsv,string,omitempty" url:"action.email.sendcsv,omitempty"`
82+
ActionEmailSendCSV FlexInt `json:"action.email.sendcsv,omitempty" url:"action.email.sendcsv,omitempty"`
4583
ActionEmailSendPDF bool `json:"action.email.sendpdf" url:"action.email.sendpdf"`
4684
ActionEmailSendResults bool `json:"action.email.sendresults" url:"action.email.sendresults"`
4785
ActionEmailSubject string `json:"action.email.subject,omitempty" url:"action.email.subject,omitempty"`
@@ -83,7 +121,7 @@ type SavedSearchObject struct {
83121
ActionSnowEventParamNode string `json:"action.snow_event.param.node,omitempty" url:"action.snow_event.param.node,omitempty"`
84122
ActionSnowEventParamType string `json:"action.snow_event.param.type,omitempty" url:"action.snow_event.param.type,omitempty"`
85123
ActionSnowEventParamResource string `json:"action.snow_event.param.resource,omitempty" url:"action.snow_event.param.resource,omitempty"`
86-
ActionSnowEventParamSeverity int `json:"action.snow_event.param.severity,string,omitempty" url:"action.snow_event.param.severity,omitempty"`
124+
ActionSnowEventParamSeverity FlexInt `json:"action.snow_event.param.severity,omitempty" url:"action.snow_event.param.severity,omitempty"`
87125
ActionSnowEventParamDescription string `json:"action.snow_event.param.description,omitempty" url:"action.snow_event.param.description,omitempty"`
88126
ActionSnowEventParamCiIdentifier string `json:"action.snow_event.param.ci_identifier,omitempty" url:"action.snow_event.param.ci_identifier,omitempty"`
89127
ActionSnowEventParamCustomFields string `json:"action.snow_event.param.custom_fields,omitempty" url:"action.snow_event.param.custom_fields,omitempty"`

splunk/resource_splunk_saved_searches.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,11 +1961,11 @@ func getSavedSearchesConfig(d *schema.ResourceData) (savedSearchesObj *models.Sa
19611961
ActionEmailFormat: d.Get("action_email_format").(string),
19621962
ActionEmailFrom: d.Get("action_email_from").(string),
19631963
ActionEmailHostname: d.Get("action_email_hostname").(string),
1964-
ActionEmailIncludeResultsLink: d.Get("action_email_include_results_link").(int),
1965-
ActionEmailIncludeSearch: d.Get("action_email_include_search").(int),
1966-
ActionEmailIncludeTrigger: d.Get("action_email_include_trigger").(int),
1967-
ActionEmailIncludeTriggerTime: d.Get("action_email_include_trigger_time").(int),
1968-
ActionEmailIncludeViewLink: d.Get("action_email_include_view_link").(int),
1964+
ActionEmailIncludeResultsLink: models.FlexInt(d.Get("action_email_include_results_link").(int)),
1965+
ActionEmailIncludeSearch: models.FlexInt(d.Get("action_email_include_search").(int)),
1966+
ActionEmailIncludeTrigger: models.FlexInt(d.Get("action_email_include_trigger").(int)),
1967+
ActionEmailIncludeTriggerTime: models.FlexInt(d.Get("action_email_include_trigger_time").(int)),
1968+
ActionEmailIncludeViewLink: models.FlexInt(d.Get("action_email_include_view_link").(int)),
19691969
ActionEmailInline: d.Get("action_email_inline").(bool),
19701970
ActionEmailMailserver: d.Get("action_email_mailserver").(string),
19711971
ActionEmailMaxResults: d.Get("action_email_max_results").(int),
@@ -1980,7 +1980,7 @@ func getSavedSearchesConfig(d *schema.ResourceData) (savedSearchesObj *models.Sa
19801980
ActionEmailReportPaperSize: d.Get("action_email_report_paper_size").(string),
19811981
ActionEmailReportServerEnabled: d.Get("action_email_report_server_enabled").(bool),
19821982
ActionEmailReportServerURL: d.Get("action_email_report_server_url").(string),
1983-
ActionEmailSendCSV: d.Get("action_email_send_csv").(int),
1983+
ActionEmailSendCSV: models.FlexInt(d.Get("action_email_send_csv").(int)),
19841984
ActionEmailSendPDF: d.Get("action_email_send_pdf").(bool),
19851985
ActionEmailSendResults: d.Get("action_email_send_results").(bool),
19861986
ActionEmailSubject: d.Get("action_email_subject").(string),
@@ -2019,7 +2019,7 @@ func getSavedSearchesConfig(d *schema.ResourceData) (savedSearchesObj *models.Sa
20192019
ActionSnowEventParamNode: d.Get("action_snow_event_param_node").(string),
20202020
ActionSnowEventParamType: d.Get("action_snow_event_param_type").(string),
20212021
ActionSnowEventParamResource: d.Get("action_snow_event_param_resource").(string),
2022-
ActionSnowEventParamSeverity: d.Get("action_snow_event_param_severity").(int),
2022+
ActionSnowEventParamSeverity: models.FlexInt(d.Get("action_snow_event_param_severity").(int)),
20232023
ActionSnowEventParamDescription: d.Get("action_snow_event_param_description").(string),
20242024
ActionSnowEventParamCiIdentifier: d.Get("action_snow_event_param_ci_identifier").(string),
20252025
ActionSnowEventParamCustomFields: d.Get("action_snow_event_param_custom_fields").(string),

0 commit comments

Comments
 (0)