Skip to content

Commit 1390319

Browse files
feat: add interactive explore and snip commands for OpenAPI specifications (#64)
1 parent 0641013 commit 1390319

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2524
-75
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ coverage.html
99
dist/
1010

1111
# Symlinked from AGENTS.md by mise install
12-
CLAUDE.md
12+
CLAUDE.md
13+
14+
go.work.sum

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,20 +95,24 @@ go install github.com/speakeasy-api/openapi/cmd/openapi@latest
9595

9696
The CLI provides three main command groups:
9797

98-
- **`openapi spec`** - Commands for working with OpenAPI specifications ([documentation](./openapi/cmd/README.md))
98+
- **`openapi spec`** - Commands for working with OpenAPI specifications ([documentation](./cmd/openapi/commands/openapi/README.md))
9999
- `bootstrap` - Create a new OpenAPI document with best practice examples
100100
- `bundle` - Bundle external references into components section
101101
- `clean` - Remove unused components from an OpenAPI specification
102+
- `explore` - Interactively explore an OpenAPI specification in the terminal
102103
- `inline` - Inline all references in an OpenAPI specification
103104
- `join` - Join multiple OpenAPI documents into a single document
105+
- `localize` - Localize an OpenAPI specification by copying external references to a target directory
104106
- `optimize` - Optimize an OpenAPI specification by deduplicating inline schemas
107+
- `sanitize` - Remove unwanted elements from an OpenAPI specification
108+
- `snip` - Remove selected operations from an OpenAPI specification (interactive or CLI)
105109
- `upgrade` - Upgrade an OpenAPI specification to the latest supported version
106110
- `validate` - Validate an OpenAPI specification document
107111

108-
- **`openapi arazzo`** - Commands for working with Arazzo workflow documents ([documentation](./arazzo/cmd/README.md))
112+
- **`openapi arazzo`** - Commands for working with Arazzo workflow documents ([documentation](./cmd/openapi/commands/arazzo/README.md))
109113
- `validate` - Validate an Arazzo workflow document
110114

111-
- **`openapi overlay`** - Commands for working with OpenAPI overlays ([documentation](./overlay/cmd/README.md))
115+
- **`openapi overlay`** - Commands for working with OpenAPI overlays ([documentation](./cmd/openapi/commands/overlay/README.md))
112116
- `apply` - Apply an overlay to an OpenAPI specification
113117
- `compare` - Compare two specifications and generate an overlay describing differences
114118
- `validate` - Validate an OpenAPI overlay document
File renamed without changes.

arazzo/cmd/root.go renamed to cmd/openapi/commands/arazzo/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package cmd
1+
package arazzo
22

33
import "github.com/spf13/cobra"
44

arazzo/cmd/validate.go renamed to cmd/openapi/commands/arazzo/validate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package cmd
1+
package arazzo
22

33
import (
44
"context"

openapi/cmd/README.md renamed to cmd/openapi/commands/openapi/README.md

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ OpenAPI specifications define REST APIs in a standard format. These commands hel
1919
- [`optimize`](#optimize)
2020
- [`bootstrap`](#bootstrap)
2121
- [`localize`](#localize)
22+
- [`explore`](#explore)
23+
- [`snip`](#snip)
2224
- [Common Options](#common-options)
2325
- [Output Formats](#output-formats)
2426
- [Examples](#examples)
@@ -730,6 +732,186 @@ Address:
730732
- You want to simplify file management for complex multi-file specifications
731733
- You're creating documentation packages that include all referenced files
732734

735+
### `explore`
736+
737+
Interactively explore an OpenAPI specification in a terminal user interface.
738+
739+
```bash
740+
# Launch the explorer
741+
openapi spec explore ./spec.yaml
742+
743+
# Get help on keyboard shortcuts
744+
# (Press '?' in the explorer)
745+
```
746+
747+
What the explorer provides:
748+
749+
- **Interactive navigation** - Browse all API operations with vim-style keyboard shortcuts
750+
- **Operation details** - View parameters, request bodies, responses, and more
751+
- **Color-coded methods** - Visual differentiation by HTTP method (GET=green, POST=blue, etc.)
752+
- **Fold/unfold details** - Toggle detailed information with Space or Enter
753+
- **Search through operations** - Quickly find endpoints in large specifications
754+
- **Help modal** - Built-in keyboard shortcut reference
755+
756+
**Keyboard Navigation:**
757+
758+
| Key | Action |
759+
| ----------------- | -------------------------- |
760+
| `↑` / `k` | Move up |
761+
| `↓` / `j` | Move down |
762+
| `gg` | Jump to top |
763+
| `G` | Jump to bottom |
764+
| `Ctrl-U` | Scroll up by half screen |
765+
| `Ctrl-D` | Scroll down by half screen |
766+
| `Enter` / `Space` | Toggle operation details |
767+
| `?` | Show/hide help |
768+
| `q` / `Esc` | Quit |
769+
770+
**What you can view:**
771+
772+
- Operation ID, summary, and description
773+
- HTTP method and path
774+
- Parameters (name, location, required status, description)
775+
- Request body content types
776+
- Response status codes and descriptions
777+
- Tags and deprecation warnings
778+
779+
**Benefits:**
780+
781+
- **Faster understanding** - Quickly grasp API structure without parsing YAML/JSON
782+
- **Better navigation** - Jump between operations more efficiently than text editors
783+
- **Visual clarity** - Color-coding and formatting make operations easier to distinguish
784+
- **No tool installation** - Works in any terminal, no browser required
785+
- **Offline friendly** - Explore specifications without network access
786+
787+
**Use Explore when:**
788+
789+
- You need to understand a new or unfamiliar API specification
790+
- You want to quickly review endpoints and their parameters
791+
- You're debugging API structure or looking for specific operations
792+
- You prefer terminal-based workflows over web-based viewers
793+
- You need to present or demo API operations in a meeting
794+
795+
### `snip`
796+
797+
Remove selected operations from an OpenAPI specification and automatically clean up unused components.
798+
799+
```bash
800+
# Interactive mode - browse and select operations via TUI
801+
openapi spec snip ./spec.yaml
802+
openapi spec snip ./spec.yaml ./filtered-spec.yaml
803+
804+
# CLI mode - remove by operation ID
805+
openapi spec snip --operationId deleteUser --operationId adminDebug ./spec.yaml
806+
807+
# CLI mode - remove by operation ID (comma-separated)
808+
openapi spec snip --operationId deleteUser,adminDebug ./spec.yaml
809+
810+
# CLI mode - remove by path:method
811+
openapi spec snip --operation /users/{id}:DELETE --operation /admin:GET ./spec.yaml
812+
813+
# CLI mode - remove by path:method (comma-separated)
814+
openapi spec snip --operation /users/{id}:DELETE,/admin:GET ./spec.yaml
815+
816+
# CLI mode - mixed approaches
817+
openapi spec snip --operationId deleteUser --operation /admin:GET ./spec.yaml
818+
819+
# Write in-place (CLI mode only)
820+
openapi spec snip -w --operation /internal/debug:GET ./spec.yaml
821+
```
822+
823+
**Two Operation Modes:**
824+
825+
**Interactive Mode** (no operation flags):
826+
- Launch a terminal UI to browse all operations
827+
- Select operations with Space key
828+
- Press 'a' to select all, 'A' to deselect all
829+
- Press 'w' to write the result (prompts for file path)
830+
- Press 'q' or Esc to cancel
831+
832+
**Command-Line Mode** (operation flags specified):
833+
- Remove operations specified via flags without UI
834+
- Supports `--operationId` for operation IDs
835+
- Supports `--operation` for path:method pairs
836+
- Both flags support comma-separated values or multiple flags
837+
838+
**What snip does:**
839+
840+
1. Removes the specified operations from the document
841+
2. Removes path items that become empty after operation removal
842+
3. Automatically runs Clean() to remove unused components
843+
4. Preserves all other operations and valid references
844+
845+
**Before snipping:**
846+
847+
```yaml
848+
paths:
849+
/users:
850+
get:
851+
operationId: getUsers
852+
responses:
853+
'200':
854+
$ref: '#/components/responses/UserResponse'
855+
delete:
856+
operationId: deleteAllUsers
857+
responses:
858+
'204':
859+
description: No content
860+
/admin/debug:
861+
get:
862+
operationId: debugInfo
863+
responses:
864+
'200':
865+
description: Debug info
866+
components:
867+
schemas:
868+
User:
869+
type: object
870+
UnusedSchema:
871+
type: object
872+
responses:
873+
UserResponse:
874+
description: User response
875+
```
876+
877+
**After snipping** (removed deleteAllUsers and debugInfo):
878+
879+
```yaml
880+
paths:
881+
/users:
882+
get:
883+
operationId: getUsers
884+
responses:
885+
'200':
886+
$ref: '#/components/responses/UserResponse'
887+
components:
888+
schemas:
889+
User:
890+
type: object
891+
responses:
892+
UserResponse:
893+
description: User response
894+
# DELETE operation removed, /admin/debug path removed entirely
895+
# UnusedSchema cleaned up automatically
896+
```
897+
898+
**Benefits of snipping:**
899+
900+
- **Reduce API surface**: Remove deprecated or internal operations before publishing
901+
- **Create filtered specs**: Generate subsets of your API for specific clients or use cases
902+
- **Interactive selection**: Visual browser makes it easy to identify and select operations
903+
- **Automatic cleanup**: Unused components are removed automatically
904+
- **Flexible input**: Support both operation IDs and path:method pairs
905+
- **Batch processing**: Remove multiple operations in one command
906+
907+
**Use Snip when:**
908+
909+
- You need to remove deprecated operations from a specification
910+
- You want to create a filtered version of your API for specific clients
911+
- You're preparing a public API specification and need to remove internal endpoints
912+
- You want to reduce the size and complexity of a specification
913+
- You need to create different API variants from a single source
914+
733915
## Common Options
734916

735917
All commands support these common options:

openapi/cmd/bootstrap.go renamed to cmd/openapi/commands/openapi/bootstrap.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package cmd
1+
package openapi
22

33
import (
44
"context"

openapi/cmd/bundle.go renamed to cmd/openapi/commands/openapi/bundle.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package cmd
1+
package openapi
22

33
import (
44
"context"

openapi/cmd/clean.go renamed to cmd/openapi/commands/openapi/clean.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package cmd
1+
package openapi
22

33
import (
44
"context"
@@ -53,9 +53,7 @@ Output options:
5353
Run: runClean,
5454
}
5555

56-
var (
57-
cleanWriteInPlace bool
58-
)
56+
var cleanWriteInPlace bool
5957

6058
func init() {
6159
cleanCmd.Flags().BoolVarP(&cleanWriteInPlace, "write", "w", false, "write result in-place to input file")
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package openapi
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
tea "github.com/charmbracelet/bubbletea"
10+
"github.com/speakeasy-api/openapi/cmd/openapi/internal/explore"
11+
"github.com/speakeasy-api/openapi/cmd/openapi/internal/explore/tui"
12+
"github.com/speakeasy-api/openapi/openapi"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
var exploreCmd = &cobra.Command{
17+
Use: "explore <file>",
18+
Short: "Interactively explore an OpenAPI specification",
19+
Long: `Launch an interactive terminal UI to browse and explore OpenAPI operations.
20+
21+
This command provides a user-friendly interface for navigating through API
22+
endpoints, viewing operation details, parameters, request/response information,
23+
and more.
24+
25+
Navigation:
26+
↑/k Move up
27+
↓/j Move down
28+
gg Jump to top
29+
G Jump to bottom
30+
Ctrl-U Scroll up by half a screen
31+
Ctrl-D Scroll down by half a screen
32+
Enter/Space Toggle operation details
33+
? Show help
34+
q/Esc Quit
35+
36+
The explore command helps you understand API structure and operation details
37+
without needing to manually parse the OpenAPI specification file.`,
38+
Args: cobra.ExactArgs(1),
39+
RunE: runExplore,
40+
}
41+
42+
func runExplore(cmd *cobra.Command, args []string) error {
43+
ctx := cmd.Context()
44+
inputFile := args[0]
45+
46+
// Load the OpenAPI document
47+
doc, err := loadOpenAPIDocument(ctx, inputFile)
48+
if err != nil {
49+
return err
50+
}
51+
52+
// Collect operations from the document
53+
operations, err := explore.CollectOperations(ctx, doc)
54+
if err != nil {
55+
return fmt.Errorf("failed to collect operations: %w", err)
56+
}
57+
58+
if len(operations) == 0 {
59+
return fmt.Errorf("no operations found in the OpenAPI document")
60+
}
61+
62+
// Get document info for display
63+
docTitle := doc.Info.Title
64+
if docTitle == "" {
65+
docTitle = "OpenAPI"
66+
}
67+
docVersion := doc.Info.Version
68+
if docVersion == "" {
69+
docVersion = "unknown"
70+
}
71+
72+
// Create and run the TUI
73+
m := tui.NewModel(operations, docTitle, docVersion)
74+
p := tea.NewProgram(m, tea.WithAltScreen())
75+
76+
if _, err := p.Run(); err != nil {
77+
return fmt.Errorf("error running explorer: %w", err)
78+
}
79+
80+
return nil
81+
}
82+
83+
// loadOpenAPIDocument loads an OpenAPI document from a file
84+
func loadOpenAPIDocument(ctx context.Context, file string) (*openapi.OpenAPI, error) {
85+
cleanFile := filepath.Clean(file)
86+
87+
f, err := os.Open(cleanFile)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to open file: %w", err)
90+
}
91+
defer f.Close()
92+
93+
doc, validationErrors, err := openapi.Unmarshal(ctx, f)
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to unmarshal OpenAPI document: %w", err)
96+
}
97+
if doc == nil {
98+
return nil, fmt.Errorf("failed to parse OpenAPI document: document is nil")
99+
}
100+
101+
// Report validation errors as warnings but continue
102+
if len(validationErrors) > 0 {
103+
fmt.Fprintf(os.Stderr, "⚠️ Found %d validation errors in document:\n", len(validationErrors))
104+
for i, validationErr := range validationErrors {
105+
if i < 5 { // Limit to first 5 errors
106+
fmt.Fprintf(os.Stderr, " %d. %s\n", i+1, validationErr.Error())
107+
}
108+
}
109+
if len(validationErrors) > 5 {
110+
fmt.Fprintf(os.Stderr, " ... and %d more\n", len(validationErrors)-5)
111+
}
112+
fmt.Fprintln(os.Stderr)
113+
}
114+
115+
return doc, nil
116+
}
117+
118+
// GetExploreCommand returns the explore command for external use
119+
func GetExploreCommand() *cobra.Command {
120+
return exploreCmd
121+
}

0 commit comments

Comments
 (0)