pkg/inspection/form/textform.go (184 lines of code) (raw):
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package form
import (
"context"
"fmt"
"github.com/GoogleCloudPlatform/khi/pkg/common/khictx"
"github.com/GoogleCloudPlatform/khi/pkg/common/typedmap"
inspection_task_contextkey "github.com/GoogleCloudPlatform/khi/pkg/inspection/contextkey"
inspection_task_interface "github.com/GoogleCloudPlatform/khi/pkg/inspection/interface"
form_metadata "github.com/GoogleCloudPlatform/khi/pkg/inspection/metadata/form"
"github.com/GoogleCloudPlatform/khi/pkg/inspection/task/label"
common_task "github.com/GoogleCloudPlatform/khi/pkg/task"
"github.com/GoogleCloudPlatform/khi/pkg/task/taskid"
)
// TextFormValidator is a function to check if the given value is valid or not.
// Returns "" as the result when it has no error, otherwise the returned value is used as an error message on frontend.
// Returning an error as the 2nd returning value is only when the validator detects an unrecoverble error.
type TextFormValidator = func(ctx context.Context, value string) (string, error)
// TextFormDefaultValueGenerator is a function type to generate the default value.
type TextFormDefaultValueGenerator = func(ctx context.Context, previousValues []string) (string, error)
// TextFormReadonlyProvider is a function type to compute if the field is allowed edit or not.
type TextFormReadonlyProvider = func(ctx context.Context) (bool, error)
// TextFormSuggestionsProvider is a function to return the list of strings shown in the autocomplete.
// Return nil instead of emptry string array means the autocomplete is disabled for the field.
type TextFormSuggestionsProvider = func(ctx context.Context, value string, previousValues []string) ([]string, error)
// TextFormValueConverter is a function type to convert the given string value to another type stored in the variable set.
type TextFormValueConverter[T any] = func(ctx context.Context, value string) (T, error)
// TextFormHintGenerator is a function type to generate a hint string
type TextFormHintGenerator = func(ctx context.Context, value string, convertedValue any) (string, form_metadata.ParameterHintType, error)
// TextFormTaskBuilder is an utility to construct an instance of task for input form field.
// This will generate the task instance with `Build()` method call after chaining several configuration methods.
type TextFormTaskBuilder[T any] struct {
FormTaskBuilderBase[T]
defaultValue TextFormDefaultValueGenerator
validator TextFormValidator
readonlyProvider TextFormReadonlyProvider
suggestionsProvider TextFormSuggestionsProvider
hintGenerator TextFormHintGenerator
converter TextFormValueConverter[T]
}
// NewTextFormTaskBuilder constructs an instace of TextFormDefinitionBuilder.
// id,prioirity and label will be initialized with the value given in the argument. The other values are initialized with the following values.
// dependencies : Initialized with an empty string array indicating this task is not depending on anything.
// description: Initialized with an empty string.
// defaultValue: Initialized with a function to return empty string.
// validator: Initialized with a function to return empty string that indicates the validation is always passing.
// allowEditProvider: Initialized with a function to return true.
// suggestionsProvider: Initialized with a function to return nil.
// converter: Initialized with a function to return the given value. This means no conversion applied and treated as a string.
func NewTextFormTaskBuilder[T any](id taskid.TaskImplementationID[T], priority int, fieldLabel string) *TextFormTaskBuilder[T] {
return &TextFormTaskBuilder[T]{
FormTaskBuilderBase: NewFormTaskBuilderBase(id, priority, fieldLabel),
defaultValue: func(ctx context.Context, previousValues []string) (string, error) {
return "", nil
},
validator: func(ctx context.Context, value string) (string, error) {
return "", nil
},
readonlyProvider: func(ctx context.Context) (bool, error) {
return false, nil
},
suggestionsProvider: func(ctx context.Context, value string, previousValues []string) ([]string, error) {
return nil, nil
},
converter: func(ctx context.Context, value string) (T, error) {
var anyValue any = value // This is needed for forcible cast from string to T.
return anyValue.(T), nil
},
hintGenerator: func(ctx context.Context, value string, convertedValue any) (string, form_metadata.ParameterHintType, error) {
return "", form_metadata.Info, nil
},
}
}
func (b *TextFormTaskBuilder[T]) WithDependencies(dependencies []taskid.UntypedTaskReference) *TextFormTaskBuilder[T] {
b.FormTaskBuilderBase.WithDependencies(dependencies)
return b
}
func (b *TextFormTaskBuilder[T]) WithDescription(description string) *TextFormTaskBuilder[T] {
b.FormTaskBuilderBase.WithDescription(description)
return b
}
func (b *TextFormTaskBuilder[T]) WithValidator(validator TextFormValidator) *TextFormTaskBuilder[T] {
b.validator = validator
return b
}
func (b *TextFormTaskBuilder[T]) WithDefaultValueFunc(defFunc TextFormDefaultValueGenerator) *TextFormTaskBuilder[T] {
b.defaultValue = defFunc
return b
}
func (b *TextFormTaskBuilder[T]) WithDefaultValueConstant(defValue string, preferPrevValue bool) *TextFormTaskBuilder[T] {
return b.WithDefaultValueFunc(func(ctx context.Context, previousValues []string) (string, error) {
if preferPrevValue {
if len(previousValues) > 0 {
return previousValues[0], nil
}
}
return defValue, nil
})
}
func (b *TextFormTaskBuilder[T]) WithReadonlyFunc(readonlyFunc TextFormReadonlyProvider) *TextFormTaskBuilder[T] {
b.readonlyProvider = readonlyFunc
return b
}
func (b *TextFormTaskBuilder[T]) WithSuggestionsFunc(suggestionsFunc TextFormSuggestionsProvider) *TextFormTaskBuilder[T] {
b.suggestionsProvider = suggestionsFunc
return b
}
func (b *TextFormTaskBuilder[T]) WithSuggestionsConstant(suggestions []string) *TextFormTaskBuilder[T] {
return b.WithSuggestionsFunc(func(ctx context.Context, value string, previousValues []string) ([]string, error) {
return suggestions, nil
})
}
func (b *TextFormTaskBuilder[T]) WithHintFunc(hintFunc TextFormHintGenerator) *TextFormTaskBuilder[T] {
b.hintGenerator = hintFunc
return b
}
func (b *TextFormTaskBuilder[T]) WithConverter(converter TextFormValueConverter[T]) *TextFormTaskBuilder[T] {
b.converter = converter
return b
}
func (b *TextFormTaskBuilder[T]) Build(labelOpts ...common_task.LabelOpt) common_task.Task[T] {
return common_task.NewTask(b.id, b.dependencies, func(ctx context.Context) (T, error) {
m := khictx.MustGetValue(ctx, inspection_task_contextkey.InspectionRunMetadata)
req := khictx.MustGetValue(ctx, inspection_task_contextkey.InspectionTaskInput)
taskMode := khictx.MustGetValue(ctx, inspection_task_contextkey.InspectionTaskMode)
globalSharedMap := khictx.MustGetValue(ctx, inspection_task_contextkey.GlobalSharedMap)
previousValueStoreKey := typedmap.NewTypedKey[[]string](fmt.Sprintf("text-form-pv-%s", b.id))
prevValue := typedmap.GetOrDefault(globalSharedMap, previousValueStoreKey, []string{})
readonly, err := b.readonlyProvider(ctx)
if err != nil {
return *new(T), fmt.Errorf("allowEdit provider for task `%s` returned an error\n%v", b.id, err)
}
field := form_metadata.TextParameterFormField{}
field.Readonly = readonly
// Compute the default value of the form
var currentValue string
defaultValue, err := b.defaultValue(ctx, prevValue)
if err != nil {
return *new(T), fmt.Errorf("default value generator for task `%s` returned an error\n%v", b.id, err)
}
field.Default = defaultValue
currentValue = defaultValue
if valueRaw, exist := req[b.id.ReferenceIDString()]; exist && !readonly {
valueString, isString := valueRaw.(string)
if !isString {
return *new(T), fmt.Errorf("request parameter `%s` was not given in string in task %s", b.id, b.id)
}
currentValue = valueString
}
field.Type = form_metadata.Text
field.HintType = form_metadata.Info
b.SetupBaseFormField(&field.ParameterFormFieldBase)
suggestions, err := b.suggestionsProvider(ctx, currentValue, prevValue)
if err != nil {
return *new(T), fmt.Errorf("suggesion provider for task `%s` returned an error\n%v", b.id, err)
}
field.Suggestions = suggestions
validationErr, err := b.validator(ctx, currentValue)
if err != nil {
return *new(T), fmt.Errorf("validator for task `%s` returned an unrecovable error\n%v", b.id, err)
}
if validationErr != "" {
// When the given string is invalid, it should be the default value.
currentValue, err = b.defaultValue(ctx, prevValue)
if err != nil {
return *new(T), fmt.Errorf("default value generator for task `%s` returned an error\n%v", b.id, err)
}
}
if validationErr != "" && taskMode == inspection_task_interface.TaskModeRun {
return *new(T), fmt.Errorf("validator for task `%s` returned a validation error. But this task was executed as a Run mode not in DryRun. All validations must be resolved before running.\n%v", b.id, validationErr)
}
convertedValue, err := b.converter(ctx, currentValue)
if err != nil {
return *new(T), fmt.Errorf("failed to convert the value `%s` to the dedicated value in task %s\n%v", currentValue, b.id, err)
}
if validationErr != "" {
field.HintType = form_metadata.Error
field.Hint = validationErr
} else {
hint, hintType, err := b.hintGenerator(ctx, currentValue, convertedValue)
if err != nil {
return *new(T), fmt.Errorf("failed to generate a hint for task %s\n%v", b.id, err)
}
if hint == "" {
hintType = form_metadata.None
}
field.Hint = hint
field.HintType = hintType
if taskMode == inspection_task_interface.TaskModeRun {
newValueHistory := append([]string{currentValue}, prevValue...)
typedmap.Set(globalSharedMap, previousValueStoreKey, newValueHistory)
}
}
formFields, found := typedmap.Get(m, form_metadata.FormFieldSetMetadataKey)
if !found {
return *new(T), fmt.Errorf("form field set was not found in the metadata set")
}
err = formFields.SetField(field)
if err != nil {
return *new(T), fmt.Errorf("failed to configure the form metadata in task `%s`\n%v", b.id, err)
}
return convertedValue, nil
}, append(labelOpts, label.NewFormTaskLabelOpt(
b.label,
b.description,
))...)
}