Compile typed command runners
This commit is contained in:
@@ -30,16 +30,15 @@ type versionDocument struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) installBuiltins() error {
|
func (a *App) installBuiltins() error {
|
||||||
help := NewCommand(CommandSpec[helpInput]{
|
help := captureCommand(CommandSpec[helpInput]{
|
||||||
Name: "help",
|
Name: "help",
|
||||||
Summary: "Show help for commands.",
|
Summary: "Show help for commands.",
|
||||||
Description: "Describe registered commands and their inputs.",
|
Description: "Describe registered commands and their inputs.",
|
||||||
Arguments: []Argument[helpInput]{RepeatArgument(ArgumentMeta{Name: "COMMAND", Usage: "registered command path"}, 0, -1,
|
Arguments: []Argument[helpInput]{RepeatArgument(ArgumentMeta{Name: "COMMAND", Usage: "registered command path"}, 0, -1,
|
||||||
func(value string) (string, error) { return value, nil },
|
func(value string) (string, error) { return value, nil },
|
||||||
func(input *helpInput, value Supplied[[]string]) { input.Targets = value })},
|
func(input *helpInput, value Supplied[[]string]) { input.Targets = value })},
|
||||||
Run: func(invocation Invocation, input helpInput) Outcome {
|
}, func(current commandExecution, input helpInput) Outcome {
|
||||||
return a.renderHelpTargets(input.Targets.Value, invocation.Streams)
|
return current.renderHelpTargets(input.Targets.Value)
|
||||||
},
|
|
||||||
})
|
})
|
||||||
scopeDefault := "app"
|
scopeDefault := "app"
|
||||||
formatDefault := "text"
|
formatDefault := "text"
|
||||||
|
|||||||
40
command.go
40
command.go
@@ -119,13 +119,12 @@ type commandPayload interface {
|
|||||||
hasRun() bool
|
hasRun() bool
|
||||||
bridgeFlags() []internalcobra.Flag
|
bridgeFlags() []internalcobra.Flag
|
||||||
bridgeArguments() []internalcobra.Argument
|
bridgeArguments() []internalcobra.Argument
|
||||||
prepare([][]string, []string, bool) (func() any, error)
|
prepare([][]string, []string, bool) (func(*execution) Outcome, error)
|
||||||
checkInput(any) error
|
|
||||||
run(Invocation, any) Outcome
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type typedCommand[I any] struct {
|
type typedCommand[I any] struct {
|
||||||
spec CommandSpec[I]
|
spec CommandSpec[I]
|
||||||
|
privateRun func(commandExecution, I) Outcome
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c typedCommand[I]) validate() error {
|
func (c typedCommand[I]) validate() error {
|
||||||
@@ -133,7 +132,7 @@ func (c typedCommand[I]) validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c typedCommand[I]) hasRun() bool {
|
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 {
|
func (c typedCommand[I]) bridgeFlags() []internalcobra.Flag {
|
||||||
@@ -167,36 +166,37 @@ func (c typedCommand[I]) bridgeArguments() []internalcobra.Argument {
|
|||||||
return arguments
|
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)
|
prepared, err := prepareInput(c.spec, flagValues, operands, enforceRequired)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return func() any {
|
return func(current *execution) Outcome {
|
||||||
input := prepared.build()
|
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
|
}, 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
|
// NewCommand captures a typed command declaration. Construction issues are
|
||||||
// retained and reported when the declaration is installed.
|
// retained and reported when the declaration is installed.
|
||||||
func NewCommand[I any](spec CommandSpec[I]) Command {
|
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 := spec
|
||||||
copySpec.Flags = append([]Flag[I](nil), spec.Flags...)
|
copySpec.Flags = append([]Flag[I](nil), spec.Flags...)
|
||||||
copySpec.Arguments = append([]Argument[I](nil), spec.Arguments...)
|
copySpec.Arguments = append([]Argument[I](nil), spec.Arguments...)
|
||||||
copySpec.Children = append([]Command(nil), spec.Children...)
|
copySpec.Children = append([]Command(nil), spec.Children...)
|
||||||
payload := typedCommand[I]{spec: copySpec}
|
payload := typedCommand[I]{spec: copySpec, privateRun: privateRun}
|
||||||
record := &commandRecord{
|
record := &commandRecord{
|
||||||
name: spec.Name, summary: spec.Summary, description: spec.Description,
|
name: spec.Name, summary: spec.Summary, description: spec.Description,
|
||||||
children: copySpec.Children, payload: payload,
|
children: copySpec.Children, payload: payload,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Implement the optional integration modules and captured registration API
|
# 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
|
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
|
assignment. This document authorizes no publication or changes to sibling
|
||||||
repositories.
|
repositories.
|
||||||
@@ -289,9 +289,14 @@ operational/usage outcomes respectively. Root tests pass.
|
|||||||
and writer failures. Backend parse errors become neutral usage diagnostics;
|
and writer failures. Backend parse errors become neutral usage diagnostics;
|
||||||
public results must not expose Cobra/pflag types.
|
public results must not expose Cobra/pflag types.
|
||||||
|
|
||||||
**Completion evidence:** factories/checks/handlers execute only on the selected
|
**Completion evidence — complete:** command payloads now compile into typed
|
||||||
ordinary path; target callbacks remain untouched by help/version. Writer failure
|
invocation closures without `any` input handoffs. Conversion precedes input
|
||||||
and wrapped wiring classifications retain causes. Stage 7 completes routing.
|
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
|
## 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)
|
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 {
|
if err != nil {
|
||||||
return current.present(Usage(err), parsed.Path)
|
return current.present(Usage(err), parsed.Path)
|
||||||
}
|
}
|
||||||
input := buildInput()
|
return current.present(run(current), parsed.Path)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type execution struct {
|
type execution struct {
|
||||||
@@ -61,6 +49,22 @@ type execution struct {
|
|||||||
session *internalcobra.Session
|
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 {
|
func newExecution(app *App, ctx context.Context, streams Streams) *execution {
|
||||||
current := &execution{
|
current := &execution{
|
||||||
app: app,
|
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) {
|
func TestExecuteUsesFreshSessionForHelpAfterFlagBearingInvocation(t *testing.T) {
|
||||||
type input struct {
|
type input struct {
|
||||||
name commandkit.Supplied[string]
|
name commandkit.Supplied[string]
|
||||||
|
|||||||
Reference in New Issue
Block a user