package play

import "git.sr.ht/~shulhan/pakakeh.go/lib/play"

Package play provides callable APIs and HTTP handlers to format, run, and test Go code, similar to Go playground but using HTTP instead of WebSocket.

For HTTP API, this package expose handlers: [HTTPHandleFormat], [HTTPHandleRun], and [HTTPHandleTest].

Formatting and running Go code

The HTTP APIs for formatting and running Go code accept the following JSON request format,

{
	"goversion": <string>, // For run only.
	"without_race": <boolean>, // For run only.
	"body": <string>
}

The "goversion" field define the Go tools and toolchain version to be used to compile the code. The default "goversion" is defined in global variable GoVersion in this package. If "without_race" is true, the Run command will not run with "-race" option. The "body" field contains the Go code to be formatted or run.

Both request return the following JSON response format,

{
	"code": <integer, HTTP status code>,
	"name": <string, error type>,
	"message": <string, optional message>,
	"data": <string>
}

For the Go.HTTPHandleFormat, the response "data" contains the formatted Go code. For the Go.HTTPHandleRun, the response "data" contains the output from running the Go code. The "message" field contains an error on pre-Run, like bad request or file system related error.

Unsafe run

As exceptional, the Go.Run and Go.HTTPHandleRun accept the following request for running program inside custom "go.mod",

{
	"unsafe_run": <path>
}

The "unsafe_run" define the path to directory relative to play.GoOptions.Root working directory. Once the request is accepted it will change the directory into "unsafe_run" first and then execute "go run ." directly. Go code that executed inside "unsafe_run" should be not modifiable and safe from mallicious execution.

Testing

The Go.Test or Go.HTTPHandleTest must run inside the directory that contains the Go code file to be tested. The Go.HTTPHandleTest API accept the following request format,

{
	"goversion": <string>,
	"file": <string>,
	"body": <string>,
	"without_race": <boolean>
}

The "file" field define the path to the "_test.go" file, default to "test_test.go" if its empty. The "body" field contains the Go code that will be saved to that "file". The test will run, by default, with "go test -count=1 -race $dirname" where "$dirname" is the path to directory where "file" located, must be under the play.GoOptions.Root directory. If "without_race" is true, the test command will not run with "-race" option.

Index

Examples

Constants

const GoVersion = `1.23.2`

GoVersion define the Go tool version for go.mod to be used to run the code.

const Timeout = 10 * time.Second

Timeout define the maximum time the program can be run until it get terminated.

Variables

var ErrEmptyFile = errors.New(`empty File`)

ErrEmptyFile error when running Go.Test with empty play.Request.File.

Types

type Go

type Go struct {
	// contains filtered or unexported fields
}

Go define the type that can format, run, and test Go code.

func NewGo

func NewGo(opts GoOptions) (playgo *Go, err error)

NewGo create and initialize new Go tools.

func (*Go) Format

func (playgo *Go) Format(req Request) (out []byte, err error)

Format the Go code in the play.Request.Body and return the result to out. Any syntax error on the code will be returned as error.

Example

Code:

{
	const codeIndentMissingImport = `
package main
func main() {
  fmt.Println("Hello, world")
}
`
	var playgo *Go
	var err error
	playgo, err = NewGo(GoOptions{
		Root: os.TempDir(),
	})
	if err != nil {
		log.Fatal(err)
	}

	var req = Request{
		Body: codeIndentMissingImport,
	}
	var out []byte
	out, err = playgo.Format(req)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s", out)

	//Output:
	//package main
	//
	//import "fmt"
	//
	//func main() {
	//	fmt.Println("Hello, world")
	//}
}

Output:

package main

import "fmt"

func main() {
	fmt.Println("Hello, world")
}

func (*Go) HTTPHandleFormat

func (playgo *Go) HTTPHandleFormat(httpresw http.ResponseWriter, httpreq *http.Request)

HTTPHandleFormat define the HTTP handler for Go.Format.

