Initial MVP commit
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful

This commit is contained in:
2026-03-17 08:35:16 -05:00
parent 8c092c2247
commit 27817f9e43
29 changed files with 1890 additions and 1 deletions

View File

@@ -0,0 +1,120 @@
package config
import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v3"
)
// Config holds weatherapi datasource configuration.
type Config struct {
Databases []DatabaseConfig
}
type DatabaseConfig struct {
Name string `yaml:"name"`
Driver string `yaml:"driver"`
Params DatabaseParams `yaml:"params"`
}
type DatabaseParams struct {
URI string `yaml:"uri"`
Username string `yaml:"username"`
Password string `yaml:"password"`
}
type configWrapper struct {
Databases []DatabaseConfig `yaml:"databases"`
}
func Load(path string) (*Config, error) {
if strings.TrimSpace(path) == "" {
path = "config.yml"
}
raw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("config.Load: read %q: %w", path, err)
}
var list []DatabaseConfig
if err := decodeStrict(raw, &list); err == nil && len(list) > 0 {
cfg := &Config{Databases: list}
if err := cfg.Validate(); err != nil {
return nil, err
}
return cfg, nil
}
var wrapped configWrapper
if err := decodeStrict(raw, &wrapped); err != nil {
return nil, fmt.Errorf("config.Load: parse YAML %q: %w", path, err)
}
cfg := &Config{Databases: wrapped.Databases}
if err := cfg.Validate(); err != nil {
return nil, err
}
return cfg, nil
}
func decodeStrict(raw []byte, out any) error {
dec := yaml.NewDecoder(strings.NewReader(string(raw)))
dec.KnownFields(true)
if err := dec.Decode(out); err != nil {
return err
}
var extra any
if err := dec.Decode(&extra); err == nil {
return fmt.Errorf("contains multiple YAML documents; expected exactly one")
}
return nil
}
func (c *Config) Validate() error {
if c == nil {
return fmt.Errorf("config validation failed: config is nil")
}
if len(c.Databases) == 0 {
return fmt.Errorf("config validation failed: no databases configured")
}
seen := map[string]struct{}{}
for i, db := range c.Databases {
path := fmt.Sprintf("databases[%d]", i)
if strings.TrimSpace(db.Name) == "" {
return fmt.Errorf("config validation failed: %s.name is required", path)
}
if _, ok := seen[db.Name]; ok {
return fmt.Errorf("config validation failed: %s.name %q is duplicated", path, db.Name)
}
seen[db.Name] = struct{}{}
if strings.TrimSpace(db.Driver) == "" {
return fmt.Errorf("config validation failed: %s.driver is required", path)
}
if strings.TrimSpace(db.Params.URI) == "" {
return fmt.Errorf("config validation failed: %s.params.uri is required", path)
}
if strings.TrimSpace(db.Params.Username) == "" {
return fmt.Errorf("config validation failed: %s.params.username is required", path)
}
if strings.TrimSpace(db.Params.Password) == "" {
return fmt.Errorf("config validation failed: %s.params.password is required", path)
}
}
return nil
}
func (c *Config) FindDatabase(name string) (DatabaseConfig, bool) {
for _, db := range c.Databases {
if db.Name == name {
return db, true
}
}
return DatabaseConfig{}, false
}

View File

@@ -0,0 +1,53 @@
package config
import (
"os"
"path/filepath"
"testing"
)
func TestLoadRootList(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yml")
content := `
- name: weatherdb
driver: postgres
params:
uri: postgres://weatherdb:5432/weatherdb?sslmode=disable
username: weatherdb
password: weatherdb
`
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write temp config: %v", err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("load config: %v", err)
}
if len(cfg.Databases) != 1 {
t.Fatalf("expected one database, got %d", len(cfg.Databases))
}
if cfg.Databases[0].Name != "weatherdb" {
t.Fatalf("unexpected db name %q", cfg.Databases[0].Name)
}
}
func TestLoadMissingRequiredFieldFails(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yml")
content := `
- name: weatherdb
driver: postgres
params:
username: weatherdb
password: weatherdb
`
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write temp config: %v", err)
}
if _, err := Load(path); err == nil {
t.Fatalf("expected validation error for missing params.uri")
}
}