diff --git a/cmd/playground/main.go b/cmd/playground/main.go
index 0e0b521a..338843db 100644
--- a/cmd/playground/main.go
+++ b/cmd/playground/main.go
@@ -62,13 +62,13 @@ func start(packagesFile, addr, goRoot string, debug bool) error {
r := mux.NewRouter()
langserver.New(packages).Mount(r.PathPrefix("/api").Subrouter())
- r.PathPrefix("/").Handler(http.FileServer(http.Dir("./public")))
+ r.PathPrefix("/").Handler(langserver.SpaFileServer("./public"))
zap.S().Infof("Listening on %q", addr)
var handler http.Handler
if debug {
- zap.S().Warn("Debug mode enabled, CORS disabled")
+ zap.S().Info("Debug mode enabled, CORS disabled")
handler = langserver.NewCORSDisablerWrapper(r)
} else {
handler = r
diff --git a/docker.mk b/docker.mk
index 125c4e5a..b2921005 100644
--- a/docker.mk
+++ b/docker.mk
@@ -1,6 +1,5 @@
DOCKERFILE ?= ./build/Dockerfile
IMG_NAME ?= x1unix/go-playground
-TAG ?= 1.0.0
.PHONY: docker
docker: docker-login docker-make-image
@@ -20,5 +19,8 @@ docker-login:
.PHONY: docker-make-image
docker-make-image:
+ @if [ -z "$(TAG)" ]; then\
+ echo "required parameter TAG is undefined" && exit 1; \
+ fi;
@echo "- Building '$(IMG_NAME):latest' $(TAG)..."
docker image build -t $(IMG_NAME):latest -t $(IMG_NAME):$(TAG) -f $(DOCKERFILE) .
\ No newline at end of file
diff --git a/pkg/analyzer/decl.go b/pkg/analyzer/decl.go
index 95be91c4..0b314a02 100644
--- a/pkg/analyzer/decl.go
+++ b/pkg/analyzer/decl.go
@@ -10,7 +10,7 @@ func formatFieldAndType(t ast.Expr, id *ast.Ident) string {
return id.String() + " " + typeStr
}
-func formatFuncParams(params *ast.FieldList) (string, int) {
+func formatFieldsList(params *ast.FieldList, joinChar string) (string, int) {
if params == nil {
return "", 0
}
@@ -30,7 +30,7 @@ func formatFuncParams(params *ast.FieldList) (string, int) {
}
}
- return strings.Join(fieldTypePair, ", "), paramsLen
+ return strings.Join(fieldTypePair, joinChar), paramsLen
}
func valSpecToItem(isConst bool, v *ast.ValueSpec, withPrivate bool) []*CompletionItem {
@@ -61,26 +61,30 @@ func valSpecToItem(isConst bool, v *ast.ValueSpec, withPrivate bool) []*Completi
return items
}
-func funcToItem(fn *ast.FuncDecl) *CompletionItem {
- ci := &CompletionItem{
- Label: fn.Name.String(),
- Kind: Function,
- Documentation: fn.Doc.Text(),
- }
-
- params, _ := formatFuncParams(fn.Type.Params)
- ci.Detail = "func(" + params + ")"
- ci.InsertText = ci.Label + "(" + params + ")"
-
- returns, retCount := formatFuncParams(fn.Type.Results)
+func funcToString(fn *ast.FuncType) string {
+ params, _ := formatFieldsList(fn.Params, ", ")
+ str := "func(" + params + ")"
+ returns, retCount := formatFieldsList(fn.Results, ", ")
switch retCount {
case 0:
break
case 1:
- ci.Detail += " " + returns
+ str += " " + returns
default:
- ci.Detail += " (" + returns + ")"
+ str += " (" + returns + ")"
+ }
+
+ return str
+}
+
+func funcToItem(fn *ast.FuncDecl) *CompletionItem {
+ ci := &CompletionItem{
+ Label: fn.Name.String(),
+ Kind: Function,
+ Documentation: fn.Doc.Text(),
}
+ ci.Detail = funcToString(fn.Type)
+ ci.InsertText = ci.Label + "()"
return ci
}
diff --git a/pkg/analyzer/expr.go b/pkg/analyzer/expr.go
index 76d516eb..853f2346 100644
--- a/pkg/analyzer/expr.go
+++ b/pkg/analyzer/expr.go
@@ -23,6 +23,24 @@ func expToString(exp ast.Expr) string {
return v.Sel.String()
case *ast.StarExpr:
return "*" + expToString(v.X)
+ case *ast.Ellipsis:
+ return "..." + expToString(v.Elt)
+ case *ast.MapType:
+ keyT := expToString(v.Key)
+ valT := expToString(v.Value)
+ return "map[" + keyT + "]" + valT
+ case *ast.ChanType:
+ chanT := expToString(v.Value)
+ return "chan " + chanT
+ case *ast.InterfaceType:
+ typ := "interface{"
+ fields, fieldCount := formatFieldsList(v.Methods, "\n")
+ if fieldCount > 0 {
+ typ += "\n" + fields + "\n"
+ }
+ return typ + "}"
+ case *ast.FuncType:
+ return funcToString(v)
default:
log.Warnf("expToString: unknown expression - [%[1]T %[1]v]", exp)
return "interface{}"
diff --git a/pkg/goplay/client.go b/pkg/goplay/client.go
index 89486ade..dba3113e 100644
--- a/pkg/goplay/client.go
+++ b/pkg/goplay/client.go
@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"fmt"
- "go.uber.org/zap"
"io"
"net/http"
"net/url"
@@ -22,29 +21,47 @@ const (
var ErrSnippetTooLarge = fmt.Errorf("code snippet too large (max %d bytes)", maxSnippetSize)
+func newRequest(ctx context.Context, method, queryPath string, body io.Reader) (*http.Request, error) {
+ uri := goPlayURL + "/" + queryPath
+ req, err := http.NewRequestWithContext(ctx, method, uri, body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("User-Agent", userAgent)
+ return req, nil
+}
+
+func getRequest(ctx context.Context, queryPath string) (*http.Response, error) {
+ req, err := newRequest(ctx, http.MethodGet, queryPath, nil)
+ if err != nil {
+ return nil, nil
+ }
+
+ return http.DefaultClient.Do(req)
+}
+
func doRequest(ctx context.Context, method, url, contentType string, body io.Reader) ([]byte, error) {
- url = goPlayURL + "/" + url
- zap.S().Debug("doRequest ", url)
- req, err := http.NewRequestWithContext(ctx, method, url, body)
+ req, err := newRequest(ctx, method, url, body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", contentType)
- req.Header.Add("User-Agent", userAgent)
response, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
- var bodyBytes bytes.Buffer
- _, err = io.Copy(&bodyBytes, io.LimitReader(response.Body, maxSnippetSize+1))
+ bodyBytes := &bytes.Buffer{}
+ _, err = io.Copy(bodyBytes, io.LimitReader(response.Body, maxSnippetSize+1))
defer response.Body.Close()
if err != nil {
return nil, err
}
- if bodyBytes.Len() > maxSnippetSize {
- return nil, ErrSnippetTooLarge
+ if err = ValidateContentLength(bodyBytes); err != nil {
+ return nil, err
}
+
return bodyBytes.Bytes(), nil
}
diff --git a/pkg/goplay/errors.go b/pkg/goplay/errors.go
new file mode 100644
index 00000000..92af7464
--- /dev/null
+++ b/pkg/goplay/errors.go
@@ -0,0 +1,18 @@
+package goplay
+
+import "errors"
+
+var ErrSnippetNotFound = errors.New("snippet not found")
+
+type CompileFailedError struct {
+ msg string
+}
+
+func (c CompileFailedError) Error() string {
+ return c.msg
+}
+
+func IsCompileError(err error) bool {
+ _, ok := err.(CompileFailedError)
+ return ok
+}
diff --git a/pkg/goplay/methods.go b/pkg/goplay/methods.go
index bc72e078..8802600e 100644
--- a/pkg/goplay/methods.go
+++ b/pkg/goplay/methods.go
@@ -5,9 +5,60 @@ import (
"encoding/json"
"fmt"
"go.uber.org/zap"
+ "io"
+ "io/ioutil"
+ "net/http"
"net/url"
)
+type lener interface {
+ Len() int
+}
+
+func ValidateContentLength(r lener) error {
+ if r.Len() > maxSnippetSize {
+ return ErrSnippetTooLarge
+ }
+
+ return nil
+}
+
+func GetSnippet(ctx context.Context, snippetID string) (*Snippet, error) {
+ fileName := snippetID + ".go"
+ resp, err := getRequest(ctx, "p/"+fileName)
+ if err != nil {
+ return nil, err
+ }
+
+ defer resp.Body.Close()
+ switch resp.StatusCode {
+ case http.StatusOK:
+ snippet, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Snippet{
+ FileName: fileName,
+ Contents: string(snippet),
+ }, nil
+ case http.StatusNotFound:
+ return nil, ErrSnippetNotFound
+ default:
+ return nil, fmt.Errorf("error from Go Playground server - %d %s", resp.StatusCode, resp.Status)
+ }
+}
+
+func Share(ctx context.Context, src io.Reader) (string, error) {
+ resp, err := doRequest(ctx, http.MethodPost, "share", "text/plain", src)
+ if err != nil {
+ return "", err
+ }
+
+ shareID := string(resp)
+ return shareID, nil
+}
+
func GoImports(ctx context.Context, src []byte) (*FmtResponse, error) {
form := url.Values{}
form.Add("imports", "true")
diff --git a/pkg/goplay/types.go b/pkg/goplay/types.go
index 8ffdc4c4..b4007603 100644
--- a/pkg/goplay/types.go
+++ b/pkg/goplay/types.go
@@ -5,6 +5,12 @@ import (
"time"
)
+// Snippet represents shared snippet
+type Snippet struct {
+ FileName string
+ Contents string
+}
+
// FmtResponse is the response returned from
// upstream play.golang.org/fmt request
type FmtResponse struct {
@@ -17,7 +23,7 @@ func (r *FmtResponse) HasError() error {
return nil
}
- return errors.New(r.Error)
+ return CompileFailedError{msg: r.Error}
}
// CompileEvent represents individual
diff --git a/pkg/langserver/request.go b/pkg/langserver/request.go
index 7b62caf1..8078a000 100644
--- a/pkg/langserver/request.go
+++ b/pkg/langserver/request.go
@@ -13,6 +13,15 @@ import (
"github.com/x1unix/go-playground/pkg/analyzer"
)
+type SnippetResponse struct {
+ FileName string `json:"fileName"`
+ Code string `json:"code"`
+}
+
+type ShareResponse struct {
+ SnippetID string `json:"snippetID"`
+}
+
type SuggestionRequest struct {
PackageName string `json:"packageName"`
Value string `json:"value"`
diff --git a/pkg/langserver/server.go b/pkg/langserver/server.go
index f6f54963..9d587b52 100644
--- a/pkg/langserver/server.go
+++ b/pkg/langserver/server.go
@@ -27,6 +27,8 @@ func (s *Service) Mount(r *mux.Router) {
r.Path("/suggest").HandlerFunc(s.GetSuggestion)
r.Path("/compile").Methods(http.MethodPost).HandlerFunc(s.Compile)
r.Path("/format").Methods(http.MethodPost).HandlerFunc(s.FormatCode)
+ r.Path("/share").Methods(http.MethodPost).HandlerFunc(s.Share)
+ r.Path("/snippet/{id}").Methods(http.MethodGet).HandlerFunc(s.GetSnippet)
}
func (s *Service) lookupBuiltin(val string) (*SuggestionsResponse, error) {
@@ -107,7 +109,7 @@ func (s *Service) goImportsCode(w http.ResponseWriter, r *http.Request) ([]byte,
}
if err = resp.HasError(); err != nil {
- Errorf(http.StatusBadRequest, err.Error())
+ Errorf(http.StatusBadRequest, err.Error()).Write(w)
return nil, err, false
}
@@ -118,6 +120,10 @@ func (s *Service) goImportsCode(w http.ResponseWriter, r *http.Request) ([]byte,
func (s *Service) FormatCode(w http.ResponseWriter, r *http.Request) {
code, err, _ := s.goImportsCode(w, r)
if err != nil {
+ if goplay.IsCompileError(err) {
+ return
+ }
+
s.log.Error(err)
return
}
@@ -125,9 +131,53 @@ func (s *Service) FormatCode(w http.ResponseWriter, r *http.Request) {
WriteJSON(w, CompilerResponse{Formatted: string(code)})
}
+func (s *Service) Share(w http.ResponseWriter, r *http.Request) {
+ shareID, err := goplay.Share(r.Context(), r.Body)
+ defer r.Body.Close()
+ if err != nil {
+ if err == goplay.ErrSnippetTooLarge {
+ Errorf(http.StatusRequestEntityTooLarge, err.Error()).Write(w)
+ return
+ }
+
+ s.log.Error("failed to share code: ", err)
+ NewErrorResponse(err).Write(w)
+ }
+
+ WriteJSON(w, ShareResponse{SnippetID: shareID})
+}
+
+func (s *Service) GetSnippet(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ snippetID := vars["id"]
+ snippet, err := goplay.GetSnippet(r.Context(), snippetID)
+ if err != nil {
+ if err == goplay.ErrSnippetNotFound {
+ Errorf(http.StatusNotFound, "snippet %q not found", snippetID).Write(w)
+ return
+ }
+
+ s.log.Errorw("failed to get snippet",
+ "snippetID", snippetID,
+ "err", err,
+ )
+ NewErrorResponse(err).Write(w)
+ return
+ }
+
+ WriteJSON(w, SnippetResponse{
+ FileName: snippet.FileName,
+ Code: snippet.Contents,
+ })
+}
+
func (s *Service) Compile(w http.ResponseWriter, r *http.Request) {
src, err, changed := s.goImportsCode(w, r)
if err != nil {
+ if goplay.IsCompileError(err) {
+ return
+ }
+
s.log.Error(err)
return
}
@@ -149,6 +199,6 @@ func (s *Service) Compile(w http.ResponseWriter, r *http.Request) {
result.Formatted = string(src)
}
- s.log.Debugw("resp from compiler", "res", res)
+ s.log.Debugw("response from compiler", "res", res)
WriteJSON(w, result)
}
diff --git a/pkg/langserver/spa.go b/pkg/langserver/spa.go
new file mode 100644
index 00000000..5abb689b
--- /dev/null
+++ b/pkg/langserver/spa.go
@@ -0,0 +1,70 @@
+package langserver
+
+import (
+ "net/http"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+)
+
+// Advanced static server
+type spaFileServer struct {
+ root http.Dir
+ NotFoundHandler func(http.ResponseWriter, *http.Request)
+}
+
+func (fs *spaFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+
+ if containsDotDot(r.URL.Path) {
+ Errorf(http.StatusBadRequest, "Bad Request").Write(w)
+ return
+ }
+
+ //if empty, set current directory
+ dir := string(fs.root)
+ if dir == "" {
+ dir = "."
+ }
+
+ //add prefix and clean
+ upath := r.URL.Path
+ if !strings.HasPrefix(upath, "/") {
+ upath = "/" + upath
+ r.URL.Path = upath
+ }
+ upath = path.Clean(upath)
+
+ //path to file
+ name := path.Join(dir, filepath.FromSlash(upath))
+
+ //check if file exists
+ f, err := os.Open(name)
+ if err != nil {
+ if os.IsNotExist(err) {
+ http.ServeFile(w, r, string(fs.root)+"/index.html")
+ return
+ }
+ }
+ defer f.Close()
+
+ http.ServeFile(w, r, name)
+}
+
+func containsDotDot(v string) bool {
+ if !strings.Contains(v, "..") {
+ return false
+ }
+ for _, ent := range strings.FieldsFunc(v, isSlashRune) {
+ if ent == ".." {
+ return true
+ }
+ }
+ return false
+}
+
+func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
+
+func SpaFileServer(root http.Dir) http.Handler {
+ return &spaFileServer{root: root}
+}
diff --git a/web/package.json b/web/package.json
index b99c8044..d0c554d4 100644
--- a/web/package.json
+++ b/web/package.json
@@ -8,6 +8,7 @@
"@testing-library/user-event": "^7.1.2",
"axios": "^0.19.1",
"circular-dependency-plugin": "^5.2.0",
+ "connected-react-router": "^6.6.1",
"file-saver": "^2.0.2",
"monaco-editor": "^0.19.3",
"monaco-editor-webpack-plugin": "^1.8.2",
@@ -17,8 +18,10 @@
"react-dom": "^16.12.0",
"react-monaco-editor": "^0.33.0",
"react-redux": "^7.1.3",
+ "react-router-dom": "^5.1.2",
"react-scripts": "3.3.0",
"redux": "^4.0.5",
+ "redux-thunk": "^2.3.0",
"typescript": "^3.7.4"
},
"scripts": {
diff --git a/web/public/index.html b/web/public/index.html
index 4f18b394..f919bf47 100644
--- a/web/public/index.html
+++ b/web/public/index.html
@@ -7,7 +7,7 @@