Example

Code:

{
	const codeIndentMissingImport = `
package main
func main() {
  fmt.Println("Hello, world")
}
`
	var playgo *Go
	var err error
	playgo, err = NewGo(GoOptions{
		Root: os.TempDir(),
	})
	if err != nil {
		log.Fatal(err)
	}

	var req = Request{
		Body: codeIndentMissingImport,
	}
	var rawbody []byte
	rawbody, err = json.Marshal(&req)
	if err != nil {
		log.Fatal(err)
	}

	var resprec = httptest.NewRecorder()
	var httpreq = httptest.NewRequest(`POST`, `/api/play/format`,
		bytes.NewReader(rawbody))
	httpreq.Header.Set(`Content-Type`, `application/json`)

	var mux = http.NewServeMux()
	mux.HandleFunc(`POST /api/play/format`, playgo.HTTPHandleFormat)
	mux.ServeHTTP(resprec, httpreq)

	var resp = resprec.Result()
	rawbody, err = io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf(`%s`, rawbody)

	// Output:
	// {"data":"package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"Hello, world\")\n}\n","code":200}
}

Output:

{"data":"package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"Hello, world\")\n}\n","code":200}

func (*Go) HTTPHandleRun

func (playgo *Go) HTTPHandleRun(
	httpresw http.ResponseWriter, httpreq *http.Request,
)

HTTPHandleRun define the HTTP handler for Go.Run. Each client is identified by unique cookie, so if two requests come from the same client, the previous run will be cancelled.

Example

Code:

{
	const code = `
package main
import "fmt"
func main() {
	fmt.Println("Hello, world")
}
`
	var playgo *Go
	var err error
	playgo, err = NewGo(GoOptions{
		Root: os.TempDir(),
	})
	if err != nil {
		log.Fatal(err)
	}

	var req = Request{
		Body: code,
	}
	var rawbody []byte
	rawbody, err = json.Marshal(&req)
	if err != nil {
		log.Fatal(err)
	}

	var resprec = httptest.NewRecorder()
	var httpreq = httptest.NewRequest(`POST`, `/api/play/run`,
		bytes.NewReader(rawbody))
	httpreq.Header.Set(`Content-Type`, `application/json`)

	var mux = http.NewServeMux()
	mux.HandleFunc(`POST /api/play/run`, playgo.HTTPHandleRun)
	mux.ServeHTTP(resprec, httpreq)

	var resp = resprec.Result()
	rawbody, err = io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf(`%s`, rawbody)

	// Output:
	// {"data":"Hello, world\n","code":200}
}

Output:

{"data":"Hello, world\n","code":200}

func (*Go) HTTPHandleTest

func (playgo *Go) HTTPHandleTest(
	httpresw http.ResponseWriter, httpreq *http.Request,
)

HTTPHandleTest define the HTTP handler for testing Go.Test. Each client is identified by unique cookie, so if two requests come from the same client, the previous Test will be cancelled.

Example

Code:

{
	const code = `
package test
import "testing"
func TestSum(t *testing.T) {
	var total = sum(1, 2, 3)
	if total != 6 {
		t.Fatalf("got %d, want 6", total)
	}
}`
	var playgo *Go
	var err error
	playgo, err = NewGo(GoOptions{
		Root: `testdata/`,
	})
	if err != nil {
		log.Fatal(err)
	}

	var req = Request{
		Body: code,
		File: `/test_test.go`,
	}
	var rawbody []byte
	rawbody, err = json.Marshal(&req)
	if err != nil {
		log.Fatal(err)
	}

	var mux = http.NewServeMux()

	mux.HandleFunc(`POST /api/play/test`, playgo.HTTPHandleTest)

	var resprec = httptest.NewRecorder()
	var httpreq = httptest.NewRequest(`POST`, `/api/play/test`,
		bytes.NewReader(rawbody))
	httpreq.Header.Set(`Content-Type`, `application/json`)

	mux.ServeHTTP(resprec, httpreq)
	var resp = resprec.Result()

	rawbody, err = io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	var rexDuration = regexp.MustCompile(`(?m)\\t(\d+\.\d+)s`)
	rawbody = rexDuration.ReplaceAll(rawbody, []byte(`\tXs`))

	fmt.Printf(`%s`, rawbody)

	// Output:
	// {"data":"ok  \tgit.sr.ht/~shulhan/pakakeh.go/lib/play/testdata\tXs\n","code":200}
}

