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 }