Skip to content

Commit dd03d4e

Browse files
committed
notifications: introduce Event & Type types
1 parent 4e98739 commit dd03d4e

File tree

6 files changed

+371
-0
lines changed

6 files changed

+371
-0
lines changed

notifications/event/event.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package event
2+
3+
import (
4+
"github.com/icinga/icinga-go-library/types"
5+
)
6+
7+
// Event represents an Icinga Notifications event that can be sent to the Icinga Notifications API.
8+
//
9+
// It contains all the necessary fields to fully describe an Icinga Notifications event and can be used to
10+
// serialize the event to JSON for transmission over HTTP as well as to deserialize it from JSON requests.
11+
type Event struct {
12+
Name string `json:"name"` // Name is the name of the object this event is all about.
13+
URL string `json:"url"` // URL represents the fully qualified URL to the object in Icinga Web 2.
14+
15+
// Tags contains additional metadata for the event that uniquely identifies the object it's referring to.
16+
//
17+
// It is a map of string keys to string values, allowing for flexible tagging of events if the event
18+
// name alone is not sufficient to identify the object. In the case of using Icinga DB as a source, the
19+
// tags will typically look like this:
20+
// For hosts: {"host": "host_name"} and for services: {"host": "host_name", "service": "service_name"}.
21+
Tags map[string]string `json:"tags"`
22+
23+
Type Type `json:"type"` // Type indicates the type of the event (see Type for possible values).
24+
Severity Severity `json:"severity,omitempty"` // The severity of the event (see Severity for possible values).
25+
Username string `json:"username"` // Username is the name of the user who triggered the event.
26+
Message string `json:"message"` // Message is a human-readable message describing the event.
27+
28+
// Mute indicates whether the object this event is referring to should be muted or not.
29+
//
30+
// If set to true, the object will be muted in Icinga Web 2, meaning that notifications for this object
31+
// will not be sent out. The MuteReason field can be used to provide a reason for muting the object.
32+
// If you don't set this field to anything, it will be omitted from the generated JSON.
33+
Mute types.Bool `json:"mute,omitzero"`
34+
35+
// MuteReason provides a reason for muting the object if Mute is set to true.
36+
//
37+
// Setting this field to an empty string while Mute is true will cause the request to fail,
38+
// as Icinga Notifications requires a reason for muting an object. Otherwise, it will be omitted
39+
// from the encoded JSON.
40+
MuteReason string `json:"mute_reason,omitempty"`
41+
}

notifications/event/event_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package event
2+
3+
import (
4+
"encoding/json"
5+
"github.com/stretchr/testify/assert"
6+
"github.com/stretchr/testify/require"
7+
"testing"
8+
)
9+
10+
func TestEvent(t *testing.T) {
11+
t.Parallel()
12+
13+
t.Run("JsonEncode", func(t *testing.T) {
14+
t.Parallel()
15+
16+
t.Run("Valid Event", func(t *testing.T) {
17+
t.Parallel()
18+
19+
event := &Event{
20+
Name: "TestEvent",
21+
URL: "example.com",
22+
Tags: map[string]string{"tag1": "value1"},
23+
Type: TypeState,
24+
Severity: SeverityOK,
25+
Username: "testuser",
26+
Message: "Test",
27+
}
28+
29+
data, err := json.Marshal(event)
30+
require.NoError(t, err)
31+
32+
expected := `{"name":"TestEvent","url":"example.com","tags":{"tag1":"value1"},"type":"state","severity":"ok","username":"testuser","message":"Test"}`
33+
assert.Equal(t, expected, string(data))
34+
})
35+
36+
t.Run("Empty Severity", func(t *testing.T) {
37+
t.Parallel()
38+
39+
event := &Event{
40+
Name: "TestEvent",
41+
URL: "example.com",
42+
Tags: map[string]string{"tag1": "value1"},
43+
Type: TypeMute,
44+
Username: "testuser",
45+
Message: "Test",
46+
}
47+
48+
data, err := json.Marshal(event)
49+
require.NoError(t, err)
50+
assert.NotContains(t, string(data), "\"severity\":", "severity should be omitted when empty")
51+
52+
event.Severity = SeverityNone
53+
data, err = json.Marshal(event)
54+
require.NoError(t, err)
55+
assert.NotContains(t, string(data), "\"severity\":", "severity should be omitted when set to none")
56+
})
57+
})
58+
}

