Merge changes I3a097fa5,I96538bd0

* changes:
  Deduplicate code in scouting/webserver/requests/debug/cli/main.go
  Deduplicate code in scouting/webserver/requests/requests.go
diff --git a/scouting/webserver/requests/debug/cli/main.go b/scouting/webserver/requests/debug/cli/main.go
index 6f2de1d..f6fb38a 100644
--- a/scouting/webserver/requests/debug/cli/main.go
+++ b/scouting/webserver/requests/debug/cli/main.go
@@ -65,6 +65,18 @@
 	return binaryFb
 }
 
+func maybePerformRequest[T interface{}](fbName, fbsPath, requestJsonPath, address string, requester func(string, []byte) (*T, error)) {
+	if requestJsonPath != "" {
+		log.Printf("Sending %s to %s", fbName, address)
+		binaryRequest := parseJson(fbsPath, requestJsonPath)
+		response, err := requester(address, binaryRequest)
+		if err != nil {
+			log.Fatalf("Failed %s: %v", fbName, err)
+		}
+		spew.Dump(*response)
+	}
+}
+
 func main() {
 	// Parse command line arguments.
 	indentPtr := flag.String("indent", " ",
@@ -86,59 +98,38 @@
 	spew.Config.Indent = *indentPtr
 
 	// Handle the actual arguments.
-	if *submitDataScoutingPtr != "" {
-		log.Printf("Sending SubmitDataScouting to %s", *addressPtr)
-		binaryRequest := parseJson(
-			"scouting/webserver/requests/messages/submit_data_scouting.fbs",
-			*submitDataScoutingPtr)
-		response, err := debug.SubmitDataScouting(*addressPtr, binaryRequest)
-		if err != nil {
-			log.Fatal("Failed SubmitDataScouting: ", err)
-		}
-		spew.Dump(*response)
-	}
-	if *requestAllMatchesPtr != "" {
-		log.Printf("Sending RequestAllMatches to %s", *addressPtr)
-		binaryRequest := parseJson(
-			"scouting/webserver/requests/messages/request_all_matches.fbs",
-			*requestAllMatchesPtr)
-		response, err := debug.RequestAllMatches(*addressPtr, binaryRequest)
-		if err != nil {
-			log.Fatal("Failed RequestAllMatches: ", err)
-		}
-		spew.Dump(*response)
-	}
-	if *requestMatchesForTeamPtr != "" {
-		log.Printf("Sending RequestMatchesForTeam to %s", *addressPtr)
-		binaryRequest := parseJson(
-			"scouting/webserver/requests/messages/request_matches_for_team.fbs",
-			*requestMatchesForTeamPtr)
-		response, err := debug.RequestMatchesForTeam(*addressPtr, binaryRequest)
-		if err != nil {
-			log.Fatal("Failed RequestMatchesForTeam: ", err)
-		}
-		spew.Dump(*response)
-	}
-	if *requestDataScoutingPtr != "" {
-		log.Printf("Sending RequestDataScouting to %s", *addressPtr)
-		binaryRequest := parseJson(
-			"scouting/webserver/requests/messages/request_data_scouting.fbs",
-			*requestDataScoutingPtr)
-		response, err := debug.RequestDataScouting(*addressPtr, binaryRequest)
-		if err != nil {
-			log.Fatal("Failed RequestDataScouting: ", err)
-		}
-		spew.Dump(*response)
-	}
-	if *refreshMatchListPtr != "" {
-		log.Printf("Sending RefreshMatchList to %s", *addressPtr)
-		binaryRequest := parseJson(
-			"scouting/webserver/requests/messages/refresh_match_list.fbs",
-			*refreshMatchListPtr)
-		response, err := debug.RefreshMatchList(*addressPtr, binaryRequest)
-		if err != nil {
-			log.Fatal("Failed RefreshMatchList: ", err)
-		}
-		spew.Dump(*response)
-	}
+	maybePerformRequest(
+		"SubmitDataScouting",
+		"scouting/webserver/requests/messages/submit_data_scouting.fbs",
+		*submitDataScoutingPtr,
+		*addressPtr,
+		debug.SubmitDataScouting)
+
+	maybePerformRequest(
+		"RequestAllMatches",
+		"scouting/webserver/requests/messages/request_all_matches.fbs",
+		*requestAllMatchesPtr,
+		*addressPtr,
+		debug.RequestAllMatches)
+
+	maybePerformRequest(
+		"RequestMatchesForTeam",
+		"scouting/webserver/requests/messages/request_matches_for_team.fbs",
+		*requestMatchesForTeamPtr,
+		*addressPtr,
+		debug.RequestMatchesForTeam)
+
+	maybePerformRequest(
+		"RequestDataScouting",
+		"scouting/webserver/requests/messages/request_data_scouting.fbs",
+		*requestDataScoutingPtr,
+		*addressPtr,
+		debug.RequestDataScouting)
+
+	maybePerformRequest(
+		"RefreshMatchList",
+		"scouting/webserver/requests/messages/refresh_match_list.fbs",
+		*refreshMatchListPtr,
+		*addressPtr,
+		debug.RefreshMatchList)
 }
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index c9a6880..d67f5e9 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -79,16 +79,15 @@
 	respondWithError(w, http.StatusNotImplemented, "")
 }
 
