diff --git a/tools/api-docs-generator/README.md b/tools/api-docs-generator/README.md index 176f0c5789e0..e0674d53a5ef 100644 --- a/tools/api-docs-generator/README.md +++ b/tools/api-docs-generator/README.md @@ -15,6 +15,20 @@ To generate the API documentation locally, run: make run ``` +## Config + +Generator is configured with `config.yml`. The following options are available: + +categoryContext: additional context for the category of the API documentation, for example: +```yaml +categoryContext: + - name: licenses-v1 + hint: | + **Note:** When you import or update Projects, changes will be reflected in the endpoint results after a one-hour delay. +``` +will add a hint on the top of the `licenses` category, for the v1 API. + + ## Development To test the utility, run: diff --git a/tools/api-docs-generator/config.yml b/tools/api-docs-generator/config.yml index 45bee283f9f1..1dcbe9a97be8 100644 --- a/tools/api-docs-generator/config.yml +++ b/tools/api-docs-generator/config.yml @@ -7,6 +7,9 @@ specs: docsHint: This document uses the v1 API. For more details, see the [v1 API](../v1-api-overview/). - path: docs/.gitbook/assets/rest-spec.json docsHint: This document uses the REST API. For more details, see the [Authentication for API](../authentication-for-api/) page. - +categoryContext: +- name: licenses-v1 + hint: | + **Note:** When you import or update Projects, changes will be reflected in the endpoint results after a one-hour delay. output: apiReferencePath: docs/snyk-api/reference \ No newline at end of file diff --git a/tools/api-docs-generator/config/config.go b/tools/api-docs-generator/config/config.go index a5b2a2bd47b6..b90e346deeb7 100644 --- a/tools/api-docs-generator/config/config.go +++ b/tools/api-docs-generator/config/config.go @@ -22,17 +22,36 @@ type Output struct { } type Config struct { - Fetcher Fetcher `yaml:"fetcher"` - Specs []Spec `yaml:"specs"` - Output Output `yaml:"output"` + Fetcher Fetcher `yaml:"fetcher"` + Specs []Spec `yaml:"specs"` + Output Output `yaml:"output"` + CategoryContext CategoryContexts `yaml:"categoryContext"` } -func Parse(filename string) (Config, error) { +type CategoryContexts []CategoryContext + +func (contexts CategoryContexts) ToMap() map[string]string { + m := make(map[string]string) + for i := range contexts { + m[contexts[i].Name] = contexts[i].Hint + } + return m +} + +type CategoryContext struct { + Name string `yaml:"name"` + Hint string `yaml:"hint"` +} + +func Parse(filename string) (*Config, error) { cfg := Config{} file, err := os.Open(filename) if err != nil { - return cfg, err + return nil, err } err = yaml.NewDecoder(file).Decode(&cfg) - return cfg, err + if err != nil { + return nil, err + } + return &cfg, err } diff --git a/tools/api-docs-generator/config/config_test.go b/tools/api-docs-generator/config/config_test.go index 0063c87e8d92..d35252794958 100644 --- a/tools/api-docs-generator/config/config_test.go +++ b/tools/api-docs-generator/config/config_test.go @@ -10,7 +10,7 @@ func TestParse(t *testing.T) { tests := []struct { name string args func() string - want Config + want *Config wantErr bool }{ { @@ -29,7 +29,7 @@ specs: output: apiReferencePath: snyk-api/reference`) }, - want: Config{ + want: &Config{ Fetcher: Fetcher{"source", "destination"}, Specs: []Spec{ {".gitbook/assets/spec.yaml", " (v1)", "hint 1"}, diff --git a/tools/api-docs-generator/fetcher/spec_fetcher.go b/tools/api-docs-generator/fetcher/spec_fetcher.go index 5c5f548cd8f9..b41425a646ff 100644 --- a/tools/api-docs-generator/fetcher/spec_fetcher.go +++ b/tools/api-docs-generator/fetcher/spec_fetcher.go @@ -15,7 +15,7 @@ import ( "github.com/snyk/user-docs/tools/api-docs-generator/config" ) -func FetchSpec(ctx context.Context, cfg config.Config, directory string) error { +func FetchSpec(ctx context.Context, cfg *config.Config, directory string) error { // #nosec G107 // cfg.Fetcher.Source is a URL from config and does not contain user input resp, err := get(ctx, cfg.Fetcher.Source) if err != nil { diff --git a/tools/api-docs-generator/generator/reference_docs.go b/tools/api-docs-generator/generator/reference_docs.go index 9b3fb0c3b29c..051d22f690ca 100644 --- a/tools/api-docs-generator/generator/reference_docs.go +++ b/tools/api-docs-generator/generator/reference_docs.go @@ -22,7 +22,7 @@ type operationPath struct { docsHint string } -func GenerateReferenceDocs(cfg config.Config, docsBasePath string) error { +func GenerateReferenceDocs(cfg *config.Config, docsBasePath string) error { aggregatedDocs, err := aggregateSpecs(cfg, docsBasePath) if err != nil { return err @@ -33,7 +33,7 @@ func GenerateReferenceDocs(cfg config.Config, docsBasePath string) error { destinationPath := path.Join(docsBasePath, cfg.Output.APIReferencePath, labelToFileName(label)) summary = append(summary, fmt.Sprintf("* [%s](%s)\n", label, path.Join(cfg.Output.APIReferencePath, labelToFileName(label)))) - err := renderReferenceDocsPage(destinationPath, label, docsBasePath, operations) + err := renderReferenceDocsPage(destinationPath, label, docsBasePath, operations, cfg.CategoryContext) if err != nil { return err } @@ -45,7 +45,7 @@ func GenerateReferenceDocs(cfg config.Config, docsBasePath string) error { return nil } -func aggregateSpecs(cfg config.Config, docsBasePath string) (map[string][]operationPath, error) { +func aggregateSpecs(cfg *config.Config, docsBasePath string) (map[string][]operationPath, error) { aggregatedDocs := map[string][]operationPath{} for _, spec := range cfg.Specs { @@ -73,17 +73,30 @@ func aggregateSpecs(cfg config.Config, docsBasePath string) (map[string][]operat return aggregatedDocs, nil } -func renderReferenceDocsPage(filePath, label, docsPath string, operation []operationPath) error { +func renderReferenceDocsPage(filePath, label, docsPath string, operation []operationPath, categoryContext config.CategoryContexts) error { docsFile, err := os.Create(filePath) if err != nil { return err } - fmt.Fprintf(docsFile, `# %s + _, err = fmt.Fprintf(docsFile, `# %s {%% hint style="info" %%} %s {%% endhint %%}`, label, operation[0].docsHint) + if err != nil { + return err + } + if categoryContextHint, found := categoryContext.ToMap()[labelToKey(label)]; found { + _, err = fmt.Fprintln(docsFile) + if err != nil { + return err + } + _, err = fmt.Fprint(docsFile, categoryContextHint) + if err != nil { + return err + } + } // sort for stability sort.Slice(operation, func(i, j int) bool { @@ -113,9 +126,14 @@ func renderReferenceDocsPage(filePath, label, docsPath string, operation []opera } func labelToFileName(label string) string { + return labelToKey(label) + ".md" +} + +func labelToKey(label string) string { replacements := []string{"(", ")"} for _, replacement := range replacements { label = strings.ReplaceAll(label, replacement, "") } - return strings.ToLower(strings.ReplaceAll(label, " ", "-")) + ".md" + + return strings.ToLower(strings.ReplaceAll(label, " ", "-")) } diff --git a/tools/api-docs-generator/generator/reference_docs_test.go b/tools/api-docs-generator/generator/reference_docs_test.go index 5168c6727bf4..a450dc0e9afb 100644 --- a/tools/api-docs-generator/generator/reference_docs_test.go +++ b/tools/api-docs-generator/generator/reference_docs_test.go @@ -5,6 +5,8 @@ import ( "path" "testing" + "github.com/snyk/user-docs/tools/api-docs-generator/config" + "github.com/stretchr/testify/assert" ) @@ -43,10 +45,11 @@ func Test_labelToFileName(t *testing.T) { func Test_renderReferenceDocsPage(t *testing.T) { type args struct { - filePath string - label string - docsPath string - operation []operationPath + filePath string + label string + docsPath string + operation []operationPath + categoryContext config.CategoryContexts } testDir := t.TempDir() tests := []struct { @@ -69,6 +72,84 @@ func Test_renderReferenceDocsPage(t *testing.T) { docsHint: "This is a hint", }, }, + categoryContext: config.CategoryContexts{}, + }, + checker: func(t *testing.T, args args) { + t.Helper() + content, err := os.ReadFile(args.filePath) + if err != nil { + t.Fatal(err) + } + renderedFileContents := string(content) + assert.Contains(t, renderedFileContents, "# Apps", "rendered file contains header") + assert.Contains(t, renderedFileContents, "This is a hint", "rendered file does not contain hint") + assert.Contains(t, renderedFileContents, `path="/apps"`, "rendered file does not contain path") + assert.Contains(t, renderedFileContents, `method="get"`, "rendered file does not contain method") + assert.Contains(t, renderedFileContents, `src="../foo/test/apps-spec.yaml"`, "renders relative path to spec file") + }, + }, + { + name: "renders reference docs page, with category context hint", + + args: args{ + filePath: createTempFile(t, testDir, "existing content"), + label: "Apps", + docsPath: testDir, + operation: []operationPath{ + { + specPath: "foo/test/apps-spec.yaml", + pathURL: "/apps", + method: "GET", + docsHint: "This is a hint", + }, + }, + categoryContext: config.CategoryContexts{ + { + Name: "apps", + Hint: "This is a hint from category context", + }, + { + Name: "not-apps", + Hint: "This is a hint from another context", + }, + }, + }, + checker: func(t *testing.T, args args) { + t.Helper() + content, err := os.ReadFile(args.filePath) + if err != nil { + t.Fatal(err) + } + renderedFileContents := string(content) + assert.Contains(t, renderedFileContents, "# Apps", "rendered file contains header") + assert.Contains(t, renderedFileContents, "This is a hint", "rendered file does not contain hint") + assert.Contains(t, renderedFileContents, `path="/apps"`, "rendered file does not contain path") + assert.Contains(t, renderedFileContents, `method="get"`, "rendered file does not contain method") + assert.Contains(t, renderedFileContents, `src="../foo/test/apps-spec.yaml"`, "renders relative path to spec file") + assert.Contains(t, renderedFileContents, `This is a hint from category context`, "renders category context hint") + }, + }, + { + name: "renders reference docs page, without category context hint if no matches", + + args: args{ + filePath: createTempFile(t, testDir, "existing content"), + label: "Apps", + docsPath: testDir, + operation: []operationPath{ + { + specPath: "foo/test/apps-spec.yaml", + pathURL: "/apps", + method: "GET", + docsHint: "This is a hint", + }, + }, + categoryContext: config.CategoryContexts{ + { + Name: "not-apps", + Hint: "This is a hint from category context", + }, + }, }, checker: func(t *testing.T, args args) { t.Helper() @@ -82,12 +163,17 @@ func Test_renderReferenceDocsPage(t *testing.T) { assert.Contains(t, renderedFileContents, `path="/apps"`, "rendered file does not contain path") assert.Contains(t, renderedFileContents, `method="get"`, "rendered file does not contain method") assert.Contains(t, renderedFileContents, `src="../foo/test/apps-spec.yaml"`, "renders relative path to spec file") + assert.NotContains(t, renderedFileContents, `This is a hint from category context`, "does not render category context hint") }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := renderReferenceDocsPage(tt.args.filePath, tt.args.label, tt.args.docsPath, tt.args.operation); (err != nil) != tt.wantErr { + if err := renderReferenceDocsPage(tt.args.filePath, + tt.args.label, + tt.args.docsPath, + tt.args.operation, + tt.args.categoryContext); (err != nil) != tt.wantErr { t.Errorf("renderReferenceDocsPage() error = %v, wantErr %v", err, tt.wantErr) } tt.checker(t, tt.args)