Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Documentation compatibility

on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:

permissions:
contents: read

jobs:
examples:
name: Compile source-owned examples
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.27'
cache-dependency-path: docs/examples/go.sum
- name: Compile examples against this Echo revision
run: go test ./...
working-directory: docs/examples
- name: Extract source-backed config fields
run: go run ./cmd/config-fields -revision "$GITHUB_SHA" > config-fields.json
working-directory: docs/examples
- name: Keep the generated reference for the website integration
uses: actions/upload-artifact@v4
with:
name: echo-config-fields
path: docs/examples/config-fields.json

website:
name: Build echox against this Echo revision
runs-on: ubuntu-latest
steps:
- name: Checkout Echo
uses: actions/checkout@v6
with:
path: echo
- name: Checkout website
uses: actions/checkout@v6
with:
repository: labstack/echox
path: echox
- uses: actions/setup-go@v6
with:
go-version: '1.27'
cache-dependency-path: echox/go.sum
- name: Use proposed Echo source for cookbook tests
run: go work init ../echo .
working-directory: echox
- name: Compile cookbook examples
run: go test ./...
working-directory: echox
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
cache-dependency-path: echox/site/package-lock.json
- name: Install site dependencies
run: npm ci --ignore-scripts
working-directory: echox/site
- name: Build site
run: npm run build
working-directory: echox/site
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions docs/examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
config-fields.json
27 changes: 27 additions & 0 deletions docs/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Echo documentation examples

These complete programs are intended to be the source for code displayed on the
Echo website. The site should embed the files directly instead of maintaining
copied snippets.
They compile against the Echo checkout that contains them.

Run `go test ./...` from this directory. The nested module keeps example-only
dependencies and files out of the published Echo library module. The local
`replace` directive is intentional: it makes an Echo pull request test its own
source rather than a previously released version.

To try the Static example, run `go run .` from `static/`, then open
`http://localhost:1323/`. Its `public/index.html` is the file served at `/`.

`go run ./cmd/config-fields -root ../.. -revision <commit>` emits deterministic
JSON for exported middleware `*Config` fields, types, deprecation markers, and
source line numbers. It is source data for the website, not a published page.
It does not generate runtime defaults, behavior claims, or security guidance.

The Echo documentation workflow also checks out `echox`, compiles its cookbook
against the proposed Echo checkout using a temporary Go workspace, and builds
the current site. A future `echox` change will consume the JSON and these exact
example files before the reference-drift check becomes a required gate.

This is the first source-owned slice: Request Logger and Static. Behavior notes,
security guidance, and translated explanations remain authored in `echox`.
23 changes: 23 additions & 0 deletions docs/examples/REFERENCE_TRIAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# API reference tool trial

The first trial used `gomarkdoc` v1.1.0 against this Echo checkout's
`middleware` package, which includes CORS, Request Logger, and Static, and
against the external `github.com/labstack/echo-jwt/v5` package from the current
`echox` module. The package output was 2,422 lines for core middleware and 389
lines for JWT. `gomarkdoc --check --output ...` successfully detected that the
unmodified generated core file matched its source. The output included
`EnablePathUnescaping` and excluded the removed `RequestLoggerConfig.LogError`.

`--embed` supports marked regions within authored Markdown. A check against an
unmarked, fully generated file failed because embed mode would append a second
generated block. This confirms that an existing page must add embed markers
before using that mode. Templates can change generated Markdown, but this
package-wide output is too broad for individual middleware pages and is not a
machine-readable field manifest. Those two requirements motivate the small
`config-fields` extractor in this module. It extracts only fields, types,
deprecation markers, and source positions; it does not infer defaults or
rewrite authored pages.

The site integration will separately evaluate a targeted `gomarkdoc` template
and marked embedding on a real page before choosing how to render reference
sections. This trial does not justify generating behavior explanations.
128 changes: 128 additions & 0 deletions docs/examples/cmd/config-fields/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// SPDX-License-Identifier: MIT

// config-fields emits source-backed middleware config fields for the website.
// It does not claim to determine runtime defaults or behavior.
package main

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"path/filepath"
"sort"
"strings"
)

type field struct {
Name string `json:"name"`
Type string `json:"type"`
Deprecated bool `json:"deprecated,omitempty"`
Line int `json:"line"`
}

type config struct {
Name string `json:"name"`
File string `json:"file"`
Line int `json:"line"`
Fields []field `json:"fields"`
}

type manifest struct {
Module string `json:"module"`
Revision string `json:"revision,omitempty"`
Configs []config `json:"configs"`
}

