Files
commandkit/internal/alias/resolver.go

135 lines
4.0 KiB
Go

// Package alias models application command-path rewriting independently of
// command execution and the parsing backend.
package alias
import (
"fmt"
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
)
// Mapping replaces Source with Target when Source is the longest matching
// prefix of an entered command path.
type Mapping struct {
Source commandpath.Path
Target commandpath.Path
}
type trieNode struct {
children map[string]int
mapping int
best int
parent int
path commandpath.Path
}
// Resolver selects longest matching command-path mappings.
type Resolver struct {
mappings []Mapping
trie []trieNode
}
// NewResolver builds a resolver from normalized command-path mappings.
func NewResolver(mappings []Mapping) (*Resolver, error) {
resolver := &Resolver{trie: []trieNode{{mapping: -1, best: -1, parent: -1}}}
for _, mapping := range mappings {
if len(mapping.Source) == 0 {
return nil, fmt.Errorf("command alias source path must not be empty")
}
if len(mapping.Target) == 0 {
return nil, fmt.Errorf("command alias target path must not be empty")
}
for _, component := range append(mapping.Source.Clone(), mapping.Target...) {
if err := commandpath.ValidateComponent(component); err != nil {
return nil, err
}
}
normalized := Mapping{Source: mapping.Source.Clone(), Target: mapping.Target.Clone()}
index := len(resolver.mappings)
resolver.mappings = append(resolver.mappings, normalized)
nodeIndex := 0
for _, component := range normalized.Source {
node := &resolver.trie[nodeIndex]
if node.children == nil {
node.children = make(map[string]int)
}
childIndex, exists := node.children[component]
if !exists {
childIndex = len(resolver.trie)
node.children[component] = childIndex
childPath := append(resolver.trie[nodeIndex].path.Clone(), component)
resolver.trie = append(resolver.trie, trieNode{mapping: -1, best: -1, parent: nodeIndex, path: childPath})
}
nodeIndex = childIndex
}
node := &resolver.trie[nodeIndex]
if node.mapping >= 0 {
previous := resolver.mappings[node.mapping]
if previous.Target.Equal(normalized.Target) {
resolver.mappings = resolver.mappings[:index]
continue
}
return nil, fmt.Errorf("conflicting command aliases for %q", normalized.Source.String())
}
node.mapping = index
node.path = normalized.Source.Clone()
}
resolver.setFallbacks()
return resolver, nil
}
func (resolver *Resolver) setFallbacks() {
for index := 0; index < len(resolver.trie); index++ {
node := &resolver.trie[index]
if index > 0 {
node.best = resolver.trie[node.parent].best
}
if node.mapping >= 0 {
node.best = node.mapping
}
}
}
// ResolveOnce applies at most one longest-prefix mapping. It returns the
// original input unchanged when no mapping matches.
func (resolver *Resolver) ResolveOnce(path commandpath.Path) (commandpath.Path, Mapping, bool) {
if resolver == nil {
return path.Clone(), Mapping{}, false
}
nodeIndex := 0
matched := -1
matchedLength := 0
for index, component := range path {
childIndex, exists := resolver.trie[nodeIndex].children[component]
if !exists {
break
}
nodeIndex = childIndex
if mapping := resolver.trie[nodeIndex].mapping; mapping >= 0 {
matched = mapping
matchedLength = index + 1
}
}
if matched < 0 {
return path.Clone(), Mapping{}, false
}
mapping := resolver.mappings[matched]
resolved := make(commandpath.Path, 0, len(mapping.Target)+len(path)-matchedLength)
resolved = append(resolved, mapping.Target...)
resolved = append(resolved, path[matchedLength:]...)
return resolved, Mapping{Source: mapping.Source.Clone(), Target: mapping.Target.Clone()}, true
}
// Resolve applies mappings until no source matches. Installation validates
// termination before a resolver is used for application input.
func (resolver *Resolver) Resolve(path commandpath.Path) commandpath.Path {
current := path.Clone()
for {
next, _, matched := resolver.ResolveOnce(current)
if !matched {
return current
}
current = next
}
}