-// TODO(phil): Can we turn this into a generic?
-func parseSubmitDataScouting(w http.ResponseWriter, buf []byte) (*SubmitDataScouting, bool) {
+func parseRequest[T interface{}](w http.ResponseWriter, buf []byte, requestName string, parser func([]byte, flatbuffers.UOffsetT) *T) (*T, bool) {
 	success := true
 	defer func() {
 		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
+			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse %s: %v", requestName, r))
 			success = false
 		}
 	}()
-	result := submit_data_scouting.GetRootAsSubmitDataScouting(buf, 0)
+	result := parser(buf, 0)
 	return result, success
 }
 
@@ -135,7 +134,7 @@
 		return
 	}
 
-	request, success := parseSubmitDataScouting(w, requestBytes)
+	request, success := parseRequest[SubmitDataScouting](w, requestBytes, "SubmitDataScouting", submit_data_scouting.GetRootAsSubmitDataScouting)
 	if !success {
 		return
 	}
@@ -181,19 +180,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-// TODO(phil): Can we turn this into a generic?
-func parseRequestAllMatches(w http.ResponseWriter, buf []byte) (*RequestAllMatches, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
-			success = false
-		}
-	}()
-	result := request_all_matches.GetRootAsRequestAllMatches(buf, 0)
-	return result, success
-}
-
 // Handles a RequestAllMaches request.
 type requestAllMatchesHandler struct {
 	db Database
@@ -206,7 +192,7 @@
 		return
 	}
 
-	_, success := parseRequestAllMatches(w, requestBytes)
+	_, success := parseRequest(w, requestBytes, "RequestAllMatches", request_all_matches.GetRootAsRequestAllMatches)
 	if !success {
 		return
 	}
@@ -237,19 +223,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-// TODO(phil): Can we turn this into a generic?
-func parseRequestMatchesForTeam(w http.ResponseWriter, buf []byte) (*RequestMatchesForTeam, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
-			success = false
-		}
-	}()
-	result := request_matches_for_team.GetRootAsRequestMatchesForTeam(buf, 0)
-	return result, success
-}
-
 // Handles a RequestMatchesForTeam request.
 type requestMatchesForTeamHandler struct {
 	db Database
@@ -262,7 +235,7 @@
 		return
 	}
 
-	request, success := parseRequestMatchesForTeam(w, requestBytes)
+	request, success := parseRequest(w, requestBytes, "RequestMatchesForTeam", request_matches_for_team.GetRootAsRequestMatchesForTeam)
 	if !success {
 		return
 	}
@@ -293,19 +266,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-// TODO(phil): Can we turn this into a generic?
-func parseRequestDataScouting(w http.ResponseWriter, buf []byte) (*RequestDataScouting, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
-			success = false
-		}
-	}()
-	result := request_data_scouting.GetRootAsRequestDataScouting(buf, 0)
-	return result, success
-}
-
 // Handles a RequestDataScouting request.
 type requestDataScoutingHandler struct {
 	db Database
@@ -318,7 +278,7 @@
 		return
 	}
 
-	_, success := parseRequestDataScouting(w, requestBytes)
+	_, success := parseRequest(w, requestBytes, "RequestDataScouting", request_data_scouting.GetRootAsRequestDataScouting)
 	if !success {
 		return
 	}
@@ -359,19 +319,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-// TODO(phil): Can we turn this into a generic?
-func parseRefreshMatchList(w http.ResponseWriter, buf []byte) (*RefreshMatchList, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
-			success = false
-		}
-	}()
-	result := refresh_match_list.GetRootAsRefreshMatchList(buf, 0)
-	return result, success
-}
-
 func parseTeamKey(teamKey string) (int, error) {
 	// TBA prefixes teams with "frc". Not sure why. Get rid of that.
 	teamKey = strings.TrimPrefix(teamKey, "frc")
@@ -422,7 +369,7 @@
 		return
 	}
 
-	request, success := parseRefreshMatchList(w, requestBytes)
+	request, success := parseRequest(w, requestBytes, "RefreshMatchList", refresh_match_list.GetRootAsRefreshMatchList)
 	if !success {
 		return
 	}
@@ -467,18 +414,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-func parseSubmitNotes(w http.ResponseWriter, buf []byte) (*SubmitNotes, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
-			success = false
-		}
-	}()
-	result := submit_notes.GetRootAsSubmitNotes(buf, 0)
-	return result, success
-}
-
 type submitNoteScoutingHandler struct {
 	db Database
 }
@@ -490,7 +425,7 @@
 		return
 	}
 
-	request, success := parseSubmitNotes(w, requestBytes)
+	request, success := parseRequest(w, requestBytes, "SubmitNotes", submit_notes.GetRootAsSubmitNotes)
 	if !success {
 		return
 	}
@@ -510,18 +445,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-func parseRequestNotesForTeam(w http.ResponseWriter, buf []byte) (*RequestNotesForTeam, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
-			success = false
-		}
-	}()
-	result := request_notes_for_team.GetRootAsRequestNotesForTeam(buf, 0)
-	return result, success
-}
-
 type requestNotesForTeamHandler struct {
 	db Database
 }
@@ -533,7 +456,7 @@
 		return
 	}
 
-	request, success := parseRequestNotesForTeam(w, requestBytes)
+	request, success := parseRequest(w, requestBytes, "RequestNotesForTeam", request_notes_for_team.GetRootAsRequestNotesForTeam)
 	if !success {
 		return
 	}