Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions internal/launchr/streams.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ func (s *commonStream) FD() uintptr {
return s.fd
}

// Fd returns the file descriptor number for this stream.
func (s *commonStream) Fd() uintptr {
return s.fd
}

// IsDiscard returns if read/write is discarded.
func (s *commonStream) IsDiscard() bool {
return s.isDiscard
Expand Down
18 changes: 11 additions & 7 deletions internal/launchr/term.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package launchr

import (
"io"
"log"
"reflect"

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

func init() {
// Initialize the default printer.
defaultTerm = &Terminal{
defaultTerm = NewTerminal()
// Do not output anything when not in the app, e.g. in tests.
defaultTerm.DisableOutput()
}

// NewTerminal creates a new instance of [Terminal]
func NewTerminal() *Terminal {
return &Terminal{
p: []TextPrinter{
printerBasic: newPTermBasicPrinter(pterm.DefaultBasicText),
printerInfo: newPTermPrefixPrinter(pterm.Info),
Expand All @@ -29,8 +35,6 @@ func init() {
},
enabled: true,
}
// Do not output anything when not in the app, e.g. in tests.
defaultTerm.DisableOutput()
}

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

// Terminal prints formatted text to the console.
Expand Down Expand Up @@ -115,8 +121,6 @@ func (t *Terminal) DisableOutput() {
// SetOutput sets an output to target writer.
func (t *Terminal) SetOutput(w io.Writer) {
t.w = w
// If some library uses std log, redirect as well.
log.SetOutput(w)
// Ensure underlying printers use self.
// Used to simplify update of writers in the printers.
for i := 0; i < len(t.p); i++ {
Expand Down
136 changes: 136 additions & 0 deletions pkg/action/action.flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package action

import (
"fmt"

"github.com/launchrctl/launchr/pkg/jsonschema"
)

// FlagsGroup holds definitions, current state, and default values of flags.
// @todo think about moving it to new input validation service alongside. See notes in actionManagerMap.ValidateFlags.
type FlagsGroup struct {
name string
definitions ParametersList
values map[string]any
defaults map[string]any
}

// NewFlagsGroup returns a new instance of [FlagsGroup]
func NewFlagsGroup(name string) *FlagsGroup {
return &FlagsGroup{
name: name,
definitions: make(ParametersList, 0),
values: make(map[string]any),
defaults: make(map[string]any),
}
}

// GetName returns the name of the flags group.
func (p *FlagsGroup) GetName() string {
return p.name
}

// GetAll returns the latest state of flags.
func (p *FlagsGroup) GetAll() InputParams {
result := make(InputParams)
for name, value := range p.defaults {
if _, ok := p.values[name]; !ok {
result[name] = value
} else {
result[name] = p.values[name]
}
}

return result
}

func (p *FlagsGroup) exists(name string) bool {
_, ok := p.defaults[name]
return ok
}

// Get returns state of a named flag.
// Return false if a flag doesn't exist.
func (p *FlagsGroup) Get(name string) (any, bool) {
if !p.exists(name) {
return nil, false
}

var value any
if v, ok := p.values[name]; ok {
value = v
} else {
value = p.defaults[name]
}

return value, true
}

// Set sets new state value for a flag. Does nothing if flag doesn't exist.
func (p *FlagsGroup) Set(name string, value any) {
if !p.exists(name) {
return
}

p.values[name] = value
}

// Unset removes the flag value.
// The default value will be returned during [FlagsGroup.GetAll] if flag is not set.
func (p *FlagsGroup) Unset(name string) {
delete(p.values, name)
}

// GetDefinitions returns [ParametersList] with flags definitions.
func (p *FlagsGroup) GetDefinitions() ParametersList {
return p.definitions
}

// AddDefinitions adds new flag definition.
func (p *FlagsGroup) AddDefinitions(opts ParametersList) {
registered := make(map[string]struct{})

for _, def := range p.definitions {
registered[def.Name] = struct{}{}
}

for _, opt := range opts {
if opt.Name == "" {
panic(fmt.Sprintf("%s flag name cannot be empty", p.name))
}

if _, exists := registered[opt.Name]; exists {
panic(fmt.Sprintf("duplicate %s flag has been detected %s", p.name, opt.Name))
}

p.definitions = append(p.definitions, opt)
}

for _, d := range p.definitions {
p.defaults[d.Name] = d.Default
}
}

// JSONSchema returns JSON schema of a flags group.
func (p *FlagsGroup) JSONSchema() jsonschema.Schema {
opts, optsReq := p.definitions.JSONSchema()
return jsonschema.Schema{
Type: jsonschema.Object,
Required: []string{p.name},
Properties: map[string]any{
p.name: map[string]any{
"type": "object",
"title": p.name,
"properties": opts,
"required": optsReq,
"additionalProperties": false,
},
},
}
}

// ValidateFlags validates input flags.
func (p *FlagsGroup) ValidateFlags(params InputParams) error {
s := p.JSONSchema()
return jsonschema.Validate(s, map[string]any{p.name: params})
}
84 changes: 4 additions & 80 deletions pkg/action/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/launchrctl/launchr/internal/launchr"
"github.com/launchrctl/launchr/pkg/driver"
"github.com/launchrctl/launchr/pkg/jsonschema"
)

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

runtime Runtime // runtime is the [Runtime] to execute the action.
input *Input // input is a storage for arguments and options used in runtime.
processors map[string]ValueProcessor // processors are [ValueProcessor] for manipulating input.
runtime Runtime // runtime is the [Runtime] to execute the action.
input *Input // input is a storage for arguments and options used in runtime.
}

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

// GetProcessors returns processors map.
func (a *Action) GetProcessors() map[string]ValueProcessor {
return a.processors
}

// Reset unsets loaded action to force reload.
func (a *Action) Reset() { a.def = nil }

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

// SetInput saves arguments and options for later processing in run, templates, etc.
func (a *Action) SetInput(input *Input) (err error) {
def := a.ActionDef()

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

// Process options.
err = a.processInputParams(def.Options, input.Opts(), input.OptsChanged(), input)
if err != nil {
return err
}

// Validate the new input.
if err = a.ValidateInput(input); err != nil {
return err
if !input.IsValidated() {
return fmt.Errorf("input is not validated")
}

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

func (a *Action) processInputParams(def ParametersList, inp InputParams, changed InputParams, input *Input) error {
var err error
for _, p := range def {
_, 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,
Input: input,
DefParam: p,
Action: a,
})
if err != nil {
return ErrValueProcessorHandler{
Processor: procDef.ID,
Param: p.Name,
Err: err,
}
}
}
// Cast to []any slice because jsonschema validator supports only this type.
if p.Type == jsonschema.Array {
res = CastSliceTypedToAny(res)
}
// If the value was changed, we can safely override the value.
// If the value was not changed and processed is nil, do not add it.
if isChanged || res != nil {
inp[p.Name] = res
}
}

return nil
}

// ValidateInput validates action input.
func (a *Action) ValidateInput(input *Input) error {
if input.IsValidated() {
return nil
}
argsDefLen := len(a.ActionDef().Arguments)
argsPosLen := len(input.ArgsPositional())
if argsPosLen > argsDefLen {
return fmt.Errorf("accepts %d arg(s), received %d", argsDefLen, argsPosLen)
}
err := validateJSONSchema(a, input)
if err != nil {
return err
}
input.SetValidated(true)
return nil
}

// Execute runs action in the specified environment.
func (a *Action) Execute(ctx context.Context) error {
// @todo maybe it shouldn't be here.
Expand Down
26 changes: 26 additions & 0 deletions pkg/action/action.input.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type Input struct {
args InputParams
// opts contains parsed options with default values.
opts InputParams
// groups contains input parameters grouped by unique name
groups map[string]InputParams
// io contains out/in/err destinations.
io launchr.Streams

Expand All @@ -52,6 +54,7 @@ func NewInput(a *Action, args InputParams, opts InputParams, io launchr.Streams)
argsPos: argsPos,
opts: setParamDefaults(opts, def.Options),
optsRaw: opts,
groups: make(map[string]InputParams),
io: io,
}
}
Expand Down Expand Up @@ -149,6 +152,29 @@ func (input *Input) IsOptChanged(name string) bool {
return ok
}

// GroupFlags returns stored group flags values.
func (input *Input) GroupFlags(group string) InputParams {
if gp, ok := input.groups[group]; ok {
return gp
}
return make(InputParams)
}

// GetFlagInGroup returns a group flag by name.
func (input *Input) GetFlagInGroup(group, name string) any {
return input.GroupFlags(group)[name]
}

// SetFlagInGroup sets group flag value.
func (input *Input) SetFlagInGroup(group, name string, val any) {
gp, ok := input.groups[group]
if !ok {
gp = make(InputParams)
input.groups[group] = gp
}
gp[name] = val
}

// Args returns input named and processed arguments.
func (input *Input) Args() InputParams {
return input.args
Expand Down
6 changes: 4 additions & 2 deletions pkg/action/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ 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.
// because [manager.ValidateInput] decides if the input correct or not.
_, okOpt := act.input.Opts()["opt6"]
assert.True(okOpt)
assert.Equal(inputArgs, act.input.Args())
Expand Down Expand Up @@ -231,6 +231,8 @@ func Test_ActionInputValidate(t *testing.T) {
expErr error
}

am := NewManager()

// Extra input preparation and testing.
setValidatedInput := func(t *testing.T, _ *Action, input *Input) {
input.SetValidated(true)
Expand Down Expand Up @@ -371,7 +373,7 @@ func Test_ActionInputValidate(t *testing.T) {
if tt.fnInit != nil {
tt.fnInit(t, a, input)
}
err := a.ValidateInput(input)
err := am.ValidateInput(a, input)
assert.Equal(t, err == nil, input.IsValidated())
assertIsSameError(t, tt.expErr, err)
})
Expand Down
Loading