Skip to content

Commit 68ac4cf

Browse files
authored
docs: add source-aligned examples and compatibility workflow
* docs: add source-aligned examples and compatibility workflow * docs: make static example serve a sample page
1 parent 3d084be commit 68ac4cf

11 files changed

Lines changed: 388 additions & 0 deletions

File tree

‎.github/workflows/docs.yml‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Documentation compatibility
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
examples:
15+
name: Compile source-owned examples
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v6
19+
- uses: actions/setup-go@v6
20+
with:
21+
go-version: '1.27'
22+
cache-dependency-path: docs/examples/go.sum
23+
- name: Compile examples against this Echo revision
24+
run: go test ./...
25+
working-directory: docs/examples
26+
- name: Extract source-backed config fields
27+
run: go run ./cmd/config-fields -revision "$GITHUB_SHA" > config-fields.json
28+
working-directory: docs/examples
29+
- name: Keep the generated reference for the website integration
30+
uses: actions/upload-artifact@v4
31+
with:
32+
name: echo-config-fields
33+
path: docs/examples/config-fields.json
34+
35+
website:
36+
name: Build echox against this Echo revision
37+
runs-on: ubuntu-latest
38+
steps:
39+
- name: Checkout Echo
40+
uses: actions/checkout@v6
41+
with:
42+
path: echo
43+
- name: Checkout website
44+
uses: actions/checkout@v6
45+
with:
46+
repository: labstack/echox
47+
path: echox
48+
- uses: actions/setup-go@v6
49+
with:
50+
go-version: '1.27'
51+
cache-dependency-path: echox/go.sum
52+
- name: Use proposed Echo source for cookbook tests
53+
run: go work init ../echo .
54+
working-directory: echox
55+
- name: Compile cookbook examples
56+
run: go test ./...
57+
working-directory: echox
58+
- uses: actions/setup-node@v6
59+
with:
60+
node-version: '24'
61+
cache: npm
62+
cache-dependency-path: echox/site/package-lock.json
63+
- name: Install site dependencies
64+
run: npm ci --ignore-scripts
65+
working-directory: echox/site
66+
- name: Build site
67+
run: npm run build
68+
working-directory: echox/site
69+
env:
70+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

‎docs/examples/.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
config-fields.json

‎docs/examples/README.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Echo documentation examples
2+
3+
These complete programs are intended to be the source for code displayed on the
4+
Echo website. The site should embed the files directly instead of maintaining
5+
copied snippets.
6+
They compile against the Echo checkout that contains them.
7+
8+
Run `go test ./...` from this directory. The nested module keeps example-only
9+
dependencies and files out of the published Echo library module. The local
10+
`replace` directive is intentional: it makes an Echo pull request test its own
11+
source rather than a previously released version.
12+
13+
To try the Static example, run `go run .` from `static/`, then open
14+
`http://localhost:1323/`. Its `public/index.html` is the file served at `/`.
15+
16+
`go run ./cmd/config-fields -root ../.. -revision <commit>` emits deterministic
17+
JSON for exported middleware `*Config` fields, types, deprecation markers, and
18+
source line numbers. It is source data for the website, not a published page.
19+
It does not generate runtime defaults, behavior claims, or security guidance.
20+
21+
The Echo documentation workflow also checks out `echox`, compiles its cookbook
22+
against the proposed Echo checkout using a temporary Go workspace, and builds
23+
the current site. A future `echox` change will consume the JSON and these exact
24+
example files before the reference-drift check becomes a required gate.
25+
26+
This is the first source-owned slice: Request Logger and Static. Behavior notes,
27+
security guidance, and translated explanations remain authored in `echox`.

