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
243 changes: 243 additions & 0 deletions openapi/bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package openapi

import (
"github.com/speakeasy-api/openapi/jsonschema/oas3"
"github.com/speakeasy-api/openapi/pointer"
"github.com/speakeasy-api/openapi/sequencedmap"
"github.com/speakeasy-api/openapi/values"
"github.com/speakeasy-api/openapi/yml"
)

// Bootstrap creates a new OpenAPI document with best practice examples.
// This serves as a template demonstrating proper structure for operations,
// components, security schemes, servers, tags, and references.
func Bootstrap() *OpenAPI {
return &OpenAPI{
OpenAPI: Version,
Info: createBootstrapInfo(),
Servers: createBootstrapServers(),
Tags: createBootstrapTags(),
Security: []*SecurityRequirement{
NewSecurityRequirement(
sequencedmap.NewElem("ApiKeyAuth", []string{}),
),
},
Paths: createBootstrapPaths(),
Components: createBootstrapComponents(),
}
}

// createBootstrapInfo creates a complete info section with best practices
func createBootstrapInfo() Info {
return Info{
Title: "My API",
Description: pointer.From("A new OpenAPI document template ready to populate"),
Version: "1.0.0",
Contact: &Contact{
Name: pointer.From("API Support"),
Email: pointer.From("[email protected]"),
URL: pointer.From("https://example.com/support"),
},
License: &License{
Name: "MIT",
URL: pointer.From("https://opensource.org/licenses/MIT"),
},
TermsOfService: pointer.From("https://example.com/terms"),
}
}

// createBootstrapServers creates example servers for different environments
func createBootstrapServers() []*Server {
return []*Server{
{
URL: "https://api.example.com/v1",
Description: pointer.From("Production server"),
},
{
URL: "https://staging-api.example.com/v1",
Description: pointer.From("Staging server"),
},
}
}

// createBootstrapTags creates example tags for organizing operations
func createBootstrapTags() []*Tag {
return []*Tag{
{
Name: "users",
Description: pointer.From("User management operations"),
ExternalDocs: &oas3.ExternalDocumentation{
Description: pointer.From("User API documentation"),
URL: "https://docs.example.com/users",
},
},
}
}

// createBootstrapPaths creates a single POST operation demonstrating best practices
func createBootstrapPaths() *Paths {
paths := NewPaths()

// Create a single POST operation as an example
usersPath := NewPathItem()
usersPath.Set(HTTPMethodPost, &Operation{
OperationID: pointer.From("createUser"),
Summary: pointer.From("Create a new user"),
Description: pointer.From("Creates a new user account with the provided information"),
Tags: []string{"users"},
RequestBody: NewReferencedRequestBodyFromRequestBody(&RequestBody{
Description: pointer.From("User creation request"),
Required: pointer.From(true),
Content: sequencedmap.New(
sequencedmap.NewElem("application/json", &MediaType{
Schema: oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{
Type: oas3.NewTypeFromString(oas3.SchemaTypeObject),
Properties: sequencedmap.New(
sequencedmap.NewElem("name", oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{
Type: oas3.NewTypeFromString(oas3.SchemaTypeString),
Description: pointer.From("Full name of the user"),
MinLength: pointer.From(int64(1)),
MaxLength: pointer.From(int64(100)),
})),
sequencedmap.NewElem("email", oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{
Type: oas3.NewTypeFromString(oas3.SchemaTypeString),
Format: pointer.From("email"),
Description: pointer.From("Email address of the user"),
})),
),
Required: []string{"name", "email"},
}),
}),
),
}),
Responses: createUserResponses(),
})

paths.Set("/users", &ReferencedPathItem{Object: usersPath})
return paths
}

// createUserResponses creates example responses with references
func createUserResponses() *Responses {
return NewResponses(
sequencedmap.NewElem("201", NewReferencedResponseFromRef("#/components/responses/UserResponse")),
sequencedmap.NewElem("400", NewReferencedResponseFromRef("#/components/responses/BadRequestResponse")),
sequencedmap.NewElem("401", NewReferencedResponseFromRef("#/components/responses/UnauthorizedResponse")),
)
}

// createBootstrapComponents creates reusable components demonstrating best practices
func createBootstrapComponents() *Components {
return &Components{
Schemas: createBootstrapSchemas(),
Responses: createBootstrapResponses(),
SecuritySchemes: createBootstrapSecuritySchemes(),
}
}

