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 }