Skip to content
Open
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
54 changes: 30 additions & 24 deletions docs/generate_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package main

import (
"bytes"
"errors"
"fmt"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -51,15 +52,23 @@ var serverCommands stringSlice = []string{"nuts config", "nuts server", "nuts cr

func generateDocs() {
system := cmd.CreateSystem(func() {})
generateServerOptions(system)
generateCLICommands(system)
for fileName, content := range generatedDocFiles(system) {
if err := os.WriteFile(fileName, content, os.ModePerm); err != nil {
panic(err)
}
}
}

func generateCLICommands(system *core.System) {
const targetFile = "docs/pages/deployment/cli-reference.rst"
writer, _ := os.OpenFile(targetFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
defer writer.Close()
// generatedDocFiles renders all documentation files that are generated from the code, keyed by their (repo-relative)
// output path. generateDocs writes them to disk; the up-to-date test compares them against the committed files.
func generatedDocFiles(system *core.System) map[string][]byte {
files := renderServerOptions(system)
files["docs/pages/deployment/cli-reference.rst"] = renderCLICommands(system)
return files
}

func renderCLICommands(system *core.System) []byte {
writer := new(bytes.Buffer)
_, _ = writer.WriteString(".. _nuts-cli-reference:" + newline + newline)
writeHeader(writer, "Server CLI Command Reference", 0)

Expand All @@ -73,6 +82,7 @@ func generateCLICommands(system *core.System) {
panic(err)
}
_, _ = io.WriteString(writer, newline)
return writer.Bytes()
}

func writeHeader(writer io.Writer, header string, level int) {
Expand All @@ -81,7 +91,7 @@ func writeHeader(writer io.Writer, header string, level int) {
_, _ = writer.Write([]byte(strings.Repeat(c, len(header)) + newline + newline))
}

func generateServerOptions(system *core.System) {
func renderServerOptions(system *core.System) map[string][]byte {
flags := make(map[string]*pflag.FlagSet)
// Resolve root command flags
globalFlags := core.FlagSet()
Expand Down Expand Up @@ -137,8 +147,10 @@ func generateServerOptions(system *core.System) {
},
}

generatePartitionedConfigOptionsDocs("Server Options", "docs/pages/deployment/server_options.rst", filterFlags(flags, v5FlagsPredicates, true))
generatePartitionedConfigOptionsDocs("did:nuts/gRPC Server Options", "docs/pages/deployment/server_options_didnuts.rst", filterFlags(flags, v5FlagsPredicates, false))
return map[string][]byte{
"docs/pages/deployment/server_options.rst": renderPartitionedConfigOptionsDocs("Server Options", filterFlags(flags, v5FlagsPredicates, true)),
"docs/pages/deployment/server_options_didnuts.rst": renderPartitionedConfigOptionsDocs("did:nuts/gRPC Server Options", filterFlags(flags, v5FlagsPredicates, false)),
}
}

func filterFlags(flags map[string]*pflag.FlagSet, predicates []func(f *pflag.Flag) bool, exclude bool) map[string]*pflag.FlagSet {
Expand Down Expand Up @@ -193,7 +205,7 @@ func extractFlagsForEngine(flagSet *pflag.FlagSet, config interface{}, engineNam
return &result, nil
}

func generatePartitionedConfigOptionsDocs(tableName, fileName string, flags map[string]*pflag.FlagSet) {
func renderPartitionedConfigOptionsDocs(tableName string, flags map[string]*pflag.FlagSet) []byte {
var keys []string
for key := range flags {
keys = append(keys, key)
Expand All @@ -210,7 +222,7 @@ func generatePartitionedConfigOptionsDocs(tableName, fileName string, flags map[
}
values = append(values, flagsToSortedValues(flags[key])...)
}
generateRstTable(tableName, fileName, values)
return renderRstTable(tableName, values)
}

func flagsToSortedValues(flags *pflag.FlagSet) [][]rstValue {
Expand Down Expand Up @@ -242,19 +254,13 @@ func normalizeDefaultValue(f *pflag.Flag) string {
return defValue
}

func generateRstTable(tableName, fileName string, values [][]rstValue) {
optionsFile, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
if err != nil {
panic(err)
}
defer optionsFile.Close()
optionsFile.WriteString(fmt.Sprintf(".. table:: %s\n", tableName))
optionsFile.WriteString(" :widths: 20 30 50\n")
optionsFile.WriteString(" :class: options-table\n\n")
printRstTable(vals("Key", "Default", "Description"), values, optionsFile)
if err := optionsFile.Sync(); err != nil {
panic(err)
}
func renderRstTable(tableName string, values [][]rstValue) []byte {
buffer := new(bytes.Buffer)
fmt.Fprintf(buffer, ".. table:: %s\n", tableName)
buffer.WriteString(" :widths: 20 30 50\n")
buffer.WriteString(" :class: options-table\n\n")
printRstTable(vals("Key", "Default", "Description"), values, buffer)
return buffer.Bytes()
}

// KeyList sorts keys alphabetically, with any nested content at the end.
Expand Down
42 changes: 42 additions & 0 deletions docs/generate_docs_uptodate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2026 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package main

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

"github.com/nuts-foundation/nuts-node/cmd"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestGeneratedDocsAreUpToDate fails when the generated documentation differs from what is committed,
// so a contributor who changed a config option or CLI command but forgot to run `make cli-docs` is caught by CI.
func TestGeneratedDocsAreUpToDate(t *testing.T) {
system := cmd.CreateSystem(func() {})
for fileName, expected := range generatedDocFiles(system) {
// generatedDocFiles keys are repo-relative paths (docs/...); tests run from the docs/ package directory.
committed, err := os.ReadFile(filepath.Join("..", fileName))
require.NoError(t, err)
assert.Equalf(t, string(committed), string(expected),
"%s is out of date; run `make cli-docs` and commit the result", fileName)
}
}
2 changes: 1 addition & 1 deletion docs/pages/deployment/server_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,5 @@
tracing.servicename Service name reported to the tracing backend. Defaults to 'nuts-node'.
**policy**
policy.directory ./config/policy Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping.
policy.authzen.endpoint Base URL of the AuthZen PDP endpoint. Required when any credential profile uses scope_policy 'dynamic'.
policy.authzen.endpoint Base URL of the AuthZen PDP endpoint. Required when any credential profile uses scope_policy 'dynamic'; the node refuses to start if such a profile is configured but this flag is empty.
======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ============================================================================================================================================================================================================================================================================================================================================
Loading