Add configuration source reports
This commit is contained in:
@@ -6,8 +6,8 @@ and shared command infrastructure across related applications.
|
|||||||
The current Go library provides an instance-owned command registry, typed flag
|
The current Go library provides an instance-owned command registry, typed flag
|
||||||
and operand bindings, Cobra-backed routing, invocation outcomes, generated help,
|
and operand bindings, Cobra-backed routing, invocation outcomes, generated help,
|
||||||
application or CommandKit version reporting, configuration scope discovery, and
|
application or CommandKit version reporting, configuration scope discovery, and
|
||||||
ConfigKit-backed validation and native effective YAML display. Source and
|
ConfigKit-backed validation, native effective YAML display, and source inventory
|
||||||
explanation reports and PromptKit commands remain planned.
|
reports. Explanation reports and PromptKit commands remain planned.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
12
config.go
12
config.go
@@ -131,8 +131,8 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
|
|||||||
return fmt.Errorf("command path %q is already registered", "config")
|
return fmt.Errorf("command path %q is already registered", "config")
|
||||||
}
|
}
|
||||||
|
|
||||||
commands := make([]Command, 0, 3)
|
commands := make([]Command, 0, 4)
|
||||||
installed := make([]ConfigOperation, 0, 2)
|
installed := make([]ConfigOperation, 0, 3)
|
||||||
if configOperationDeclared(normalized.Scopes, ConfigValidate) {
|
if configOperationDeclared(normalized.Scopes, ConfigValidate) {
|
||||||
validate, err := newConfigValidateCommand(normalized)
|
validate, err := newConfigValidateCommand(normalized)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -149,6 +149,14 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
|
|||||||
commands = append(commands, show)
|
commands = append(commands, show)
|
||||||
installed = append(installed, ConfigShow)
|
installed = append(installed, ConfigShow)
|
||||||
}
|
}
|
||||||
|
if configOperationDeclared(normalized.Scopes, ConfigSources) {
|
||||||
|
sources, err := newConfigSourcesCommand(normalized)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
commands = append(commands, sources)
|
||||||
|
installed = append(installed, ConfigSources)
|
||||||
|
}
|
||||||
command, err := newConfigScopesCommandWithScopes(normalized.Scopes, installed)
|
command, err := newConfigScopesCommandWithScopes(normalized.Scopes, installed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func TestConfigScopeDiscoveryOrdersAndDeduplicatesCapabilities(t *testing.T) {
|
|||||||
if outcome.Kind != OutcomeSuccess {
|
if outcome.Kind != OutcomeSuccess {
|
||||||
t.Fatalf("text inventory outcome = %#v", outcome)
|
t.Fatalf("text inventory outcome = %#v", outcome)
|
||||||
}
|
}
|
||||||
wantText := "app: validate, show\npromptkit:\nmetadata:\nzeta: show\n"
|
wantText := "app: validate, show\npromptkit: sources\nmetadata:\nzeta: show\n"
|
||||||
if textOutput.String() != wantText {
|
if textOutput.String() != wantText {
|
||||||
t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText)
|
t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,155 @@ func (document effectiveYAML) WriteEffective(writer io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type capturedSourceReader struct {
|
||||||
|
entries []configkit.SourceInfo
|
||||||
|
calls int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (reader *capturedSourceReader) Sources() []configkit.SourceInfo {
|
||||||
|
reader.calls++
|
||||||
|
return reader.entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSourcesReportsCapturedConfigKitSourceInventory(t *testing.T) {
|
||||||
|
type configuration struct {
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
Shared string `yaml:"shared"`
|
||||||
|
Synthetic string `yaml:"synthetic"`
|
||||||
|
}
|
||||||
|
filesystem := configkit.NewFilesystem("fixture", fstest.MapFS{
|
||||||
|
"root/config/main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: ../shared.yml}]}\nvalue: base\n")},
|
||||||
|
"root/shared.yml": &fstest.MapFile{Data: []byte("shared: imported\n")},
|
||||||
|
"root/overlays/dev.yml": &fstest.MapFile{Data: []byte("value: overlay\n")},
|
||||||
|
})
|
||||||
|
filesystemContext := &configkit.FilesystemContext{Root: "root", FS: &filesystem}
|
||||||
|
overlayFilesystemContext := &configkit.FilesystemContext{Root: "root", BaseDir: "root/config", FS: &filesystem}
|
||||||
|
loaderCalls := 0
|
||||||
|
reader := &capturedSourceReader{}
|
||||||
|
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
|
||||||
|
loaderCalls++
|
||||||
|
result, err := configkit.LoadLayered(configkit.Input{
|
||||||
|
Source: filesystem.File("root/config/main.yml"),
|
||||||
|
Imports: true,
|
||||||
|
Filesystem: filesystemContext,
|
||||||
|
}, configkit.LayeredOptions[configuration]{
|
||||||
|
Definition: configkit.Definition[configuration]{},
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("synthetic source", map[string]any{"synthetic": "underlay"})}}}},
|
||||||
|
Overlay: "dev",
|
||||||
|
OverlayFilesystem: overlayFilesystemContext,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reader.entries = result.Sources()
|
||||||
|
return result, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Replace the result's reader after the owning loader has captured its source inventory.
|
||||||
|
originalAcquire := scope.Acquire
|
||||||
|
scope.Acquire = func(ctx context.Context, request commandkit.ConfigRequest, input struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||||
|
acquired, err := originalAcquire(ctx, request, input)
|
||||||
|
if err == nil {
|
||||||
|
acquired.Value.Sources = reader
|
||||||
|
}
|
||||||
|
return acquired, err
|
||||||
|
}
|
||||||
|
// Register the wrapped scope so the command reads the captured public facet once.
|
||||||
|
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||||
|
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), []string{"config", "sources", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess || loaderCalls != 1 || reader.calls != 1 {
|
||||||
|
var details bytes.Buffer
|
||||||
|
for _, diagnostic := range outcome.Diagnostics {
|
||||||
|
_ = diagnostic.Write(&details)
|
||||||
|
}
|
||||||
|
t.Fatalf("sources outcome=%#v cause=%v details=%q loader calls=%d reader calls=%d", outcome, outcome.Cause, details.String(), loaderCalls, reader.calls)
|
||||||
|
}
|
||||||
|
var report struct {
|
||||||
|
Scope string `yaml:"scope"`
|
||||||
|
Sources []struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Label string `yaml:"label"`
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Layer string `yaml:"layer"`
|
||||||
|
Kind string `yaml:"kind"`
|
||||||
|
UnderlayIndex int `yaml:"underlay_index"`
|
||||||
|
ImportedFrom *struct {
|
||||||
|
SourceID string `yaml:"source_id"`
|
||||||
|
Line int `yaml:"line"`
|
||||||
|
Column int `yaml:"column"`
|
||||||
|
} `yaml:"imported_from"`
|
||||||
|
} `yaml:"sources"`
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(output.Bytes(), &report); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if report.Scope != "app" || len(report.Sources) != 4 {
|
||||||
|
t.Fatalf("source report = %#v", report)
|
||||||
|
}
|
||||||
|
if report.Sources[0].Path != "root/config/main.yml" || report.Sources[0].ID != "source-1" {
|
||||||
|
t.Fatalf("main source = %#v", report.Sources[0])
|
||||||
|
}
|
||||||
|
if report.Sources[0].ImportedFrom != nil {
|
||||||
|
t.Fatalf("root source imported_from = %#v, want null", report.Sources[0].ImportedFrom)
|
||||||
|
}
|
||||||
|
imported := report.Sources[1]
|
||||||
|
if imported.Path != "root/shared.yml" || imported.ImportedFrom == nil || imported.ImportedFrom.SourceID != report.Sources[0].ID || imported.ImportedFrom.Line == 0 {
|
||||||
|
t.Fatalf("import source correlation = %#v", imported)
|
||||||
|
}
|
||||||
|
synthetic := report.Sources[2]
|
||||||
|
if synthetic.Kind != string(configkit.SourceKindMapping) || synthetic.Path != "" || synthetic.Layer != string(configkit.LayerUnderlay) || synthetic.UnderlayIndex != 1 {
|
||||||
|
t.Fatalf("synthetic underlay source = %#v", synthetic)
|
||||||
|
}
|
||||||
|
if report.Sources[3].Layer != string(configkit.LayerOverlay) || report.Sources[3].Path != "root/overlays/dev.yml" {
|
||||||
|
t.Fatalf("overlay source = %#v", report.Sources[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSourcesHandlesEmptyInventoryAllPreflightAndWriterFailure(t *testing.T) {
|
||||||
|
reader := &capturedSourceReader{}
|
||||||
|
newContextCalls, acquireCalls := 0, 0
|
||||||
|
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
suite := commandkit.ConfigSuite[string]{
|
||||||
|
NewContext: func() string { newContextCalls++; return "context" },
|
||||||
|
Scopes: []commandkit.ConfigScope[string]{{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigSources}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||||
|
acquireCalls++
|
||||||
|
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Sources: reader}}, nil
|
||||||
|
}}},
|
||||||
|
}
|
||||||
|
if err := commandkit.RegisterConfig(app, suite); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), []string{"config", "sources", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeUsage || newContextCalls != 0 || acquireCalls != 0 || output.Len() != 0 {
|
||||||
|
t.Fatalf("all preflight outcome=%#v contexts=%d acquires=%d output=%q", outcome, newContextCalls, acquireCalls, output.String())
|
||||||
|
}
|
||||||
|
outcome = app.Execute(context.Background(), []string{"config", "sources", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess || output.String() != "scope: app\nsources: []\n" || reader.calls != 1 {
|
||||||
|
t.Fatalf("empty source report outcome=%#v output=%q reader calls=%d", outcome, output.String(), reader.calls)
|
||||||
|
}
|
||||||
|
reader.entries = []configkit.SourceInfo{{ID: "source-1", Label: "label with \"quotes\"", Path: "path with spaces.yml", Layer: configkit.LayerBase, Kind: configkit.SourceKindFile}}
|
||||||
|
var textOutput bytes.Buffer
|
||||||
|
outcome = app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: &textOutput, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(textOutput.String(), `label: "label with \"quotes\""`) || !strings.Contains(textOutput.String(), `path: "path with spaces.yml"`) {
|
||||||
|
t.Fatalf("quoted text source output outcome=%#v output=%q", outcome, textOutput.String())
|
||||||
|
}
|
||||||
|
partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe}
|
||||||
|
outcome = app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: partial, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) {
|
||||||
|
t.Fatalf("source writer failure = %#v", outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfigShowWritesNativeSingleScopeAndFramesAggregateYAML(t *testing.T) {
|
func TestConfigShowWritesNativeSingleScopeAndFramesAggregateYAML(t *testing.T) {
|
||||||
const native = "%YAML 1.1\n# authored comment\n---\nvalue: null\nitems: []\nmissing: {}\n"
|
const native = "%YAML 1.1\n# authored comment\n---\nvalue: null\nitems: []\nmissing: {}\n"
|
||||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||||
|
|||||||
26
docs/cli.md
26
docs/cli.md
@@ -147,6 +147,32 @@ one comment line. A provider failure leaves the output stream empty. A rendering
|
|||||||
or output failure may leave an aggregate prefix, and the outcome reports
|
or output failure may leave an aggregate prefix, and the outcome reports
|
||||||
incomplete report delivery.
|
incomplete report delivery.
|
||||||
|
|
||||||
|
### List configuration sources
|
||||||
|
|
||||||
|
`config sources` reports the source inventory captured during the selected
|
||||||
|
scope's acquisition. It reads the source facet once and does not reload
|
||||||
|
configuration. The default scope is `app`; `--scope all` is unsupported. Text is
|
||||||
|
the default format, and `--format yaml` returns a structured report:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
scope: app
|
||||||
|
sources:
|
||||||
|
- id: source-1
|
||||||
|
label: "application config"
|
||||||
|
path: /etc/example/config.yml
|
||||||
|
layer: base
|
||||||
|
kind: file
|
||||||
|
underlay_index: 0
|
||||||
|
imported_from: null
|
||||||
|
```
|
||||||
|
|
||||||
|
Each source includes `id`, `label`, `path`, `layer`, `kind`, `underlay_index`,
|
||||||
|
and `imported_from`. The import location is null when absent; otherwise it has
|
||||||
|
`source_id`, `line`, and `column`. Owning values and source order are preserved,
|
||||||
|
including empty fields, zero coordinates, and synthetic sources without paths.
|
||||||
|
An empty inventory is `sources: []`. Text output begins with the scope heading
|
||||||
|
and then prints the same source fields, quoting labels and paths.
|
||||||
|
|
||||||
## Outcome status
|
## Outcome status
|
||||||
|
|
||||||
CommandKit usage failures use status `2`; operational failures use status `1`.
|
CommandKit usage failures use status `2`; operational failures use status `1`.
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ decisions, implementation plans, identity and outcome primitives, typed command
|
|||||||
declarations, an instance-owned registry, a Cobra-backed routing and parsing
|
declarations, an instance-owned registry, a Cobra-backed routing and parsing
|
||||||
adapter, public `App.Execute` coordination, and generated help/version commands.
|
adapter, public `App.Execute` coordination, and generated help/version commands.
|
||||||
Configuration suite registration, scope discovery, ConfigKit integration,
|
Configuration suite registration, scope discovery, ConfigKit integration,
|
||||||
validation, and native effective YAML display are implemented; other
|
validation, native effective YAML display, and source inventory reports are
|
||||||
configuration and PromptKit operations remain pending. The
|
implemented; other configuration and PromptKit operations remain pending. The
|
||||||
[README](../README.md) provides project orientation; the
|
[README](../README.md) provides project orientation; the
|
||||||
[documentation policy](policy/documentation.md#canonical-ownership) assigns
|
[documentation policy](policy/documentation.md#canonical-ownership) assigns
|
||||||
canonical documentation owners.
|
canonical documentation owners.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Initial CommandKit implementation plan
|
# Initial CommandKit implementation plan
|
||||||
|
|
||||||
Status: implementation underway; Stages 1-9 complete and Stages 10-14 pending.
|
Status: implementation underway; Stages 1-10 complete and Stages 11-14 pending.
|
||||||
|
|
||||||
Date: 2026-10-07.
|
Date: 2026-10-07.
|
||||||
|
|
||||||
@@ -645,7 +645,7 @@ passes; README, development orientation, and CLI reference describe the feature.
|
|||||||
|
|
||||||
## Stage 10: implement source inventory reports
|
## Stage 10: implement source inventory reports
|
||||||
|
|
||||||
**Status:** pending. **Prerequisite:** Stage 9.
|
**Status:** complete. **Prerequisite:** Stage 9.
|
||||||
|
|
||||||
1. Implement single-scope `config sources`, default text with yaml alternative.
|
1. Implement single-scope `config sources`, default text with yaml alternative.
|
||||||
Read the captured SourceReader once. Never reload sources for this report.
|
Read the captured SourceReader once. Never reload sources for this report.
|
||||||
@@ -664,6 +664,16 @@ one captured acquisition, YAML field types and import correlation, empty invento
|
|||||||
unsupported all usage preflight, writer failure. Update the source-report CLI
|
unsupported all usage preflight, writer failure. Update the source-report CLI
|
||||||
reference with the implemented shape.
|
reference with the implemented shape.
|
||||||
|
|
||||||
|
**Completion evidence:** installed `config sources` for scopes declaring the
|
||||||
|
capability. The command reads the acquired source facet once and renders explicit
|
||||||
|
snake-case YAML DTOs or a quoted text inventory, preserving source order, empty
|
||||||
|
and zero values, synthetic paths, and import locations. Checks use a real
|
||||||
|
ConfigKit filesystem root, import, overlay, and mapping underlay; they verify the
|
||||||
|
YAML field types and import correlation, captured acquisition, empty inventory,
|
||||||
|
text quoting, unsupported aggregate preflight, and partial writer failure.
|
||||||
|
`go test ./...` passes; README, development orientation, and CLI reference
|
||||||
|
describe the report.
|
||||||
|
|
||||||
## Stage 11: implement path explanation reports
|
## Stage 11: implement path explanation reports
|
||||||
|
|
||||||
**Status:** pending. **Prerequisite:** Stage 10.
|
**Status:** pending. **Prerequisite:** Stage 10.
|
||||||
|
|||||||
128
sources.go
Normal file
128
sources.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package commandkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newConfigSourcesCommand[C any](suite ConfigSuite[C]) (Command, error) {
|
||||||
|
spec := CommandSpec[configInput[C]]{
|
||||||
|
Name: string(ConfigSources),
|
||||||
|
Summary: "List configuration sources used by a scope.",
|
||||||
|
Flags: configInputFlags(suite, "text"),
|
||||||
|
CheckInput: func(input configInput[C]) error {
|
||||||
|
_, err := preflightConfig(suite, input, ConfigSources)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
Run: func(invocation Invocation, input configInput[C]) Outcome {
|
||||||
|
return runConfigSources(invocation, suite, input)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return NewCommand(spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sourceReport struct {
|
||||||
|
Scope string `yaml:"scope"`
|
||||||
|
Sources []sourceDTO `yaml:"sources"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type sourceDTO struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Label string `yaml:"label"`
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Layer string `yaml:"layer"`
|
||||||
|
Kind string `yaml:"kind"`
|
||||||
|
UnderlayIndex int `yaml:"underlay_index"`
|
||||||
|
ImportedFrom *locationDTO `yaml:"imported_from"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type locationDTO struct {
|
||||||
|
SourceID string `yaml:"source_id"`
|
||||||
|
Line int `yaml:"line"`
|
||||||
|
Column int `yaml:"column"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfigSources[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome {
|
||||||
|
scopeNames, err := selectConfigScopes(suite, input, ConfigSources)
|
||||||
|
if err != nil {
|
||||||
|
return Failure(err)
|
||||||
|
}
|
||||||
|
contexts := contextsForScopes(suite, input, scopeNames)
|
||||||
|
name := scopeNames[0]
|
||||||
|
scope, ok := findConfigScope(suite.Scopes, name)
|
||||||
|
if !ok || scope.Acquire == nil {
|
||||||
|
return sourceReportFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false)
|
||||||
|
}
|
||||||
|
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigSources, Selection: input.Selection}, contexts[name])
|
||||||
|
diagnostics := append([]Diagnostic(nil), acquired.Diagnostics...)
|
||||||
|
if acquireErr != nil {
|
||||||
|
return sourceReportFailure(acquireErr, diagnostics, false)
|
||||||
|
}
|
||||||
|
if acquired.Value.Sources == nil {
|
||||||
|
return sourceReportFailure(fmt.Errorf("configuration scope %q did not provide a source reader", name), diagnostics, false)
|
||||||
|
}
|
||||||
|
sources := acquired.Value.Sources.Sources()
|
||||||
|
document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))}
|
||||||
|
for _, source := range sources {
|
||||||
|
item := sourceDTO{
|
||||||
|
ID: source.ID,
|
||||||
|
Label: source.Label,
|
||||||
|
Path: source.Path,
|
||||||
|
Layer: string(source.Layer),
|
||||||
|
Kind: string(source.Kind),
|
||||||
|
UnderlayIndex: source.UnderlayIndex,
|
||||||
|
}
|
||||||
|
if source.ImportedFrom != nil {
|
||||||
|
item.ImportedFrom = &locationDTO{SourceID: source.ImportedFrom.SourceID, Line: source.ImportedFrom.Line, Column: source.ImportedFrom.Column}
|
||||||
|
}
|
||||||
|
document.Sources = append(document.Sources, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
var report bytes.Buffer
|
||||||
|
if input.Format == "yaml" {
|
||||||
|
if err := encodeYAML(&report, document); err != nil {
|
||||||
|
return sourceReportFailure(err, diagnostics, false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeSourceReportText(&report, document)
|
||||||
|
}
|
||||||
|
if err := writeReport(invocation.Streams.Out, report.Bytes()); err != nil {
|
||||||
|
outcome := sourceReportFailure(err, diagnostics, true)
|
||||||
|
outcome.DeliveryError = err
|
||||||
|
return outcome
|
||||||
|
}
|
||||||
|
outcome := Success()
|
||||||
|
outcome.Diagnostics = diagnostics
|
||||||
|
return outcome
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSourceReportText(writer *bytes.Buffer, document sourceReport) {
|
||||||
|
_, _ = fmt.Fprintf(writer, "Scope: %s\n", strconv.Quote(document.Scope))
|
||||||
|
if len(document.Sources) == 0 {
|
||||||
|
_, _ = writer.WriteString("Sources: []\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = writer.WriteString("Sources:\n")
|
||||||
|
for _, source := range document.Sources {
|
||||||
|
_, _ = fmt.Fprintf(writer, " - id: %s\n", strconv.Quote(source.ID))
|
||||||
|
_, _ = fmt.Fprintf(writer, " label: %s\n", strconv.Quote(source.Label))
|
||||||
|
_, _ = fmt.Fprintf(writer, " path: %s\n", strconv.Quote(source.Path))
|
||||||
|
_, _ = fmt.Fprintf(writer, " layer: %s\n", strconv.Quote(source.Layer))
|
||||||
|
_, _ = fmt.Fprintf(writer, " kind: %s\n", strconv.Quote(source.Kind))
|
||||||
|
_, _ = fmt.Fprintf(writer, " underlay_index: %d\n", source.UnderlayIndex)
|
||||||
|
if source.ImportedFrom == nil {
|
||||||
|
_, _ = writer.WriteString(" imported_from: null\n")
|
||||||
|
} else {
|
||||||
|
_, _ = fmt.Fprintf(writer, " imported_from: {source_id: %s, line: %d, column: %d}\n",
|
||||||
|
strconv.Quote(source.ImportedFrom.SourceID), source.ImportedFrom.Line, source.ImportedFrom.Column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sourceReportFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
|
||||||
|
outcome := Failure(cause)
|
||||||
|
outcome.Diagnostics = diagnostics
|
||||||
|
outcome.ReportIncomplete = reportStarted
|
||||||
|
return outcome
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user