Skip to content

Commit 8bb1e2e

Browse files
committed
examples: derive examples from the examples directory
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 <rfindley@google.com> Run-TryBot: Robert Findley <rfindley@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Alexander Rakoczy <alex@golang.org>
1 parent 6987932 commit 8bb1e2e

File tree

15 files changed

+242
-76
lines changed

15 files changed

+242
-76
lines changed

edit.go

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type editData struct {
2424
Analytics bool
2525
GoVersion string
2626
Gotip bool
27+
Examples []example
2728
}
2829

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

48-
content := hello
49-
if s.gotip {
50-
content = helloGotip
51-
}
52-
snip := &snippet{Body: []byte(content)}
49+
snip := &snippet{Body: []byte(s.examples.hello())}
5350
if strings.HasPrefix(r.URL.Path, "/p/") {
5451
if !allowShare(r) {
5552
w.WriteHeader(http.StatusUnavailableForLegalReasons)
@@ -88,40 +85,10 @@ func (s *server) handleEdit(w http.ResponseWriter, r *http.Request) {
8885
Analytics: r.Host == hostname,
8986
GoVersion: runtime.Version(),
9087
Gotip: s.gotip,
88+
Examples: s.examples.examples,
9189
}
9290
if err := editTemplate.Execute(w, data); err != nil {
9391
s.log.Errorf("editTemplate.Execute(w, %+v): %v", data, err)
9492
return
9593
}
9694
}
97-
98-
const hello = `package main
99-
100-
import (
101-
"fmt"
102-
)
103-
104-
func main() {
105-
fmt.Println("Hello, playground")
106-
}
107-
`
108-
109-
var helloGotip = fmt.Sprintf(`package main
110-
111-
import (
112-
"fmt"
113-
)
114-
115-
// This playground uses a development build of Go:
116-
// %s
117-
118-
func Print[T any](s ...T) {
119-
for _, v := range s {
120-
fmt.Print(v)
121-
}
122-
}
123-
124-
func main() {
125-
Print("Hello, ", "playground\n")
126-
}
127-
`, runtime.Version())

edit.html

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,9 @@
121121
</label>
122122
{{end}}
123123
<select class="js-playgroundToysEl">
124-
<option value="hello.txt">Hello, playground</option>
125-
<option value="test.txt">Tests</option>
126-
<option value="multi.txt">Multiple files</option>
127-
<option value="http.txt">HTTP server</option>
128-
<option value="image.txt">Display image</option>
129-
<option value="sleep.txt">Sleep</option>
130-
<option value="clear.txt">Clear</option>
124+
{{range .Examples}}
125+
<option value="{{.Path}}">{{.Title}}</option>
126+
{{end}}
131127
</select>
132128
<input type="button" value="About" id="aboutButton">
133129
</div>

examples.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"fmt"
9+
"net/http"
10+
"os"
11+
"path/filepath"
12+
"runtime"
13+
"sort"
14+
"strings"
15+
"time"
16+
)
17+
18+
// examplesHandler serves example content out of the examples directory.
19+
type examplesHandler struct {
20+
modtime time.Time
21+
examples []example
22+
}
23+
24+
type example struct {
25+
Title string
26+
Path string
27+
Content string
28+
}
29+
30+
func (h *examplesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
31+
w.Header().Set("Access-Control-Allow-Origin", "*")
32+
for _, e := range h.examples {
33+
if e.Path == req.URL.Path {
34+
http.ServeContent(w, req, e.Path, h.modtime, strings.NewReader(e.Content))
35+
return
36+
}
37+
}
38+
http.NotFound(w, req)
39+
}
40+
41+
// hello returns the hello text for this instance, which depends on the Go
42+
// version and whether or not we are serving Gotip examples.
43+
func (h *examplesHandler) hello() string {
44+
return h.examples[0].Content
45+
}
46+
47+
// newExamplesHandler reads from the examples directory, returning a handler to
48+
// serve their content.
49+
//
50+
// If gotip is set, all files ending in .txt will be included in the set of
51+
// examples. If gotip is not set, files ending in .gotip.txt are excluded.
52+
// Examples must start with a line beginning "// Title:" that sets their title.
53+
//
54+
// modtime is used for content caching headers.
55+
func newExamplesHandler(gotip bool, modtime time.Time) (*examplesHandler, error) {
56+
const dir = "examples"
57+
entries, err := os.ReadDir(dir)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
var examples []example
63+
for _, entry := range entries {
64+
name := entry.Name()
65+
66+
// Read examples ending in .txt, skipping those ending in .gotip.txt if
67+
// gotip is not set.
68+
prefix := "" // if non-empty, this is a relevant example file
69+
if strings.HasSuffix(name, ".gotip.txt") {
70+
if gotip {
71+
prefix = strings.TrimSuffix(name, ".gotip.txt")
72+
}
73+
} else if strings.HasSuffix(name, ".txt") {
74+
prefix = strings.TrimSuffix(name, ".txt")
75+
}
76+
77+
if prefix == "" {
78+
continue
79+
}
80+
81+
data, err := os.ReadFile(filepath.Join(dir, name))
82+
if err != nil {
83+
return nil, err
84+
}
85+
content := string(data)
86+
87+
// Extract the magic "// Title:" comment specifying the example's title.
88+
nl := strings.IndexByte(content, '\n')
89+
const titlePrefix = "// Title:"
90+
if nl == -1 || !strings.HasPrefix(content, titlePrefix) {
91+
return nil, fmt.Errorf("malformed example for %q: must start with a title line beginning %q", name, titlePrefix)
92+
}
93+
title := strings.TrimPrefix(content[:nl], titlePrefix)
94+
title = strings.TrimSpace(title)
95+
96+
examples = append(examples, example{
97+
Title: title,
98+
Path: name,
99+
Content: content[nl+1:],
100+
})
101+
}
102+
103+
// Sort by title, before prepending the hello example (we always want Hello
104+
// to be first).
105+
sort.Slice(examples, func(i, j int) bool {
106+
return examples[i].Title < examples[j].Title
107+
})
108+
109+
// For Gotip, serve hello content that includes the Go version.
110+
hi := hello
111+
if gotip {
112+
hi = helloGotip
113+
}
114+
115+
examples = append([]example{
116+
{"Hello, playground", "hello.txt", hi},
117+
}, examples...)
118+
return &examplesHandler{
119+
modtime: modtime,
120+
examples: examples,
121+
}, nil
122+
}
123+
124+
const hello = `package main
125+
126+
import (
127+
"fmt"
128+
)
129+
130+
func main() {
131+
fmt.Println("Hello, playground")
132+
}
133+
`
134+
135+
var helloGotip = fmt.Sprintf(`package main
136+
137+
import (
138+
"fmt"
139+
)
140+
141+
// This playground uses a development build of Go:
142+
// %s
143+
144+
func Print[T any](s ...T) {
145+
for _, v := range s {
146+
fmt.Print(v)
147+
}
148+
}
149+
150+
func main() {
151+
Print("Hello, ", "playground\n")
152+
}
153+
`, runtime.Version())

examples/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Playground Examples
2+
3+
Add examples to the playground by adding files to this directory with the
4+
`.txt` file extension. Examples with file names ending in `.gotip.txt` are only
5+
displayed on the gotip playground.
6+
7+
Each example must start with a line beginning with "// Title:", specifying the
8+
title of the example in the selection menu. This title line will be stripped
9+
from the example before serving.

examples/clear.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Title: Clear
12
package main
23

34
import (

examples/hello.txt

Lines changed: 0 additions & 9 deletions
This file was deleted.

examples/http.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Title: HTTP server
12
package main
23

34
import (

examples/image.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Title: Display image
12
package main
23

34
import (

examples/min.gotip.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Title: Generic min
2+
package main
3+
4+
import (
5+
"fmt"
6+
"constraints"
7+
)
8+
9+
func min[P constraints.Ordered](x, y P) P {
10+
if x < y {
11+
return x
12+
} else {
13+
return y
14+
}
15+
}
16+
17+
func main() {
18+
fmt.Println(min(42, 24))
19+
}

examples/multi.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Title: Multiple files
12
package main
23

34
import (

0 commit comments

Comments
 (0)