Skip to content

Commit b3ec91a

Browse files
authored
add value processors. Now it's possible for user to process options i… (#36)
add value processors. Now it's possible for user to process options input before passing to command
1 parent d63e8ed commit b3ec91a

File tree

5 files changed

+194
-15
lines changed

5 files changed

+194
-15
lines changed

app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ func (app *appImpl) init() error {
125125
app.actionMngr = action.NewManager(
126126
action.WithDefaultRunEnvironment,
127127
action.WithContainerRunEnvironmentConfig(app.config, name+"_"),
128+
action.WithValueProcessors(),
128129
)
129130

130131
// Register services for other modules.

pkg/action/action.go

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,23 @@ import (
44
"bytes"
55
"context"
66
"encoding/json"
7+
"errors"
8+
"fmt"
79
"path/filepath"
810

9-
"github.com/santhosh-tekuri/jsonschema/v5"
11+
jsvalidate "github.com/santhosh-tekuri/jsonschema/v5"
1012

1113
"github.com/launchrctl/launchr/pkg/cli"
14+
"github.com/launchrctl/launchr/pkg/jsonschema"
1215
"github.com/launchrctl/launchr/pkg/types"
1316
)
1417

18+
var (
19+
errInvalidProcessor = errors.New("invalid configuration, processor is required")
20+
errTplNotApplicableProcessor = "invalid configuration, processor can't be applied to value of type %s"
21+
errTplNonExistProcessor = "requested processor %q doesn't exist"
22+
)
23+
1524
// Action is an action definition with a contextual id (name), working directory path
1625
// and a runtime context such as input arguments and options.
1726
type Action struct {
@@ -25,8 +34,9 @@ type Action struct {
2534
fpath string // fpath is a path to action definition file.
2635
def *Definition // def is an action definition. Loaded by Loader, may be nil when not initialized.
2736

28-
env RunEnvironment // env is the run environment driver to execute the action.
29-
input Input // input is a container for env variables.
37+
env RunEnvironment // env is the run environment driver to execute the action.
38+
input Input // input is a container for env variables.
39+
processors map[string]ValueProcessor // processors are ValueProcessors for manipulating input.
3040
}
3141

3242
// Input is a container for action input arguments and options.
@@ -68,6 +78,16 @@ func (a *Action) Clone() *Action {
6878
return c
6979
}
7080

81+
// SetProcessors sets the value processors for an Action.
82+
func (a *Action) SetProcessors(list map[string]ValueProcessor) {
83+
a.processors = list
84+
}
85+
86+
// GetProcessors returns processors map.
87+
func (a *Action) GetProcessors() map[string]ValueProcessor {
88+
return a.processors
89+
}
90+
7191
// Reset unsets loaded action to force reload.
7292
func (a *Action) Reset() { a.def = nil }
7393

@@ -128,14 +148,94 @@ func (a *Action) SetInput(input Input) (err error) {
128148
//if err = a.ValidateInput(input); err != nil {
129149
// return err
130150
//}
151+
152+
err = a.processArgs(input.Args)
153+
if err != nil {
154+
return err
155+
}
156+
157+
err = a.processOptions(input.Opts)
158+
if err != nil {
159+
return err
160+
}
161+
131162
a.input = input
132163
// Reset to load the action file again with new replacements.
133164
a.Reset()
134165
return a.EnsureLoaded()
135166
}
136167

168+
func (a *Action) processOptions(opts TypeOpts) error {
169+
for _, optDef := range a.ActionDef().Options {
170+
if _, ok := opts[optDef.Name]; !ok {
171+
continue
172+
}
173+
174+
value := opts[optDef.Name]
175+
toApply := optDef.Process
176+
177+
value, err := a.processValue(value, optDef.Type, toApply)
178+
if err != nil {
179+
return err
180+
}
181+
182+
opts[optDef.Name] = value
183+
}
184+
185+
return nil
186+
}
187+
188+
func (a *Action) processArgs(args TypeArgs) error {
189+
for _, argDef := range a.ActionDef().Arguments {
190+
if _, ok := args[argDef.Name]; !ok {
191+
continue
192+
}
193+
194+
value := args[argDef.Name]
195+
toApply := argDef.Process
196+
value, err := a.processValue(value, argDef.Type, toApply)
197+
if err != nil {
198+
return err
199+
}
200+
201+
args[argDef.Name] = value
202+
}
203+
204+
return nil
205+
}
206+
207+
func (a *Action) processValue(value interface{}, valueType jsonschema.Type, toApplyProcessors []ValueProcessDef) (interface{}, error) {
208+
newValue := value
209+
processors := a.GetProcessors()
210+
211+
for _, processor := range toApplyProcessors {
212+
if processor.Processor == "" {
213+
return value, errInvalidProcessor
214+
}
215+
216+
proc, ok := processors[processor.Processor]
217+
if !ok {
218+
return value, fmt.Errorf(errTplNonExistProcessor, processor.Processor)
219+
}
220+
221+
if !proc.IsApplicable(valueType) {
222+
return value, fmt.Errorf(errTplNotApplicableProcessor, valueType)
223+
}
224+
225+
processedValue, err := proc.Execute(newValue, processor.Options)
226+
if err != nil {
227+
return value, err
228+
}
229+
230+
newValue = processedValue
231+
}
232+
233+
return newValue, nil
234+
}
235+
137236
// ValidateInput validates arguments and options according to
138237
// a specified json schema in action definition.
238+
// @todo move to jsonschema
139239
func (a *Action) ValidateInput(inp Input) error {
140240
jsch := a.JSONSchema()
141241
// @todo cache jsonschema and resources.
@@ -144,7 +244,7 @@ func (a *Action) ValidateInput(inp Input) error {
144244
return err
145245
}
146246
buf := bytes.NewBuffer(b)
147-
c := jsonschema.NewCompiler()
247+
c := jsvalidate.NewCompiler()
148248
err = c.AddResource(a.Filepath(), buf)
149249
if err != nil {
150250
return err

pkg/action/manager.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package action
22

33
import (
44
"context"
5+
"fmt"
56
"strconv"
67
"sync"
78
"time"
@@ -22,6 +23,10 @@ type Manager interface {
2223
Get(id string) (*Action, bool)
2324
// GetRef returns an original action value from the storage.
2425
GetRef(id string) (*Action, bool)
26+
// AddValueProcessor adds processor to list of available processors
27+
AddValueProcessor(name string, vp ValueProcessor)
28+
// GetValueProcessors returns list of available processors
29+
GetValueProcessors() map[string]ValueProcessor
2530
// Decorate decorates an action with given behaviors and returns its copy.
2631
// If functions withFn are not provided, default functions are applied.
2732
Decorate(a *Action, withFn ...DecorateWithFn) *Action
@@ -49,6 +54,7 @@ type actionManagerMap struct {
4954
mx sync.Mutex
5055
mxRun sync.Mutex
5156
dwFns []DecorateWithFn
57+
processors map[string]ValueProcessor
5258
}
5359

5460
// NewManager constructs a new action manager.
@@ -57,6 +63,7 @@ func NewManager(withFns ...DecorateWithFn) Manager {
5763
actionStore: make(map[string]*Action),
5864
runStore: make(map[string]RunInfo),
5965
dwFns: withFns,
66+
processors: make(map[string]ValueProcessor),
6067
}
6168
}
6269

@@ -97,6 +104,18 @@ func (m *actionManagerMap) GetRef(id string) (*Action, bool) {
97104
return a, ok
98105
}
99106

107+
func (m *actionManagerMap) AddValueProcessor(name string, vp ValueProcessor) {
108+
if _, ok := m.processors[name]; ok {
109+
panic(fmt.Sprintf("processor `%q` with the same name already exists", name))
110+
}
111+
112+
m.processors[name] = vp
113+
}
114+
115+
func (m *actionManagerMap) GetValueProcessors() map[string]ValueProcessor {
116+
return m.processors
117+
}
118+
100119
func (m *actionManagerMap) Decorate(a *Action, withFns ...DecorateWithFn) *Action {
101120
if a == nil {
102121
return nil
@@ -206,3 +225,10 @@ func WithContainerRunEnvironmentConfig(cfg launchr.Config, prefix string) Decora
206225
}
207226
}
208227
}
228+
229+
// WithValueProcessors sets processors for action from manager.
230+
func WithValueProcessors() DecorateWithFn {
231+
return func(m Manager, a *Action) {
232+
a.SetProcessors(m.GetValueProcessors())
233+
}
234+
}

pkg/action/process.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package action
2+
3+
import (
4+
"github.com/launchrctl/launchr/pkg/jsonschema"
5+
)
6+
7+
// ValueProcessor defines an interface for processing a value based on its type and some options.
8+
type ValueProcessor interface {
9+
IsApplicable(valueType jsonschema.Type) bool
10+
Execute(value interface{}, options map[string]interface{}) (interface{}, error)
11+
}
12+
13+
// ValueProcessorFn is a function signature used as a callback in processors.
14+
type ValueProcessorFn func(value interface{}, options map[string]interface{}) (interface{}, error)
15+
16+
// NewFuncProcessor creates a new instance of FuncProcessor with the specified formats and callback.
17+
func NewFuncProcessor(formats []jsonschema.Type, callback ValueProcessorFn) FuncProcessor {
18+
return FuncProcessor{
19+
applicableFormats: formats,
20+
callback: callback,
21+
}
22+
}
23+
24+
// FuncProcessor represents a processor that applies a callback function to values based on certain applicable formats.
25+
type FuncProcessor struct {
26+
applicableFormats []jsonschema.Type
27+
callback ValueProcessorFn
28+
}
29+
30+
// IsApplicable checks if the given valueType is present in the applicableFormats slice of the FuncProcessor.
31+
func (p FuncProcessor) IsApplicable(valueType jsonschema.Type) bool {
32+
for _, item := range p.applicableFormats {
33+
if valueType == item {
34+
return true
35+
}
36+
}
37+
38+
return false
39+
}
40+
41+
// Execute applies the callback function of the FuncProcessor to the given value with options.
42+
func (p FuncProcessor) Execute(value interface{}, options map[string]interface{}) (interface{}, error) {
43+
return p.callback(value, options)
44+
}

pkg/action/yaml.def.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,11 @@ func (l *ArgumentsList) UnmarshalYAML(nodeList *yaml.Node) (err error) {
247247

248248
// Argument stores command arguments declaration.
249249
type Argument struct {
250-
Name string `yaml:"name"`
251-
Title string `yaml:"title"`
252-
Description string `yaml:"description"`
253-
Type jsonschema.Type `yaml:"type"`
250+
Name string `yaml:"name"`
251+
Title string `yaml:"title"`
252+
Description string `yaml:"description"`
253+
Type jsonschema.Type `yaml:"type"`
254+
Process []ValueProcessDef `yaml:"process"`
254255
RawMap map[string]interface{}
255256
}
256257

@@ -277,16 +278,23 @@ func (l *OptionsList) UnmarshalYAML(nodeList *yaml.Node) (err error) {
277278

278279
// Option stores command options declaration.
279280
type Option struct {
280-
Name string `yaml:"name"`
281-
Shorthand string `yaml:"shorthand"` // @todo test definition, validate, catch panic if overlay, add to readme.
282-
Title string `yaml:"title"`
283-
Description string `yaml:"description"`
284-
Type jsonschema.Type `yaml:"type"`
285-
Default interface{} `yaml:"default"`
286-
Required bool `yaml:"required"` // @todo that conflicts with json schema object definition
281+
Name string `yaml:"name"`
282+
Shorthand string `yaml:"shorthand"` // @todo test definition, validate, catch panic if overlay, add to readme.
283+
Title string `yaml:"title"`
284+
Description string `yaml:"description"`
285+
Type jsonschema.Type `yaml:"type"`
286+
Default interface{} `yaml:"default"`
287+
Required bool `yaml:"required"` // @todo that conflicts with json schema object definition
288+
Process []ValueProcessDef `yaml:"process"`
287289
RawMap map[string]interface{}
288290
}
289291

292+
// ValueProcessDef stores information about processor and options that should be applied to processor.
293+
type ValueProcessDef struct {
294+
Processor string `yaml:"processor"`
295+
Options map[string]interface{} `yaml:"options"`
296+
}
297+
290298
// UnmarshalYAML implements yaml.Unmarshaler to parse Option.
291299
func (o *Option) UnmarshalYAML(node *yaml.Node) (err error) {
292300
type yamlT Option

0 commit comments

Comments
 (0)