// Package config defines PgTidy's formatter (and, later, linter) configuration. // // Defaults encode the project house style. A .pgtidy.yaml file discovered by // walking up from the target directory overrides individual fields. package config import ( "fmt" "os" "path/filepath" "gopkg.in/yaml.v3" ) // Case controls keyword/identifier casing. type Case string const ( CaseUpper Case = "upper" CaseLower Case = "lower" // CasePreserve leaves the token text unchanged. CasePreserve Case = "preserve" ) // CommaStyle controls where separators sit in multi-line lists. type CommaStyle string const ( // CommaLeading puts the comma at the start of the continuation line // (",col"), the house style. CommaLeading CommaStyle = "leading" // CommaTrailing puts the comma at the end of the preceding line ("col,"). CommaTrailing CommaStyle = "trailing" ) // Style is the formatter configuration. type Style struct { // Indent is one indentation level (default two spaces). Indent string // Newline is the line terminator emitted by the formatter. Newline string // KeywordCase controls SQL keyword casing (types excluded — see TypeCase). KeywordCase Case // IdentCase controls unquoted identifier casing (quoted identifiers are // never touched). IdentCase Case // TypeCase controls built-in type-name casing. TypeCase Case // Commas controls list separator placement. Commas CommaStyle } // Default returns the house-style configuration. func Default() Style { return Style{ Indent: " ", Newline: "\n", KeywordCase: CaseUpper, IdentCase: CaseLower, TypeCase: CaseLower, Commas: CommaLeading, } } // yamlFile is the on-disk representation of .pgtidy.yaml. // All fields are pointers so we can distinguish "not set" from "set to zero value". type yamlFile struct { Indent *string `yaml:"indent"` Newline *string `yaml:"newline"` KeywordCase *string `yaml:"keyword_case"` IdentCase *string `yaml:"ident_case"` TypeCase *string `yaml:"type_case"` Commas *string `yaml:"commas"` } // Load discovers and parses the nearest .pgtidy.yaml by walking up from // startDir. Fields present in the file override the house-style defaults; // missing fields keep the default value. Returns Default() when no config // file is found. func Load(startDir string) (Style, error) { st := Default() path, err := findConfig(startDir) if err != nil || path == "" { return st, err } data, err := os.ReadFile(path) if err != nil { return st, fmt.Errorf("pgtidy: read %s: %w", path, err) } var yf yamlFile if err := yaml.Unmarshal(data, &yf); err != nil { return st, fmt.Errorf("pgtidy: parse %s: %w", path, err) } if yf.Indent != nil { st.Indent = *yf.Indent } if yf.Newline != nil { st.Newline = *yf.Newline } if yf.KeywordCase != nil { c := Case(*yf.KeywordCase) if err := validCase(c); err != nil { return st, fmt.Errorf("pgtidy: %s: keyword_case: %w", path, err) } st.KeywordCase = c } if yf.IdentCase != nil { c := Case(*yf.IdentCase) if err := validCase(c); err != nil { return st, fmt.Errorf("pgtidy: %s: ident_case: %w", path, err) } st.IdentCase = c } if yf.TypeCase != nil { c := Case(*yf.TypeCase) if err := validCase(c); err != nil { return st, fmt.Errorf("pgtidy: %s: type_case: %w", path, err) } st.TypeCase = c } if yf.Commas != nil { cs := CommaStyle(*yf.Commas) if cs != CommaLeading && cs != CommaTrailing { return st, fmt.Errorf("pgtidy: %s: commas: must be \"leading\" or \"trailing\"", path) } st.Commas = cs } return st, nil } // findConfig walks parent directories from startDir looking for .pgtidy.yaml. // Returns ("", nil) when no file is found before reaching the filesystem root. func findConfig(startDir string) (string, error) { dir, err := filepath.Abs(startDir) if err != nil { return "", err } for { candidate := filepath.Join(dir, ".pgtidy.yaml") if _, err := os.Stat(candidate); err == nil { return candidate, nil } parent := filepath.Dir(dir) if parent == dir { return "", nil } dir = parent } } func validCase(c Case) error { switch c { case CaseUpper, CaseLower, CasePreserve: return nil } return fmt.Errorf("must be \"upper\", \"lower\", or \"preserve\"") }