Skip to content

Commit

Permalink
examples: derive examples from the examples directory
Browse files Browse the repository at this point in the history
With the gotip playground we may want to swap out examples with greater
frequency, to demonstrate new features at Go tip. Make this easier by
deriving examples directly from the examples directory via a new
examplesHandler type. This also enables having dynamic content for
hello.txt, which depends on the value of runtime.GoVersion().

This will impact the non-tip playground in a few ways:
- We will now pre-load examples at server start up.
- Examples will be sorted by their title (with the exception of Hello,
  playground, which is always first).
- We will set a CORS header for examples. This was added for consistency
  with other handlers, and seems harmless.

Generalize TestShare to TestServer, and use it to test the examples
handler.

Add a single gotip example demonstrating generics.

Updates golang/go#48517

Change-Id: I7ab58eb391829d581f7aeae95c291666be5718b9
Reviewed-on: https://go-review.googlesource.com/c/playground/+/364374
Trust: Robert Findley <[email protected]>
Run-TryBot: Robert Findley <[email protected]>
TryBot-Result: Go Bot <[email protected]>
Reviewed-by: Alexander Rakoczy <[email protected]>
  • Loading branch information
findleyr committed Nov 19, 2021
1 parent 6987932 commit 8bb1e2e
Show file tree
Hide file tree
Showing 15 changed files with 242 additions and 76 deletions.
39 changes: 3 additions & 36 deletions edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type editData struct {
Analytics bool
GoVersion string
Gotip bool
Examples []example
}

func (s *server) handleEdit(w http.ResponseWriter, r *http.Request) {
Expand All @@ -45,11 +46,7 @@ func (s *server) handleEdit(w http.ResponseWriter, r *http.Request) {
return
}

content := hello
if s.gotip {
content = helloGotip
}
snip := &snippet{Body: []byte(content)}
snip := &snippet{Body: []byte(s.examples.hello())}
if strings.HasPrefix(r.URL.Path, "/p/") {
if !allowShare(r) {
w.WriteHeader(http.StatusUnavailableForLegalReasons)
Expand Down Expand Up @@ -88,40 +85,10 @@ func (s *server) handleEdit(w http.ResponseWriter, r *http.Request) {
Analytics: r.Host == hostname,
GoVersion: runtime.Version(),
Gotip: s.gotip,
Examples: s.examples.examples,
}
if err := editTemplate.Execute(w, data); err != nil {
s.log.Errorf("editTemplate.Execute(w, %+v): %v", data, err)
return
}
}

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

var helloGotip = fmt.Sprintf(`package main
import (
"fmt"
)
// This playground uses a development build of Go:
// %s
func Print[T any](s ...T) {
for _, v := range s {
fmt.Print(v)
}
}
func main() {
Print("Hello, ", "playground\n")
}
`, runtime.Version())
10 changes: 3 additions & 7 deletions edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,9 @@
</label>
{{end}}
<select class="js-playgroundToysEl">
<option value="hello.txt">Hello, playground</option>
<option value="test.txt">Tests</option>
<option value="multi.txt">Multiple files</option>
<option value="http.txt">HTTP server</option>
<option value="image.txt">Display image</option>
<option value="sleep.txt">Sleep</option>
<option value="clear.txt">Clear</option>
{{range .Examples}}
<option value="{{.Path}}">{{.Title}}</option>
{{end}}
</select>
<input type="button" value="About" id="aboutButton">
</div>
Expand Down
153 changes: 153 additions & 0 deletions examples.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// 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"
"net/http"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
)

// examplesHandler serves example content out of the examples directory.
type examplesHandler struct {
modtime time.Time
examples []example
}

type example struct {
Title string
Path string
Content string
}

func (h *examplesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
for _, e := range h.examples {
if e.Path == req.URL.Path {
http.ServeContent(w, req, e.Path, h.modtime, strings.NewReader(e.Content))
return
}
}
http.NotFound(w, req)
}

// hello returns the hello text for this instance, which depends on the Go
// version and whether or not we are serving Gotip examples.
func (h *examplesHandler) hello() string {
return h.examples[0].Content
}

// newExamplesHandler reads from the examples directory, returning a handler to
// serve their content.
//
// If gotip is set, all files ending in .txt will be included in the set of
// examples. If gotip is not set, files ending in .gotip.txt are excluded.
// Examples must start with a line beginning "// Title:" that sets their title.
//
// modtime is used for content caching headers.
func newExamplesHandler(gotip bool, modtime time.Time) (*examplesHandler, error) {
const dir = "examples"
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}

var examples []example
for _, entry := range entries {
name := entry.Name()

// Read examples ending in .txt, skipping those ending in .gotip.txt if
// gotip is not set.
prefix := "" // if non-empty, this is a relevant example file
if strings.HasSuffix(name, ".gotip.txt") {
if gotip {
prefix = strings.TrimSuffix(name, ".gotip.txt")
}
} else if strings.HasSuffix(name, ".txt") {
prefix = strings.TrimSuffix(name, ".txt")
}

if prefix == "" {
continue
}

data, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
return nil, err
}
content := string(data)

// Extract the magic "// Title:" comment specifying the example's title.
nl := strings.IndexByte(content, '\n')
const titlePrefix = "// Title:"
if nl == -1 || !strings.HasPrefix(content, titlePrefix) {
return nil, fmt.Errorf("malformed example for %q: must start with a title line beginning %q", name, titlePrefix)
}
title := strings.TrimPrefix(content[:nl], titlePrefix)
title = strings.TrimSpace(title)

examples = append(examples, example{
Title: title,
Path: name,
Content: content[nl+1:],
})
}

