unsyncedConfigAccess traverses into the current config and performs the operation at path according to method, using body and out as needed. This is a low-level, unsynchronized function; most callers will want to use changeConfig or readConfig instead. This requires a read or write lock on currentCt
(method, path string, body []byte, out io.Writer)
| 1157 | // read or write lock on currentCtxMu, depending on method (GET needs |
| 1158 | // only a read lock; all others need a write lock). |
| 1159 | func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error { |
| 1160 | var err error |
| 1161 | var val any |
| 1162 | |
| 1163 | // if there is a request body, decode it into the |
| 1164 | // variable that will be set in the config according |
| 1165 | // to method and path |
| 1166 | if len(body) > 0 { |
| 1167 | err = json.Unmarshal(body, &val) |
| 1168 | if err != nil { |
| 1169 | if jsonErr, ok := err.(*json.SyntaxError); ok { |
| 1170 | return fmt.Errorf("decoding request body: %w, at offset %d", jsonErr, jsonErr.Offset) |
| 1171 | } |
| 1172 | return fmt.Errorf("decoding request body: %w", err) |
| 1173 | } |
| 1174 | } |
| 1175 | |
| 1176 | enc := json.NewEncoder(out) |
| 1177 | |
| 1178 | cleanPath := strings.Trim(path, "/") |
| 1179 | if cleanPath == "" { |
| 1180 | return fmt.Errorf("no traversable path") |
| 1181 | } |
| 1182 | |
| 1183 | parts := strings.Split(cleanPath, "/") |
| 1184 | if len(parts) == 0 { |
| 1185 | return fmt.Errorf("path missing") |
| 1186 | } |
| 1187 | |
| 1188 | // A path that ends with "..." implies: |
| 1189 | // 1) the part before it is an array |
| 1190 | // 2) the payload is an array |
| 1191 | // and means that the user wants to expand the elements |
| 1192 | // in the payload array and append each one into the |
| 1193 | // destination array, like so: |
| 1194 | // array = append(array, elems...) |
| 1195 | // This special case is handled below. |
| 1196 | ellipses := parts[len(parts)-1] == "..." |
| 1197 | if ellipses { |
| 1198 | parts = parts[:len(parts)-1] |
| 1199 | } |
| 1200 | |
| 1201 | var ptr any = rawCfg |
| 1202 | |
| 1203 | traverseLoop: |
| 1204 | for i, part := range parts { |
| 1205 | switch v := ptr.(type) { |
| 1206 | case map[string]any: |
| 1207 | // if the next part enters a slice, and the slice is our destination, |
| 1208 | // handle it specially (because appending to the slice copies the slice |
| 1209 | // header, which does not replace the original one like we want) |
| 1210 | if arr, ok := v[part].([]any); ok && i == len(parts)-2 { |
| 1211 | var idx int |
| 1212 | if method != http.MethodPost { |
| 1213 | idxStr := parts[len(parts)-1] |
| 1214 | idx, err = parseCanonicalArrayIndex(idxStr) |
| 1215 | if err != nil { |
| 1216 | return fmt.Errorf("[%s] invalid array index '%s': %v", |