scouting: Add support /requests/request/all_matches

This patch adds a new endpoint that accepts the `RequestAllMatches`
message. It simply returns the full list of matches that the database
knows about.

I decided to change public `int` members in the `db` module to `int32`
so they match the flatbuffer definition. This makes comparison
simpler.

Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: I9bb2eed020e2889644f5a122105a232a68f2f4bd
diff --git a/scouting/webserver/requests/BUILD b/scouting/webserver/requests/BUILD
index 15e4c05..14cf97f 100644
--- a/scouting/webserver/requests/BUILD
+++ b/scouting/webserver/requests/BUILD
@@ -9,6 +9,8 @@
     deps = [
         "//scouting/db",
         "//scouting/webserver/requests/messages:error_response_go_fbs",
+        "//scouting/webserver/requests/messages:request_all_matches_go_fbs",
+        "//scouting/webserver/requests/messages:request_all_matches_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
         "//scouting/webserver/server",
@@ -23,7 +25,10 @@
     target_compatible_with = ["@platforms//cpu:x86_64"],
     deps = [
         "//scouting/db",
+        "//scouting/webserver/requests/debug",
         "//scouting/webserver/requests/messages:error_response_go_fbs",
+        "//scouting/webserver/requests/messages:request_all_matches_go_fbs",
+        "//scouting/webserver/requests/messages:request_all_matches_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
         "//scouting/webserver/server",
diff --git a/scouting/webserver/requests/debug/BUILD b/scouting/webserver/requests/debug/BUILD
index cfeee2f..189296d 100644
--- a/scouting/webserver/requests/debug/BUILD
+++ b/scouting/webserver/requests/debug/BUILD
@@ -8,6 +8,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//scouting/webserver/requests/messages:error_response_go_fbs",
+        "//scouting/webserver/requests/messages:request_all_matches_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
     ],
 )
diff --git a/scouting/webserver/requests/debug/cli/cli_test.py b/scouting/webserver/requests/debug/cli/cli_test.py
index 87c22c0..8020c64 100644
--- a/scouting/webserver/requests/debug/cli/cli_test.py
+++ b/scouting/webserver/requests/debug/cli/cli_test.py
@@ -1,3 +1,5 @@
+# TODO(phil): Rewrite this in Go.
+
 import json
 import os
 from pathlib import Path
@@ -67,5 +69,14 @@
         self.assertEqual(exit_code, 1)
         self.assertIn("/requests/submit/data_scouting returned 501 Not Implemented", stderr)
 
+    def test_request_all_matches(self):
+        # RequestAllMatches has no fields.
+        json_path = write_json({})
+        exit_code, _stdout, stderr = run_debug_cli(["-requestAllMatches", json_path])
+
+        # TODO(phil): Actually add some matches here.
+        self.assertEqual(exit_code, 0)
+        self.assertIn("{MatchList:[]}", stderr)
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/scouting/webserver/requests/debug/cli/main.go b/scouting/webserver/requests/debug/cli/main.go
index ac24b25..204e541 100644
--- a/scouting/webserver/requests/debug/cli/main.go
+++ b/scouting/webserver/requests/debug/cli/main.go
@@ -50,9 +50,9 @@
 	// Execute the `flatc` command.
 	flatcCommand := exec.Command(absFlatcPath, "--binary", absFbsPath, jsonSymlink)
 	flatcCommand.Dir = dir
-	err = flatcCommand.Run()
+	output, err := flatcCommand.CombinedOutput()
 	if err != nil {
-		log.Fatal("Failed to execute flatc: ", err)
+		log.Fatal("Failed to execute flatc: ", err, ": ", string(output))
 	}
 
 	// Read the serialized flatbuffer and return it.
@@ -70,6 +70,8 @@
 		"The end point where the server is listening.")
 	submitDataScoutingPtr := flag.String("submitDataScouting", "",
 		"If specified, parse the file as a SubmitDataScouting JSON request.")
+	requestAllMatchesPtr := flag.String("requestAllMatches", "",
+		"If specified, parse the file as a RequestAllMatches JSON request.")
 	flag.Parse()
 
 	// Handle the actual arguments.
@@ -82,6 +84,17 @@
 		if err != nil {
 			log.Fatal("Failed SubmitDataScouting: ", err)
 		}
