Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
113 changes: 113 additions & 0 deletions tools/cli/internal/openapi/filter/code_sample.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2025 MongoDB Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package filter

import (
"time"

"github.com/getkin/kin-openapi/openapi3"
"github.com/mongodb/openapi/tools/cli/internal/apiversion"
)

const codeSampleExtensionName = "x-codeSamples"

// https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-code-samples#x-codesamples
type codeSample struct {
Lang string `json:"lang,omitempty" yaml:"lang,omitempty"`
Label string `json:"label,omitempty" yaml:"label,omitempty"`
Source string `json:"source,omitempty" yaml:"source,omitempty"`
}

// CodeSampleFilter modifies includes the fields "x-state" and "x-beta" to the "preview" and "upcoming" APIs Operations.
// The "x-state" and "x-beta" fields are bump.sh custom fields to include budges
// Bump.sh feature: https://docs.bump.sh/help/specification-support/doc-code-samples/#example-usage
type CodeSampleFilter struct {
oas *openapi3.T
metadata *Metadata
}

func (f *CodeSampleFilter) ValidateMetadata() error {
return validateMetadataWithVersion(f.metadata)
}

func (f *CodeSampleFilter) Apply() error {
for pathName, p := range f.oas.Paths.Map() {
for opK, op := range p.Operations() {
if err := f.includeCodeSamplesForOperation(pathName, opK, op); err != nil {
return err
}
}
}

return nil
}

func (f *CodeSampleFilter) newCurlCodeSamplesForOperation(pathName, opK string) codeSample {
source := "curl --user \"{PUBLIC-KEY}:{PRIVATE-KEY}\" \\\n --digest \\\n " +
"--header \"Accept: application/vnd.atlas." + apiVersion(f.metadata.targetVersion) + "+json\" \\\n "

switch opK {
case "GET":
source += "-X " + opK + " \"" + pathName + "?pretty=true\""
case "DELETE":
source += "-X " + opK + " \"" + pathName + "\""
case "POST", "PATCH", "PUT":
source += "-X " + opK + " \"" + pathName + "\"\n "
source += "-d " + "{ <Payload> }"
}

return codeSample{
Lang: "cURL",
Label: "curl",
Source: source,
}
}

func apiVersion(version *apiversion.APIVersion) string {
if version.IsStable() {
return version.Date().Format(time.DateOnly)
}

if version.IsPreview() {
return "preview"
}

// Upcoming api version
return version.Date().Format(time.DateOnly) + ".upcoming"
}

func newAtlasCliCodeSamplesForOperation(op *openapi3.Operation) codeSample {
return codeSample{
Lang: "cURL",
Label: "Atlas CLI",
Source: "atlas api " + op.OperationID + " --help",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more of a q for @jeroenvervaeke or maybe @andreaangiolillo already checked this, are there any OperationIDs/apis not included in auto-gen that we should account for here and filter them out?

}
}

func (f *CodeSampleFilter) includeCodeSamplesForOperation(pathName, opK string, op *openapi3.Operation) error {
if op == nil || opK == "" || pathName == "" {
return nil
}

if op.Extensions == nil {
op.Extensions = map[string]any{}
}

op.Extensions[codeSampleExtensionName] = []codeSample{
f.newCurlCodeSamplesForOperation(pathName, opK),
newAtlasCliCodeSamplesForOperation(op),
}
return nil
}
242 changes: 242 additions & 0 deletions tools/cli/internal/openapi/filter/code_sample_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// Copyright 2025 MongoDB Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package filter

import (
"reflect"
"testing"

"github.com/getkin/kin-openapi/openapi3"
"github.com/mongodb/openapi/tools/cli/internal/apiversion"
"github.com/stretchr/testify/require"
)

