219 lines
5.5 KiB
Go
219 lines
5.5 KiB
Go
package basic
|
||
|
||
import (
|
||
"bufio"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"reflect"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/fatih/color"
|
||
|
||
"github.com/nicholas-fedor/shoutrrr/pkg/format"
|
||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||
)
|
||
|
||
// Errors defined as static variables for better error handling.
|
||
var (
|
||
ErrInvalidConfigType = errors.New("config does not implement types.ServiceConfig")
|
||
ErrInvalidConfigField = errors.New("config field is invalid or nil")
|
||
ErrRequiredFieldMissing = errors.New("field is required and has no default value")
|
||
)
|
||
|
||
// Generator is the Basic Generator implementation for creating service configurations.
|
||
type Generator struct{}
|
||
|
||
// Generate creates a service configuration by prompting the user for field values or using provided properties.
|
||
func (g *Generator) Generate(
|
||
service types.Service,
|
||
props map[string]string,
|
||
_ []string,
|
||
) (types.ServiceConfig, error) {
|
||
configPtr := reflect.ValueOf(service).Elem().FieldByName("Config")
|
||
if !configPtr.IsValid() || configPtr.IsNil() {
|
||
return nil, ErrInvalidConfigField
|
||
}
|
||
|
||
scanner := bufio.NewScanner(os.Stdin)
|
||
if err := g.promptUserForFields(configPtr, props, scanner); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if config, ok := configPtr.Interface().(types.ServiceConfig); ok {
|
||
return config, nil
|
||
}
|
||
|
||
return nil, ErrInvalidConfigType
|
||
}
|
||
|
||
// promptUserForFields iterates over config fields, prompting the user or using props to set values.
|
||
func (g *Generator) promptUserForFields(
|
||
configPtr reflect.Value,
|
||
props map[string]string,
|
||
scanner *bufio.Scanner,
|
||
) error {
|
||
serviceConfig, ok := configPtr.Interface().(types.ServiceConfig)
|
||
if !ok {
|
||
return ErrInvalidConfigType
|
||
}
|
||
|
||
configNode := format.GetConfigFormat(serviceConfig)
|
||
config := configPtr.Elem() // Dereference for setting fields
|
||
|
||
for _, item := range configNode.Items {
|
||
field := item.Field()
|
||
propKey := strings.ToLower(field.Name)
|
||
|
||
for {
|
||
inputValue, err := g.getInputValue(field, propKey, props, scanner)
|
||
if err != nil {
|
||
return err // Propagate the error immediately
|
||
}
|
||
|
||
if valid, err := g.setFieldValue(config, field, inputValue); valid {
|
||
break
|
||
} else if err != nil {
|
||
g.printError(field.Name, err.Error())
|
||
} else {
|
||
g.printInvalidType(field.Name, field.Type.Kind().String())
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// getInputValue retrieves the value for a field from props or user input.
|
||
func (g *Generator) getInputValue(
|
||
field *format.FieldInfo,
|
||
propKey string,
|
||
props map[string]string,
|
||
scanner *bufio.Scanner,
|
||
) (string, error) {
|
||
if propValue, ok := props[propKey]; ok && len(propValue) > 0 {
|
||
_, _ = fmt.Fprint(
|
||
color.Output,
|
||
"Using property ",
|
||
color.HiCyanString(propValue),
|
||
" for ",
|
||
color.HiMagentaString(field.Name),
|
||
" field\n",
|
||
)
|
||
props[propKey] = ""
|
||
|
||
return propValue, nil
|
||
}
|
||
|
||
prompt := g.formatPrompt(field)
|
||
_, _ = fmt.Fprint(color.Output, prompt)
|
||
|
||
if scanner.Scan() {
|
||
input := scanner.Text()
|
||
if len(input) == 0 {
|
||
if len(field.DefaultValue) > 0 {
|
||
return field.DefaultValue, nil
|
||
}
|
||
|
||
if field.Required {
|
||
return "", fmt.Errorf("%s: %w", field.Name, ErrRequiredFieldMissing)
|
||
}
|
||
|
||
return "", nil
|
||
}
|
||
|
||
// More specific type validation
|
||
if field.Type != nil {
|
||
kind := field.Type.Kind()
|
||
if kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 ||
|
||
kind == reflect.Int32 || kind == reflect.Int64 {
|
||
if _, err := strconv.ParseInt(input, 10, field.Type.Bits()); err != nil {
|
||
return "", fmt.Errorf("invalid integer value for %s: %w", field.Name, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
return input, nil
|
||
} else if scanErr := scanner.Err(); scanErr != nil {
|
||
return "", fmt.Errorf("scanner error: %w", scanErr)
|
||
}
|
||
|
||
return field.DefaultValue, nil
|
||
}
|
||
|
||
// formatPrompt creates a user prompt based on the field’s name and default value.
|
||
func (g *Generator) formatPrompt(field *format.FieldInfo) string {
|
||
if len(field.DefaultValue) > 0 {
|
||
return fmt.Sprintf("%s[%s]: ", color.HiWhiteString(field.Name), field.DefaultValue)
|
||
}
|
||
|
||
return color.HiWhiteString(field.Name) + ": "
|
||
}
|
||
|
||
// setFieldValue attempts to set a field’s value and handles required field validation.
|
||
func (g *Generator) setFieldValue(
|
||
config reflect.Value,
|
||
field *format.FieldInfo,
|
||
inputValue string,
|
||
) (bool, error) {
|
||
if len(inputValue) == 0 {
|
||
if field.Required {
|
||
_, _ = fmt.Fprint(
|
||
color.Output,
|
||
"Field ",
|
||
color.HiCyanString(field.Name),
|
||
" is required!\n\n",
|
||
)
|
||
|
||
return false, nil
|
||
}
|
||
|
||
if len(field.DefaultValue) == 0 {
|
||
return true, nil
|
||
}
|
||
|
||
inputValue = field.DefaultValue
|
||
}
|
||
|
||
valid, err := format.SetConfigField(config, *field, inputValue)
|
||
if err != nil {
|
||
return false, fmt.Errorf("failed to set field %s: %w", field.Name, err)
|
||
}
|
||
|
||
return valid, nil
|
||
}
|
||
|
||
// printError displays an error message for an invalid field value.
|
||
func (g *Generator) printError(fieldName, errorMsg string) {
|
||
_, _ = fmt.Fprint(
|
||
color.Output,
|
||
"Invalid format for field ",
|
||
color.HiCyanString(fieldName),
|
||
": ",
|
||
errorMsg,
|
||
"\n\n",
|
||
)
|
||
}
|
||
|
||
// printInvalidType displays a type mismatch error for a field.
|
||
func (g *Generator) printInvalidType(fieldName, typeName string) {
|
||
_, _ = fmt.Fprint(
|
||
color.Output,
|
||
"Invalid type ",
|
||
color.HiYellowString(typeName),
|
||
" for field ",
|
||
color.HiCyanString(fieldName),
|
||
"\n\n",
|
||
)
|
||
}
|
||
|
||
// validateAndReturnConfig ensures the config implements ServiceConfig and returns it.
|
||
func (g *Generator) validateAndReturnConfig(config reflect.Value) (types.ServiceConfig, error) {
|
||
configInterface := config.Interface()
|
||
if serviceConfig, ok := configInterface.(types.ServiceConfig); ok {
|
||
return serviceConfig, nil
|
||
}
|
||
|
||
return nil, ErrInvalidConfigType
|
||
}
|