‎docs/examples/REFERENCE_TRIAL.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# API reference tool trial
2+
3+
The first trial used `gomarkdoc` v1.1.0 against this Echo checkout's
4+
`middleware` package, which includes CORS, Request Logger, and Static, and
5+
against the external `github.com/labstack/echo-jwt/v5` package from the current
6+
`echox` module. The package output was 2,422 lines for core middleware and 389
7+
lines for JWT. `gomarkdoc --check --output ...` successfully detected that the
8+
unmodified generated core file matched its source. The output included
9+
`EnablePathUnescaping` and excluded the removed `RequestLoggerConfig.LogError`.
10+
11+
`--embed` supports marked regions within authored Markdown. A check against an
12+
unmarked, fully generated file failed because embed mode would append a second
13+
generated block. This confirms that an existing page must add embed markers
14+
before using that mode. Templates can change generated Markdown, but this
15+
package-wide output is too broad for individual middleware pages and is not a
16+
machine-readable field manifest. Those two requirements motivate the small
17+
`config-fields` extractor in this module. It extracts only fields, types,
18+
deprecation markers, and source positions; it does not infer defaults or
19+
rewrite authored pages.
20+
21+
The site integration will separately evaluate a targeted `gomarkdoc` template
22+
and marked embedding on a real page before choosing how to render reference
23+
sections. This trial does not justify generating behavior explanations.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
// config-fields emits source-backed middleware config fields for the website.
4+
// It does not claim to determine runtime defaults or behavior.
5+
package main
6+
7+
import (
8+
"bytes"
9+
"encoding/json"
10+
"flag"
11+
"fmt"
12+
"go/ast"
13+
"go/format"
14+
"go/parser"
15+
"go/token"
16+
"os"
17+
"path/filepath"
18+
"sort"
19+
"strings"
20+
)
21+
22+
type field struct {
23+
Name string `json:"name"`
24+
Type string `json:"type"`
25+
Deprecated bool `json:"deprecated,omitempty"`
26+
Line int `json:"line"`
27+
}
28+
29+
type config struct {
30+
Name string `json:"name"`
31+
File string `json:"file"`
32+
Line int `json:"line"`
33+
Fields []field `json:"fields"`
34+
}
35+
36+
type manifest struct {
37+
Module string `json:"module"`
38+
Revision string `json:"revision,omitempty"`
39+
Configs []config `json:"configs"`
40+
}
41+
42+
func main() {
43+
root := flag.String("root", "../..", "Echo repository root, relative to the current directory")
44+
revision := flag.String("revision", "", "exact Echo commit or tag used for this build")
45+
flag.Parse()
46+
47+
result, err := extract(*root, *revision)
48+
if err != nil {
49+
fmt.Fprintln(os.Stderr, err)
50+
os.Exit(1)
51+
}
52+
encoder := json.NewEncoder(os.Stdout)
53+
encoder.SetIndent("", " ")
54+
if err := encoder.Encode(result); err != nil {
55+
fmt.Fprintln(os.Stderr, err)
56+
os.Exit(1)
57+
}
58+
}
59+
60+
func extract(root, revision string) (manifest, error) {
61+
root, err := filepath.Abs(root)
62+
if err != nil {
63+
return manifest{}, err
64+
}
65+
fs := token.NewFileSet()
66+
packageDir := filepath.Join(root, "middleware")
67+
packages, err := parser.ParseDir(fs, packageDir, func(info os.FileInfo) bool {
68+
return strings.HasSuffix(info.Name(), ".go") && !strings.HasSuffix(info.Name(), "_test.go")
69+
}, parser.ParseComments)
70+
if err != nil {
71+
return manifest{}, err
72+
}
73+
pkg, ok := packages["middleware"]
74+
if !ok {
75+
return manifest{}, fmt.Errorf("middleware package not found in %s", packageDir)
76+
}
77+
78+
result := manifest{Module: "github.com/labstack/echo/v5", Revision: revision, Configs: []config{}}
79+
for filename, source := range pkg.Files {
80+
for _, declaration := range source.Decls {
81+
group, ok := declaration.(*ast.GenDecl)
82+
if !ok || group.Tok != token.TYPE {
83+
continue
84+
}
85+
for _, item := range group.Specs {
86+
typeSpec := item.(*ast.TypeSpec)
87+
structure, ok := typeSpec.Type.(*ast.StructType)
88+
if !ok || !ast.IsExported(typeSpec.Name.Name) || !strings.HasSuffix(typeSpec.Name.Name, "Config") {
89+
continue
90+
}
91+
entry := config{
92+
Name: typeSpec.Name.Name,
93+
File: filepath.ToSlash(strings.TrimPrefix(filename, root+string(filepath.Separator))),
94+
Line: fs.Position(typeSpec.Pos()).Line,
95+
Fields: []field{},
96+
}
97+
for _, sourceField := range structure.Fields.List {
98+
var rendered bytes.Buffer
99+
if err := format.Node(&rendered, fs, sourceField.Type); err != nil {
100+
return manifest{}, err
101+
}
102+
fieldDoc := comment(sourceField.Doc)
103+
for _, name := range sourceField.Names {
104+
if !ast.IsExported(name.Name) {
105+
continue
106+
}
107+
entry.Fields = append(entry.Fields, field{
108+
Name: name.Name,
109+
Type: rendered.String(),
110+
Deprecated: strings.Contains(fieldDoc, "Deprecated:"),
111+
Line: fs.Position(name.Pos()).Line,
112+
})
113+
}
114+
}
115+
result.Configs = append(result.Configs, entry)
116+
}
117+
}
118+
}
119+
sort.Slice(result.Configs, func(i, j int) bool { return result.Configs[i].Name < result.Configs[j].Name })
120+
return result, nil
121+
}
122+
123+
func comment(group *ast.CommentGroup) string {
124+
if group == nil {
125+
return ""
126+
}
127+
return strings.TrimSpace(group.Text())
128+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
package main
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
)
10+
11+
func TestExtractOnlyExportedConfigFields(t *testing.T) {
12+
root := t.TempDir()
13+
dir := filepath.Join(root, "middleware")
14+
if err := os.Mkdir(dir, 0755); err != nil {
15+
t.Fatal(err)
16+
}
17+
source := `package middleware
18+
type ExampleConfig struct {
19+
// Deprecated: use New instead.
20+
Old bool
21+
New string
22+
private int
23+
}
24+
type hiddenConfig struct { Visible bool }
25+
type OtherType struct { Visible bool }
26+
`
27+
if err := os.WriteFile(filepath.Join(dir, "example.go"), []byte(source), 0644); err != nil {
28+
t.Fatal(err)
29+
}
30+
31+
got, err := extract(root, "abc123")
32+
if err != nil {
33+
t.Fatal(err)
34+
}
35+
if got.Revision != "abc123" || len(got.Configs) != 1 {
36+
t.Fatalf("unexpected manifest: %#v", got)
37+
}
38+
fields := got.Configs[0].Fields
39+
if got.Configs[0].File != "middleware/example.go" || len(fields) != 2 {
40+
t.Fatalf("unexpected config: %#v", got.Configs[0])
41+
}
42+
if fields[0].Name != "Old" || fields[0].Type != "bool" || !fields[0].Deprecated || fields[1].Name != "New" {
43+
t.Fatalf("unexpected fields: %#v", fields)
44+
}
45+
}