func TestCodeSampleFilter(t *testing.T) {
testCases := []struct {
name string
oas *openapi3.T
version string
expectedOas *openapi3.T
}{
{
name: "stable api",
version: "2025-01-01",
oas: &openapi3.T{
Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "testOperationID",
Summary: "testSummary",
Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{
Content: openapi3.Content{
"application/vnd.atlas.2025-01-01+json": {
Schema: &openapi3.SchemaRef{
Ref: "#/components/schemas/PaginatedAppUserView",
},
Extensions: map[string]any{
"x-gen-version": "2025-01-01",
},
},
},
})),
Extensions: map[string]any{
"x-sunset": "9999-12-31",
},
},
})),
},
expectedOas: &openapi3.T{
Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "testOperationID",
Summary: "testSummary",
Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{
Content: openapi3.Content{
"application/vnd.atlas.2025-01-01+json": {
Schema: &openapi3.SchemaRef{
Ref: "#/components/schemas/PaginatedAppUserView",
},
Extensions: map[string]any{
"x-gen-version": "2025-01-01",
},
},
},
})),
Extensions: map[string]any{
"x-sunset": "9999-12-31",
"x-codeSamples": []codeSample{
{
Lang: "cURL",
Label: "curl",
Source: "curl --user \"{PUBLIC-KEY}:{PRIVATE-KEY}\" \\\n --digest \\\n " +
"--header \"Accept: application/vnd.atlas.2025-01-01+json\" \\\n " + "-X GET \"test?pretty=true\"",
},
{
Lang: "cURL",
Label: "Atlas CLI",
Source: "atlas api testOperationID --help",
},
},
},
},
})),
},
},
{
name: "preview api",
version: "preview",
oas: &openapi3.T{
Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "testOperationID",
Summary: "testSummary",
Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{
Content: openapi3.Content{
"application/vnd.atlas.preview+json": {
Schema: &openapi3.SchemaRef{
Ref: "#/components/schemas/PaginatedAppUserView",
},
Extensions: map[string]any{
"x-gen-version": "preview",
},
},
},
})),
Extensions: map[string]any{
"x-sunset": "9999-12-31",
},
},
})),
},
expectedOas: &openapi3.T{
Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "testOperationID",
Summary: "testSummary",
Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{
Content: openapi3.Content{
"application/vnd.atlas.preview+json": {
Schema: &openapi3.SchemaRef{
Ref: "#/components/schemas/PaginatedAppUserView",
},
Extensions: map[string]any{
"x-gen-version": "preview",
},
},
},
})),
Extensions: map[string]any{
"x-sunset": "9999-12-31",
"x-codeSamples": []codeSample{
{
Lang: "cURL",
Label: "curl",
Source: "curl --user \"{PUBLIC-KEY}:{PRIVATE-KEY}\" \\\n --digest \\\n " +
"--header \"Accept: application/vnd.atlas.preview+json\" \\\n " + "-X GET \"test?pretty=true\"",
},
{
Lang: "cURL",
Label: "Atlas CLI",
Source: "atlas api testOperationID --help",
},
},
},
},
})),
},
},
{
name: "upcoming api",
version: "2025-01-01.upcoming",
oas: &openapi3.T{
Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "testOperationID",
Summary: "testSummary",
Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{
Content: openapi3.Content{
"application/vnd.atlas.2025-01-01.upcoming+json": {
Schema: &openapi3.SchemaRef{
Ref: "#/components/schemas/PaginatedAppUserView",
},
Extensions: map[string]any{
"x-gen-version": "2025-01-01.upcoming",
},
},
},
})),
Extensions: map[string]any{
"x-sunset": "9999-12-31",
},
},
})),
},
expectedOas: &openapi3.T{
Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "testOperationID",
Summary: "testSummary",
Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{
Content: openapi3.Content{
"application/vnd.atlas.2025-01-01.upcoming+json": {
Schema: &openapi3.SchemaRef{
Ref: "#/components/schemas/PaginatedAppUserView",
},
Extensions: map[string]any{
"x-gen-version": "2025-01-01.upcoming",
},
},
},
})),
Extensions: map[string]any{
"x-sunset": "9999-12-31",
"x-codeSamples": []codeSample{
{
Lang: "cURL",
Label: "curl",
Source: "curl --user \"{PUBLIC-KEY}:{PRIVATE-KEY}\" \\\n --digest \\\n " +
"--header \"Accept: application/vnd.atlas.2025-01-01.upcoming+json\" \\\n " + "-X GET \"test?pretty=true\"",
},
{
Lang: "cURL",
Label: "Atlas CLI",
Source: "atlas api testOperationID --help",
},
},
},
},
})),
},
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
oas := tt.oas
version, err := apiversion.New(apiversion.WithVersion(tt.version))
require.NoError(t, err)

filter := &CodeSampleFilter{
oas: oas,
metadata: &Metadata{targetVersion: version, targetEnv: "dev"},
}

require.NoError(t, filter.Apply())
if !reflect.DeepEqual(tt.expectedOas, tt.oas) {
t.Errorf("expected %v, got %v", tt.expectedOas, oas)
}
})
}
}
1 change: 1 addition & 0 deletions tools/cli/internal/openapi/filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func DefaultFilters(oas *openapi3.T, metadata *Metadata) []Filter {
&SunsetFilter{oas: oas},
&SchemasFilter{oas: oas},
&BumpFilter{oas: oas, metadata: metadata},
&CodeSampleFilter{oas: oas, metadata: metadata},
}
}

Expand Down
2 changes: 1 addition & 1 deletion tools/cli/internal/openapi/filter/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func TestDefaultFilters(t *testing.T) {
metadata := &Metadata{}
filters := DefaultFilters(doc, metadata)

assert.Len(t, filters, 10)
assert.Len(t, filters, 11)
}

func TestFiltersWithoutVersioning(t *testing.T) {
Expand Down
Loading
Loading