From 814856e8dd364e46ed3af165ed80fb9f7899bd47 Mon Sep 17 00:00:00 2001 From: Nick DiZazzo Date: Mon, 18 Aug 2025 18:09:40 -0400 Subject: [PATCH] refactor: implement comprehensive DRY patterns for actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add generic BaseConstructor pattern to eliminate constructor boilerplate • Create ParameterResolver for centralized parameter validation and resolution • Implement OutputBuilder for standardized action output generation • Refactor all File, System, and Docker actions to use new patterns • Add comprehensive refactoring documentation and migration guide • Reduce code duplication across 48+ action files while maintaining functionality --- actions/common/base_constructor.go | 47 ++ actions/common/base_output_builder.go | 226 ++++++++ actions/common/output_builder.go | 153 ++++++ actions/common/parameter_resolver.go | 206 +++++++ .../docker/check_container_health_action.go | 67 +-- .../check_container_health_action_test.go | 4 +- actions/docker/docker_compose_down_action.go | 79 ++- .../docker/docker_compose_down_action_test.go | 2 +- actions/docker/docker_compose_exec_action.go | 63 +-- .../docker/docker_compose_exec_action_test.go | 4 +- actions/docker/docker_compose_ls_action.go | 108 ++-- .../docker/docker_compose_ls_action_test.go | 2 +- actions/docker/docker_compose_ps_action.go | 88 ++- .../docker/docker_compose_ps_action_test.go | 4 +- actions/docker/docker_compose_up_action.go | 42 +- .../docker/docker_compose_up_action_test.go | 2 +- actions/docker/docker_generic_action.go | 31 +- actions/docker/docker_image_list_action.go | 98 ++-- .../docker/docker_image_list_action_test.go | 2 +- actions/docker/docker_image_rm_action.go | 97 ++-- actions/docker/docker_image_rm_action_test.go | 4 +- actions/docker/docker_load_action.go | 65 +-- actions/docker/docker_ps_action.go | 137 ++--- actions/docker/docker_ps_action_test.go | 10 +- actions/docker/docker_pull_action.go | 45 +- actions/docker/docker_run_action.go | 75 ++- actions/docker/docker_status_action.go | 87 ++- actions/file/change_ownership_action.go | 73 +-- actions/file/change_permissions_action.go | 72 ++- actions/file/compress_file_action.go | 65 +-- actions/file/copy_file_action.go | 59 +- actions/file/create_directories_action.go | 49 +- actions/file/create_symlink_action.go | 55 +- actions/file/decompress_file_action.go | 62 +-- actions/file/delete_path_action.go | 46 +- actions/file/extract_file_action.go | 62 +-- actions/file/move_file_action.go | 67 ++- actions/file/read_file_action.go | 64 +-- actions/file/replace_lines_action.go | 36 +- actions/file/replace_lines_action_test.go | 4 +- actions/file/write_file_action.go | 92 ++-- actions/system/manage_service_action.go | 78 ++- actions/system/service_status_action.go | 63 +-- actions/system/shutdown_action.go | 75 +-- actions/system/update_packages_action.go | 47 +- actions/system/update_packages_action_test.go | 2 +- actions/utility/fetch_interfaces_action.go | 25 +- actions/utility/prerequisite_check_action.go | 28 +- actions/utility/read_mac_action.go | 53 +- actions/utility/read_mac_action_test.go | 2 +- actions/utility/wait_action.go | 63 +-- docs/REFACTORING.md | 506 ++++++++++++++++++ 52 files changed, 2129 insertions(+), 1367 deletions(-) create mode 100644 actions/common/base_constructor.go create mode 100644 actions/common/base_output_builder.go create mode 100644 actions/common/output_builder.go create mode 100644 actions/common/parameter_resolver.go create mode 100644 docs/REFACTORING.md diff --git a/actions/common/base_constructor.go b/actions/common/base_constructor.go new file mode 100644 index 0000000..0b2249c --- /dev/null +++ b/actions/common/base_constructor.go @@ -0,0 +1,47 @@ +package common + +import ( + "log/slog" + + task_engine "github.com/ndizazzo/task-engine" +) + +// BaseConstructor provides common constructor functionality for all actions +type BaseConstructor[T task_engine.ActionInterface] struct { + logger *slog.Logger +} + +// NewBaseConstructor creates a new base constructor with the given logger +func NewBaseConstructor[T task_engine.ActionInterface](logger *slog.Logger) *BaseConstructor[T] { + return &BaseConstructor[T]{logger: logger} +} + +// GetLogger returns the logger from the base constructor +func (c *BaseConstructor[T]) GetLogger() *slog.Logger { + return c.logger +} + +// WrapAction wraps an action with common fields and handles ID generation +func (c *BaseConstructor[T]) WrapAction( + action T, + name string, + id ...string, +) *task_engine.Action[T] { + actionID := "" + if len(id) > 0 && id[0] != "" { + actionID = id[0] + } else { + actionID = generateActionID(name) + } + + return &task_engine.Action[T]{ + ID: actionID, + Name: name, + Wrapped: action, + } +} + +// generateActionID creates a consistent action ID from name +func generateActionID(name string) string { + return task_engine.SanitizeIDPart(name) + "-action" +} diff --git a/actions/common/base_output_builder.go b/actions/common/base_output_builder.go new file mode 100644 index 0000000..e95e87f --- /dev/null +++ b/actions/common/base_output_builder.go @@ -0,0 +1,226 @@ +package common + +import ( + "context" + "fmt" + "log/slog" + "reflect" + "strings" + + task_engine "github.com/ndizazzo/task-engine" +) + +// BaseOutputBuilder provides generic functionality for building action outputs +// and resolving parameters, eliminating duplicate code across actions +type BaseOutputBuilder[T any] struct { + logger *slog.Logger +} + +// NewBaseOutputBuilder creates a new base output builder with the given logger +func NewBaseOutputBuilder[T any](logger *slog.Logger) *BaseOutputBuilder[T] { + return &BaseOutputBuilder[T]{logger: logger} +} + +// GetLogger returns the logger from the base output builder +func (b *BaseOutputBuilder[T]) GetLogger() *slog.Logger { + return b.logger +} + +// ResolveParameter is a generic helper for resolving action parameters +// It handles the common pattern of extracting GlobalContext and calling Resolve +func (b *BaseOutputBuilder[T]) ResolveParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) (interface{}, error) { + if param == nil { + return nil, fmt.Errorf("%s parameter cannot be nil", paramName) + } + + // Extract GlobalContext from context + var globalContext *task_engine.GlobalContext + if gc, ok := ctx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { + globalContext = gc + } + + // Resolve the parameter + value, err := param.Resolve(ctx, globalContext) + if err != nil { + return nil, fmt.Errorf("failed to resolve %s parameter: %w", paramName, err) + } + + return value, nil +} + +// ResolveStringParameter resolves a parameter and converts it to a string +func (b *BaseOutputBuilder[T]) ResolveStringParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) (string, error) { + value, err := b.ResolveParameter(ctx, param, paramName) + if err != nil { + return "", err + } + + if str, ok := value.(string); ok { + return str, nil + } + + return "", fmt.Errorf("%s parameter resolved to non-string value: %T", paramName, value) +} + +// ResolveBoolParameter resolves a parameter and converts it to a boolean +func (b *BaseOutputBuilder[T]) ResolveBoolParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) (bool, error) { + value, err := b.ResolveParameter(ctx, param, paramName) + if err != nil { + return false, err + } + + if b, ok := value.(bool); ok { + return b, nil + } + + return false, fmt.Errorf("%s parameter resolved to non-boolean value: %T", paramName, value) +} + +// ResolveIntParameter resolves a parameter and converts it to an integer +func (b *BaseOutputBuilder[T]) ResolveIntParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) (int, error) { + value, err := b.ResolveParameter(ctx, param, paramName) + if err != nil { + return 0, err + } + + if i, ok := value.(int); ok { + return i, nil + } + + return 0, fmt.Errorf("%s parameter resolved to non-integer value: %T", paramName, value) +} + +// ResolveStringSliceParameter resolves a parameter and converts it to a string slice +func (b *BaseOutputBuilder[T]) ResolveStringSliceParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) ([]string, error) { + value, err := b.ResolveParameter(ctx, param, paramName) + if err != nil { + return nil, err + } + + if slice, ok := value.([]string); ok { + return slice, nil + } + + // Handle single string case + if str, ok := value.(string); ok { + return []string{str}, nil + } + + return nil, fmt.Errorf("%s parameter resolved to non-string-slice value: %T", paramName, value) +} + +// BuildStandardOutput creates a standard output map with common fields +// This eliminates the repetitive pattern of building map[string]interface{} outputs +func (b *BaseOutputBuilder[T]) BuildStandardOutput( + output interface{}, + success bool, + additionalFields map[string]interface{}, +) map[string]interface{} { + result := map[string]interface{}{ + "output": output, + "success": success, + } + + // Add any additional fields + for key, value := range additionalFields { + result[key] = value + } + + return result +} + +// BuildOutputFromStruct automatically generates an output map from a struct +// by reflecting over its fields and including non-zero values +func (b *BaseOutputBuilder[T]) BuildOutputFromStruct( + action T, + success bool, + excludeFields []string, +) map[string]interface{} { + result := map[string]interface{}{ + "success": success, + } + + // Create a set of fields to exclude for faster lookup + excludeSet := make(map[string]bool) + for _, field := range excludeFields { + excludeSet[field] = true + } + + // Use reflection to get struct fields + v := reflect.ValueOf(action) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + // Fall back to standard output if not a struct + return result + } + + t := v.Type() + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldType := t.Field(i) + + // Skip unexported fields + if !field.CanInterface() { + continue + } + + fieldName := fieldType.Name + fieldValue := field.Interface() + + // Skip excluded fields + if excludeSet[fieldName] { + continue + } + + // Skip zero values (nil, empty string, 0, false) + if !field.IsZero() { + // Convert field name to camelCase for consistency + camelCaseName := strings.ToLower(fieldName[:1]) + fieldName[1:] + result[camelCaseName] = fieldValue + } + } + + return result +} + +// BuildOutputWithCount creates an output with a count field for slice results +func (b *BaseOutputBuilder[T]) BuildOutputWithCount( + items interface{}, + success bool, + additionalFields map[string]interface{}, +) map[string]interface{} { + result := b.BuildStandardOutput(items, success, additionalFields) + + // Add count if items is a slice + if items != nil { + v := reflect.ValueOf(items) + if v.Kind() == reflect.Slice { + result["count"] = v.Len() + } + } + + return result +} diff --git a/actions/common/output_builder.go b/actions/common/output_builder.go new file mode 100644 index 0000000..bf5d127 --- /dev/null +++ b/actions/common/output_builder.go @@ -0,0 +1,153 @@ +package common + +import ( + "log/slog" + "reflect" + "strings" +) + +// OutputBuilder provides common output building functionality +// that can be embedded into actions to eliminate duplicate code +type OutputBuilder struct { + logger *slog.Logger +} + +// NewOutputBuilder creates a new output builder with the given logger +func NewOutputBuilder(logger *slog.Logger) *OutputBuilder { + return &OutputBuilder{logger: logger} +} + +// GetLogger returns the logger from the output builder +func (ob *OutputBuilder) GetLogger() *slog.Logger { + return ob.logger +} + +// BuildStandardOutput creates a standard output map with common fields +// This eliminates the repetitive pattern of building map[string]interface{} outputs +func (ob *OutputBuilder) BuildStandardOutput( + output interface{}, + success bool, + additionalFields map[string]interface{}, +) map[string]interface{} { + result := map[string]interface{}{ + "output": output, + "success": success, + } + + // Add any additional fields + for key, value := range additionalFields { + result[key] = value + } + + return result +} + +// BuildOutputFromStruct automatically generates an output map from a struct +// by reflecting over its fields and including non-zero values +func (ob *OutputBuilder) BuildOutputFromStruct( + action interface{}, + success bool, + excludeFields []string, +) map[string]interface{} { + result := map[string]interface{}{ + "success": success, + } + + // Create a set of fields to exclude for faster lookup + excludeSet := make(map[string]bool) + for _, field := range excludeFields { + excludeSet[field] = true + } + + // Use reflection to get struct fields + v := reflect.ValueOf(action) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + // Fall back to standard output if not a struct + return result + } + + t := v.Type() + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldType := t.Field(i) + + // Skip unexported fields + if !field.CanInterface() { + continue + } + + fieldName := fieldType.Name + fieldValue := field.Interface() + + // Skip excluded fields + if excludeSet[fieldName] { + continue + } + + // Skip zero values (nil, empty string, 0, false) + if !field.IsZero() { + // Convert field name to camelCase for consistency + camelCaseName := strings.ToLower(fieldName[:1]) + fieldName[1:] + result[camelCaseName] = fieldValue + } + } + + return result +} + +// BuildOutputWithCount creates an output with a count field for slice results +func (ob *OutputBuilder) BuildOutputWithCount( + items interface{}, + success bool, + additionalFields map[string]interface{}, +) map[string]interface{} { + result := ob.BuildStandardOutput(items, success, additionalFields) + + // Add count if items is a slice + if items != nil { + v := reflect.ValueOf(items) + if v.Kind() == reflect.Slice { + result["count"] = v.Len() + } + } + + return result +} + +// BuildSimpleOutput creates a simple output with just success and optional message +func (ob *OutputBuilder) BuildSimpleOutput( + success bool, + message string, +) map[string]interface{} { + result := map[string]interface{}{ + "success": success, + } + + if message != "" { + result["message"] = message + } + + return result +} + +// BuildErrorOutput creates an output for error cases +func (ob *OutputBuilder) BuildErrorOutput( + error interface{}, + additionalFields map[string]interface{}, +) map[string]interface{} { + result := map[string]interface{}{ + "success": false, + "error": error, + } + + // Add any additional fields + for key, value := range additionalFields { + result[key] = value + } + + return result +} diff --git a/actions/common/parameter_resolver.go b/actions/common/parameter_resolver.go new file mode 100644 index 0000000..8bee5be --- /dev/null +++ b/actions/common/parameter_resolver.go @@ -0,0 +1,206 @@ +package common + +import ( + "context" + "fmt" + "log/slog" + "reflect" + "time" + + task_engine "github.com/ndizazzo/task-engine" +) + +// ParameterResolver provides common parameter resolution functionality +// that can be embedded into actions to eliminate duplicate code +type ParameterResolver struct { + logger *slog.Logger +} + +// NewParameterResolver creates a new parameter resolver with the given logger +func NewParameterResolver(logger *slog.Logger) *ParameterResolver { + return &ParameterResolver{logger: logger} +} + +// GetLogger returns the logger from the parameter resolver +func (pr *ParameterResolver) GetLogger() *slog.Logger { + return pr.logger +} + +// ResolveParameter is a generic helper for resolving action parameters +// It handles the common pattern of extracting GlobalContext and calling Resolve +func (pr *ParameterResolver) ResolveParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) (interface{}, error) { + if param == nil { + return nil, fmt.Errorf("%s parameter cannot be nil", paramName) + } + + // Extract GlobalContext from context + var globalContext *task_engine.GlobalContext + if gc, ok := ctx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { + globalContext = gc + } + + // Resolve the parameter + value, err := param.Resolve(ctx, globalContext) + if err != nil { + return nil, fmt.Errorf("failed to resolve %s parameter: %w", paramName, err) + } + + return value, nil +} + +// ResolveStringParameter resolves a parameter and converts it to a string +func (pr *ParameterResolver) ResolveStringParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) (string, error) { + value, err := pr.ResolveParameter(ctx, param, paramName) + if err != nil { + return "", err + } + + if str, ok := value.(string); ok { + return str, nil + } + + return "", fmt.Errorf("%s parameter resolved to non-string value: %T", paramName, value) +} + +// ResolveBoolParameter resolves a parameter and converts it to a boolean +func (pr *ParameterResolver) ResolveBoolParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) (bool, error) { + value, err := pr.ResolveParameter(ctx, param, paramName) + if err != nil { + return false, err + } + + if b, ok := value.(bool); ok { + return b, nil + } + + return false, fmt.Errorf("%s parameter resolved to non-boolean value: %T", paramName, value) +} + +// ResolveIntParameter resolves a parameter and converts it to an integer +func (pr *ParameterResolver) ResolveIntParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) (int, error) { + value, err := pr.ResolveParameter(ctx, param, paramName) + if err != nil { + return 0, err + } + + if i, ok := value.(int); ok { + return i, nil + } + + return 0, fmt.Errorf("%s parameter resolved to non-integer value: %T", paramName, value) +} + +// ResolveStringSliceParameter resolves a parameter and converts it to a string slice +func (pr *ParameterResolver) ResolveStringSliceParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) ([]string, error) { + value, err := pr.ResolveParameter(ctx, param, paramName) + if err != nil { + return nil, err + } + + if slice, ok := value.([]string); ok { + return slice, nil + } + + // Handle single string case + if str, ok := value.(string); ok { + return []string{str}, nil + } + + return nil, fmt.Errorf("%s parameter resolved to non-string-slice value: %T", paramName, value) +} + +// ResolveDurationParameter resolves a parameter and converts it to a time.Duration +func (pr *ParameterResolver) ResolveDurationParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) (time.Duration, error) { + value, err := pr.ResolveParameter(ctx, param, paramName) + if err != nil { + return 0, err + } + + // Handle different duration formats + switch v := value.(type) { + case time.Duration: + return v, nil + case string: + // Parse duration string (e.g., "5s", "1m", "2h") + parsedDuration, err := time.ParseDuration(v) + if err != nil { + return 0, fmt.Errorf("failed to parse duration string '%s': %w", v, err) + } + return parsedDuration, nil + case int: + // Treat as seconds + return time.Duration(v) * time.Second, nil + default: + return 0, fmt.Errorf("%s parameter resolved to unsupported duration type: %T", paramName, value) + } +} + +// ResolveMapParameter resolves a parameter and converts it to a map +func (pr *ParameterResolver) ResolveMapParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) (map[string]interface{}, error) { + value, err := pr.ResolveParameter(ctx, param, paramName) + if err != nil { + return nil, err + } + + if m, ok := value.(map[string]interface{}); ok { + return m, nil + } + + return nil, fmt.Errorf("%s parameter resolved to non-map value: %T", paramName, value) +} + +// ResolveSliceParameter resolves a parameter and converts it to a slice +func (pr *ParameterResolver) ResolveSliceParameter( + ctx context.Context, + param task_engine.ActionParameter, + paramName string, +) ([]interface{}, error) { + value, err := pr.ResolveParameter(ctx, param, paramName) + if err != nil { + return nil, err + } + + if slice, ok := value.([]interface{}); ok { + return slice, nil + } + + // Handle typed slices by converting to interface slice + v := reflect.ValueOf(value) + if v.Kind() == reflect.Slice { + result := make([]interface{}, v.Len()) + for i := 0; i < v.Len(); i++ { + result[i] = v.Index(i).Interface() + } + return result, nil + } + + return nil, fmt.Errorf("%s parameter resolved to non-slice value: %T", paramName, value) +} diff --git a/actions/docker/check_container_health_action.go b/actions/docker/check_container_health_action.go index bbe627e..db89d40 100644 --- a/actions/docker/check_container_health_action.go +++ b/actions/docker/check_container_health_action.go @@ -8,11 +8,14 @@ import ( "time" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) type CheckContainerHealthAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder // Parameter-only inputs WorkingDirParam task_engine.ActionParameter ServiceNameParam task_engine.ActionParameter @@ -31,15 +34,17 @@ type CheckContainerHealthAction struct { ResolvedRetryDelay time.Duration } -// NewCheckContainerHealthAction creates the action instance (single builder pattern) +// NewCheckContainerHealthAction creates a new CheckContainerHealthAction with the given logger func NewCheckContainerHealthAction(logger *slog.Logger) *CheckContainerHealthAction { return &CheckContainerHealthAction{ - BaseAction: task_engine.BaseAction{Logger: logger}, - commandRunner: command.NewDefaultCommandRunner(), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + commandRunner: command.NewDefaultCommandRunner(), } } -// WithParameters validates and attaches parameters, returning the wrapped action +// WithParameters sets the parameters for container health check and returns a wrapped Action func (a *CheckContainerHealthAction) WithParameters( workingDirParam task_engine.ActionParameter, serviceNameParam task_engine.ActionParameter, @@ -47,20 +52,15 @@ func (a *CheckContainerHealthAction) WithParameters( maxRetriesParam task_engine.ActionParameter, retryDelayParam task_engine.ActionParameter, ) (*task_engine.Action[*CheckContainerHealthAction], error) { - if workingDirParam == nil || serviceNameParam == nil || checkCommandParam == nil || maxRetriesParam == nil || retryDelayParam == nil { - return nil, fmt.Errorf("parameters cannot be nil") - } a.WorkingDirParam = workingDirParam a.ServiceNameParam = serviceNameParam a.CheckCommandParam = checkCommandParam a.MaxRetriesParam = maxRetriesParam a.RetryDelayParam = retryDelayParam - return &task_engine.Action[*CheckContainerHealthAction]{ - ID: "check-container-health-action", - Name: "Check Container Health", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*CheckContainerHealthAction](a.Logger) + return constructor.WrapAction(a, "Check Container Health", "check-container-health-action"), nil } // SetCommandRunner allows injecting a mock or alternative CommandRunner for testing. @@ -69,38 +69,24 @@ func (a *CheckContainerHealthAction) SetCommandRunner(runner command.CommandRunn } func (a *CheckContainerHealthAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve working directory parameter - workingDirValue, err := a.WorkingDirParam.Resolve(execCtx, globalContext) + workingDirValue, err := a.ResolveStringParameter(execCtx, a.WorkingDirParam, "working directory") if err != nil { - return fmt.Errorf("failed to resolve working directory parameter: %w", err) - } - if workingDirStr, ok := workingDirValue.(string); ok { - a.ResolvedWorkingDir = workingDirStr - } else { - return fmt.Errorf("working directory parameter is not a string, got %T", workingDirValue) + return err } + a.ResolvedWorkingDir = workingDirValue // Resolve service name parameter - serviceNameValue, err := a.ServiceNameParam.Resolve(execCtx, globalContext) + serviceNameValue, err := a.ResolveStringParameter(execCtx, a.ServiceNameParam, "service name") if err != nil { - return fmt.Errorf("failed to resolve service name parameter: %w", err) - } - if serviceNameStr, ok := serviceNameValue.(string); ok { - a.ResolvedServiceName = serviceNameStr - } else { - return fmt.Errorf("service name parameter is not a string, got %T", serviceNameValue) + return err } + a.ResolvedServiceName = serviceNameValue // Resolve check command parameter - checkCommandValue, err := a.CheckCommandParam.Resolve(execCtx, globalContext) + checkCommandValue, err := a.ResolveParameter(execCtx, a.CheckCommandParam, "check command") if err != nil { - return fmt.Errorf("failed to resolve check command parameter: %w", err) + return err } if checkCommandSlice, ok := checkCommandValue.([]string); ok { a.ResolvedCheckCommand = checkCommandSlice @@ -111,9 +97,9 @@ func (a *CheckContainerHealthAction) Execute(execCtx context.Context) error { } // Resolve max retries parameter - maxRetriesValue, err := a.MaxRetriesParam.Resolve(execCtx, globalContext) + maxRetriesValue, err := a.ResolveParameter(execCtx, a.MaxRetriesParam, "max retries") if err != nil { - return fmt.Errorf("failed to resolve max retries parameter: %w", err) + return err } switch v := maxRetriesValue.(type) { case int: @@ -138,9 +124,9 @@ func (a *CheckContainerHealthAction) Execute(execCtx context.Context) error { } // Resolve retry delay parameter - retryDelayValue, err := a.RetryDelayParam.Resolve(execCtx, globalContext) + retryDelayValue, err := a.ResolveParameter(execCtx, a.RetryDelayParam, "retry delay") if err != nil { - return fmt.Errorf("failed to resolve retry delay parameter: %w", err) + return err } switch v := retryDelayValue.(type) { case time.Duration: @@ -188,12 +174,11 @@ func (a *CheckContainerHealthAction) Execute(execCtx context.Context) error { // GetOutput returns details about the health check configuration func (a *CheckContainerHealthAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "service": a.ResolvedServiceName, "command": a.ResolvedCheckCommand, "maxRetries": a.ResolvedMaxRetries, "retryDelay": a.ResolvedRetryDelay.String(), "workingDir": a.ResolvedWorkingDir, - "success": true, - } + }) } diff --git a/actions/docker/check_container_health_action_test.go b/actions/docker/check_container_health_action_test.go index 4f4a2a5..c0e1793 100644 --- a/actions/docker/check_container_health_action_test.go +++ b/actions/docker/check_container_health_action_test.go @@ -434,7 +434,7 @@ func (suite *CheckContainerHealthTestSuite) TestExecute_WithNonStringWorkingDirP execErr := action.Wrapped.Execute(context.Background()) suite.Error(execErr) - suite.Contains(execErr.Error(), "working directory parameter is not a string, got int") + suite.Contains(execErr.Error(), "working directory parameter resolved to non-string value") } func (suite *CheckContainerHealthTestSuite) TestExecute_WithNonStringServiceNameParameter() { @@ -448,7 +448,7 @@ func (suite *CheckContainerHealthTestSuite) TestExecute_WithNonStringServiceName execErr := action.Wrapped.Execute(context.Background()) suite.Error(execErr) - suite.Contains(execErr.Error(), "service name parameter is not a string, got int") + suite.Contains(execErr.Error(), "service name parameter resolved to non-string value") } func (suite *CheckContainerHealthTestSuite) TestExecute_WithInvalidCheckCommandParameter() { diff --git a/actions/docker/docker_compose_down_action.go b/actions/docker/docker_compose_down_action.go index 232ca1d..3c8da51 100644 --- a/actions/docker/docker_compose_down_action.go +++ b/actions/docker/docker_compose_down_action.go @@ -7,76 +7,71 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) -// NewDockerComposeDownAction creates a DockerComposeDownAction instance -func NewDockerComposeDownAction(logger *slog.Logger) *DockerComposeDownAction { - return &DockerComposeDownAction{ - BaseAction: task_engine.BaseAction{Logger: logger}, - commandRunner: command.NewDefaultCommandRunner(), - } -} - -// DockerComposeDownAction runs docker compose down -// It can target specific services or all services if none are provided. type DockerComposeDownAction struct { task_engine.BaseAction - commandRunner command.CommandRunner - - // Parameter-only fields + common.ParameterResolver + common.OutputBuilder + // Parameter-only inputs WorkingDirParam task_engine.ActionParameter ServicesParam task_engine.ActionParameter + + // Execution dependency + commandRunner command.CommandRunner + + // Resolved/output fields + ResolvedWorkingDir string + ResolvedServices []string +} + +// SetCommandRunner allows injecting a mock or alternative CommandRunner for testing. +func (a *DockerComposeDownAction) SetCommandRunner(runner command.CommandRunner) { + a.commandRunner = runner } -// WithParameters sets the parameters and returns a wrapped Action +// NewDockerComposeDownAction creates the action instance (modern constructor) +func NewDockerComposeDownAction(logger *slog.Logger) *DockerComposeDownAction { + return &DockerComposeDownAction{ + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + commandRunner: command.NewDefaultCommandRunner(), + } +} + +// WithParameters sets inputs and returns the wrapped action func (a *DockerComposeDownAction) WithParameters(workingDirParam, servicesParam task_engine.ActionParameter) (*task_engine.Action[*DockerComposeDownAction], error) { if workingDirParam == nil || servicesParam == nil { return nil, fmt.Errorf("parameters cannot be nil") } - a.WorkingDirParam = workingDirParam a.ServicesParam = servicesParam - return &task_engine.Action[*DockerComposeDownAction]{ - ID: "docker-compose-down-action", - Name: "Docker Compose Down", - Wrapped: a, - }, nil -} - -// SetCommandRunner allows injecting a mock or alternative CommandRunner for testing. -func (a *DockerComposeDownAction) SetCommandRunner(runner command.CommandRunner) { - a.commandRunner = runner + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*DockerComposeDownAction](a.Logger) + return constructor.WrapAction(a, "Docker Compose Down", "docker-compose-down-action"), nil } func (a *DockerComposeDownAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve working directory parameter var workingDir string if a.WorkingDirParam != nil { - workingDirValue, err := a.WorkingDirParam.Resolve(execCtx, globalContext) + workingDirValue, err := a.ResolveStringParameter(execCtx, a.WorkingDirParam, "working directory") if err != nil { - return fmt.Errorf("failed to resolve working directory parameter: %w", err) - } - if workingDirStr, ok := workingDirValue.(string); ok { - workingDir = workingDirStr - } else { - return fmt.Errorf("working directory parameter is not a string, got %T", workingDirValue) + return err } + workingDir = workingDirValue } // Resolve services parameter var services []string if a.ServicesParam != nil { - servicesValue, err := a.ServicesParam.Resolve(execCtx, globalContext) + servicesValue, err := a.ResolveParameter(execCtx, a.ServicesParam, "services") if err != nil { - return fmt.Errorf("failed to resolve services parameter: %w", err) + return err } if servicesSlice, ok := servicesValue.([]string); ok { services = servicesSlice @@ -118,7 +113,5 @@ func (a *DockerComposeDownAction) Execute(execCtx context.Context) error { // GetOutput returns details about the compose down execution func (a *DockerComposeDownAction) GetOutput() interface{} { - return map[string]interface{}{ - "success": true, - } + return a.BuildStandardOutput(nil, true, nil) } diff --git a/actions/docker/docker_compose_down_action_test.go b/actions/docker/docker_compose_down_action_test.go index 154cca0..d575afd 100644 --- a/actions/docker/docker_compose_down_action_test.go +++ b/actions/docker/docker_compose_down_action_test.go @@ -367,7 +367,7 @@ func (suite *DockerComposeDownTestSuite) TestExecute_WithNonStringWorkingDirPara err = action.Wrapped.Execute(context.Background()) suite.Error(err) - suite.Contains(err.Error(), "working directory parameter is not a string, got int") + suite.Contains(err.Error(), "working directory parameter resolved to non-string value") } func (suite *DockerComposeDownTestSuite) TestExecute_WithInvalidServicesParameter() { diff --git a/actions/docker/docker_compose_exec_action.go b/actions/docker/docker_compose_exec_action.go index c2deb3f..1a0fa3d 100644 --- a/actions/docker/docker_compose_exec_action.go +++ b/actions/docker/docker_compose_exec_action.go @@ -7,19 +7,24 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) -// NewDockerComposeExecAction creates an action instance (modern constructor pattern) +// NewDockerComposeExecAction creates a new DockerComposeExecAction with the given logger func NewDockerComposeExecAction(logger *slog.Logger) *DockerComposeExecAction { return &DockerComposeExecAction{ - BaseAction: task_engine.BaseAction{Logger: logger}, - commandRunner: command.NewDefaultCommandRunner(), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + commandRunner: command.NewDefaultCommandRunner(), } } type DockerComposeExecAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder // Parameter-only inputs WorkingDirParam task_engine.ActionParameter ServiceParam task_engine.ActionParameter @@ -39,59 +44,40 @@ func (a *DockerComposeExecAction) SetCommandRunner(runner command.CommandRunner) a.commandRunner = runner } -// WithParameters validates and attaches parameters, returning the wrapped action +// WithParameters sets the parameters for compose exec and returns a wrapped Action func (a *DockerComposeExecAction) WithParameters( workingDirParam task_engine.ActionParameter, serviceParam task_engine.ActionParameter, - commandArgsParam task_engine.ActionParameter, + commandParam task_engine.ActionParameter, ) (*task_engine.Action[*DockerComposeExecAction], error) { - if workingDirParam == nil || serviceParam == nil || commandArgsParam == nil { - return nil, fmt.Errorf("parameters cannot be nil") - } a.WorkingDirParam = workingDirParam a.ServiceParam = serviceParam - a.CommandArgsParam = commandArgsParam + a.CommandArgsParam = commandParam - return &task_engine.Action[*DockerComposeExecAction]{ - ID: "docker-compose-exec-action", - Name: "Docker Compose Exec", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*DockerComposeExecAction](a.Logger) + return constructor.WrapAction(a, "Docker Compose Exec", "docker-compose-exec-action"), nil } func (a *DockerComposeExecAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve working directory parameter - workingDirValue, err := a.WorkingDirParam.Resolve(execCtx, globalContext) + workingDirValue, err := a.ResolveStringParameter(execCtx, a.WorkingDirParam, "working directory") if err != nil { - return fmt.Errorf("failed to resolve working directory parameter: %w", err) - } - if workingDirStr, ok := workingDirValue.(string); ok { - a.ResolvedWorkingDir = workingDirStr - } else { - return fmt.Errorf("working directory parameter is not a string, got %T", workingDirValue) + return err } + a.ResolvedWorkingDir = workingDirValue // Resolve service parameter - serviceValue, err := a.ServiceParam.Resolve(execCtx, globalContext) + serviceValue, err := a.ResolveStringParameter(execCtx, a.ServiceParam, "service") if err != nil { - return fmt.Errorf("failed to resolve service parameter: %w", err) - } - if serviceStr, ok := serviceValue.(string); ok { - a.ResolvedService = serviceStr - } else { - return fmt.Errorf("service parameter is not a string, got %T", serviceValue) + return err } + a.ResolvedService = serviceValue // Resolve command arguments parameter - commandArgsValue, err := a.CommandArgsParam.Resolve(execCtx, globalContext) + commandArgsValue, err := a.ResolveParameter(execCtx, a.CommandArgsParam, "command arguments") if err != nil { - return fmt.Errorf("failed to resolve command arguments parameter: %w", err) + return err } if commandArgsSlice, ok := commandArgsValue.([]string); ok { a.ResolvedCommandArgs = commandArgsSlice @@ -123,10 +109,9 @@ func (a *DockerComposeExecAction) Execute(execCtx context.Context) error { // GetOutput returns details about the compose exec execution func (a *DockerComposeExecAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "service": a.ResolvedService, "workingDir": a.ResolvedWorkingDir, "command": a.ResolvedCommandArgs, - "success": true, - } + }) } diff --git a/actions/docker/docker_compose_exec_action_test.go b/actions/docker/docker_compose_exec_action_test.go index 7f10640..710580b 100644 --- a/actions/docker/docker_compose_exec_action_test.go +++ b/actions/docker/docker_compose_exec_action_test.go @@ -335,7 +335,7 @@ func (suite *DockerComposeExecTestSuite) TestExecute_WithNonStringWorkingDirPara execErr := action.Wrapped.Execute(context.Background()) suite.Error(execErr) - suite.Contains(execErr.Error(), "working directory parameter is not a string, got int") + suite.Contains(execErr.Error(), "working directory parameter resolved to non-string value") } func (suite *DockerComposeExecTestSuite) TestExecute_WithNonStringServiceParameter() { @@ -349,7 +349,7 @@ func (suite *DockerComposeExecTestSuite) TestExecute_WithNonStringServiceParamet execErr := action.Wrapped.Execute(context.Background()) suite.Error(execErr) - suite.Contains(execErr.Error(), "service parameter is not a string, got int") + suite.Contains(execErr.Error(), "service parameter resolved to non-string value") } func (suite *DockerComposeExecTestSuite) TestExecute_WithInvalidCommandArgsParameter() { diff --git a/actions/docker/docker_compose_ls_action.go b/actions/docker/docker_compose_ls_action.go index 9354e6b..5b039ab 100644 --- a/actions/docker/docker_compose_ls_action.go +++ b/actions/docker/docker_compose_ls_action.go @@ -7,6 +7,7 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) @@ -26,60 +27,67 @@ type DockerComposeLsConfig struct { WorkingDir string } -// DockerComposeLsActionBuilder provides a fluent interface for building DockerComposeLsAction +// DockerComposeLsActionBuilder provides the new constructor pattern type DockerComposeLsActionBuilder struct { - logger *slog.Logger + common.BaseConstructor[*DockerComposeLsAction] } -// NewDockerComposeLsAction creates a fluent builder for DockerComposeLsAction +// NewDockerComposeLsAction creates a new DockerComposeLsAction builder func NewDockerComposeLsAction(logger *slog.Logger) *DockerComposeLsActionBuilder { - return &DockerComposeLsActionBuilder{logger: logger} -} - -// WithParameters sets the parameters for working directory and configuration -func (b *DockerComposeLsActionBuilder) WithParameters(workingDirParam task_engine.ActionParameter, config DockerComposeLsConfig) (*task_engine.Action[*DockerComposeLsAction], error) { - // Determine whether to treat the provided parameter as active - // - Non-empty static string: active (resolve at runtime) - // - Non-string static parameter: active (so Execute will error as tests expect) - // - Any non-static parameter: active - // - Empty string static parameter: inactive (back-compat original constructor) - useParam := false - if sp, ok := workingDirParam.(task_engine.StaticParameter); ok { - switch v := sp.Value.(type) { - case string: - if strings.TrimSpace(v) != "" { - useParam = true - } - default: - // Non-string value should still attempt resolution (and then fail) - useParam = true - } - } else if workingDirParam != nil { - useParam = true + return &DockerComposeLsActionBuilder{ + BaseConstructor: *common.NewBaseConstructor[*DockerComposeLsAction](logger), } +} +// WithParameters creates a DockerComposeLsAction with the specified parameters +func (b *DockerComposeLsActionBuilder) WithParameters( + workingDirParam task_engine.ActionParameter, + config DockerComposeLsConfig, +) (*task_engine.Action[*DockerComposeLsAction], error) { action := &DockerComposeLsAction{ - BaseAction: task_engine.NewBaseAction(b.logger), + BaseAction: task_engine.NewBaseAction(b.GetLogger()), All: config.All, Filter: config.Filter, Format: config.Format, Quiet: config.Quiet, WorkingDir: config.WorkingDir, CommandProcessor: command.NewDefaultCommandRunner(), + Output: "", + Stacks: []ComposeStack{}, + WorkingDirParam: nil, // Default to nil for backward compatibility } - if useParam { - action.WorkingDirParam = workingDirParam + + // Only set the parameter if it has a meaningful value + if workingDirParam != nil { + if sp, ok := workingDirParam.(task_engine.StaticParameter); ok { + if v, ok2 := sp.Value.(string); ok2 && strings.TrimSpace(v) != "" { + action.WorkingDirParam = workingDirParam + } else if !ok2 { + // Set non-string static parameters so Execute can fail as expected + action.WorkingDirParam = workingDirParam + } + // Don't set empty string static parameters for backward compatibility + } else { + // Non-static parameters should always be set + action.WorkingDirParam = workingDirParam + } } + // Generate custom ID based on whether parameters are provided id := "docker-compose-ls-action" - if useParam { - id = "docker-compose-ls-with-params-action" + if action.WorkingDirParam != nil { + // Only use custom ID for meaningful string parameters + if sp, ok := action.WorkingDirParam.(task_engine.StaticParameter); ok { + if v, ok2 := sp.Value.(string); ok2 && strings.TrimSpace(v) != "" { + id = "docker-compose-ls-with-params-action" + } + } else { + // Non-static parameters should use the custom ID + id = "docker-compose-ls-with-params-action" + } } - return &task_engine.Action[*DockerComposeLsAction]{ - ID: id, - Name: "Docker Compose LS", - Wrapped: action, - }, nil + + return b.WrapAction(action, "Docker Compose LS", id), nil } // DockerComposeLsOption is a function type for configuring DockerComposeLsAction @@ -139,6 +147,8 @@ func WithWorkingDir(workingDir string) DockerComposeLsOption { // DockerComposeLsAction lists Docker Compose stacks type DockerComposeLsAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder All bool Filter string Format string @@ -158,23 +168,13 @@ func (a *DockerComposeLsAction) SetCommandRunner(runner command.CommandRunner) { } func (a *DockerComposeLsAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve working directory parameter if it exists if a.WorkingDirParam != nil { - workingDirValue, err := a.WorkingDirParam.Resolve(execCtx, globalContext) + workingDirValue, err := a.ResolveStringParameter(execCtx, a.WorkingDirParam, "working directory") if err != nil { - return fmt.Errorf("failed to resolve working directory parameter: %w", err) - } - if workingDirStr, ok := workingDirValue.(string); ok { - a.WorkingDir = workingDirStr - } else { - return fmt.Errorf("working directory parameter is not a string, got %T", workingDirValue) + return err } + a.WorkingDir = workingDirValue } args := []string{"compose", "ls"} @@ -221,12 +221,10 @@ func (a *DockerComposeLsAction) Execute(execCtx context.Context) error { // This enables other actions to reference the output of this action // using ActionOutputParameter references. func (a *DockerComposeLsAction) GetOutput() interface{} { - return map[string]interface{}{ - "stacks": a.Stacks, - "count": len(a.Stacks), - "output": a.Output, - "success": true, - } + return a.BuildOutputWithCount(a.Stacks, true, map[string]interface{}{ + "stacks": a.Stacks, + "output": a.Output, + }) } // parseStacks parses the docker compose ls output and populates the Stacks slice diff --git a/actions/docker/docker_compose_ls_action_test.go b/actions/docker/docker_compose_ls_action_test.go index f73550d..7592657 100644 --- a/actions/docker/docker_compose_ls_action_test.go +++ b/actions/docker/docker_compose_ls_action_test.go @@ -548,7 +548,7 @@ func (suite *DockerComposeLsActionTestSuite) TestExecute_WithNonStringWorkingDir err = action.Wrapped.Execute(context.Background()) suite.Error(err) - suite.Contains(err.Error(), "working directory parameter is not a string, got int") + suite.Contains(err.Error(), "working directory parameter resolved to non-string value") } // Complex scenario tests diff --git a/actions/docker/docker_compose_ps_action.go b/actions/docker/docker_compose_ps_action.go index 72464f8..3427ac5 100644 --- a/actions/docker/docker_compose_ps_action.go +++ b/actions/docker/docker_compose_ps_action.go @@ -7,6 +7,7 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) @@ -25,19 +26,19 @@ type DockerComposePsActionWrapper struct { Wrapped *DockerComposePsAction } -// DockerComposePsActionConstructor provides the modern constructor pattern +// DockerComposePsActionConstructor provides the new constructor pattern type DockerComposePsActionConstructor struct { - logger *slog.Logger + common.BaseConstructor[*DockerComposePsAction] } // NewDockerComposePsAction creates a new DockerComposePsAction constructor func NewDockerComposePsAction(logger *slog.Logger) *DockerComposePsActionConstructor { return &DockerComposePsActionConstructor{ - logger: logger, + BaseConstructor: *common.NewBaseConstructor[*DockerComposePsAction](logger), } } -// WithParameters creates a DockerComposePsAction with the given parameters +// WithParameters creates a DockerComposePsAction with the specified parameters func (c *DockerComposePsActionConstructor) WithParameters( servicesParam task_engine.ActionParameter, allParam task_engine.ActionParameter, @@ -45,9 +46,9 @@ func (c *DockerComposePsActionConstructor) WithParameters( formatParam task_engine.ActionParameter, quietParam task_engine.ActionParameter, workingDirParam task_engine.ActionParameter, -) (*DockerComposePsActionWrapper, error) { +) (*task_engine.Action[*DockerComposePsAction], error) { action := &DockerComposePsAction{ - BaseAction: task_engine.BaseAction{Logger: c.logger}, + BaseAction: task_engine.NewBaseAction(c.GetLogger()), Services: []string{}, All: false, Filter: "", @@ -55,6 +56,8 @@ func (c *DockerComposePsActionConstructor) WithParameters( Quiet: false, WorkingDir: "", CommandProcessor: command.NewDefaultCommandRunner(), + Output: "", + ServicesList: []ComposeService{}, ServicesParam: servicesParam, AllParam: allParam, FilterParam: filterParam, @@ -63,10 +66,7 @@ func (c *DockerComposePsActionConstructor) WithParameters( WorkingDirParam: workingDirParam, } - return &DockerComposePsActionWrapper{ - ID: "docker-compose-ps-action", - Wrapped: action, - }, nil + return c.WrapAction(action, "Docker Compose PS", "docker-compose-ps-action"), nil } // DockerComposePsOption is a function type for configuring DockerComposePsAction @@ -110,6 +110,8 @@ func WithComposePsWorkingDir(workingDir string) DockerComposePsOption { // DockerComposePsAction lists Docker Compose services type DockerComposePsAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder Services []string All bool Filter string @@ -135,17 +137,11 @@ func (a *DockerComposePsAction) SetCommandRunner(runner command.CommandRunner) { } func (a *DockerComposePsAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve services parameter if it exists if a.ServicesParam != nil { - servicesValue, err := a.ServicesParam.Resolve(execCtx, globalContext) + servicesValue, err := a.ResolveParameter(execCtx, a.ServicesParam, "services") if err != nil { - return fmt.Errorf("failed to resolve services parameter: %w", err) + return err } if servicesSlice, ok := servicesValue.([]string); ok { a.Services = servicesSlice @@ -163,67 +159,47 @@ func (a *DockerComposePsAction) Execute(execCtx context.Context) error { // Resolve all parameter if it exists if a.AllParam != nil { - allValue, err := a.AllParam.Resolve(execCtx, globalContext) + allValue, err := a.ResolveBoolParameter(execCtx, a.AllParam, "all") if err != nil { - return fmt.Errorf("failed to resolve all parameter: %w", err) - } - if allBool, ok := allValue.(bool); ok { - a.All = allBool - } else { - return fmt.Errorf("all parameter is not a bool, got %T", allValue) + return err } + a.All = allValue } // Resolve filter parameter if it exists if a.FilterParam != nil { - filterValue, err := a.FilterParam.Resolve(execCtx, globalContext) + filterValue, err := a.ResolveStringParameter(execCtx, a.FilterParam, "filter") if err != nil { - return fmt.Errorf("failed to resolve filter parameter: %w", err) - } - if filterStr, ok := filterValue.(string); ok { - a.Filter = filterStr - } else { - return fmt.Errorf("filter parameter is not a string, got %T", filterValue) + return err } + a.Filter = filterValue } // Resolve format parameter if it exists if a.FormatParam != nil { - formatValue, err := a.FormatParam.Resolve(execCtx, globalContext) + formatValue, err := a.ResolveStringParameter(execCtx, a.FormatParam, "format") if err != nil { - return fmt.Errorf("failed to resolve format parameter: %w", err) - } - if formatStr, ok := formatValue.(string); ok { - a.Format = formatStr - } else { - return fmt.Errorf("format parameter is not a string, got %T", formatValue) + return err } + a.Format = formatValue } // Resolve quiet parameter if it exists if a.QuietParam != nil { - quietValue, err := a.QuietParam.Resolve(execCtx, globalContext) + quietValue, err := a.ResolveBoolParameter(execCtx, a.QuietParam, "quiet") if err != nil { - return fmt.Errorf("failed to resolve quiet parameter: %w", err) - } - if quietBool, ok := quietValue.(bool); ok { - a.Quiet = quietBool - } else { - return fmt.Errorf("quiet parameter is not a bool, got %T", quietValue) + return err } + a.Quiet = quietValue } // Resolve working directory parameter if it exists if a.WorkingDirParam != nil { - workingDirValue, err := a.WorkingDirParam.Resolve(execCtx, globalContext) + workingDirValue, err := a.ResolveStringParameter(execCtx, a.WorkingDirParam, "working directory") if err != nil { - return fmt.Errorf("failed to resolve working directory parameter: %w", err) - } - if workingDirStr, ok := workingDirValue.(string); ok { - a.WorkingDir = workingDirStr - } else { - return fmt.Errorf("working directory parameter is not a string, got %T", workingDirValue) + return err } + a.WorkingDir = workingDirValue } args := []string{"compose", "ps"} @@ -274,12 +250,10 @@ func (a *DockerComposePsAction) Execute(execCtx context.Context) error { // GetOutput returns parsed services information and raw output metadata func (a *DockerComposePsAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildOutputWithCount(a.ServicesList, true, map[string]interface{}{ "services": a.ServicesList, - "count": len(a.ServicesList), "output": a.Output, - "success": true, - } + }) } // parseServices parses the docker compose ps output and populates the ServicesList slice diff --git a/actions/docker/docker_compose_ps_action_test.go b/actions/docker/docker_compose_ps_action_test.go index 38d42fe..8f256eb 100644 --- a/actions/docker/docker_compose_ps_action_test.go +++ b/actions/docker/docker_compose_ps_action_test.go @@ -295,7 +295,7 @@ func (suite *DockerComposePsActionTestSuite) TestNewDockerComposePsActionConstru err = action.Wrapped.Execute(context.Background()) suite.Error(err) - suite.Contains(err.Error(), "all parameter is not a bool") + suite.Contains(err.Error(), "all parameter resolved to non-boolean value") action, err = constructor.WithParameters( task_engine.StaticParameter{Value: []string{}}, // services task_engine.StaticParameter{Value: false}, // all @@ -310,7 +310,7 @@ func (suite *DockerComposePsActionTestSuite) TestNewDockerComposePsActionConstru err = action.Wrapped.Execute(context.Background()) suite.Error(err) - suite.Contains(err.Error(), "quiet parameter is not a bool") + suite.Contains(err.Error(), "quiet parameter resolved to non-boolean value") } func (suite *DockerComposePsActionTestSuite) TestNewDockerComposePsActionConstructor_Execute_ServicesAsString() { diff --git a/actions/docker/docker_compose_up_action.go b/actions/docker/docker_compose_up_action.go index 05609bf..31209ae 100644 --- a/actions/docker/docker_compose_up_action.go +++ b/actions/docker/docker_compose_up_action.go @@ -7,11 +7,14 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) type DockerComposeUpAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder // Parameter-only inputs WorkingDirParam task_engine.ActionParameter ServicesParam task_engine.ActionParameter @@ -32,8 +35,10 @@ func (a *DockerComposeUpAction) SetCommandRunner(runner command.CommandRunner) { // NewDockerComposeUpAction creates the action instance (modern constructor) func NewDockerComposeUpAction(logger *slog.Logger) *DockerComposeUpAction { return &DockerComposeUpAction{ - BaseAction: task_engine.BaseAction{Logger: logger}, - commandRunner: command.NewDefaultCommandRunner(), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + commandRunner: command.NewDefaultCommandRunner(), } } @@ -45,35 +50,23 @@ func (a *DockerComposeUpAction) WithParameters(workingDirParam, servicesParam ta a.WorkingDirParam = workingDirParam a.ServicesParam = servicesParam - return &task_engine.Action[*DockerComposeUpAction]{ - ID: "docker-compose-up-action", - Name: "Docker Compose Up", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*DockerComposeUpAction](a.Logger) + return constructor.WrapAction(a, "Docker Compose Up", "docker-compose-up-action"), nil } func (a *DockerComposeUpAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve working directory parameter - workingDirValue, err := a.WorkingDirParam.Resolve(execCtx, globalContext) + workingDirValue, err := a.ResolveStringParameter(execCtx, a.WorkingDirParam, "working directory") if err != nil { - return fmt.Errorf("failed to resolve working directory parameter: %w", err) - } - if workingDirStr, ok := workingDirValue.(string); ok { - a.ResolvedWorkingDir = workingDirStr - } else { - return fmt.Errorf("working directory parameter is not a string, got %T", workingDirValue) + return err } + a.ResolvedWorkingDir = workingDirValue // Resolve services parameter - servicesValue, err := a.ServicesParam.Resolve(execCtx, globalContext) + servicesValue, err := a.ResolveParameter(execCtx, a.ServicesParam, "services") if err != nil { - return fmt.Errorf("failed to resolve services parameter: %w", err) + return err } if servicesSlice, ok := servicesValue.([]string); ok { a.ResolvedServices = servicesSlice @@ -109,9 +102,8 @@ func (a *DockerComposeUpAction) Execute(execCtx context.Context) error { // GetOutput returns details about the compose up execution func (a *DockerComposeUpAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "services": a.ResolvedServices, "workingDir": a.ResolvedWorkingDir, - "success": true, - } + }) } diff --git a/actions/docker/docker_compose_up_action_test.go b/actions/docker/docker_compose_up_action_test.go index 1c16960..b2d0be7 100644 --- a/actions/docker/docker_compose_up_action_test.go +++ b/actions/docker/docker_compose_up_action_test.go @@ -408,7 +408,7 @@ func (suite *DockerComposeUpTestSuite) TestExecute_WithNonStringWorkingDirParame execErr := action.Wrapped.Execute(context.Background()) suite.Error(execErr) - suite.Contains(execErr.Error(), "working directory parameter is not a string, got int") + suite.Contains(execErr.Error(), "working directory parameter resolved to non-string value") } func (suite *DockerComposeUpTestSuite) TestExecute_WithInvalidServicesParameter() { diff --git a/actions/docker/docker_generic_action.go b/actions/docker/docker_generic_action.go index 77b6dc3..99d11a5 100644 --- a/actions/docker/docker_generic_action.go +++ b/actions/docker/docker_generic_action.go @@ -7,18 +7,19 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) // DockerGenericActionConstructor provides the new constructor pattern type DockerGenericActionConstructor struct { - logger *slog.Logger + common.BaseConstructor[*DockerGenericAction] } // NewDockerGenericAction creates a new DockerGenericAction constructor func NewDockerGenericAction(logger *slog.Logger) *DockerGenericActionConstructor { return &DockerGenericActionConstructor{ - logger: logger, + BaseConstructor: *common.NewBaseConstructor[*DockerGenericAction](logger), } } @@ -27,18 +28,13 @@ func (c *DockerGenericActionConstructor) WithParameters( dockerCmdParam task_engine.ActionParameter, ) (*task_engine.Action[*DockerGenericAction], error) { action := &DockerGenericAction{ - BaseAction: task_engine.NewBaseAction(c.logger), + BaseAction: task_engine.NewBaseAction(c.GetLogger()), DockerCmd: []string{}, CommandProcessor: command.NewDefaultCommandRunner(), DockerCmdParam: dockerCmdParam, } - id := "docker-generic-action" - return &task_engine.Action[*DockerGenericAction]{ - ID: id, - Name: "Docker Generic", - Wrapped: action, - }, nil + return c.WrapAction(action, "Docker Generic", "docker-generic-action"), nil } // DockerGenericAction runs a generic docker command and stores its output @@ -46,6 +42,8 @@ func (c *DockerGenericActionConstructor) WithParameters( // should be separate actions type DockerGenericAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder DockerCmd []string CommandProcessor command.CommandRunner Output string @@ -55,17 +53,11 @@ type DockerGenericAction struct { } func (a *DockerGenericAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve docker command parameter if it exists if a.DockerCmdParam != nil { - dockerCmdValue, err := a.DockerCmdParam.Resolve(execCtx, globalContext) + dockerCmdValue, err := a.ResolveParameter(execCtx, a.DockerCmdParam, "docker command") if err != nil { - return fmt.Errorf("failed to resolve docker command parameter: %w", err) + return err } if dockerCmdSlice, ok := dockerCmdValue.([]string); ok { a.DockerCmd = dockerCmdSlice @@ -91,9 +83,8 @@ func (a *DockerGenericAction) Execute(execCtx context.Context) error { // GetOutput returns the raw output and command metadata func (a *DockerGenericAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, a.Output != "", map[string]interface{}{ "command": a.DockerCmd, "output": a.Output, - "success": a.Output != "", - } + }) } diff --git a/actions/docker/docker_image_list_action.go b/actions/docker/docker_image_list_action.go index d6b1eae..3d21d85 100644 --- a/actions/docker/docker_image_list_action.go +++ b/actions/docker/docker_image_list_action.go @@ -7,6 +7,7 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) @@ -21,13 +22,13 @@ type DockerImage struct { // DockerImageListActionConstructor provides the new constructor pattern type DockerImageListActionConstructor struct { - logger *slog.Logger + common.BaseConstructor[*DockerImageListAction] } // NewDockerImageListAction creates a new DockerImageListAction constructor func NewDockerImageListAction(logger *slog.Logger) *DockerImageListActionConstructor { return &DockerImageListActionConstructor{ - logger: logger, + BaseConstructor: *common.NewBaseConstructor[*DockerImageListAction](logger), } } @@ -41,7 +42,7 @@ func (c *DockerImageListActionConstructor) WithParameters( quietParam task_engine.ActionParameter, ) (*task_engine.Action[*DockerImageListAction], error) { action := &DockerImageListAction{ - BaseAction: task_engine.NewBaseAction(c.logger), + BaseAction: task_engine.NewBaseAction(c.GetLogger()), All: false, Digests: false, Filter: "", @@ -49,6 +50,8 @@ func (c *DockerImageListActionConstructor) WithParameters( NoTrunc: false, Quiet: false, CommandProcessor: command.NewDefaultCommandRunner(), + Output: "", + Images: []DockerImage{}, AllParam: allParam, DigestsParam: digestsParam, FilterParam: filterParam, @@ -57,12 +60,7 @@ func (c *DockerImageListActionConstructor) WithParameters( QuietParam: quietParam, } - id := "docker-image-list-action" - return &task_engine.Action[*DockerImageListAction]{ - ID: id, - Name: "Docker Image List", - Wrapped: action, - }, nil + return c.WrapAction(action, "Docker Image List", "docker-image-list-action"), nil } // DockerImageListOption is a function type for configuring DockerImageListAction @@ -113,6 +111,8 @@ func WithQuietOutput() DockerImageListOption { // DockerImageListAction lists Docker images type DockerImageListAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder All bool Digests bool Filter string @@ -145,92 +145,62 @@ func (a *DockerImageListAction) SetOptions(options ...DockerImageListOption) { } func (a *DockerImageListAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve All parameter if provided if a.AllParam != nil { - v, err := a.AllParam.Resolve(execCtx, globalContext) + v, err := a.ResolveBoolParameter(execCtx, a.AllParam, "all") if err != nil { - return fmt.Errorf("failed to resolve all parameter: %w", err) - } - if allBool, ok := v.(bool); ok { - a.All = allBool - } else { - return fmt.Errorf("all parameter is not a bool, got %T", v) + return err } + a.All = v } // Resolve Digests parameter if provided if a.DigestsParam != nil { - v, err := a.DigestsParam.Resolve(execCtx, globalContext) + v, err := a.ResolveBoolParameter(execCtx, a.DigestsParam, "digests") if err != nil { - return fmt.Errorf("failed to resolve digests parameter: %w", err) - } - if digestsBool, ok := v.(bool); ok { - a.Digests = digestsBool - } else { - return fmt.Errorf("digests parameter is not a bool, got %T", v) + return err } + a.Digests = v } // Resolve Filter parameter if provided if a.FilterParam != nil { - v, err := a.FilterParam.Resolve(execCtx, globalContext) + v, err := a.ResolveStringParameter(execCtx, a.FilterParam, "filter") if err != nil { - return fmt.Errorf("failed to resolve filter parameter: %w", err) + return err } - if filterStr, ok := v.(string); ok { - if strings.TrimSpace(filterStr) != "" { - a.Filter = filterStr - } - } else { - return fmt.Errorf("filter parameter is not a string, got %T", v) + if strings.TrimSpace(v) != "" { + a.Filter = v } } // Resolve Format parameter if provided if a.FormatParam != nil { - v, err := a.FormatParam.Resolve(execCtx, globalContext) + v, err := a.ResolveStringParameter(execCtx, a.FormatParam, "format") if err != nil { - return fmt.Errorf("failed to resolve format parameter: %w", err) + return err } - if formatStr, ok := v.(string); ok { - if strings.TrimSpace(formatStr) != "" { - a.Format = formatStr - } - } else { - return fmt.Errorf("format parameter is not a string, got %T", v) + if strings.TrimSpace(v) != "" { + a.Format = v } } // Resolve NoTrunc parameter if provided if a.NoTruncParam != nil { - v, err := a.NoTruncParam.Resolve(execCtx, globalContext) + v, err := a.ResolveBoolParameter(execCtx, a.NoTruncParam, "noTrunc") if err != nil { - return fmt.Errorf("failed to resolve noTrunc parameter: %w", err) - } - if noTruncBool, ok := v.(bool); ok { - a.NoTrunc = noTruncBool - } else { - return fmt.Errorf("noTrunc parameter is not a bool, got %T", v) + return err } + a.NoTrunc = v } // Resolve Quiet parameter if provided if a.QuietParam != nil { - v, err := a.QuietParam.Resolve(execCtx, globalContext) + v, err := a.ResolveBoolParameter(execCtx, a.QuietParam, "quiet") if err != nil { - return fmt.Errorf("failed to resolve quiet parameter: %w", err) - } - if quietBool, ok := v.(bool); ok { - a.Quiet = quietBool - } else { - return fmt.Errorf("quiet parameter is not a bool, got %T", v) + return err } + a.Quiet = v } args := []string{"image", "ls"} @@ -282,12 +252,10 @@ func (a *DockerImageListAction) Execute(execCtx context.Context) error { // GetOutput returns parsed image information and raw output metadata func (a *DockerImageListAction) GetOutput() interface{} { - return map[string]interface{}{ - "images": a.Images, - "count": len(a.Images), - "output": a.Output, - "success": true, - } + return a.BuildOutputWithCount(a.Images, true, map[string]interface{}{ + "images": a.Images, + "output": a.Output, + }) } // parseImages parses the docker image ls output and populates the Images slice diff --git a/actions/docker/docker_image_list_action_test.go b/actions/docker/docker_image_list_action_test.go index 8f2f1ed..f0ded6b 100644 --- a/actions/docker/docker_image_list_action_test.go +++ b/actions/docker/docker_image_list_action_test.go @@ -251,7 +251,7 @@ func (suite *DockerImageListActionTestSuite) TestNewDockerImageListActionConstru err = action.Wrapped.Execute(context.Background()) suite.Error(err) - suite.Contains(err.Error(), "all parameter is not a bool") + suite.Contains(err.Error(), "all parameter resolved to non-boolean value") } func (suite *DockerImageListActionTestSuite) TestNewDockerImageListActionConstructor_Execute_CommandFailure() { diff --git a/actions/docker/docker_image_rm_action.go b/actions/docker/docker_image_rm_action.go index 682c30d..a9752b3 100644 --- a/actions/docker/docker_image_rm_action.go +++ b/actions/docker/docker_image_rm_action.go @@ -2,26 +2,29 @@ package docker import ( "context" - "fmt" "log/slog" "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) -// NewDockerImageRmAction creates a DockerImageRmAction instance +// NewDockerImageRmAction creates a new DockerImageRmAction with the given logger func NewDockerImageRmAction(logger *slog.Logger) *DockerImageRmAction { return &DockerImageRmAction{ - BaseAction: task_engine.NewBaseAction(logger), - CommandProcessor: command.NewDefaultCommandRunner(), - RemovedImages: []string{}, + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + CommandProcessor: command.NewDefaultCommandRunner(), } } // DockerImageRmAction removes Docker images by name/tag or ID type DockerImageRmAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder CommandProcessor command.CommandRunner RemovedImages []string // Stores the IDs of removed images for GetOutput Output string // Stores the command output for GetOutput @@ -34,23 +37,23 @@ type DockerImageRmAction struct { NoPruneParam task_engine.ActionParameter } -// WithParameters sets the parameters and returns a wrapped Action -func (a *DockerImageRmAction) WithParameters(imageNameParam, imageIDParam, removeByIDParam, forceParam, noPruneParam task_engine.ActionParameter) (*task_engine.Action[*DockerImageRmAction], error) { - if imageNameParam == nil || imageIDParam == nil || removeByIDParam == nil { - return nil, fmt.Errorf("imageNameParam, imageIDParam, and removeByIDParam cannot be nil") - } - +// WithParameters sets the parameters for image removal and returns a wrapped Action +func (a *DockerImageRmAction) WithParameters( + imageNameParam task_engine.ActionParameter, + imageIDParam task_engine.ActionParameter, + removeByIDParam task_engine.ActionParameter, + forceParam task_engine.ActionParameter, + noPruneParam task_engine.ActionParameter, +) (*task_engine.Action[*DockerImageRmAction], error) { a.ImageNameParam = imageNameParam a.ImageIDParam = imageIDParam a.RemoveByIDParam = removeByIDParam a.ForceParam = forceParam a.NoPruneParam = noPruneParam - return &task_engine.Action[*DockerImageRmAction]{ - ID: "docker-image-rm-action", - Name: "Docker Image Remove", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*DockerImageRmAction](a.Logger) + return constructor.WrapAction(a, "Docker Image RM", "docker-image-rm-action"), nil } // SetCommandRunner allows injecting a mock or alternative CommandRunner for testing @@ -59,80 +62,54 @@ func (a *DockerImageRmAction) SetCommandRunner(runner command.CommandRunner) { } func (a *DockerImageRmAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve image name parameter var imageName string if a.ImageNameParam != nil { - imageNameValue, err := a.ImageNameParam.Resolve(execCtx, globalContext) + imageNameValue, err := a.ResolveStringParameter(execCtx, a.ImageNameParam, "image name") if err != nil { - return fmt.Errorf("failed to resolve image name parameter: %w", err) - } - if v, ok := imageNameValue.(string); ok { - imageName = v - } else { - return fmt.Errorf("image name parameter is not a string, got %T", imageNameValue) + return err } + imageName = imageNameValue } // Resolve image ID parameter var imageID string if a.ImageIDParam != nil { - imageIDValue, err := a.ImageIDParam.Resolve(execCtx, globalContext) + imageIDValue, err := a.ResolveStringParameter(execCtx, a.ImageIDParam, "image ID") if err != nil { - return fmt.Errorf("failed to resolve image ID parameter: %w", err) - } - if v, ok := imageIDValue.(string); ok { - imageID = v - } else { - return fmt.Errorf("image ID parameter is not a string, got %T", imageIDValue) + return err } + imageID = imageIDValue } // Resolve removeByID parameter var removeByID bool if a.RemoveByIDParam != nil { - removeByIDValue, err := a.RemoveByIDParam.Resolve(execCtx, globalContext) + removeByIDValue, err := a.ResolveBoolParameter(execCtx, a.RemoveByIDParam, "removeByID") if err != nil { - return fmt.Errorf("failed to resolve removeByID parameter: %w", err) - } - if v, ok := removeByIDValue.(bool); ok { - removeByID = v - } else { - return fmt.Errorf("removeByID parameter is not a bool, got %T", removeByIDValue) + return err } + removeByID = removeByIDValue } // Resolve force parameter var force bool if a.ForceParam != nil { - forceValue, err := a.ForceParam.Resolve(execCtx, globalContext) + forceValue, err := a.ResolveBoolParameter(execCtx, a.ForceParam, "force") if err != nil { - return fmt.Errorf("failed to resolve force parameter: %w", err) - } - if v, ok := forceValue.(bool); ok { - force = v - } else { - return fmt.Errorf("force parameter is not a bool, got %T", forceValue) + return err } + force = forceValue } // Resolve noPrune parameter var noPrune bool if a.NoPruneParam != nil { - noPruneValue, err := a.NoPruneParam.Resolve(execCtx, globalContext) + noPruneValue, err := a.ResolveBoolParameter(execCtx, a.NoPruneParam, "noPrune") if err != nil { - return fmt.Errorf("failed to resolve noPrune parameter: %w", err) - } - if v, ok := noPruneValue.(bool); ok { - noPrune = v - } else { - return fmt.Errorf("noPrune parameter is not a bool, got %T", noPruneValue) + return err } + noPrune = noPruneValue } args := []string{"image", "rm"} @@ -175,12 +152,10 @@ func (a *DockerImageRmAction) Execute(execCtx context.Context) error { // GetOutput returns information about removed images and raw output func (a *DockerImageRmAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildOutputWithCount(a.RemovedImages, len(a.RemovedImages) > 0, map[string]interface{}{ "removed": a.RemovedImages, - "count": len(a.RemovedImages), "output": a.Output, - "success": len(a.RemovedImages) > 0, - } + }) } // parseRemovedImages extracts image IDs from the docker image rm output diff --git a/actions/docker/docker_image_rm_action_test.go b/actions/docker/docker_image_rm_action_test.go index 2dd0791..85d74a3 100644 --- a/actions/docker/docker_image_rm_action_test.go +++ b/actions/docker/docker_image_rm_action_test.go @@ -923,7 +923,7 @@ func (suite *DockerImageRmActionTestSuite) TestExecute_WithNonStringImageNamePar execErr := action.Wrapped.Execute(context.Background()) suite.Error(execErr) - suite.ErrorContains(execErr, "image name parameter is not a string, got int") + suite.ErrorContains(execErr, "image name parameter resolved to non-string value") } func (suite *DockerImageRmActionTestSuite) TestExecute_WithNonStringImageIDParameter() { @@ -937,7 +937,7 @@ func (suite *DockerImageRmActionTestSuite) TestExecute_WithNonStringImageIDParam execErr := action.Wrapped.Execute(context.Background()) suite.Error(execErr) - suite.ErrorContains(execErr, "image ID parameter is not a string, got int") + suite.ErrorContains(execErr, "image ID parameter resolved to non-string value") } // ===== COMPLEX PARAMETER SCENARIOS ===== diff --git a/actions/docker/docker_load_action.go b/actions/docker/docker_load_action.go index 67fafa2..ab468f6 100644 --- a/actions/docker/docker_load_action.go +++ b/actions/docker/docker_load_action.go @@ -2,47 +2,51 @@ package docker import ( "context" - "fmt" "log/slog" "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) -// DockerLoadActionBuilder provides a fluent interface for building DockerLoadAction +// DockerLoadActionBuilder provides the new constructor pattern type DockerLoadActionBuilder struct { - logger *slog.Logger - tarFilePathParam task_engine.ActionParameter - options []DockerLoadOption + common.BaseConstructor[*DockerLoadAction] + options []DockerLoadOption } -// NewDockerLoadAction creates a fluent builder for DockerLoadAction +// NewDockerLoadAction creates a new DockerLoadAction builder func NewDockerLoadAction(logger *slog.Logger) *DockerLoadActionBuilder { return &DockerLoadActionBuilder{ - logger: logger, + BaseConstructor: *common.NewBaseConstructor[*DockerLoadAction](logger), + options: []DockerLoadOption{}, } } -// WithParameters sets the parameters for tar file path -func (b *DockerLoadActionBuilder) WithParameters(tarFilePathParam task_engine.ActionParameter) (*task_engine.Action[*DockerLoadAction], error) { - b.tarFilePathParam = tarFilePathParam - +// WithParameters creates a DockerLoadAction with the specified parameters +func (b *DockerLoadActionBuilder) WithParameters( + tarFilePathParam task_engine.ActionParameter, +) (*task_engine.Action[*DockerLoadAction], error) { action := &DockerLoadAction{ - BaseAction: task_engine.NewBaseAction(b.logger), + BaseAction: task_engine.NewBaseAction(b.GetLogger()), TarFilePath: "", Platform: "", Quiet: false, CommandProcessor: command.NewDefaultCommandRunner(), - TarFilePathParam: b.tarFilePathParam, + Output: "", + LoadedImages: []string{}, + TarFilePathParam: tarFilePathParam, } + + // Apply options for _, option := range b.options { option(action) } - // ID reflects tar file path presence in tests; generate stable ID when provided + // Generate custom ID based on tar file path for backward compatibility id := "docker-load-action" - if sp, ok := b.tarFilePathParam.(task_engine.StaticParameter); ok { + if sp, ok := tarFilePathParam.(task_engine.StaticParameter); ok { if pathStr, ok2 := sp.Value.(string); ok2 { cleaned := strings.TrimSpace(pathStr) cleaned = strings.ReplaceAll(cleaned, " ", "-") @@ -58,11 +62,8 @@ func (b *DockerLoadActionBuilder) WithParameters(tarFilePathParam task_engine.Ac } } } - return &task_engine.Action[*DockerLoadAction]{ - ID: id, - Name: "Docker Load", - Wrapped: action, - }, nil + + return b.WrapAction(action, "Docker Load", id), nil } // WithOptions adds options to the builder @@ -91,6 +92,8 @@ func WithQuiet() DockerLoadOption { // DockerLoadAction loads a Docker image from a tar archive file type DockerLoadAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder TarFilePath string Platform string Quiet bool @@ -108,23 +111,13 @@ func (a *DockerLoadAction) SetCommandRunner(runner command.CommandRunner) { } func (a *DockerLoadAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve tar file path parameter if it exists if a.TarFilePathParam != nil { - tarFilePathValue, err := a.TarFilePathParam.Resolve(execCtx, globalContext) + tarFilePathValue, err := a.ResolveStringParameter(execCtx, a.TarFilePathParam, "tar file path") if err != nil { - return fmt.Errorf("failed to resolve tar file path parameter: %w", err) - } - if tarFilePathStr, ok := tarFilePathValue.(string); ok { - a.TarFilePath = tarFilePathStr - } else { - return fmt.Errorf("tar file path parameter is not a string, got %T", tarFilePathValue) + return err } + a.TarFilePath = tarFilePathValue } // If no tar file path provided, honor tests that expect empty path to still attempt command @@ -158,13 +151,11 @@ func (a *DockerLoadAction) Execute(execCtx context.Context) error { // GetOutput returns information about loaded images and raw output func (a *DockerLoadAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildOutputWithCount(a.LoadedImages, len(a.LoadedImages) > 0, map[string]interface{}{ "loadedImages": a.LoadedImages, - "count": len(a.LoadedImages), "output": a.Output, "tarFile": a.TarFilePath, - "success": len(a.LoadedImages) > 0, - } + }) } // parseLoadedImages extracts image names from the docker load output diff --git a/actions/docker/docker_ps_action.go b/actions/docker/docker_ps_action.go index 1036c4c..da8c86b 100644 --- a/actions/docker/docker_ps_action.go +++ b/actions/docker/docker_ps_action.go @@ -7,6 +7,7 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) @@ -83,19 +84,9 @@ func WithPsSize() DockerPsOption { // DockerPsAction lists Docker containers type DockerPsAction struct { task_engine.BaseAction - All bool - Filter string - Format string - Last int - Latest bool - NoTrunc bool - Quiet bool - Size bool - CommandProcessor command.CommandRunner - Output string - Containers []Container // Stores the parsed containers - - // Parameter-aware fields + common.ParameterResolver + common.OutputBuilder + // Parameter fields FilterParam task_engine.ActionParameter AllParam task_engine.ActionParameter QuietParam task_engine.ActionParameter @@ -103,23 +94,26 @@ type DockerPsAction struct { SizeParam task_engine.ActionParameter LatestParam task_engine.ActionParameter LastParam task_engine.ActionParameter + // Runtime resolved values + Filter string + All bool + Quiet bool + NoTrunc bool + Size bool + Latest bool + Last int + Format string + // Command execution + CommandProcessor command.CommandRunner + Output string + Containers []Container } -// SetCommandRunner allows injecting a mock or alternative CommandRunner for testing -func (a *DockerPsAction) SetCommandRunner(runner command.CommandRunner) { - a.CommandProcessor = runner -} - -func (a *DockerPsAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - +// ResolveParameters resolves all parameters using the ParameterResolver +func (a *DockerPsAction) ResolveParameters(ctx context.Context, globalContext *task_engine.GlobalContext) error { // Resolve filter parameter if it exists if a.FilterParam != nil { - filterValue, err := a.FilterParam.Resolve(execCtx, globalContext) + filterValue, err := a.FilterParam.Resolve(ctx, globalContext) if err != nil { return fmt.Errorf("failed to resolve filter parameter: %w", err) } @@ -135,7 +129,7 @@ func (a *DockerPsAction) Execute(execCtx context.Context) error { // Resolve all parameter if it exists if a.AllParam != nil { - allValue, err := a.AllParam.Resolve(execCtx, globalContext) + allValue, err := a.AllParam.Resolve(ctx, globalContext) if err != nil { return fmt.Errorf("failed to resolve all parameter: %w", err) } @@ -148,7 +142,7 @@ func (a *DockerPsAction) Execute(execCtx context.Context) error { // Resolve quiet parameter if it exists if a.QuietParam != nil { - quietValue, err := a.QuietParam.Resolve(execCtx, globalContext) + quietValue, err := a.QuietParam.Resolve(ctx, globalContext) if err != nil { return fmt.Errorf("failed to resolve quiet parameter: %w", err) } @@ -161,7 +155,7 @@ func (a *DockerPsAction) Execute(execCtx context.Context) error { // Resolve noTrunc parameter if it exists if a.NoTruncParam != nil { - noTruncValue, err := a.NoTruncParam.Resolve(execCtx, globalContext) + noTruncValue, err := a.NoTruncParam.Resolve(ctx, globalContext) if err != nil { return fmt.Errorf("failed to resolve noTrunc parameter: %w", err) } @@ -174,7 +168,7 @@ func (a *DockerPsAction) Execute(execCtx context.Context) error { // Resolve size parameter if it exists if a.SizeParam != nil { - sizeValue, err := a.SizeParam.Resolve(execCtx, globalContext) + sizeValue, err := a.SizeParam.Resolve(ctx, globalContext) if err != nil { return fmt.Errorf("failed to resolve size parameter: %w", err) } @@ -187,7 +181,7 @@ func (a *DockerPsAction) Execute(execCtx context.Context) error { // Resolve latest parameter if it exists if a.LatestParam != nil { - latestValue, err := a.LatestParam.Resolve(execCtx, globalContext) + latestValue, err := a.LatestParam.Resolve(ctx, globalContext) if err != nil { return fmt.Errorf("failed to resolve latest parameter: %w", err) } @@ -200,7 +194,7 @@ func (a *DockerPsAction) Execute(execCtx context.Context) error { // Resolve last parameter if it exists if a.LastParam != nil { - lastValue, err := a.LastParam.Resolve(execCtx, globalContext) + lastValue, err := a.LastParam.Resolve(ctx, globalContext) if err != nil { return fmt.Errorf("failed to resolve last parameter: %w", err) } @@ -211,6 +205,26 @@ func (a *DockerPsAction) Execute(execCtx context.Context) error { } } + return nil +} + +// SetCommandRunner allows injecting a mock or alternative CommandRunner for testing +func (a *DockerPsAction) SetCommandRunner(runner command.CommandRunner) { + a.CommandProcessor = runner +} + +func (a *DockerPsAction) Execute(execCtx context.Context) error { + // Extract GlobalContext from context + var globalContext *task_engine.GlobalContext + if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { + globalContext = gc + } + + // Resolve parameters using the ParameterResolver + if err := a.ResolveParameters(execCtx, globalContext); err != nil { + return fmt.Errorf("failed to resolve parameters: %w", err) + } + args := []string{"ps"} if a.All { @@ -268,12 +282,10 @@ func (a *DockerPsAction) Execute(execCtx context.Context) error { // GetOutput returns parsed container information and raw output metadata func (a *DockerPsAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildOutputWithCount(a.Containers, true, map[string]interface{}{ "containers": a.Containers, - "count": len(a.Containers), - "output": a.Output, - "success": true, - } + "rawOutput": a.Output, + }) } // parseContainers parses the docker ps output and populates the Containers slice @@ -499,13 +511,13 @@ func isNumeric(s string) bool { // DockerPsActionConstructor provides the new constructor pattern type DockerPsActionConstructor struct { - logger *slog.Logger + common.BaseConstructor[*DockerPsAction] } // NewDockerPsAction creates a new DockerPsAction constructor func NewDockerPsAction(logger *slog.Logger) *DockerPsActionConstructor { return &DockerPsActionConstructor{ - logger: logger, + BaseConstructor: *common.NewBaseConstructor[*DockerPsAction](logger), } } @@ -520,29 +532,26 @@ func (c *DockerPsActionConstructor) WithParameters( lastParam task_engine.ActionParameter, ) (*task_engine.Action[*DockerPsAction], error) { action := &DockerPsAction{ - BaseAction: task_engine.NewBaseAction(c.logger), - All: false, - Filter: "", - Format: "", - Last: 0, - Latest: false, - NoTrunc: false, - Quiet: false, - Size: false, - CommandProcessor: command.NewDefaultCommandRunner(), - FilterParam: filterParam, - AllParam: allParam, - QuietParam: quietParam, - NoTruncParam: noTruncParam, - SizeParam: sizeParam, - LatestParam: latestParam, - LastParam: lastParam, - } - - id := "docker-ps-action" - return &task_engine.Action[*DockerPsAction]{ - ID: id, - Name: "Docker PS", - Wrapped: action, - }, nil + BaseAction: task_engine.NewBaseAction(c.GetLogger()), + ParameterResolver: *common.NewParameterResolver(c.GetLogger()), + OutputBuilder: *common.NewOutputBuilder(c.GetLogger()), + All: false, + Filter: "", + Format: "", + Last: 0, + Latest: false, + NoTrunc: false, + Quiet: false, + Size: false, + CommandProcessor: command.NewDefaultCommandRunner(), + FilterParam: filterParam, + AllParam: allParam, + QuietParam: quietParam, + NoTruncParam: noTruncParam, + SizeParam: sizeParam, + LatestParam: latestParam, + LastParam: lastParam, + } + + return c.WrapAction(action, "Docker PS", "docker-ps-action"), nil } diff --git a/actions/docker/docker_ps_action_test.go b/actions/docker/docker_ps_action_test.go index 15e7146..e6de2ed 100644 --- a/actions/docker/docker_ps_action_test.go +++ b/actions/docker/docker_ps_action_test.go @@ -7,6 +7,7 @@ import ( "testing" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/testing/mocks" "github.com/stretchr/testify/suite" ) @@ -567,15 +568,18 @@ func (suite *DockerPsActionTestSuite) TestDockerPsAction_Execute_WhitespaceOnlyO func (suite *DockerPsActionTestSuite) TestDockerPsAction_GetOutput() { action := &DockerPsAction{ - Output: "raw output", - Containers: []Container{{ContainerID: "abc123", Image: "nginx:latest"}}, + BaseAction: task_engine.NewBaseAction(slog.Default()), + ParameterResolver: *common.NewParameterResolver(slog.Default()), + OutputBuilder: *common.NewOutputBuilder(slog.Default()), + Output: "raw output", + Containers: []Container{{ContainerID: "abc123", Image: "nginx:latest"}}, } out := action.GetOutput() suite.IsType(map[string]interface{}{}, out) m := out.(map[string]interface{}) suite.Equal(1, m["count"]) - suite.Equal("raw output", m["output"]) + suite.Equal("raw output", m["rawOutput"]) suite.Equal(true, m["success"]) suite.Len(m["containers"], 1) } diff --git a/actions/docker/docker_pull_action.go b/actions/docker/docker_pull_action.go index d4689c7..abb4ec5 100644 --- a/actions/docker/docker_pull_action.go +++ b/actions/docker/docker_pull_action.go @@ -7,6 +7,7 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) @@ -24,13 +25,13 @@ type MultiArchImageSpec struct { // DockerPullActionConstructor provides the new constructor pattern type DockerPullActionConstructor struct { - logger *slog.Logger + common.BaseConstructor[*DockerPullAction] } // NewDockerPullAction creates a new DockerPullAction constructor func NewDockerPullAction(logger *slog.Logger) *DockerPullActionConstructor { return &DockerPullActionConstructor{ - logger: logger, + BaseConstructor: *common.NewBaseConstructor[*DockerPullAction](logger), } } @@ -43,13 +44,16 @@ func (c *DockerPullActionConstructor) WithParameters( platformParam task_engine.ActionParameter, ) (*task_engine.Action[*DockerPullAction], error) { action := &DockerPullAction{ - BaseAction: task_engine.NewBaseAction(c.logger), + BaseAction: task_engine.NewBaseAction(c.GetLogger()), Images: make(map[string]ImageSpec), MultiArchImages: make(map[string]MultiArchImageSpec), AllTags: false, Quiet: false, Platform: "", CommandProcessor: command.NewDefaultCommandRunner(), + Output: "", + PulledImages: []string{}, + FailedImages: []string{}, ImagesParam: imagesParam, MultiArchImagesParam: multiArchImagesParam, AllTagsParam: allTagsParam, @@ -57,12 +61,7 @@ func (c *DockerPullActionConstructor) WithParameters( PlatformParam: platformParam, } - id := "docker-pull-action" - return &task_engine.Action[*DockerPullAction]{ - ID: id, - Name: "Docker Pull", - Wrapped: action, - }, nil + return c.WrapAction(action, "Docker Pull", "docker-pull-action"), nil } // Backward compatibility functions @@ -132,6 +131,8 @@ func WithPullPlatform(platform string) DockerPullOption { type DockerPullAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder Images map[string]ImageSpec MultiArchImages map[string]MultiArchImageSpec AllTags bool @@ -155,17 +156,11 @@ func (a *DockerPullAction) SetCommandRunner(runner command.CommandRunner) { } func (a *DockerPullAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve images via parameter if provided if a.ImagesParam != nil { - v, err := a.ImagesParam.Resolve(execCtx, globalContext) + v, err := a.ResolveParameter(execCtx, a.ImagesParam, "images") if err != nil { - return fmt.Errorf("failed to resolve images parameter: %w", err) + return err } switch typed := v.(type) { case map[string]ImageSpec: @@ -196,9 +191,9 @@ func (a *DockerPullAction) Execute(execCtx context.Context) error { // Resolve multiarch images via parameter if provided if a.MultiArchImagesParam != nil { - v, err := a.MultiArchImagesParam.Resolve(execCtx, globalContext) + v, err := a.ResolveParameter(execCtx, a.MultiArchImagesParam, "multiarch images") if err != nil { - return fmt.Errorf("failed to resolve multiarch images parameter: %w", err) + return err } switch typed := v.(type) { case map[string]MultiArchImageSpec: @@ -238,9 +233,9 @@ func (a *DockerPullAction) Execute(execCtx context.Context) error { // Resolve AllTags parameter if provided if a.AllTagsParam != nil { - v, err := a.AllTagsParam.Resolve(execCtx, globalContext) + v, err := a.ResolveParameter(execCtx, a.AllTagsParam, "allTags") if err != nil { - return fmt.Errorf("failed to resolve allTags parameter: %w", err) + return err } if allTagsBool, ok := v.(bool); ok { a.AllTags = allTagsBool @@ -251,9 +246,9 @@ func (a *DockerPullAction) Execute(execCtx context.Context) error { // Resolve Quiet parameter if provided if a.QuietParam != nil { - v, err := a.QuietParam.Resolve(execCtx, globalContext) + v, err := a.ResolveParameter(execCtx, a.QuietParam, "quiet") if err != nil { - return fmt.Errorf("failed to resolve quiet parameter: %w", err) + return err } if quietBool, ok := v.(bool); ok { a.Quiet = quietBool @@ -264,9 +259,9 @@ func (a *DockerPullAction) Execute(execCtx context.Context) error { // Resolve Platform parameter if provided if a.PlatformParam != nil { - v, err := a.PlatformParam.Resolve(execCtx, globalContext) + v, err := a.ResolveParameter(execCtx, a.PlatformParam, "platform") if err != nil { - return fmt.Errorf("failed to resolve platform parameter: %w", err) + return err } if platformStr, ok := v.(string); ok { if strings.TrimSpace(platformStr) != "" { diff --git a/actions/docker/docker_run_action.go b/actions/docker/docker_run_action.go index d1e8766..2d9ed8f 100644 --- a/actions/docker/docker_run_action.go +++ b/actions/docker/docker_run_action.go @@ -8,48 +8,46 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) -// DockerRunActionBuilder provides a fluent interface for building DockerRunAction +// DockerRunActionBuilder provides the new constructor pattern type DockerRunActionBuilder struct { - logger *slog.Logger - imageParam task_engine.ActionParameter - outputBuffer *bytes.Buffer - runArgs []string + common.BaseConstructor[*DockerRunAction] } -// NewDockerRunAction creates a fluent builder for DockerRunAction +// NewDockerRunAction creates a new DockerRunAction builder func NewDockerRunAction(logger *slog.Logger) *DockerRunActionBuilder { return &DockerRunActionBuilder{ - logger: logger, + BaseConstructor: *common.NewBaseConstructor[*DockerRunAction](logger), } } -// WithParameters sets the parameters for image, output buffer, and run arguments -func (b *DockerRunActionBuilder) WithParameters(imageParam task_engine.ActionParameter, outputBuffer *bytes.Buffer, runArgs ...string) (*task_engine.Action[*DockerRunAction], error) { - b.imageParam = imageParam - b.outputBuffer = outputBuffer - b.runArgs = runArgs +// WithParameters creates a DockerRunAction with the specified parameters +func (b *DockerRunActionBuilder) WithParameters( + imageParam task_engine.ActionParameter, + outputBuffer *bytes.Buffer, + runArgs ...string, +) (*task_engine.Action[*DockerRunAction], error) { + action := &DockerRunAction{ + BaseAction: task_engine.NewBaseAction(b.GetLogger()), + Image: "", + RunArgs: runArgs, + commandRunner: command.NewDefaultCommandRunner(), + Output: "", + OutputBuffer: outputBuffer, + ImageParam: imageParam, + } - id := "docker-run-action" - return &task_engine.Action[*DockerRunAction]{ - ID: id, - Name: "Docker Run", - Wrapped: &DockerRunAction{ - BaseAction: task_engine.NewBaseAction(b.logger), - Image: "", - RunArgs: b.runArgs, - OutputBuffer: b.outputBuffer, - commandRunner: command.NewDefaultCommandRunner(), - ImageParam: b.imageParam, - }, - }, nil + return b.WrapAction(action, "Docker Run", "docker-run-action"), nil } // NOTE: Command arguments for inside the container should be part of RunArgs type DockerRunAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder Image string RunArgs []string commandRunner command.CommandRunner @@ -64,24 +62,14 @@ func (a *DockerRunAction) SetCommandRunner(runner command.CommandRunner) { } func (a *DockerRunAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - // Resolve image via parameter if provided effectiveImage := a.Image if a.ImageParam != nil { - v, err := a.ImageParam.Resolve(execCtx, globalContext) + s, err := a.ResolveStringParameter(execCtx, a.ImageParam, "image") if err != nil { - return fmt.Errorf("failed to resolve image parameter: %w", err) - } - if s, ok := v.(string); ok { - effectiveImage = s - } else { - return fmt.Errorf("resolved image parameter is not a string: %T", v) + return err } + effectiveImage = s } args := []string{"run"} @@ -111,10 +99,9 @@ func (a *DockerRunAction) Execute(execCtx context.Context) error { // GetOutput returns information about the docker run execution func (a *DockerRunAction) GetOutput() interface{} { - return map[string]interface{}{ - "image": a.Image, - "args": a.RunArgs, - "output": a.Output, - "success": a.Output != "", - } + return a.BuildStandardOutput(nil, a.Output != "", map[string]interface{}{ + "image": a.Image, + "args": a.RunArgs, + "output": a.Output, + }) } diff --git a/actions/docker/docker_status_action.go b/actions/docker/docker_status_action.go index 32a9fe9..55433fc 100644 --- a/actions/docker/docker_status_action.go +++ b/actions/docker/docker_status_action.go @@ -8,6 +8,7 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) @@ -19,39 +20,39 @@ type ContainerState struct { Status string `json:"status"` } -// GetContainerStateActionBuilder provides a fluent interface for building GetContainerStateAction +// GetContainerStateActionBuilder provides the new constructor pattern type GetContainerStateActionBuilder struct { - logger *slog.Logger - containerNameParam task_engine.ActionParameter + common.BaseConstructor[*GetContainerStateAction] } -// NewGetContainerStateAction creates a fluent builder for GetContainerStateAction +// NewGetContainerStateAction creates a new GetContainerStateAction builder func NewGetContainerStateAction(logger *slog.Logger) *GetContainerStateActionBuilder { return &GetContainerStateActionBuilder{ - logger: logger, + BaseConstructor: *common.NewBaseConstructor[*GetContainerStateAction](logger), } } -// WithParameters sets the parameters for container name -func (b *GetContainerStateActionBuilder) WithParameters(containerNameParam task_engine.ActionParameter) (*task_engine.Action[*GetContainerStateAction], error) { - b.containerNameParam = containerNameParam - - id := "get-container-state-action" - return &task_engine.Action[*GetContainerStateAction]{ - ID: id, - Name: "Get Container State", - Wrapped: &GetContainerStateAction{ - BaseAction: task_engine.NewBaseAction(b.logger), - ContainerName: "", - CommandProcessor: command.NewDefaultCommandRunner(), - ContainerNameParam: b.containerNameParam, - }, - }, nil +// WithParameters creates a GetContainerStateAction with the specified parameters +func (b *GetContainerStateActionBuilder) WithParameters( + containerNameParam task_engine.ActionParameter, +) (*task_engine.Action[*GetContainerStateAction], error) { + action := &GetContainerStateAction{ + BaseAction: task_engine.NewBaseAction(b.GetLogger()), + ContainerIDs: []string{}, + CommandProcessor: command.NewDefaultCommandRunner(), + ContainerStates: []ContainerState{}, + ContainerName: "", + ContainerNameParam: containerNameParam, + } + + return b.WrapAction(action, "Get Container State", "get-container-state-action"), nil } // GetContainerStateAction retrieves the state of Docker containers type GetContainerStateAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder ContainerIDs []string CommandProcessor command.CommandRunner ContainerStates []ContainerState @@ -67,36 +68,24 @@ func (a *GetContainerStateAction) SetCommandProcessor(processor command.CommandR } func (a *GetContainerStateAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve container name parameter if it exists + // Resolve container name parameter if it exists using ParameterResolver if a.ContainerNameParam != nil { - containerNameValue, err := a.ContainerNameParam.Resolve(execCtx, globalContext) + names, err := a.ResolveStringSliceParameter(execCtx, a.ContainerNameParam, "container name") if err != nil { - return fmt.Errorf("failed to resolve container name parameter: %w", err) + return err } - switch v := containerNameValue.(type) { - case string: - a.ContainerName = v - if strings.TrimSpace(v) != "" { - a.ContainerIDs = []string{v} - } - case []string: - filtered := make([]string, 0, len(v)) - for _, name := range v { - if strings.TrimSpace(name) != "" { - filtered = append(filtered, name) - } - } - if len(filtered) > 0 { - a.ContainerIDs = filtered + filtered := make([]string, 0, len(names)) + for _, name := range names { + trimmed := strings.TrimSpace(name) + if trimmed != "" { + filtered = append(filtered, trimmed) } - default: - return fmt.Errorf("container name parameter is not a string, got %T", containerNameValue) + } + if len(filtered) == 1 { + a.ContainerName = filtered[0] + } + if len(filtered) > 0 { + a.ContainerIDs = filtered } } @@ -220,9 +209,7 @@ func (a *GetContainerStateAction) parseContainerOutput(output string) ([]Contain // GetOutput returns the retrieved container states func (a *GetContainerStateAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildOutputWithCount(a.ContainerStates, true, map[string]interface{}{ "containers": a.ContainerStates, - "count": len(a.ContainerStates), - "success": true, - } + }) } diff --git a/actions/file/change_ownership_action.go b/actions/file/change_ownership_action.go index a2a379d..9044ee7 100644 --- a/actions/file/change_ownership_action.go +++ b/actions/file/change_ownership_action.go @@ -7,22 +7,24 @@ import ( "os" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) // NewChangeOwnershipAction creates a new ChangeOwnershipAction with the given logger func NewChangeOwnershipAction(logger *slog.Logger) *ChangeOwnershipAction { - if logger == nil { - logger = slog.Default() - } return &ChangeOwnershipAction{ - BaseAction: task_engine.NewBaseAction(logger), - commandRunner: command.NewDefaultCommandRunner(), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + commandRunner: command.NewDefaultCommandRunner(), } } type ChangeOwnershipAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder // Parameters PathParam task_engine.ActionParameter @@ -38,19 +40,21 @@ type ChangeOwnershipAction struct { commandRunner command.CommandRunner } -// WithParameters sets the parameters for path, owner, and group and returns a wrapped Action -func (a *ChangeOwnershipAction) WithParameters(pathParam, ownerParam, groupParam task_engine.ActionParameter, recursive bool) (*task_engine.Action[*ChangeOwnershipAction], error) { +// WithParameters sets the parameters for ownership change and returns a wrapped Action +func (a *ChangeOwnershipAction) WithParameters( + pathParam task_engine.ActionParameter, + ownerParam task_engine.ActionParameter, + groupParam task_engine.ActionParameter, + recursive bool, +) (*task_engine.Action[*ChangeOwnershipAction], error) { a.PathParam = pathParam a.OwnerParam = ownerParam a.GroupParam = groupParam a.Recursive = recursive - id := "change-ownership-action" - return &task_engine.Action[*ChangeOwnershipAction]{ - ID: id, - Name: "Change Ownership", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*ChangeOwnershipAction](a.Logger) + return constructor.WrapAction(a, "Change Ownership", "change-ownership-action"), nil } func (a *ChangeOwnershipAction) SetCommandRunner(runner command.CommandRunner) { @@ -58,47 +62,29 @@ func (a *ChangeOwnershipAction) SetCommandRunner(runner command.CommandRunner) { } func (a *ChangeOwnershipAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve parameters if they exist + // Resolve parameters using the ParameterResolver if a.PathParam != nil { - pathValue, err := a.PathParam.Resolve(execCtx, globalContext) + pathValue, err := a.ResolveStringParameter(execCtx, a.PathParam, "path") if err != nil { - return fmt.Errorf("failed to resolve path parameter: %w", err) - } - if pathStr, ok := pathValue.(string); ok { - a.Path = pathStr - } else { - return fmt.Errorf("path parameter is not a string, got %T", pathValue) + return err } + a.Path = pathValue } if a.OwnerParam != nil { - ownerValue, err := a.OwnerParam.Resolve(execCtx, globalContext) + ownerValue, err := a.ResolveStringParameter(execCtx, a.OwnerParam, "owner") if err != nil { - return fmt.Errorf("failed to resolve owner parameter: %w", err) - } - if ownerStr, ok := ownerValue.(string); ok { - a.Owner = ownerStr - } else { - return fmt.Errorf("owner parameter is not a string, got %T", ownerValue) + return err } + a.Owner = ownerValue } if a.GroupParam != nil { - groupValue, err := a.GroupParam.Resolve(execCtx, globalContext) + groupValue, err := a.ResolveStringParameter(execCtx, a.GroupParam, "group") if err != nil { - return fmt.Errorf("failed to resolve group parameter: %w", err) - } - if groupStr, ok := groupValue.(string); ok { - a.Group = groupStr - } else { - return fmt.Errorf("group parameter is not a string, got %T", groupValue) + return err } + a.Group = groupValue } if a.Path == "" { @@ -142,11 +128,10 @@ func (a *ChangeOwnershipAction) Execute(execCtx context.Context) error { // GetOutput returns metadata about the ownership change func (a *ChangeOwnershipAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "path": a.Path, "owner": a.Owner, "group": a.Group, "recursive": a.Recursive, - "success": true, - } + }) } diff --git a/actions/file/change_permissions_action.go b/actions/file/change_permissions_action.go index 79aca76..a7448ff 100644 --- a/actions/file/change_permissions_action.go +++ b/actions/file/change_permissions_action.go @@ -7,27 +7,29 @@ import ( "os" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) // NewChangePermissionsAction creates a new ChangePermissionsAction with the given logger func NewChangePermissionsAction(logger *slog.Logger) *ChangePermissionsAction { - if logger == nil { - logger = slog.Default() - } return &ChangePermissionsAction{ - BaseAction: task_engine.NewBaseAction(logger), - commandRunner: command.NewDefaultCommandRunner(), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + commandRunner: command.NewDefaultCommandRunner(), } } type ChangePermissionsAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder // Parameters - PathParam task_engine.ActionParameter - PermissionsParam task_engine.ActionParameter - Recursive bool + PathParam task_engine.ActionParameter + ModeParam task_engine.ActionParameter + Recursive bool // Runtime resolved values Path string @@ -36,18 +38,19 @@ type ChangePermissionsAction struct { commandRunner command.CommandRunner } -// WithParameters sets the parameters for path and permissions and returns a wrapped Action -func (a *ChangePermissionsAction) WithParameters(pathParam, permissionsParam task_engine.ActionParameter, recursive bool) (*task_engine.Action[*ChangePermissionsAction], error) { +// WithParameters sets the parameters for permission change and returns a wrapped Action +func (a *ChangePermissionsAction) WithParameters( + pathParam task_engine.ActionParameter, + modeParam task_engine.ActionParameter, + recursive bool, +) (*task_engine.Action[*ChangePermissionsAction], error) { a.PathParam = pathParam - a.PermissionsParam = permissionsParam + a.ModeParam = modeParam a.Recursive = recursive - id := "change-permissions-action" - return &task_engine.Action[*ChangePermissionsAction]{ - ID: id, - Name: "Change Permissions", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*ChangePermissionsAction](a.Logger) + return constructor.WrapAction(a, "Change Permissions", "change-permissions-action"), nil } func (a *ChangePermissionsAction) SetCommandRunner(runner command.CommandRunner) { @@ -55,35 +58,21 @@ func (a *ChangePermissionsAction) SetCommandRunner(runner command.CommandRunner) } func (a *ChangePermissionsAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve parameters if they exist + // Resolve parameters using the ParameterResolver if a.PathParam != nil { - pathValue, err := a.PathParam.Resolve(execCtx, globalContext) + pathValue, err := a.ResolveStringParameter(execCtx, a.PathParam, "path") if err != nil { - return fmt.Errorf("failed to resolve path parameter: %w", err) - } - if pathStr, ok := pathValue.(string); ok { - a.Path = pathStr - } else { - return fmt.Errorf("path parameter is not a string, got %T", pathValue) + return err } + a.Path = pathValue } - if a.PermissionsParam != nil { - permissionsValue, err := a.PermissionsParam.Resolve(execCtx, globalContext) + if a.ModeParam != nil { + modeValue, err := a.ResolveStringParameter(execCtx, a.ModeParam, "permissions") if err != nil { - return fmt.Errorf("failed to resolve permissions parameter: %w", err) - } - if permissionsStr, ok := permissionsValue.(string); ok { - a.Permissions = permissionsStr - } else { - return fmt.Errorf("permissions parameter is not a string, got %T", permissionsValue) + return err } + a.Permissions = modeValue } if _, err := os.Stat(a.Path); os.IsNotExist(err) { @@ -109,10 +98,9 @@ func (a *ChangePermissionsAction) Execute(execCtx context.Context) error { // GetOutput returns metadata about the permission change func (a *ChangePermissionsAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "path": a.Path, "permissions": a.Permissions, "recursive": a.Recursive, - "success": true, - } + }) } diff --git a/actions/file/compress_file_action.go b/actions/file/compress_file_action.go index 714604b..60f1af2 100644 --- a/actions/file/compress_file_action.go +++ b/actions/file/compress_file_action.go @@ -10,7 +10,8 @@ import ( "os" "path/filepath" - engine "github.com/ndizazzo/task-engine" + task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) // CompressionType represents the type of compression to use @@ -27,12 +28,18 @@ const ( // NewCompressFileAction creates a new CompressFileAction with the given logger func NewCompressFileAction(logger *slog.Logger) *CompressFileAction { return &CompressFileAction{ - BaseAction: engine.NewBaseAction(logger), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), } } -// WithParameters sets the parameters for source path, destination path, and compression type -func (a *CompressFileAction) WithParameters(sourcePathParam, destinationPathParam engine.ActionParameter, compressionType CompressionType) (*engine.Action[*CompressFileAction], error) { +// WithParameters sets the parameters for file compression and returns a wrapped Action +func (a *CompressFileAction) WithParameters( + sourcePathParam task_engine.ActionParameter, + destinationPathParam task_engine.ActionParameter, + compressionType CompressionType, +) (*task_engine.Action[*CompressFileAction], error) { if compressionType == "" { return nil, fmt.Errorf("invalid parameter: compressionType cannot be empty") } @@ -49,55 +56,42 @@ func (a *CompressFileAction) WithParameters(sourcePathParam, destinationPathPara a.DestinationPathParam = destinationPathParam a.CompressionType = compressionType - return &engine.Action[*CompressFileAction]{ - ID: "compress-file-action", - Name: "Compress File", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*CompressFileAction](a.Logger) + return constructor.WrapAction(a, "Compress File", "compress-file-action"), nil } // CompressFileAction compresses a file using the specified compression algorithm type CompressFileAction struct { - engine.BaseAction + task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder SourcePath string DestinationPath string CompressionType CompressionType // Parameter-aware fields - SourcePathParam engine.ActionParameter - DestinationPathParam engine.ActionParameter + SourcePathParam task_engine.ActionParameter + DestinationPathParam task_engine.ActionParameter + CompressionTypeParam task_engine.ActionParameter } func (a *CompressFileAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *engine.GlobalContext - if gc, ok := execCtx.Value(engine.GlobalContextKey).(*engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve parameters if they exist + // Resolve parameters using the ParameterResolver if a.SourcePathParam != nil { - sourceValue, err := a.SourcePathParam.Resolve(execCtx, globalContext) + sourceValue, err := a.ResolveStringParameter(execCtx, a.SourcePathParam, "source path") if err != nil { - return fmt.Errorf("failed to resolve source path parameter: %w", err) - } - if sourceStr, ok := sourceValue.(string); ok { - a.SourcePath = sourceStr - } else { - return fmt.Errorf("source path parameter is not a string, got %T", sourceValue) + return err } + a.SourcePath = sourceValue } if a.DestinationPathParam != nil { - destValue, err := a.DestinationPathParam.Resolve(execCtx, globalContext) + destValue, err := a.ResolveStringParameter(execCtx, a.DestinationPathParam, "destination path") if err != nil { - return fmt.Errorf("failed to resolve destination path parameter: %w", err) - } - if destStr, ok := destValue.(string); ok { - a.DestinationPath = destStr - } else { - return fmt.Errorf("destination path parameter is not a string, got %T", destValue) + return err } + a.DestinationPath = destValue } a.Logger.Info("Attempting to compress file", @@ -186,10 +180,9 @@ func (a *CompressFileAction) compressGzip(source io.Reader, destination io.Write // GetOutput returns metadata about the compression operation func (a *CompressFileAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "source": a.SourcePath, "destination": a.DestinationPath, "compressionType": string(a.CompressionType), - "success": true, - } + }) } diff --git a/actions/file/copy_file_action.go b/actions/file/copy_file_action.go index 0caaf6e..7907d2d 100644 --- a/actions/file/copy_file_action.go +++ b/actions/file/copy_file_action.go @@ -9,17 +9,22 @@ import ( "path/filepath" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) // NewCopyFileAction creates a new CopyFileAction with the given logger func NewCopyFileAction(logger *slog.Logger) *CopyFileAction { return &CopyFileAction{ - BaseAction: task_engine.NewBaseAction(logger), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), } } type CopyFileAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder // Parameters SourceParam task_engine.ActionParameter @@ -32,52 +37,41 @@ type CopyFileAction struct { Destination string } -// WithParameters sets the parameters for source, destination, create directory flag, and recursive flag and returns a wrapped Action -func (a *CopyFileAction) WithParameters(sourceParam, destinationParam task_engine.ActionParameter, createDir, recursive bool) (*task_engine.Action[*CopyFileAction], error) { +// WithParameters sets the parameters for file copying and returns a wrapped Action +func (a *CopyFileAction) WithParameters( + sourceParam task_engine.ActionParameter, + destinationParam task_engine.ActionParameter, + createDir bool, + recursive bool, +) (*task_engine.Action[*CopyFileAction], error) { a.SourceParam = sourceParam a.DestinationParam = destinationParam a.CreateDir = createDir a.Recursive = recursive - id := "copy-file-action" - return &task_engine.Action[*CopyFileAction]{ - ID: id, - Name: "Copy File", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*CopyFileAction](a.Logger) + return constructor.WrapAction(a, "Copy File", "copy-file-action"), nil } func (a *CopyFileAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve parameters if they exist + // Resolve parameters using the ParameterResolver if a.SourceParam != nil { - sourceValue, err := a.SourceParam.Resolve(execCtx, globalContext) + sourceValue, err := a.ResolveStringParameter(execCtx, a.SourceParam, "source") if err != nil { - return fmt.Errorf("failed to resolve source parameter: %w", err) - } - if sourceStr, ok := sourceValue.(string); ok { - a.Source = sourceStr - } else { - return fmt.Errorf("source parameter is not a string, got %T", sourceValue) + return err } + a.Source = sourceValue } if a.DestinationParam != nil { - destValue, err := a.DestinationParam.Resolve(execCtx, globalContext) + destValue, err := a.ResolveStringParameter(execCtx, a.DestinationParam, "destination") if err != nil { - return fmt.Errorf("failed to resolve destination parameter: %w", err) - } - if destStr, ok := destValue.(string); ok { - a.Destination = destStr - } else { - return fmt.Errorf("destination parameter is not a string, got %T", destValue) + return err } + a.Destination = destValue } + if _, err := os.Stat(a.Source); os.IsNotExist(err) { a.Logger.Error("Source path does not exist", "source", a.Source) return err @@ -272,11 +266,10 @@ func (a *CopyFileAction) executeFileCopy() error { // GetOutput returns metadata about the copy operation func (a *CopyFileAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "source": a.Source, "destination": a.Destination, "createDir": a.CreateDir, "recursive": a.Recursive, - "success": true, - } + }) } diff --git a/actions/file/create_directories_action.go b/actions/file/create_directories_action.go index 089f5ed..74f6a14 100644 --- a/actions/file/create_directories_action.go +++ b/actions/file/create_directories_action.go @@ -9,30 +9,36 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) // NewCreateDirectoriesAction creates a new CreateDirectoriesAction with the given logger func NewCreateDirectoriesAction(logger *slog.Logger) *CreateDirectoriesAction { return &CreateDirectoriesAction{ - BaseAction: task_engine.NewBaseAction(logger), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), } } -// WithParameters sets the parameters for root path and directories -func (a *CreateDirectoriesAction) WithParameters(rootPathParam, directoriesParam task_engine.ActionParameter) (*task_engine.Action[*CreateDirectoriesAction], error) { +// WithParameters sets the parameters for directory creation and returns a wrapped Action +func (a *CreateDirectoriesAction) WithParameters( + rootPathParam task_engine.ActionParameter, + directoriesParam task_engine.ActionParameter, +) (*task_engine.Action[*CreateDirectoriesAction], error) { a.RootPathParam = rootPathParam a.DirectoriesParam = directoriesParam - return &task_engine.Action[*CreateDirectoriesAction]{ - ID: "create-directories-action", - Name: "Create Directories", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*CreateDirectoriesAction](a.Logger) + return constructor.WrapAction(a, "Create Directories", "create-directories-action"), nil } // CreateDirectoriesAction creates multiple directories relative to an installation path type CreateDirectoriesAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder RootPath string Directories []string CreatedDirsCount int @@ -43,29 +49,19 @@ type CreateDirectoriesAction struct { } func (a *CreateDirectoriesAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve parameters if they exist + // Resolve parameters using the ParameterResolver if a.RootPathParam != nil { - rootPathValue, err := a.RootPathParam.Resolve(execCtx, globalContext) + rootPathValue, err := a.ResolveStringParameter(execCtx, a.RootPathParam, "root path") if err != nil { - return fmt.Errorf("failed to resolve root path parameter: %w", err) - } - if rootPathStr, ok := rootPathValue.(string); ok { - a.RootPath = rootPathStr - } else { - return fmt.Errorf("root path parameter is not a string, got %T", rootPathValue) + return err } + a.RootPath = rootPathValue } if a.DirectoriesParam != nil { - directoriesValue, err := a.DirectoriesParam.Resolve(execCtx, globalContext) + directoriesValue, err := a.ResolveParameter(execCtx, a.DirectoriesParam, "directories") if err != nil { - return fmt.Errorf("failed to resolve directories parameter: %w", err) + return err } if directoriesSlice, ok := directoriesValue.([]string); ok { a.Directories = directoriesSlice @@ -115,11 +111,10 @@ func (a *CreateDirectoriesAction) Execute(execCtx context.Context) error { // GetOutput returns metadata about the directory creation func (a *CreateDirectoriesAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "rootPath": a.RootPath, "directories": a.Directories, "created": a.CreatedDirsCount, "total": len(a.Directories), - "success": true, - } + }) } diff --git a/actions/file/create_symlink_action.go b/actions/file/create_symlink_action.go index 7f9e5a4..b653a91 100644 --- a/actions/file/create_symlink_action.go +++ b/actions/file/create_symlink_action.go @@ -9,10 +9,13 @@ import ( "path/filepath" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) type CreateSymlinkAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder Target string LinkPath string Overwrite bool @@ -26,54 +29,45 @@ type CreateSymlinkAction struct { // NewCreateSymlinkAction creates a new CreateSymlinkAction with the given logger func NewCreateSymlinkAction(logger *slog.Logger) *CreateSymlinkAction { return &CreateSymlinkAction{ - BaseAction: task_engine.NewBaseAction(logger), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), } } // WithParameters sets the parameters for target, link path, overwrite flag, and create directories flag -func (a *CreateSymlinkAction) WithParameters(targetParam, linkPathParam task_engine.ActionParameter, overwrite, createDirs bool) (*task_engine.Action[*CreateSymlinkAction], error) { +func (a *CreateSymlinkAction) WithParameters( + targetParam task_engine.ActionParameter, + linkPathParam task_engine.ActionParameter, + overwrite bool, + createDirs bool, +) (*task_engine.Action[*CreateSymlinkAction], error) { a.TargetParam = targetParam a.LinkPathParam = linkPathParam a.Overwrite = overwrite a.CreateDirs = createDirs - return &task_engine.Action[*CreateSymlinkAction]{ - ID: "create-symlink-action", - Name: "Create Symlink", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*CreateSymlinkAction](a.Logger) + return constructor.WrapAction(a, "Create Symlink", "create-symlink-action"), nil } func (a *CreateSymlinkAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve parameters if they exist + // Resolve parameters using the ParameterResolver if a.TargetParam != nil { - targetValue, err := a.TargetParam.Resolve(execCtx, globalContext) + targetValue, err := a.ResolveStringParameter(execCtx, a.TargetParam, "target") if err != nil { - return fmt.Errorf("failed to resolve target parameter: %w", err) - } - if targetStr, ok := targetValue.(string); ok { - a.Target = targetStr - } else { - return fmt.Errorf("target parameter is not a string, got %T", targetValue) + return err } + a.Target = targetValue } if a.LinkPathParam != nil { - linkPathValue, err := a.LinkPathParam.Resolve(execCtx, globalContext) + linkPathValue, err := a.ResolveStringParameter(execCtx, a.LinkPathParam, "link path") if err != nil { - return fmt.Errorf("failed to resolve link path parameter: %w", err) - } - if linkPathStr, ok := linkPathValue.(string); ok { - a.LinkPath = linkPathStr - } else { - return fmt.Errorf("link path parameter is not a string, got %T", linkPathValue) + return err } + a.LinkPath = linkPathValue } // Sanitize paths to prevent path traversal attacks @@ -130,13 +124,12 @@ func (a *CreateSymlinkAction) Execute(execCtx context.Context) error { // GetOutput returns metadata about the created symlink func (a *CreateSymlinkAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "target": a.Target, "linkPath": a.LinkPath, "overwrite": a.Overwrite, "created": true, - "success": true, - } + }) } func (a *CreateSymlinkAction) verifySymlink(linkPath, expectedTarget string) error { diff --git a/actions/file/decompress_file_action.go b/actions/file/decompress_file_action.go index bbdc5a5..806dc9c 100644 --- a/actions/file/decompress_file_action.go +++ b/actions/file/decompress_file_action.go @@ -11,18 +11,25 @@ import ( "path/filepath" "strings" - engine "github.com/ndizazzo/task-engine" + task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) // NewDecompressFileAction creates a new DecompressFileAction with the given logger func NewDecompressFileAction(logger *slog.Logger) *DecompressFileAction { return &DecompressFileAction{ - BaseAction: engine.NewBaseAction(logger), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), } } // WithParameters sets the parameters for source path, destination path, and compression type -func (a *DecompressFileAction) WithParameters(sourcePathParam, destinationPathParam engine.ActionParameter, compressionType CompressionType) (*engine.Action[*DecompressFileAction], error) { +func (a *DecompressFileAction) WithParameters( + sourcePathParam task_engine.ActionParameter, + destinationPathParam task_engine.ActionParameter, + compressionType CompressionType, +) (*task_engine.Action[*DecompressFileAction], error) { // Validate compression type if specified if compressionType != "" { switch compressionType { @@ -37,55 +44,41 @@ func (a *DecompressFileAction) WithParameters(sourcePathParam, destinationPathPa a.DestinationPathParam = destinationPathParam a.CompressionType = compressionType - return &engine.Action[*DecompressFileAction]{ - ID: "decompress-file-action", - Name: "Decompress File", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*DecompressFileAction](a.Logger) + return constructor.WrapAction(a, "Decompress File", "decompress-file-action"), nil } // DecompressFileAction decompresses a file using the specified compression algorithm type DecompressFileAction struct { - engine.BaseAction + task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder SourcePath string DestinationPath string CompressionType CompressionType // Parameter-aware fields - SourcePathParam engine.ActionParameter - DestinationPathParam engine.ActionParameter + SourcePathParam task_engine.ActionParameter + DestinationPathParam task_engine.ActionParameter } func (a *DecompressFileAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *engine.GlobalContext - if gc, ok := execCtx.Value(engine.GlobalContextKey).(*engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve parameters if they exist + // Resolve parameters using the ParameterResolver if a.SourcePathParam != nil { - sourceValue, err := a.SourcePathParam.Resolve(execCtx, globalContext) + sourceValue, err := a.ResolveStringParameter(execCtx, a.SourcePathParam, "source path") if err != nil { - return fmt.Errorf("failed to resolve source path parameter: %w", err) - } - if sourceStr, ok := sourceValue.(string); ok { - a.SourcePath = sourceStr - } else { - return fmt.Errorf("source path parameter is not a string, got %T", sourceValue) + return err } + a.SourcePath = sourceValue } if a.DestinationPathParam != nil { - destValue, err := a.DestinationPathParam.Resolve(execCtx, globalContext) + destValue, err := a.ResolveStringParameter(execCtx, a.DestinationPathParam, "destination path") if err != nil { - return fmt.Errorf("failed to resolve destination path parameter: %w", err) - } - if destStr, ok := destValue.(string); ok { - a.DestinationPath = destStr - } else { - return fmt.Errorf("destination path parameter is not a string, got %T", destValue) + return err } + a.DestinationPath = destValue } // Auto-detect compression type if not specified @@ -190,12 +183,11 @@ func (a *DecompressFileAction) decompressGzip(source io.Reader, destination io.W // GetOutput returns metadata about the decompression operation func (a *DecompressFileAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "source": a.SourcePath, "destination": a.DestinationPath, "compressionType": string(a.CompressionType), - "success": true, - } + }) } // DetectCompressionType auto-detects the compression type from file extension diff --git a/actions/file/delete_path_action.go b/actions/file/delete_path_action.go index 536a349..f8b95b4 100644 --- a/actions/file/delete_path_action.go +++ b/actions/file/delete_path_action.go @@ -8,10 +8,13 @@ import ( "path/filepath" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) type DeletePathAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder Path string Recursive bool DryRun bool @@ -33,43 +36,39 @@ type DeleteEntry struct { // NewDeletePathAction creates a new DeletePathAction with the given logger func NewDeletePathAction(logger *slog.Logger) *DeletePathAction { return &DeletePathAction{ - BaseAction: task_engine.NewBaseAction(logger), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), } } // WithParameters sets the parameters for path, recursive flag, dry run flag, include hidden flag, and exclude patterns -func (a *DeletePathAction) WithParameters(pathParam task_engine.ActionParameter, recursive, dryRun, includeHidden bool, excludePatterns []string) (*task_engine.Action[*DeletePathAction], error) { +func (a *DeletePathAction) WithParameters( + pathParam task_engine.ActionParameter, + recursive bool, + dryRun bool, + includeHidden bool, + excludePatterns []string, +) (*task_engine.Action[*DeletePathAction], error) { a.PathParam = pathParam a.Recursive = recursive a.DryRun = dryRun a.IncludeHidden = includeHidden a.ExcludePatterns = excludePatterns - return &task_engine.Action[*DeletePathAction]{ - ID: "delete-path-action", - Name: "Delete Path", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*DeletePathAction](a.Logger) + return constructor.WrapAction(a, "Delete Path", "delete-path-action"), nil } func (a *DeletePathAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve path parameter if it exists + // Resolve path parameter using the ParameterResolver if a.PathParam != nil { - pathValue, err := a.PathParam.Resolve(execCtx, globalContext) + pathValue, err := a.ResolveStringParameter(execCtx, a.PathParam, "path") if err != nil { - return fmt.Errorf("failed to resolve path parameter: %w", err) - } - if pathStr, ok := pathValue.(string); ok { - a.Path = pathStr - } else { - return fmt.Errorf("path parameter is not a string, got %T", pathValue) + return err } + a.Path = pathValue } if a.Path == "" { @@ -268,10 +267,9 @@ func (a *DeletePathAction) executeFileDelete(sanitizedPath string) error { // GetOutput returns metadata about the delete operation func (a *DeletePathAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "path": a.Path, "recursive": a.Recursive, "dryRun": a.DryRun, - "success": true, - } + }) } diff --git a/actions/file/extract_file_action.go b/actions/file/extract_file_action.go index e3ae9cb..ea90f8d 100644 --- a/actions/file/extract_file_action.go +++ b/actions/file/extract_file_action.go @@ -13,7 +13,8 @@ import ( "path/filepath" "strings" - engine "github.com/ndizazzo/task-engine" + task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) // ArchiveType represents the type of archive to extract @@ -31,12 +32,18 @@ const ( // NewExtractFileAction creates a new ExtractFileAction with the given logger func NewExtractFileAction(logger *slog.Logger) *ExtractFileAction { return &ExtractFileAction{ - BaseAction: engine.NewBaseAction(logger), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), } } // WithParameters sets the parameters for source and destination paths and archive type -func (a *ExtractFileAction) WithParameters(sourcePathParam, destinationPathParam engine.ActionParameter, archiveType ArchiveType) (*engine.Action[*ExtractFileAction], error) { +func (a *ExtractFileAction) WithParameters( + sourcePathParam task_engine.ActionParameter, + destinationPathParam task_engine.ActionParameter, + archiveType ArchiveType, +) (*task_engine.Action[*ExtractFileAction], error) { // Validate archive type if specified if archiveType != "" { switch archiveType { @@ -51,55 +58,41 @@ func (a *ExtractFileAction) WithParameters(sourcePathParam, destinationPathParam a.DestinationPathParam = destinationPathParam a.ArchiveType = archiveType - return &engine.Action[*ExtractFileAction]{ - ID: "extract-file-action", - Name: "Extract File", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*ExtractFileAction](a.Logger) + return constructor.WrapAction(a, "Extract File", "extract-file-action"), nil } // ExtractFileAction extracts an archive to the specified destination type ExtractFileAction struct { - engine.BaseAction + task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder SourcePath string DestinationPath string ArchiveType ArchiveType // Parameter-aware fields - SourcePathParam engine.ActionParameter - DestinationPathParam engine.ActionParameter + SourcePathParam task_engine.ActionParameter + DestinationPathParam task_engine.ActionParameter } func (a *ExtractFileAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *engine.GlobalContext - if gc, ok := execCtx.Value(engine.GlobalContextKey).(*engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve parameters if they exist + // Resolve parameters using the ParameterResolver if a.SourcePathParam != nil { - sourceValue, err := a.SourcePathParam.Resolve(execCtx, globalContext) + sourceValue, err := a.ResolveStringParameter(execCtx, a.SourcePathParam, "source path") if err != nil { - return fmt.Errorf("failed to resolve source path parameter: %w", err) - } - if sourceStr, ok := sourceValue.(string); ok { - a.SourcePath = sourceStr - } else { - return fmt.Errorf("source path parameter is not a string, got %T", sourceValue) + return err } + a.SourcePath = sourceValue } if a.DestinationPathParam != nil { - destValue, err := a.DestinationPathParam.Resolve(execCtx, globalContext) + destValue, err := a.ResolveStringParameter(execCtx, a.DestinationPathParam, "destination path") if err != nil { - return fmt.Errorf("failed to resolve destination path parameter: %w", err) - } - if destStr, ok := destValue.(string); ok { - a.DestinationPath = destStr - } else { - return fmt.Errorf("destination path parameter is not a string, got %T", destValue) + return err } + a.DestinationPath = destValue } if a.SourcePath == "" { @@ -336,12 +329,11 @@ func (a *ExtractFileAction) extractZip(source io.Reader, destination string) err // GetOutput returns metadata about the extraction operation func (a *ExtractFileAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "source": a.SourcePath, "destination": a.DestinationPath, "archiveType": string(a.ArchiveType), - "success": true, - } + }) } // detectCompression checks if a file is compressed and returns the compression type diff --git a/actions/file/move_file_action.go b/actions/file/move_file_action.go index 23bf61f..f789e84 100644 --- a/actions/file/move_file_action.go +++ b/actions/file/move_file_action.go @@ -9,6 +9,7 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) @@ -18,34 +19,43 @@ func NewMoveFileAction(logger *slog.Logger) *MoveFileAction { logger = slog.Default() } return &MoveFileAction{ - BaseAction: task_engine.NewBaseAction(logger), - commandRunner: command.NewDefaultCommandRunner(), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + commandRunner: command.NewDefaultCommandRunner(), } } // WithParameters sets the parameters for source, destination, and create directories flag -func (a *MoveFileAction) WithParameters(sourceParam, destinationParam task_engine.ActionParameter, createDirs bool) (*task_engine.Action[*MoveFileAction], error) { +func (a *MoveFileAction) WithParameters( + sourceParam task_engine.ActionParameter, + destinationParam task_engine.ActionParameter, + createDirs bool, +) (*task_engine.Action[*MoveFileAction], error) { a.SourceParam = sourceParam a.DestinationParam = destinationParam a.CreateDirs = createDirs - return &task_engine.Action[*MoveFileAction]{ - ID: "move-file-action", - Name: "Move File", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*MoveFileAction](a.Logger) + return constructor.WrapAction(a, "Move File", "move-file-action"), nil } +// MoveFileAction moves a file from source to destination type MoveFileAction struct { task_engine.BaseAction - Source string - Destination string - CreateDirs bool - commandRunner command.CommandRunner + common.ParameterResolver + common.OutputBuilder + Source string + Destination string + CreateDirs bool // Parameter-aware fields SourceParam task_engine.ActionParameter DestinationParam task_engine.ActionParameter + + // Execution dependency + commandRunner command.CommandRunner } func (a *MoveFileAction) SetCommandRunner(runner command.CommandRunner) { @@ -53,35 +63,21 @@ func (a *MoveFileAction) SetCommandRunner(runner command.CommandRunner) { } func (a *MoveFileAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve parameters if they exist + // Resolve parameters using the ParameterResolver if a.SourceParam != nil { - sourceValue, err := a.SourceParam.Resolve(execCtx, globalContext) + sourceValue, err := a.ResolveStringParameter(execCtx, a.SourceParam, "source") if err != nil { - return fmt.Errorf("failed to resolve source parameter: %w", err) - } - if sourceStr, ok := sourceValue.(string); ok { - a.Source = sourceStr - } else { - return fmt.Errorf("source parameter is not a string, got %T", sourceValue) + return err } + a.Source = sourceValue } if a.DestinationParam != nil { - destValue, err := a.DestinationParam.Resolve(execCtx, globalContext) + destValue, err := a.ResolveStringParameter(execCtx, a.DestinationParam, "destination") if err != nil { - return fmt.Errorf("failed to resolve destination parameter: %w", err) - } - if destStr, ok := destValue.(string); ok { - a.Destination = destStr - } else { - return fmt.Errorf("destination parameter is not a string, got %T", destValue) + return err } + a.Destination = destValue } // Basic validations before any external calls @@ -121,10 +117,9 @@ func (a *MoveFileAction) Execute(execCtx context.Context) error { // GetOutput returns metadata about the move operation func (a *MoveFileAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "source": a.Source, "destination": a.Destination, "createDirs": a.CreateDirs, - "success": true, - } + }) } diff --git a/actions/file/read_file_action.go b/actions/file/read_file_action.go index 853d456..0eabdb2 100644 --- a/actions/file/read_file_action.go +++ b/actions/file/read_file_action.go @@ -7,18 +7,24 @@ import ( "log/slog" "os" - engine "github.com/ndizazzo/task-engine" + task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) // NewReadFileAction creates a new ReadFileAction with the given logger func NewReadFileAction(logger *slog.Logger) *ReadFileAction { return &ReadFileAction{ - BaseAction: engine.NewBaseAction(logger), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), } } // WithParameters sets the parameters for file path and output buffer -func (a *ReadFileAction) WithParameters(pathParam engine.ActionParameter, outputBuffer *[]byte) (*engine.Action[*ReadFileAction], error) { +func (a *ReadFileAction) WithParameters( + pathParam task_engine.ActionParameter, + outputBuffer *[]byte, +) (*task_engine.Action[*ReadFileAction], error) { if outputBuffer == nil { return nil, fmt.Errorf("invalid parameter: outputBuffer cannot be nil") } @@ -26,46 +32,30 @@ func (a *ReadFileAction) WithParameters(pathParam engine.ActionParameter, output a.PathParam = pathParam a.OutputBuffer = outputBuffer - return &engine.Action[*ReadFileAction]{ - ID: "read-file-action", - Name: "Read File", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*ReadFileAction](a.Logger) + return constructor.WrapAction(a, "Read File", "read-file-action"), nil } // ReadFileAction reads content from a file and stores it in the provided buffer type ReadFileAction struct { - engine.BaseAction + task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder FilePath string - OutputBuffer *[]byte // Pointer to buffer where file contents will be stored - PathParam engine.ActionParameter // optional parameter to resolve path + OutputBuffer *[]byte // Pointer to buffer where file contents will be stored + PathParam task_engine.ActionParameter // optional parameter to resolve path } func (a *ReadFileAction) Execute(execCtx context.Context) error { - // Resolve path parameter if provided + // Resolve path parameter if provided using the ParameterResolver effectivePath := a.FilePath if a.PathParam != nil { - if gc, ok := execCtx.Value(engine.GlobalContextKey).(*engine.GlobalContext); ok { - v, err := a.PathParam.Resolve(execCtx, gc) - if err != nil { - return fmt.Errorf("failed to resolve path parameter: %w", err) - } - if s, ok := v.(string); ok { - effectivePath = s - } else { - return fmt.Errorf("resolved path parameter is not a string: %T", v) - } - } else { - if sp, ok := a.PathParam.(engine.StaticParameter); ok { - if s, ok2 := sp.Value.(string); ok2 { - effectivePath = s - } else { - return fmt.Errorf("static path parameter is not a string: %T", sp.Value) - } - } else { - return fmt.Errorf("global context not available for dynamic path resolution") - } + pathValue, err := a.ResolveStringParameter(execCtx, a.PathParam, "path") + if err != nil { + return err } + effectivePath = pathValue } // Sanitize path to prevent path traversal attacks @@ -109,18 +99,16 @@ func (a *ReadFileAction) Execute(execCtx context.Context) error { // GetOutput returns the file content and metadata func (a *ReadFileAction) GetOutput() interface{} { if a.OutputBuffer == nil { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, false, map[string]interface{}{ "content": nil, "fileSize": 0, "filePath": a.FilePath, - "success": false, - } + }) } - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "content": *a.OutputBuffer, "fileSize": len(*a.OutputBuffer), "filePath": a.FilePath, - "success": true, - } + }) } diff --git a/actions/file/replace_lines_action.go b/actions/file/replace_lines_action.go index 0c6eb23..e5de6c8 100644 --- a/actions/file/replace_lines_action.go +++ b/actions/file/replace_lines_action.go @@ -9,10 +9,13 @@ import ( "regexp" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) type ReplaceLinesAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder FilePath string ReplacePatterns map[*regexp.Regexp]string @@ -25,7 +28,9 @@ type ReplaceLinesAction struct { // NewReplaceLinesAction creates a new ReplaceLinesAction with the given logger func NewReplaceLinesAction(logger *slog.Logger) *ReplaceLinesAction { return &ReplaceLinesAction{ - BaseAction: task_engine.NewBaseAction(logger), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), } } @@ -42,21 +47,13 @@ func (a *ReplaceLinesAction) WithParameters(filePathParam task_engine.ActionPara } func (a *ReplaceLinesAction) Execute(ctx context.Context) error { - // Resolve file path parameter if provided + // Resolve file path parameter if provided using the ParameterResolver if a.FilePathParam != nil { - gc, ok := ctx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext) - if !ok || gc == nil { - return fmt.Errorf("global context not available for path parameter resolution") - } - val, err := a.FilePathParam.Resolve(ctx, gc) + pathValue, err := a.ResolveStringParameter(ctx, a.FilePathParam, "file path") if err != nil { - return fmt.Errorf("failed to resolve file path parameter: %w", err) - } - if pathStr, ok := val.(string); ok { - a.FilePath = pathStr - } else { - return fmt.Errorf("file path parameter is not a string, got %T", val) + return err } + a.FilePath = pathValue } if a.FilePath == "" { @@ -67,18 +64,14 @@ func (a *ReplaceLinesAction) Execute(ctx context.Context) error { var resolvedReplacements map[*regexp.Regexp]string if len(a.ReplaceParamPatterns) > 0 { resolvedReplacements = make(map[*regexp.Regexp]string, len(a.ReplaceParamPatterns)) - gc, ok := ctx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext) - if !ok || gc == nil { - return fmt.Errorf("global context not available for parameter resolution") - } for pattern, param := range a.ReplaceParamPatterns { if param == nil { resolvedReplacements[pattern] = "" continue } - val, err := param.Resolve(ctx, gc) + val, err := a.ResolveParameter(ctx, param, "replacement") if err != nil { - return fmt.Errorf("failed to resolve replacement parameter: %w", err) + return err } var replacement string switch v := val.(type) { @@ -162,9 +155,8 @@ func (a *ReplaceLinesAction) Execute(ctx context.Context) error { } func (a *ReplaceLinesAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "filePath": a.FilePath, "patterns": len(a.ReplacePatterns), - "success": true, - } + }) } diff --git a/actions/file/replace_lines_action_test.go b/actions/file/replace_lines_action_test.go index 0f7926b..452f16e 100644 --- a/actions/file/replace_lines_action_test.go +++ b/actions/file/replace_lines_action_test.go @@ -255,7 +255,7 @@ func (suite *ReplaceLinesTestSuite) TestExecuteFilePathInvalidType() { err := action.Execute(ctx) suite.Error(err) - suite.Contains(err.Error(), "file path parameter is not a string") + suite.Contains(err.Error(), "file path parameter resolved to non-string value") } func (suite *ReplaceLinesTestSuite) TestExecuteEmptyFilePath() { @@ -280,7 +280,7 @@ func (suite *ReplaceLinesTestSuite) TestExecuteNoGlobalContextForParameters() { err := action.Execute(ctx) suite.Error(err) - suite.Contains(err.Error(), "global context not available for path parameter resolution") + suite.Contains(err.Error(), "open /some/path: no such file or directory") } func (suite *ReplaceLinesTestSuite) TestExecuteReplacementParameterResolutionFailure() { diff --git a/actions/file/write_file_action.go b/actions/file/write_file_action.go index 1fb383a..ab72821 100644 --- a/actions/file/write_file_action.go +++ b/actions/file/write_file_action.go @@ -10,12 +10,15 @@ import ( "path/filepath" engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) // NewWriteFileAction creates a new WriteFileAction with the given logger func NewWriteFileAction(logger *slog.Logger) *WriteFileAction { return &WriteFileAction{ - BaseAction: engine.NewBaseAction(logger), + BaseAction: engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), } } @@ -43,6 +46,8 @@ func (a *WriteFileAction) WithParameters(pathParam, content engine.ActionParamet // If InputBuffer is set, its content will be used. type WriteFileAction struct { engine.BaseAction + common.ParameterResolver + common.OutputBuilder FilePath string Content engine.ActionParameter // Now supports ActionParameter Overwrite bool @@ -54,32 +59,15 @@ type WriteFileAction struct { } func (a *WriteFileAction) Execute(execCtx context.Context) error { - // Resolve path parameter if provided + // Resolve path parameter if provided using the ParameterResolver effectivePath := a.FilePath if a.PathParam != nil { - if gc, ok := execCtx.Value(engine.GlobalContextKey).(*engine.GlobalContext); ok { - v, err := a.PathParam.Resolve(execCtx, gc) - if err != nil { - a.writeError = fmt.Errorf("failed to resolve path parameter: %w", err) - return a.writeError - } - if s, ok := v.(string); ok { - effectivePath = s - } else { - a.writeError = fmt.Errorf("resolved path parameter is not a string: %T", v) - return a.writeError - } - } else if sp, ok := a.PathParam.(engine.StaticParameter); ok { - if s, ok2 := sp.Value.(string); ok2 { - effectivePath = s - } else { - a.writeError = fmt.Errorf("static path parameter is not a string: %T", sp.Value) - return a.writeError - } - } else { - a.writeError = fmt.Errorf("global context not available for dynamic path resolution") + pathValue, err := a.ResolveStringParameter(execCtx, a.PathParam, "path") + if err != nil { + a.writeError = err return a.writeError } + effectivePath = pathValue } // Sanitize path to prevent path traversal attacks @@ -91,44 +79,27 @@ func (a *WriteFileAction) Execute(execCtx context.Context) error { var contentToWrite []byte - // Resolve content parameter if provided + // Resolve content parameter if provided using the ParameterResolver if a.Content != nil { - // For now, we'll need a global context to resolve parameters - // This will be enhanced in future iterations - if globalCtx, ok := execCtx.Value(engine.GlobalContextKey).(*engine.GlobalContext); ok { - resolvedContent, err := a.Content.Resolve(execCtx, globalCtx) - if err != nil { - a.writeError = fmt.Errorf("failed to resolve content parameter: %w", err) - return a.writeError - } + resolvedContent, err := a.ResolveParameter(execCtx, a.Content, "content") + if err != nil { + a.writeError = err + return a.writeError + } - // Convert resolved content to bytes - switch v := resolvedContent.(type) { - case []byte: - contentToWrite = v - case string: - contentToWrite = []byte(v) - case *[]byte: - if v != nil { - contentToWrite = *v - } - default: - a.writeError = fmt.Errorf("unsupported content type: %T", resolvedContent) - return a.writeError - } - } else { - // Fallback to static content if no global context - if staticParam, ok := a.Content.(engine.StaticParameter); ok { - switch v := staticParam.Value.(type) { - case []byte: - contentToWrite = v - case string: - contentToWrite = []byte(v) - default: - a.writeError = fmt.Errorf("unsupported static content type: %T", v) - return a.writeError - } + // Convert resolved content to bytes + switch v := resolvedContent.(type) { + case []byte: + contentToWrite = v + case string: + contentToWrite = []byte(v) + case *[]byte: + if v != nil { + contentToWrite = *v } + default: + a.writeError = fmt.Errorf("unsupported content type: %T", resolvedContent) + return a.writeError } } @@ -180,11 +151,10 @@ func (a *WriteFileAction) Execute(execCtx context.Context) error { // GetOutput returns information about the write operation func (a *WriteFileAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, a.writeError == nil, map[string]interface{}{ "filePath": a.FilePath, "contentLength": len(a.writtenContent), "overwrite": a.Overwrite, - "success": a.writeError == nil, "error": a.writeError, - } + }) } diff --git a/actions/system/manage_service_action.go b/actions/system/manage_service_action.go index b62d8e3..d45c157 100644 --- a/actions/system/manage_service_action.go +++ b/actions/system/manage_service_action.go @@ -6,23 +6,28 @@ import ( "log/slog" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) // NewManageServiceAction creates a new ManageServiceAction with the given logger func NewManageServiceAction(logger *slog.Logger) *ManageServiceAction { return &ManageServiceAction{ - BaseAction: task_engine.NewBaseAction(logger), - CommandProcessor: command.NewDefaultCommandRunner(), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + CommandProcessor: command.NewDefaultCommandRunner(), } } type ManageServiceAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder // Parameters ServiceNameParam task_engine.ActionParameter - ActionTypeParam task_engine.ActionParameter + OperationParam task_engine.ActionParameter // Runtime resolved values ServiceName string @@ -30,61 +35,41 @@ type ManageServiceAction struct { CommandProcessor command.CommandRunner } -// WithParameters sets the parameters for service name and action type and returns a wrapped Action -func (a *ManageServiceAction) WithParameters(serviceNameParam, actionTypeParam task_engine.ActionParameter) (*task_engine.Action[*ManageServiceAction], error) { +// WithParameters sets the parameters for service management and returns a wrapped Action +func (a *ManageServiceAction) WithParameters( + serviceNameParam task_engine.ActionParameter, + operationParam task_engine.ActionParameter, +) (*task_engine.Action[*ManageServiceAction], error) { a.ServiceNameParam = serviceNameParam - a.ActionTypeParam = actionTypeParam + a.OperationParam = operationParam - id := "manage-service-action" - return &task_engine.Action[*ManageServiceAction]{ - ID: id, - Name: "Manage Service", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*ManageServiceAction](a.Logger) + return constructor.WrapAction(a, "Manage Service", "manage-service-action"), nil } func (a *ManageServiceAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve service name parameter if it exists - if a.ServiceNameParam != nil { - serviceNameValue, err := a.ServiceNameParam.Resolve(execCtx, globalContext) - if err != nil { - return fmt.Errorf("failed to resolve service name parameter: %w", err) - } - if serviceNameStr, ok := serviceNameValue.(string); ok { - a.ServiceName = serviceNameStr - } else { - return fmt.Errorf("service name parameter is not a string, got %T", serviceNameValue) - } + // Resolve required parameters + serviceName, err := a.ResolveStringParameter(execCtx, a.ServiceNameParam, "service name") + if err != nil { + return err } + a.ServiceName = serviceName - // Resolve action type parameter if it exists - if a.ActionTypeParam != nil { - actionTypeValue, err := a.ActionTypeParam.Resolve(execCtx, globalContext) - if err != nil { - return fmt.Errorf("failed to resolve action type parameter: %w", err) - } - if actionTypeStr, ok := actionTypeValue.(string); ok { - a.ActionType = actionTypeStr - } else { - return fmt.Errorf("action type parameter is not a string, got %T", actionTypeValue) - } + actionType, err := a.ResolveStringParameter(execCtx, a.OperationParam, "action type") + if err != nil { + return err } + a.ActionType = actionType switch a.ActionType { case "start", "stop", "restart": - // Dont allow anything except these commands to be passed to systemctl + // valid default: - err := fmt.Errorf("invalid action type: %s; must be 'start', 'stop', or 'restart'", a.ActionType) - return err + return fmt.Errorf("invalid action type: %s; must be 'start', 'stop', or 'restart'", a.ActionType) } - _, err := a.CommandProcessor.RunCommand("systemctl", a.ActionType, a.ServiceName) + _, err = a.CommandProcessor.RunCommand("systemctl", a.ActionType, a.ServiceName) if err != nil { return fmt.Errorf("failed to %s service %s: %w", a.ActionType, a.ServiceName, err) } @@ -94,9 +79,8 @@ func (a *ManageServiceAction) Execute(execCtx context.Context) error { // GetOutput returns the service operation performed func (a *ManageServiceAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "service": a.ServiceName, "action": a.ActionType, - "success": true, - } + }) } diff --git a/actions/system/service_status_action.go b/actions/system/service_status_action.go index 6310b4c..b5fd459 100644 --- a/actions/system/service_status_action.go +++ b/actions/system/service_status_action.go @@ -7,6 +7,7 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) @@ -25,8 +26,10 @@ type ServiceStatus struct { // NewServiceStatusAction creates a new ServiceStatusAction with the given logger func NewServiceStatusAction(logger *slog.Logger) *ServiceStatusAction { return &ServiceStatusAction{ - BaseAction: task_engine.NewBaseAction(logger), - CommandProcessor: command.NewDefaultCommandRunner(), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + CommandProcessor: command.NewDefaultCommandRunner(), } } @@ -37,9 +40,11 @@ func NewGetAllServicesStatusAction(logger *slog.Logger) *task_engine.Action[*Ser ID: "get-all-services-status-action", Name: "Get All Services Status", Wrapped: &ServiceStatusAction{ - BaseAction: task_engine.NewBaseAction(logger), - ServiceNames: []string{}, // Empty means get all - will cause error - CommandProcessor: command.NewDefaultCommandRunner(), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + ServiceNames: []string{}, // Empty means get all - will cause error + CommandProcessor: command.NewDefaultCommandRunner(), }, } } @@ -47,9 +52,11 @@ func NewGetAllServicesStatusAction(logger *slog.Logger) *task_engine.Action[*Ser // ServiceStatusAction retrieves the status of systemd services type ServiceStatusAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder // Parameters - ServiceNamesParam task_engine.ActionParameter + ServiceNameParam task_engine.ActionParameter // Runtime resolved values ServiceNames []string @@ -58,16 +65,15 @@ type ServiceStatusAction struct { CommandProcessor command.CommandRunner } -// WithParameters sets the service names parameter and returns a wrapped Action -func (a *ServiceStatusAction) WithParameters(serviceNamesParam task_engine.ActionParameter) (*task_engine.Action[*ServiceStatusAction], error) { - a.ServiceNamesParam = serviceNamesParam +// WithParameters sets the parameters for service status and returns a wrapped Action +func (a *ServiceStatusAction) WithParameters( + serviceNameParam task_engine.ActionParameter, +) (*task_engine.Action[*ServiceStatusAction], error) { + a.ServiceNameParam = serviceNameParam - id := "service-status-action" - return &task_engine.Action[*ServiceStatusAction]{ - ID: id, - Name: "Service Status", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*ServiceStatusAction](a.Logger) + return constructor.WrapAction(a, "Service Status", "service-status-action"), nil } // SetCommandProcessor allows injecting a mock or alternative CommandProcessor for testing @@ -76,22 +82,19 @@ func (a *ServiceStatusAction) SetCommandProcessor(processor command.CommandRunne } func (a *ServiceStatusAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve service names parameter if it exists - if a.ServiceNamesParam != nil { - serviceNamesValue, err := a.ServiceNamesParam.Resolve(execCtx, globalContext) + // Resolve service names parameter using the ParameterResolver + if a.ServiceNameParam != nil { + serviceNameValue, err := a.ResolveParameter(execCtx, a.ServiceNameParam, "service name") if err != nil { - return fmt.Errorf("failed to resolve service names parameter: %w", err) + return err } - if serviceNamesSlice, ok := serviceNamesValue.([]string); ok { + + if serviceNamesSlice, ok := serviceNameValue.([]string); ok { a.ServiceNames = serviceNamesSlice + } else if serviceName, ok := serviceNameValue.(string); ok { + a.ServiceNames = []string{serviceName} } else { - return fmt.Errorf("service names parameter is not a []string, got %T", serviceNamesValue) + return fmt.Errorf("service name parameter is not a []string or string, got %T", serviceNameValue) } } @@ -124,11 +127,9 @@ func (a *ServiceStatusAction) Execute(execCtx context.Context) error { // GetOutput returns the retrieved service statuses func (a *ServiceStatusAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildOutputWithCount(a.ServiceStatuses, true, map[string]interface{}{ "services": a.ServiceStatuses, - "count": len(a.ServiceStatuses), - "success": true, - } + }) } // getServiceStatus gets the status of a single service using systemctl show diff --git a/actions/system/shutdown_action.go b/actions/system/shutdown_action.go index f489251..28be605 100644 --- a/actions/system/shutdown_action.go +++ b/actions/system/shutdown_action.go @@ -7,11 +7,14 @@ import ( "time" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) type ShutdownAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder CommandProcessor command.CommandRunner // Parameter-only fields @@ -27,28 +30,27 @@ const ( ShutdownOperation_Sleep ShutdownCommandOperation = "sleep" ) -// NewShutdownAction creates a ShutdownAction instance +// NewShutdownAction creates a new ShutdownAction with the given logger func NewShutdownAction(logger *slog.Logger) *ShutdownAction { return &ShutdownAction{ - BaseAction: task_engine.BaseAction{Logger: logger}, - CommandProcessor: command.NewDefaultCommandRunner(), + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + CommandProcessor: command.NewDefaultCommandRunner(), } } -// WithParameters sets the parameters and returns a wrapped Action -func (a *ShutdownAction) WithParameters(operationParam, delayParam task_engine.ActionParameter) (*task_engine.Action[*ShutdownAction], error) { - if operationParam == nil || delayParam == nil { - return nil, fmt.Errorf("operationParam and delayParam cannot be nil") - } - +// WithParameters sets the parameters for shutdown and returns a wrapped Action +func (a *ShutdownAction) WithParameters( + operationParam task_engine.ActionParameter, + delayParam task_engine.ActionParameter, +) (*task_engine.Action[*ShutdownAction], error) { a.OperationParam = operationParam a.DelayParam = delayParam - return &task_engine.Action[*ShutdownAction]{ - ID: "shutdown-action", - Name: "Shutdown", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*ShutdownAction](a.Logger) + return constructor.WrapAction(a, "Shutdown", "shutdown-action"), nil } // SetCommandRunner allows injecting a mock or alternative CommandRunner for testing @@ -57,49 +59,24 @@ func (a *ShutdownAction) SetCommandRunner(runner command.CommandRunner) { } func (a *ShutdownAction) Execute(ctx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := ctx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve operation parameter + // Resolve operation parameter using the ParameterResolver var operation ShutdownCommandOperation if a.OperationParam != nil { - operationValue, err := a.OperationParam.Resolve(ctx, globalContext) + operationValue, err := a.ResolveStringParameter(ctx, a.OperationParam, "operation") if err != nil { - return fmt.Errorf("failed to resolve operation parameter: %w", err) - } - if operationStr, ok := operationValue.(string); ok { - operation = ShutdownCommandOperation(operationStr) - } else { - return fmt.Errorf("operation parameter is not a string, got %T", operationValue) + return err } + operation = ShutdownCommandOperation(operationValue) } - // Resolve delay parameter + // Resolve delay parameter using the ParameterResolver var delay time.Duration if a.DelayParam != nil { - delayValue, err := a.DelayParam.Resolve(ctx, globalContext) + delayValue, err := a.ResolveDurationParameter(ctx, a.DelayParam, "delay") if err != nil { - return fmt.Errorf("failed to resolve delay parameter: %w", err) - } - switch v := delayValue.(type) { - case time.Duration: - delay = v - case int: - delay = time.Duration(v) * time.Second - case int64: - delay = time.Duration(v) * time.Second - case string: - parsed, parseErr := time.ParseDuration(v) - if parseErr != nil { - return fmt.Errorf("delay parameter could not be parsed as duration: %w", parseErr) - } - delay = parsed - default: - return fmt.Errorf("delay parameter is not a valid type, got %T", delayValue) + return err } + delay = delayValue } additionalFlags := shutdownArgs(operation, delay) @@ -109,9 +86,7 @@ func (a *ShutdownAction) Execute(ctx context.Context) error { // GetOutput returns the requested shutdown operation and delay func (a *ShutdownAction) GetOutput() interface{} { - return map[string]interface{}{ - "success": true, - } + return a.BuildSimpleOutput(true, "") } func shutdownArgs(operation ShutdownCommandOperation, duration time.Duration) []string { diff --git a/actions/system/update_packages_action.go b/actions/system/update_packages_action.go index 4adc2af..65db3d2 100644 --- a/actions/system/update_packages_action.go +++ b/actions/system/update_packages_action.go @@ -11,6 +11,7 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" "github.com/ndizazzo/task-engine/command" ) @@ -26,7 +27,7 @@ const ( // UpdatePackagesActionConstructor provides the modern constructor pattern type UpdatePackagesActionConstructor struct { - logger *slog.Logger + common.BaseConstructor[*UpdatePackagesAction] } // NewUpdatePackagesAction creates a new UpdatePackagesAction constructor @@ -35,7 +36,7 @@ func NewUpdatePackagesAction(logger *slog.Logger) *UpdatePackagesActionConstruct logger = slog.New(slog.NewTextHandler(os.Stdout, nil)) } return &UpdatePackagesActionConstructor{ - logger: logger, + BaseConstructor: *common.NewBaseConstructor[*UpdatePackagesAction](logger), } } @@ -48,7 +49,9 @@ func (c *UpdatePackagesActionConstructor) WithParameters( defaultPackageManager := detectPackageManager() action := &UpdatePackagesAction{ - BaseAction: task_engine.NewBaseAction(c.logger), + BaseAction: task_engine.NewBaseAction(c.GetLogger()), + ParameterResolver: *common.NewParameterResolver(c.GetLogger()), + OutputBuilder: *common.NewOutputBuilder(c.GetLogger()), PackageNames: []string{}, PackageManager: defaultPackageManager, // May be overridden at runtime CommandRunner: command.NewDefaultCommandRunner(), @@ -56,16 +59,14 @@ func (c *UpdatePackagesActionConstructor) WithParameters( PackageManagerParam: packageManagerParam, } - return &task_engine.Action[*UpdatePackagesAction]{ - ID: "update-packages-action", - Name: "Update Packages", - Wrapped: action, - }, nil + return c.WrapAction(action, "Update Packages", "update-packages-action"), nil } // UpdatePackagesAction updates packages using the appropriate package manager type UpdatePackagesAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder PackageNames []string PackageManager PackageManager CommandRunner command.CommandRunner @@ -81,18 +82,13 @@ func (a *UpdatePackagesAction) SetCommandRunner(runner command.CommandRunner) { } func (a *UpdatePackagesAction) Execute(execCtx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := execCtx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve package names parameter if it exists + // Resolve package names parameter using the ParameterResolver if a.PackageNamesParam != nil { - packageNamesValue, err := a.PackageNamesParam.Resolve(execCtx, globalContext) + packageNamesValue, err := a.ResolveParameter(execCtx, a.PackageNamesParam, "package names") if err != nil { - return fmt.Errorf("failed to resolve package names parameter: %w", err) + return err } + if packageNamesSlice, ok := packageNamesValue.([]string); ok { a.PackageNames = packageNamesSlice } else if packageNamesStr, ok := packageNamesValue.(string); ok { @@ -107,17 +103,13 @@ func (a *UpdatePackagesAction) Execute(execCtx context.Context) error { } } - // Resolve package manager parameter if it exists + // Resolve package manager parameter using the ParameterResolver if a.PackageManagerParam != nil { - packageManagerValue, err := a.PackageManagerParam.Resolve(execCtx, globalContext) + packageManagerValue, err := a.ResolveStringParameter(execCtx, a.PackageManagerParam, "package manager") if err != nil { - return fmt.Errorf("failed to resolve package manager parameter: %w", err) - } - if packageManagerStr, ok := packageManagerValue.(string); ok { - a.PackageManager = PackageManager(packageManagerStr) - } else { - return fmt.Errorf("package manager parameter is not a string, got %T", packageManagerValue) + return err } + a.PackageManager = PackageManager(packageManagerValue) } a.Logger.Info("Attempting to update packages", @@ -195,11 +187,10 @@ func (a *UpdatePackagesAction) installWithBrew(execCtx context.Context) error { // GetOutput returns information about attempted package installation func (a *UpdatePackagesAction) GetOutput() interface{} { - return map[string]interface{}{ + return a.BuildStandardOutput(nil, true, map[string]interface{}{ "packages": a.PackageNames, "packageManager": string(a.PackageManager), - "success": true, - } + }) } // detectPackageManager detects the appropriate package manager based on the operating system diff --git a/actions/system/update_packages_action_test.go b/actions/system/update_packages_action_test.go index bc623d3..950befc 100644 --- a/actions/system/update_packages_action_test.go +++ b/actions/system/update_packages_action_test.go @@ -294,7 +294,7 @@ func (suite *UpdatePackagesActionTestSuite) TestNewUpdatePackagesActionConstruct err = action.Wrapped.Execute(context.Background()) suite.Error(err) - suite.Contains(err.Error(), "package manager parameter is not a string") + suite.Contains(err.Error(), "package manager parameter resolved to non-string value") } func (suite *UpdatePackagesActionTestSuite) TestNewUpdatePackagesActionConstructor_Execute_NilParameters() { diff --git a/actions/utility/fetch_interfaces_action.go b/actions/utility/fetch_interfaces_action.go index 79e3e4b..0db8d39 100644 --- a/actions/utility/fetch_interfaces_action.go +++ b/actions/utility/fetch_interfaces_action.go @@ -9,6 +9,7 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) // FetchNetInterfacesAction represents an action that fetches network interfaces @@ -22,28 +23,24 @@ type FetchNetInterfacesAction struct { Interfaces []string // Discovered or provided interfaces } -// NewFetchNetInterfacesAction creates a new FetchNetInterfacesAction with the provided logger +// NewFetchNetInterfacesAction creates a new FetchNetInterfacesAction with the given logger func NewFetchNetInterfacesAction(logger *slog.Logger) *FetchNetInterfacesAction { return &FetchNetInterfacesAction{ - BaseAction: task_engine.BaseAction{Logger: logger}, + BaseAction: task_engine.NewBaseAction(logger), } } -// WithParameters sets the parameters for device path and optional interfaces -func (a *FetchNetInterfacesAction) WithParameters(netDevicePathParam task_engine.ActionParameter, interfacesParam task_engine.ActionParameter) (*task_engine.Action[*FetchNetInterfacesAction], error) { - if netDevicePathParam == nil { - return nil, fmt.Errorf("net device path parameter cannot be nil") - } - // interfacesParam can be nil - it's optional - +// WithParameters sets the parameters for interface fetching and returns a wrapped Action +func (a *FetchNetInterfacesAction) WithParameters( + netDevicePathParam task_engine.ActionParameter, + interfacesParam task_engine.ActionParameter, +) (*task_engine.Action[*FetchNetInterfacesAction], error) { a.NetDevicePathParam = netDevicePathParam a.InterfacesParam = interfacesParam - return &task_engine.Action[*FetchNetInterfacesAction]{ - ID: "fetch-interfaces-action", - Name: "Fetch Network Interfaces", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*FetchNetInterfacesAction](a.Logger) + return constructor.WrapAction(a, "Fetch Network Interfaces", "fetch-interfaces-action"), nil } // gathers and sorts the network interfaces from the specified device path diff --git a/actions/utility/prerequisite_check_action.go b/actions/utility/prerequisite_check_action.go index f4394bc..3b81c7b 100644 --- a/actions/utility/prerequisite_check_action.go +++ b/actions/utility/prerequisite_check_action.go @@ -6,6 +6,7 @@ import ( "log/slog" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) // PrerequisiteCheckFunc defines the signature for a callback function that checks a prerequisite. @@ -21,35 +22,30 @@ type PrerequisiteCheckAction struct { // Parameter fields DescriptionParam task_engine.ActionParameter CheckParam task_engine.ActionParameter + CommandsParam task_engine.ActionParameter // Result fields (resolved from parameters during execution) Check PrerequisiteCheckFunc Description string // A human-readable description of what is being checked } -// NewPrerequisiteCheckAction creates a new PrerequisiteCheckAction with the provided logger +// NewPrerequisiteCheckAction creates a new PrerequisiteCheckAction with the given logger func NewPrerequisiteCheckAction(logger *slog.Logger) *PrerequisiteCheckAction { return &PrerequisiteCheckAction{ - BaseAction: task_engine.BaseAction{Logger: logger}, + BaseAction: task_engine.NewBaseAction(logger), } } -// WithParameters sets the description and check function parameters -func (a *PrerequisiteCheckAction) WithParameters(descriptionParam task_engine.ActionParameter, checkParam task_engine.ActionParameter) (*task_engine.Action[*PrerequisiteCheckAction], error) { - if descriptionParam == nil { - return nil, fmt.Errorf("description parameter cannot be nil") - } - if checkParam == nil { - return nil, fmt.Errorf("check parameter cannot be nil") - } - +// WithParameters sets the parameters for prerequisite checking and returns a wrapped Action +func (a *PrerequisiteCheckAction) WithParameters( + descriptionParam task_engine.ActionParameter, + checkParam task_engine.ActionParameter, +) (*task_engine.Action[*PrerequisiteCheckAction], error) { a.DescriptionParam = descriptionParam a.CheckParam = checkParam - return &task_engine.Action[*PrerequisiteCheckAction]{ - ID: "prerequisite-check-action", - Name: "Prerequisite Check", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*PrerequisiteCheckAction](a.Logger) + return constructor.WrapAction(a, "Prerequisite Check", "prerequisite-check-action"), nil } // Execute runs the prerequisite check callback. diff --git a/actions/utility/read_mac_action.go b/actions/utility/read_mac_action.go index 1a7b0c6..2bd34da 100644 --- a/actions/utility/read_mac_action.go +++ b/actions/utility/read_mac_action.go @@ -8,56 +8,53 @@ import ( "strings" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) // ReadMACAddressAction represents an action that reads the MAC address of a network interface type ReadMACAddressAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder // Parameter fields InterfaceNameParam task_engine.ActionParameter // Execution result fields (not parameters) Interface string // Resolved interface name MAC string // Read MAC address + // Direct logger field for backward compatibility + Logger *slog.Logger } -// NewReadMACAddressAction creates a new ReadMACAddressAction with the provided logger +// NewReadMACAddressAction creates a new ReadMACAddressAction with the given logger func NewReadMACAddressAction(logger *slog.Logger) *ReadMACAddressAction { return &ReadMACAddressAction{ - BaseAction: task_engine.BaseAction{Logger: logger}, + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), + Logger: logger, } } -// WithParameters sets the interface name parameter and returns the wrapped action -func (a *ReadMACAddressAction) WithParameters(interfaceNameParam task_engine.ActionParameter) (*task_engine.Action[*ReadMACAddressAction], error) { - if interfaceNameParam == nil { +// WithParameters sets the parameters for MAC address reading and returns a wrapped Action +func (a *ReadMACAddressAction) WithParameters( + interfaceParam task_engine.ActionParameter, +) (*task_engine.Action[*ReadMACAddressAction], error) { + if interfaceParam == nil { return nil, fmt.Errorf("interface name parameter cannot be nil") } - a.InterfaceNameParam = interfaceNameParam + a.InterfaceNameParam = interfaceParam - return &task_engine.Action[*ReadMACAddressAction]{ - ID: "read-mac-action", - Name: "Read MAC Address", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*ReadMACAddressAction](a.Logger) + return constructor.WrapAction(a, "Read MAC Address", "read-mac-action"), nil } func (a *ReadMACAddressAction) Execute(ctx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := ctx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve interface name parameter - interfaceNameValue, err := a.InterfaceNameParam.Resolve(ctx, globalContext) + // Use the new parameter resolver to handle interface name parameter + interfaceName, err := a.ResolveStringParameter(ctx, a.InterfaceNameParam, "interface name") if err != nil { - return fmt.Errorf("failed to resolve interface name parameter: %w", err) - } - - interfaceName, ok := interfaceNameValue.(string) - if !ok { - return fmt.Errorf("interface name parameter is not a string, got %T", interfaceNameValue) + return err } if interfaceName == "" { @@ -83,9 +80,9 @@ func (a *ReadMACAddressAction) Execute(ctx context.Context) error { } func (a *ReadMACAddressAction) GetOutput() interface{} { - return map[string]interface{}{ + // Use the new output builder to create the output + return a.BuildStandardOutput(nil, a.MAC != "", map[string]interface{}{ "interface": a.Interface, "mac": a.MAC, - "success": a.MAC != "", - } + }) } diff --git a/actions/utility/read_mac_action_test.go b/actions/utility/read_mac_action_test.go index 8654329..f228047 100644 --- a/actions/utility/read_mac_action_test.go +++ b/actions/utility/read_mac_action_test.go @@ -101,7 +101,7 @@ func (suite *ReadMacActionTestSuite) TestExecuteInvalidParameterType() { err := action.Execute(context.Background()) suite.Error(err) - suite.Contains(err.Error(), "interface name parameter is not a string") + suite.Contains(err.Error(), "interface name parameter resolved to non-string value") } func (suite *ReadMacActionTestSuite) TestExecuteParameterResolutionFailure() { diff --git a/actions/utility/wait_action.go b/actions/utility/wait_action.go index 379b4e0..f3c2c13 100644 --- a/actions/utility/wait_action.go +++ b/actions/utility/wait_action.go @@ -7,66 +7,43 @@ import ( "time" task_engine "github.com/ndizazzo/task-engine" + "github.com/ndizazzo/task-engine/actions/common" ) // WaitAction represents an action that waits for a specified duration type WaitAction struct { task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder // Parameter fields DurationParam task_engine.ActionParameter } -// NewWaitAction creates a new WaitAction with the provided logger +// NewWaitAction creates a new WaitAction with the given logger func NewWaitAction(logger *slog.Logger) *WaitAction { return &WaitAction{ - BaseAction: task_engine.BaseAction{Logger: logger}, + BaseAction: task_engine.NewBaseAction(logger), + ParameterResolver: *common.NewParameterResolver(logger), + OutputBuilder: *common.NewOutputBuilder(logger), } } -// WithParameters sets the duration parameter and returns the wrapped action -func (a *WaitAction) WithParameters(durationParam task_engine.ActionParameter) (*task_engine.Action[*WaitAction], error) { - if durationParam == nil { - return nil, fmt.Errorf("duration parameter cannot be nil") - } - +// WithParameters sets the parameters for wait action and returns a wrapped Action +func (a *WaitAction) WithParameters( + durationParam task_engine.ActionParameter, +) (*task_engine.Action[*WaitAction], error) { a.DurationParam = durationParam - return &task_engine.Action[*WaitAction]{ - ID: "wait-action", - Name: "Wait", - Wrapped: a, - }, nil + // Create a temporary constructor to use the base functionality + constructor := common.NewBaseConstructor[*WaitAction](a.Logger) + return constructor.WrapAction(a, "Wait", "wait-action"), nil } func (a *WaitAction) Execute(ctx context.Context) error { - // Extract GlobalContext from context - var globalContext *task_engine.GlobalContext - if gc, ok := ctx.Value(task_engine.GlobalContextKey).(*task_engine.GlobalContext); ok { - globalContext = gc - } - - // Resolve duration parameter - durationValue, err := a.DurationParam.Resolve(ctx, globalContext) + // Use the new parameter resolver to handle duration parameter + duration, err := a.ResolveDurationParameter(ctx, a.DurationParam, "duration") if err != nil { - return fmt.Errorf("failed to resolve duration parameter: %w", err) - } - - var duration time.Duration - if durationStr, ok := durationValue.(string); ok { - // Parse duration string (e.g., "5s", "1m", "2h") - parsedDuration, err := time.ParseDuration(durationStr) - if err != nil { - return fmt.Errorf("failed to parse duration string '%s': %w", durationStr, err) - } - duration = parsedDuration - } else if durationInt, ok := durationValue.(int); ok { - // Treat as seconds - duration = time.Duration(durationInt) * time.Second - } else if durationDirect, ok := durationValue.(time.Duration); ok { - // Direct time.Duration value - duration = durationDirect - } else { - return fmt.Errorf("duration parameter is not a string, int, or time.Duration, got %T", durationValue) + return err } if duration <= 0 { @@ -81,9 +58,7 @@ func (a *WaitAction) Execute(ctx context.Context) error { } } -// GetOutput returns the waited duration +// GetOutput returns the waited duration using the new output builder func (a *WaitAction) GetOutput() interface{} { - return map[string]interface{}{ - "success": true, - } + return a.BuildSimpleOutput(true, "") } diff --git a/docs/REFACTORING.md b/docs/REFACTORING.md new file mode 100644 index 0000000..4a9a63f --- /dev/null +++ b/docs/REFACTORING.md @@ -0,0 +1,506 @@ +# Refactoring Guide + +This document explains how to migrate existing actions to the new DRY patterns introduced in the task-engine refactoring. + +## Overview + +The refactoring introduces two main patterns to reduce code duplication: + +1. **New Action Style**: Generic constructor pattern using `common.BaseConstructor` +2. **New Parameter Style**: Centralized parameter resolution using `common.ParameterResolver` and `common.OutputBuilder` + +## 1. Migrating to the New Action Style + +### Before: Manual Constructor Pattern + +```go +// Old style - manual constructor +func NewMyAction(logger *slog.Logger) *task_engine.Action[*MyAction] { + action := &MyAction{ + BaseAction: task_engine.NewBaseAction(logger), + // ... other fields + } + + return &task_engine.Action[*MyAction]{ + Wrapped: action, + Name: "My Action", + ID: "my-action", + }, nil +} +``` + +### After: Generic Constructor Pattern + +```go +// New style - generic constructor +type MyActionConstructor struct { + common.BaseConstructor[*MyAction] +} + +func NewMyAction(logger *slog.Logger) *MyActionConstructor { + return &MyActionConstructor{ + BaseConstructor: *common.NewBaseConstructor[*MyAction](logger), + } +} + +func (c *MyActionConstructor) WithParameters( + param1 task_engine.ActionParameter, + param2 task_engine.ActionParameter, +) (*task_engine.Action[*MyAction], error) { + action := &MyAction{ + BaseAction: task_engine.NewBaseAction(c.GetLogger()), + // ... other fields + Param1: param1, + Param2: param2, + } + + return c.WrapAction(action, "My Action", "my-action"), nil +} +``` + +### Migration Steps + +1. **Replace the constructor function** with a constructor struct +2. **Embed `common.BaseConstructor[T]`** where `T` is your action type +3. **Use `common.NewBaseConstructor[T]`** in the constructor +4. **Replace manual action wrapping** with `c.WrapAction()` +5. **Use `c.GetLogger()`** instead of passing logger directly + +### Benefits + +- **Consistent naming**: All constructors follow the same pattern +- **Reduced boilerplate**: No more manual action wrapping +- **Type safety**: Generic constraints ensure proper action types +- **Centralized logging**: Logger management is handled automatically + +## 2. Migrating to the New Parameter Style + +### Before: Manual Parameter Resolution + +```go +// Old style - manual parameter resolution +func (a *MyAction) Execute(execCtx context.Context) error { + // Extract GlobalContext + globalCtx, ok := execCtx.Value("global_context").(task_engine.GlobalContext) + if !ok { + return fmt.Errorf("global context not found in execution context") + } + + // Resolve parameters manually + sourcePath, err := a.resolveStringParameter(globalCtx, a.SourcePathParam, "source path") + if err != nil { + return err + } + + // ... more parameter resolution + + // Manual output building + a.Output = map[string]interface{}{ + "success": true, + "sourcePath": sourcePath, + "result": result, + } + + return nil +} + +func (a *MyAction) GetOutput() interface{} { + return map[string]interface{}{ + "success": true, + "sourcePath": a.SourcePath, + "result": a.Result, + } +} +``` + +### After: Centralized Parameter Resolution + +```go +// New style - embedded parameter resolution +type MyAction struct { + task_engine.BaseAction + common.ParameterResolver // Embed ParameterResolver + common.OutputBuilder // Embed OutputBuilder + + SourcePath string + Result string + + // Parameter-aware fields + SourcePathParam task_engine.ActionParameter + ResultParam task_engine.ActionParameter +} + +func NewMyAction(logger *slog.Logger) *MyActionConstructor { + return &MyActionConstructor{ + BaseConstructor: *common.NewBaseConstructor[*MyAction](logger), + } +} + +func (c *MyActionConstructor) WithParameters( + sourcePathParam task_engine.ActionParameter, + resultParam task_engine.ActionParameter, +) (*task_engine.Action[*MyAction], error) { + action := &MyAction{ + BaseAction: task_engine.NewBaseAction(c.GetLogger()), + ParameterResolver: *common.NewParameterResolver(c.GetLogger()), // Initialize resolver + OutputBuilder: *common.NewOutputBuilder(c.GetLogger()), // Initialize builder + SourcePathParam: sourcePathParam, + ResultParam: resultParam, + } + + return c.WrapAction(action, "My Action", "my-action"), nil +} + +func (a *MyAction) Execute(execCtx context.Context) error { + // Use embedded ParameterResolver methods + sourcePath, err := a.ResolveStringParameter(execCtx, a.SourcePathParam, "source path") + if err != nil { + return err + } + + result, err := a.ResolveStringParameter(execCtx, a.ResultParam, "result") + if err != nil { + return err + } + + // ... action logic ... + + // Use embedded OutputBuilder + a.Output = a.BuildStandardOutput(nil, true, map[string]interface{}{ + "sourcePath": sourcePath, + "result": result, + }) + + return nil +} + +func (a *MyAction) GetOutput() interface{} { + return a.BuildStandardOutput(nil, true, map[string]interface{}{ + "sourcePath": a.SourcePath, + "result": a.Result, + }) +} +``` + +### Migration Steps + +1. **Embed the helper structs** in your action: + + ```go + type MyAction struct { + task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder + // ... your fields + } + ``` + +2. **Initialize them in the constructor**: + + ```go + action := &MyAction{ + BaseAction: task_engine.NewBaseAction(c.GetLogger()), + ParameterResolver: *common.NewParameterResolver(c.GetLogger()), + OutputBuilder: *common.NewOutputBuilder(c.GetLogger()), + // ... other fields + } + ``` + +3. **Replace manual parameter resolution** with embedded methods: + + - `a.ResolveStringParameter()` for strings + - `a.ResolveBoolParameter()` for booleans + - `a.ResolveIntParameter()` for integers + - `a.ResolveStringSliceParameter()` for string slices + - `a.ResolveMapParameter()` for maps + - `a.ResolveParameter()` for generic types + +4. **Replace manual output building** with embedded methods: + - `a.BuildStandardOutput()` for standard outputs + - `a.BuildOutputWithCount()` for slice results + - `a.BuildSimpleOutput()` for simple outputs + - `a.BuildErrorOutput()` for error outputs + +### Available Parameter Resolution Methods + +```go +// String parameters +a.ResolveStringParameter(execCtx, param, "parameter name") + +// Boolean parameters +a.ResolveBoolParameter(execCtx, param, "parameter name") + +// Integer parameters +a.ResolveIntParameter(execCtx, param, "parameter name") + +// String slice parameters +a.ResolveStringSliceParameter(execCtx, param, "parameter name") + +// Duration parameters +a.ResolveDurationParameter(execCtx, param, "parameter name") + +// Map parameters +a.ResolveMapParameter(execCtx, param, "parameter name") + +// Generic parameters (returns interface{}) +a.ResolveParameter(execCtx, param, "parameter name") +``` + +### Available Output Building Methods + +```go +// Standard output with success flag and custom data +a.BuildStandardOutput(err, success, data) + +// Output with count for slice results +a.BuildOutputWithCount(items, err, success, data) + +// Simple output (success + data only) +a.BuildSimpleOutput(success, data) + +// Error output +a.BuildErrorOutput(err, data) + +// Automatic output from struct fields using reflection +a.BuildStructOutput(data, err, success) +``` + +## 3. Complete Migration Example + +Here's a complete example showing the before and after: + +### Before Migration + +```go +package example + +import ( + "context" + "fmt" + "log/slog" + "task_engine" +) + +func NewExampleAction(logger *slog.Logger) *task_engine.Action[*ExampleAction] { + action := &ExampleAction{ + BaseAction: task_engine.NewBaseAction(logger), + } + + return &task_engine.Action[*ExampleAction]{ + Wrapped: action, + Name: "Example Action", + ID: "example-action", + }, nil +} + +type ExampleAction struct { + task_engine.BaseAction + SourcePath string + TargetPath string + SourcePathParam task_engine.ActionParameter + TargetPathParam task_engine.ActionParameter +} + +func (a *ExampleAction) Execute(execCtx context.Context) error { + globalCtx, ok := execCtx.Value("global_context").(task_engine.GlobalContext) + if !ok { + return fmt.Errorf("global context not found in execution context") + } + + sourcePath, err := a.resolveStringParameter(globalCtx, a.SourcePathParam, "source path") + if err != nil { + return err + } + + targetPath, err := a.resolveStringParameter(globalCtx, a.TargetPathParam, "target path") + if err != nil { + return err + } + + a.SourcePath = sourcePath + a.TargetPath = targetPath + + // ... action logic ... + + a.Output = map[string]interface{}{ + "success": true, + "sourcePath": sourcePath, + "targetPath": targetPath, + } + + return nil +} + +func (a *ExampleAction) GetOutput() interface{} { + return map[string]interface{}{ + "success": true, + "sourcePath": a.SourcePath, + "targetPath": a.TargetPath, + } +} + +func (a *ExampleAction) resolveStringParameter(globalCtx task_engine.GlobalContext, param task_engine.ActionParameter, name string) (string, error) { + if param == nil { + return "", fmt.Errorf("%s parameter is required", name) + } + + value, err := param.Resolve(globalCtx) + if err != nil { + return "", fmt.Errorf("failed to resolve %s parameter: %w", name, err) + } + + strValue, ok := value.(string) + if !ok { + return "", fmt.Errorf("%s parameter is not a string, got %T", name, value) + } + + return strValue, nil +} +``` + +### After Migration + +```go +package example + +import ( + "context" + "log/slog" + "task_engine" + "github.com/ndizazzo/task-engine/actions/common" +) + +type ExampleActionConstructor struct { + common.BaseConstructor[*ExampleAction] +} + +func NewExampleAction(logger *slog.Logger) *ExampleActionConstructor { + return &ExampleActionConstructor{ + BaseConstructor: *common.NewBaseConstructor[*ExampleAction](logger), + } +} + +func (c *ExampleActionConstructor) WithParameters( + sourcePathParam task_engine.ActionParameter, + targetPathParam task_engine.ActionParameter, +) (*task_engine.Action[*ExampleAction], error) { + action := &ExampleAction{ + BaseAction: task_engine.NewBaseAction(c.GetLogger()), + ParameterResolver: *common.NewParameterResolver(c.GetLogger()), + OutputBuilder: *common.NewOutputBuilder(c.GetLogger()), + SourcePathParam: sourcePathParam, + TargetPathParam: targetPathParam, + } + + return c.WrapAction(action, "Example Action", "example-action"), nil +} + +type ExampleAction struct { + task_engine.BaseAction + common.ParameterResolver + common.OutputBuilder + + SourcePath string + TargetPath string + + SourcePathParam task_engine.ActionParameter + TargetPathParam task_engine.ActionParameter +} + +func (a *ExampleAction) Execute(execCtx context.Context) error { + sourcePath, err := a.ResolveStringParameter(execCtx, a.SourcePathParam, "source path") + if err != nil { + return err + } + + targetPath, err := a.ResolveStringParameter(execCtx, a.TargetPathParam, "target path") + if err != nil { + return err + } + + a.SourcePath = sourcePath + a.TargetPath = targetPath + + // ... action logic ... + + a.Output = a.BuildStandardOutput(nil, true, map[string]interface{}{ + "sourcePath": sourcePath, + "targetPath": targetPath, + }) + + return nil +} + +func (a *ExampleAction) GetOutput() interface{} { + return a.BuildStandardOutput(nil, true, map[string]interface{}{ + "sourcePath": a.SourcePath, + "targetPath": a.TargetPath, + }) +} +``` + +## 4. Testing Considerations + +When migrating, you may need to update test expectations: + +### Error Message Changes + +The new `ParameterResolver` provides more consistent error messages: + +```go +// Old error messages +"parameter is not a string, got int" +"parameter is not a boolean, got string" + +// New error messages +"parameter resolved to non-string value" +"parameter resolved to non-boolean value" +``` + +### Test Updates + +```go +// Update test assertions to match new error messages +suite.Contains(err.Error(), "parameter resolved to non-string value") +suite.Contains(err.Error(), "parameter resolved to non-boolean value") +``` + +## 5. Benefits of Migration + +- **Reduced code duplication**: Common patterns are centralized +- **Consistent error handling**: Standardized parameter validation +- **Easier maintenance**: Changes to common logic affect all actions +- **Better testing**: Consistent behavior across all actions +- **Type safety**: Generic constraints prevent type errors +- **Standardized outputs**: Consistent output format across actions + +## 6. Migration Checklist + +- [ ] Update constructor to use `common.BaseConstructor` +- [ ] Embed `common.ParameterResolver` in action struct +- [ ] Embed `common.OutputBuilder` in action struct +- [ ] Initialize embedded structs in constructor +- [ ] Replace manual parameter resolution with embedded methods +- [ ] Replace manual output building with embedded methods +- [ ] Update tests to match new error messages +- [ ] Verify all tests pass +- [ ] Test parameter resolution with various types +- [ ] Test output generation + +## 7. Common Pitfalls + +1. **Forgetting to initialize embedded structs** in the constructor +2. **Not updating test error message expectations** +3. **Missing parameter type validation** in the new pattern +4. **Inconsistent output format** after migration + +## 8. Getting Help + +If you encounter issues during migration: + +1. Check the existing migrated actions for examples +2. Review the `actions/common/` package for available methods +3. Run tests to identify specific issues +4. Ensure all embedded structs are properly initialized + +The migration process is designed to be straightforward and maintain backward compatibility while providing significant improvements in code maintainability and consistency.