Skip to content
Merged
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
277 changes: 277 additions & 0 deletions pkg/cmd/generate/clients/clients.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
package clients

import (
_ "embed"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"text/template"

"github.com/MakeNowJust/heredoc"
"github.com/algolia/docli/pkg/cmd/generate/utils"
"github.com/algolia/docli/pkg/dictionary"
"github.com/pb33f/libopenapi"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/spf13/cobra"
)

// Options represents configuration options and CLI flags for this command.
type Options struct {
ApiName string
InputFilename string
OutputDirectory string
}

// OperationData represents relevant information about an API operation.
type OperationData struct {
Acl string
ApiName string
CodeSamples []CodeSample
Description string
InputFilename string
OutputFilename string
OutputPath string
OperationIdKebab string
Params []Parameter
RequestBody RequestBody
RequiresAdmin bool
ShortDescription string
Summary string
}

type CodeSample struct {
Lang string
Label string
Source string
}

type Parameter struct {
Name string
Description string
Required bool
}

type RequestBody struct {
Name string
Description string
}

//go:embed method.mdx.tmpl
var methodTemplate string

func NewClientsCommand() *cobra.Command {
opts := &Options{}

cmd := &cobra.Command{
Use: "clients",
Aliases: []string{"c"},
Short: "Generate MDX files for the API client method references",
Long: heredoc.Doc(`
This command reads an OpenAPI 3 spec file and generates one MDX file per operation.
It writes an API reference with usage information specific to API clients,
which may follow different conventions depending on the programming language used.
This commadn doesn't delete MDX files. If you remove or rename an operation,
you need to update or delete its MDX file manually.
`),
Example: heredoc.Doc(`
# Run from root of algolia/docs-new
docli gen clients specs/search.yml -o doc/libraries/sdk/methods
`),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
opts.InputFilename = args[0]
opts.ApiName = utils.GetApiName(opts.InputFilename)
runCommand(opts)
},
}

cmd.Flags().
StringVarP(&opts.OutputDirectory, "output", "o", "out", "Output directory for generated MDX files")

return cmd
}

func runCommand(opts *Options) {
specFile, err := os.ReadFile(opts.InputFilename)
if err != nil {
log.Fatalf("Error: %e", err)
}

fmt.Printf("Generating API client references for spec: %s\n", opts.InputFilename)
fmt.Printf("Writing output in: %s\n", opts.OutputDirectory)

spec, err := utils.LoadSpec(specFile)
if err != nil {
log.Fatalf("Error: %e", err)
}

opData, err := getApiData(spec, opts)
if err != nil {
log.Fatalf("Error: %e", err)
}

tmpl := template.Must(template.New("method").Funcs(template.FuncMap{
"trim": strings.TrimSpace,
}).Parse(methodTemplate))

writeApiData(opData, tmpl)
}

// getApiData reads the OpenAPI spec and parses the operation data.
func getApiData(
doc *libopenapi.DocumentModel[v3.Document],
opts *Options,
) ([]OperationData, error) {
var result []OperationData

count := 0

prefix := fmt.Sprintf("%s/%s", opts.OutputDirectory, opts.ApiName)

for pathPairs := doc.Model.Paths.PathItems.First(); pathPairs != nil; pathPairs = pathPairs.Next() {
pathName := pathPairs.Key()
// Ignore custom HTTP requests
if pathName == "/{path}" {
continue
}

pathItem := pathPairs.Value()

for opPairs := pathItem.GetOperations().First(); opPairs != nil; opPairs = opPairs.Next() {
op := opPairs.Value()

acl, err := utils.GetAcl(op)
if err != nil {
return nil, err
}

short, long := splitDescription(op.Description)

data := OperationData{
Acl: utils.AclToString(acl),
ApiName: opts.ApiName,
CodeSamples: getCodeSamples(op),
Description: long,
OutputFilename: utils.GetOutputFilename(op),
OutputPath: prefix,
OperationIdKebab: utils.ToKebabCase(op.OperationId),
Params: getParameters(op),
RequiresAdmin: false,
RequestBody: getRequestBody(op),
ShortDescription: short,
Summary: op.Summary,
}

if data.Acl == "`admin`" {
data.RequiresAdmin = true
}

result = append(result, data)
count++
}
}

fmt.Printf("Spec %s has %d operations.\n", opts.InputFilename, count)

return result, nil
}

// writeApiData writes the OpenAPI data to MDX files.
func writeApiData(data []OperationData, template *template.Template) error {
for _, item := range data {
if err := os.MkdirAll(item.OutputPath, 0o755); err != nil {
return err
}

fullPath := filepath.Join(item.OutputPath, item.OutputFilename)

out, err := os.Create(fullPath)
if err != nil {
return err
}

err = template.Execute(out, item)
if err != nil {
return err
}
}

return nil
}

func splitDescription(p string) (string, string) {
p = strings.TrimSpace(p)

// Split by empty line
parts := strings.SplitN(p, "\n\n", 2)
if len(parts) > 1 && strings.TrimSpace(parts[0]) != "" {
short := strings.TrimSpace(parts[0])
long := strings.TrimSpace(parts[1])

// No extra newline characters in between
short = strings.ReplaceAll(short, "\n", "")

return short, long
}

// No empty line: find first period
if idx := strings.Index(p, "."); idx != -1 {
short := strings.TrimSpace(p[:idx+1])
long := strings.TrimSpace(p[idx+1:])

return short, long
}

// No period: entire paragraph is the shortDescription
return p, ""
}

