Skip to content

Commit

Permalink
Fix value processor. Cover with tests. (#79)
Browse files Browse the repository at this point in the history
Fix value processor. Cover with tests.
  • Loading branch information
lexbritvin authored Jan 21, 2025
1 parent 2530e05 commit b6b8366
Show file tree
Hide file tree
Showing 18 changed files with 613 additions and 162 deletions.
1 change: 0 additions & 1 deletion app.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ func (app *appImpl) init() error {
actionMngr := action.NewManager(
action.WithDefaultRuntime,
action.WithContainerRuntimeConfig(config, name+"_"),
action.WithValueProcessors(),
)

// Register services for other modules.
Expand Down
12 changes: 6 additions & 6 deletions internal/launchr/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ type ConfigAware interface {

type cachedProps = map[string]reflect.Value
type config struct {
mx sync.Mutex
root fs.FS
fname fs.DirEntry
rootPath string
cached cachedProps
koanf *koanf.Koanf
mx sync.Mutex // mx is a mutex to read/cache values.
root fs.FS // root is a base dir filesystem.
fname fs.DirEntry // fname is a file storing the config.
rootPath string // rootPath is a base dir path.
cached cachedProps // cached is a map of cached properties read from a file.
koanf *koanf.Koanf // koanf is the driver to read the yaml config.
}

func findConfigFile(root fs.FS) fs.DirEntry {
Expand Down
86 changes: 33 additions & 53 deletions pkg/action/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (
)

var (
errTplNotApplicableProcessor = "invalid configuration, processor can't be applied to value of type %s"
errTplNotApplicableProcessor = "invalid configuration, processor %q can't be applied to a parameter of type %s"
errTplNonExistProcessor = "requested processor %q doesn't exist"
errTplErrorOnProcessor = "failed to process parameter %q with %q: %w"
)

// Action is an action definition with a contextual id (name), working directory path
Expand Down Expand Up @@ -78,8 +79,18 @@ func (a *Action) Clone() *Action {
}

// SetProcessors sets the value processors for an [Action].
func (a *Action) SetProcessors(list map[string]ValueProcessor) {
a.processors = list
func (a *Action) SetProcessors(list map[string]ValueProcessor) error {
def := a.ActionDef()
for _, params := range []ParametersList{def.Arguments, def.Options} {
for _, p := range params {
err := p.InitProcessors(list)
if err != nil {
return err
}
}
}

return nil
}

// GetProcessors returns processors map.
Expand Down Expand Up @@ -193,13 +204,13 @@ func (a *Action) SetInput(input *Input) (err error) {
def := a.ActionDef()

// Process arguments.
err = a.processInputParams(def.Arguments, input.ArgsNamed(), nil)
err = a.processInputParams(def.Arguments, input.Args(), input.ArgsChanged())
if err != nil {
return err
}

// Process options.
err = a.processInputParams(def.Options, input.OptsAll(), input.OptsChanged())
err = a.processInputParams(def.Options, input.Opts(), input.OptsChanged())
if err != nil {
return err
}
Expand All @@ -216,63 +227,32 @@ func (a *Action) SetInput(input *Input) (err error) {
}

func (a *Action) processInputParams(def ParametersList, inp InputParams, changed InputParams) error {
var err error
for _, p := range def {
if _, ok := inp[p.Name]; !ok {
continue
}

if changed != nil {
if _, ok := changed[p.Name]; ok {
continue
_, isChanged := changed[p.Name]
res := inp[p.Name]
for i, procDef := range p.Process {
handler := p.processors[i]
res, err = handler(res, ValueProcessorContext{
ValOrig: inp[p.Name],
IsChanged: isChanged,
DefParam: p,
Action: a,
})
if err != nil {
return fmt.Errorf(errTplErrorOnProcessor, p.Name, procDef.ID, err)
}
}

value := inp[p.Name]
toApply := p.Process

value, err := a.processValue(value, p.Type, toApply)
if err != nil {
return err
}
// Replace the value.
// Check for nil not to override the default value.
if value != nil {
inp[p.Name] = value
// Cast to []any slice because jsonschema validator supports only this type.
if p.Type == jsonschema.Array {
res = CastSliceTypedToAny(res)
}
inp[p.Name] = res
}

return nil
}

func (a *Action) processValue(v any, vtype jsonschema.Type, applyProc []DefValueProcessor) (any, error) {
res := v
processors := a.GetProcessors()

for _, procDef := range applyProc {
proc, ok := processors[procDef.ID]
if !ok {
return v, fmt.Errorf(errTplNonExistProcessor, procDef.ID)
}

if !proc.IsApplicable(vtype) {
return v, fmt.Errorf(errTplNotApplicableProcessor, vtype)
}

processedValue, err := proc.Execute(res, procDef.Options)
if err != nil {
return v, err
}

res = processedValue
}
// Cast to []any slice because jsonschema validator supports only this type.
if vtype == jsonschema.Array {
res = CastSliceTypedToAny(res)
}

return res, nil
}

// ValidateInput validates action input.
func (a *Action) ValidateInput(input *Input) error {
if input.IsValidated() {
Expand Down
37 changes: 25 additions & 12 deletions pkg/action/action.input.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package action

import (
"maps"
"reflect"
"strings"

Expand Down Expand Up @@ -30,6 +31,8 @@ type Input struct {

// argsPos contains raw positional arguments.
argsPos []string
// argsRaw contains arguments that were input by a user and without default values.
argsRaw InputParams
// optsRaw contains options that were input by a user and without default values.
optsRaw InputParams
}
Expand All @@ -45,6 +48,7 @@ func NewInput(a *Action, args InputParams, opts InputParams, io launchr.Streams)
return &Input{
action: a,
args: setParamDefaults(args, def.Arguments),
argsRaw: args,
argsPos: argsPos,
opts: setParamDefaults(opts, def.Options),
optsRaw: opts,
Expand Down Expand Up @@ -98,31 +102,32 @@ func (input *Input) SetValidated(v bool) {

// Arg returns argument by a name.
func (input *Input) Arg(name string) any {
return input.ArgsNamed()[name]
return input.Args()[name]
}

// SetArg sets an argument value.
func (input *Input) SetArg(name string, val any) {
input.optsRaw[name] = val
input.opts[name] = val
input.argsRaw[name] = val
input.args[name] = val
}

// UnsetArg unsets the arguments and recalculates default and positional values.
func (input *Input) UnsetArg(name string) {
delete(input.args, name)
input.args = setParamDefaults(input.args, input.action.ActionDef().Arguments)
input.argsPos = argsNamedToPos(input.args, input.action.ActionDef().Arguments)
delete(input.argsRaw, name)
input.args = setParamDefaults(input.argsRaw, input.action.ActionDef().Arguments)
input.argsPos = argsNamedToPos(input.argsRaw, input.action.ActionDef().Arguments)
}

// IsArgChanged checks if an argument was changed by user.
func (input *Input) IsArgChanged(name string) bool {
_, ok := input.args[name]
_, ok := input.argsRaw[name]
return ok
}

// Opt returns option by a name.
func (input *Input) Opt(name string) any {
return input.OptsAll()[name]
return input.Opts()[name]
}

// SetOpt sets an option value.
Expand All @@ -144,18 +149,23 @@ func (input *Input) IsOptChanged(name string) bool {
return ok
}

// ArgsNamed returns input named and processed arguments.
func (input *Input) ArgsNamed() InputParams {
// Args returns input named and processed arguments.
func (input *Input) Args() InputParams {
return input.args
}

// ArgsChanged returns arguments that were set manually by user (not processed).
func (input *Input) ArgsChanged() InputParams {
return input.argsRaw
}

// ArgsPositional returns positional arguments set by user (not processed).
func (input *Input) ArgsPositional() []string {
return input.argsPos
}

// OptsAll returns options with default values and processed.
func (input *Input) OptsAll() InputParams {
// Opts returns options with default values and processed.
func (input *Input) Opts() InputParams {
return input.opts
}

Expand Down Expand Up @@ -184,7 +194,10 @@ func argsNamedToPos(args InputParams, argsDef ParametersList) []string {
}

func setParamDefaults(params InputParams, paramDef ParametersList) InputParams {
res := copyMap(params)
res := maps.Clone(params)
if res == nil {
res = make(InputParams)
}
for _, d := range paramDef {
k := d.Name
v, ok := params[k]
Expand Down
27 changes: 18 additions & 9 deletions pkg/action/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ func Test_Action(t *testing.T) {
require.NotNil(act.input)
// Option is not defined, but should be there
// because [Action.ValidateInput] decides if the input correct or not.
_, okOpt := act.input.OptsAll()["opt6"]
_, okOpt := act.input.Opts()["opt6"]
assert.True(okOpt)
assert.Equal(inputArgs, act.input.ArgsNamed())
assert.Equal(inputOpts, act.input.OptsAll())
assert.Equal(inputArgs, act.input.Args())
assert.Equal(inputOpts, act.input.Opts())

// Test templating in executable.
envVar1 := "envval1"
Expand Down Expand Up @@ -129,22 +129,30 @@ func Test_ActionInput(t *testing.T) {
// Check argument is changed.
input = NewInput(a, InputParams{"arg_str": "my_string"}, nil, nil)
require.NotNil(input)
changed := input.ArgsNamed()
assert.Equal(InputParams{"arg_str": "my_string", "arg_default": "my_default_string"}, changed)
assert.Equal(InputParams{"arg_str": "my_string", "arg_default": "my_default_string"}, input.Args())
assert.Equal(InputParams{"arg_str": "my_string"}, input.ArgsChanged())
assert.True(input.IsArgChanged("arg_str"))
assert.False(input.IsArgChanged("arg_int"))
assert.False(input.IsArgChanged("arg_str2"))
input.SetArg("arg_str2", "my_str2")
assert.True(input.IsArgChanged("arg_str2"))
assert.Equal(InputParams{"arg_str": "my_string", "arg_str2": "my_str2"}, input.ArgsChanged())
input.UnsetArg("arg_str")
assert.Equal(InputParams{"arg_str2": "my_str2"}, input.ArgsChanged())
assert.False(input.IsArgChanged("arg_str"))
// Check option is changed.
input = NewInput(a, nil, InputParams{"opt_str": "my_string"}, nil)
require.NotNil(input)
changed = input.OptsChanged()
assert.Equal(InputParams{"opt_str": "my_string"}, changed)
assert.Equal(InputParams{"opt_str": "my_string"}, input.OptsChanged())
assert.True(input.IsOptChanged("opt_str"))
assert.False(input.IsOptChanged("opt_int"))
// Set option and check it's changed.
input.SetOpt("opt_int", 24)
assert.True(input.IsOptChanged("opt_int"))
assert.Equal(InputParams{"opt_str": "my_string", "opt_int": 24, "opt_str_default": "optdefault"}, input.OptsAll())
assert.Equal(InputParams{"opt_str": "my_string", "opt_int": 24, "opt_str_default": "optdefault"}, input.Opts())
input.UnsetOpt("opt_str")
assert.Equal(InputParams{"opt_int": 24}, input.OptsChanged())
assert.False(input.IsOptChanged("opt_str"))

// Test create with positional arguments of different types.
argsPos := []string{"42", "str", "str2", "true", "str3", "undstr", "24"}
Expand All @@ -163,7 +171,7 @@ func Test_ActionInput(t *testing.T) {
}
_, posKeyOk = input.args[inputMapKeyArgsPos]
assert.False(posKeyOk)
assert.Equal(expArgs, input.ArgsNamed())
assert.Equal(expArgs, input.Args())
assert.Equal(argsPos, input.ArgsPositional())
}

Expand Down Expand Up @@ -320,6 +328,7 @@ func Test_ActionInputValidate(t *testing.T) {
tt.fnInit(t, a, input)
}
err := a.ValidateInput(input)
assert.Equal(t, err == nil, input.IsValidated())
if tt.expErr == errAny {
assert.True(t, assert.Error(t, err))
} else if assert.IsType(t, tt.expErr, err) {
Expand Down
7 changes: 4 additions & 3 deletions pkg/action/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package action

import (
"fmt"
"maps"

"github.com/launchrctl/launchr/pkg/jsonschema"
)
Expand All @@ -17,8 +18,8 @@ func validateJSONSchema(a *Action, input *Input) error {
return jsonschema.Validate(
a.JSONSchema(),
map[string]any{
jsonschemaPropArgs: input.ArgsNamed(),
jsonschemaPropOpts: input.OptsAll(),
jsonschemaPropArgs: input.Args(),
jsonschemaPropOpts: input.Opts(),
},
)
}
Expand Down Expand Up @@ -84,5 +85,5 @@ func (l *ParametersList) JSONSchema() (map[string]any, []string) {

// JSONSchema returns json schema definition of an option.
func (p *DefParameter) JSONSchema() map[string]any {
return copyMap(p.raw)
return maps.Clone(p.raw)
}
4 changes: 2 additions & 2 deletions pkg/action/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ Action definition is correct, but dashes are not allowed in templates, replace "

// ConvertInputToTplVars creates a map with input variables suitable for template engine.
func ConvertInputToTplVars(input *Input, ac *DefAction) map[string]any {
args := input.ArgsNamed()
opts := input.OptsAll()
args := input.Args()
opts := input.Opts()
values := make(map[string]any, len(args)+len(opts))

// Collect arguments and options values.
Expand Down
Loading

0 comments on commit b6b8366

Please sign in to comment.