golift.io/starr@v1.0.0/starrcmd/parser.go (about)

     1  // Package starrcmd provides the bindings to consume a custom script command hook from any Starr app.
     2  // Create these by going into Settings->Connect->Custom Script in Lidarr, Prowlarr, Radarr, Readarr, or Sonarr.
     3  // See the included example_test.go file for examples on how to use this module.
     4  package starrcmd
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"reflect"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  )
    14  
    15  // get offloads the error checking from all the other routines.
    16  // This is where our journey into the data truly begins.
    17  func (c *CmdEvent) get(wanted Event, output interface{}) error {
    18  	if c.Type != wanted {
    19  		return fmt.Errorf("%w: requested '%s' have '%s'", ErrInvalidEvent, wanted, c.Type)
    20  	}
    21  
    22  	if err := fillStructFromEnv(output); err != nil {
    23  		return fmt.Errorf("reading environment: %w", err)
    24  	}
    25  
    26  	return nil
    27  }
    28  
    29  // This does not traverse structs and will only stay on normal members.
    30  func fillStructFromEnv(dataStruct interface{}) error {
    31  	field := reflect.ValueOf(dataStruct)
    32  	if field.Kind() != reflect.Ptr || field.Elem().Kind() != reflect.Struct {
    33  		panic("yuh dun ate in sumthin bahd! This is a bug in the starrcmd library.")
    34  	}
    35  
    36  	t := field.Type().Elem()
    37  	for idx := 0; idx < t.NumField(); idx++ { // Loop each struct member
    38  		split := strings.SplitN(t.Field(idx).Tag.Get("env"), ",", 2) //nolint:gomnd
    39  
    40  		tag := strings.ToLower(split[0]) // lower to protect naming mistakes.
    41  		if !field.Elem().Field(idx).CanSet() || tag == "-" || tag == "" {
    42  			continue // This only works with non-empty reflection tags on exported members.
    43  		}
    44  
    45  		// If the tag has a comma, the value that follows is used to split strings into []string.
    46  		var splitVal string
    47  		if len(split) == 2 { //nolint:gomnd
    48  			splitVal = split[1]
    49  		}
    50  
    51  		value := os.Getenv(tag)
    52  		if value == "" {
    53  			// fmt.Println("skipping", tag)
    54  			continue
    55  		}
    56  
    57  		err := parseStructMember(field.Elem().Field(idx), value, splitVal)
    58  		if err != nil {
    59  			return fmt.Errorf("%s: (%s) %w", tag, os.Getenv(tag), err)
    60  		}
    61  	}
    62  
    63  	return nil
    64  }
    65  
    66  /* All of the code below was taken from the golift.io/cnfg module. */
    67  
    68  // This is trimmed and does not parse some types.
    69  func parseStructMember(field reflect.Value, value, splitVal string) error { //nolint:cyclop
    70  	var err error
    71  
    72  	switch fieldType := field.Type().String(); fieldType {
    73  	// Handle each member type appropriately (differently).
    74  	case "string":
    75  		// SetString is a reflect package method to update a struct member by index.
    76  		field.SetString(value)
    77  	case "int", "int64":
    78  		var val int64
    79  
    80  		val, err = parseInt(fieldType, value)
    81  		field.SetInt(val)
    82  		/*
    83  			case "float64":
    84  				// uncomment float64 if needed.
    85  				var val float64
    86  				//nolint:gomnd
    87  				val, err = strconv.ParseFloat(value, 64)
    88  				field.SetFloat(val)
    89  			case "time.Duration":
    90  				// this needs to be fixed to work with any duration values we find in starr apps.
    91  				var val time.Duration
    92  
    93  				val, err = time.ParseDuration(value)
    94  				field.Set(reflect.ValueOf(val))
    95  		*/
    96  	case "time.Time":
    97  		var val time.Time
    98  
    99  		if val, err = time.Parse(DateFormat, value); err != nil {
   100  			var err2 error
   101  			if val, err2 = time.Parse(DateFormat2, value); err2 != nil {
   102  				err = fmt.Errorf("error1: %v, error2: %w", err, err2) //nolint:errorlint
   103  			} else {
   104  				err = nil
   105  			}
   106  		}
   107  
   108  		field.Set(reflect.ValueOf(val))
   109  	case "bool":
   110  		var val bool
   111  
   112  		val, err = strconv.ParseBool(value)
   113  		field.SetBool(val)
   114  	default:
   115  		if missing, err := parseSlices(field, value, splitVal); err != nil {
   116  			return fmt.Errorf("%s: %w", value, err)
   117  		} else if missing {
   118  			panic(fmt.Sprintf("invalid type provided to parser, this is a bug in the starrcmd library: %s (%s)",
   119  				fieldType, value))
   120  		}
   121  	}
   122  
   123  	if err != nil {
   124  		return fmt.Errorf("%s: %w", value, err)
   125  	}
   126  
   127  	return nil
   128  }
   129  
   130  func parseSlices(field reflect.Value, value, splitVal string) (bool, error) { //nolint:cyclop
   131  	if splitVal == "" {
   132  		// this will trigger a panic() if you forget a splitVal on an env tag.
   133  		return true, nil
   134  	}
   135  
   136  	var err error
   137  
   138  	switch fieldType := field.Type().String(); fieldType {
   139  	default:
   140  		return true, nil
   141  	case "[]time.Time":
   142  		split := strings.Split(value, splitVal)
   143  		vals := make([]time.Time, len(split))
   144  
   145  		for idx, val := range split {
   146  			if vals[idx], err = time.Parse(DateFormat, val); err != nil {
   147  				if err != nil {
   148  					var err2 error
   149  					if vals[idx], err2 = time.Parse(DateFormat2, value); err2 != nil {
   150  						return false, fmt.Errorf("error1: %v, error2: %w", err, err2) //nolint:errorlint
   151  					}
   152  
   153  					err = nil
   154  				}
   155  			}
   156  		}
   157  
   158  		field.Set(reflect.ValueOf(vals))
   159  	case "[]int":
   160  		split := strings.Split(value, splitVal)
   161  		vals := make([]int, len(split))
   162  
   163  		for idx, val := range split {
   164  			if vals[idx], err = strconv.Atoi(val); err != nil {
   165  				return false, fmt.Errorf("%s: %w", value, err)
   166  			}
   167  		}
   168  
   169  		field.Set(reflect.ValueOf(vals))
   170  	case "[]int64":
   171  		split := strings.Split(value, splitVal)
   172  		vals := make([]int64, len(split))
   173  
   174  		for idx, val := range split {
   175  			if vals[idx], err = parseInt(fieldType, val); err != nil {
   176  				return false, fmt.Errorf("%s: %w", value, err)
   177  			}
   178  		}
   179  
   180  		field.Set(reflect.ValueOf(vals))
   181  	case "[]string":
   182  		vals := strings.Split(value, splitVal)
   183  		field.Set(reflect.ValueOf(vals))
   184  	}
   185  
   186  	return false, err
   187  }
   188  
   189  // parseInt parses an integer from a string as specific size.
   190  // If you need int8, 16 or 32, add them...
   191  func parseInt(intType, envval string) (int64, error) {
   192  	var (
   193  		val int64
   194  		err error
   195  	)
   196  
   197  	//nolint:gomnd,nolintlint
   198  	switch intType {
   199  	default:
   200  		val, err = strconv.ParseInt(envval, 10, 0)
   201  	case "int64":
   202  		val, err = strconv.ParseInt(envval, 10, 64)
   203  	}
   204  
   205  	if err != nil { // this error may prove to suck...
   206  		return val, fmt.Errorf("parsing integer: %w", err)
   207  	}
   208  
   209  	return val, nil
   210  }