-		log.Printf("%+v", response)
+		log.Printf("%+v", *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)
+		}
+		log.Printf("%+v", *response)
 	}
 }
diff --git a/scouting/webserver/requests/debug/debug.go b/scouting/webserver/requests/debug/debug.go
index dd70c1b..5984c7a 100644
--- a/scouting/webserver/requests/debug/debug.go
+++ b/scouting/webserver/requests/debug/debug.go
@@ -9,11 +9,13 @@
 	"net/http"
 
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
 )
 
 // Use aliases to make the rest of the code more readable.
 type SubmitDataScoutingResponseT = submit_data_scouting_response.SubmitDataScoutingResponseT
+type RequestAllMatchesResponseT = request_all_matches_response.RequestAllMatchesResponseT
 
 // A struct that can be used as an `error`. It contains information about the
 // why the server was unhappy and what the corresponding request was.
@@ -85,3 +87,15 @@
 	response := submit_data_scouting_response.GetRootAsSubmitDataScoutingResponse(responseBytes, 0)
 	return response.UnPack(), nil
 }
+
+// Sends a `RequestAllMatches` message to the server and returns the
+// deserialized response.
+func RequestAllMatches(server string, requestBytes []byte) (*RequestAllMatchesResponseT, error) {
+	responseBytes, err := performPost(server+"/requests/request/all_matches", requestBytes)
+	if err != nil {
+		return nil, err
+	}
+	log.Printf("Parsing RequestAllMatchesResponse")
+	response := request_all_matches_response.GetRootAsRequestAllMatchesResponse(responseBytes, 0)
+	return response.UnPack(), nil
+}
diff --git a/scouting/webserver/requests/messages/request_all_matches.fbs b/scouting/webserver/requests/messages/request_all_matches.fbs
index 1d4acc2..2ec9aa1 100644
--- a/scouting/webserver/requests/messages/request_all_matches.fbs
+++ b/scouting/webserver/requests/messages/request_all_matches.fbs
@@ -1,6 +1,6 @@
 namespace scouting.webserver.requests;
 
-table RequestMatchList {
+table RequestAllMatches {
 }
 
-root_type RequestMatchList;
\ No newline at end of file
+root_type RequestAllMatches;
diff --git a/scouting/webserver/requests/messages/request_all_matches_response.fbs b/scouting/webserver/requests/messages/request_all_matches_response.fbs
index d4a1658..90401e3 100644
--- a/scouting/webserver/requests/messages/request_all_matches_response.fbs
+++ b/scouting/webserver/requests/messages/request_all_matches_response.fbs
@@ -12,8 +12,8 @@
     b3:int (id: 8);
 }
 
-table RequestMatchListResponse  {
+table RequestAllMatchesResponse  {
     match_list:[Match] (id:0);
 }
 
-root_type RequestMatchListResponse;
\ No newline at end of file
+root_type RequestAllMatchesResponse;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 2462ccb..0a28ca0 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -7,12 +7,18 @@
 
 	"github.com/frc971/971-Robot-Code/scouting/db"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
 	_ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
 	flatbuffers "github.com/google/flatbuffers/go"
 )
 
+type SubmitDataScouting = submit_data_scouting.SubmitDataScouting
+type RequestAllMatches = request_all_matches.RequestAllMatches
+type RequestAllMatchesResponseT = request_all_matches_response.RequestAllMatchesResponseT
+
 // The interface we expect the database abstraction to conform to.
 // We use an interface here because it makes unit testing easier.
 type Database interface {
@@ -43,7 +49,7 @@
 }
 
 // TODO(phil): Can we turn this into a generic?