func getCodeSamples(op *v3.Operation) []CodeSample {
node, ok := op.Extensions.Get("x-codeSamples")
// Operations can be without code samples
if !ok {
return nil
}

var result []CodeSample

for _, child := range node.Content {
var c CodeSample

child.Decode(&c)

c.Lang = dictionary.NormalizeLang(c.Lang)

if strings.ToLower(c.Label) != "curl" {
result = append(result, c)
}
}

return result
}

func getParameters(op *v3.Operation) []Parameter {
var result []Parameter

for _, p := range op.Parameters {
param := Parameter{
Name: p.Name,
Description: p.Description,
}
if p.Required != nil {
param.Required = *p.Required
}

result = append(result, param)
}

return result
}

func getRequestBody(op *v3.Operation) RequestBody {
return RequestBody{
Description: "Unknown",
}
}
33 changes: 33 additions & 0 deletions pkg/cmd/generate/clients/method.mdx.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: {{ .Summary }}
description: {{ .ShortDescription }}
---
{{ if .RequiresAdmin }}
**Requires Admin API key**
{{ else if .Acl }}
**Required ACL:** {{ .Acl }}
{{- end }}

{{ .Description }}
{{ if .CodeSamples }}
## Usage

<CodeGroup>
{{ range .CodeSamples }}
```{{ .Lang }} {{ .Label }}
{{ trim .Source }}
```
{{ end }}
</CodeGroup>
{{- end }}

<Card
icon="folder-code"
horizontal="true"
title="See the full API reference"
arrow="true"
href="/doc/rest-api/{{ .ApiName }}/{{ .OperationIdKebab }}"
>
For more details about input parameters
and response fields.
</Card>
2 changes: 2 additions & 0 deletions pkg/cmd/generate/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package generate
import (
"github.com/MakeNowJust/heredoc"
"github.com/algolia/docli/pkg/cmd/generate/cdn"
"github.com/algolia/docli/pkg/cmd/generate/clients"
"github.com/algolia/docli/pkg/cmd/generate/openapi"
"github.com/algolia/docli/pkg/cmd/generate/sla"
"github.com/algolia/docli/pkg/cmd/generate/snippets"
Expand All @@ -27,6 +28,7 @@ func NewGenerateCmd() *cobra.Command {
`),
}

command.AddCommand(clients.NewClientsCommand())
command.AddCommand(openapi.NewOpenApiCommand())
command.AddCommand(sla.NewSlaCommand())
command.AddCommand(snippets.NewSnippetsCommand())
Expand Down
11 changes: 2 additions & 9 deletions pkg/cmd/generate/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ type Options struct {
ApiName string
InputFileName string
OutputDirectory string
SpecFile []byte
}

// OperationData holds data relevant to a single API operation stub file.
Expand Down Expand Up @@ -82,9 +81,7 @@ func runCommand(opts *Options) {
fmt.Printf("Generating MDX stub files for spec: %s\n", opts.InputFileName)
fmt.Printf("Writing output in: %s\n", opts.OutputDirectory)

opts.SpecFile = specFile

spec, err := utils.LoadSpec(opts.SpecFile)
spec, err := utils.LoadSpec(specFile)
if err != nil {
log.Fatalf("Error: %e", err)
}
Expand All @@ -96,10 +93,6 @@ func runCommand(opts *Options) {

tmpl := template.Must(template.New("stub").Parse(stubTemplate))

if err != nil {
log.Fatalf("Error: %e", err)
}

writeApiData(opData, tmpl)
}

Expand Down Expand Up @@ -136,7 +129,7 @@ func getApiData(
ApiPath: pathName,
InputFilename: normalizePath(opts.InputFileName),
OutputFilename: utils.GetOutputFilename(op),
OutputPath: utils.GetOutputPath(op, prefix),
OutputPath: prefix,
RequiresAdmin: false,
Title: strings.TrimSpace(op.Summary),
Verb: opPairs.Key(),
Expand Down
3 changes: 2 additions & 1 deletion pkg/cmd/generate/snippets/snippets.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"

"github.com/MakeNowJust/heredoc"
"github.com/algolia/docli/pkg/cmd/generate/utils"
Expand Down Expand Up @@ -85,7 +86,7 @@ func generateMarkdownSnippet(snippet map[string]string) string {

for _, lang := range languages {
result += fmt.Sprintf("\n```%s %s\n", lang, utils.GetLanguageName(lang))
result += snippet[lang]
result += strings.ReplaceAll(snippet[lang], "<YOUR_INDEX_NAME>", "ALGOLIA_INDEX_NAME")
result += "\n```\n"
}

Expand Down
9 changes: 0 additions & 9 deletions pkg/cmd/generate/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,6 @@ func LoadSpec(specFile []byte) (*libopenapi.DocumentModel[v3.Document], error) {
return docModel, nil
}

// GetOutputPath returns the output path for the MDX file for the given operation.
func GetOutputPath(op *v3.Operation, prefix string) string {
if len(op.Tags) > 0 {
return fmt.Sprintf("%s/%s", prefix, ToKebabCase(op.Tags[0]))
}

return fmt.Sprintf("%s", prefix)
}

// GetOutputFilename generates the filename from the operationId.
func GetOutputFilename(op *v3.Operation) string {
return fmt.Sprintf("%s.mdx", ToKebabCase(op.OperationId))
Expand Down
Loading