Files
sendico/api/fx/ingestor/internal/config/config.go
2026-01-04 12:57:40 +01:00

150 lines
4.0 KiB
Go

package config
import (
"os"
"strings"
"time"
mmodel "github.com/tech/sendico/fx/ingestor/internal/model"
"github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/messaging"
"gopkg.in/yaml.v3"
)
const defaultPollInterval = 30 * time.Second
type Config struct {
PollIntervalSeconds int `yaml:"poll_interval_seconds"`
Market MarketConfig `yaml:"market"`
Database *db.Config `yaml:"database"`
Metrics *MetricsConfig `yaml:"metrics"`
Messaging *messaging.Config `yaml:"messaging"`
pairs []Pair
pairsBySource map[mmodel.Driver][]PairConfig
}
func Load(path string) (*Config, error) {
if path == "" {
return nil, merrors.InvalidArgument("config: path is empty")
}
data, err := os.ReadFile(path)
if err != nil {
return nil, merrors.InternalWrap(err, "config: failed to read file")
}
cfg := &Config{}
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, merrors.InternalWrap(err, "config: failed to parse yaml")
}
if len(cfg.Market.Sources) == 0 {
return nil, merrors.InvalidArgument("config: no market sources configured")
}
sourceSet := make(map[mmodel.Driver]struct{}, len(cfg.Market.Sources))
for idx := range cfg.Market.Sources {
src := &cfg.Market.Sources[idx]
if src.Driver.IsEmpty() {
return nil, merrors.InvalidArgument("config: market source driver is empty")
}
sourceSet[src.Driver] = struct{}{}
}
if len(cfg.Market.Pairs) == 0 {
return nil, merrors.InvalidArgument("config: no pairs configured")
}
normalizedPairs := make(map[string][]PairConfig, len(cfg.Market.Pairs))
pairsBySource := make(map[mmodel.Driver][]PairConfig, len(cfg.Market.Pairs))
var flattened []Pair
for rawSource, pairList := range cfg.Market.Pairs {
driver := mmodel.Driver(rawSource)
if driver.IsEmpty() {
return nil, merrors.InvalidArgument("config: pair source is empty")
}
if _, ok := sourceSet[driver]; !ok {
return nil, merrors.InvalidArgument("config: pair references unknown source: "+driver.String(), "pairs."+driver.String())
}
processed := make([]PairConfig, len(pairList))
for idx := range pairList {
pair := pairList[idx]
pair.Base = strings.ToUpper(strings.TrimSpace(pair.Base))
pair.Quote = strings.ToUpper(strings.TrimSpace(pair.Quote))
pair.Symbol = strings.TrimSpace(pair.Symbol)
if pair.Base == "" || pair.Quote == "" || pair.Symbol == "" {
return nil, merrors.InvalidArgument("config: pair entries must define base, quote, and symbol", "pairs."+driver.String())
}
if strings.TrimSpace(pair.Provider) == "" {
pair.Provider = strings.ToLower(driver.String())
}
processed[idx] = pair
flattened = append(flattened, Pair{
PairConfig: pair,
Source: driver,
})
}
pairsBySource[driver] = processed
normalizedPairs[driver.String()] = processed
}
cfg.Market.Pairs = normalizedPairs
cfg.pairsBySource = pairsBySource
cfg.pairs = flattened
if cfg.Database == nil {
return nil, merrors.InvalidArgument("config: database configuration is required")
}
if cfg.Metrics != nil && cfg.Metrics.Enabled {
cfg.Metrics.Address = strings.TrimSpace(cfg.Metrics.Address)
if cfg.Metrics.Address == "" {
cfg.Metrics.Address = ":9102"
}
}
return cfg, nil
}
func (c *Config) PollInterval() time.Duration {
if c == nil {
return defaultPollInterval
}
if c.PollIntervalSeconds <= 0 {
return defaultPollInterval
}
return time.Duration(c.PollIntervalSeconds) * time.Second
}
func (c *Config) Pairs() []Pair {
if c == nil {
return nil
}
out := make([]Pair, len(c.pairs))
copy(out, c.pairs)
return out
}
func (c *Config) PairsBySource() map[mmodel.Driver][]PairConfig {
if c == nil {
return nil
}
out := make(map[mmodel.Driver][]PairConfig, len(c.pairsBySource))
for driver, pairs := range c.pairsBySource {
cp := make([]PairConfig, len(pairs))
copy(cp, pairs)
out[driver] = cp
}
return out
}
func (c *Config) MetricsConfig() *MetricsConfig {
if c == nil || c.Metrics == nil {
return nil
}
cp := *c.Metrics
return &cp
}