// createBootstrapSchemas creates example schemas with proper structure
func createBootstrapSchemas() *sequencedmap.Map[string, *oas3.JSONSchema[oas3.Referenceable]] {
return sequencedmap.New(
sequencedmap.NewElem("User", oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{
Type: oas3.NewTypeFromString(oas3.SchemaTypeObject),
Title: pointer.From("User"),
Description: pointer.From("A user account"),
Properties: sequencedmap.New(
sequencedmap.NewElem("id", oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{
Type: oas3.NewTypeFromString(oas3.SchemaTypeInteger),
Format: pointer.From("int64"),
Description: pointer.From("Unique identifier for the user"),
Examples: []values.Value{yml.CreateIntNode(123)},
})),
sequencedmap.NewElem("name", oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{
Type: oas3.NewTypeFromString(oas3.SchemaTypeString),
Description: pointer.From("Full name of the user"),
MinLength: pointer.From(int64(1)),
MaxLength: pointer.From(int64(100)),
Examples: []values.Value{yml.CreateStringNode("John Doe")},
})),
sequencedmap.NewElem("email", oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{
Type: oas3.NewTypeFromString(oas3.SchemaTypeString),
Format: pointer.From("email"),
Description: pointer.From("Email address of the user"),
Examples: []values.Value{yml.CreateStringNode("[email protected]")},
})),
sequencedmap.NewElem("status", oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{
Type: oas3.NewTypeFromString(oas3.SchemaTypeString),
Description: pointer.From("Current status of the user account"),
Enum: []values.Value{yml.CreateStringNode("active"), yml.CreateStringNode("inactive"), yml.CreateStringNode("pending")},
Examples: []values.Value{yml.CreateStringNode("active")},
})),
),
Required: []string{"name", "email"},
})),
sequencedmap.NewElem("Error", oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{
Type: oas3.NewTypeFromString(oas3.SchemaTypeObject),
Title: pointer.From("Error"),
Description: pointer.From("Error response"),
Properties: sequencedmap.New(
sequencedmap.NewElem("code", oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{
Type: oas3.NewTypeFromString(oas3.SchemaTypeString),
Description: pointer.From("Error code"),
Examples: []values.Value{yml.CreateStringNode("VALIDATION_ERROR")},
})),
sequencedmap.NewElem("message", oas3.NewJSONSchemaFromSchema[oas3.Referenceable](&oas3.Schema{
Type: oas3.NewTypeFromString(oas3.SchemaTypeString),
Description: pointer.From("Human-readable error message"),
Examples: []values.Value{yml.CreateStringNode("The request is invalid")},
})),
),
Required: []string{"code", "message"},
})),
)
}

// createBootstrapResponses creates reusable response components
func createBootstrapResponses() *sequencedmap.Map[string, *ReferencedResponse] {
return sequencedmap.New(
sequencedmap.NewElem("UserResponse", &ReferencedResponse{
Object: &Response{
Description: "User details",
Content: sequencedmap.New(
sequencedmap.NewElem("application/json", &MediaType{
Schema: oas3.NewJSONSchemaFromReference("#/components/schemas/User"),
}),
),
},
}),
sequencedmap.NewElem("BadRequestResponse", &ReferencedResponse{
Object: &Response{
Description: "Bad request - validation error",
Content: sequencedmap.New(
sequencedmap.NewElem("application/json", &MediaType{
Schema: oas3.NewJSONSchemaFromReference("#/components/schemas/Error"),
}),
),
},
}),
sequencedmap.NewElem("UnauthorizedResponse", &ReferencedResponse{
Object: &Response{
Description: "Unauthorized - authentication required",
Content: sequencedmap.New(
sequencedmap.NewElem("application/json", &MediaType{
Schema: oas3.NewJSONSchemaFromReference("#/components/schemas/Error"),
}),
),
},
}),
)
}

// createBootstrapSecuritySchemes creates example security schemes
func createBootstrapSecuritySchemes() *sequencedmap.Map[string, *ReferencedSecurityScheme] {
return sequencedmap.New(
sequencedmap.NewElem("ApiKeyAuth", &ReferencedSecurityScheme{
Object: &SecurityScheme{
Type: "apiKey",
In: pointer.From[SecuritySchemeIn]("header"),
Name: pointer.From("X-API-Key"),
Description: pointer.From("API key for authentication"),
},
}),
)
}
41 changes: 41 additions & 0 deletions openapi/bootstrap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package openapi_test