notifications/event/type.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//go:generate go tool stringer -linecomment -type Type -output type_string.go
2+
3+
package event
4+
5+
import (
6+
"database/sql/driver"
7+
"encoding/json"
8+
"fmt"
9+
)
10+
11+
// Type represents the type of event sent to the Icinga Notifications API.
12+
type Type uint8
13+
14+
const (
15+
TypeUnknown Type = iota // unknown
16+
17+
TypeAcknowledgementCleared // acknowledgement-cleared
18+
TypeAcknowledgementSet // acknowledgement-set
19+
TypeCustom // custom
20+
TypeDowntimeEnd // downtime-end
21+
TypeDowntimeRemoved // downtime-removed
22+
TypeDowntimeStart // downtime-start
23+
TypeFlappingEnd // flapping-end
24+
TypeFlappingStart // flapping-start
25+
TypeIncidentAge // incident-age
26+
TypeMute // mute
27+
TypeState // state
28+
TypeUnmute // unmute
29+
30+
typeMax // internal
31+
)
32+
33+
// MarshalJSON implements the [json.Marshaler] interface for Type.
34+
func (t Type) MarshalJSON() ([]byte, error) {
35+
if t != TypeUnknown {
36+
return json.Marshal(t.String())
37+
} else {
38+
return json.Marshal(nil)
39+
}
40+
}
41+
42+
// UnmarshalJSON implements the [json.Unmarshaler] interface for Type.
43+
func (t *Type) UnmarshalJSON(data []byte) error {
44+
if string(data) == "null" {
45+
*t = TypeUnknown
46+
return nil
47+
}
48+
49+
var typeString string
50+
if err := json.Unmarshal(data, &typeString); err != nil {
51+
return err
52+
}
53+
54+
parsedType, err := ParseType(typeString)
55+
if err != nil {
56+
return err
57+
}
58+
59+
*t = parsedType
60+
return nil
61+
}
62+
63+
// Scan implements the [sql.Scanner] interface for Severity.
64+
// Supports SQL NULL values.
65+
func (t *Type) Scan(src any) error {
66+
if src == nil {
67+
*t = TypeUnknown
68+
return nil
69+
}
70+
71+
var typeStr string
72+
switch val := src.(type) {
73+
case string:
74+
typeStr = val
75+
case []byte:
76+
typeStr = string(val)
77+
default:
78+
return fmt.Errorf("cannot scan Type from %T", src)
79+
}
80+
81+
parsedType, err := ParseType(typeStr)
82+
if err != nil {
83+
return err
84+
}
85+
86+
*t = parsedType
87+
return nil
88+
}
89+
90+
// Value implements the [driver.Valuer] interface for Severity.
91+
func (t Type) Value() (driver.Value, error) {
92+
if t != TypeUnknown {
93+
return t.String(), nil
94+
}
95+
return nil, nil // Return nil for SeverityNone or invalid values
96+
}
97+
98+
// ParseType parses a string into a Type.
99+
//
100+
// If the string does not match any known type, it returns an error indicating the unknown type.
101+
func ParseType(s string) (Type, error) {
102+
for t := TypeUnknown + 1; t < typeMax; t++ {
103+
if s == t.String() {
104+
return t, nil
105+
}
106+
}
107+
108+
return TypeUnknown, fmt.Errorf("unknown type %q", s)
109+
}

notifications/event/type_string.go

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

