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 }