Expand ordered configuration search candidates

This commit is contained in:
2026-10-24 15:43:10 +00:00
parent e2634c8815
commit 4748313105
5 changed files with 390 additions and 2 deletions

View File

@@ -1,6 +1,8 @@
package engine
import (
"errors"
"os"
"path/filepath"
"runtime"
"strings"
@@ -264,3 +266,196 @@ func validWindowsFilenameComponent(value string) bool {
return true
}
}
var errUnixSystemPresetUnavailable = errors.New("Unix system directories are unavailable on this platform")
type discoveryDirectoryHelpers struct {
userConfigDir func() (string, error)
userHomeDir func() (string, error)
workingDir func() (string, error)
unixSupported func() bool
}
type searchCandidateIterator struct {
search Search
application string
helpers discoveryDirectoryHelpers
entryIndex int
groupIndex int
nameIndex int
directory string
dirReady bool
}
func newSearchCandidateIterator(search Search, application string, helpers discoveryDirectoryHelpers) (*searchCandidateIterator, []Diagnostic) {
if diagnostics := validateSearch(search, application); len(diagnostics) != 0 {
return nil, diagnostics
}
defaults := defaultDiscoveryDirectoryHelpers()
if helpers.userConfigDir == nil {
helpers.userConfigDir = defaults.userConfigDir
}
if helpers.userHomeDir == nil {
helpers.userHomeDir = defaults.userHomeDir
}
if helpers.workingDir == nil {
helpers.workingDir = defaults.workingDir
}
if helpers.unixSupported == nil {
helpers.unixSupported = defaults.unixSupported
}
return &searchCandidateIterator{search: search, application: application, helpers: helpers}, nil
}
func defaultDiscoveryDirectoryHelpers() discoveryDirectoryHelpers {
return discoveryDirectoryHelpers{
userConfigDir: os.UserConfigDir,
userHomeDir: os.UserHomeDir,
workingDir: os.Getwd,
unixSupported: unixSystemPresetSupported,
}
}
// next expands one candidate at a time, resolving only the directory currently
// reached. Directory filenames are visited before moving to the next directory.
func (iterator *searchCandidateIterator) next() (string, bool, error) {
for {
if iterator.dirReady {
entry := iterator.search.entries[iterator.entryIndex]
for iterator.groupIndex < len(entry.filenames) {
group := entry.filenames[iterator.groupIndex]
count := filenameGroupLength(group)
if iterator.nameIndex < count {
name := filenameGroupAt(group, iterator.nameIndex, iterator.application)
iterator.nameIndex++
return filepath.Join(iterator.directory, name), true, nil
}
iterator.groupIndex++
iterator.nameIndex = 0
}
iterator.dirReady = false
iterator.entryIndex++
}
if iterator.entryIndex >= len(iterator.search.entries) {
return "", false, nil
}
entry := iterator.search.entries[iterator.entryIndex]
if entry.kind == searchEntryPath {
iterator.entryIndex++
path, err := iterator.absoluteCandidate(entry.path)
if err != nil {
return "", false, err
}
return path, true, nil
}
if !directoryHasCandidates(entry.filenames) {
iterator.entryIndex++
continue
}
directory, err := iterator.resolveDirectory(entry)
if err != nil {
return "", false, err
}
iterator.directory = directory
iterator.groupIndex = 0
iterator.nameIndex = 0
iterator.dirReady = true
}
}
func (iterator *searchCandidateIterator) absoluteCandidate(path string) (string, error) {
if filepath.IsAbs(path) {
return filepath.Clean(path), nil
}
if filepath.VolumeName(path) != "" {
return filepath.Abs(path)
}
working, err := iterator.helpers.workingDir()
if err != nil {
return "", err
}
return filepath.Clean(filepath.Join(working, path)), nil
}
func (iterator *searchCandidateIterator) resolveDirectory(entry searchEntry) (string, error) {
var base string
switch entry.baseKind {
case directoryBaseLiteral:
base = entry.directory
case directoryBaseUnixLocal:
if !iterator.helpers.unixSupported() {
return "", errUnixSystemPresetUnavailable
}
base = "/usr/local/etc"
case directoryBaseUnixEtc:
if !iterator.helpers.unixSupported() {
return "", errUnixSystemPresetUnavailable
}
base = "/etc"
case directoryBaseUserConfig:
var err error
base, err = iterator.helpers.userConfigDir()
if err != nil {
return "", err
}
case directoryBaseHome:
var err error
base, err = iterator.helpers.userHomeDir()
if err != nil {
return "", err
}
case directoryBaseWorking:
var err error
base, err = iterator.helpers.workingDir()
if err != nil {
return "", err
}
default:
return "", errors.New("directory search base is invalid")
}
if entry.appendSlug {
base = filepath.Join(base, entry.slugPrefix+iterator.application)
}
if entry.baseKind == directoryBaseLiteral && !filepath.IsAbs(base) {
return iterator.absoluteCandidate(base)
}
return filepath.Clean(base), nil
}
func directoryHasCandidates(groups []FilenameGroup) bool {
for _, group := range groups {
if filenameGroupLength(group) != 0 {
return true
}
}
return false
}
func filenameGroupLength(group FilenameGroup) int {
switch group.kind {
case filenameGroupNamed:
return 2
case filenameGroupGeneric:
return 4
case filenameGroupCustom:
return len(group.names)
default:
return 0
}
}
func filenameGroupAt(group FilenameGroup, index int, application string) string {
switch group.kind {
case filenameGroupNamed:
if index == 0 {
return application + ".yml"
}
return application + ".yaml"
case filenameGroupGeneric:
return [...]string{"config.yml", "config.yaml", "main.yml", "main.yaml"}[index]
case filenameGroupCustom:
return group.names[index]
default:
return ""
}
}