func main() {
root := flag.String("root", "../..", "Echo repository root, relative to the current directory")
revision := flag.String("revision", "", "exact Echo commit or tag used for this build")
flag.Parse()

result, err := extract(*root, *revision)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(result); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func extract(root, revision string) (manifest, error) {
root, err := filepath.Abs(root)
if err != nil {
return manifest{}, err
}
fs := token.NewFileSet()
packageDir := filepath.Join(root, "middleware")
packages, err := parser.ParseDir(fs, packageDir, func(info os.FileInfo) bool {
return strings.HasSuffix(info.Name(), ".go") && !strings.HasSuffix(info.Name(), "_test.go")
}, parser.ParseComments)
if err != nil {
return manifest{}, err
}
pkg, ok := packages["middleware"]
if !ok {
return manifest{}, fmt.Errorf("middleware package not found in %s", packageDir)
}

result := manifest{Module: "github.com/labstack/echo/v5", Revision: revision, Configs: []config{}}
for filename, source := range pkg.Files {
for _, declaration := range source.Decls {
group, ok := declaration.(*ast.GenDecl)
if !ok || group.Tok != token.TYPE {
continue
}
for _, item := range group.Specs {
typeSpec := item.(*ast.TypeSpec)
structure, ok := typeSpec.Type.(*ast.StructType)
if !ok || !ast.IsExported(typeSpec.Name.Name) || !strings.HasSuffix(typeSpec.Name.Name, "Config") {
continue
}
entry := config{
Name: typeSpec.Name.Name,
File: filepath.ToSlash(strings.TrimPrefix(filename, root+string(filepath.Separator))),
Line: fs.Position(typeSpec.Pos()).Line,
Fields: []field{},
}
for _, sourceField := range structure.Fields.List {
var rendered bytes.Buffer
if err := format.Node(&rendered, fs, sourceField.Type); err != nil {
return manifest{}, err
}
fieldDoc := comment(sourceField.Doc)
for _, name := range sourceField.Names {
if !ast.IsExported(name.Name) {
continue
}
entry.Fields = append(entry.Fields, field{
Name: name.Name,
Type: rendered.String(),
Deprecated: strings.Contains(fieldDoc, "Deprecated:"),
Line: fs.Position(name.Pos()).Line,
})
}
}
result.Configs = append(result.Configs, entry)
}
}
}
sort.Slice(result.Configs, func(i, j int) bool { return result.Configs[i].Name < result.Configs[j].Name })
return result, nil
}

func comment(group *ast.CommentGroup) string {
if group == nil {
return ""
}
return strings.TrimSpace(group.Text())
}
45 changes: 45 additions & 0 deletions docs/examples/cmd/config-fields/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT

package main

import (
"os"
"path/filepath"
"testing"
)

func TestExtractOnlyExportedConfigFields(t *testing.T) {
root := t.TempDir()
dir := filepath.Join(root, "middleware")
if err := os.Mkdir(dir, 0755); err != nil {
t.Fatal(err)
}
source := `package middleware
type ExampleConfig struct {
// Deprecated: use New instead.
Old bool
New string
private int
}
type hiddenConfig struct { Visible bool }
type OtherType struct { Visible bool }
`
if err := os.WriteFile(filepath.Join(dir, "example.go"), []byte(source), 0644); err != nil {
t.Fatal(err)
}

got, err := extract(root, "abc123")
if err != nil {
t.Fatal(err)
}
if got.Revision != "abc123" || len(got.Configs) != 1 {
t.Fatalf("unexpected manifest: %#v", got)
}
fields := got.Configs[0].Fields
if got.Configs[0].File != "middleware/example.go" || len(fields) != 2 {
t.Fatalf("unexpected config: %#v", got.Configs[0])
}
if fields[0].Name != "Old" || fields[0].Type != "bool" || !fields[0].Deprecated || fields[1].Name != "New" {
t.Fatalf("unexpected fields: %#v", fields)
}
}
9 changes: 9 additions & 0 deletions docs/examples/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/labstack/echo/docs/examples

go 1.25.0

require github.com/labstack/echo/v5 v5.3.0

require golang.org/x/time v0.15.0 // indirect

replace github.com/labstack/echo/v5 => ../..
14 changes: 14 additions & 0 deletions docs/examples/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
40 changes: 40 additions & 0 deletions docs/examples/request-logger/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT

// This complete example is the source for the Request Logger documentation.
package main

import (
"log/slog"
"net/http"
"os"

"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
)

func main() {
e := echo.New()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogURI: true,
LogStatus: true,
HandleError: true,
LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {
if v.Error != nil {
logger.Error("request failed", "uri", v.URI, "status", v.Status, "error", v.Error)
return nil
}
logger.Info("request", "uri", v.URI, "status", v.Status)
return nil
},
}))

e.GET("/", func(c *echo.Context) error {
return c.String(http.StatusOK, "hello")
})

if err := e.Start(":1323"); err != nil {
e.Logger.Error("server stopped", "error", err)
}
}
21 changes: 21 additions & 0 deletions docs/examples/static/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT

// This complete example is the source for the Static middleware documentation.
package main

import (
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
)

func main() {
e := echo.New()
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Root: "public",
EnablePathUnescaping: false, // Keep encoded slashes encoded when route guards protect files.
}))

if err := e.Start(":1323"); err != nil {
e.Logger.Error("server stopped", "error", err)
}
}
10 changes: 10 additions & 0 deletions docs/examples/static/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Echo Static example</title>
</head>
<body>
<h1>Hello from Echo Static middleware</h1>
</body>
</html>
Loading