Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 83 additions & 51 deletions x/config/cli/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package cli
import (
"errors"
"fmt"
"iter"
"maps"
"reflect"
"slices"
"strings"
"unicode"

"github.com/huandu/go-clone"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -49,7 +51,7 @@ func ProfileWithSelector[T, S any, K comparable](

scope := selector.goPath[:len(selector.goPath)-1]
for _, other := range entry.profileSelectors {
if scopesOverlap(other.goPath[:len(other.goPath)-1], scope) {
if scopesNest(other.goPath[:len(other.goPath)-1], scope) {
return nil, fmt.Errorf("profiles for %s and %s overlap: each rebuilds its whole section, so one would drop the other's values", other.key, selector.key)
}
}
Expand Down Expand Up @@ -106,15 +108,10 @@ func checkProfileKeys[T any, S any, K comparable](

m := readField(vBuilt, leaf.goPath[len(scope):])
for _, el := range leaf.entries {
err := visitEntries(el, leaf.key, m, 0, func(name string, v reflect.Value, isMap bool) error {
if !isMap {
return nil
for name, v := range entryMaps(el, leaf.key, m) {
if err := foldCollision(k, name, v); err != nil {
return err
}

return foldCollision(k, name, v)
})
if err != nil {
return err
}
}
}
Expand All @@ -131,42 +128,69 @@ func foldCollision(profile any, key string, m reflect.Value) error {
}

slices.Sort(texts)
for i, text := range texts {
for _, other := range texts[i+1:] {
if strings.EqualFold(text, other) {
return fmt.Errorf("profile %v gives %s the keys %q and %q, which differ only in case, so an env var for either is ambiguous", profile, key, text, other)
}
seen := map[string]string{}
for _, text := range texts {
folded := foldText(text)
if other, dup := seen[folded]; dup {
return fmt.Errorf("profile %v gives %s the keys %q and %q, which differ only in case, so an env var for either is ambiguous", profile, key, other, text)
}

seen[folded] = text
}

return nil
}

// visitEntries calls visit for m and each map below it on leaf's path, then for each value leaf sets, named as their
// flags would be, such as section.chains.mainnet.labels.
func visitEntries(leaf *entryLeaf, name string, m reflect.Value, depth int, visit func(name string, v reflect.Value, isMap bool) error) error {
if err := visit(name, m, true); err != nil {
return err
// Maps each rune to the smallest in its case orbit, so two texts fold alike exactly when strings.EqualFold, which env
// vars match keys with, holds. Full case folding, as in golang.org/x/text/cases, would also join keys such as "ß" and
// "ss", which strings.EqualFold keeps apart.
func foldText(s string) string {
return strings.Map(func(r rune) rune {
least := r
for f := unicode.SimpleFold(r); f != r; f = unicode.SimpleFold(f) {
least = min(least, f)
}

return least
}, s)
}

// entryMaps yields m and each map below it on leaf's path, named as their flags would be, such as
// section.chains.mainnet.labels.
func entryMaps(leaf *entryLeaf, name string, m reflect.Value) iter.Seq2[string, reflect.Value] {
return func(yield func(string, reflect.Value) bool) {
walkEntries(leaf, name, m, 0, yield, func(string, reflect.Value) bool { return true })
}
}

// entryValues yields each value leaf sets inside m, named as entryMaps names maps.
func entryValues(leaf *entryLeaf, name string, m reflect.Value) iter.Seq2[string, reflect.Value] {
return func(yield func(string, reflect.Value) bool) {
walkEntries(leaf, name, m, 0, func(string, reflect.Value) bool { return true }, yield)
}
}

// Keys are sorted, so the order is deterministic.
func walkEntries(leaf *entryLeaf, name string, m reflect.Value, depth int, yieldMap, yieldValue func(string, reflect.Value) bool) bool {
if !yieldMap(name, m) {
return false
}

parts := strings.Split(leaf.key, "."+cliKeyPlaceholder)
byText := func(a, b reflect.Value) int { return strings.Compare(textOf(a), textOf(b)) }
for _, key := range slices.SortedFunc(slices.Values(m.MapKeys()), byText) {
inner := name + "." + textOf(key) + parts[depth+1]
v := readField(m.MapIndex(key), leaf.paths[depth])
var err error
if depth == len(leaf.keyTypes)-1 {
err = visit(inner, v, false)
} else {
err = visitEntries(leaf, inner, v, depth+1, visit)
}

if err != nil {
return err
if !yieldValue(inner, v) {
return false
}
} else if !walkEntries(leaf, inner, v, depth+1, yieldMap, yieldValue) {
return false
}
}

return nil
return true
}

// A required field left zero is omitted from help, as a source must set it.
Expand Down Expand Up @@ -204,13 +228,11 @@ func describeProfiles[T any, K comparable, S any](
continue
}

_ = visitEntries(el, leaf.key, derefOrZero(v), 0, func(name string, v reflect.Value, isMap bool) error {
if text, ok := displayText(v); ok && !isMap {
for name, v := range entryValues(el, leaf.key, derefOrZero(v)) {
if text, ok := displayText(v); ok {
_, _ = fmt.Fprintf(&out, " %s = %s\n", name, text)
}

return nil
})
}
}
}
}
Expand All @@ -228,27 +250,24 @@ func locateProfile[T, S any, K comparable](
allocateField(v, k.goPath)
}

defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("profile selector panicked: %v", r)
}
}()
sType, kType := reflect.TypeFor[S](), reflect.TypeFor[K]()
if holdsIdentityKind(kType) {
return leafKey{}, fmt.Errorf("profile key type %s compares by identity, so no value a source sets would match a profile", kType)
}

if holdsIdentityKind(reflect.TypeFor[K]()) {
return leafKey{}, fmt.Errorf("profile key type %s compares by identity, so no value a source sets would match a profile", reflect.TypeFor[K]())
wantSection, wantKey, err := callSelector(scratch, section, key)
if err != nil {
return leafKey{}, err
}

s := section(scratch)
wantKey := reflect.ValueOf(key(s)).Pointer()
wantSection := reflect.ValueOf(s).Pointer()
for _, k := range entry.leaves {
parent := lookupField(v, k.goPath[:len(k.goPath)-1])
// Types are checked as a struct shares its first field's address.
if parent.Type() != reflect.TypeFor[S]() || parent.Addr().Pointer() != wantSection {
if parent.Type() != sType || parent.Addr().Pointer() != wantSection {
continue
}

if k.flag != nil && k.goType == reflect.TypeFor[K]() &&
if k.flag != nil && k.goType == kType &&
lookupField(parent, k.goPath[len(k.goPath)-1:]).Addr().Pointer() == wantKey {
return k, nil
}
Expand All @@ -257,16 +276,29 @@ func locateProfile[T, S any, K comparable](
return leafKey{}, errors.New("profile key must return a top-level field of its section bound as a flag, such as &c.ID")
}

// The section and key functions are the caller's, and can panic on anything, such as an index out of range.
func callSelector[T, S any, K comparable](scratch *T, section func(*T) *S, key func(*S) *K) (wantSection, wantKey uintptr, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("profile selector panicked: %v", r)
}
}()

s := section(scratch)
return reflect.ValueOf(s).Pointer(), reflect.ValueOf(key(s)).Pointer(), nil
}

// A scope is the Go path to a profile's section, such as [Chain] for &c.Chain.ID.
func inScope(k leafKey, scope []string) bool {
return len(k.goPath) > len(scope) && slices.Equal(k.goPath[:len(scope)], scope)
return len(k.goPath) > len(scope) && hasPrefix(k.goPath, scope)
}

func scopesOverlap(a, b []string) bool {
if len(a) > len(b) {
a, b = b, a
}
func scopesNest(a, b []string) bool {
return hasPrefix(a, b) || hasPrefix(b, a)
}

return slices.Equal(a, b[:len(a)])
func hasPrefix(path, prefix []string) bool {
return len(path) >= len(prefix) && slices.Equal(path[:len(prefix)], prefix)
}

// Decoding overwrites the section, so its defaults are copied first, deeply. Each use copies them again, so that a
Expand Down
Loading