Compile typed command runners
This commit is contained in:
@@ -30,16 +30,15 @@ type versionDocument struct {
|
||||
}
|
||||
|
||||
func (a *App) installBuiltins() error {
|
||||
help := NewCommand(CommandSpec[helpInput]{
|
||||
help := captureCommand(CommandSpec[helpInput]{
|
||||
Name: "help",
|
||||
Summary: "Show help for commands.",
|
||||
Description: "Describe registered commands and their inputs.",
|
||||
Arguments: []Argument[helpInput]{RepeatArgument(ArgumentMeta{Name: "COMMAND", Usage: "registered command path"}, 0, -1,
|
||||
func(value string) (string, error) { return value, nil },
|
||||
func(input *helpInput, value Supplied[[]string]) { input.Targets = value })},
|
||||
Run: func(invocation Invocation, input helpInput) Outcome {
|
||||
return a.renderHelpTargets(input.Targets.Value, invocation.Streams)
|
||||
},
|
||||
}, func(current commandExecution, input helpInput) Outcome {
|
||||
return current.renderHelpTargets(input.Targets.Value)
|
||||
})
|
||||
scopeDefault := "app"
|
||||
formatDefault := "text"
|
||||
|
||||
40
command.go
40
command.go
@@ -119,13 +119,12 @@ type commandPayload interface {
|
||||
hasRun() bool
|
||||
bridgeFlags() []internalcobra.Flag
|
||||
bridgeArguments() []internalcobra.Argument
|
||||
prepare([][]string, []string, bool) (func() any, error)
|
||||
checkInput(any) error
|
||||
run(Invocation, any) Outcome
|
||||
prepare([][]string, []string, bool) (func(*execution) Outcome, error)
|
||||
}
|
||||
|
||||
type typedCommand[I any] struct {
|
||||
spec CommandSpec[I]
|
||||
spec CommandSpec[I]
|
||||
privateRun func(commandExecution, I) Outcome
|
||||
}
|
||||
|
||||
func (c typedCommand[I]) validate() error {
|
||||
@@ -133,7 +132,7 @@ func (c typedCommand[I]) validate() error {
|
||||
}
|
||||
|
||||
func (c typedCommand[I]) hasRun() bool {
|
||||
return c.spec.Run != nil
|
||||
return c.spec.Run != nil || c.privateRun != nil
|
||||
}
|
||||
|
||||
func (c typedCommand[I]) bridgeFlags() []internalcobra.Flag {
|
||||
@@ -167,36 +166,37 @@ func (c typedCommand[I]) bridgeArguments() []internalcobra.Argument {
|
||||
return arguments
|
||||
}
|
||||
|
||||
func (c typedCommand[I]) prepare(flagValues [][]string, operands []string, enforceRequired bool) (func() any, error) {
|
||||
func (c typedCommand[I]) prepare(flagValues [][]string, operands []string, enforceRequired bool) (func(*execution) Outcome, error) {
|
||||
prepared, err := prepareInput(c.spec, flagValues, operands, enforceRequired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() any {
|
||||
return func(current *execution) Outcome {
|
||||
input := prepared.build()
|
||||
return &input
|
||||
if c.spec.CheckInput != nil {
|
||||
if err := c.spec.CheckInput(input); err != nil {
|
||||
return classifyInputError(err)
|
||||
}
|
||||
}
|
||||
if c.privateRun != nil {
|
||||
return c.privateRun(current.commandContext(), input)
|
||||
}
|
||||
return c.spec.Run(Invocation{Context: current.ctx, Streams: current.streams}, input)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c typedCommand[I]) run(invocation Invocation, input any) Outcome {
|
||||
return c.spec.Run(invocation, *input.(*I))
|
||||
}
|
||||
|
||||
func (c typedCommand[I]) checkInput(input any) error {
|
||||
if c.spec.CheckInput == nil {
|
||||
return nil
|
||||
}
|
||||
return c.spec.CheckInput(*input.(*I))
|
||||
}
|
||||
|
||||
// NewCommand captures a typed command declaration. Construction issues are
|
||||
// retained and reported when the declaration is installed.
|
||||
func NewCommand[I any](spec CommandSpec[I]) Command {
|
||||
return captureCommand(spec, nil)
|
||||
}
|
||||
|
||||
func captureCommand[I any](spec CommandSpec[I], privateRun func(commandExecution, I) Outcome) Command {
|
||||
copySpec := spec
|
||||
copySpec.Flags = append([]Flag[I](nil), spec.Flags...)
|
||||
copySpec.Arguments = append([]Argument[I](nil), spec.Arguments...)
|
||||
copySpec.Children = append([]Command(nil), spec.Children...)
|
||||
payload := typedCommand[I]{spec: copySpec}
|
||||
payload := typedCommand[I]{spec: copySpec, privateRun: privateRun}
|
||||
record := &commandRecord{
|
||||
name: spec.Name, summary: spec.Summary, description: spec.Description,
|
||||
children: copySpec.Children, payload: payload,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Implement the optional integration modules and captured registration API
|
||||
|
||||
Status: implementation in progress. Stages 1 through 4 are complete; subsequent work follows the ordered roadmap.
|
||||
Status: implementation in progress. Stages 1 through 5 are complete; subsequent work follows the ordered roadmap.
|
||||
Execute the stages in order. Each stage is sized as a separate Luna coding-agent
|
||||
assignment. This document authorizes no publication or changes to sibling
|
||||
repositories.
|
||||
@@ -289,9 +289,14 @@ operational/usage outcomes respectively. Root tests pass.
|
||||
and writer failures. Backend parse errors become neutral usage diagnostics;
|
||||
public results must not expose Cobra/pflag types.
|
||||
|
||||
**Completion evidence:** factories/checks/handlers execute only on the selected
|
||||
ordinary path; target callbacks remain untouched by help/version. Writer failure
|
||||
and wrapped wiring classifications retain causes. Stage 7 completes routing.
|
||||
**Completion evidence — complete:** command payloads now compile into typed
|
||||
invocation closures without `any` input handoffs. Conversion precedes input
|
||||
materialization; omitted factories preserve the exact zero value. Public handlers
|
||||
receive only `Invocation` and their typed input, while help uses a narrow private
|
||||
presentation context through the same capture/compiler path. The path-name special
|
||||
case for help is gone. Focused execution, pointer-zero-value, help/version callback
|
||||
isolation, writer-failure, and wrapped-error tests pass. Stage 7 remains responsible
|
||||
for routing snapshot/session consolidation.
|
||||
|
||||
## Stage 6: Make batch installation the sole registry mutation path
|
||||
|
||||
|
||||
32
execute.go
32
execute.go
@@ -33,23 +33,11 @@ func (a *App) Execute(ctx context.Context, args []string, streams Streams) Outco
|
||||
}
|
||||
return current.present(current.renderHelp(parsed.Path), parsed.Path)
|
||||
}
|
||||
buildInput, err := command.record.payload.prepare(parsed.FlagValues, parsed.Operands, true)
|
||||
run, err := command.record.payload.prepare(parsed.FlagValues, parsed.Operands, true)
|
||||
if err != nil {
|
||||
return current.present(Usage(err), parsed.Path)
|
||||
}
|
||||
input := buildInput()
|
||||
if err := command.record.payload.checkInput(input); err != nil {
|
||||
return current.present(classifyInputError(err), parsed.Path)
|
||||
}
|
||||
if len(parsed.Path) == 1 && parsed.Path[0] == "help" {
|
||||
help, ok := input.(*helpInput)
|
||||
if !ok {
|
||||
return current.present(Failure(errors.New("built-in help input has an unexpected type")), parsed.Path)
|
||||
}
|
||||
return current.present(current.renderHelpTargets(help.Targets.Value), parsed.Path)
|
||||
}
|
||||
outcome := command.record.payload.run(Invocation{Context: ctx, Streams: streams}, input)
|
||||
return current.present(outcome, parsed.Path)
|
||||
return current.present(run(current), parsed.Path)
|
||||
}
|
||||
|
||||
type execution struct {
|
||||
@@ -61,6 +49,22 @@ type execution struct {
|
||||
session *internalcobra.Session
|
||||
}
|
||||
|
||||
type commandExecution struct {
|
||||
invocation Invocation
|
||||
renderHelpTargetsFunc func([]string) Outcome
|
||||
}
|
||||
|
||||
func (current *execution) commandContext() commandExecution {
|
||||
return commandExecution{
|
||||
invocation: Invocation{Context: current.ctx, Streams: current.streams},
|
||||
renderHelpTargetsFunc: current.renderHelpTargets,
|
||||
}
|
||||
}
|
||||
|
||||
func (current commandExecution) renderHelpTargets(targets []string) Outcome {
|
||||
return current.renderHelpTargetsFunc(targets)
|
||||
}
|
||||
|
||||
func newExecution(app *App, ctx context.Context, streams Streams) *execution {
|
||||
current := &execution{
|
||||
app: app,
|
||||
|
||||
@@ -60,6 +60,25 @@ func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOmittedInputFactoryUsesExactPointerZeroValue(t *testing.T) {
|
||||
command := commandkit.NewCommand(commandkit.CommandSpec[*int]{
|
||||
Name: "run",
|
||||
Run: func(_ commandkit.Invocation, value *int) commandkit.Outcome {
|
||||
if value != nil {
|
||||
return commandkit.Failure(errors.New("omitted input factory allocated a pointer"))
|
||||
}
|
||||
return commandkit.Success()
|
||||
},
|
||||
})
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{}); outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("pointer zero value outcome = %#v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteUsesFreshSessionForHelpAfterFlagBearingInvocation(t *testing.T) {
|
||||
type input struct {
|
||||
name commandkit.Supplied[string]
|
||||
|
||||
Reference in New Issue
Block a user