diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..aa906a48e --- /dev/null +++ b/.github/workflows/docs.yml @@ -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 }} diff --git a/docs/examples/.gitignore b/docs/examples/.gitignore new file mode 100644 index 000000000..2c846f858 --- /dev/null +++ b/docs/examples/.gitignore @@ -0,0 +1 @@ +config-fields.json diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 000000000..e8058db7d --- /dev/null +++ b/docs/examples/README.md @@ -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 ` 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`. diff --git a/docs/examples/REFERENCE_TRIAL.md b/docs/examples/REFERENCE_TRIAL.md new file mode 100644 index 000000000..77d21d300 --- /dev/null +++ b/docs/examples/REFERENCE_TRIAL.md @@ -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. diff --git a/docs/examples/cmd/config-fields/main.go b/docs/examples/cmd/config-fields/main.go new file mode 100644 index 000000000..7748f8848 --- /dev/null +++ b/docs/examples/cmd/config-fields/main.go @@ -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()) +} diff --git a/docs/examples/cmd/config-fields/main_test.go b/docs/examples/cmd/config-fields/main_test.go new file mode 100644 index 000000000..33a402969 --- /dev/null +++ b/docs/examples/cmd/config-fields/main_test.go @@ -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) + } +} diff --git a/docs/examples/go.mod b/docs/examples/go.mod new file mode 100644 index 000000000..deafd6a52 --- /dev/null +++ b/docs/examples/go.mod @@ -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 => ../.. diff --git a/docs/examples/go.sum b/docs/examples/go.sum new file mode 100644 index 000000000..8b2a4ffa2 --- /dev/null +++ b/docs/examples/go.sum @@ -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= diff --git a/docs/examples/request-logger/main.go b/docs/examples/request-logger/main.go new file mode 100644 index 000000000..edbcbe57c --- /dev/null +++ b/docs/examples/request-logger/main.go @@ -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) + } +} diff --git a/docs/examples/static/main.go b/docs/examples/static/main.go new file mode 100644 index 000000000..7c066599c --- /dev/null +++ b/docs/examples/static/main.go @@ -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) + } +} diff --git a/docs/examples/static/public/index.html b/docs/examples/static/public/index.html new file mode 100644 index 000000000..5fbf861c6 --- /dev/null +++ b/docs/examples/static/public/index.html @@ -0,0 +1,10 @@ + + + + + Echo Static example + + +

Hello from Echo Static middleware

+ +