Output:

{"data":"ok  \tgit.sr.ht/~shulhan/pakakeh.go/lib/play/testdata\tXs\n","code":200}

func (*Go) Run

func (playgo *Go) Run(req *Request) (out []byte, err error)

Run the Go code in the play.Request.Body.

Example

Code:

{
	const codeRun = `
package main
import "fmt"
func main() {
	fmt.Println("Hello, world")
}`

	var playgo *Go
	var err error
	playgo, err = NewGo(GoOptions{
		Root: os.TempDir(),
	})
	if err != nil {
		log.Fatal(err)
	}

	var req = Request{
		Body: codeRun,
	}
	var out []byte
	out, err = playgo.Run(&req)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf(`%s`, out)

	//Output:
	//Hello, world
}

Output:

Hello, world

func (*Go) Test

func (playgo *Go) Test(req *Request) (out []byte, err error)

Test the Go code in the play.Request.Body.

Example

Code:

{
	const codeTest = `
package test
import "testing"
func TestSum(t *testing.T) {
	var total = sum(1, 2, 3)
	if total != 6 {
		t.Fatalf("got %d, want 6", total)
	}
}`
	var rexDuration = regexp.MustCompile(`(?m)\s+(\d+\.\d+)s$`)

	var playgo *Go
	var err error
	playgo, err = NewGo(GoOptions{
		Root: `testdata/`,
	})
	if err != nil {
		log.Fatal(err)
	}

	var req = Request{
		Body: codeTest,
		File: `/test_test.go`,
	}
	var out []byte
	out, err = playgo.Test(&req)
	if err != nil {
		log.Fatal(err)
	}
	// Replace the test duration.
	out = rexDuration.ReplaceAll(out, []byte(" Xs"))
	fmt.Printf(`%s`, out)

	//Output:
	//ok  	git.sr.ht/~shulhan/pakakeh.go/lib/play/testdata Xs
}

Output:

ok  	git.sr.ht/~shulhan/pakakeh.go/lib/play/testdata Xs

type GoOptions

type GoOptions struct {
	// Root directory of where the Go code to be written, run, or test.
	// Default to [os.UserCacheDir] if its not set.
	Root string

	// Version define the Go tool version in go.mod to be used to run the
	// code.
	// Default to package [GoVersion] if its not set.
	Version string

	// Timeout define the maximum time the program can be run until it
	// gets terminated.
	// Default to package [Timeout] if its not set.
	Timeout time.Duration
	// contains filtered or unexported fields
}

GoOptions define the options for running and test Go code.

type Request

type Request struct {

	// The Go version that will be used in go.mod.
	GoVersion string `json:"goversion"`

	// File define the path to test "_test.go" file.
	// This field is for Test.
	File string `json:"file"`

	// Body contains the Go code to be Format-ed or Run.
	Body string `json:"body"`

	// UnsafeRun define the working directory where "go run ." will be
	// executed directly.
	UnsafeRun string `json:"unsafe_run"`

	// WithoutRace define the field to opt out the "-race" option when
	// running Go code.
	WithoutRace bool `json:"without_race"`
	// contains filtered or unexported fields
}

Request for calling Go.Format, Go.Run, or Go.Test.

Source Files

command.go go.go http.go play.go request.go run_manager.go

Version
v0.60.0 (latest)
Published
Feb 1, 2025
Platform
linux/amd64
Imports
20 packages
Last checked
20 minutes ago

Tools for package owners.