import (
"bytes"
"os"
"testing"

"github.com/speakeasy-api/openapi/openapi"
"github.com/speakeasy-api/openapi/yml"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestBootstrap_Success(t *testing.T) {
t.Parallel()
ctx := t.Context()

// Create bootstrap document
doc := openapi.Bootstrap()

// Marshal to YAML
var buf bytes.Buffer
ctx = yml.ContextWithConfig(ctx, &yml.Config{
ValueStringStyle: yaml.DoubleQuotedStyle,
Indentation: 2,
OutputFormat: yml.OutputFormatYAML,
})
err := openapi.Marshal(ctx, doc, &buf)
require.NoError(t, err, "should marshal without error")

// Read expected output
expectedBytes, err := os.ReadFile("testdata/bootstrap_expected.yaml")
require.NoError(t, err, "should read expected file")

// Compare outputs
expected := string(expectedBytes)
actual := buf.String()

assert.Equal(t, expected, actual, "marshaled output should match expected YAML")
}
8 changes: 0 additions & 8 deletions openapi/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ func TestBundle_Success(t *testing.T) {
require.NoError(t, err)
actualYAML := buf.Bytes()

// Save the current output for comparison
err = os.WriteFile("testdata/inline/bundled_current.yaml", actualYAML, 0644)
require.NoError(t, err)

// Load the expected output
expectedBytes, err := os.ReadFile("testdata/inline/bundled_expected.yaml")
require.NoError(t, err)
Expand Down Expand Up @@ -88,10 +84,6 @@ func TestBundle_CounterNaming_Success(t *testing.T) {
require.NoError(t, err)
actualYAML := buf.Bytes()

// Save the current output for comparison
err = os.WriteFile("testdata/inline/bundled_counter_current.yaml", actualYAML, 0644)
require.NoError(t, err)

// Load the expected output
expectedBytes, err := os.ReadFile("testdata/inline/bundled_counter_expected.yaml")
require.NoError(t, err)
Expand Down
90 changes: 90 additions & 0 deletions openapi/cmd/bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package cmd

import (
"context"
"fmt"
"os"

"github.com/speakeasy-api/openapi/openapi"
"github.com/speakeasy-api/openapi/yml"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

var bootstrapCmd = &cobra.Command{
Use: "bootstrap [output-file]",
Short: "Create a new OpenAPI document with best practice examples",
Long: `Bootstrap creates a new OpenAPI document template with comprehensive examples of best practices.

This command generates a complete OpenAPI specification that demonstrates:
• Proper document structure and metadata (info, servers, tags)
• Example operations with request/response definitions
• Reusable components (schemas, responses, security schemes)
• Reference usage ($ref) for component reuse
• Security scheme definitions (API key authentication)
• Comprehensive schema examples with validation rules

The generated document serves as both a template for new APIs and a learning
resource for OpenAPI best practices.

Examples:
# Create bootstrap document and output to stdout
openapi openapi bootstrap

# Create bootstrap document and save to file
openapi openapi bootstrap ./my-api.yaml

# Create bootstrap document in current directory
openapi openapi bootstrap ./openapi.yaml`,
Args: cobra.MaximumNArgs(1),
Run: runBootstrap,
}

func runBootstrap(cmd *cobra.Command, args []string) {
ctx := cmd.Context()

if err := createBootstrapDocument(ctx, args); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

func createBootstrapDocument(ctx context.Context, args []string) error {
// Create the bootstrap document
doc := openapi.Bootstrap()

// Determine output destination
var outputFile string
writeToStdout := true

if len(args) > 0 {
outputFile = args[0]
writeToStdout = false
}

// Create processor for output handling
processor, err := NewOpenAPIProcessor("", outputFile, false)
if err != nil {
return err
}

// Override stdout setting based on our logic
processor.WriteToStdout = writeToStdout

// Write the document
ctx = yml.ContextWithConfig(ctx, &yml.Config{
ValueStringStyle: yaml.DoubleQuotedStyle,
Indentation: 2,
OutputFormat: yml.OutputFormatYAML,
})
if err := processor.WriteDocument(ctx, doc); err != nil {
return fmt.Errorf("failed to write bootstrap document: %w", err)
}

// Print success message (only if not writing to stdout)
if !writeToStdout {
processor.PrintSuccess("Bootstrap OpenAPI document created: " + outputFile)
}

return nil
}
Loading
Loading