1
0
mirror of https://github.com/deuill/grawkit.git synced 2024-09-28 00:12:45 +00:00
grawkit/play/play.go
Alex Palaistras 24d63cd8a9 Implement initial version of Grawkit Web Playground
Running `grawkit` locally can be a hassle for one-off tasks, and can be hard to iterate against
when plotting graphs based on complex command-line descriptions. Given recent fixes for POSIX
compatibility, this commit implements a basic web-based "playground"-style application that allows
for entering command-line descriptions in an HTML textarea, and seeing the results (or errors)
instantly.

The playground application itself is built in Go (Awk itself would be insufficient for the
throughput required, but may be investigated in the future), with the excellent GoAwk library
providing parsing and execution duties, therefore making for a pure-Go implementation (other than
Grawkit itself).

Additional support for setting custom styling and an online deployment with Docker are forthcoming.
2019-08-25 16:30:34 +01:00

188 lines
5.2 KiB
Go

package main
import (
// Standard library
"bytes"
"errors"
"flag"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"path"
"syscall"
"text/template"
"time"
// Third-party packages
"github.com/benhoyt/goawk/interp"
"github.com/benhoyt/goawk/parser"
)
const (
// Error messages.
errReadRequest = "Error reading request, please try again"
errValidate = "Error validating content"
errRender = "Error rendering preview"
// The maximum content size we're going to parse.
maxContentSize = 4096
)
var (
// Command-line flags to parse.
scriptPath = flag.String("script-path", "../grawkit", "The path to the Grawkit script")
staticDir = flag.String("static-dir", "static", "The directory under which static files can be found")
listenAddress = flag.String("listen-address", "localhost:8080", "The default address to listen on")
index *template.Template // The base template to render.
program *parser.Program // The parsed version of the Grawkit script.
)
type templateData struct {
Content string
Preview string
Error string
}
// ParseContent accepts un-filtered POST form content, and returns the content to render as a string.
// An error is returned if the content is missing or otherwise invalid.
func parseContent(form url.Values) (string, error) {
if _, ok := form["content"]; !ok || len(form["content"]) == 0 {
return "", errors.New("missing or empty content")
}
var content = form["content"][0]
switch true {
case len(content) > maxContentSize:
return "", errors.New("content too large")
}
return content, nil
}
// HandleRequest accepts a GET or POST HTTP request and responds appropriately based on given data
// and pre-parsed template files. For GET requests not against the document root, HandleRequest will
// attempt to find and return the contents of an equivalent file under the configured static directory.
func handleRequest(w http.ResponseWriter, r *http.Request) {
// Handle template rendering on root path.
if r.URL.Path == "/" {
var data templateData
var outbuf, errbuf bytes.Buffer
switch r.Method {
case "POST":
if err := r.ParseForm(); err != nil {
data.Error = errReadRequest
} else if data.Content, err = parseContent(r.PostForm); err != nil {
data.Error = errValidate + ": " + err.Error()
} else {
config := &interp.Config{
Stdin: bytes.NewReader([]byte(data.Content)),
Output: &outbuf,
Error: &errbuf,
}
// Render generated preview from content given.
if n, err := interp.ExecProgram(program, config); err != nil {
data.Error = errRender
} else if n != 0 {
data.Error = "Error: " + string(errbuf.Bytes())
} else if _, ok := r.PostForm["generate"]; ok {
data.Preview = string(outbuf.Bytes())
} else if _, ok = r.PostForm["download"]; ok {
w.Header().Set("Content-Disposition", `attachment; filename="generated.svg"`)
http.ServeContent(w, r, "generated.svg", time.Now(), bytes.NewReader(outbuf.Bytes()))
return
}
}
fallthrough
case "GET":
// Set correct status code and error message, if any.
if data.Error != "" {
w.Header().Set("X-Error-Message", data.Error)
w.WriteHeader(http.StatusBadRequest)
}
// Render index page template.
if err := index.Execute(w, data); err != nil {
log.Printf("error rendering template: %s", err)
}
}
return
}
// Get sanitized filename for request path given.
name := path.Join(*staticDir, path.Clean(r.URL.Path))
// Check if a file exists for the path requested.
stat, err := os.Stat(name)
if os.IsNotExist(err) || stat != nil && stat.IsDir() {
http.NotFound(w, r)
return
} else if err != nil {
code := http.StatusInternalServerError
http.Error(w, http.StatusText(code), code)
return
}
// Serve file as fallback.
http.ServeFile(w, r, name)
}
// Setup reads configuration flags and initializes global state for the service, returning an error
// if any of the service pre-requisites are not fulfilled.
func setup() error {
// Set up command-line flags.
flag.Parse()
// Set up and parse known template files.
var err error
var files = []string{
path.Join(*staticDir, "template", "index.template"),
path.Join(*staticDir, "template", "default-content.template"),
path.Join(*staticDir, "template", "default-preview.template"),
}
if index, err = template.ParseFiles(files...); err != nil {
return err
}
// Parse Grawkit script into concrete representation.
if script, err := ioutil.ReadFile(*scriptPath); err != nil {
return err
} else if program, err = parser.ParseProgram(script, nil); err != nil {
return err
}
return nil
}
func main() {
// Set up base service dependencies.
if err := setup(); err != nil {
log.Fatalf("Failed setting up service: %s", err)
}
// Set up TCP socket and handlers for HTTP server.
ln, err := net.Listen("tcp", *listenAddress)
if err != nil {
log.Fatalf("Failed listening on address '%s': %s", *listenAddress, err)
}
// Listen on given address and wait for INT or TERM signals.
log.Println("Listening on " + *listenAddress + "...")
go http.Serve(ln, http.HandlerFunc(handleRequest))
halt := make(chan os.Signal, 1)
signal.Notify(halt, syscall.SIGINT, syscall.SIGTERM)
<-halt
log.Println("Shutting down listener...")
}