notifications/event/type_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package event
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
"github.com/stretchr/testify/require"
6+
"testing"
7+
)
8+
9+
func TestType(t *testing.T) {
10+
t.Parallel()
11+
12+
t.Run("MarshalJSON", func(t *testing.T) {
13+
t.Parallel()
14+
15+
tests := []struct {
16+
name string
17+
typeVal Type
18+
expected string
19+
}{
20+
{"Unknown", TypeUnknown, "null"},
21+
{"State", TypeState, `"state"`},
22+
{"Mute", TypeMute, `"mute"`},
23+
{"Unmute", TypeUnmute, `"unmute"`},
24+
}
25+
26+
for _, tt := range tests {
27+
t.Run(tt.name, func(t *testing.T) {
28+
t.Parallel()
29+
30+
data, err := tt.typeVal.MarshalJSON()
31+
require.NoError(t, err)
32+
assert.Equal(t, tt.expected, string(data))
33+
})
34+
}
35+
})
36+
37+
t.Run("UnmarshalJSON", func(t *testing.T) {
38+
t.Parallel()
39+
40+
tests := []struct {
41+
name string
42+
input string
43+
expected Type
44+
err bool
45+
}{
46+
{"Unknown", "null", TypeUnknown, false},
47+
{"State", `"state"`, TypeState, false},
48+
{"Mute", `"mute"`, TypeMute, false},
49+
{"Unmute", `"unmute"`, TypeUnmute, false},
50+
{"Invalid", `"invalid"`, TypeUnknown, true}, // Should return an error for unsupported type.
51+
}
52+
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
t.Parallel()
56+
57+
var tType Type
58+
if err := tType.UnmarshalJSON([]byte(tt.input)); tt.err {
59+
assert.Error(t, err)
60+
} else {
61+
require.NoError(t, err)
62+
assert.Equal(t, tt.expected, tType)
63+
}
64+
})
65+
}
66+
})
67+
68+
t.Run("Scan", func(t *testing.T) {
69+
t.Parallel()
70+
71+
tests := []struct {
72+
name string
73+
input any
74+
expected Type
75+
err bool
76+
}{
77+
{"Unknown", nil, TypeUnknown, false},
78+
{"State", "state", TypeState, false},
79+
{"Mute", "mute", TypeMute, false},
80+
{"Unmute", "unmute", TypeUnmute, false},
81+
{"Invalid", "invalid", TypeUnknown, true}, // Should return an error for unsupported type.
82+
}
83+
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
t.Parallel()
87+
88+
var tType Type
89+
if err := tType.Scan(tt.input); tt.err {
90+
assert.Error(t, err)
91+
} else {
92+
require.NoError(t, err)
93+
assert.Equal(t, tt.expected, tType)
94+
}
95+
})
96+
}
97+
})
98+
99+
t.Run("Value", func(t *testing.T) {
100+
t.Parallel()
101+
102+
tests := []struct {
103+
name string
104+
typeVal Type
105+
expected any
106+
}{
107+
{"Unknown", TypeUnknown, nil},
108+
{"State", TypeState, "state"},
109+
{"Mute", TypeMute, "mute"},
110+
{"Unmute", TypeUnmute, "unmute"},
111+
}
112+
113+
for _, tt := range tests {
114+
t.Run(tt.name, func(t *testing.T) {
115+
t.Parallel()
116+
117+
value, err := tt.typeVal.Value()
118+
require.NoError(t, err)
119+
assert.Equal(t, tt.expected, value)
120+
})
121+
}
122+
})
123+
}

types/bool.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ type Bool struct {
2222
Valid bool // Valid is true if Bool is not NULL
2323
}
2424

25+
// IsZero implements the json.isZeroer interface.
26+
// A Bool is considered zero if its Valid field is false regardless of its actual Bool value.
27+
func (b Bool) IsZero() bool { return !b.Valid }
28+
2529
// MarshalJSON implements the json.Marshaler interface.
2630
func (b Bool) MarshalJSON() ([]byte, error) {
2731
if !b.Valid {

0 commit comments

Comments
 (0)