diff --git a/pkg/cmd/generate/clients/clients.go b/pkg/cmd/generate/clients/clients.go
index 9530fc5..f1a2c3e 100644
--- a/pkg/cmd/generate/clients/clients.go
+++ b/pkg/cmd/generate/clients/clients.go
@@ -36,6 +36,7 @@ type ExternalDocs struct {
type OperationData struct {
ACL string
APIName string
+ Beta bool
CodeSamples []CodeSample
Deprecated bool
Description string
@@ -152,8 +153,6 @@ func runCommand(opts *Options, printer *output.Printer) error {
}
// getAPIData reads the OpenAPI spec and parses the operation data.
-//
-//nolint:funlen
func getAPIData(
doc *libopenapi.DocumentModel[v3.Document],
opts *Options,
@@ -162,6 +161,11 @@ func getAPIData(
count := 0
+ beta, err := utils.IsBetaAPI(&doc.Model)
+ if err != nil {
+ return nil, fmt.Errorf("get beta status: %w", err)
+ }
+
prefix := fmt.Sprintf("%s/%s", opts.OutputDirectory, opts.APIName)
for pathPairs := doc.Model.Paths.PathItems.First(); pathPairs != nil; pathPairs = pathPairs.Next() {
@@ -174,44 +178,16 @@ func getAPIData(
pathItem := pathPairs.Value()
for opPairs := pathItem.GetOperations().First(); opPairs != nil; opPairs = opPairs.Next() {
- op := opPairs.Value()
-
- acl, err := utils.GetACL(op)
+ data, err := buildOperationData(
+ opPairs.Key(),
+ pathName,
+ opPairs.Value(),
+ opts,
+ prefix,
+ beta,
+ )
if err != nil {
- return nil, fmt.Errorf("get ACL for %s %s: %w", opPairs.Key(), pathName, err)
- }
-
- short, long := utils.SplitDescription(op.Description)
- short = utils.StripMarkdown(short)
-
- data := OperationData{
- ACL: utils.AclToString(acl),
- APIName: opts.APIName,
- CodeSamples: getCodeSamples(op),
- Deprecated: boolOrFalse(op.Deprecated),
- 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
- }
-
- if op.ExternalDocs != nil {
- desc := strings.TrimSpace(op.ExternalDocs.Description)
- data.ExternalDocs.Description = strings.TrimSuffix(desc, ".")
- data.ExternalDocs.URL = op.ExternalDocs.URL
- }
-
- if data.ExternalDocs.Description != "" && data.ExternalDocs.URL != "" {
- data.SeeAlso = true
+ return nil, err
}
result = append(result, data)
@@ -222,6 +198,60 @@ func getAPIData(
return result, nil
}
+func buildOperationData(
+ verb, pathName string,
+ op *v3.Operation,
+ opts *Options,
+ prefix string,
+ beta bool,
+) (OperationData, error) {
+ opBeta, err := utils.IsBetaOperation(op)
+ if err != nil {
+ return OperationData{}, fmt.Errorf("get beta status for %s %s: %w", verb, pathName, err)
+ }
+
+ acl, err := utils.GetACL(op)
+ if err != nil {
+ return OperationData{}, fmt.Errorf("get ACL for %s %s: %w", verb, pathName, err)
+ }
+
+ short, long := utils.SplitDescription(op.Description)
+ short = utils.StripMarkdown(short)
+
+ data := OperationData{
+ ACL: utils.AclToString(acl),
+ APIName: opts.APIName,
+ Beta: beta || opBeta,
+ CodeSamples: getCodeSamples(op),
+ Deprecated: boolOrFalse(op.Deprecated),
+ 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
+ }
+
+ if op.ExternalDocs != nil {
+ desc := strings.TrimSpace(op.ExternalDocs.Description)
+ data.ExternalDocs.Description = strings.TrimSuffix(desc, ".")
+ data.ExternalDocs.URL = op.ExternalDocs.URL
+ }
+
+ if data.ExternalDocs.Description != "" && data.ExternalDocs.URL != "" {
+ data.SeeAlso = true
+ }
+
+ return data, nil
+}
+
// writeAPIData writes the OpenAPI data to MDX files.
func writeAPIData(
data []OperationData,
diff --git a/pkg/cmd/generate/clients/clients_test.go b/pkg/cmd/generate/clients/clients_test.go
index 982f29e..3f3f45d 100644
--- a/pkg/cmd/generate/clients/clients_test.go
+++ b/pkg/cmd/generate/clients/clients_test.go
@@ -26,6 +26,7 @@ paths:
Use the [API key](https://algolia.com) endpoint with **filters:active**.
This body keeps [markdown](https://algolia.com/doc) and **details** intact.
+ x-beta: true
x-acl:
- search
`)
@@ -78,6 +79,8 @@ paths:
assertFrontmatterDescription(t, frontmatter, "Use the API key endpoint with filters:active.")
assertRenderedContains(t, rendered.String(), []string{
+ `import Beta from "/snippets/beta.mdx";`,
+ "",
`description: "Use the API key endpoint with filters:active."`,
"This body keeps [markdown](https://algolia.com/doc) and **details** intact.",
"**Required ACL:** `search`",
diff --git a/pkg/cmd/generate/clients/method.mdx.tmpl b/pkg/cmd/generate/clients/method.mdx.tmpl
index b61b344..af93f5e 100644
--- a/pkg/cmd/generate/clients/method.mdx.tmpl
+++ b/pkg/cmd/generate/clients/method.mdx.tmpl
@@ -3,6 +3,12 @@ title: {{ .Summary }}
description: {{ frontmatterString .ShortDescription }}
public: true
---
+{{- if .Beta }}
+
+import Beta from "/snippets/beta.mdx";
+
+
+{{- end }}
{{- if .Deprecated }}
This method is **deprecated.**
diff --git a/pkg/cmd/generate/openapi/openapi.go b/pkg/cmd/generate/openapi/openapi.go
index d4d99f4..1787f16 100644
--- a/pkg/cmd/generate/openapi/openapi.go
+++ b/pkg/cmd/generate/openapi/openapi.go
@@ -44,6 +44,7 @@ type OverviewData struct {
type OperationData struct {
ACL string
APIPath string
+ Beta bool
Description string
ExternalDocs ExternalDocs
InputFilename string
@@ -187,6 +188,11 @@ func getAPIData(
count := 0
+ beta, err := utils.IsBetaAPI(&doc.Model)
+ if err != nil {
+ return nil, fmt.Errorf("get beta status: %w", err)
+ }
+
prefix := fmt.Sprintf("%s/%s", opts.OutputDirectory, opts.APIName)
for pathPairs := doc.Model.Paths.PathItems.First(); pathPairs != nil; pathPairs = pathPairs.Next() {
@@ -199,7 +205,14 @@ func getAPIData(
pathItem := pathPairs.Value()
for opPairs := pathItem.GetOperations().First(); opPairs != nil; opPairs = opPairs.Next() {
- data, err := buildOperationData(opPairs.Key(), pathName, opPairs.Value(), opts, prefix)
+ data, err := buildOperationData(
+ opPairs.Key(),
+ pathName,
+ opPairs.Value(),
+ opts,
+ prefix,
+ beta,
+ )
if err != nil {
return nil, err
}
@@ -217,6 +230,7 @@ func buildOperationData(
op *v3.Operation,
opts *Options,
prefix string,
+ beta bool,
) (OperationData, error) {
short, long := utils.SplitDescription(op.Description)
@@ -225,9 +239,15 @@ func buildOperationData(
return OperationData{}, fmt.Errorf("get ACL for %s %s: %w", verb, pathName, err)
}
+ opBeta, err := utils.IsBetaOperation(op)
+ if err != nil {
+ return OperationData{}, fmt.Errorf("get beta status for %s %s: %w", verb, pathName, err)
+ }
+
data := OperationData{
ACL: utils.AclToString(acl),
APIPath: pathName,
+ Beta: beta || opBeta,
Description: long,
InputFilename: normalizePath(opts.InputFileName),
OutputFilename: utils.GetOutputFilename(op),
diff --git a/pkg/cmd/generate/openapi/openapi_test.go b/pkg/cmd/generate/openapi/openapi_test.go
index 96a8d59..75e462a 100644
--- a/pkg/cmd/generate/openapi/openapi_test.go
+++ b/pkg/cmd/generate/openapi/openapi_test.go
@@ -26,6 +26,7 @@ paths:
Retrieve the [API key](https://algolia.com) with **filters:active**.
Use this endpoint to fetch a key by its value with **sample** output.
+ x-beta: true
x-acl:
- search
`)
@@ -75,6 +76,8 @@ paths:
assertFrontmatterDescription(t, frontmatter, "Retrieve the API key with filters:active.")
assertRenderedContains(t, rendered, []string{
+ `import Beta from "/snippets/beta.mdx";`,
+ "",
"title: Get an API key",
`description: "Retrieve the API key with filters:active."`,
"Use this endpoint to fetch a key by its value with **sample** output.",
diff --git a/pkg/cmd/generate/openapi/stub.mdx.tmpl b/pkg/cmd/generate/openapi/stub.mdx.tmpl
index 89edd1f..2f111c5 100644
--- a/pkg/cmd/generate/openapi/stub.mdx.tmpl
+++ b/pkg/cmd/generate/openapi/stub.mdx.tmpl
@@ -4,6 +4,12 @@ description: {{ frontmatterString .ShortDescription }}
openapi: {{ .InputFilename }} {{ .Verb }} {{ .APIPath }}
public: true
---
+{{- if .Beta }}
+
+import Beta from "/snippets/beta.mdx";
+
+
+{{- end }}
{{- if .Description }}
{{ .Description }}
diff --git a/pkg/cmd/generate/utils/utils.go b/pkg/cmd/generate/utils/utils.go
index 87b7759..eb21783 100644
--- a/pkg/cmd/generate/utils/utils.go
+++ b/pkg/cmd/generate/utils/utils.go
@@ -54,6 +54,47 @@ func GetAPIName(path string) string {
return strings.TrimSuffix(base, filepath.Ext(base))
}
+// IsBetaAPI returns true if the root document has an `x-beta: true` extension.
+func IsBetaAPI(doc *v3.Document) (bool, error) {
+ if doc.Extensions == nil {
+ return false, nil
+ }
+
+ node, ok := doc.Extensions.Get("x-beta")
+ if !ok {
+ return false, nil
+ }
+
+ return parseBetaNode(node)
+}
+
+// IsBetaOperation returns true if the operation has an `x-beta: true` extension.
+func IsBetaOperation(op *v3.Operation) (bool, error) {
+ if op.Extensions == nil {
+ return false, nil
+ }
+
+ node, ok := op.Extensions.Get("x-beta")
+ if !ok {
+ return false, nil
+ }
+
+ return parseBetaNode(node)
+}
+
+func parseBetaNode(node *yaml.Node) (bool, error) {
+ if node.Kind != yaml.ScalarNode {
+ return false, fmt.Errorf("expected a scalar node, got kind %d", node.Kind)
+ }
+
+ var result bool
+ if err := node.Decode(&result); err != nil {
+ return false, fmt.Errorf("expected a boolean node: %w", err)
+ }
+
+ return result, nil
+}
+
// GetACL returns the ACL required to perform the given operation.
func GetACL(op *v3.Operation) ([]string, error) {
node, ok := op.Extensions.Get("x-acl")
diff --git a/pkg/cmd/generate/utils/utils_test.go b/pkg/cmd/generate/utils/utils_test.go
index 24bbc33..b0b110a 100644
--- a/pkg/cmd/generate/utils/utils_test.go
+++ b/pkg/cmd/generate/utils/utils_test.go
@@ -347,6 +347,162 @@ func mockOp(extensions *yaml.Node) v3.Operation {
return op
}
+func mockApi(extensions *yaml.Node) v3.Document {
+ doc := v3.Document{}
+ doc.Extensions = orderedmap.New[string, *yaml.Node]()
+ doc.Extensions.Set("x-beta", extensions)
+
+ return doc
+}
+
+func mockBetaOp(extensions *yaml.Node) v3.Operation {
+ op := v3.Operation{}
+ op.Extensions = orderedmap.New[string, *yaml.Node]()
+ op.Extensions.Set("x-beta", extensions)
+
+ return op
+}
+
+func TestIsBetaAPI(t *testing.T) {
+ tests := []struct {
+ name string
+ extensions *yaml.Node
+ expected bool
+ expectError bool
+ errorSubstr string
+ }{
+ {
+ name: "Has x-beta: true on root",
+ extensions: &yaml.Node{
+ Kind: yaml.ScalarNode,
+ Tag: "!!bool",
+ Value: "true",
+ },
+ expected: true,
+ },
+ {
+ name: "Has x-beta: false on root",
+ extensions: &yaml.Node{
+ Kind: yaml.ScalarNode,
+ Tag: "!!bool",
+ Value: "false",
+ },
+ expected: false,
+ },
+ {
+ name: "Extension is a sequence node",
+ extensions: &yaml.Node{
+ Kind: yaml.SequenceNode,
+ },
+ expected: false,
+ expectError: true,
+ errorSubstr: "expected a scalar node",
+ },
+ {
+ name: "Extension is a string scalar",
+ extensions: &yaml.Node{
+ Kind: yaml.ScalarNode,
+ Tag: "!!str",
+ Value: "true",
+ },
+ expected: false,
+ expectError: true,
+ errorSubstr: "expected a boolean node",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ doc := mockApi(tt.extensions)
+
+ got, err := IsBetaAPI(&doc)
+ if !tt.expectError && err != nil {
+ t.Fatalf("unexpected error %v", err)
+ }
+
+ if tt.expectError {
+ if err == nil {
+ t.Fatal("error expected, but there was none")
+ }
+
+ if !strings.Contains(err.Error(), tt.errorSubstr) {
+ t.Errorf("expected error substring %s, got %s", tt.errorSubstr, err.Error())
+ }
+ }
+
+ if got != tt.expected {
+ t.Errorf("expected %t, got %t", tt.expected, got)
+ }
+ })
+ }
+}
+
+func TestIsBetaOperation(t *testing.T) {
+ tests := []struct {
+ name string
+ extensions *yaml.Node
+ expected bool
+ expectError bool
+ errorSubstr string
+ }{
+ {
+ name: "Has x-beta: true on operation",
+ extensions: &yaml.Node{
+ Kind: yaml.ScalarNode,
+ Tag: "!!bool",
+ Value: "true",
+ },
+ expected: true,
+ },
+ {
+ name: "Has x-beta: false on operation",
+ extensions: &yaml.Node{
+ Kind: yaml.ScalarNode,
+ Tag: "!!bool",
+ Value: "false",
+ },
+ expected: false,
+ },
+ {
+ name: "Extension is a mapping node",
+ extensions: &yaml.Node{
+ Kind: yaml.MappingNode,
+ },
+ expectError: true,
+ errorSubstr: "expected a scalar node",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ op := mockBetaOp(tt.extensions)
+
+ got, err := IsBetaOperation(&op)
+ if !tt.expectError && err != nil {
+ t.Fatalf("unexpected error %v", err)
+ }
+
+ if tt.expectError {
+ if err == nil {
+ t.Fatal("error expected, but there was none")
+ }
+
+ if !strings.Contains(err.Error(), tt.errorSubstr) {
+ t.Errorf("expected error substring %s, got %s", tt.errorSubstr, err.Error())
+ }
+ }
+
+ if got != tt.expected {
+ t.Errorf("expected %t, got %t", tt.expected, got)
+ }
+ })
+ }
+}
+
func TestGetAcl(t *testing.T) {
tests := []struct {
name string