Files
commandkit/integrations/ckconfig/sources.go

198 lines
8.3 KiB
Go

package ckconfig
import (
"fmt"
"io"
"text/template"
commandkit "go.maximumdirect.net/commandkit"
configkit "go.maximumdirect.net/configkit"
)
type sourceReport struct {
Scope string `json:"scope"`
Sources []sourceDTO `json:"sources"`
}
type sourceDTO struct {
ID string `json:"id"`
Label string `json:"label"`
Path string `json:"path"`
Destination string `json:"destination"`
Placement string `json:"placement"`
DestinationIsFallback bool `json:"destination_is_fallback"`
Layer string `json:"layer"`
Kind string `json:"kind"`
UnderlayIndex int `json:"underlay_index"`
ImportedFrom *locationDTO `json:"imported_from"`
VersionEvidence *sourceVersionEvidenceDTO `json:"version_evidence"`
EmptyImportSelections []emptyImportSelectionDTO `json:"empty_import_selections"`
}
type emptyImportSelectionDTO struct {
Path string `json:"path"`
Location locationDTO `json:"location"`
Selector string `json:"selector"`
}
type locationDTO struct {
SourceID string `json:"source_id"`
Line int `json:"line"`
Column int `json:"column"`
}
type sourceVersionEvidenceDTO struct {
Path string `json:"path"`
Version versionDTO `json:"version"`
Location *locationDTO `json:"location"`
Redacted bool `json:"redacted"`
}
type versionDTO struct {
Present bool `json:"present"`
Null bool `json:"null"`
Text string `json:"text"`
}
func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
selected := plan.scopes[0]
name := selected.scope.scope.Name
reporter := invocation.Reporter()
if err := invocation.Context.Err(); err != nil {
return configFailure("Configuration source listing was canceled.", commandkit.Explanation{}, err)
}
if selected.scope.scope.Acquire == nil {
cause := fmt.Errorf("configuration scope %q has no acquisition callback", name)
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), commandkit.Explanation{}, cause)
}
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
Operation: Sources,
Selection: selected.selection,
}, selected.input, state)
queueConfigAcquisition(reporter, acquired)
if acquireErr == nil && invocation.Context.Err() != nil {
err := invocation.Context.Err()
return configFailure("Configuration source listing was canceled.", acquired.Explanation, err)
}
if acquired.Value.Sources == nil {
if acquireErr != nil {
explanation := sourceSelectionExplanation(acquired.Explanation, "Source inventory is unavailable.")
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), explanation, acquireErr)
}
cause := fmt.Errorf("configuration scope %q did not provide a source reader", name)
return configFailure("Configuration scope did not provide source information.", acquired.Explanation, cause)
}
sources := acquired.Value.Sources.Sources()
document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))}
for _, source := range sources {
if plan.sourceID.Set && source.ID != plan.sourceID.Value {
continue
}
placement := "mapping"
if source.PlacementMode == configkit.PlacementModeSequenceItem {
placement = "sequence_item"
}
item := sourceDTO{
ID: source.ID,
Label: source.Label,
Path: source.Path,
Destination: source.Destination.String(),
Placement: placement,
DestinationIsFallback: source.DestinationIsFallback,
Layer: string(source.Layer),
Kind: string(source.Kind),
UnderlayIndex: source.UnderlayIndex,
EmptyImportSelections: make([]emptyImportSelectionDTO, 0, len(source.EmptyImportSelections)),
}
if source.ImportedFrom != nil {
item.ImportedFrom = &locationDTO{SourceID: source.ImportedFrom.SourceID, Line: source.ImportedFrom.Line, Column: source.ImportedFrom.Column}
}
if source.VersionEvidence != nil {
check := source.VersionEvidence
item.VersionEvidence = &sourceVersionEvidenceDTO{
Path: check.Path.String(),
Version: versionDTO{
Present: check.Version.Present,
Null: check.Version.Null,
Text: check.Version.Text,
},
Redacted: check.Redacted,
}
if check.Location != nil {
item.VersionEvidence.Location = &locationDTO{SourceID: check.Location.SourceID, Line: check.Location.Line, Column: check.Location.Column}
}
}
for _, selection := range source.EmptyImportSelections {
item.EmptyImportSelections = append(item.EmptyImportSelections, emptyImportSelectionDTO{Path: selection.Path.String(), Location: locationDTO{SourceID: selection.Location.SourceID, Line: selection.Location.Line, Column: selection.Location.Column}, Selector: string(selection.Selector)})
}
document.Sources = append(document.Sources, item)
if plan.sourceID.Set {
break
}
}
if plan.sourceID.Set && len(document.Sources) == 0 {
message := fmt.Sprintf("Source ID %q was not found in configuration scope %q.", plan.sourceID.Value, name)
if acquireErr != nil {
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), sourceSelectionExplanation(acquired.Explanation, message), acquireErr)
}
return configFailure(message, acquired.Explanation, fmt.Errorf("source ID %q was not found", plan.sourceID.Value))
}
if acquireErr == nil && invocation.Context.Err() != nil {
err := invocation.Context.Err()
return configFailure("Configuration source listing was canceled.", acquired.Explanation, err)
}
_ = reporter.Report(commandkit.Report{Value: document, TextTemplate: sourceTextTemplate})
if acquireErr != nil {
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), acquired.Explanation, acquireErr)
}
return commandkit.Success()
}
func sourceSelectionExplanation(explanation commandkit.Explanation, message string) commandkit.Explanation {
explanation.Details = append(append([]commandkit.Detail(nil), explanation.Details...), func(writer io.Writer) error {
_, err := fmt.Fprintln(writer, message)
return err
})
return explanation
}
var sourceTextTemplate = template.Must(commandkit.ParseTextTemplate("sources", `Scope: {{printf "%q" .Scope}}
{{if .Sources}}Sources:
{{range .Sources}} - id: {{printf "%q" .ID}}
label: {{printf "%q" .Label}}
path: {{printf "%q" .Path}}
destination: {{printf "%q" .Destination}}
placement: {{printf "%q" .Placement}}
destination_is_fallback: {{.DestinationIsFallback}}
layer: {{printf "%q" .Layer}}
kind: {{printf "%q" .Kind}}
underlay_index: {{.UnderlayIndex}}
{{if .ImportedFrom}} imported_from: {source_id: {{printf "%q" .ImportedFrom.SourceID}}, line: {{.ImportedFrom.Line}}, column: {{.ImportedFrom.Column}}}
{{else}} imported_from: null
{{end}}{{if .VersionEvidence}} version_evidence:
path: {{printf "%q" .VersionEvidence.Path}}
version:
present: {{.VersionEvidence.Version.Present}}
null: {{.VersionEvidence.Version.Null}}
text: {{printf "%q" .VersionEvidence.Version.Text}}
{{if .VersionEvidence.Location}} location: {source_id: {{printf "%q" .VersionEvidence.Location.SourceID}}, line: {{.VersionEvidence.Location.Line}}, column: {{.VersionEvidence.Location.Column}}}
{{else}} location: null
{{end}} redacted: {{.VersionEvidence.Redacted}}
{{else}} version_evidence: null
{{end}}{{if .EmptyImportSelections}} empty_import_selections:
{{range .EmptyImportSelections}} - path: {{printf "%q" .Path}}
location: {source_id: {{printf "%q" .Location.SourceID}}, line: {{.Location.Line}}, column: {{.Location.Column}}}
selector: {{printf "%q" .Selector}}
{{end}}{{else}} empty_import_selections: []
{{end}}{{end}}{{else}}Sources: []
{{end}}`, nil))
func locationReports(locations []configkit.Location) []locationDTO {
report := make([]locationDTO, 0, len(locations))
for _, location := range locations {
report = append(report, locationDTO{SourceID: location.SourceID, Line: location.Line, Column: location.Column})
}
return report
}