Skip to content

Commit da7425e

Browse files
authored
doc: variabilized various metrics in docs using codegen counts. (#97)
> Motivation: the documentation site is peppered with counts of methods, > assertion etc. Now these are maintained automatically using a generated > hugo parameters file. doc: added a generated quick index table. This table displays all assertions with their logical opposite (complementary assertion) in compact form. * evolved codegen to produce this table, using the new "opposite" comment annotation * added notes relative to the confusing Decreasing/NonDecreasing pairs, with additional tests to relieve any doubt on these being complementary * regenerated all code for go doc update Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
1 parent ac2dada commit da7425e

53 files changed

Lines changed: 756 additions & 186 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,5 @@ that require external deps.
7777
## Module Information
7878

7979
- Module path: `github.com/go-openapi/testify/v2`
80-
- Go version: 1.24.0
80+
- Go version: 1.25.0
8181
- License: Apache-2.0

README.md

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This is the go-openapi fork of the great [testify](https://github.com/stretchr/t
2626
* 95% compatible with `stretchr/testify` — if you already use it, our migration tool automates the switch
2727
* Actively maintained: regular fixes and evolutions, many PRs proposed upstream are already in
2828
* Zero external dependencies — you import what you need, with opt-in modules for extras (e.g. YAML, colorized output)
29-
* Modernized codebase targeting go1.24+
29+
* Modernized codebase targeting go1.25+
3030
* Go routine leak detection built in: zero-setup, no false positives, works with parallel tests (unlike `go.uber.org/goleak`)
3131
* File descriptor leak detection (linux-only)
3232
* Type-safe assertions with generics (see [a basic example][example-with-generics-url]) — migration to generics can be automated too. [Read the full story][doc-generics]
@@ -37,7 +37,7 @@ This is the go-openapi fork of the great [testify](https://github.com/stretchr/t
3737
### This fork isn't for everyone
3838

3939
* You need the `mock` package — we removed it and won't bring it back. For suites, we're [open to discussion][suite-discussion] about a redesigned approach
40-
* Your project must support Go versions older than 1.24
40+
* Your project must support Go versions older than 1.25
4141
* You rely on `testifylint` or other tooling that expects the `stretchr/testify` import path
4242
* You need 100% API compatibility — we're at 95%, and the remaining 5% are intentional removals
4343

@@ -60,31 +60,7 @@ Feedback, contributions and proposals are welcome.
6060

6161
> **Recent news**
6262
>
63-
> ✅ Stabibilized API
64-
>
65-
> ✅ Migration tool
66-
>
67-
> ✅ Fully refactored how assertions are generated and documented. Opt-in features with their dependencies.
68-
>
69-
> Fixes
70-
>
71-
> ✅ Fixed hangs & panics when using `spew`. Fuzzed `spew`. Fixed deterministic order of keys in diff.
72-
>
73-
> ✅ Fixed go routine leaks with `EventuallyWith` and co.
74-
>
75-
> ✅ Fixed wrong logic with `IsNonIncreasing`, `InNonDecreasing`
76-
>
77-
> ✅ Fixed edge cases with `InDelta`, `InEpsilon`
78-
>
79-
> ✅ Fixed edge cases with `EqualValues`
80-
>
81-
> Additions
82-
>
83-
> ✅ Introduced generics: ~ 40 new type-safe assertions with generic types (doc: added usage guide, examples and benchmark)
84-
>
85-
> ✅ Added `Kind` & `NotKind`, `Consistently`, `NoGoRoutineLeak`, `NoFileDescriptorLeak`
86-
>
87-
> ✅ Added opt-in support for colorized output
63+
> ✅ Preparing v2.5.0: new features, a few fixes (`EventuallyWithT`)
8864
>
8965
> See also our [ROADMAP][doc-roadmap].
9066

assert/assert_assertions.go

Lines changed: 29 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codegen/internal/generator/doc_generator.go

Lines changed: 145 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
package generator
55

66
import (
7+
"cmp"
78
"errors"
89
"fmt"
910
"iter"
1011
"os"
1112
"path"
1213
"path/filepath"
14+
"slices"
15+
16+
yaml "go.yaml.in/yaml/v3"
1317

1418
"github.com/go-openapi/testify/codegen/v2/internal/generator/domains"
1519
"github.com/go-openapi/testify/codegen/v2/internal/generator/funcmaps"
@@ -19,9 +23,12 @@ import (
1923

2024
const (
2125
// index page metadata.
22-
indexTitle = "Assertions index"
23-
indexDescription = "Index of assertion domains"
24-
indexFile = "_index.md"
26+
indexTitle = "Assertions index"
27+
indexDescription = "Index of assertion domains"
28+
indexFile = "_index.md"
29+
metricsTitle = "Quick API index"
30+
metricsDescription = "API quick index & metrics"
31+
metricsFile = "metrics.md"
2532

2633
// sensible default preallocated slots.
2734
allocatedEntries = 15
@@ -78,7 +85,11 @@ func (d *DocGenerator) Generate(opts ...GenerateOption) error {
7885
}
7986
}
8087

81-
return nil
88+
if err := d.generateMetricsPage(indexDoc); err != nil {
89+
return err
90+
}
91+
92+
return d.writeYAMLMetrics(indexDoc.Metrics)
8293
}
8394

8495
type uniqueValues struct {
@@ -173,6 +184,8 @@ func (d *DocGenerator) buildIndexDocument(docsByDomain iter.Seq2[string, model.D
173184
}
174185

175186
doc.RefCount = len(doc.Index)
187+
doc.Metrics = buildMetrics(docsByDomain)
188+
doc.QuickIndex = buildQuickIndex(docsByDomain)
176189

177190
return doc
178191
}
@@ -196,6 +209,95 @@ func buildIndexEntries(docsByDomain iter.Seq2[string, model.Document]) []model.I
196209
return entries
197210
}
198211

212+
func buildMetrics(docsByDomain iter.Seq2[string, model.Document]) (metrics model.Metrics) {
213+
metrics.ByDomain = make(map[string]model.DomainMetrics)
214+
215+
for domain, doc := range docsByDomain {
216+
metrics.Domains++
217+
var domainMetrics model.DomainMetrics
218+
domainMetrics.Name = doc.Title
219+
for _, fn := range doc.Package.Functions {
220+
metrics.Functions++
221+
222+
if fn.IsHelper {
223+
metrics.Helpers++
224+
continue
225+
}
226+
227+
if fn.IsConstructor {
228+
metrics.Others++
229+
continue
230+
}
231+
232+
metrics.Assertions++
233+
domainMetrics.Count++
234+
235+
if fn.IsGeneric {
236+
metrics.Generics++
237+
}
238+
}
239+
240+
metrics.ByDomain[domain] = domainMetrics
241+
}
242+
243+
metrics.NonGenerics = metrics.Functions - metrics.Generics
244+
245+
return metrics
246+
}
247+
248+
func buildQuickIndex(docsByDomain iter.Seq2[string, model.Document]) model.QuickIndex {
249+
const sensibleAlloc = 150
250+
index := make(model.QuickIndex, 0, sensibleAlloc)
251+
seen := make(map[string]struct{}, sensibleAlloc)
252+
opposites := make(map[string]string, sensibleAlloc)
253+
254+
genericNames := make(map[string]string, sensibleAlloc)
255+
for _, doc := range docsByDomain {
256+
for _, fn := range doc.Package.Functions {
257+
genericNames[fn.Name] = fn.GenericName()
258+
for _, tag := range fn.ExtraComments {
259+
opposite := tag.Opposite()
260+
if opposite != "" {
261+
seen[opposite] = struct{}{}
262+
opposites[fn.Name] = opposite
263+
break
264+
}
265+
}
266+
}
267+
}
268+
269+
for domain, doc := range docsByDomain {
270+
for _, fn := range doc.Package.Functions {
271+
_, isOpposite := seen[fn.Name]
272+
if isOpposite {
273+
continue
274+
}
275+
276+
opposite := opposites[fn.Name]
277+
entry := model.QuickIndexEntry{
278+
Name: fn.GenericName(),
279+
Anchor: funcmaps.Slugize(fn.GenericName()),
280+
Opposite: opposite,
281+
Domain: domain,
282+
IsGeneric: fn.IsGeneric,
283+
IsHelper: fn.IsHelper,
284+
}
285+
if opposite != "" {
286+
if gn, ok := genericNames[opposite]; ok {
287+
entry.OppositeAnchor = funcmaps.Slugize(gn)
288+
}
289+
}
290+
291+
index = append(index, entry)
292+
}
293+
}
294+
295+
slices.SortFunc(index, func(a, b model.QuickIndexEntry) int {
296+
return cmp.Compare(a.Name, b.Name)
297+
})
298+
return index
299+
}
300+
199301
func (d *DocGenerator) generateDomainIndex(document model.Document) error {
200302
base := filepath.Join(d.ctx.targetRoot, d.ctx.targetDoc, document.Path)
201303
if err := os.MkdirAll(base, dirPermissions); err != nil {
@@ -213,6 +315,19 @@ func (d *DocGenerator) generateDomainPage(document model.Document) error {
213315
return d.render("doc_page", filepath.Join(base, document.File), document)
214316
}
215317

318+
func (d *DocGenerator) generateMetricsPage(document model.Document) error {
319+
document.File = metricsFile
320+
document.Title = metricsTitle
321+
document.Description = metricsDescription
322+
323+
base := filepath.Join(d.ctx.targetRoot, d.ctx.targetDoc, document.Path)
324+
if err := os.MkdirAll(base, dirPermissions); err != nil {
325+
return fmt.Errorf("can't make target folder: %w", err)
326+
}
327+
328+
return d.render("doc_metrics", filepath.Join(base, document.File), document)
329+
}
330+
216331
func (d *DocGenerator) loadTemplates() error {
217332
const (
218333
tplExt = ".md.gotmpl"
@@ -222,6 +337,7 @@ func (d *DocGenerator) loadTemplates() error {
222337
index := make(map[string]string, expectedTemplates)
223338
index["doc_index"] = "doc_index"
224339
index["doc_page"] = "doc_page"
340+
index["doc_metrics"] = "doc_metrics"
225341

226342
templates, err := loadTemplatesFromIndex(index, tplExt, templatesFS)
227343
if err != nil {
@@ -331,3 +447,28 @@ func (d *DocGenerator) render(name string, target string, data any) error {
331447
renderMD,
332448
)
333449
}
450+
451+
// writeYAMLMetrics writes the Metrics structure as a YAML file in the hugo generation folder.
452+
//
453+
// This allows doc pages to use metrics directly with shortcodes.
454+
func (d *DocGenerator) writeYAMLMetrics(metrics model.Metrics) error {
455+
base := filepath.Join(d.ctx.targetRoot, "hack", "doc-site", "hugo")
456+
if err := os.MkdirAll(base, dirPermissions); err != nil {
457+
return fmt.Errorf("can't make target folder: %w", err)
458+
}
459+
target := filepath.Join(base, "metrics.yaml")
460+
461+
type containerT struct {
462+
Params struct {
463+
Metrics model.Metrics `yaml:"metrics"`
464+
} `yaml:"params"`
465+
}
466+
var container containerT
467+
container.Params.Metrics = metrics
468+
buf, err := yaml.Marshal(container)
469+
if err != nil {
470+
return err
471+
}
472+
473+
return os.WriteFile(target, buf, filePermissions)
474+
}

codegen/internal/generator/funcmaps/funcmaps.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func FuncMap() template.FuncMap {
5959
"returns": PrintReturns,
6060
"sourceLink": sourceLink,
6161
"titleize": titleize,
62-
"slugize": slugize,
62+
"slugize": Slugize,
6363
"blockquote": blockquote,
6464
"hopen": hugoopen,
6565
"hclose": hugoclose,
@@ -353,8 +353,8 @@ func printDate() string {
353353
return time.Now().Format(time.DateOnly)
354354
}
355355

356-
// slugize converts a name into a markdown ref inside a document.
357-
func slugize(in string) string {
356+
// Slugize converts a name into a markdown ref inside a document.
357+
func Slugize(in string) string {
358358
return strings.ToLower(
359359
strings.Map(func(r rune) rune {
360360
switch r {

codegen/internal/generator/funcmaps/funcmaps_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -822,8 +822,8 @@ func TestSlugize(t *testing.T) {
822822
t.Run(tt.name, func(t *testing.T) {
823823
t.Parallel()
824824

825-
if got := slugize(tt.input); got != tt.expected {
826-
t.Errorf("slugize(%q) = %q, want %q", tt.input, got, tt.expected)
825+
if got := Slugize(tt.input); got != tt.expected {
826+
t.Errorf("Slugize(%q) = %q, want %q", tt.input, got, tt.expected)
827827
}
828828
})
829829
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
title: {{ .Title | quote }}
3+
description: |
4+
{{ titleize .Description }}.
5+
weight: -1
6+
---
7+
8+
{{- with .Metrics }}
9+
10+
## Domains
11+
12+
All assertions are classified into **{{ .Domains }}** domains to help navigate the API, depending on your use case.
13+
14+
## API metrics
15+
16+
Counts for core functionality, excluding variants (formatted, forward, forward-formatted).
17+
18+
| Kind | Count |
19+
| ------------------------ | ----------------- |
20+
| All functions | {{ .Functions }} |
21+
| All core assertions | {{ .Assertions }} |
22+
| Generic assertions | {{ .Generics }} |
23+
| Helpers (not assertions) | {{ .Helpers }} |
24+
| Others | {{ .Others }} |
25+
26+
{{- end }}
27+
28+
## Quick index
29+
30+
Table of core assertions, excluding variants. Each function is side by side with its logical opposite (when available).
31+
32+
| Assertion | Opposite | Domain | Kind |
33+
| ------------------------ | ----------------- | ------ | ---- |
34+
{{- range .QuickIndex }}
35+
| [{{ .Name }}]({{ .Domain }}/#{{ .Anchor }}){{ if .IsGeneric }} {{ hopen }}% icon icon="star" color=orange %{{ hclose }}{{ end }} | {{ if .Opposite }}[{{ .Opposite }}]({{ .Domain }}/#{{ .OppositeAnchor }}){{ end }} | {{ .Domain }} | {{ if .IsHelper }}helper{{ end }} |
36+
{{- end }}
37+

0 commit comments

Comments
 (0)