Skip to content

Commit 8f1ae26

Browse files
authored
change behavior of runtime and globals opts for action (#98)
1 parent f403a15 commit 8f1ae26

File tree

21 files changed

+759
-232
lines changed

21 files changed

+759
-232
lines changed

internal/launchr/streams.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ func (s *commonStream) FD() uintptr {
3131
return s.fd
3232
}
3333

34+
// Fd returns the file descriptor number for this stream.
35+
func (s *commonStream) Fd() uintptr {
36+
return s.fd
37+
}
38+
3439
// IsDiscard returns if read/write is discarded.
3540
func (s *commonStream) IsDiscard() bool {
3641
return s.isDiscard

internal/launchr/term.go

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

33
import (
44
"io"
5-
"log"
65
"reflect"
76

87
"github.com/pterm/pterm"
@@ -19,7 +18,14 @@ var DefaultTextPrinter = message.NewPrinter(language.English)
1918

2019
func init() {
2120
// Initialize the default printer.
22-
defaultTerm = &Terminal{
21+
defaultTerm = NewTerminal()
22+
// Do not output anything when not in the app, e.g. in tests.
23+
defaultTerm.DisableOutput()
24+
}
25+
26+
// NewTerminal creates a new instance of [Terminal]
27+
func NewTerminal() *Terminal {
28+
return &Terminal{
2329
p: []TextPrinter{
2430
printerBasic: newPTermBasicPrinter(pterm.DefaultBasicText),
2531
printerInfo: newPTermPrefixPrinter(pterm.Info),
@@ -29,8 +35,6 @@ func init() {
2935
},
3036
enabled: true,
3137
}
32-
// Do not output anything when not in the app, e.g. in tests.
33-
defaultTerm.DisableOutput()
3438
}
3539

3640
// Predefined keys of terminal printers.
@@ -84,7 +88,9 @@ func (p *ptermPrinter) SetOutput(w io.Writer) {
8488
if !method.IsValid() {
8589
panic("WithWriter is not implemented for this pterm.TextPrinter")
8690
}
87-
method.Call([]reflect.Value{reflect.ValueOf(w)})
91+
result := method.Call([]reflect.Value{reflect.ValueOf(w)})
92+
// Replace old printer by new one as WithWriter returns fresh copy of struct.
93+
p.pterm = result[0].Interface().(pterm.TextPrinter)
8894
}
8995

9096
// Terminal prints formatted text to the console.
@@ -115,8 +121,6 @@ func (t *Terminal) DisableOutput() {
115121
// SetOutput sets an output to target writer.
116122
func (t *Terminal) SetOutput(w io.Writer) {
117123
t.w = w
118-
// If some library uses std log, redirect as well.
119-
log.SetOutput(w)
120124
// Ensure underlying printers use self.
121125
// Used to simplify update of writers in the printers.
122126
for i := 0; i < len(t.p); i++ {

pkg/action/action.flags.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package action
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/launchrctl/launchr/pkg/jsonschema"
7+
)
8+
9+
// FlagsGroup holds definitions, current state, and default values of flags.
10+
// @todo think about moving it to new input validation service alongside. See notes in actionManagerMap.ValidateFlags.
11+
type FlagsGroup struct {
12+
name string
13+
definitions ParametersList
14+
values map[string]any
15+
defaults map[string]any
16+
}
17+
18+
// NewFlagsGroup returns a new instance of [FlagsGroup]
19+
func NewFlagsGroup(name string) *FlagsGroup {
20+
return &FlagsGroup{
21+
name: name,
22+
definitions: make(ParametersList, 0),
23+
values: make(map[string]any),
24+
defaults: make(map[string]any),
25+
}
26+
}
27+
28+
// GetName returns the name of the flags group.
29+
func (p *FlagsGroup) GetName() string {
30+
return p.name
31+
}
32+
33+
// GetAll returns the latest state of flags.
34+
func (p *FlagsGroup) GetAll() InputParams {
35+
result := make(InputParams)
36+
for name, value := range p.defaults {
37+
if _, ok := p.values[name]; !ok {
38+
result[name] = value
39+
} else {
40+
result[name] = p.values[name]
41+
}
42+
}
43+
44+
return result
45+
}
46+
47+
func (p *FlagsGroup) exists(name string) bool {
48+
_, ok := p.defaults[name]
49+
return ok
50+
}
51+
52+
// Get returns state of a named flag.
53+
// Return false if a flag doesn't exist.
54+
func (p *FlagsGroup) Get(name string) (any, bool) {
55+
if !p.exists(name) {
56+
return nil, false
57+
}
58+
59+
var value any
60+
if v, ok := p.values[name]; ok {
61+
value = v
62+
} else {
63+
value = p.defaults[name]
64+
}
65+
66+
return value, true
67+
}
68+
69+
// Set sets new state value for a flag. Does nothing if flag doesn't exist.
70+
func (p *FlagsGroup) Set(name string, value any) {
71+
if !p.exists(name) {
72+
return
73+
}
74+
75+
p.values[name] = value
76+
}
77+
78+
// Unset removes the flag value.
79+
// The default value will be returned during [FlagsGroup.GetAll] if flag is not set.
80+
func (p *FlagsGroup) Unset(name string) {
81+
delete(p.values, name)
82+
}
83+
84+
// GetDefinitions returns [ParametersList] with flags definitions.
85+
func (p *FlagsGroup) GetDefinitions() ParametersList {
86+
return p.definitions
87+
}
88+
89+
// AddDefinitions adds new flag definition.
90+
func (p *FlagsGroup) AddDefinitions(opts ParametersList) {
91+
registered := make(map[string]struct{})
92+
93+
for _, def := range p.definitions {
94+
registered[def.Name] = struct{}{}
95+
}
96+
97+
for _, opt := range opts {
98+
if opt.Name == "" {
99+
panic(fmt.Sprintf("%s flag name cannot be empty", p.name))
100+
}
101+
102+
if _, exists := registered[opt.Name]; exists {
103+
panic(fmt.Sprintf("duplicate %s flag has been detected %s", p.name, opt.Name))
104+
}
105+
106+
p.definitions = append(p.definitions, opt)
107+
}
108+
109+
for _, d := range p.definitions {
110+
p.defaults[d.Name] = d.Default
111+
}
112+
}
113+
114+
// JSONSchema returns JSON schema of a flags group.
115+
func (p *FlagsGroup) JSONSchema() jsonschema.Schema {
116+
opts, optsReq := p.definitions.JSONSchema()
117+
return jsonschema.Schema{
118+
Type: jsonschema.Object,
119+
Required: []string{p.name},
120+
Properties: map[string]any{
121+
p.name: map[string]any{
122+
"type": "object",
123+
"title": p.name,
124+
"properties": opts,
125+
"required": optsReq,
126+
"additionalProperties": false,
127+
},
128+
},
129+
}
130+
}
131+
132+
// ValidateFlags validates input flags.
133+
func (p *FlagsGroup) ValidateFlags(params InputParams) error {
134+
s := p.JSONSchema()
135+
return jsonschema.Validate(s, map[string]any{p.name: params})
136+
}

pkg/action/action.go

Lines changed: 4 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010

1111
"github.com/launchrctl/launchr/internal/launchr"
1212
"github.com/launchrctl/launchr/pkg/driver"
13-
"github.com/launchrctl/launchr/pkg/jsonschema"
1413
)
1514

1615
// Action is an action definition with a contextual id (name), working directory path
@@ -29,9 +28,8 @@ type Action struct {
2928
def *Definition // def is an action definition. Loaded by [Loader], may be nil when not initialized.
3029
defRaw *Definition // defRaw is a raw action definition. Loaded by [Loader], may be nil when not initialized.
3130

32-
runtime Runtime // runtime is the [Runtime] to execute the action.
33-
input *Input // input is a storage for arguments and options used in runtime.
34-
processors map[string]ValueProcessor // processors are [ValueProcessor] for manipulating input.
31+
runtime Runtime // runtime is the [Runtime] to execute the action.
32+
input *Input // input is a storage for arguments and options used in runtime.
3533
}
3634

3735
// New creates a new action.
@@ -110,11 +108,6 @@ func (a *Action) SetProcessors(list map[string]ValueProcessor) error {
110108
return nil
111109
}
112110

113-
// GetProcessors returns processors map.
114-
func (a *Action) GetProcessors() map[string]ValueProcessor {
115-
return a.processors
116-
}
117-
118111
// Reset unsets loaded action to force reload.
119112
func (a *Action) Reset() { a.def = nil }
120113

@@ -256,23 +249,8 @@ func (a *Action) ImageBuildInfo(image string) *driver.BuildDefinition {
256249

257250
// SetInput saves arguments and options for later processing in run, templates, etc.
258251
func (a *Action) SetInput(input *Input) (err error) {
259-
def := a.ActionDef()
260-
261-
// Process arguments.
262-
err = a.processInputParams(def.Arguments, input.Args(), input.ArgsChanged(), input)
263-
if err != nil {
264-
return err
265-
}
266-
267-
// Process options.
268-
err = a.processInputParams(def.Options, input.Opts(), input.OptsChanged(), input)
269-
if err != nil {
270-
return err
271-
}
272-
273-
// Validate the new input.
274-
if err = a.ValidateInput(input); err != nil {
275-
return err
252+
if !input.IsValidated() {
253+
return fmt.Errorf("input is not validated")
276254
}
277255

278256
a.input = input
@@ -281,60 +259,6 @@ func (a *Action) SetInput(input *Input) (err error) {
281259
return a.EnsureLoaded()
282260
}
283261

284-
func (a *Action) processInputParams(def ParametersList, inp InputParams, changed InputParams, input *Input) error {
285-
var err error
286-
for _, p := range def {
287-
_, isChanged := changed[p.Name]
288-
res := inp[p.Name]
289-
for i, procDef := range p.Process {
290-
handler := p.processors[i]
291-
res, err = handler(res, ValueProcessorContext{
292-
ValOrig: inp[p.Name],
293-
IsChanged: isChanged,
294-
Input: input,
295-
DefParam: p,
296-
Action: a,
297-
})
298-
if err != nil {
299-
return ErrValueProcessorHandler{
300-
Processor: procDef.ID,
301-
Param: p.Name,
302-
Err: err,
303-
}
304-
}
305-
}
306-
// Cast to []any slice because jsonschema validator supports only this type.
307-
if p.Type == jsonschema.Array {
308-
res = CastSliceTypedToAny(res)
309-
}
310-
// If the value was changed, we can safely override the value.
311-
// If the value was not changed and processed is nil, do not add it.
312-
if isChanged || res != nil {
313-
inp[p.Name] = res
314-
}
315-
}
316-
317-
return nil
318-
}
319-
320-
// ValidateInput validates action input.
321-
func (a *Action) ValidateInput(input *Input) error {
322-
if input.IsValidated() {
323-
return nil
324-
}
325-
argsDefLen := len(a.ActionDef().Arguments)
326-
argsPosLen := len(input.ArgsPositional())
327-
if argsPosLen > argsDefLen {
328-
return fmt.Errorf("accepts %d arg(s), received %d", argsDefLen, argsPosLen)
329-
}
330-
err := validateJSONSchema(a, input)
331-
if err != nil {
332-
return err
333-
}
334-
input.SetValidated(true)
335-
return nil
336-
}
337-
338262
// Execute runs action in the specified environment.
339263
func (a *Action) Execute(ctx context.Context) error {
340264
// @todo maybe it shouldn't be here.

pkg/action/action.input.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ type Input struct {
2626
args InputParams
2727
// opts contains parsed options with default values.
2828
opts InputParams
29+
// groups contains input parameters grouped by unique name
30+
groups map[string]InputParams
2931
// io contains out/in/err destinations.
3032
io launchr.Streams
3133

@@ -52,6 +54,7 @@ func NewInput(a *Action, args InputParams, opts InputParams, io launchr.Streams)
5254
argsPos: argsPos,
5355
opts: setParamDefaults(opts, def.Options),
5456
optsRaw: opts,
57+
groups: make(map[string]InputParams),
5558
io: io,
5659
}
5760
}
@@ -149,6 +152,29 @@ func (input *Input) IsOptChanged(name string) bool {
149152
return ok
150153
}
151154

155+
// GroupFlags returns stored group flags values.
156+
func (input *Input) GroupFlags(group string) InputParams {
157+
if gp, ok := input.groups[group]; ok {
158+
return gp
159+
}
160+
return make(InputParams)
161+
}
162+
163+
// GetFlagInGroup returns a group flag by name.
164+
func (input *Input) GetFlagInGroup(group, name string) any {
165+
return input.GroupFlags(group)[name]
166+
}
167+
168+
// SetFlagInGroup sets group flag value.
169+
func (input *Input) SetFlagInGroup(group, name string, val any) {
170+
gp, ok := input.groups[group]
171+
if !ok {
172+
gp = make(InputParams)
173+
input.groups[group] = gp
174+
}
175+
gp[name] = val
176+
}
177+
152178
// Args returns input named and processed arguments.
153179
func (input *Input) Args() InputParams {
154180
return input.args

pkg/action/action_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func Test_Action(t *testing.T) {
5656
require.NotNil(act.input)
5757

5858
// Option is not defined, but should be there
59-
// because [Action.ValidateInput] decides if the input correct or not.
59+
// because [manager.ValidateInput] decides if the input correct or not.
6060
_, okOpt := act.input.Opts()["opt6"]
6161
assert.True(okOpt)
6262
assert.Equal(inputArgs, act.input.Args())
@@ -231,6 +231,8 @@ func Test_ActionInputValidate(t *testing.T) {
231231
expErr error
232232
}
233233

234+
am := NewManager()
235+
234236
// Extra input preparation and testing.
235237
setValidatedInput := func(t *testing.T, _ *Action, input *Input) {
236238
input.SetValidated(true)
@@ -371,7 +373,7 @@ func Test_ActionInputValidate(t *testing.T) {
371373
if tt.fnInit != nil {
372374
tt.fnInit(t, a, input)
373375
}
374-
err := a.ValidateInput(input)
376+
err := am.ValidateInput(a, input)
375377
assert.Equal(t, err == nil, input.IsValidated())
376378
assertIsSameError(t, tt.expErr, err)
377379
})

0 commit comments

Comments
 (0)