214 lines
8.1 KiB
Go
214 lines
8.1 KiB
Go
package ckconfig
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
|
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
// FromConfigKit adapts a typed ConfigKit loader to all five native
|
|
// configuration operations. The loader receives the operation's explicit
|
|
// Selection, initialized input, and shared invocation state, and runs only when
|
|
// an installed operation selects this scope. Comparison invokes it separately
|
|
// for each reached side. Construction and help do not call it. A nil original
|
|
// loader is retained as a construction issue, even if callers later narrow the
|
|
// scope's operation list.
|
|
func FromConfigKit[I, S, T any](
|
|
name, summary string,
|
|
load func(context.Context, Selection, I, S) (*configkit.Result[T], error),
|
|
) Scope[I, S] {
|
|
scope := Scope[I, S]{
|
|
Name: name,
|
|
Summary: summary,
|
|
Operations: []Operation{Validate, Show, Sources, Explain, Diff},
|
|
}
|
|
if load == nil {
|
|
return WithScopeError(scope, fmt.Errorf("ConfigKit scope %q loader must not be nil", name))
|
|
}
|
|
|
|
scope.Acquire = func(ctx context.Context, request Request, input I, state S) (commandkit.Acquired[View], error) {
|
|
result, err := load(ctx, request.Selection, input, state)
|
|
acquired := commandkit.Acquired[View]{}
|
|
if result != nil {
|
|
acquired.Value = View{Effective: result, Sources: result, Explanation: result}
|
|
acquired.Diagnostics, acquired.Explanation = configKitDiagnostics(name, result.Sources(), result.Diagnostics())
|
|
}
|
|
if err != nil {
|
|
var loadError *configkit.LoadError
|
|
if errors.As(err, &loadError) && loadError != nil {
|
|
warnings, explanation := configKitDiagnostics(name, loadError.Sources(), loadError.Diagnostics())
|
|
acquired.Diagnostics = append(acquired.Diagnostics, warnings...)
|
|
acquired.Explanation = appendConfigKitExplanation(acquired.Explanation, explanation)
|
|
}
|
|
return acquired, err
|
|
}
|
|
if result == nil {
|
|
return commandkit.Acquired[View]{}, fmt.Errorf("ConfigKit scope %q returned a nil result", name)
|
|
}
|
|
return acquired, nil
|
|
}
|
|
scope.Compare = func(ctx context.Context, request ComparisonRequest[I], state S) (commandkit.Acquired[[]configkit.Difference], error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return commandkit.Acquired[[]configkit.Difference]{}, fmt.Errorf("ConfigKit scope %q comparison before left load: %w", name, err)
|
|
}
|
|
|
|
acquired := commandkit.Acquired[[]configkit.Difference]{}
|
|
left, leftDiagnostics, leftExplanation, err := loadComparisonSide(ctx, name, "left", load, request.Left, state)
|
|
acquired.Diagnostics = append(acquired.Diagnostics, leftDiagnostics...)
|
|
acquired.Explanation = appendConfigKitExplanation(acquired.Explanation, leftExplanation)
|
|
if err != nil {
|
|
return acquired, err
|
|
}
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
return acquired, fmt.Errorf("ConfigKit scope %q comparison after left load: %w", name, err)
|
|
}
|
|
|
|
right, rightDiagnostics, rightExplanation, err := loadComparisonSide(ctx, name, "right", load, request.Right, state)
|
|
acquired.Diagnostics = append(acquired.Diagnostics, rightDiagnostics...)
|
|
acquired.Explanation = appendConfigKitExplanation(acquired.Explanation, rightExplanation)
|
|
if err != nil {
|
|
return acquired, err
|
|
}
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
return acquired, fmt.Errorf("ConfigKit scope %q comparison before native diff: %w", name, err)
|
|
}
|
|
|
|
acquired.Value, err = configkit.Diff(left, right)
|
|
if err != nil {
|
|
return acquired, fmt.Errorf("ConfigKit scope %q native diff: %w", name, err)
|
|
}
|
|
return acquired, nil
|
|
}
|
|
return scope
|
|
}
|
|
|
|
func loadComparisonSide[I, S, T any](
|
|
ctx context.Context,
|
|
scope, side string,
|
|
load func(context.Context, Selection, I, S) (*configkit.Result[T], error),
|
|
request ComparisonSide[I],
|
|
state S,
|
|
) (*configkit.Result[T], []commandkit.Diagnostic, commandkit.Explanation, error) {
|
|
result, err := load(ctx, request.Selection, request.Input, state)
|
|
var diagnostics []commandkit.Diagnostic
|
|
var explanation commandkit.Explanation
|
|
if result != nil {
|
|
warnings, terminal := configKitComparisonDiagnostics(scope, side, result.Sources(), result.Diagnostics())
|
|
diagnostics = append(diagnostics, warnings...)
|
|
explanation = appendConfigKitExplanation(explanation, terminal)
|
|
}
|
|
if err != nil {
|
|
var loadError *configkit.LoadError
|
|
if errors.As(err, &loadError) && loadError != nil {
|
|
warnings, terminal := configKitComparisonDiagnostics(scope, side, loadError.Sources(), loadError.Diagnostics())
|
|
diagnostics = append(diagnostics, warnings...)
|
|
explanation = appendConfigKitExplanation(explanation, terminal)
|
|
}
|
|
return result, diagnostics, explanation, fmt.Errorf("ConfigKit scope %q %s load: %w", scope, side, err)
|
|
}
|
|
if result == nil {
|
|
return nil, diagnostics, explanation, fmt.Errorf("ConfigKit scope %q %s loader returned a nil result", scope, side)
|
|
}
|
|
return result, diagnostics, explanation, nil
|
|
}
|
|
|
|
func configKitDiagnostics(scope string, sources []configkit.SourceInfo, records []configkit.Diagnostic) ([]commandkit.Diagnostic, commandkit.Explanation) {
|
|
return configKitDiagnosticsWithHeading("Configuration scope "+strconv.Quote(scope)+":\n", sources, records)
|
|
}
|
|
|
|
func configKitComparisonDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) ([]commandkit.Diagnostic, commandkit.Explanation) {
|
|
return configKitDiagnosticsWithHeading("Configuration scope "+strconv.Quote(scope)+" "+side+":\n", sources, records)
|
|
}
|
|
|
|
func configKitDiagnosticsWithHeading(heading string, sources []configkit.SourceInfo, records []configkit.Diagnostic) ([]commandkit.Diagnostic, commandkit.Explanation) {
|
|
if len(records) == 0 {
|
|
return nil, commandkit.Explanation{}
|
|
}
|
|
|
|
sources = append([]configkit.SourceInfo(nil), sources...)
|
|
records = cloneConfigKitDiagnostics(records)
|
|
warnings := make([]configkit.Diagnostic, 0, len(records))
|
|
terminal := make([]configkit.Diagnostic, 0, len(records))
|
|
for _, record := range records {
|
|
if record.Severity == configkit.SeverityError {
|
|
terminal = append(terminal, record)
|
|
} else {
|
|
warnings = append(warnings, record)
|
|
}
|
|
}
|
|
|
|
if len(warnings) == 0 {
|
|
return nil, explanationWithConfigKitRecords(heading, sources, terminal)
|
|
}
|
|
warning := commandkit.Diagnostic{
|
|
Severity: commandkit.SeverityWarning,
|
|
Write: configKitDiagnosticWriter(heading, sources, warnings),
|
|
}
|
|
return []commandkit.Diagnostic{warning}, explanationWithConfigKitRecords(heading, sources, terminal)
|
|
}
|
|
|
|
func explanationWithConfigKitRecords(heading string, sources []configkit.SourceInfo, records []configkit.Diagnostic) commandkit.Explanation {
|
|
if len(records) == 0 {
|
|
return commandkit.Explanation{}
|
|
}
|
|
return commandkit.Explanation{Details: []commandkit.Detail{configKitDiagnosticWriter(heading, sources, records)}}
|
|
}
|
|
|
|
func configKitDiagnosticWriter(heading string, sources []configkit.SourceInfo, records []configkit.Diagnostic) commandkit.Detail {
|
|
return func(writer io.Writer) error {
|
|
if err := writeDiagnosticText(writer, heading); err != nil {
|
|
return err
|
|
}
|
|
if len(sources) > 0 {
|
|
if err := writeDiagnosticText(writer, "Source IDs:\n"); err != nil {
|
|
return err
|
|
}
|
|
for _, source := range sources {
|
|
line := " " + strconv.Quote(source.ID) + ": label=" + strconv.Quote(source.Label) + ", path=" + strconv.Quote(source.Path) + "\n"
|
|
if err := writeDiagnosticText(writer, line); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return configkit.WriteDiagnostics(writer, records)
|
|
}
|
|
}
|
|
|
|
func cloneConfigKitDiagnostics(records []configkit.Diagnostic) []configkit.Diagnostic {
|
|
cloned := append([]configkit.Diagnostic(nil), records...)
|
|
for index := range cloned {
|
|
if records[index].Primary != nil {
|
|
primary := *records[index].Primary
|
|
cloned[index].Primary = &primary
|
|
}
|
|
cloned[index].Related = append([]configkit.Location(nil), records[index].Related...)
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func appendConfigKitExplanation(left, right commandkit.Explanation) commandkit.Explanation {
|
|
if left.Message == "" {
|
|
left.Message = right.Message
|
|
}
|
|
left.Details = append(left.Details, right.Details...)
|
|
return left
|
|
}
|
|
|
|
func writeDiagnosticText(writer io.Writer, value string) error {
|
|
written, err := io.WriteString(writer, value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if written != len(value) {
|
|
return io.ErrShortWrite
|
|
}
|
|
return nil
|
|
}
|