diff --git a/README.md b/README.md index 59b99ee..c9ef34f 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ go mod tidy # Set the .env mv .env.example .env # Source the env variables -. <(cat .env | grep -v -e '^$' | grep -v "#") +export $(grep -v '^#' .env | grep -v '^$' | xargs) ``` ## Build and Run diff --git a/cmd/main.go b/cmd/main.go index d2ad1a7..fec483f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,7 +8,6 @@ import ( "controtto/src/gateways/sqlite" "controtto/src/rest" "log/slog" - "os" ) const ( @@ -21,23 +20,25 @@ const ( ) func main() { - // Database - dbPath := "pnl.db" - if dbPathEnv := os.Getenv("CONTROTTO_DB_PATH"); dbPathEnv != "" { - dbPath = dbPathEnv + // Load configuration + cfg, err := config.LoadFromEnv() + if err != nil { + slog.Error("Error loading config from env:", "error", err) + panic("Bye") } - slog.Info("Initiating SQLite path", "path", dbPath) - sqliteDB, err := sqlite.NewSQLite(dbPath) + cfgManager := config.NewManager(cfg) + + // Database + slog.Info("Initiating SQLite path", "path", cfgManager.Get().DBPath) + sqliteDB, err := sqlite.NewSQLite(cfgManager) if err != nil { slog.Error("Error creating SQLite:", "error", err) panic("Bye") } - // Load configuration - cfg := config.NewConfig(PREFIX) - config := config.NewService(cfg) + // Use Cases ac := managing.NewAssetCreator(sqliteDB) - pc := managing.NewPairManager(cfg, sqliteDB, sqliteDB) + pc := managing.NewPairManager(cfgManager, sqliteDB, sqliteDB) pq := querying.NewPairQuerier(sqliteDB) mm := managing.NewMarketManager(marketsConfig) ppm := managing.NewPriceProviderManager(pricers) @@ -47,9 +48,8 @@ func main() { tr := trading.NewTradeRecorder(sqliteDB) at := trading.NewAssetTrader(mm, sqliteDB) trader := trading.NewService(*at, *tr) - // *trading.NewAssetTrader(markets map[string]pnl.Market) port := cfg.Port slog.Info("Initiating server", "port", port) - rest.Run(&config, &manager, &querier, &trader) + rest.Run(cfgManager, &manager, &querier, &trader) } diff --git a/src/app/config/config.go b/src/app/config/config.go index 784ea14..a76ae3f 100644 --- a/src/app/config/config.go +++ b/src/app/config/config.go @@ -1,11 +1,8 @@ package config -// Service just hols all the managing use cases -type Service struct { - ConfigManager *ConfigManager -} - -// NewService is the interctor for all Managing Use cases -func NewService(cfg *ConfigManager) Service { - return Service{cfg} +type Config struct { + Port int // CONTROTTO_PORT + DBPath string // CONTROTTO_DB_PATH + UncommonPairs bool // CONTROTTO_UNCOMMON_PAIRS + LoadSampleData bool // CONTROTTO_LOAD_SAMPLE_DATA } diff --git a/src/app/config/configManager.go b/src/app/config/configManager.go deleted file mode 100644 index 3decbff..0000000 --- a/src/app/config/configManager.go +++ /dev/null @@ -1,61 +0,0 @@ -package config - -import ( - "os" - "strings" - "sync" -) - -const ( - PORT = "PORT" - DEFAULT_PORT = "8000" - UNCOMMON_PAIRS = "UNCOMMON_PAIRS" -) - -type ConfigManager struct { - prefix string - mu sync.RWMutex - Port string - uncommonPairs bool -} - -// NewConfig initializes the configuration from environment variables. -// If PORT is not set, it defaults to "8000". -func NewConfig(prefix string) *ConfigManager { - cfg := &ConfigManager{ - Port: os.Getenv(strings.ToUpper(prefix) + PORT), - uncommonPairs: os.Getenv(strings.ToUpper(prefix)+UNCOMMON_PAIRS) == "true", - } - cfg.prefix = strings.ToUpper(prefix) - if cfg.Port == "" { - cfg.Port = DEFAULT_PORT - os.Setenv(PORT, DEFAULT_PORT) - } - return cfg -} - -// GetPort returns the configured port. -func (c *ConfigManager) GetPort() string { - c.mu.RLock() - defer c.mu.RUnlock() - return c.Port -} - -func (c *ConfigManager) SetUncommonPairs(enabled bool) { - c.mu.Lock() - defer c.mu.Unlock() - - c.uncommonPairs = enabled - if enabled { - os.Setenv(c.prefix+UNCOMMON_PAIRS, "true") - } else { - os.Setenv(c.prefix+UNCOMMON_PAIRS, "false") - } -} - -// GetUncommonPairs returns whether uncommon pairs are enabled. -func (c *ConfigManager) GetUncommonPairs() bool { - c.mu.RLock() - defer c.mu.RUnlock() - return c.uncommonPairs -} diff --git a/src/app/config/load.go b/src/app/config/load.go new file mode 100644 index 0000000..28d2da3 --- /dev/null +++ b/src/app/config/load.go @@ -0,0 +1,75 @@ +package config + +import ( + "log/slog" + "os" + "strconv" +) + +const ( + DefaultPort = 8000 + DefaultDBPath = "./data/pnl.db" + DefaultUncommonPairs = false + DefaultLoadSampleData = false +) + +func LoadFromEnv() (*Config, error) { + port, err := getEnvInt("CONTROTTO_PORT", DefaultPort) + if err != nil { + return nil, err + } + + dbPath := getEnvString("CONTROTTO_DB_PATH", DefaultDBPath) + + uncommonPairs, err := getEnvBool("CONTROTTO_UNCOMMON_PAIRS", DefaultUncommonPairs) + if err != nil { + return nil, err + } + + loadSampleData, err := getEnvBool("CONTROTTO_LOAD_SAMPLE_DATA", DefaultLoadSampleData) + if err != nil { + return nil, err + } + + cfg := &Config{ + Port: port, + DBPath: dbPath, + UncommonPairs: uncommonPairs, + LoadSampleData: loadSampleData, + } + + return cfg, nil +} + +func getEnvString(key string, defaultVal string) string { + val := os.Getenv(key) + slog.Info("Loading env var", "key", key, "value", val) + if val == "" { + return defaultVal + } + return val +} + +func getEnvBool(key string, defaultVal bool) (bool, error) { + valStr := os.Getenv(key) + if valStr == "" { + return defaultVal, nil + } + val, err := strconv.ParseBool(valStr) + if err != nil { + return false, err + } + return val, nil +} + +func getEnvInt(key string, defaultVal int) (int, error) { + valStr := os.Getenv(key) + if valStr == "" { + return defaultVal, nil + } + val, err := strconv.Atoi(valStr) + if err != nil { + return 0, err + } + return val, nil +} diff --git a/src/app/config/manager.go b/src/app/config/manager.go new file mode 100644 index 0000000..c3b37e0 --- /dev/null +++ b/src/app/config/manager.go @@ -0,0 +1,33 @@ +package config + +import "sync" + +// Manager holds the application configuration and provides thread-safe access to it. +type Manager struct { + mu sync.RWMutex + config *Config +} + +// NewManager creates a new ConfigManager. +func NewManager(config *Config) *Manager { + return &Manager{config: config} +} + +// Get returns the current configuration. +func (m *Manager) Get() *Config { + m.mu.RLock() + defer m.mu.RUnlock() + return m.config +} + +// Update updates the configuration. +func (m *Manager) Update(config *Config) { + // If Config is updated on runtime, we don't want to change the env vars. Config will be re-loaded to env var value on restart. + m.mu.Lock() + defer m.mu.Unlock() + // We are re-setting these values to their original ones, as they are not expected to be changed at runtime. + config.DBPath = m.config.DBPath + config.Port = m.config.Port + config.LoadSampleData = m.config.LoadSampleData + m.config = config +} diff --git a/src/app/managing/pairManager.go b/src/app/managing/pairManager.go index b4f4be3..acfed7a 100644 --- a/src/app/managing/pairManager.go +++ b/src/app/managing/pairManager.go @@ -27,12 +27,12 @@ type CreatePairReq struct { } type PairsManager struct { - config *config.ConfigManager + config *config.Manager assets pnl.Assets tradingPairs pnl.Pairs } -func NewPairManager(cfg *config.ConfigManager, a pnl.Assets, tp pnl.Pairs) *PairsManager { +func NewPairManager(cfg *config.Manager, a pnl.Assets, tp pnl.Pairs) *PairsManager { return &PairsManager{cfg, a, tp} } @@ -65,7 +65,7 @@ func (tpm *PairsManager) Create(req CreatePairReq) (*CreatePairResp, error) { } // // Uncommon pairs validation - if !tpm.config.GetUncommonPairs() { + if !tpm.config.Get().UncommonPairs { bt := base.Type // Assuming these are strings like "Crypto", "Forex", etc. qt := quote.Type valid := (bt == "Crypto" && qt == "Crypto") || diff --git a/src/gateways/sqlite/sqlite.go b/src/gateways/sqlite/sqlite.go index 11e19d7..f94b844 100644 --- a/src/gateways/sqlite/sqlite.go +++ b/src/gateways/sqlite/sqlite.go @@ -1,11 +1,11 @@ package sqlite import ( + "controtto/src/app/config" "controtto/src/domain/pnl" "database/sql" "fmt" "log/slog" - "os" _ "github.com/mattn/go-sqlite3" ) @@ -156,14 +156,15 @@ type SQLiteStorage struct { } // NewStorage creates a new instance of SQLite. -func NewSQLite(dbPath string) (*SQLiteStorage, error) { - db, err := sql.Open("sqlite3", dbPath) +func NewSQLite(cfg *config.Manager) (*SQLiteStorage, error) { + db, err := sql.Open("sqlite3", cfg.Get().DBPath) if err != nil { return nil, err } // Create the Pair and Trade tables if they don't exist. qString := tables - if os.Getenv("LOAD_SAMPLE_DATA") == "true" { + if cfg.Get().LoadSampleData { + slog.Info("Loading sample data into the database.") qString += demo } _, err = db.Exec(qString) diff --git a/src/rest/routes.go b/src/rest/routes.go index 34ca0b6..08e1664 100644 --- a/src/rest/routes.go +++ b/src/rest/routes.go @@ -5,13 +5,14 @@ import ( "controtto/src/app/managing" "controtto/src/app/querying" "controtto/src/app/trading" + "fmt" "log" "github.com/gofiber/fiber/v2" "github.com/gofiber/template/html/v2" ) -func Run(c *config.Service, m *managing.Service, q *querying.Service, t *trading.Service) { +func Run(c *config.Manager, m *managing.Service, q *querying.Service, t *trading.Service) { engine := html.New("./views", ".html") engine.Debug(true) app := fiber.New(fiber.Config{ @@ -42,7 +43,7 @@ func Run(c *config.Service, m *managing.Service, q *querying.Service, t *trading app.Get("/ui/pairs/:id/newTrade/form", newTradeForm(q.PairQuerier, m.PriceProviderManager)) // In rest/rest.go, add this line to the GET section: app.Get("/pairs/:id/market/:mktkey/trades", fetchMarketTrades(t.AssetTrader, m.MarketManager)) - app.Get("/settings", settingsSection(m.PriceProviderManager, m.MarketManager, c.ConfigManager)) + app.Get("/settings", settingsSection(m.PriceProviderManager, m.MarketManager, c)) // app.Get("/settings/anyMarket", marketsSetAPI(c.ConfigManager)) // app.Get("/markets", marketsSection(m.MarketManager)) app.Get("/ui/pairs/:id/market", getMarketAssets(q.PairQuerier, m.MarketManager)) @@ -57,7 +58,7 @@ func Run(c *config.Service, m *managing.Service, q *querying.Service, t *trading app.Post("/pairs/:id/trades", newTrade(t.TradeRecorder)) app.Post("/pairs/:id/trades/mktImport", importMarketTrades(t.TradeRecorder)) app.Post("/pairs/:id/trades/csvImport", newTradeImport(t.TradeRecorder)) - app.Post("/settings/edit", saveSettingsForm(m.PriceProviderManager, m.MarketManager, c.ConfigManager)) + app.Post("/settings/edit", saveSettingsForm(m.PriceProviderManager, m.MarketManager, c)) - log.Fatal(app.Listen("0.0.0.0" + ":" + c.ConfigManager.GetPort())) + log.Fatal(app.Listen("0.0.0.0" + ":" + fmt.Sprint(c.Get().Port))) } diff --git a/src/rest/settingsHandlers.go b/src/rest/settingsHandlers.go index cfdd4b8..9d36a3c 100644 --- a/src/rest/settingsHandlers.go +++ b/src/rest/settingsHandlers.go @@ -19,7 +19,7 @@ import ( // return c.SendString(fmt.Sprintf("%t", marketsSet(marketManager))) // } // } -func saveSettingsForm(priceProviderManager *managing.PriceProviderManager, marketManager *managing.MarketManager, cfg *config.ConfigManager) func(*fiber.Ctx) error { +func saveSettingsForm(priceProviderManager *managing.PriceProviderManager, marketManager *managing.MarketManager, cfg *config.Manager) func(*fiber.Ctx) error { return func(c *fiber.Ctx) error { // Update price providers providers := priceProviderManager.ListProviders(true) // true = get all providers @@ -58,7 +58,9 @@ func saveSettingsForm(priceProviderManager *managing.PriceProviderManager, marke // Update uncommon pairs setting uncommon := c.FormValue("uncommon_pairs") != "" - cfg.SetUncommonPairs(uncommon) + cfg.Update(&config.Config{ + UncommonPairs: uncommon, + }) c.Append("HX-Trigger", "reloadSettings") slog.Info("Config updated successfully") diff --git a/src/rest/uiHandlers.go b/src/rest/uiHandlers.go index 4309629..0af8fa9 100644 --- a/src/rest/uiHandlers.go +++ b/src/rest/uiHandlers.go @@ -72,7 +72,7 @@ func pairSection() func(*fiber.Ctx) error { } } -func settingsSection(priceProviderManager *managing.PriceProviderManager, marketManager *managing.MarketManager, cfg *config.ConfigManager) func(*fiber.Ctx) error { +func settingsSection(priceProviderManager *managing.PriceProviderManager, marketManager *managing.MarketManager, cfg *config.Manager) func(*fiber.Ctx) error { return func(c *fiber.Ctx) error { if c.Get("HX-Request") != "true" { return c.Render("main", fiber.Map{ @@ -86,7 +86,7 @@ func settingsSection(priceProviderManager *managing.PriceProviderManager, market return c.Render("settingsSection", fiber.Map{ "Today": time.Now().Format("Mon Jan 02 15:04 2006"), - "Uncommon": cfg.GetUncommonPairs(), + "Uncommon": cfg.Get().UncommonPairs, "PriceProviders": providers, "MarketTraders": traders, })