From 284491a93a220e60bb0d4600ee86e4048afc3514 Mon Sep 17 00:00:00 2001 From: zirain Date: Mon, 8 Apr 2024 20:55:15 +0800 Subject: [PATCH] Support custom markers (#76) --------- Co-authored-by: Thibault Richard --- README.md | 55 ++++- config/config.go | 15 ++ processor/config.go | 2 + processor/processor.go | 30 ++- test.sh | 26 ++- test/api/v1/guestbook_types.go | 1 + test/config.yaml | 3 + test/hide.md | 282 +++++++++++++++++++++++ test/templates/markdown/gv_details.tpl | 19 ++ test/templates/markdown/gv_list.tpl | 15 ++ test/templates/markdown/type.tpl | 43 ++++ test/templates/markdown/type_members.tpl | 8 + 12 files changed, 472 insertions(+), 27 deletions(-) create mode 100644 test/hide.md create mode 100644 test/templates/markdown/gv_details.tpl create mode 100644 test/templates/markdown/gv_list.tpl create mode 100644 test/templates/markdown/type.tpl create mode 100644 test/templates/markdown/type_members.tpl diff --git a/README.md b/README.md index 6100e44..92268c0 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,12 @@ ![](https://github.com/elastic/crd-ref-docs/workflows/Build/badge.svg) - -CRD Reference Documentation Generator -====================================== +# CRD Reference Documentation Generator Generates API reference documentation by scanning a source tree for exported CRD types. This is a fresh implementation inspired by the https://github.com/ahmetb/gen-crd-api-reference-docs project. While trying to adopt the `gen-crd-api-refernce-docs` to generate documentation for [Elastic Cloud on Kubernetes](https://github.com/elastic/cloud-on-k8s), we encountered a few shortcomings such as the lack of support for Go modules, slow scan times, and rendering logic that was hard to adapt to Asciidoc (our preferred documentation markup language). This project attempts to address those issues by re-implementing the type discovery logic and decoupling the rendering logic so that different markup formats can be supported. - -Usage ------ +## Usage Pre-built Linux binaries can be downloaded from the Github Releases tab. Alternatively, you can download and build the source with Go tooling: @@ -26,8 +22,7 @@ crd-ref-docs \ --config=config.yaml ``` -By default, documentation is rendered in Asciidoc format. -In order to generate documentation in Markdown format, you will have to specify the `markdown` renderer: +By default, documentation is rendered in Asciidoc format. In order to generate documentation in Markdown format, you will have to specify the `markdown` renderer: ``` crd-ref-docs \ @@ -36,8 +31,7 @@ crd-ref-docs \ --renderer=markdown ``` -Default templates are embedded in the binary. -You may provide your own templates by specifying the templates directory: +Default templates are embedded in the binary. You may provide your own templates by specifying the templates directory: ``` crd-ref-docs \ @@ -85,3 +79,44 @@ render: package: sigs.k8s.io/gateway-api/apis/v1beta1 link: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.SecretObjectReference ``` + +### Advanced Features + +#### Custom Markers + +You can add custom markers to your CRD types to provide additional information in the generated documentation. +For example, you can add a `hidefromdoc` marker to indicate that a type is hide from the documentation. + +```yaml +processor: + ignoreGroupVersions: + - "GVK" + ignoreTypes: + - "Embedded[2-4]$" + ignoreFields: + - "status$" + - "TypeMeta$" + customMarkers: + - name: "hidefromdoc" + target: field + +render: + kubernetesVersion: 1.25 + knownTypes: + - name: SecretObjectReference + package: sigs.k8s.io/gateway-api/apis/v1beta1 + link: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.SecretObjectReference +``` + +You can then add the `hidefromdoc` marker to the field you want to hidden from the documentation. + +```go +type Embedded1 struct { + Embedded2 `json:",inline"` + // +hidefromdoc + E string `json:"e,omitempty"` + EmbeddedX `json:",inline"` +} +``` + +Then update the templates to render the custom markers. You can find an example [here](./test/templates/markdown/type.tpl). diff --git a/config/config.go b/config/config.go index 5d940de..763b2e9 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,7 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. + package config import ( @@ -34,8 +35,22 @@ type ProcessorConfig struct { IgnoreFields []string `json:"ignoreFields"` IgnoreGroupVersions []string `json:"ignoreGroupVersions"` UseRawDocstring bool `json:"useRawDocstring"` + CustomMarkers []Marker `json:"customMarkers"` +} + +type Marker struct { + Name string + Target TargetType } +type TargetType string + +const ( + TargetTypePackage TargetType = "package" + TargetTypeType TargetType = "type" + TargetTypeField TargetType = "field" +) + type RenderConfig struct { KnownTypes []*KnownType `json:"knownTypes"` KubernetesVersion string `json:"kubernetesVersion"` diff --git a/processor/config.go b/processor/config.go index ec479be..37cd640 100644 --- a/processor/config.go +++ b/processor/config.go @@ -33,6 +33,7 @@ func compileConfig(conf *config.Config) (cc *compiledConfig, err error) { ignoreFields: make([]*regexp.Regexp, len(conf.Processor.IgnoreFields)), ignoreGroupVersions: make([]*regexp.Regexp, len(conf.Processor.IgnoreGroupVersions)), useRawDocstring: conf.Processor.UseRawDocstring, + markers: conf.Processor.CustomMarkers, } for i, t := range conf.Processor.IgnoreTypes { @@ -61,6 +62,7 @@ type compiledConfig struct { ignoreFields []*regexp.Regexp ignoreGroupVersions []*regexp.Regexp useRawDocstring bool + markers []config.Marker } func (cc *compiledConfig) shouldIgnoreGroupVersion(gv string) bool { diff --git a/processor/processor.go b/processor/processor.go index a019354..dc876ad 100644 --- a/processor/processor.go +++ b/processor/processor.go @@ -14,6 +14,7 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. + package processor import ( @@ -124,7 +125,7 @@ func Process(config *config.Config) ([]types.GroupVersionDetails, error) { } func newProcessor(compiledConfig *compiledConfig, maxDepth int) (*processor, error) { - registry, err := mkRegistry() + registry, err := mkRegistry(compiledConfig.markers) if err != nil { return nil, err } @@ -485,20 +486,37 @@ func (p *processor) addReference(parent *types.Type, child *types.Type) { } } -func mkRegistry() (*markers.Registry, error) { +func mkRegistry(customMarkers []config.Marker) (*markers.Registry, error) { registry := &markers.Registry{} - err := registry.Define(objectRootMarker, markers.DescribesType, true) - if err != nil { + if err := registry.Define(objectRootMarker, markers.DescribesType, true); err != nil { return nil, err } for _, marker := range crdmarkers.AllDefinitions { - err = registry.Register(marker.Definition) - if err != nil { + if err := registry.Register(marker.Definition); err != nil { return nil, err } } + for _, marker := range customMarkers { + t := markers.DescribesField + switch marker.Target { + case config.TargetTypePackage: + t = markers.DescribesPackage + case config.TargetTypeType: + t = markers.DescribesType + case config.TargetTypeField: + t = markers.DescribesField + default: + zap.S().Warnf("Skipping custom marker %s with unknown target type %s", marker.Name, marker.Target) + continue + } + + if err := registry.Define(marker.Name, t, struct{}{}); err != nil { + return nil, fmt.Errorf("failed to define custom marker %s: %w", marker.Name, err) + } + } + return registry, nil } diff --git a/test.sh b/test.sh index 9387542..cc5c24f 100755 --- a/test.sh +++ b/test.sh @@ -33,6 +33,7 @@ run_test() { local renderer=asciidoctor local templates_dir= + local expected=expected.asciidoc while :; do case "${1:-}" in @@ -54,6 +55,15 @@ run_test() { exit 1 fi ;; + --expected) + if [[ -n "${2:-}" ]]; then + expected="$2" + shift + else + printf "ERROR: '--expected' cannot be empty.\n\n" >&2 + exit 1 + fi + ;; *) break ;; @@ -67,13 +77,6 @@ run_test() { args+=(--templates-dir="$templates_dir") fi - local expected - if [[ "$renderer" == "asciidoctor" ]]; then - expected=expected.asciidoc - else - expected=expected.md - fi - ( cd "$SCRIPT_DIR" cmd=(go run main.go "${args[@]}") @@ -85,7 +88,7 @@ run_test() { if diff=$(diff -a -y --suppress-common-lines "${SCRIPT_DIR}/test/${expected}" "$actual"); then echo "OK" else - echo "ERROR: outputs differ" + echo "ERROR: outputs differ with ${expected}" echo "" echo "$diff" exit 1 @@ -94,6 +97,7 @@ run_test() { } run_test -run_test --renderer asciidoctor --templates-dir templates/asciidoctor -run_test --renderer markdown -run_test --renderer markdown --templates-dir templates/markdown +run_test --renderer asciidoctor --templates-dir templates/asciidoctor --expected expected.asciidoc +run_test --renderer markdown --expected expected.md +run_test --renderer markdown --templates-dir templates/markdown --expected expected.md +run_test --renderer markdown --templates-dir test/templates/markdown --expected hide.md diff --git a/test/api/v1/guestbook_types.go b/test/api/v1/guestbook_types.go index f9a6705..8389059 100644 --- a/test/api/v1/guestbook_types.go +++ b/test/api/v1/guestbook_types.go @@ -35,6 +35,7 @@ type Embedded struct { } type Embedded1 struct { Embedded2 `json:",inline"` + // +hidefromdoc E string `json:"e,omitempty"` EmbeddedX `json:",inline"` } diff --git a/test/config.yaml b/test/config.yaml index 5879409..b35e7a9 100644 --- a/test/config.yaml +++ b/test/config.yaml @@ -6,6 +6,9 @@ processor: ignoreFields: - "status$" - "TypeMeta$" + customMarkers: + - name: "hidefromdoc" + target: field render: kubernetesVersion: 1.25 diff --git a/test/hide.md b/test/hide.md new file mode 100644 index 0000000..eadacb9 --- /dev/null +++ b/test/hide.md @@ -0,0 +1,282 @@ +# API Reference + +## Packages +- [webapp.test.k8s.elastic.co/common](#webapptestk8selasticcocommon) +- [webapp.test.k8s.elastic.co/v1](#webapptestk8selasticcov1) + + +## webapp.test.k8s.elastic.co/common + +Package common contains common API Schema definitions + + + +#### CommonString + +_Underlying type:_ _string_ + + + + + +_Appears in:_ +- [GuestbookSpec](#guestbookspec) +- [GuestbookStatus](#guestbookstatus) + + + + +## webapp.test.k8s.elastic.co/v1 + +Package v1 contains API Schema definitions for the webapp v1 API group + +### Resource Types +- [Embedded](#embedded) +- [Guestbook](#guestbook) +- [GuestbookList](#guestbooklist) +- [Underlying](#underlying) + + + +#### Embedded + + + + + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `webapp.test.k8s.elastic.co/v1` | | | +| `kind` _string_ | `Embedded` | | | + +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | + +| `a` _string_ | | | | + +| `x` _string_ | | | | + + +#### Embedded1 + + + + + + + +_Appears in:_ +- [Embedded](#embedded) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | + +| `x` _string_ | | | | + + +#### EmbeddedX + + + + + + + +_Appears in:_ +- [Embedded](#embedded) +- [Embedded1](#embedded1) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | + +| `x` _string_ | | | | + + +#### Guestbook + + + +Guestbook is the Schema for the guestbooks API. + + + +_Appears in:_ +- [GuestbookList](#guestbooklist) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `webapp.test.k8s.elastic.co/v1` | | | +| `kind` _string_ | `Guestbook` | | | + +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | + +| `spec` _[GuestbookSpec](#guestbookspec)_ | | \{ page:1 \} | | + + +#### GuestbookEntry + + + +GuestbookEntry defines an entry in a guest book. + + + +_Appears in:_ +- [GuestbookSpec](#guestbookspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | + +| `name` _string_ | Name of the guest (pipe \| should be escaped) | | MaxLength: 80
Pattern: `0*[a-z0-9]*[a-z]*[0-9]`
| + +| `time` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#time-v1-meta)_ | Time of entry | | | + +| `comment` _string_ | Comment by guest. This can be a multi-line comment.
Like this one.
Now let's test a list:
* a
* b

Another isolated comment.

Looks good? | | Pattern: `0*[a-z0-9]*[a-z]*[0-9]*`
| + +| `rating` _[Rating](#rating)_ | Rating provided by the guest | | Maximum: 5
Minimum: 1
| + + +#### GuestbookHeader + +_Underlying type:_ _string_ + +GuestbookHeaders are strings to include at the top of a page. + + + +_Appears in:_ +- [GuestbookSpec](#guestbookspec) + + + +#### GuestbookList + + + +GuestbookList contains a list of Guestbook. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `webapp.test.k8s.elastic.co/v1` | | | +| `kind` _string_ | `GuestbookList` | | | + +| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | + +| `items` _[Guestbook](#guestbook) array_ | | | | + + +#### GuestbookSpec + + + +GuestbookSpec defines the desired state of Guestbook. + + + +_Appears in:_ +- [Guestbook](#guestbook) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | + +| `page` _[PositiveInt](#positiveint)_ | Page indicates the page number | 1 | Minimum: 1
| + +| `entries` _[GuestbookEntry](#guestbookentry) array_ | Entries contain guest book entries for the page | | | + +| `selector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#labelselector-v1-meta)_ | Selector selects something | | | + +| `headers` _[GuestbookHeader](#guestbookheader) array_ | Headers contains a list of header items to include in the page | | MaxItems: 10
UniqueItems: true
| + +| `certificateRef` _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.SecretObjectReference)_ | CertificateRef is a reference to a secret containing a certificate | | | + +| `str` _[CommonString](#commonstring)_ | | | | + + + + +#### PositiveInt + +_Underlying type:_ _integer_ + + + +_Validation:_ +- Minimum: 1 + +_Appears in:_ +- [GuestbookSpec](#guestbookspec) + + + +#### Rating + +_Underlying type:_ _integer_ + +Rating is the rating provided by a guest. + +_Validation:_ +- Maximum: 5 +- Minimum: 1 + +_Appears in:_ +- [GuestbookEntry](#guestbookentry) + + + + + +#### Underlying + + + +Underlying tests that Underlying1's underlying type is Underlying2 instead of string. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `webapp.test.k8s.elastic.co/v1` | | | +| `kind` _string_ | `Underlying` | | | + +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | + +| `a` _[Underlying1](#underlying1)_ | | b | MaxLength: 10
| + + +#### Underlying1 + +_Underlying type:_ _[Underlying2](#underlying2)_ + +Underlying1 has an underlying type with an underlying type + +_Validation:_ +- MaxLength: 10 + +_Appears in:_ +- [Underlying](#underlying) + + + +#### Underlying2 + +_Underlying type:_ _string_ + +Underlying2 is a string alias + +_Validation:_ +- MaxLength: 10 + +_Appears in:_ +- [Underlying1](#underlying1) + + + diff --git a/test/templates/markdown/gv_details.tpl b/test/templates/markdown/gv_details.tpl new file mode 100644 index 0000000..30ad0d7 --- /dev/null +++ b/test/templates/markdown/gv_details.tpl @@ -0,0 +1,19 @@ +{{- define "gvDetails" -}} +{{- $gv := . -}} + +## {{ $gv.GroupVersionString }} + +{{ $gv.Doc }} + +{{- if $gv.Kinds }} +### Resource Types +{{- range $gv.SortedKinds }} +- {{ $gv.TypeForKind . | markdownRenderTypeLink }} +{{- end }} +{{ end }} + +{{ range $gv.SortedTypes }} +{{ template "type" . }} +{{ end }} + +{{- end -}} diff --git a/test/templates/markdown/gv_list.tpl b/test/templates/markdown/gv_list.tpl new file mode 100644 index 0000000..a4d3dad --- /dev/null +++ b/test/templates/markdown/gv_list.tpl @@ -0,0 +1,15 @@ +{{- define "gvList" -}} +{{- $groupVersions := . -}} + +# API Reference + +## Packages +{{- range $groupVersions }} +- {{ markdownRenderGVLink . }} +{{- end }} + +{{ range $groupVersions }} +{{ template "gvDetails" . }} +{{ end }} + +{{- end -}} diff --git a/test/templates/markdown/type.tpl b/test/templates/markdown/type.tpl new file mode 100644 index 0000000..3e1bc1e --- /dev/null +++ b/test/templates/markdown/type.tpl @@ -0,0 +1,43 @@ +{{- define "type" -}} +{{- $type := . -}} +{{- if markdownShouldRenderType $type -}} + +#### {{ $type.Name }} + +{{ if $type.IsAlias }}_Underlying type:_ _{{ markdownRenderTypeLink $type.UnderlyingType }}_{{ end }} + +{{ $type.Doc }} + +{{ if $type.Validation -}} +_Validation:_ +{{- range $type.Validation }} +- {{ . }} +{{- end }} +{{- end }} + +{{ if $type.References -}} +_Appears in:_ +{{- range $type.SortedReferences }} +- {{ markdownRenderTypeLink . }} +{{- end }} +{{- end }} + +{{ if $type.Members -}} +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +{{ if $type.GVK -}} +| `apiVersion` _string_ | `{{ $type.GVK.Group }}/{{ $type.GVK.Version }}` | | | +| `kind` _string_ | `{{ $type.GVK.Kind }}` | | | +{{ end -}} + +{{ range $type.Members -}} +{{ with .Markers.hidefromdoc -}} +{{ else }} +| `{{ .Name }}` _{{ markdownRenderType .Type }}_ | {{ template "type_members" . }} | {{ markdownRenderDefault .Default }} | {{ range .Validation -}} {{ . }}
{{ end }} | +{{ end -}} +{{ end -}} + +{{ end -}} + +{{- end -}} +{{- end -}} diff --git a/test/templates/markdown/type_members.tpl b/test/templates/markdown/type_members.tpl new file mode 100644 index 0000000..041758a --- /dev/null +++ b/test/templates/markdown/type_members.tpl @@ -0,0 +1,8 @@ +{{- define "type_members" -}} +{{- $field := . -}} +{{- if eq $field.Name "metadata" -}} +Refer to Kubernetes API documentation for fields of `metadata`. +{{- else -}} +{{ markdownRenderFieldDoc $field.Doc }} +{{- end -}} +{{- end -}}