Merge pull request #3120 from carlosprados/feat/register-channel-settings

feat(config): add RegisterChannelSettings hook for out-of-tree channels
This commit is contained in:
Mauro 2026-06-16 22:09:00 +02:00 committed by GitHub
commit 910bfbe6a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 0 deletions

View file

@ -7,6 +7,7 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/caarlos0/env/v11" "github.com/caarlos0/env/v11"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -655,6 +656,8 @@ func filterSecureFields(r RawNode, secureFields map[string]struct{}) RawNode {
// channelSettingsFactory maps channel type to a zero-value prototype of the // channelSettingsFactory maps channel type to a zero-value prototype of the
// corresponding Settings struct. InitChannelList uses reflect.New to create // corresponding Settings struct. InitChannelList uses reflect.New to create
// fresh instances, avoiding repeated closure boilerplate. // fresh instances, avoiding repeated closure boilerplate.
var channelSettingsMu sync.RWMutex
var channelSettingsFactory = map[string]any{ var channelSettingsFactory = map[string]any{
ChannelPico: (PicoSettings{}), ChannelPico: (PicoSettings{}),
ChannelPicoClient: (PicoClientSettings{}), ChannelPicoClient: (PicoClientSettings{}),
@ -679,10 +682,24 @@ var channelSettingsFactory = map[string]any{
ChannelSlackWebHook: (SlackWebhookSettings{}), ChannelSlackWebHook: (SlackWebhookSettings{}),
} }
// RegisterChannelSettings registers a settings struct prototype for a custom
// channel type. External packages (out-of-tree channels registered via
// channels.RegisterFactory) call this from an init() so their channel type
// passes config validation (isValidChannelType) and its settings block decodes
// into the right struct (newChannelSettings). The prototype must be a struct
// value, e.g. RegisterChannelSettings("my_channel", MyChannelSettings{}).
func RegisterChannelSettings(channelType string, prototype any) {
channelSettingsMu.Lock()
defer channelSettingsMu.Unlock()
channelSettingsFactory[channelType] = prototype
}
// newChannelSettings creates a fresh zero-value pointer for the given channel type. // newChannelSettings creates a fresh zero-value pointer for the given channel type.
// Returns nil if the type is not registered. // Returns nil if the type is not registered.
func newChannelSettings(channelType string) any { func newChannelSettings(channelType string) any {
channelSettingsMu.RLock()
proto, ok := channelSettingsFactory[channelType] proto, ok := channelSettingsFactory[channelType]
channelSettingsMu.RUnlock()
if !ok { if !ok {
return nil return nil
} }
@ -691,7 +708,9 @@ func newChannelSettings(channelType string) any {
// isValidChannelType returns true if the channel type is a known, registered type. // isValidChannelType returns true if the channel type is a known, registered type.
func isValidChannelType(channelType string) bool { func isValidChannelType(channelType string) bool {
channelSettingsMu.RLock()
_, ok := channelSettingsFactory[channelType] _, ok := channelSettingsFactory[channelType]
channelSettingsMu.RUnlock()
return ok return ok
} }

View file

@ -0,0 +1,54 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// customChannelSettings stands in for an out-of-tree channel's settings struct.
type customChannelSettings struct {
Token string `json:"token"`
}
// TestRegisterChannelSettings verifies the public registration hook makes a
// previously-unknown channel type valid and decodable — the behavior out-of-tree
// channels rely on (they call RegisterChannelSettings from init()).
func TestRegisterChannelSettings(t *testing.T) {
const typ = "custom_test_channel"
assert.False(t, isValidChannelType(typ), "type should be unknown before registration")
RegisterChannelSettings(typ, customChannelSettings{})
assert.True(t, isValidChannelType(typ), "type should be valid after registration")
got := newChannelSettings(typ)
_, ok := got.(*customChannelSettings)
assert.Truef(t, ok, "newChannelSettings(%q) = %T, want *customChannelSettings", typ, got)
}
// TestRegisterChannelSettings_InitChannelList verifies that a config carrying a
// registered out-of-tree channel type passes InitChannelList and decodes its
// settings — the full path that previously errored with "unknown type".
func TestRegisterChannelSettings_InitChannelList(t *testing.T) {
const typ = "custom_initlist_channel"
RegisterChannelSettings(typ, customChannelSettings{})
channels := ChannelsConfig{
"mychan": {
Type: typ,
Enabled: true,
Settings: RawNode(`{"token":"secret-123"}`),
},
}
require.NoError(t, InitChannelList(channels))
decoded, err := channels["mychan"].GetDecoded()
require.NoError(t, err)
cfg, ok := decoded.(*customChannelSettings)
require.True(t, ok)
assert.Equal(t, "secret-123", cfg.Token)
}