scouting: Add a debug CLI for the webserver

This CLI should let us debug the webserver easily. It simulates calls
that the web page can make. Create a JSON version of the flatbuffer
message you want to send and pass it as a file to the correct option
on the `cli` binary. There's nothing really implemented yet because
the webserver doesn't have a whole lot implemented either.

Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: If396dd8dc3b1e24515cb2d5765b3d5f233066cda
diff --git a/scouting/webserver/requests/debug/cli/main.go b/scouting/webserver/requests/debug/cli/main.go
new file mode 100644
index 0000000..ac24b25
--- /dev/null
+++ b/scouting/webserver/requests/debug/cli/main.go
@@ -0,0 +1,87 @@
+// This binary lets users interact with the scouting web server in order to
+// debug it. Run with `--help` to see all the options.
+
+package main
+
+import (
+	"flag"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+
+	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/debug"
+)
+
+// Returns the absolute path of the specified path. This is an unwrapped
+// version of `filepath.Abs`.
+func absPath(path string) string {
+	result, err := filepath.Abs(path)
+	if err != nil {
+		log.Fatal("Failed to determine absolute path for ", path, ": ", err)
+	}
+	return result
+}
+
+// Parses the specified JSON file into a binary version (i.e. serialized
+// flatbuffer). This uses the `flatc` binary and the JSON's corresponding
+// `.fbs` file.
+func parseJson(fbsPath string, jsonPath string) []byte {
+	// Work inside a temporary directory since `flatc` doesn't allow us to
+	// customize the name of the output file.
+	dir, err := ioutil.TempDir("", "webserver_debug_cli")
+	if err != nil {
+		log.Fatal("Failed to create temporary directory: ", err)
+	}
+	defer os.RemoveAll(dir)
+
+	// Turn these paths absolute so that it everything still works from
+	// inside the temporary directory.
+	absFlatcPath := absPath("external/com_github_google_flatbuffers/flatc")
+	absFbsPath := absPath(fbsPath)
+
+	// Create a symlink to the .fbs file so that the output filename that
+	// `flatc` generates is predictable. I.e. `fb.json` gets serialized
+	// into `fb.bin`.
+	jsonSymlink := filepath.Join(dir, "fb.json")
+	os.Symlink(jsonPath, jsonSymlink)
+
+	// Execute the `flatc` command.
+	flatcCommand := exec.Command(absFlatcPath, "--binary", absFbsPath, jsonSymlink)
+	flatcCommand.Dir = dir
+	err = flatcCommand.Run()
+	if err != nil {
+		log.Fatal("Failed to execute flatc: ", err)
+	}
+
+	// Read the serialized flatbuffer and return it.
+	binaryPath := filepath.Join(dir, "fb.bin")
+	binaryFb, err := os.ReadFile(binaryPath)
+	if err != nil {
+		log.Fatal("Failed to read flatc output ", binaryPath, ": ", err)
+	}
+	return binaryFb
+}
+
+func main() {
+	// Parse command line arguments.
+	addressPtr := flag.String("address", "http://localhost:8080",
+		"The end point where the server is listening.")
+	submitDataScoutingPtr := flag.String("submitDataScouting", "",
+		"If specified, parse the file as a SubmitDataScouting JSON request.")
+	flag.Parse()
+
+	// 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)
+		}
+		log.Printf("%+v", response)
+	}
+}