‎docs/examples/go.mod‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/labstack/echo/docs/examples
2+
3+
go 1.25.0
4+
5+
require github.com/labstack/echo/v5 v5.3.0
6+
7+
require golang.org/x/time v0.15.0 // indirect
8+
9+
replace github.com/labstack/echo/v5 => ../..

‎docs/examples/go.sum‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
6+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
7+
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
8+
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
9+
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
10+
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
11+
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
12+
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
13+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
14+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
// This complete example is the source for the Request Logger documentation.
4+
package main
5+
6+
import (
7+
"log/slog"
8+
"net/http"
9+
"os"
10+
11+
"github.com/labstack/echo/v5"
12+
"github.com/labstack/echo/v5/middleware"
13+
)
14+
15+
func main() {
16+
e := echo.New()
17+
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
18+
19+
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
20+
LogURI: true,
21+
LogStatus: true,
22+
HandleError: true,
23+
LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {
24+
if v.Error != nil {
25+
logger.Error("request failed", "uri", v.URI, "status", v.Status, "error", v.Error)
26+
return nil
27+
}
28+
logger.Info("request", "uri", v.URI, "status", v.Status)
29+
return nil
30+
},
31+
}))
32+
33+
e.GET("/", func(c *echo.Context) error {
34+
return c.String(http.StatusOK, "hello")
35+
})
36+
37+
if err := e.Start(":1323"); err != nil {
38+
e.Logger.Error("server stopped", "error", err)
39+
}
40+
}

‎docs/examples/static/main.go‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
// This complete example is the source for the Static middleware documentation.
4+
package main
5+
6+
import (
7+
"github.com/labstack/echo/v5"
8+
"github.com/labstack/echo/v5/middleware"
9+
)
10+
11+
func main() {
12+
e := echo.New()
13+
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
14+
Root: "public",
15+
EnablePathUnescaping: false, // Keep encoded slashes encoded when route guards protect files.
16+
}))
17+
18+
if err := e.Start(":1323"); err != nil {
19+
e.Logger.Error("server stopped", "error", err)
20+
}
21+
}

0 commit comments

Comments
 (0)