// Sort by title, before prepending the hello example (we always want Hello
// to be first).
sort.Slice(examples, func(i, j int) bool {
return examples[i].Title < examples[j].Title
})

// For Gotip, serve hello content that includes the Go version.
hi := hello
if gotip {
hi = helloGotip
}

examples = append([]example{
{"Hello, playground", "hello.txt", hi},
}, examples...)
return &examplesHandler{
modtime: modtime,
examples: examples,
}, nil
}

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

var helloGotip = fmt.Sprintf(`package main
import (
"fmt"
)
// This playground uses a development build of Go:
// %s
func Print[T any](s ...T) {
for _, v := range s {
fmt.Print(v)
}
}
func main() {
Print("Hello, ", "playground\n")
}
`, runtime.Version())
9 changes: 9 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Playground Examples

Add examples to the playground by adding files to this directory with the
`.txt` file extension. Examples with file names ending in `.gotip.txt` are only
displayed on the gotip playground.

Each example must start with a line beginning with "// Title:", specifying the
title of the example in the selection menu. This title line will be stripped
from the example before serving.
1 change: 1 addition & 0 deletions examples/clear.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Title: Clear
package main

import (
Expand Down
9 changes: 0 additions & 9 deletions examples/hello.txt

This file was deleted.

1 change: 1 addition & 0 deletions examples/http.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Title: HTTP server
package main

import (
Expand Down
1 change: 1 addition & 0 deletions examples/image.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Title: Display image
package main

import (
Expand Down
19 changes: 19 additions & 0 deletions examples/min.gotip.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Title: Generic min
package main

import (
"fmt"
"constraints"
)

func min[P constraints.Ordered](x, y P) P {
if x < y {
return x
} else {
return y
}
}

func main() {
fmt.Println(min(42, 24))
}
1 change: 1 addition & 0 deletions examples/multi.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Title: Multiple files
package main

import (
Expand Down
1 change: 1 addition & 0 deletions examples/sleep.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Title: Sleep
package main

import (
Expand Down
1 change: 1 addition & 0 deletions examples/test.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Title: Test
package main

import (
Expand Down
11 changes: 11 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ func main() {
if gotip := os.Getenv("GOTIP"); gotip == "true" {
s.gotip = true
}
execpath, _ := os.Executable()
if execpath != "" {
if fi, _ := os.Stat(execpath); fi != nil {
s.modtime = fi.ModTime()
}
}
eh, err := newExamplesHandler(s.gotip, s.modtime)
if err != nil {
return err
}
s.examples = eh
return nil
}, enableMetrics)
if err != nil {
Expand Down
23 changes: 9 additions & 14 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ package main
import (
"fmt"
"net/http"
"os"
"strings"
"time"

"golang.org/x/tools/godoc/static"
)

type server struct {
mux *http.ServeMux
db store
log logger
cache responseCache
gotip bool // if set, server is using gotip
mux *http.ServeMux
db store
log logger
cache responseCache
gotip bool // if set, server is using gotip
examples *examplesHandler

// When the executable was last modified. Used for caching headers of compiled assets.
modtime time.Time
Expand All @@ -38,11 +38,8 @@ func newServer(options ...func(s *server) error) (*server, error) {
if s.log == nil {
return nil, fmt.Errorf("must provide an option func that specifies a logger")
}
execpath, _ := os.Executable()
if execpath != "" {
if fi, _ := os.Stat(execpath); fi != nil {
s.modtime = fi.ModTime()
}
if s.examples == nil {
return nil, fmt.Errorf("must provide an option func that sets the examples handler")
}
s.init()
return s, nil
Expand All @@ -60,9 +57,7 @@ func (s *server) init() {

staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))
s.mux.Handle("/static/", staticHandler)

examplesHandler := http.StripPrefix("/doc/play/", http.FileServer(http.Dir("./examples")))
s.mux.Handle("/doc/play/", examplesHandler)
s.mux.Handle("/doc/play/", http.StripPrefix("/doc/play/", s.examples))
}

func (s *server) handlePlaygroundJS(w http.ResponseWriter, r *http.Request) {
Expand Down
Loading

0 comments on commit 8bb1e2e

Please sign in to comment.