From 2b6b1c2a00ccb2d4753f636e091f65d571fb1dcd Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 16 Jul 2025 09:01:33 +0300 Subject: [PATCH 1/3] Implement comprehensive configuration system with database-agnostic namespaced structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add config package with JSON file storage in ~/.pho/config.json - Support namespaced config keys (mongo.*, postgres.*, app.*, etc) - Implement config CLI commands: get, set, list - Add environment variable override support for all config settings - Integrate config defaults with CLI flags and environment sources - Design future-proof architecture for PostgreSQL support - Add comprehensive test coverage for config functionality - Maintain backward compatibility with existing MongoDB workflow Features: • Configuration file: ~/.pho/config.json with proper permissions • CLI commands: pho config get/set/list with validation and aliases • Environment override: All settings can be overridden via PHO_* env vars • Database agnostic: Separate mongo.* and postgres.* namespaces • Full integration: CLI flags use config defaults with env var precedence • Type safety: Proper validation for timeouts, limits, boolean values • Extensible: Easy to add new config sections and settings --- cmd/pho/main.go | 18 +- internal/app/app.go | 246 +++++++++++++++-- internal/app/app_test.go | 2 +- internal/config/config.go | 492 +++++++++++++++++++++++++++++++++ internal/config/config_test.go | 200 ++++++++++++++ 5 files changed, 932 insertions(+), 26 deletions(-) create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go diff --git a/cmd/pho/main.go b/cmd/pho/main.go index ab7085c..a7a142a 100644 --- a/cmd/pho/main.go +++ b/cmd/pho/main.go @@ -6,6 +6,7 @@ import ( "os" "os/signal" "pho/internal/app" + "pho/internal/config" "syscall" "time" ) @@ -14,18 +15,17 @@ func main() { os.Exit(run()) } -// getTimeout returns the configured timeout from environment variable or default. +// getTimeout returns the configured timeout from config or environment variable. func getTimeout() time.Duration { - const defaultTimeout = 60 * time.Second - - if timeoutStr := os.Getenv("PHO_TIMEOUT"); timeoutStr != "" { - if timeout, err := time.ParseDuration(timeoutStr); err == nil { - return timeout - } - // If parsing fails, fall back to default + // Load config to get timeout + cfg, err := config.Load() + if err != nil { + // Fallback to default if config loading fails + const defaultTimeoutSeconds = 60 + return defaultTimeoutSeconds * time.Second } - return defaultTimeout + return cfg.GetTimeoutDuration() } func run() int { diff --git a/internal/app/app.go b/internal/app/app.go index d26c387..c0c9bc0 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "os/signal" + "pho/internal/config" "pho/internal/logging" "pho/internal/pho" "pho/internal/render" @@ -15,10 +16,6 @@ import ( "github.com/urfave/cli/v3" ) -const ( - defaultDocumentLimit = 10000 // Default limit for document retrieval -) - var ( // Version is injected via ldflags during build. Version = "dev" @@ -26,12 +23,21 @@ var ( // App represents the CLI application. type App struct { - cmd *cli.Command + cmd *cli.Command + config *config.Config } // New creates a new CLI application. func New() *App { + // Load configuration + cfg, err := config.Load() + if err != nil { + // Use default config if loading fails + cfg = config.NewDefault() + } + return &App{ + config: cfg, cmd: &cli.Command{ Name: "pho", Usage: "MongoDB document editor - query, edit, and apply changes interactively", @@ -102,6 +108,44 @@ This will execute the actual database operations.`, Action: applyAction, Flags: getConnectionFlags(), }, + { + Name: "config", + Aliases: []string{"cfg"}, + Usage: "Manage pho configuration", + Description: `Manage pho configuration settings. +Configuration is stored in ~/.pho/config.json and can be overridden by environment variables.`, + Commands: []*cli.Command{ + { + Name: "get", + Aliases: []string{"g"}, + Usage: "Get configuration value", + Description: `Get a configuration value by key. +Examples: + pho config get mongo.uri + pho config get app.editor + pho config get output.format`, + Action: configGetAction, + }, + { + Name: "set", + Aliases: []string{"s"}, + Usage: "Set configuration value", + Description: `Set a configuration value by key. +Examples: + pho config set mongo.uri mongodb://localhost:27017 + pho config set app.editor nano + pho config set output.format json`, + Action: configSetAction, + }, + { + Name: "list", + Aliases: []string{"ls"}, + Usage: "List all configuration values", + Description: `List all current configuration values.`, + Action: configListAction, + }, + }, + }, }, Flags: getCommonFlags(), Action: defaultAction, // Default action when no subcommand is specified @@ -118,74 +162,104 @@ func (a *App) Run(ctx context.Context, args []string) error { // getRenderFlags returns common flags used for output rendering. func getRenderFlags() []cli.Flag { + // Load config to get defaults + cfg, _ := config.Load() + if cfg == nil { + cfg = config.NewDefault() + } + return []cli.Flag{ &cli.StringFlag{ Name: "extjson-mode", Aliases: []string{"m"}, - Value: "canonical", + Value: cfg.Mongo.ExtJSONMode, Usage: "ExtJSON output mode: canonical, relaxed, or shell", + Sources: cli.EnvVars("PHO_EXTJSON_MODE"), }, &cli.BoolFlag{ Name: "compact", Aliases: []string{"C"}, + Value: cfg.Output.Compact, Usage: "Use compact JSON output (no indentation)", + Sources: cli.EnvVars("PHO_OUTPUT_COMPACT"), }, &cli.BoolFlag{ Name: "line-numbers", Aliases: []string{"n"}, - Value: true, + Value: cfg.Output.LineNumbers, Usage: "Show line numbers in output", + Sources: cli.EnvVars("PHO_OUTPUT_LINE_NUMBERS"), }, } } // getVerbosityFlags returns common flags used for verbosity control. func getVerbosityFlags() []cli.Flag { + // Load config to get defaults + cfg, _ := config.Load() + if cfg == nil { + cfg = config.NewDefault() + } + return []cli.Flag{ &cli.BoolFlag{ Name: "verbose", Aliases: []string{"v"}, + Value: cfg.Output.Verbose, Usage: "Enable verbose output with detailed progress information", + Sources: cli.EnvVars("PHO_OUTPUT_VERBOSE"), }, &cli.BoolFlag{ Name: "quiet", Aliases: []string{"Q"}, + Value: cfg.Output.Quiet, Usage: "Suppress all non-essential output (quiet mode)", + Sources: cli.EnvVars("PHO_OUTPUT_QUIET"), }, } } // getConnectionFlags returns flags for MongoDB connection. func getConnectionFlags() []cli.Flag { + // Load config to get defaults + cfg, _ := config.Load() + if cfg == nil { + cfg = config.NewDefault() + } + return []cli.Flag{ &cli.StringFlag{ Name: "uri", Aliases: []string{"u"}, - Value: "mongodb://localhost:27017", + Value: cfg.Mongo.URI, Usage: "MongoDB URI Connection String", Sources: cli.EnvVars("MONGODB_URI"), }, &cli.StringFlag{ Name: "host", Aliases: []string{"H"}, + Value: cfg.Mongo.Host, Usage: "MongoDB hostname (alternative to --uri)", Sources: cli.EnvVars("MONGODB_HOST"), }, &cli.StringFlag{ Name: "port", Aliases: []string{"P"}, + Value: cfg.Mongo.Port, Usage: "MongoDB port (used with --host)", Sources: cli.EnvVars("MONGODB_PORT"), }, &cli.StringFlag{ Name: "db", Aliases: []string{"d"}, + Value: cfg.Mongo.Database, Usage: "MongoDB database name", Sources: cli.EnvVars("MONGODB_DB"), }, &cli.StringFlag{ Name: "collection", Aliases: []string{"c"}, + Value: cfg.Mongo.Collection, Usage: "MongoDB collection name", Sources: cli.EnvVars("MONGODB_COLLECTION"), }, @@ -194,12 +268,19 @@ func getConnectionFlags() []cli.Flag { // getEditFlags returns flags for the edit command. func getEditFlags() []cli.Flag { + // Load config to get defaults + cfg, _ := config.Load() + if cfg == nil { + cfg = config.NewDefault() + } + editorFlags := []cli.Flag{ &cli.StringFlag{ Name: "editor", Aliases: []string{"e"}, - Value: "vim", + Value: cfg.App.Editor, Usage: "Editor command to use for editing documents", + Sources: cli.EnvVars("PHO_EDITOR"), }, } @@ -217,33 +298,46 @@ func getReviewFlags() []cli.Flag { // getCommonFlags returns all flags including connection and query flags. func getCommonFlags() []cli.Flag { + // Load config to get defaults + cfg, _ := config.Load() + if cfg == nil { + cfg = config.NewDefault() + } + connectionFlags := getConnectionFlags() queryFlags := []cli.Flag{ &cli.StringFlag{ Name: "query", Aliases: []string{"q"}, - Value: "{}", + Value: cfg.Query.Query, Usage: "MongoDB query as a JSON document", + Sources: cli.EnvVars("PHO_QUERY"), }, &cli.Int64Flag{ Name: "limit", Aliases: []string{"l"}, - Value: defaultDocumentLimit, + Value: cfg.Query.Limit, Usage: "Maximum number of documents to retrieve", + Sources: cli.EnvVars("PHO_LIMIT"), }, &cli.StringFlag{ - Name: "sort", - Usage: "Sort order for documents (JSON format, e.g. '{\"_id\": 1}')", + Name: "sort", + Value: cfg.Query.Sort, + Usage: "Sort order for documents (JSON format, e.g. '{\"_id\": 1}')", + Sources: cli.EnvVars("PHO_SORT"), }, &cli.StringFlag{ - Name: "projection", - Usage: "Projection for documents (JSON format, e.g. '{\"field\": 1}')", + Name: "projection", + Value: cfg.Query.Projection, + Usage: "Projection for documents (JSON format, e.g. '{\"field\": 1}')", + Sources: cli.EnvVars("PHO_PROJECTION"), }, &cli.StringFlag{ Name: "editor", Aliases: []string{"e"}, - Value: "vim", + Value: cfg.App.Editor, Usage: "Editor command to use for editing documents", + Sources: cli.EnvVars("PHO_EDITOR"), }, &cli.BoolFlag{ Name: "edit", @@ -763,6 +857,7 @@ func defaultAction(ctx context.Context, cmd *cli.Command) error { fmt.Fprintf(os.Stderr, " edit Edit documents from previous query\n") fmt.Fprintf(os.Stderr, " review Review changes made to documents\n") fmt.Fprintf(os.Stderr, " apply Apply changes to MongoDB\n") + fmt.Fprintf(os.Stderr, " config Manage pho configuration\n") fmt.Fprintf(os.Stderr, " version Show version information\n\n") fmt.Fprintf(os.Stderr, "Run 'pho --help' for detailed usage information.\n") return errors.New("database name is required") @@ -771,3 +866,122 @@ func defaultAction(ctx context.Context, cmd *cli.Command) error { // If database is specified, run the query action (default behavior) return queryAction(ctx, cmd) } + +// configGetAction handles the config get command. +func configGetAction(ctx context.Context, cmd *cli.Command) error { + _, _ = ctx, cmd + + args := cmd.Args() + if args.Len() == 0 { + fmt.Fprintf(os.Stderr, "Error: config key is required\n") + fmt.Fprintf(os.Stderr, "Usage: pho config get \n") + fmt.Fprintf(os.Stderr, "Example: pho config get mongo.uri\n") + return errors.New("config key is required") + } + + key := args.First() + + cfg, err := config.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + return err + } + + value, err := cfg.Get(key) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting config value: %v\n", err) + return err + } + + fmt.Fprintf(os.Stdout, "%v\n", value) + return nil +} + +// configSetAction handles the config set command. +func configSetAction(ctx context.Context, cmd *cli.Command) error { + _, _ = ctx, cmd + + args := cmd.Args() + if args.Len() < 2 { + fmt.Fprintf(os.Stderr, "Error: config key and value are required\n") + fmt.Fprintf(os.Stderr, "Usage: pho config set \n") + fmt.Fprintf(os.Stderr, "Example: pho config set mongo.uri mongodb://localhost:27017\n") + return errors.New("config key and value are required") + } + + key := args.Get(0) + value := args.Get(1) + + cfg, err := config.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + return err + } + + if err := cfg.Set(key, value); err != nil { + fmt.Fprintf(os.Stderr, "Error setting config value: %v\n", err) + return err + } + + if err := cfg.Save(); err != nil { + fmt.Fprintf(os.Stderr, "Error saving config: %v\n", err) + return err + } + + fmt.Fprintf(os.Stdout, "Set %s = %s\n", key, value) + return nil +} + +// configListAction handles the config list command. +func configListAction(ctx context.Context, cmd *cli.Command) error { + _, _ = ctx, cmd + + cfg, err := config.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + return err + } + + fmt.Fprintf(os.Stdout, "Current configuration:\n\n") + + categories := map[string][]string{ + "MongoDB": { + "mongo.uri", "mongo.host", "mongo.port", "mongo.database", "mongo.collection", "mongo.extjson_mode", + }, + "PostgreSQL": { + "postgres.host", "postgres.port", "postgres.database", "postgres.schema", "postgres.user", "postgres.ssl_mode", + }, + "Database": { + "database.type", + }, + "Query": { + "query.query", "query.limit", "query.sort", "query.projection", + }, + "Application": { + "app.editor", "app.timeout", + }, + "Output": { + "output.format", "output.line_numbers", "output.compact", "output.verbose", "output.quiet", + }, + "Directories": { + "directories.data_dir", "directories.config_dir", + }, + } + + for category, categoryKeys := range categories { + fmt.Fprintf(os.Stdout, "[%s]\n", category) + for _, key := range categoryKeys { + if value, err := cfg.Get(key); err == nil { + // Show empty values as + displayValue := fmt.Sprintf("%v", value) + if displayValue == "" { + displayValue = "" + } + fmt.Fprintf(os.Stdout, " %-20s = %s\n", key, displayValue) + } + } + fmt.Fprintf(os.Stdout, "\n") + } + + return nil +} diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 1d043c6..fcb4bac 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -18,7 +18,7 @@ func TestNew(t *testing.T) { require.NotNil(t, cmd) assert.Equal(t, "pho", cmd.Name) assert.Equal(t, "MongoDB document editor - query, edit, and apply changes interactively", cmd.Usage) - assert.Len(t, cmd.Commands, 5) // version, query, edit, review, apply + assert.Len(t, cmd.Commands, 6) // version, query, edit, review, apply, config } func TestParseExtJSONMode(t *testing.T) { diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..6eb0e21 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,492 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "time" +) + +const ( + defaultLimit = 10000 // Default limit for document retrieval + defaultTimeoutSeconds = 60 // Default timeout in seconds +) + +// Config represents the application configuration. +type Config struct { + // Database-specific settings + Mongo MongoConfig `json:"mongo"` + Postgres PostgresConfig `json:"postgres"` + + // Database selection and generic query settings + Database DatabaseConfig `json:"database"` + Query QueryConfig `json:"query"` + + // Application settings + App AppConfig `json:"app"` + + // Output/Display settings + Output OutputConfig `json:"output"` + + // Directory settings + Directories DirectoriesConfig `json:"directories"` +} + +// MongoConfig contains MongoDB-specific connection settings. +type MongoConfig struct { + URI string `json:"uri"` + Host string `json:"host"` + Port string `json:"port"` + Database string `json:"database"` + Collection string `json:"collection"` + ExtJSONMode string `json:"extjson_mode"` +} + +// PostgresConfig contains PostgreSQL-specific connection settings (future). +type PostgresConfig struct { + Host string `json:"host"` + Port string `json:"port"` + Database string `json:"database"` + Schema string `json:"schema"` + User string `json:"user"` + SSLMode string `json:"ssl_mode"` +} + +// DatabaseConfig contains database type selection. +type DatabaseConfig struct { + Type string `json:"type"` // "mongodb", "postgres", etc. +} + +// QueryConfig contains database-agnostic query settings. +type QueryConfig struct { + Query string `json:"query"` + Limit int64 `json:"limit"` + Sort string `json:"sort"` + Projection string `json:"projection"` +} + +// AppConfig contains application behavior settings. +type AppConfig struct { + Editor string `json:"editor"` + Timeout string `json:"timeout"` +} + +// OutputConfig contains output formatting settings. +type OutputConfig struct { + Format string `json:"format"` // "json", "yaml", "csv", etc. + LineNumbers bool `json:"line_numbers"` + Compact bool `json:"compact"` + Verbose bool `json:"verbose"` + Quiet bool `json:"quiet"` +} + +// DirectoriesConfig contains directory path settings. +type DirectoriesConfig struct { + DataDir string `json:"data_dir"` + ConfigDir string `json:"config_dir"` +} + +// NewDefault returns a new Config with default values. +func NewDefault() *Config { + return &Config{ + Mongo: MongoConfig{ + URI: "mongodb://localhost:27017", + Host: "", + Port: "", + Database: "", + Collection: "", + ExtJSONMode: "canonical", + }, + Postgres: PostgresConfig{ + Host: "localhost", + Port: "5432", + Database: "", + Schema: "public", + User: "", + SSLMode: "prefer", + }, + Database: DatabaseConfig{ + Type: "mongodb", // Default to MongoDB for backward compatibility + }, + Query: QueryConfig{ + Query: "{}", + Limit: defaultLimit, + Sort: "", + Projection: "", + }, + App: AppConfig{ + Editor: "vim", + Timeout: "60s", + }, + Output: OutputConfig{ + Format: "json", + LineNumbers: true, + Compact: false, + Verbose: false, + Quiet: false, + }, + Directories: DirectoriesConfig{ + DataDir: "", // Will be computed dynamically if empty + ConfigDir: "", // Will be computed dynamically if empty + }, + } +} + +// Load loads configuration from file and applies environment variable overrides. +func Load() (*Config, error) { + config := NewDefault() + + // Load from config file if it exists + configPath, err := getConfigFilePath() + if err != nil { + return nil, fmt.Errorf("could not get config file path: %w", err) + } + + if data, err := os.ReadFile(configPath); err == nil { + if err := json.Unmarshal(data, config); err != nil { + return nil, fmt.Errorf("could not parse config file: %w", err) + } + } else if !os.IsNotExist(err) { + return nil, fmt.Errorf("could not read config file: %w", err) + } + + // Apply environment variable overrides + config.applyEnvironmentOverrides() + + return config, nil +} + +// Save saves the configuration to file. +func (c *Config) Save() error { + configPath, err := getConfigFilePath() + if err != nil { + return fmt.Errorf("could not get config file path: %w", err) + } + + // Ensure config directory exists + configDir := filepath.Dir(configPath) + if err := os.MkdirAll(configDir, 0750); err != nil { + return fmt.Errorf("could not create config directory: %w", err) + } + + data, err := json.MarshalIndent(c, "", " ") + if err != nil { + return fmt.Errorf("could not marshal config: %w", err) + } + + if err := os.WriteFile(configPath, data, 0600); err != nil { + return fmt.Errorf("could not write config file: %w", err) + } + + return nil +} + +// applyEnvironmentOverrides applies environment variable overrides to the config. +func (c *Config) applyEnvironmentOverrides() { + // MongoDB settings + if val := os.Getenv("MONGODB_URI"); val != "" { + c.Mongo.URI = val + } + if val := os.Getenv("MONGODB_HOST"); val != "" { + c.Mongo.Host = val + } + if val := os.Getenv("MONGODB_PORT"); val != "" { + c.Mongo.Port = val + } + if val := os.Getenv("MONGODB_DB"); val != "" { + c.Mongo.Database = val + } + if val := os.Getenv("MONGODB_COLLECTION"); val != "" { + c.Mongo.Collection = val + } + + // PostgreSQL settings (for future use) + if val := os.Getenv("POSTGRES_HOST"); val != "" { + c.Postgres.Host = val + } + if val := os.Getenv("POSTGRES_PORT"); val != "" { + c.Postgres.Port = val + } + if val := os.Getenv("POSTGRES_DB"); val != "" { + c.Postgres.Database = val + } + if val := os.Getenv("POSTGRES_USER"); val != "" { + c.Postgres.User = val + } + if val := os.Getenv("POSTGRES_SCHEMA"); val != "" { + c.Postgres.Schema = val + } + + // Database type + if val := os.Getenv("PHO_DATABASE_TYPE"); val != "" { + c.Database.Type = val + } + + // Query settings + if val := os.Getenv("PHO_QUERY"); val != "" { + c.Query.Query = val + } + if val := os.Getenv("PHO_LIMIT"); val != "" { + if limit, err := strconv.ParseInt(val, 10, 64); err == nil { + c.Query.Limit = limit + } + } + if val := os.Getenv("PHO_SORT"); val != "" { + c.Query.Sort = val + } + if val := os.Getenv("PHO_PROJECTION"); val != "" { + c.Query.Projection = val + } + + // App settings + if val := os.Getenv("PHO_EDITOR"); val != "" { + c.App.Editor = val + } + if val := os.Getenv("PHO_TIMEOUT"); val != "" { + c.App.Timeout = val + } + + // MongoDB specific + if val := os.Getenv("PHO_EXTJSON_MODE"); val != "" { + c.Mongo.ExtJSONMode = val + } + + // Directory settings + if val := os.Getenv("PHO_DATA_DIR"); val != "" { + c.Directories.DataDir = val + } + if val := os.Getenv("PHO_CONFIG_DIR"); val != "" { + c.Directories.ConfigDir = val + } + + // Output settings + if val := os.Getenv("PHO_OUTPUT_COMPACT"); val != "" { + if compact, err := strconv.ParseBool(val); err == nil { + c.Output.Compact = compact + } + } + if val := os.Getenv("PHO_OUTPUT_LINE_NUMBERS"); val != "" { + if lineNumbers, err := strconv.ParseBool(val); err == nil { + c.Output.LineNumbers = lineNumbers + } + } + if val := os.Getenv("PHO_OUTPUT_VERBOSE"); val != "" { + if verbose, err := strconv.ParseBool(val); err == nil { + c.Output.Verbose = verbose + } + } + if val := os.Getenv("PHO_OUTPUT_QUIET"); val != "" { + if quiet, err := strconv.ParseBool(val); err == nil { + c.Output.Quiet = quiet + } + } +} + +// Set sets a configuration value by key path (e.g., "mongo.uri", "app.editor"). +func (c *Config) Set(key, value string) error { + switch key { + // MongoDB settings + case "mongo.uri": + c.Mongo.URI = value + case "mongo.host": + c.Mongo.Host = value + case "mongo.port": + c.Mongo.Port = value + case "mongo.database", "mongo.db": + c.Mongo.Database = value + case "mongo.collection": + c.Mongo.Collection = value + case "mongo.extjson_mode", "mongo.extjson-mode": + if value != "canonical" && value != "relaxed" && value != "shell" { + return fmt.Errorf("invalid extjson mode: %s (valid: canonical, relaxed, shell)", value) + } + c.Mongo.ExtJSONMode = value + + // PostgreSQL settings (future) + case "postgres.host": + c.Postgres.Host = value + case "postgres.port": + c.Postgres.Port = value + case "postgres.database", "postgres.db": + c.Postgres.Database = value + case "postgres.schema": + c.Postgres.Schema = value + case "postgres.user": + c.Postgres.User = value + case "postgres.ssl_mode", "postgres.ssl-mode": + c.Postgres.SSLMode = value + + // Database selection + case "database.type": + if value != "mongodb" && value != "postgres" { + return fmt.Errorf("invalid database type: %s (valid: mongodb, postgres)", value) + } + c.Database.Type = value + + // Query settings + case "query.query": + c.Query.Query = value + case "query.limit": + limit, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return fmt.Errorf("invalid limit value: %w", err) + } + c.Query.Limit = limit + case "query.sort": + c.Query.Sort = value + case "query.projection": + c.Query.Projection = value + + // App settings + case "app.editor": + c.App.Editor = value + case "app.timeout": + // Validate the duration format + if _, err := time.ParseDuration(value); err != nil { + return fmt.Errorf("invalid timeout duration: %w", err) + } + c.App.Timeout = value + + // Output settings + case "output.format": + // Accept common format types + if value != "json" && value != "yaml" && value != "csv" { + return fmt.Errorf("invalid format: %s (valid: json, yaml, csv)", value) + } + c.Output.Format = value + case "output.line_numbers", "output.line-numbers": + val, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("invalid boolean value: %w", err) + } + c.Output.LineNumbers = val + case "output.compact": + val, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("invalid boolean value: %w", err) + } + c.Output.Compact = val + case "output.verbose": + val, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("invalid boolean value: %w", err) + } + c.Output.Verbose = val + case "output.quiet": + val, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("invalid boolean value: %w", err) + } + c.Output.Quiet = val + + // Directory settings + case "directories.data_dir", "directories.data-dir": + c.Directories.DataDir = value + case "directories.config_dir", "directories.config-dir": + c.Directories.ConfigDir = value + + default: + return fmt.Errorf("unknown config key: %s", key) + } + + return nil +} + +// Get gets a configuration value by key path. +func (c *Config) Get(key string) (interface{}, error) { + switch key { + // MongoDB settings + case "mongo.uri": + return c.Mongo.URI, nil + case "mongo.host": + return c.Mongo.Host, nil + case "mongo.port": + return c.Mongo.Port, nil + case "mongo.database", "mongo.db": + return c.Mongo.Database, nil + case "mongo.collection": + return c.Mongo.Collection, nil + case "mongo.extjson_mode", "mongo.extjson-mode": + return c.Mongo.ExtJSONMode, nil + + // PostgreSQL settings (future) + case "postgres.host": + return c.Postgres.Host, nil + case "postgres.port": + return c.Postgres.Port, nil + case "postgres.database", "postgres.db": + return c.Postgres.Database, nil + case "postgres.schema": + return c.Postgres.Schema, nil + case "postgres.user": + return c.Postgres.User, nil + case "postgres.ssl_mode", "postgres.ssl-mode": + return c.Postgres.SSLMode, nil + + // Database selection + case "database.type": + return c.Database.Type, nil + + // Query settings + case "query.query": + return c.Query.Query, nil + case "query.limit": + return c.Query.Limit, nil + case "query.sort": + return c.Query.Sort, nil + case "query.projection": + return c.Query.Projection, nil + + // App settings + case "app.editor": + return c.App.Editor, nil + case "app.timeout": + return c.App.Timeout, nil + + // Output settings + case "output.format": + return c.Output.Format, nil + case "output.line_numbers", "output.line-numbers": + return c.Output.LineNumbers, nil + case "output.compact": + return c.Output.Compact, nil + case "output.verbose": + return c.Output.Verbose, nil + case "output.quiet": + return c.Output.Quiet, nil + + // Directory settings + case "directories.data_dir", "directories.data-dir": + return c.Directories.DataDir, nil + case "directories.config_dir", "directories.config-dir": + return c.Directories.ConfigDir, nil + + default: + return nil, fmt.Errorf("unknown config key: %s", key) + } +} + +// getConfigFilePath returns the path to the configuration file. +func getConfigFilePath() (string, error) { + configDir := os.Getenv("PHO_CONFIG_DIR") + if configDir == "" { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("could not get user home directory: %w", err) + } + configDir = filepath.Join(homeDir, ".pho") + } + + return filepath.Join(configDir, "config.json"), nil +} + +// GetTimeoutDuration returns the timeout as a time.Duration. +func (c *Config) GetTimeoutDuration() time.Duration { + if timeout, err := time.ParseDuration(c.App.Timeout); err == nil { + return timeout + } + // Fallback to default if parsing fails + return defaultTimeoutSeconds * time.Second +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..e780c6f --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,200 @@ +package config_test + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "pho/internal/config" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewDefault(t *testing.T) { + cfg := config.NewDefault() + + // MongoDB config + assert.Equal(t, "mongodb://localhost:27017", cfg.Mongo.URI) + assert.Equal(t, "canonical", cfg.Mongo.ExtJSONMode) + + // PostgreSQL config + assert.Equal(t, "localhost", cfg.Postgres.Host) + assert.Equal(t, "5432", cfg.Postgres.Port) + assert.Equal(t, "public", cfg.Postgres.Schema) + assert.Equal(t, "prefer", cfg.Postgres.SSLMode) + + // Database selection + assert.Equal(t, "mongodb", cfg.Database.Type) + + // Query config + assert.Equal(t, "{}", cfg.Query.Query) + assert.Equal(t, int64(10000), cfg.Query.Limit) + + // App config + assert.Equal(t, "vim", cfg.App.Editor) + assert.Equal(t, "60s", cfg.App.Timeout) + + // Output config + assert.Equal(t, "json", cfg.Output.Format) + assert.True(t, cfg.Output.LineNumbers) + assert.False(t, cfg.Output.Compact) + assert.False(t, cfg.Output.Verbose) + assert.False(t, cfg.Output.Quiet) +} + +func TestConfig_SetAndGet(t *testing.T) { + cfg := config.NewDefault() + + tests := []struct { + key string + setValue string + getValue interface{} + }{ + {"mongo.uri", "mongodb://example.com:27017", "mongodb://example.com:27017"}, + {"mongo.database", "testdb", "testdb"}, + {"mongo.extjson_mode", "relaxed", "relaxed"}, + {"postgres.host", "localhost", "localhost"}, + {"postgres.database", "testdb", "testdb"}, + {"database.type", "postgres", "postgres"}, + {"query.query", "{\"test\": 1}", "{\"test\": 1}"}, + {"query.limit", "5000", int64(5000)}, + {"app.editor", "nano", "nano"}, + {"app.timeout", "30s", "30s"}, + {"output.format", "yaml", "yaml"}, + {"output.line_numbers", "false", false}, + {"output.compact", "true", true}, + } + + for _, tt := range tests { + t.Run(tt.key, func(t *testing.T) { + err := cfg.Set(tt.key, tt.setValue) + require.NoError(t, err) + + value, err := cfg.Get(tt.key) + require.NoError(t, err) + assert.Equal(t, tt.getValue, value) + }) + } +} + +func TestConfig_SetInvalidValues(t *testing.T) { + cfg := config.NewDefault() + + tests := []struct { + key string + value string + }{ + {"app.timeout", "invalid"}, + {"query.limit", "not-a-number"}, + {"mongo.extjson_mode", "invalid"}, + {"output.format", "invalid"}, + {"database.type", "invalid"}, + {"output.line_numbers", "not-bool"}, + {"unknown.key", "value"}, + } + + for _, tt := range tests { + t.Run(tt.key, func(t *testing.T) { + err := cfg.Set(tt.key, tt.value) + assert.Error(t, err) + }) + } +} + +func TestConfig_GetInvalidKey(t *testing.T) { + cfg := config.NewDefault() + + _, err := cfg.Get("unknown.key") + assert.Error(t, err) +} + +func TestConfig_SaveAndLoad(t *testing.T) { + // Create temporary directory for test + tempDir := t.TempDir() + + // Set PHO_CONFIG_DIR to temp directory + t.Setenv("PHO_CONFIG_DIR", tempDir) + + // Create and save config + cfg := config.NewDefault() + cfg.Mongo.URI = "mongodb://test:27017" + cfg.App.Editor = "emacs" + cfg.Output.Format = "yaml" + + err := cfg.Save() + require.NoError(t, err) + + // Verify file was created + configPath := filepath.Join(tempDir, "config.json") + assert.FileExists(t, configPath) + + // Load config + loadedCfg, err := config.Load() + require.NoError(t, err) + + assert.Equal(t, "mongodb://test:27017", loadedCfg.Mongo.URI) + assert.Equal(t, "emacs", loadedCfg.App.Editor) + assert.Equal(t, "yaml", loadedCfg.Output.Format) +} + +func TestConfig_EnvironmentOverrides(t *testing.T) { + // Create temporary directory for test + tempDir := t.TempDir() + + // Set test environment variables + envVars := map[string]string{ + "PHO_CONFIG_DIR": tempDir, + "MONGODB_URI": "mongodb://env:27017", + "MONGODB_DB": "envdb", + "PHO_TIMEOUT": "120s", + } + + // Set environment variables + for key, value := range envVars { + t.Setenv(key, value) + } + + // Create config file with different values + configPath := filepath.Join(tempDir, "config.json") + configData := map[string]interface{}{ + "mongo": map[string]interface{}{ + "uri": "mongodb://file:27017", + "database": "filedb", + }, + "app": map[string]interface{}{ + "timeout": "60s", + }, + } + + data, err := json.Marshal(configData) + require.NoError(t, err) + + err = os.WriteFile(configPath, data, 0600) + require.NoError(t, err) + + // Load config - environment should override file + cfg, err := config.Load() + require.NoError(t, err) + + assert.Equal(t, "mongodb://env:27017", cfg.Mongo.URI) // from env + assert.Equal(t, "envdb", cfg.Mongo.Database) // from env + assert.Equal(t, "120s", cfg.App.Timeout) // from env +} + +func TestConfig_LoadNonExistentFile(t *testing.T) { + // Create temporary directory for test + tempDir := t.TempDir() + + // Set PHO_CONFIG_DIR to temp directory (no config file) + t.Setenv("PHO_CONFIG_DIR", tempDir) + + // Load should succeed with defaults + cfg, err := config.Load() + require.NoError(t, err) + + // Should have default values + assert.Equal(t, "mongodb://localhost:27017", cfg.Mongo.URI) + assert.Equal(t, "vim", cfg.App.Editor) +} From 373ad2d4d7188cd34ef6d01d898df53e7197fb52 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 16 Jul 2025 10:28:16 +0300 Subject: [PATCH 2/3] Migrate configuration format from JSON to TOML and update storage path - Replace JSON serialization with TOML using github.com/BurntSushi/toml - Update all struct tags from json to toml format - Change configuration file from config.json to config.toml - Move config directory from ~/.pho to ~/.config/pho following XDG spec - Update config parsing and marshaling logic accordingly - Update tests to use TOML format and new file paths - All functionality remains the same, only storage format changed --- go.mod | 1 + go.sum | 2 + internal/config/config.go | 77 +++++++++++++++++----------------- internal/config/config_test.go | 8 ++-- 4 files changed, 46 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index 41733f3..258c0c4 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24 toolchain go1.24.3 require ( + github.com/BurntSushi/toml v1.5.0 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.3.8 go.mongodb.org/mongo-driver v1.17.4 // latest diff --git a/go.sum b/go.sum index 84606c7..ffa68fd 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= diff --git a/internal/config/config.go b/internal/config/config.go index 6eb0e21..597680f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,12 +1,13 @@ package config import ( - "encoding/json" "fmt" "os" "path/filepath" "strconv" "time" + + "github.com/BurntSushi/toml" ) const ( @@ -17,75 +18,75 @@ const ( // Config represents the application configuration. type Config struct { // Database-specific settings - Mongo MongoConfig `json:"mongo"` - Postgres PostgresConfig `json:"postgres"` + Mongo MongoConfig `toml:"mongo"` + Postgres PostgresConfig `toml:"postgres"` // Database selection and generic query settings - Database DatabaseConfig `json:"database"` - Query QueryConfig `json:"query"` + Database DatabaseConfig `toml:"database"` + Query QueryConfig `toml:"query"` // Application settings - App AppConfig `json:"app"` + App AppConfig `toml:"app"` // Output/Display settings - Output OutputConfig `json:"output"` + Output OutputConfig `toml:"output"` // Directory settings - Directories DirectoriesConfig `json:"directories"` + Directories DirectoriesConfig `toml:"directories"` } // MongoConfig contains MongoDB-specific connection settings. type MongoConfig struct { - URI string `json:"uri"` - Host string `json:"host"` - Port string `json:"port"` - Database string `json:"database"` - Collection string `json:"collection"` - ExtJSONMode string `json:"extjson_mode"` + URI string `toml:"uri"` + Host string `toml:"host"` + Port string `toml:"port"` + Database string `toml:"database"` + Collection string `toml:"collection"` + ExtJSONMode string `toml:"extjson_mode"` } // PostgresConfig contains PostgreSQL-specific connection settings (future). type PostgresConfig struct { - Host string `json:"host"` - Port string `json:"port"` - Database string `json:"database"` - Schema string `json:"schema"` - User string `json:"user"` - SSLMode string `json:"ssl_mode"` + Host string `toml:"host"` + Port string `toml:"port"` + Database string `toml:"database"` + Schema string `toml:"schema"` + User string `toml:"user"` + SSLMode string `toml:"ssl_mode"` } // DatabaseConfig contains database type selection. type DatabaseConfig struct { - Type string `json:"type"` // "mongodb", "postgres", etc. + Type string `toml:"type"` // "mongodb", "postgres", etc. } // QueryConfig contains database-agnostic query settings. type QueryConfig struct { - Query string `json:"query"` - Limit int64 `json:"limit"` - Sort string `json:"sort"` - Projection string `json:"projection"` + Query string `toml:"query"` + Limit int64 `toml:"limit"` + Sort string `toml:"sort"` + Projection string `toml:"projection"` } // AppConfig contains application behavior settings. type AppConfig struct { - Editor string `json:"editor"` - Timeout string `json:"timeout"` + Editor string `toml:"editor"` + Timeout string `toml:"timeout"` } // OutputConfig contains output formatting settings. type OutputConfig struct { - Format string `json:"format"` // "json", "yaml", "csv", etc. - LineNumbers bool `json:"line_numbers"` - Compact bool `json:"compact"` - Verbose bool `json:"verbose"` - Quiet bool `json:"quiet"` + Format string `toml:"format"` // "json", "yaml", "csv", etc. + LineNumbers bool `toml:"line_numbers"` + Compact bool `toml:"compact"` + Verbose bool `toml:"verbose"` + Quiet bool `toml:"quiet"` } // DirectoriesConfig contains directory path settings. type DirectoriesConfig struct { - DataDir string `json:"data_dir"` - ConfigDir string `json:"config_dir"` + DataDir string `toml:"data_dir"` + ConfigDir string `toml:"config_dir"` } // NewDefault returns a new Config with default values. @@ -145,7 +146,7 @@ func Load() (*Config, error) { } if data, err := os.ReadFile(configPath); err == nil { - if err := json.Unmarshal(data, config); err != nil { + if err := toml.Unmarshal(data, config); err != nil { return nil, fmt.Errorf("could not parse config file: %w", err) } } else if !os.IsNotExist(err) { @@ -171,7 +172,7 @@ func (c *Config) Save() error { return fmt.Errorf("could not create config directory: %w", err) } - data, err := json.MarshalIndent(c, "", " ") + data, err := toml.Marshal(c) if err != nil { return fmt.Errorf("could not marshal config: %w", err) } @@ -476,10 +477,10 @@ func getConfigFilePath() (string, error) { if err != nil { return "", fmt.Errorf("could not get user home directory: %w", err) } - configDir = filepath.Join(homeDir, ".pho") + configDir = filepath.Join(homeDir, ".config", "pho") } - return filepath.Join(configDir, "config.json"), nil + return filepath.Join(configDir, "config.toml"), nil } // GetTimeoutDuration returns the timeout as a time.Duration. diff --git a/internal/config/config_test.go b/internal/config/config_test.go index e780c6f..d637600 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,13 +1,13 @@ package config_test import ( - "encoding/json" "os" "path/filepath" "testing" "pho/internal/config" + "github.com/BurntSushi/toml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -127,7 +127,7 @@ func TestConfig_SaveAndLoad(t *testing.T) { require.NoError(t, err) // Verify file was created - configPath := filepath.Join(tempDir, "config.json") + configPath := filepath.Join(tempDir, "config.toml") assert.FileExists(t, configPath) // Load config @@ -157,7 +157,7 @@ func TestConfig_EnvironmentOverrides(t *testing.T) { } // Create config file with different values - configPath := filepath.Join(tempDir, "config.json") + configPath := filepath.Join(tempDir, "config.toml") configData := map[string]interface{}{ "mongo": map[string]interface{}{ "uri": "mongodb://file:27017", @@ -168,7 +168,7 @@ func TestConfig_EnvironmentOverrides(t *testing.T) { }, } - data, err := json.Marshal(configData) + data, err := toml.Marshal(configData) require.NoError(t, err) err = os.WriteFile(configPath, data, 0600) From 17922bbbf7b025db26b68322d12dfb3c09010900 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 16 Jul 2025 10:48:53 +0300 Subject: [PATCH 3/3] Remove PostgreSQL configuration and add section-specific config listing - Remove PostgreSQL configuration struct and related code (not supported yet) - Remove PostgreSQL environment variable handling - Remove PostgreSQL config keys from Set/Get methods - Update tests to remove PostgreSQL references - Add support for pho config list [section] command - Update configuration path references from ~/.pho to ~/.config/pho - Add section shortcuts: mongo, database, query, app, output, directories - Improve error handling for unknown sections --- internal/app/app.go | 81 ++++++++++++++++++++++++---------- internal/config/config.go | 74 +++---------------------------- internal/config/config_test.go | 10 +---- 3 files changed, 64 insertions(+), 101 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index c0c9bc0..e652f83 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -113,7 +113,7 @@ This will execute the actual database operations.`, Aliases: []string{"cfg"}, Usage: "Manage pho configuration", Description: `Manage pho configuration settings. -Configuration is stored in ~/.pho/config.json and can be overridden by environment variables.`, +Configuration is stored in ~/.config/pho/config.toml and can be overridden by environment variables.`, Commands: []*cli.Command{ { Name: "get", @@ -138,11 +138,18 @@ Examples: Action: configSetAction, }, { - Name: "list", - Aliases: []string{"ls"}, - Usage: "List all configuration values", - Description: `List all current configuration values.`, - Action: configListAction, + Name: "list", + Aliases: []string{"ls"}, + Usage: "List configuration values [section]", + Description: `List all current configuration values, or values for a specific section. + +Examples: + pho config list # List all configuration values + pho config list mongo # List only MongoDB configuration + pho config list app # List only Application configuration + +Available sections: mongo, database, query, app, output, directories`, + Action: configListAction, }, }, }, @@ -942,15 +949,10 @@ func configListAction(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Fprintf(os.Stdout, "Current configuration:\n\n") - categories := map[string][]string{ "MongoDB": { "mongo.uri", "mongo.host", "mongo.port", "mongo.database", "mongo.collection", "mongo.extjson_mode", }, - "PostgreSQL": { - "postgres.host", "postgres.port", "postgres.database", "postgres.schema", "postgres.user", "postgres.ssl_mode", - }, "Database": { "database.type", }, @@ -968,20 +970,53 @@ func configListAction(ctx context.Context, cmd *cli.Command) error { }, } - for category, categoryKeys := range categories { - fmt.Fprintf(os.Stdout, "[%s]\n", category) - for _, key := range categoryKeys { - if value, err := cfg.Get(key); err == nil { - // Show empty values as - displayValue := fmt.Sprintf("%v", value) - if displayValue == "" { - displayValue = "" - } - fmt.Fprintf(os.Stdout, " %-20s = %s\n", key, displayValue) - } + // Map section shortcuts to full category names + sectionMap := map[string]string{ + "mongo": "MongoDB", + "database": "Database", + "query": "Query", + "app": "Application", + "output": "Output", + "directories": "Directories", + } + + // Check if specific section is requested + args := cmd.Args() + if args.Len() > 0 { + sectionName := args.Get(0) + if categoryName, exists := sectionMap[sectionName]; exists { + // List only the specific section + fmt.Fprintf(os.Stdout, "Configuration for %s:\n\n", categoryName) + printConfigSection(cfg, categoryName, categories[categoryName]) + return nil } - fmt.Fprintf(os.Stdout, "\n") + fmt.Fprintf(os.Stderr, "Error: Unknown section '%s'\n", sectionName) + fmt.Fprintf(os.Stderr, "Available sections: mongo, database, query, app, output, directories\n") + return fmt.Errorf("unknown section: %s", sectionName) + } + + // List all sections + fmt.Fprintf(os.Stdout, "Current configuration:\n\n") + for category, categoryKeys := range categories { + printConfigSection(cfg, category, categoryKeys) } return nil } + +// printConfigSection prints a single configuration section. +func printConfigSection(cfg *config.Config, category string, categoryKeys []string) { + fmt.Fprintf(os.Stdout, "[%s]\n", category) + + for _, key := range categoryKeys { + if value, err := cfg.Get(key); err == nil { + // Show empty values as + displayValue := fmt.Sprintf("%v", value) + if displayValue == "" { + displayValue = "" + } + fmt.Fprintf(os.Stdout, " %-20s = %s\n", key, displayValue) + } + } + fmt.Fprintf(os.Stdout, "\n") +} diff --git a/internal/config/config.go b/internal/config/config.go index 597680f..d634a90 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,8 +18,7 @@ const ( // Config represents the application configuration. type Config struct { // Database-specific settings - Mongo MongoConfig `toml:"mongo"` - Postgres PostgresConfig `toml:"postgres"` + Mongo MongoConfig `toml:"mongo"` // Database selection and generic query settings Database DatabaseConfig `toml:"database"` @@ -45,19 +44,9 @@ type MongoConfig struct { ExtJSONMode string `toml:"extjson_mode"` } -// PostgresConfig contains PostgreSQL-specific connection settings (future). -type PostgresConfig struct { - Host string `toml:"host"` - Port string `toml:"port"` - Database string `toml:"database"` - Schema string `toml:"schema"` - User string `toml:"user"` - SSLMode string `toml:"ssl_mode"` -} - // DatabaseConfig contains database type selection. type DatabaseConfig struct { - Type string `toml:"type"` // "mongodb", "postgres", etc. + Type string `toml:"type"` // "mongodb", etc. } // QueryConfig contains database-agnostic query settings. @@ -100,16 +89,8 @@ func NewDefault() *Config { Collection: "", ExtJSONMode: "canonical", }, - Postgres: PostgresConfig{ - Host: "localhost", - Port: "5432", - Database: "", - Schema: "public", - User: "", - SSLMode: "prefer", - }, Database: DatabaseConfig{ - Type: "mongodb", // Default to MongoDB for backward compatibility + Type: "mongodb", // Default to MongoDB }, Query: QueryConfig{ Query: "{}", @@ -203,23 +184,6 @@ func (c *Config) applyEnvironmentOverrides() { c.Mongo.Collection = val } - // PostgreSQL settings (for future use) - if val := os.Getenv("POSTGRES_HOST"); val != "" { - c.Postgres.Host = val - } - if val := os.Getenv("POSTGRES_PORT"); val != "" { - c.Postgres.Port = val - } - if val := os.Getenv("POSTGRES_DB"); val != "" { - c.Postgres.Database = val - } - if val := os.Getenv("POSTGRES_USER"); val != "" { - c.Postgres.User = val - } - if val := os.Getenv("POSTGRES_SCHEMA"); val != "" { - c.Postgres.Schema = val - } - // Database type if val := os.Getenv("PHO_DATABASE_TYPE"); val != "" { c.Database.Type = val @@ -305,24 +269,10 @@ func (c *Config) Set(key, value string) error { } c.Mongo.ExtJSONMode = value - // PostgreSQL settings (future) - case "postgres.host": - c.Postgres.Host = value - case "postgres.port": - c.Postgres.Port = value - case "postgres.database", "postgres.db": - c.Postgres.Database = value - case "postgres.schema": - c.Postgres.Schema = value - case "postgres.user": - c.Postgres.User = value - case "postgres.ssl_mode", "postgres.ssl-mode": - c.Postgres.SSLMode = value - // Database selection case "database.type": - if value != "mongodb" && value != "postgres" { - return fmt.Errorf("invalid database type: %s (valid: mongodb, postgres)", value) + if value != "mongodb" { + return fmt.Errorf("invalid database type: %s (valid: mongodb)", value) } c.Database.Type = value @@ -412,20 +362,6 @@ func (c *Config) Get(key string) (interface{}, error) { case "mongo.extjson_mode", "mongo.extjson-mode": return c.Mongo.ExtJSONMode, nil - // PostgreSQL settings (future) - case "postgres.host": - return c.Postgres.Host, nil - case "postgres.port": - return c.Postgres.Port, nil - case "postgres.database", "postgres.db": - return c.Postgres.Database, nil - case "postgres.schema": - return c.Postgres.Schema, nil - case "postgres.user": - return c.Postgres.User, nil - case "postgres.ssl_mode", "postgres.ssl-mode": - return c.Postgres.SSLMode, nil - // Database selection case "database.type": return c.Database.Type, nil diff --git a/internal/config/config_test.go b/internal/config/config_test.go index d637600..5a5e943 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -19,12 +19,6 @@ func TestNewDefault(t *testing.T) { assert.Equal(t, "mongodb://localhost:27017", cfg.Mongo.URI) assert.Equal(t, "canonical", cfg.Mongo.ExtJSONMode) - // PostgreSQL config - assert.Equal(t, "localhost", cfg.Postgres.Host) - assert.Equal(t, "5432", cfg.Postgres.Port) - assert.Equal(t, "public", cfg.Postgres.Schema) - assert.Equal(t, "prefer", cfg.Postgres.SSLMode) - // Database selection assert.Equal(t, "mongodb", cfg.Database.Type) @@ -55,9 +49,7 @@ func TestConfig_SetAndGet(t *testing.T) { {"mongo.uri", "mongodb://example.com:27017", "mongodb://example.com:27017"}, {"mongo.database", "testdb", "testdb"}, {"mongo.extjson_mode", "relaxed", "relaxed"}, - {"postgres.host", "localhost", "localhost"}, - {"postgres.database", "testdb", "testdb"}, - {"database.type", "postgres", "postgres"}, + {"database.type", "mongodb", "mongodb"}, {"query.query", "{\"test\": 1}", "{\"test\": 1}"}, {"query.limit", "5000", int64(5000)}, {"app.editor", "nano", "nano"},