Files
proxy/internal/app/app.go
T

168 lines
4.4 KiB
Go

package app
import (
"errors"
"fmt"
"proxyctl/internal/profile"
)
type IntentKind int
const (
CreateProfile IntentKind = iota + 1
UpdateProfile
DeleteProfile
ActivateProfile
)
type Intent struct {
Kind IntentKind
Name string
Profile profile.Profile
Confirm bool
}
type Snapshot struct {
Profiles []profile.Profile
ActiveProfile string
}
type Result struct {
Snapshot Snapshot
Notice string
Err error
}
type Module interface {
Snapshot() Snapshot
Dispatch(Intent) Result
}
type Application struct {
store profile.Store
state profile.State
}
func New(store profile.Store) (*Application, error) {
state, err := store.Load()
if err != nil {
return nil, err
}
return &Application{store: store, state: state}, nil
}
func (a *Application) Snapshot() Snapshot {
profiles := append([]profile.Profile(nil), a.state.Profiles...)
return Snapshot{Profiles: profiles, ActiveProfile: a.state.ActiveProfile}
}
func (a *Application) Dispatch(intent Intent) Result {
var notice string
next, err := a.store.Mutate(func(current profile.State) (profile.State, error) {
var mutationErr error
switch intent.Kind {
case CreateProfile:
current, notice, mutationErr = create(current, intent.Profile)
case ActivateProfile:
current, notice, mutationErr = activate(current, intent.Name)
case UpdateProfile:
current, notice, mutationErr = update(current, intent.Name, intent.Profile)
case DeleteProfile:
current, notice, mutationErr = deleteProfile(current, intent.Name, intent.Confirm)
default:
mutationErr = errors.New("unsupported Profile action")
}
return current, mutationErr
})
if err == nil {
a.state = next
} else {
var committed *profile.CommitError
if errors.As(err, &committed) {
// Rename committed next before directory durability reporting failed.
// Prefer that state even if the required verification reload also fails.
a.state = next
reloaded, loadErr := a.store.Load()
if loadErr != nil {
err = errors.Join(err, fmt.Errorf("reload committed Profile configuration: %w", loadErr))
} else {
a.state = reloaded
}
}
}
return Result{Snapshot: a.Snapshot(), Notice: notice, Err: err}
}
func create(state profile.State, input profile.Profile) (profile.State, string, error) {
p, err := profile.ValidateAndNormalize(input)
if err != nil {
return state, "", err
}
if index(state, p.Name) >= 0 {
return state, "", fmt.Errorf("a Profile named %q already exists", p.Name)
}
next := cloneState(state)
next.Profiles = append(next.Profiles, p)
return next, fmt.Sprintf("Created Profile %q", p.Name), nil
}
func activate(state profile.State, name string) (profile.State, string, error) {
if index(state, name) < 0 {
return state, "", fmt.Errorf("Profile %q does not exist", name)
}
next := cloneState(state)
next.ActiveProfile = name
return next, fmt.Sprintf("Activated Profile %q", name), nil
}
func update(state profile.State, originalName string, input profile.Profile) (profile.State, string, error) {
profileIndex := index(state, originalName)
if profileIndex < 0 {
return state, "", fmt.Errorf("Profile %q does not exist", originalName)
}
p, err := profile.ValidateAndNormalize(input)
if err != nil {
return state, "", err
}
if other := index(state, p.Name); other >= 0 && other != profileIndex {
return state, "", fmt.Errorf("a Profile named %q already exists", p.Name)
}
next := cloneState(state)
next.Profiles[profileIndex] = p
if next.ActiveProfile == originalName {
next.ActiveProfile = p.Name
}
return next, fmt.Sprintf("Updated Profile %q", p.Name), nil
}
func deleteProfile(state profile.State, name string, confirm bool) (profile.State, string, error) {
profileIndex := index(state, name)
if profileIndex < 0 {
return state, "", fmt.Errorf("Profile %q does not exist", name)
}
if len(state.Profiles) == 1 && !confirm {
return state, "", errors.New("deleting the final Profile requires confirmation")
}
next := cloneState(state)
next.Profiles = append(next.Profiles[:profileIndex], next.Profiles[profileIndex+1:]...)
if next.ActiveProfile == name {
next.ActiveProfile = ""
}
return next, fmt.Sprintf("Deleted Profile %q", name), nil
}
func index(state profile.State, name string) int {
for i := range state.Profiles {
if state.Profiles[i].Name == name {
return i
}
}
return -1
}
func cloneState(state profile.State) profile.State {
state.Profiles = append([]profile.Profile(nil), state.Profiles...)
return state
}