Skip to content

Commit 0ac64a1

Browse files
committed
doc(codegen): refactored templates and generators for readability
This change introduces the rendering of testable examples on the doc site, with a link to go playground. Several examples may be rendered for a given assertion function. Examples may be either auto-generated like tests or added manually as ad'hoc examples in tests. This refactoring comes together with some polishing effort for the doc site (fixed minor bugs, add custom css, more robust extraction of various notes used for documentation). Signed-off-by: Frederic BIDON <[email protected]>
1 parent 2b96445 commit 0ac64a1

29 files changed

+1341
-604
lines changed

codegen/internal/generator/doc_generator.go

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/go-openapi/testify/codegen/v2/internal/generator/domains"
1515
"github.com/go-openapi/testify/codegen/v2/internal/generator/funcmaps"
1616
"github.com/go-openapi/testify/codegen/v2/internal/model"
17+
exparser "github.com/go-openapi/testify/codegen/v2/internal/scanner/examples-parser"
1718
)
1819

1920
const (
@@ -53,8 +54,11 @@ func (d *DocGenerator) Generate(opts ...GenerateOption) error {
5354
return err
5455
}
5556

56-
// Proposal for enhancement: other fun stuff
57-
// - capture testable examples and render their source code
57+
// capture testable examples from generated packages and attach them to
58+
// the model so templates may render their source code.
59+
if err := d.populateExamples(); err != nil {
60+
return err
61+
}
5862

5963
// reorganize accumulated package-based docs into domain-based docs
6064
//
@@ -128,6 +132,13 @@ func (d *DocGenerator) reorganizeByDomain() (iter.Seq2[string, model.Document],
128132
}
129133
weight++
130134

135+
// populate document context in all children
136+
doc.Package.Context = &doc
137+
for i, fn := range doc.Package.Functions {
138+
fn.Context = &doc
139+
doc.Package.Functions[i] = fn
140+
}
141+
131142
if !yield(doc.Domain, doc) {
132143
return
133144
}
@@ -223,6 +234,93 @@ func (d *DocGenerator) loadTemplates() error {
223234
return nil
224235
}
225236

237+
// populateExamples runs the examples-parser against all generated packages in the
238+
// merged Documentation and attaches the discovered testable examples to the
239+
// corresponding Function and Ident objects.
240+
//
241+
// This must run before [reorganizeByDomain] because domain discovery copies
242+
// functions and types into domain entries.
243+
func (d *DocGenerator) populateExamples() error {
244+
if !d.ctx.runnableExamples {
245+
return nil
246+
}
247+
248+
docs := domains.FlattenDocumentation(d.doc)
249+
250+
// derive the module root from the assertions import that every generated
251+
// package carries (e.g. "github.com/go-openapi/testify/v2").
252+
var rootPkg string
253+
for _, doc := range docs {
254+
if doc.Package != nil && doc.Package.Imports != nil {
255+
if assertionsPath, ok := doc.Package.Imports[assertions]; ok {
256+
rootPkg = path.Dir(path.Dir(assertionsPath))
257+
258+
break
259+
}
260+
}
261+
}
262+
if rootPkg == "" {
263+
return nil // nothing to do
264+
}
265+
266+
workDir, err := filepath.Abs(d.ctx.targetRoot)
267+
if err != nil {
268+
return fmt.Errorf("resolving target root: %w", err)
269+
}
270+
271+
for _, doc := range docs {
272+
pkg := doc.Package
273+
if pkg == nil {
274+
continue
275+
}
276+
277+
// Skip the internal assertions package: testable examples live in the
278+
// generated packages (assert, require), not in the source package.
279+
if path.Base(pkg.Package) == assertions {
280+
continue
281+
}
282+
283+
importPath := rootPkg + "/" + pkg.Package
284+
examples, parseErr := exparser.New(importPath, exparser.WithWorkDir(workDir)).Parse()
285+
if parseErr != nil {
286+
return fmt.Errorf("parsing examples for %s: %w", pkg.Package, parseErr)
287+
}
288+
289+
populateFunctionExamples(pkg, examples)
290+
populateIdentExamples(pkg.Types, examples)
291+
}
292+
293+
return nil
294+
}
295+
296+
func populateFunctionExamples(pkg *model.AssertionPackage, examples exparser.Examples) {
297+
for i, fn := range pkg.Functions {
298+
exs, ok := examples[fn.Name]
299+
if !ok {
300+
continue
301+
}
302+
renderables := make([]model.Renderable, len(exs))
303+
for j := range exs {
304+
renderables[j] = exs[j]
305+
}
306+
pkg.Functions[i].Examples = renderables
307+
}
308+
}
309+
310+
func populateIdentExamples(idents []model.Ident, examples exparser.Examples) {
311+
for i, id := range idents {
312+
exs, ok := examples[id.Name]
313+
if !ok {
314+
continue
315+
}
316+
renderables := make([]model.Renderable, len(exs))
317+
for j := range exs {
318+
renderables[j] = exs[j]
319+
}
320+
idents[i].Examples = renderables
321+
}
322+
}
323+
226324
func (d *DocGenerator) render(name string, target string, data any) error {
227325
return renderTemplate(
228326
d.ctx.index,

codegen/internal/generator/funcmaps/funcmaps.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ func FuncMap() template.FuncMap {
6161
"titleize": titleize,
6262
"slugize": slugize,
6363
"blockquote": blockquote,
64+
"hopen": hugoopen,
65+
"hclose": hugoclose,
66+
"cr": shouldLineFeed,
67+
"testSetup": testSetup,
6468
}
6569
}
6670

@@ -378,3 +382,59 @@ func blockquote(in string) string {
378382

379383
return strings.Join(result, "\n")
380384
}
385+
386+
func hugoopen() string {
387+
return "{{"
388+
}
389+
390+
func hugoclose() string {
391+
return "}}"
392+
}
393+
394+
func shouldLineFeed(dontFeed bool) string {
395+
if dontFeed {
396+
return ""
397+
}
398+
399+
return "\n"
400+
}
401+
402+
// testSetup computes the test parameterization for a function variant.
403+
//
404+
// Supported variants:
405+
// - "assertions": package-level test (e.g., TestEqual)
406+
// - "format": format variant test (e.g., TestEqualf)
407+
// - "forward": forwarded method test (e.g., TestAssertionsEqual)
408+
// - "forward-format": forwarded format method test (e.g., TestAssertionsEqualf)
409+
func testSetup(fn model.Function, variant string, receiver string) model.Function {
410+
switch variant {
411+
case "assertions":
412+
fn.TestCall = fn.Name + fn.GenericSuffix() + "(mock, "
413+
fn.TestMock = fmt.Sprintf("mock := new(%s)", fn.UseMock)
414+
fn.TestErrorPrefix = fn.Name
415+
fn.TestPanicWrapper = "Panics(t, "
416+
fn.TestMockFailure = fn.FailMsg()
417+
case "format":
418+
fn.TestCall = fn.Name + "f" + fn.GenericSuffix() + "(mock, "
419+
fn.TestMock = fmt.Sprintf("mock := new(%s)", fn.UseMock)
420+
fn.TestErrorPrefix = fn.Name + "f"
421+
fn.TestPanicWrapper = "Panics(t, "
422+
fn.TestMockFailure = fn.FailMsg("f")
423+
fn.TestMsg = "test message"
424+
case "forward":
425+
fn.TestCall = "a." + fn.Name + "("
426+
fn.TestMock = fmt.Sprintf("mock := new(%s)\na := New(mock)", fn.UseMock)
427+
fn.TestErrorPrefix = receiver + "." + fn.Name
428+
fn.TestPanicWrapper = "a.Panics("
429+
fn.TestMockFailure = fn.FailMsg(receiver, "")
430+
case "forward-format":
431+
fn.TestCall = "a." + fn.Name + "f("
432+
fn.TestMock = fmt.Sprintf("mock := new(%s)\na := New(mock)", fn.UseMock)
433+
fn.TestErrorPrefix = receiver + "." + fn.Name + "f"
434+
fn.TestPanicWrapper = "a.Panics("
435+
fn.TestMockFailure = fn.FailMsg(receiver, "f")
436+
fn.TestMsg = "test message"
437+
}
438+
439+
return fn
440+
}

0 commit comments

Comments
 (0)