playground: add a version endpoint

Add a /version handler to serve information about the playground Go
version.

Also consolidate writing JSON responses into a common helper.

Change-Id: I1bb3de4c23320eb58306c93a51dbe9ae5176382d
Reviewed-on: https://go-review.googlesource.com/c/playground/+/365854
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/fmt.go b/fmt.go
index 91027be..98811e4 100644
--- a/fmt.go
+++ b/fmt.go
@@ -20,7 +20,7 @@
 	Error string
 }
 
-func handleFmt(w http.ResponseWriter, r *http.Request) {
+func (s *server) handleFmt(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Access-Control-Allow-Origin", "*")
 	if r.Method == "OPTIONS" {
 		// This is likely a pre-flight CORS request.
@@ -69,7 +69,7 @@
 		}
 	}
 
-	json.NewEncoder(w).Encode(fmtResponse{Body: string(fs.Format())})
+	s.writeJSONResponse(w, fmtResponse{Body: string(fs.Format())}, http.StatusOK)
 }
 
 func formatGoMod(file string, data []byte) ([]byte, error) {
diff --git a/fmt_test.go b/fmt_test.go
index ceea152..b6e3f19 100644
--- a/fmt_test.go
+++ b/fmt_test.go
@@ -14,6 +14,11 @@
 )
 
 func TestHandleFmt(t *testing.T) {
+	s, err := newServer(testingOptions(t))
+	if err != nil {
+		t.Fatalf("newServer(testingOptions(t)): %v", err)
+	}
+
 	for _, tt := range []struct {
 		name    string
 		method  string
@@ -125,7 +130,7 @@
 			}
 			req := httptest.NewRequest("POST", "/fmt", strings.NewReader(form.Encode()))
 			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-			handleFmt(rec, req)
+			s.handleFmt(rec, req)
 			resp := rec.Result()
 			if resp.StatusCode != 200 {
 				t.Fatalf("code = %v", resp.Status)
diff --git a/sandbox.go b/sandbox.go
index 3d26dbb..b7864be 100644
--- a/sandbox.go
+++ b/sandbox.go
@@ -135,7 +135,7 @@
 				// if we've timed out because of an error in the code snippet, or instability
 				// on the playground itself. Either way, we should try to show the user the
 				// partial output of their program.
-				s.writeResponse(w, resp, http.StatusOK)
+				s.writeJSONResponse(w, resp, http.StatusOK)
 				return
 			}
 			for _, e := range internalErrors {
@@ -162,21 +162,7 @@
 			}
 		}
 
-		s.writeResponse(w, resp, http.StatusOK)
-	}
-}
-
-func (s *server) writeResponse(w http.ResponseWriter, resp *response, status int) {
-	var buf bytes.Buffer
-	if err := json.NewEncoder(&buf).Encode(resp); err != nil {
-		s.log.Errorf("error encoding response: %v", err)
-		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-		return
-	}
-	w.WriteHeader(status)
-	if _, err := io.Copy(w, &buf); err != nil {
-		s.log.Errorf("io.Copy(w, &buf): %v", err)
-		return
+		s.writeJSONResponse(w, resp, http.StatusOK)
 	}
 }
 
diff --git a/server.go b/server.go
index 21a9270..1ecb134 100644
--- a/server.go
+++ b/server.go
@@ -5,7 +5,10 @@
 package main
 
 import (
+	"bytes"
+	"encoding/json"
 	"fmt"
+	"io"
 	"net/http"
 	"strings"
 	"time"
@@ -47,7 +50,8 @@
 
 func (s *server) init() {
 	s.mux.HandleFunc("/", s.handleEdit)
-	s.mux.HandleFunc("/fmt", handleFmt)
+	s.mux.HandleFunc("/fmt", s.handleFmt)
+	s.mux.HandleFunc("/version", s.handleVersion)
 	s.mux.HandleFunc("/vet", s.commandHandler("vet", vetCheck))
 	s.mux.HandleFunc("/compile", s.commandHandler("prog", compileAndRun))
 	s.mux.HandleFunc("/share", s.handleShare)
@@ -90,3 +94,20 @@
 	}
 	s.mux.ServeHTTP(w, r)
 }
+
+// writeJSONResponse JSON-encodes resp and writes to w with the given HTTP
+// status.
+func (s *server) writeJSONResponse(w http.ResponseWriter, resp interface{}, status int) {
+	w.Header().Set("Content-Type", "application/json")
+	var buf bytes.Buffer
+	if err := json.NewEncoder(&buf).Encode(resp); err != nil {
+		s.log.Errorf("error encoding response: %v", err)
+		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+		return
+	}
+	w.WriteHeader(status)
+	if _, err := io.Copy(w, &buf); err != nil {
+		s.log.Errorf("io.Copy(w, &buf): %v", err)
+		return
+	}
+}
diff --git a/server_test.go b/server_test.go
index fdac9d7..10e8df2 100644
--- a/server_test.go
+++ b/server_test.go
@@ -13,6 +13,7 @@
 	"net/http"
 	"net/http/httptest"
 	"os"
+	"runtime"
 	"sync"
 	"testing"
 	"time"
@@ -133,6 +134,8 @@
 		{"HTTP example", http.MethodGet, "https://play.golang.org/doc/play/http.txt", http.StatusOK, nil, []byte("net/http")},
 		// Gotip examples should not be available on the non-tip playground.
 		{"Gotip example", http.MethodGet, "https://play.golang.org/doc/play/min.gotip.txt", http.StatusNotFound, nil, nil},
+
+		{"Versions json", http.MethodGet, "https://play.golang.org/version", http.StatusOK, nil, []byte(runtime.Version())},
 	}
 
 	for _, tc := range testCases {
diff --git a/version.go b/version.go
new file mode 100644
index 0000000..e2e164f
--- /dev/null
+++ b/version.go
@@ -0,0 +1,39 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"fmt"
+	"go/build"
+	"net/http"
+	"runtime"
+)
+
+func (s *server) handleVersion(w http.ResponseWriter, req *http.Request) {
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+
+	tag := build.Default.ReleaseTags[len(build.Default.ReleaseTags)-1]
+	var maj, min int
+	if _, err := fmt.Sscanf(tag, "go%d.%d", &maj, &min); err != nil {
+		code := http.StatusInternalServerError
+		http.Error(w, http.StatusText(code), code)
+		return
+	}
+
+	version := struct {
+		Version, Release, Name string
+	}{
+		Version: runtime.Version(),
+		Release: tag,
+	}
+
+	if s.gotip {
+		version.Name = "Go dev branch"
+	} else {
+		version.Name = fmt.Sprintf("Go %d.%d", maj, min)
+	}
+
+	s.writeJSONResponse(w, version, http.StatusOK)
+}