-func parseSubmitDataScouting(w http.ResponseWriter, buf []byte) (*submit_data_scouting.SubmitDataScouting, bool) {
+func parseSubmitDataScouting(w http.ResponseWriter, buf []byte) (*SubmitDataScouting, bool) {
 	success := true
 	defer func() {
 		if r := recover(); r != nil {
@@ -79,7 +85,63 @@
 	respondNotImplemented(w)
 }
 
+// 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
+}
+
+func (handler requestAllMatchesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	requestBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
+		return
+	}
+
+	_, success := parseRequestAllMatches(w, requestBytes)
+	if !success {
+		return
+	}
+
+	matches, err := handler.db.ReturnMatches()
+	if err != nil {
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
+	}
+
+	var response RequestAllMatchesResponseT
+	for _, match := range matches {
+		response.MatchList = append(response.MatchList, &request_all_matches_response.MatchT{
+			MatchNumber: match.MatchNumber,
+			Round:       match.Round,
+			CompLevel:   match.CompLevel,
+			R1:          match.R1,
+			R2:          match.R2,
+			R3:          match.R3,
+			B1:          match.B1,
+			B2:          match.B2,
+			B3:          match.B3,
+		})
+	}
+
+	builder := flatbuffers.NewBuilder(50 * 1024)
+	builder.Finish((&response).Pack(builder))
+	w.Write(builder.FinishedBytes())
+}
+
 func HandleRequests(db Database, scoutingServer server.ScoutingServer) {
 	scoutingServer.HandleFunc("/requests", unknown)
 	scoutingServer.Handle("/requests/submit/data_scouting", submitDataScoutingHandler{db})
+	scoutingServer.Handle("/requests/request/all_matches", requestAllMatchesHandler{db})
 }
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index 0125da5..26256be 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -4,10 +4,14 @@
 	"bytes"
 	"io"
 	"net/http"
+	"reflect"
 	"testing"
 
 	"github.com/frc971/971-Robot-Code/scouting/db"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/debug"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/error_response"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches"
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_all_matches_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting"
 	_ "github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
@@ -92,10 +96,71 @@
 	// TODO(phil): Can we use scouting/webserver/requests/debug here?
 }
 
+// Validates that we can request the full match list.
+func TestRequestAllMatches(t *testing.T) {
+	db := MockDatabase{
+		matches: []db.Match{
+			{
+				MatchNumber: 1, Round: 1, CompLevel: "qual",
+				R1: 5, R2: 42, R3: 600, B1: 971, B2: 400, B3: 200,
+			},
+			{
+				MatchNumber: 2, Round: 1, CompLevel: "qual",
+				R1: 6, R2: 43, R3: 601, B1: 972, B2: 401, B3: 201,
+			},
+			{
+				MatchNumber: 3, Round: 1, CompLevel: "qual",
+				R1: 7, R2: 44, R3: 602, B1: 973, B2: 402, B3: 202,
+			},
+		},
+	}
+	scoutingServer := server.NewScoutingServer()
+	HandleRequests(&db, scoutingServer)
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	builder := flatbuffers.NewBuilder(1024)
+	builder.Finish((&request_all_matches.RequestAllMatchesT{}).Pack(builder))
+
+	response, err := debug.RequestAllMatches("http://localhost:8080", builder.FinishedBytes())
+	if err != nil {
+		t.Fatal("Failed to request all matches: ", err)
+	}
+
+	expected := request_all_matches_response.RequestAllMatchesResponseT{
+		MatchList: []*request_all_matches_response.MatchT{
+			// MatchNumber, Round, CompLevel
+			// R1, R2, R3, B1, B2, B3
+			{
+				1, 1, "qual",
+				5, 42, 600, 971, 400, 200,
+			},
+			{
+				2, 1, "qual",
+				6, 43, 601, 972, 401, 201,
+			},
+			{
+				3, 1, "qual",
+				7, 44, 602, 973, 402, 202,
+			},
+		},
+	}
+	if len(expected.MatchList) != len(response.MatchList) {
+		t.Fatal("Expected ", expected, ", but got ", *response)
+	}
+	for i, match := range expected.MatchList {
+		if !reflect.DeepEqual(*match, *response.MatchList[i]) {
+			t.Fatal("Expected for match", i, ":", *match, ", but got:", *response.MatchList[i])
+		}
+	}
+}
+
 // A mocked database we can use for testing. Add functionality to this as
 // needed for your tests.
 
-type MockDatabase struct{}
+type MockDatabase struct {
+	matches []db.Match
+}
 
 func (database *MockDatabase) AddToMatch(db.Match) error {
 	return nil
@@ -106,7 +171,7 @@
 }
 
 func (database *MockDatabase) ReturnMatches() ([]db.Match, error) {
-	return []db.Match{}, nil
+	return database.matches, nil
 }
 
 func (database *MockDatabase) ReturnStats() ([]db.Stats, error) {