diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..9208ef7 --- /dev/null +++ b/src/config.py @@ -0,0 +1,195 @@ +"""Configuration management for gitignore-generator.""" +import os +from pathlib import Path +from typing import Any, Dict, List, Optional + + +class Config: + """Configuration management class.""" + + DEFAULT_CONFIG_NAME = ".gitignorerc" + + def __init__( + self, + custom_templates: Optional[Dict[str, str]] = None, + default_types: Optional[List[str]] = None, + exclude_patterns: Optional[List[str]] = None, + include_patterns: Optional[List[str]] = None, + ) -> None: + """Initialize configuration.""" + self.custom_templates = custom_templates or {} + self.default_types = default_types or [] + self.exclude_patterns = exclude_patterns or [] + self.include_patterns = include_patterns or {} + + @classmethod + def load(cls, config_path: str) -> "Config": + """Load configuration from YAML file.""" + import yaml + + path = Path(config_path) + if not path.exists(): + raise FileNotFoundError(f"Config file not found: {config_path}") + + try: + with open(path, "r") as f: + data = yaml.safe_load(f) + except yaml.YAMLError as e: + raise ValueError(f"Invalid YAML in config file: {e}") + except OSError as e: + raise OSError(f"Error reading config file: {e}") + + if not isinstance(data, dict): + raise ValueError("Config file must contain a YAML dictionary") + + custom_templates = data.get("custom_templates", {}) + default_types = data.get("default_types", []) + exclude_patterns = data.get("exclude_patterns", []) + include_patterns = data.get("include_patterns", {}) + + return cls( + custom_templates=custom_templates, + default_types=default_types, + exclude_patterns=exclude_patterns, + include_patterns=include_patterns, + ) + + @classmethod + def find_config( + cls, search_paths: Optional[List[str]] = None + ) -> Optional[Path]: + """Find configuration file in search paths.""" + if search_paths is None: + search_paths = [ + os.getcwd(), + os.path.expanduser("~"), + "/etc", + ] + + for path_str in search_paths: + path = Path(path_str) + config_path = path / cls.DEFAULT_CONFIG_NAME + if config_path.exists(): + return config_path + + yaml_path = path / "config.yaml" + if yaml_path.exists(): + return yaml_path + + return None + + def save(self, config_path: str) -> None: + """Save configuration to YAML file.""" + import yaml + + path = Path(config_path) + + data = { + "custom_templates": self.custom_templates, + "default_types": self.default_types, + "exclude_patterns": self.exclude_patterns, + "include_patterns": self.include_patterns, + } + + try: + with open(path, "w") as f: + yaml.dump(data, f, default_flow_style=False, indent=2) + except OSError as e: + raise OSError(f"Error writing config file: {e}") + + def get(self, key: str, default: Any = None) -> Any: + """Get configuration value.""" + if hasattr(self, key): + return getattr(self, key) + return default + + def set(self, key: str, value: Any) -> None: + """Set configuration value.""" + if hasattr(self, key): + setattr(self, key, value) + + def merge(self, other: "Config") -> "Config": + """Merge with another configuration.""" + return Config( + custom_templates={ + **self.custom_templates, + **other.custom_templates, + }, + default_types=self.default_types or other.default_types, + exclude_patterns=list( + set(self.exclude_patterns + other.exclude_patterns) + ), + include_patterns={ + **self.include_patterns, + **other.include_patterns, + }, + ) + + def validate(self) -> List[str]: + """Validate configuration and return list of errors.""" + errors = [] + + if not isinstance(self.custom_templates, dict): + errors.append("custom_templates must be a dictionary") + else: + for key, value in self.custom_templates.items(): + if not isinstance(key, str): + errors.append(f"Template key must be string: {key}") + if not isinstance(value, str): + errors.append( + f"Template value for '{key}' must be a string" + ) + + if not isinstance(self.default_types, list): + errors.append("default_types must be a list") + else: + for item in self.default_types: + if not isinstance(item, str): + errors.append(f"Default type must be string: {item}") + + if not isinstance(self.exclude_patterns, list): + errors.append("exclude_patterns must be a list") + else: + for item in self.exclude_patterns: + if not isinstance(item, str): + errors.append( + f"Exclude pattern must be string: {item}" + ) + + if not isinstance(self.include_patterns, dict): + errors.append("include_patterns must be a dictionary") + else: + for key, value in self.include_patterns.items(): + if not isinstance(key, str): + errors.append(f"Include pattern key must be string: {key}") + if isinstance(value, list): + for item in value: + if not isinstance(item, str): + errors.append( + f"Include pattern value for '{key}' must be string: {item}" + ) + elif not isinstance(value, str): + errors.append( + f"Include pattern value for '{key}' must be string or list" + ) + + return errors + + def to_dict(self) -> Dict[str, Any]: + """Convert configuration to dictionary.""" + return { + "custom_templates": self.custom_templates, + "default_types": self.default_types, + "exclude_patterns": self.exclude_patterns, + "include_patterns": self.include_patterns, + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Config": + """Create configuration from dictionary.""" + return cls( + custom_templates=data.get("custom_templates", {}), + default_types=data.get("default_types", []), + exclude_patterns=data.get("exclude_patterns", []), + include_patterns=data.get("include_patterns", {}), + )