Files
PgTidy/pkg/config/config.go
T
warkanum 932c83dbad feat(cmd): add unified diff output option for formatting
* Implemented `-d`/`--diff` flag to print unified diffs.
* Added `unifiedDiff` function for generating diffs.
* Config discovery now merges fields from `.pgtidy.yaml`.
2026-06-27 19:32:57 +02:00

162 lines
4.2 KiB
Go

// 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\"")
}