Expose canonical paths on command invocations
This commit is contained in:
@@ -232,7 +232,7 @@ func (c typedCommand[I]) prepare(flagValues [][]string, operands []string, enfor
|
||||
if c.privateRun != nil {
|
||||
return c.privateRun(current.commandContext(), input)
|
||||
}
|
||||
return c.spec.Run(Invocation{Context: current.ctx, Streams: current.streams}, input)
|
||||
return c.spec.Run(current.invocation(), input)
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Application-owned aliases implementation plan
|
||||
|
||||
Status: in progress; Stages 1–8 completed.
|
||||
Status: in progress; Stages 1–9 completed.
|
||||
|
||||
## Authority, baseline, and scope
|
||||
|
||||
@@ -495,6 +495,13 @@ and canonical forms reach the same binding/callback once; repeated calls and
|
||||
declaration reuse do not leak invocation values; zero Invocation accessors
|
||||
are empty. Existing cancellation and domain outcome checks remain green.
|
||||
|
||||
**Completed:** Invocation now retains the entered and canonical route paths
|
||||
alongside the caller's context and streams. Public typed handlers and private
|
||||
callbacks receive the same resolved identity. Tests cover canonical and alias
|
||||
calls with operands, repeated calls, declaration reuse across Apps, private
|
||||
callback plumbing, and zero-value accessors. Verified with the root, ckconfig,
|
||||
and ckprompt suites under the local workspace.
|
||||
|
||||
## Stage 10: Add alias-aware help and discovery views
|
||||
|
||||
**Work**
|
||||
|
||||
11
execute.go
11
execute.go
@@ -62,11 +62,20 @@ type commandExecution struct {
|
||||
|
||||
func (current *execution) commandContext() commandExecution {
|
||||
return commandExecution{
|
||||
invocation: Invocation{Context: current.ctx, Streams: current.streams},
|
||||
invocation: current.invocation(),
|
||||
renderHelpTargetsFunc: current.renderHelpTargets,
|
||||
}
|
||||
}
|
||||
|
||||
func (current *execution) invocation() Invocation {
|
||||
return Invocation{
|
||||
Context: current.ctx,
|
||||
Streams: current.streams,
|
||||
enteredPath: current.route.enteredPath.String(),
|
||||
canonicalPath: current.route.canonicalPath.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func (current commandExecution) renderHelpTargets(targets []string) Outcome {
|
||||
return current.renderHelpTargetsFunc(targets)
|
||||
}
|
||||
|
||||
11
identity.go
11
identity.go
@@ -34,4 +34,15 @@ type Invocation struct {
|
||||
Context context.Context
|
||||
// Streams are the caller-selected output destinations.
|
||||
Streams Streams
|
||||
|
||||
enteredPath string
|
||||
canonicalPath string
|
||||
}
|
||||
|
||||
// EnteredPath returns the command path used to invoke the selected command.
|
||||
// It is empty for an Invocation that was not created by command execution.
|
||||
func (inv Invocation) EnteredPath() string { return inv.enteredPath }
|
||||
|
||||
// CanonicalPath returns the selected command's canonical path.
|
||||
// It is empty for an Invocation that was not created by command execution.
|
||||
func (inv Invocation) CanonicalPath() string { return inv.canonicalPath }
|
||||
|
||||
@@ -190,6 +190,104 @@ func TestExecuteDispatchesEnteredAliasToCanonicalRunner(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvocationReportsEnteredAndCanonicalPathsPerCall(t *testing.T) {
|
||||
type input struct {
|
||||
label Supplied[string]
|
||||
values Supplied[[]string]
|
||||
}
|
||||
type observed struct {
|
||||
entered string
|
||||
canonical string
|
||||
label string
|
||||
values []string
|
||||
}
|
||||
var calls []observed
|
||||
command := NewCommand(CommandSpec[input]{
|
||||
Name: "run",
|
||||
Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "label"}, (*string)(nil),
|
||||
func(value string) (string, error) { return value, nil },
|
||||
func(target *input, label Supplied[string]) { target.label = label },
|
||||
)},
|
||||
Arguments: []Argument[input]{RepeatArgument(ArgumentMeta{Name: "value"}, 0, -1,
|
||||
func(value string) (string, error) { return value, nil },
|
||||
func(target *input, values Supplied[[]string]) { target.values = values },
|
||||
)},
|
||||
Run: func(inv Invocation, value input) Outcome {
|
||||
calls = append(calls, observed{
|
||||
entered: inv.EnteredPath(),
|
||||
canonical: inv.CanonicalPath(),
|
||||
label: value.label.Value,
|
||||
values: append([]string(nil), value.values.Value...),
|
||||
})
|
||||
return Success()
|
||||
},
|
||||
})
|
||||
shortcut := Alias("shortcut", "workspace run")
|
||||
app, err := New(Identity{Name: "Example"}, Group("workspace", "Workspace", command), shortcut)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, args := range [][]string{{"workspace", "run", "--label=first", "first"}, {"shortcut", "--label=second", "second"}, {"workspace", "run"}} {
|
||||
if outcome := app.Execute(context.Background(), args, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess {
|
||||
t.Fatalf("Execute(%q) = %v, want success", args, outcome.Kind)
|
||||
}
|
||||
}
|
||||
if len(calls) != 3 {
|
||||
t.Fatalf("handler calls = %d, want 3", len(calls))
|
||||
}
|
||||
want := []observed{
|
||||
{entered: "workspace run", canonical: "workspace run", label: "first", values: []string{"first"}},
|
||||
{entered: "shortcut", canonical: "workspace run", label: "second", values: []string{"second"}},
|
||||
{entered: "workspace run", canonical: "workspace run"},
|
||||
}
|
||||
if !reflect.DeepEqual(calls, want) {
|
||||
t.Fatalf("handler invocations = %#v, want %#v", calls, want)
|
||||
}
|
||||
|
||||
secondApp, err := New(Identity{Name: "Another"}, Group("workspace", "Workspace", command), shortcut)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if outcome := secondApp.Execute(context.Background(), []string{"shortcut"}, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess {
|
||||
t.Fatalf("reused declaration execution = %v, want success", outcome.Kind)
|
||||
}
|
||||
if got := calls[3]; got.entered != "shortcut" || got.canonical != "workspace run" || len(got.values) != 0 {
|
||||
t.Fatalf("reused declaration invocation = %#v", got)
|
||||
}
|
||||
|
||||
var zero Invocation
|
||||
if zero.EnteredPath() != "" || zero.CanonicalPath() != "" {
|
||||
t.Fatalf("zero Invocation paths = (%q, %q), want empty paths", zero.EnteredPath(), zero.CanonicalPath())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateCommandCallbacksReceiveResolvedInvocationPaths(t *testing.T) {
|
||||
var calls []Invocation
|
||||
command := Command{record: &commandRecord{
|
||||
name: "inspect",
|
||||
payload: typedCommand[struct{}]{
|
||||
spec: CommandSpec[struct{}]{Name: "inspect"},
|
||||
privateRun: func(exec commandExecution, _ struct{}) Outcome {
|
||||
calls = append(calls, exec.invocation)
|
||||
return Success()
|
||||
},
|
||||
},
|
||||
}}
|
||||
app, err := New(Identity{Name: "Example"}, command, Alias("shortcut", "inspect"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, args := range [][]string{{"inspect"}, {"shortcut"}} {
|
||||
if outcome := app.Execute(context.Background(), args, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess {
|
||||
t.Fatalf("Execute(%q) = %v, want success", args, outcome.Kind)
|
||||
}
|
||||
}
|
||||
if len(calls) != 2 || calls[0].EnteredPath() != "inspect" || calls[0].CanonicalPath() != "inspect" || calls[1].EnteredPath() != "shortcut" || calls[1].CanonicalPath() != "inspect" {
|
||||
t.Fatalf("private callback invocations = %#v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteAppliesCanonicalCommandFlagAliasesAfterRouting(t *testing.T) {
|
||||
type input struct{ color string }
|
||||
var got string
|
||||
|
||||
Reference in New Issue
Block a user