feat(config): support multi-service yaml config

This commit is contained in:
tiennm99 committed 2026-06-28 09:27:43 +07:00
1 parent 655db8dcbf
commit f3a23ee91e
19 files changed
+709 -268

No files matched your search

+22 -5
View File
@@ -17,16 +17,33 @@ type Adapter interface {
Close(ctx context.Context) error
}
type Factory func() (Adapter, error)
type Config map[string]string
func (c Config) Required(name string) (string, error) {
v, ok := c[name]
if !ok || v == "" {
return "", fmt.Errorf("config %s is required", name)
}
return v, nil
}
func (c Config) Optional(name, def string) string {
if v, ok := c[name]; ok && v != "" {
return v
}
return def
}
type Factory func(Config) (Adapter, error)
var Registry = map[string]Factory{}
func New(dbType string) (Adapter, error) {
f, ok := Registry[dbType]
func New(adapterType string, cfg Config) (Adapter, error) {
f, ok := Registry[adapterType]
if !ok {
return nil, fmt.Errorf("unknown adapter %q (known: %v)", dbType, Known())
return nil, fmt.Errorf("unknown adapter %q (known: %v)", adapterType, Known())
}
return f()
return f(cfg)
}
func Known() []string {
+48
View File
@@ -0,0 +1,48 @@
package adapter
import (
"strings"
"testing"
)
func TestConfigRequiredReturnsConfiguredValue(t *testing.T) {
cfg := Config{"url": "redis://127.0.0.1:6379"}
got, err := cfg.Required("url")
if err != nil {
t.Fatalf("Required returned error: %v", err)
}
if got != "redis://127.0.0.1:6379" {
t.Fatalf("Required() = %q, want configured value", got)
}
}
func TestConfigRequiredReportsMissingName(t *testing.T) {
cfg := Config{}
_, err := cfg.Required("url")
if err == nil {
t.Fatal("Required returned nil error")
}
if !strings.Contains(err.Error(), "url") {
t.Fatalf("error %q does not include config key", err)
}
}
func TestConfigOptionalReturnsConfiguredValue(t *testing.T) {
cfg := Config{"counter_key": "custom"}
got := cfg.Optional("counter_key", "counter")
if got != "custom" {
t.Fatalf("Optional() = %q, want custom", got)
}
}
func TestConfigOptionalUsesDefault(t *testing.T) {
cfg := Config{}
got := cfg.Optional("counter_key", "counter")
if got != "counter" {
t.Fatalf("Optional() = %q, want counter", got)
}
}
+48 -34
View File
@@ -8,58 +8,72 @@ import (
)
func init() {
Registry["couchbase"] = func() (Adapter, error) { return &couchbaseAdapter{}, nil }
Registry["couchbase"] = func(cfg Config) (Adapter, error) {
conn, err := cfg.Required("connection_string")
if err != nil {
return nil, err
}
user, err := cfg.Required("username")
if err != nil {
return nil, err
}
pass, err := cfg.Required("password")
if err != nil {
return nil, err
}
bucket, err := cfg.Required("bucket_name")
if err != nil {
return nil, err
}
scope, err := cfg.Required("scope_name")
if err != nil {
return nil, err
}
collName, err := cfg.Required("collection_name")
if err != nil {
return nil, err
}
return &couchbaseAdapter{
conn: conn,
user: user,
pass: pass,
bucket: bucket,
scope: scope,
collName: collName,
docID: cfg.Optional("counter_key", "counter"),
}, nil
}
}
type couchbaseAdapter struct {
cluster *gocb.Cluster
coll *gocb.Collection
docID string
cluster *gocb.Cluster
coll *gocb.Collection
conn string
user string
pass string
bucket string
scope string
collName string
docID string
}
func (a *couchbaseAdapter) Connect(_ context.Context) error {
conn, err := envOrFail("KEEPALIVE_COUCHBASE_CONNECTION_STRING")
if err != nil {
return err
}
user, err := envOrFail("KEEPALIVE_COUCHBASE_USERNAME")
if err != nil {
return err
}
pass, err := envOrFail("KEEPALIVE_COUCHBASE_PASSWORD")
if err != nil {
return err
}
bucket, err := envOrFail("KEEPALIVE_COUCHBASE_BUCKET_NAME")
if err != nil {
return err
}
scope, err := envOrFail("KEEPALIVE_COUCHBASE_SCOPE_NAME")
if err != nil {
return err
}
collName, err := envOrFail("KEEPALIVE_COUCHBASE_COLLECTION_NAME")
if err != nil {
return err
}
opts := gocb.ClusterOptions{
Authenticator: gocb.PasswordAuthenticator{Username: user, Password: pass},
Authenticator: gocb.PasswordAuthenticator{Username: a.user, Password: a.pass},
}
if err := opts.ApplyProfile(gocb.ClusterConfigProfileWanDevelopment); err != nil {
return err
}
cluster, err := gocb.Connect(conn, opts)
cluster, err := gocb.Connect(a.conn, opts)
if err != nil {
return err
}
b := cluster.Bucket(bucket)
b := cluster.Bucket(a.bucket)
if err := b.WaitUntilReady(5*time.Second, nil); err != nil {
return err
}
a.cluster = cluster
a.coll = b.Scope(scope).Collection(collName)
a.docID = envOr("KEEPALIVE_COUNTER_KEY", "counter")
a.coll = b.Scope(a.scope).Collection(a.collName)
return nil
}
-21
View File
@@ -1,21 +0,0 @@
package adapter
import (
"fmt"
"os"
)
func envOrFail(name string) (string, error) {
v, ok := os.LookupEnv(name)
if !ok || v == "" {
return "", fmt.Errorf("env %s is required", name)
}
return v, nil
}
func envOr(name, def string) string {
if v, ok := os.LookupEnv(name); ok && v != "" {
return v
}
return def
}
-44
View File
@@ -1,44 +0,0 @@
package adapter
import (
"strings"
"testing"
)
func TestEnvOrFailReturnsConfiguredValue(t *testing.T) {
t.Setenv("KEEPALIVE_TEST_REQUIRED", "configured")
got, err := envOrFail("KEEPALIVE_TEST_REQUIRED")
if err != nil {
t.Fatalf("envOrFail returned error: %v", err)
}
if got != "configured" {
t.Fatalf("envOrFail() = %q, want configured", got)
}
}
func TestEnvOrFailReportsMissingName(t *testing.T) {
_, err := envOrFail("KEEPALIVE_TEST_MISSING_REQUIRED")
if err == nil {
t.Fatal("envOrFail returned nil error")
}
if !strings.Contains(err.Error(), "KEEPALIVE_TEST_MISSING_REQUIRED") {
t.Fatalf("error %q does not include env name", err)
}
}
func TestEnvOrReturnsConfiguredValue(t *testing.T) {
t.Setenv("KEEPALIVE_TEST_OPTIONAL", "configured")
got := envOr("KEEPALIVE_TEST_OPTIONAL", "default")
if got != "configured" {
t.Fatalf("envOr() = %q, want configured", got)
}
}
func TestEnvOrUsesDefaultWhenNoNamesAreSet(t *testing.T) {
got := envOr("KEEPALIVE_TEST_MISSING_DEFAULT", "default")
if got != "default" {
t.Fatalf("envOr() = %q, want default", got)
}
}
+28 -19
View File
@@ -9,36 +9,45 @@ import (
)
func init() {
Registry["mongodb"] = func() (Adapter, error) { return &mongoAdapter{}, nil }
Registry["mongodb"] = func(cfg Config) (Adapter, error) {
uri, err := cfg.Required("uri")
if err != nil {
return nil, err
}
dbName, err := cfg.Required("database")
if err != nil {
return nil, err
}
collName, err := cfg.Required("collection")
if err != nil {
return nil, err
}
return &mongoAdapter{
uri: uri,
dbName: dbName,
collName: collName,
docID: cfg.Optional("counter_key", "counter"),
}, nil
}
Registry["mongo"] = Registry["mongodb"]
}
type mongoAdapter struct {
client *mongo.Client
coll *mongo.Collection
docID string
client *mongo.Client
coll *mongo.Collection
uri string
dbName string
collName string
docID string
}
func (a *mongoAdapter) Connect(ctx context.Context) error {
uri, err := envOrFail("KEEPALIVE_MONGODB_URI")
if err != nil {
return err
}
dbName, err := envOrFail("KEEPALIVE_MONGODB_DATABASE")
if err != nil {
return err
}
collName, err := envOrFail("KEEPALIVE_MONGODB_COLLECTION")
if err != nil {
return err
}
client, err := mongo.Connect(options.Client().ApplyURI(uri))
client, err := mongo.Connect(options.Client().ApplyURI(a.uri))
if err != nil {
return err
}
a.client = client
a.coll = client.Database(dbName).Collection(collName)
a.docID = envOr("KEEPALIVE_COUNTER_KEY", "counter")
a.coll = client.Database(a.dbName).Collection(a.collName)
return client.Ping(ctx, nil)
}
+12 -7
View File
@@ -9,20 +9,26 @@ import (
)
func init() {
Registry["mysql"] = func() (Adapter, error) { return &mysqlAdapter{}, nil }
Registry["mysql"] = func(cfg Config) (Adapter, error) {
dsn, err := cfg.Required("dsn")
if err != nil {
return nil, err
}
return &mysqlAdapter{
dsn: dsn,
key: cfg.Optional("counter_key", "counter"),
}, nil
}
}
type mysqlAdapter struct {
db *sql.DB
dsn string
key string
}
func (a *mysqlAdapter) Connect(ctx context.Context) error {
dsn, err := envOrFail("KEEPALIVE_MYSQL_DSN")
if err != nil {
return err
}
db, err := sql.Open("mysql", dsn)
db, err := sql.Open("mysql", a.dsn)
if err != nil {
return err
}
@@ -30,7 +36,6 @@ func (a *mysqlAdapter) Connect(ctx context.Context) error {
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
a.db = db
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
return a.db.PingContext(ctx)
}
+12 -7
View File
@@ -8,26 +8,31 @@ import (
)
func init() {
Registry["postgresql"] = func() (Adapter, error) { return &postgresAdapter{}, nil }
Registry["postgresql"] = func(cfg Config) (Adapter, error) {
url, err := cfg.Required("url")
if err != nil {
return nil, err
}
return &postgresAdapter{
url: url,
key: cfg.Optional("counter_key", "counter"),
}, nil
}
Registry["postgres"] = Registry["postgresql"]
}
type postgresAdapter struct {
db *sql.DB
url string
key string
}
func (a *postgresAdapter) Connect(ctx context.Context) error {
uri, err := envOrFail("KEEPALIVE_POSTGRESQL_URL")
if err != nil {
return err
}
db, err := sql.Open("postgres", uri)
db, err := sql.Open("postgres", a.url)
if err != nil {
return err
}
a.db = db
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
return a.db.PingContext(ctx)
}
+12 -7
View File
@@ -7,25 +7,30 @@ import (
)
func init() {
Registry["redis"] = func() (Adapter, error) { return &redisAdapter{}, nil }
Registry["redis"] = func(cfg Config) (Adapter, error) {
url, err := cfg.Required("url")
if err != nil {
return nil, err
}
return &redisAdapter{
url: url,
key: cfg.Optional("counter_key", "counter"),
}, nil
}
}
type redisAdapter struct {
client *redis.Client
url string
key string
}
func (a *redisAdapter) Connect(ctx context.Context) error {
url, err := envOrFail("KEEPALIVE_REDIS_URL")
if err != nil {
return err
}
opt, err := redis.ParseURL(url)
opt, err := redis.ParseURL(a.url)
if err != nil {
return err
}
a.client = redis.NewClient(opt)
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
return a.client.Ping(ctx).Err()
}
+12 -7
View File
@@ -7,20 +7,26 @@ import (
)
func init() {
Registry["valkey"] = func() (Adapter, error) { return &valkeyAdapter{}, nil }
Registry["valkey"] = func(cfg Config) (Adapter, error) {
url, err := cfg.Required("url")
if err != nil {
return nil, err
}
return &valkeyAdapter{
url: url,
key: cfg.Optional("counter_key", "counter"),
}, nil
}
}
type valkeyAdapter struct {
client valkey.Client
url string
key string
}
func (a *valkeyAdapter) Connect(_ context.Context) error {
url, err := envOrFail("KEEPALIVE_VALKEY_URL")
if err != nil {
return err
}
opt, err := valkey.ParseURL(url)
opt, err := valkey.ParseURL(a.url)
if err != nil {
return err
}
@@ -29,7 +35,6 @@ func (a *valkeyAdapter) Connect(_ context.Context) error {
return err
}
a.client = client
a.key = envOr("KEEPALIVE_COUNTER_KEY", "counter")
return nil
}