Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/playground/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docker.mk
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
DOCKERFILE ?= ./build/Dockerfile
IMG_NAME ?= x1unix/go-playground
TAG ?= 1.0.0

.PHONY: docker
docker: docker-login docker-make-image
Expand All @@ -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) .
36 changes: 20 additions & 16 deletions pkg/analyzer/decl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
18 changes: 18 additions & 0 deletions pkg/analyzer/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}"
Expand Down
35 changes: 26 additions & 9 deletions pkg/goplay/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"context"
"fmt"
"go.uber.org/zap"
"io"
"net/http"
"net/url"
Expand All @@ -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
}

Expand Down
18 changes: 18 additions & 0 deletions pkg/goplay/errors.go
Original file line number Diff line number Diff line change
@@ -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
}
51 changes: 51 additions & 0 deletions pkg/goplay/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
8 changes: 7 additions & 1 deletion pkg/goplay/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -17,7 +23,7 @@ func (r *FmtResponse) HasError() error {
return nil
}

return errors.New(r.Error)
return CompileFailedError{msg: r.Error}
}

// CompileEvent represents individual
Expand Down
9 changes: 9 additions & 0 deletions pkg/langserver/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
54 changes: 52 additions & 2 deletions pkg/langserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}

Expand All @@ -118,16 +120,64 @@ 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
}

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
}
Expand All @@ -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)
}
Loading