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 }