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 }