├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── components │ ├── list.go │ ├── root.go │ └── view.go ├── config │ └── config.go ├── endpoints │ ├── convert.go │ ├── list.go │ ├── root.go │ └── view.go ├── info │ ├── root.go │ └── view.go ├── mockserver │ ├── root.go │ └── run.go ├── root.go ├── servers │ ├── list.go │ └── root.go └── tags │ ├── list.go │ ├── root.go │ └── view.go ├── go.mod ├── go.sum ├── internal ├── converter │ ├── converter.go │ ├── curl.go │ └── fetch.go ├── mockserver │ ├── mockserver.go │ └── utils.go ├── model │ ├── operation.go │ └── parameter.go ├── printer │ ├── common.go │ ├── definition.go │ ├── parameters.go │ ├── request_body.go │ ├── responses.go │ └── schema.go ├── swagger │ ├── components.go │ ├── endpoints.go │ ├── loader.go │ ├── servers.go │ └── tags.go └── util │ └── color.go └── main.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release with Assets 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Triggers the workflow when a tag is pushed 7 | 8 | permissions: 9 | contents: write # Explicitly grant write permissions for contents 10 | 11 | jobs: 12 | create_release: 13 | runs-on: ubuntu-latest 14 | 15 | outputs: 16 | release_upload_url: ${{ steps.create_release.outputs.upload_url }} 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v3 21 | 22 | - name: Create Release 23 | id: create_release 24 | uses: actions/create-release@v1 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | with: 28 | tag_name: ${{ github.ref }} 29 | release_name: Release ${{ github.ref }} 30 | draft: false 31 | prerelease: false 32 | 33 | build: 34 | needs: create_release # Wait for the create_release job to complete 35 | runs-on: ${{ matrix.os }} 36 | 37 | strategy: 38 | matrix: 39 | os: [ubuntu-latest, windows-latest, macos-latest] 40 | arch: [amd64, arm64] # Different architectures 41 | 42 | steps: 43 | - name: Checkout code 44 | uses: actions/checkout@v3 45 | 46 | - name: Set up Go 47 | uses: actions/setup-go@v4 48 | with: 49 | go-version: '1.23' 50 | 51 | - name: Build binary (Windows) 52 | if: matrix.os == 'windows-latest' 53 | shell: pwsh 54 | run: | 55 | $GOOS = "windows" 56 | $EXT = ".exe" 57 | $GOARCH = "${{ matrix.arch }}" 58 | 59 | # Create the build directory and build the binary 60 | New-Item -ItemType Directory -Force -Path ./bin 61 | go build -o ./bin/swama-$GOOS-$GOARCH$EXT 62 | 63 | - name: Build binary (Linux & macOS) 64 | if: matrix.os != 'windows-latest' 65 | run: | 66 | if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then 67 | GOOS=linux 68 | EXT="" 69 | elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then 70 | GOOS=darwin 71 | EXT="" 72 | fi 73 | 74 | GOARCH=${{ matrix.arch }} 75 | 76 | # Create the build directory and build the binary 77 | mkdir -p ./bin 78 | go build -o ./bin/swama-${GOOS}-${GOARCH}${EXT} 79 | 80 | - name: Upload Release Assets 81 | uses: actions/upload-release-asset@v1 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | upload_url: ${{ needs.create_release.outputs.release_upload_url }} 86 | asset_path: ./bin/swama-${{ matrix.os == 'ubuntu-latest' && 'linux' || matrix.os == 'macos-latest' && 'darwin' || matrix.os == 'windows-latest' && 'windows' }}-${{ matrix.arch }}${{ matrix.os == 'windows-latest' && '.exe' || '' }} 87 | asset_name: swama-${{ matrix.os == 'ubuntu-latest' && 'linux' || matrix.os == 'macos-latest' && 'darwin' || matrix.os == 'windows-latest' && 'windows' }}-${{ matrix.arch }}${{ matrix.os == 'windows-latest' && '.exe' || '' }} 88 | asset_content_type: application/octet-stream -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | bin 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Go workspace file 18 | go.work 19 | go.work.sum 20 | 21 | # env file 22 | .env 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Suleiman Dibirov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Variables for Go paths 2 | GOPATH ?= $(shell go env GOPATH) 3 | GOBIN ?= $(or $(shell go env GOBIN),$(GOPATH)/bin) 4 | GOOS ?= $(shell go env GOOS) 5 | GOARCH ?= $(shell go env GOARCH) 6 | 7 | # Directories and project settings 8 | BUILD_DIR ?= ./bin 9 | PROJECT_NAME = swama 10 | BINARY = $(BUILD_DIR)/$(PROJECT_NAME) 11 | 12 | # PHONY targets 13 | .PHONY: all build install clean 14 | 15 | # Default target 16 | all: build 17 | 18 | # Ensure the build directory exists and build the binary 19 | build: | $(BUILD_DIR) 20 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(BINARY) 21 | 22 | $(BUILD_DIR): 23 | mkdir -p $(BUILD_DIR) 24 | 25 | # Install the binary to GOBIN 26 | install: build 27 | install -d $(GOBIN) 28 | install -m 755 $(BINARY) $(GOBIN)/$(PROJECT_NAME) 29 | 30 | # Clean build artifacts 31 | clean: 32 | rm -rf $(BUILD_DIR) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swama 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/idsulik/swama)](https://goreportcard.com/report/github.com/idsulik/swama) 3 | [![Version](https://img.shields.io/github/v/release/idsulik/swama)](https://github.com/idsulik/swama/v2/releases) 4 | [![License](https://img.shields.io/github/license/idsulik/swama)](https://github.com/idsulik/swama/v2/blob/main/LICENSE) 5 | [![GoDoc](https://pkg.go.dev/badge/github.com/idsulik/swama)](https://pkg.go.dev/github.com/idsulik/swama) 6 | 7 | Swama is a powerful command-line interface (CLI) tool for interacting with Swagger/OpenAPI definitions. It allows you to list, view, convert, and explore API specifications directly from the command line. Swama supports JSON and YAML formats for Swagger files, and it's available for multiple platforms through pre-built binaries. 8 | 9 | ## Features 10 | 11 | - **List and View Endpoints**: Explore API endpoints and their details. 12 | - **Convert Endpoints**: Convert API endpoints to `curl` or `fetch` commands for testing. 13 | - **Explore Tags and Servers**: Easily view API tags and servers. 14 | - **Flexible Filtering**: Filter endpoints by method, tag, or specific endpoint using wildcards. 15 | - **Grouping**: Group endpoint listings by tag or method. 16 | - **Support for Autocompletion**: Enable shell autocompletion for faster workflows. 17 | - **Mock Server**: Start a local mock server based on your Swagger/OpenAPI definitions. The mock server simulates API responses based on the specification, enabling rapid prototyping and testing. 18 | 19 | ## Installation 20 | 21 | ### Download Pre-Built Binaries 22 | 23 | Swama provides pre-built binaries for Linux, macOS, and Windows. You can download the appropriate binary from the [releases page](https://github.com/idsulik/swama/v2/releases). 24 | 25 | 1. **Download the latest release**: 26 | - Navigate to the [releases page](https://github.com/idsulik/swama/v2/releases). 27 | - Choose the binary for your platform (Linux, macOS, Windows). 28 | 29 | 2. **Install the binary**: 30 | - **Linux/MacOS**: Move the binary to a directory in your `$PATH`: 31 | ```bash 32 | sudo mv swama /usr/local/bin/ 33 | sudo chmod +x /usr/local/bin/swama 34 | ``` 35 | - **Windows**: Add the binary to your system's `PATH` for global access. 36 | 37 | ### Build from Source 38 | 39 | Alternatively, you can build Swama from source: 40 | 41 | ```bash 42 | git clone https://github.com/idsulik/swama 43 | cd swama 44 | go build -o swama 45 | ``` 46 | 47 | ## Usage 48 | 49 | After installation, you can use the `swama` command to interact with Swagger/OpenAPI files. 50 | 51 | ### General Command Usage 52 | 53 | ```bash 54 | swama [command] 55 | ``` 56 | 57 | ### Available Commands 58 | 59 | - **`completion`**: Generate the autocompletion script for the specified shell. 60 | - **`endpoints`**: Interact with API endpoints (list, view, convert). 61 | - **`mock-server`**: Start a local mock server based on the Swagger file. 62 | - **`components`**: Interact with API components (list, view). 63 | - **`info`**: Display general information about the Swagger file. 64 | - **`servers`**: List API servers. 65 | - **`tags`**: List and view API tags. 66 | 67 | ### Global Flags 68 | 69 | - **`-f, --file string`**: Path to the Swagger JSON/YAML file. If not provided, the tool will attempt to locate the Swagger file in the current directory. 70 | - **`-h, --help`**: Displays help for the `swama` command or any subcommand. 71 | 72 | --- 73 | 74 | ## Commands Overview 75 | 76 | ### Endpoints 77 | 78 | The `endpoints` command allows you to list, view, and convert API endpoints. 79 | 80 | #### List Endpoints 81 | 82 | Lists all API endpoints from a Swagger/OpenAPI file. 83 | 84 | ```bash 85 | swama endpoints list [flags] 86 | ``` 87 | 88 | **Available Flags**: 89 | 90 | - `-e, --endpoint string`: Filter by endpoint, supports wildcard. 91 | - `-g, --group string`: Group output by tag or method (default: "tag"). 92 | - `-m, --method string`: Filter by method (GET, POST, etc.). 93 | - `-t, --tag string`: Filter by tag. 94 | 95 | **Example**: 96 | 97 | ```bash 98 | swama endpoints list 99 | ``` 100 | 101 | ![preview](https://github.com/user-attachments/assets/59937e51-3992-4ee7-b629-a9d004310afc) 102 | 103 | #### View Endpoint Details 104 | 105 | Displays detailed information for a specific API endpoint. 106 | 107 | ```bash 108 | swama endpoints view [flags] 109 | ``` 110 | 111 | **Available Flags**: 112 | 113 | - `-e, --endpoint string`: Specify the endpoint to view. 114 | - `-m, --method string`: Specify the method (GET, POST, etc.) of the endpoint to view. 115 | 116 | **Example**: 117 | 118 | ```bash 119 | swama endpoints view --method=GET --endpoint=/user 120 | ``` 121 | 122 | ![preview](https://github.com/user-attachments/assets/7eff7784-f276-4027-9606-f59fdd6b0951) 123 | 124 | 125 | #### Convert an Endpoint 126 | 127 | Converts an API endpoint to either a `curl` or `fetch` command. 128 | 129 | ```bash 130 | swama endpoints convert [flags] 131 | ``` 132 | 133 | **Available Flags**: 134 | 135 | - `-e, --endpoint string`: Specify the endpoint to convert. 136 | - `-m, --method string`: Specify the method (GET, POST, etc.). 137 | - `-t, --type string`: Type to convert to (`curl`, `fetch`). 138 | 139 | **Example**: 140 | 141 | ```bash 142 | swama endpoints convert --file swagger.yaml --endpoint /api/users --method POST --type curl 143 | ``` 144 | 145 | ### Mock Server 146 | 147 | The `mock-server` command allows you to run a local mock server based on a Swagger/OpenAPI specification file. This mock server simulates API responses, making it easier to test and prototype API interactions locally. 148 | 149 | #### Run Mock Server 150 | 151 | Starts a mock server. 152 | 153 | ```bash 154 | swama mock-server run [flags] 155 | ``` 156 | **Available Flags**: 157 | 158 | - `--port int`: Specify the port for the mock server (default: 8080). 159 | - `--host string`: Set the host address for the mock server (default: "localhost"). 160 | - `--delay int`: Add a delay in milliseconds to each response, useful for simulating network latency. 161 | - `--default-response-code int`: Set the default response code to use (default: 200). 162 | - `--default-response-type string`: Set the default response type to use (default: "json"). 163 | 164 | **Example**: 165 | 166 | ```bash 167 | swama mock-server run --port 8081 --host 0.0.0.0 --delay 200 168 | ``` 169 | 170 | This command starts a mock server on port 8081, accessible on all network interfaces (`0.0.0.0`), with a 200ms delay added to each response to simulate latency. 171 | 172 | ### Components 173 | 174 | The `components` command allows you to list, and view API components(requests, responses etc.). 175 | 176 | #### List Components 177 | 178 | Lists all API components from a Swagger/OpenAPI file. 179 | 180 | ```bash 181 | swama components list [flags] 182 | ``` 183 | 184 | **Example**: 185 | 186 | ```bash 187 | swama components list 188 | ``` 189 | 190 | ![preview](https://github.com/user-attachments/assets/a83c32ba-7b8d-4aec-b9c0-33e0bacfdff8) 191 | 192 | #### View Component Details 193 | 194 | Displays detailed information for a specific API component. 195 | 196 | ```bash 197 | swama components view [flags] 198 | ``` 199 | 200 | **Available Flags**: 201 | 202 | - `-n, --name string`: Specify the component's name to view. 203 | 204 | **Example**: 205 | 206 | ```bash 207 | swama components view --name customer 208 | ``` 209 | 210 | ![preview](https://github.com/user-attachments/assets/073d93bd-d348-48e2-b750-571e803c0a73) 211 | 212 | ### Tags 213 | 214 | The `tags` command allows you to list API tags in the Swagger/OpenAPI file. 215 | 216 | ```bash 217 | swama tags list [flags] 218 | ``` 219 | 220 | **Available Flags**: 221 | 222 | - `-h, --help`: Displays help for the `tags` command. 223 | 224 | **Example**: 225 | 226 | ```bash 227 | swama tags list --file swagger.yaml 228 | ``` 229 | 230 | ### Servers 231 | 232 | The `servers` command allows you to list servers from the Swagger/OpenAPI file. 233 | 234 | ```bash 235 | swama servers list [flags] 236 | ``` 237 | 238 | **Available Flags**: 239 | 240 | - `-h, --help`: Displays help for the `servers` command. 241 | 242 | **Example**: 243 | 244 | ```bash 245 | swama servers list --file swagger.yaml 246 | ``` 247 | 248 | ### Info 249 | 250 | Displays general information about the Swagger/OpenAPI file, such as the version, title, and description. 251 | 252 | ```bash 253 | swama info view --file swagger.yaml 254 | ``` 255 | 256 | ![preview](https://github.com/user-attachments/assets/6fd03077-e7f6-4baa-8b17-17626c5d12a2) 257 | --- 258 | 259 | ## Autocompletion 260 | 261 | Swama supports autocompletion for various shells, such as Bash and Zsh. You can generate a script for your shell to enable autocompletion. 262 | 263 | ### Example: Generate Bash Completion Script 264 | 265 | ```bash 266 | swama completion bash > /etc/bash_completion.d/swama 267 | ``` 268 | 269 | ### Example: Generate Zsh Completion Script 270 | 271 | ```bash 272 | swama completion zsh > ~/.zsh/completion/_swama 273 | ``` 274 | 275 | ## Contributing 276 | 277 | Contributions to Swama are welcome! Feel free to submit issues or pull requests on the [GitHub repository](https://github.com/idsulik/swama). 278 | 279 | ## License 280 | 281 | Swama is licensed under the MIT License. See the `LICENSE` file for more details. 282 | 283 | --- 284 | 285 | With Swama, interacting with Swagger/OpenAPI files is straightforward and efficient. Whether you're exploring API endpoints, converting them to testable commands, or managing servers and tags, Swama provides a simple and powerful interface for your needs. Get started by downloading the binary or building from source today! 286 | -------------------------------------------------------------------------------- /cmd/components/list.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/idsulik/swama/v2/cmd/config" 7 | "github.com/idsulik/swama/v2/internal/swagger" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // newListCommand creates the "components list" subcommand 12 | func newListCommand() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "list", 15 | Short: "Lists all API components from a Swagger file", 16 | Example: "swama components list", 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | doc, err := swagger.LoadSwaggerFile(cmd.Context(), config.SwaggerPath) 19 | 20 | if err != nil { 21 | return fmt.Errorf("failed to load Swagger file: %w", err) 22 | } 23 | 24 | components := swagger.NewComponents(doc) 25 | 26 | return components.ListComponents() 27 | }, 28 | } 29 | 30 | return cmd 31 | } 32 | -------------------------------------------------------------------------------- /cmd/components/root.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewComponentsCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "components", 10 | Aliases: []string{"component", "compo", "schemas"}, 11 | Short: "Interact with API components", 12 | } 13 | 14 | cmd.AddCommand(newListCommand()) 15 | cmd.AddCommand(newViewCommand()) 16 | 17 | return cmd 18 | } 19 | -------------------------------------------------------------------------------- /cmd/components/view.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/idsulik/swama/v2/cmd/config" 7 | "github.com/idsulik/swama/v2/internal/swagger" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type viewConfig struct { 12 | name string 13 | } 14 | 15 | // Command-specific flags for the view command 16 | var viewCfg = viewConfig{} 17 | 18 | // newViewCommand creates the "components view" subcommand 19 | func newViewCommand() *cobra.Command { 20 | cmd := &cobra.Command{ 21 | Use: "view", 22 | Short: "View details of a specific component", 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | doc, err := swagger.LoadSwaggerFile(cmd.Context(), config.SwaggerPath) 25 | 26 | if err != nil { 27 | return fmt.Errorf("failed to load Swagger file: %w", err) 28 | } 29 | 30 | components := swagger.NewComponents(doc) 31 | 32 | return components.ViewComponent(viewCfg.name) 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVarP(&viewCfg.name, "name", "n", "", "Name of the component to view") 37 | 38 | return cmd 39 | } 40 | -------------------------------------------------------------------------------- /cmd/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var ( 4 | SwaggerPath string 5 | ) 6 | -------------------------------------------------------------------------------- /cmd/endpoints/convert.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/idsulik/swama/v2/cmd/config" 7 | "github.com/idsulik/swama/v2/internal/swagger" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type convertConfig struct { 12 | method string 13 | endpoint string 14 | toType string 15 | } 16 | 17 | // Command-specific flags for the convert command 18 | var convertCfg = convertConfig{} 19 | 20 | // newConvertCommand creates the "endpoints convert" subcommand 21 | func newConvertCommand() *cobra.Command { 22 | cmd := &cobra.Command{ 23 | Use: "convert", 24 | Short: "Convert an endpoint to curl or fetch", 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | doc, err := swagger.LoadSwaggerFile(cmd.Context(), config.SwaggerPath) 27 | 28 | if err != nil { 29 | return fmt.Errorf("failed to load Swagger file: %w", err) 30 | } 31 | 32 | endpoints := swagger.NewEndpoints(doc) 33 | 34 | return endpoints.ConvertEndpoint( 35 | swagger.ConvertOptions{ 36 | Method: convertCfg.method, 37 | Endpoint: convertCfg.endpoint, 38 | ToType: convertCfg.toType, 39 | }, 40 | ) 41 | }, 42 | } 43 | 44 | cmd.Flags().StringVarP(&convertCfg.method, "method", "m", "", "Method of the endpoint to convert") 45 | cmd.Flags().StringVarP(&convertCfg.endpoint, "endpoint", "e", "", "Endpoint to convert") 46 | cmd.Flags().StringVarP(&convertCfg.toType, "type", "t", "", "Type to convert to (curl, fetch)") 47 | 48 | return cmd 49 | } 50 | -------------------------------------------------------------------------------- /cmd/endpoints/list.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/idsulik/swama/v2/cmd/config" 7 | "github.com/idsulik/swama/v2/internal/swagger" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type listConfig struct { 12 | method string 13 | endpoint string 14 | tag string 15 | group string 16 | } 17 | 18 | // Command-specific flags for the list command 19 | var listCfg = listConfig{} 20 | 21 | // newListCommand creates the "endpoints list" subcommand 22 | func newListCommand() *cobra.Command { 23 | cmd := &cobra.Command{ 24 | Use: "list", 25 | Short: "Lists all API endpoints from a Swagger file", 26 | Example: "swama endpoints list --method GET --tag user", 27 | RunE: func(cmd *cobra.Command, args []string) error { 28 | doc, err := swagger.LoadSwaggerFile(cmd.Context(), config.SwaggerPath) 29 | 30 | if err != nil { 31 | return fmt.Errorf("failed to load Swagger file: %w", err) 32 | } 33 | 34 | endpoints := swagger.NewEndpoints(doc) 35 | 36 | return endpoints.ListEndpoints( 37 | swagger.ListOptions{ 38 | Method: listCfg.method, 39 | Endpoint: listCfg.endpoint, 40 | Tag: listCfg.tag, 41 | Group: listCfg.group, 42 | }, 43 | ) 44 | }, 45 | } 46 | 47 | cmd.Flags().StringVarP(&listCfg.method, "method", "m", "", "Filter by method") 48 | cmd.Flags().StringVarP(&listCfg.endpoint, "endpoint", "e", "", "Filter by endpoint, supports wildcard") 49 | cmd.Flags().StringVarP(&listCfg.tag, "tag", "t", "", "Filter by tag") 50 | cmd.Flags().StringVarP(&listCfg.group, "group", "g", "tag", "Group output by tag, method") 51 | 52 | return cmd 53 | } 54 | -------------------------------------------------------------------------------- /cmd/endpoints/root.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewEndpointsCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "endpoints", 10 | Aliases: []string{"endpoint", "ep"}, 11 | Short: "Interact with API endpoints", 12 | } 13 | 14 | cmd.AddCommand(newListCommand()) 15 | cmd.AddCommand(newViewCommand()) 16 | cmd.AddCommand(newConvertCommand()) 17 | 18 | return cmd 19 | } 20 | -------------------------------------------------------------------------------- /cmd/endpoints/view.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/idsulik/swama/v2/cmd/config" 7 | "github.com/idsulik/swama/v2/internal/swagger" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type viewConfig struct { 12 | method string 13 | endpoint string 14 | } 15 | 16 | // Command-specific flags for the view command 17 | var viewCfg = viewConfig{} 18 | 19 | // newViewCommand creates the "endpoints view" subcommand 20 | func newViewCommand() *cobra.Command { 21 | cmd := &cobra.Command{ 22 | Use: "view", 23 | Short: "View details of a specific endpoint", 24 | RunE: func(cmd *cobra.Command, args []string) error { 25 | doc, err := swagger.LoadSwaggerFile(cmd.Context(), config.SwaggerPath) 26 | 27 | if err != nil { 28 | return fmt.Errorf("failed to load Swagger file: %w", err) 29 | } 30 | 31 | endpoints := swagger.NewEndpoints(doc) 32 | 33 | return endpoints.ViewEndpoint( 34 | swagger.ViewOptions{ 35 | Method: viewCfg.method, 36 | Endpoint: viewCfg.endpoint, 37 | }, 38 | ) 39 | }, 40 | } 41 | 42 | cmd.Flags().StringVarP(&viewCfg.method, "method", "m", "", "Method of the endpoint to view") 43 | cmd.Flags().StringVarP(&viewCfg.endpoint, "endpoint", "e", "", "Endpoint to view") 44 | 45 | return cmd 46 | } 47 | -------------------------------------------------------------------------------- /cmd/info/root.go: -------------------------------------------------------------------------------- 1 | package info 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewInfoCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "info", 10 | Short: "Interact with info", 11 | } 12 | 13 | cmd.AddCommand(vewViewCommand()) 14 | 15 | return cmd 16 | } 17 | -------------------------------------------------------------------------------- /cmd/info/view.go: -------------------------------------------------------------------------------- 1 | package info 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/idsulik/swama/v2/cmd/config" 8 | "github.com/idsulik/swama/v2/internal/swagger" 9 | "github.com/olekukonko/tablewriter" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // vewViewCommand creates the "view" subcommand 14 | func vewViewCommand() *cobra.Command { 15 | cmd := &cobra.Command{ 16 | Use: "view", 17 | Short: "Displays information about the Swagger file", 18 | RunE: viewCommandFunc, 19 | } 20 | 21 | return cmd 22 | } 23 | 24 | func viewCommandFunc(cmd *cobra.Command, _ []string) error { 25 | doc, err := swagger.LoadSwaggerFile(cmd.Context(), config.SwaggerPath) 26 | 27 | if err != nil { 28 | return fmt.Errorf("failed to load Swagger file: %w", err) 29 | } 30 | 31 | table := tablewriter.NewWriter(os.Stdout) 32 | table.SetRowLine(true) 33 | table.SetAutoWrapText(false) 34 | 35 | table.Append([]string{"Title", doc.Info.Title}) 36 | table.Append([]string{"Version", doc.Info.Version}) 37 | if doc.Info.Contact != nil { 38 | if doc.Info.Contact.Email != "" { 39 | table.Append([]string{"Email", doc.Info.Contact.Email}) 40 | } 41 | } 42 | if doc.Info.License != nil { 43 | if doc.Info.License.Name != "" { 44 | table.Append([]string{"License", fmt.Sprintf("%s (%s)", doc.Info.License.Name, doc.Info.License.URL)}) 45 | } 46 | } 47 | table.Append([]string{"Description", doc.Info.Description}) 48 | table.Append([]string{"Terms of Service", doc.Info.TermsOfService}) 49 | 50 | if doc.ExternalDocs != nil { 51 | table.Append([]string{"External docs", doc.ExternalDocs.URL}) 52 | } 53 | table.Render() 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /cmd/mockserver/root.go: -------------------------------------------------------------------------------- 1 | package mockserver 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewMockCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "mock-server", 10 | Aliases: []string{"mock-server"}, 11 | Short: "Interact with mock server", 12 | } 13 | 14 | cmd.AddCommand(newRunCommand()) 15 | 16 | return cmd 17 | } 18 | -------------------------------------------------------------------------------- /cmd/mockserver/run.go: -------------------------------------------------------------------------------- 1 | package mockserver 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/idsulik/swama/v2/cmd/config" 7 | mockserver2 "github.com/idsulik/swama/v2/internal/mockserver" 8 | "github.com/idsulik/swama/v2/internal/swagger" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type runConfig struct { 13 | host string 14 | port int 15 | delay int 16 | defaultResponseCode string 17 | defaultResponseType string 18 | } 19 | 20 | // Command-specific flags for the run command 21 | var runCfg = runConfig{} 22 | 23 | // newRunCommand creates the "tags run" subcommand 24 | func newRunCommand() *cobra.Command { 25 | cmd := &cobra.Command{ 26 | Use: "run", 27 | Short: "Run details of a specific tag", 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | doc, err := swagger.LoadSwaggerFile(cmd.Context(), config.SwaggerPath) 30 | 31 | if err != nil { 32 | return fmt.Errorf("failed to load Swagger file: %w", err) 33 | } 34 | 35 | mockserver := mockserver2.NewMockServer(doc) 36 | 37 | return mockserver.Run( 38 | mockserver2.RunOptions{ 39 | Host: runCfg.host, 40 | Port: runCfg.port, 41 | Delay: runCfg.delay, 42 | DefaultResponseCode: runCfg.defaultResponseCode, 43 | DefaultResponseType: runCfg.defaultResponseType, 44 | }, 45 | ) 46 | }, 47 | } 48 | 49 | cmd.Flags().StringVarP(&runCfg.host, "host", "", "127.0.0.1", "Host to run the mock server on") 50 | cmd.Flags().IntVarP(&runCfg.port, "port", "p", 8080, "Port to run the mock server on") 51 | cmd.Flags().IntVarP(&runCfg.delay, "delay", "d", 0, "Delay in milliseconds to simulate network latency") 52 | cmd.Flags().StringVarP( 53 | &runCfg.defaultResponseCode, 54 | "default-response-code", 55 | "", 56 | "200", 57 | "Default response code to use", 58 | ) 59 | cmd.Flags().StringVarP( 60 | &runCfg.defaultResponseType, 61 | "default-response-type", 62 | "", 63 | "json", 64 | "Default response type to use", 65 | ) 66 | 67 | return cmd 68 | } 69 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/idsulik/swama/v2/cmd/components" 7 | "github.com/idsulik/swama/v2/cmd/config" 8 | "github.com/idsulik/swama/v2/cmd/endpoints" 9 | "github.com/idsulik/swama/v2/cmd/info" 10 | "github.com/idsulik/swama/v2/cmd/mockserver" 11 | "github.com/idsulik/swama/v2/cmd/servers" 12 | "github.com/idsulik/swama/v2/cmd/tags" 13 | "github.com/idsulik/swama/v2/internal/swagger" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // rootCmd represents the base command 18 | var rootCmd = &cobra.Command{ 19 | Use: "swama", 20 | Short: "Swama is a CLI tool for interacting with Swagger/OpenAPI definitions", 21 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 22 | if config.SwaggerPath == "" { 23 | config.SwaggerPath = swagger.LocateSwaggerFile() 24 | } 25 | }, 26 | } 27 | 28 | func Execute(ctx context.Context) error { 29 | return rootCmd.ExecuteContext(ctx) 30 | } 31 | 32 | func init() { 33 | rootCmd.PersistentFlags().StringVarP( 34 | &config.SwaggerPath, 35 | "file", 36 | "f", 37 | "", 38 | "Path to the Swagger JSON/YAML file. If not provided, the tool will try to locate it.", 39 | ) 40 | 41 | rootCmd.AddCommand(info.NewInfoCommand()) 42 | rootCmd.AddCommand(endpoints.NewEndpointsCommand()) 43 | rootCmd.AddCommand(components.NewComponentsCommand()) 44 | rootCmd.AddCommand(servers.NewServersCommand()) 45 | rootCmd.AddCommand(tags.NewTagsCommand()) 46 | rootCmd.AddCommand(mockserver.NewMockCommand()) 47 | } 48 | -------------------------------------------------------------------------------- /cmd/servers/list.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/idsulik/swama/v2/cmd/config" 7 | "github.com/idsulik/swama/v2/internal/swagger" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // newListCommand creates the "servers list" subcommand 12 | func newListCommand() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "list", 15 | Short: "Lists all servers from a Swagger file", 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | doc, err := swagger.LoadSwaggerFile(cmd.Context(), config.SwaggerPath) 18 | 19 | if err != nil { 20 | return fmt.Errorf("failed to load Swagger file: %w", err) 21 | } 22 | 23 | servers := swagger.NewServers(doc) 24 | 25 | return servers.ListServers() 26 | }, 27 | } 28 | 29 | return cmd 30 | } 31 | -------------------------------------------------------------------------------- /cmd/servers/root.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewServersCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "servers", 10 | Aliases: []string{"server"}, 11 | Short: "Interact with servers", 12 | } 13 | 14 | cmd.AddCommand(newListCommand()) 15 | 16 | return cmd 17 | } 18 | -------------------------------------------------------------------------------- /cmd/tags/list.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/idsulik/swama/v2/cmd/config" 7 | "github.com/idsulik/swama/v2/internal/swagger" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // newListCommand creates the "tags list" subcommand 12 | func newListCommand() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "list", 15 | Short: "Lists all tags from a Swagger file", 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | doc, err := swagger.LoadSwaggerFile(cmd.Context(), config.SwaggerPath) 18 | 19 | if err != nil { 20 | return fmt.Errorf("failed to load Swagger file: %w", err) 21 | } 22 | 23 | tags := swagger.NewTags(doc) 24 | 25 | return tags.ListTags() 26 | }, 27 | } 28 | 29 | return cmd 30 | } 31 | -------------------------------------------------------------------------------- /cmd/tags/root.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewTagsCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "tags", 10 | Aliases: []string{"tag"}, 11 | Short: "Interact with tags", 12 | } 13 | 14 | cmd.AddCommand(newListCommand()) 15 | cmd.AddCommand(newViewCommand()) 16 | 17 | return cmd 18 | } 19 | -------------------------------------------------------------------------------- /cmd/tags/view.go: -------------------------------------------------------------------------------- 1 | package tags 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/idsulik/swama/v2/cmd/config" 7 | "github.com/idsulik/swama/v2/internal/swagger" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type viewConfig struct { 12 | name string 13 | } 14 | 15 | // Command-specific flags for the view command 16 | var viewCfg = viewConfig{} 17 | 18 | // newViewCommand creates the "tags view" subcommand 19 | func newViewCommand() *cobra.Command { 20 | cmd := &cobra.Command{ 21 | Use: "view", 22 | Short: "View details of a specific tag", 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | doc, err := swagger.LoadSwaggerFile(cmd.Context(), config.SwaggerPath) 25 | 26 | if err != nil { 27 | return fmt.Errorf("failed to load Swagger file: %w", err) 28 | } 29 | 30 | tags := swagger.NewTags(doc) 31 | 32 | return tags.ViewTag(viewCfg.name) 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVarP(&viewCfg.name, "name", "n", "", "Name of the tag to view") 37 | 38 | return cmd 39 | } 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/idsulik/swama/v2 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/getkin/kin-openapi v0.127.0 7 | github.com/manifoldco/promptui v0.9.0 8 | github.com/olekukonko/tablewriter v0.0.5 9 | github.com/spf13/cobra v1.8.1 10 | ) 11 | 12 | require ( 13 | github.com/brianvoe/gofakeit/v7 v7.1.2 // indirect 14 | github.com/bytedance/sonic v1.12.3 // indirect 15 | github.com/bytedance/sonic/loader v0.2.1 // indirect 16 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 17 | github.com/cloudwego/base64x v0.1.4 // indirect 18 | github.com/cloudwego/iasm v0.2.0 // indirect 19 | github.com/gabriel-vasile/mimetype v1.4.6 // indirect 20 | github.com/gin-contrib/sse v0.1.0 // indirect 21 | github.com/gin-gonic/gin v1.10.0 // indirect 22 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 23 | github.com/go-openapi/swag v0.23.0 // indirect 24 | github.com/go-playground/locales v0.14.1 // indirect 25 | github.com/go-playground/universal-translator v0.18.1 // indirect 26 | github.com/go-playground/validator/v10 v10.22.1 // indirect 27 | github.com/goccy/go-json v0.10.3 // indirect 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 29 | github.com/invopop/yaml v0.3.1 // indirect 30 | github.com/josharian/intern v1.0.0 // indirect 31 | github.com/json-iterator/go v1.1.12 // indirect 32 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 33 | github.com/leodido/go-urn v1.4.0 // indirect 34 | github.com/mailru/easyjson v0.7.7 // indirect 35 | github.com/mattn/go-isatty v0.0.20 // indirect 36 | github.com/mattn/go-runewidth v0.0.9 // indirect 37 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 38 | github.com/modern-go/reflect2 v1.0.2 // indirect 39 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 40 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 41 | github.com/perimeterx/marshmallow v1.1.5 // indirect 42 | github.com/spf13/pflag v1.0.5 // indirect 43 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 44 | github.com/ugorji/go/codec v1.2.12 // indirect 45 | golang.org/x/arch v0.11.0 // indirect 46 | golang.org/x/crypto v0.28.0 // indirect 47 | golang.org/x/net v0.30.0 // indirect 48 | golang.org/x/sys v0.26.0 // indirect 49 | golang.org/x/text v0.19.0 // indirect 50 | google.golang.org/protobuf v1.35.1 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/brianvoe/gofakeit/v7 v7.1.2 h1:vSKaVScNhWVpf1rlyEKSvO8zKZfuDtGqoIHT//iNNb8= 2 | github.com/brianvoe/gofakeit/v7 v7.1.2/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= 3 | github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= 4 | github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= 5 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 6 | github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= 7 | github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 8 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 9 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 10 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 11 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 12 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 13 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 14 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 15 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 16 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 17 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 18 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= 23 | github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= 24 | github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= 25 | github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= 26 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 27 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 28 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 29 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 30 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 31 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 32 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 33 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 34 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 35 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 36 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 37 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 38 | github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= 39 | github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 40 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 41 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 42 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 43 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 44 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 45 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 46 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 47 | github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= 48 | github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= 49 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 50 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 51 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 52 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 53 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 54 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 55 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 56 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 57 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 58 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 59 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 60 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 61 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 62 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 63 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 64 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 65 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 66 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 67 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 68 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 69 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 70 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 71 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 73 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 74 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 75 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 76 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 77 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 78 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 79 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 80 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 81 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 82 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 83 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 84 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 85 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 86 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 87 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 88 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 89 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 90 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 91 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 92 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 93 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 94 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 95 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 96 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 97 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 98 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 99 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 100 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 101 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 102 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 103 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 104 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 105 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 106 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 107 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 108 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 109 | golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= 110 | golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 111 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 112 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 113 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 114 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 115 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 116 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= 117 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 121 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 122 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 123 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 124 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 125 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 126 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 127 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 128 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 129 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 130 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 131 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 132 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 133 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 134 | -------------------------------------------------------------------------------- /internal/converter/converter.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/getkin/kin-openapi/openapi3" 8 | "github.com/idsulik/swama/v2/internal/model" 9 | "github.com/manifoldco/promptui" 10 | ) 11 | 12 | const ( 13 | CurlType = "curl" 14 | FetchType = "fetch" 15 | ) 16 | 17 | var ( 18 | ErrInvalidConvertType = errors.New(fmt.Sprintf("invalid convert type. Must be %q or %q", CurlType, FetchType)) 19 | ) 20 | 21 | type Converter interface { 22 | ConvertEndpoint(method string, endpoint string, _ *model.Operation) string 23 | } 24 | 25 | func NewConverter(convertType string) (Converter, error) { 26 | switch convertType { 27 | case CurlType: 28 | return NewCurlConverter(), nil 29 | case FetchType: 30 | return NewFetchConverter(), nil 31 | default: 32 | return nil, ErrInvalidConvertType 33 | } 34 | } 35 | 36 | func askForValue(param *model.Parameter) string { 37 | var paramValue string 38 | fmt.Printf("Enter value for parameter %q: ", param.Name) 39 | _, _ = fmt.Scanln(¶mValue) 40 | if paramValue == "" && param.Required { 41 | fmt.Printf("parameter %q is required\n", param.Name) 42 | return askForValue(param) 43 | } 44 | 45 | return paramValue 46 | } 47 | 48 | func askForContentType(content openapi3.Content) string { 49 | if len(content) == 0 { 50 | return "" 51 | } 52 | 53 | contentTypes := make([]string, 0, len(content)) 54 | for contentType := range content { 55 | contentTypes = append(contentTypes, contentType) 56 | } 57 | 58 | if len(contentTypes) == 1 { 59 | return contentTypes[0] 60 | } 61 | 62 | prompt := promptui.Select{ 63 | Label: "Content Type", 64 | Items: contentTypes, 65 | } 66 | 67 | _, result, err := prompt.Run() 68 | 69 | if err != nil { 70 | fmt.Printf("Prompt failed %v\n", err) 71 | return askForContentType(content) 72 | } 73 | 74 | return result 75 | } 76 | -------------------------------------------------------------------------------- /internal/converter/curl.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/idsulik/swama/v2/internal/model" 8 | ) 9 | 10 | type Curl struct { 11 | } 12 | 13 | const curlPattern = "curl -X %s %s" 14 | 15 | func NewCurlConverter() *Curl { 16 | return &Curl{} 17 | } 18 | 19 | func (c *Curl) ConvertEndpoint(method string, endpoint string, operation *model.Operation) string { 20 | var headers string 21 | 22 | for _, param := range operation.Parameters { 23 | if param.In == model.ParameterInPath { 24 | value := askForValue(param) 25 | 26 | if value == "" { 27 | continue 28 | } 29 | 30 | endpoint = strings.Replace(endpoint, fmt.Sprintf("{%s}", param.Name), value, 1) 31 | } else if param.In == model.ParameterInQuery { 32 | value := askForValue(param) 33 | 34 | if value == "" { 35 | continue 36 | } 37 | 38 | if strings.Contains(endpoint, "?") { 39 | endpoint = fmt.Sprintf("%s&%s=%s", endpoint, param.Name, value) 40 | } else { 41 | endpoint = fmt.Sprintf("%s?%s=%s", endpoint, param.Name, value) 42 | } 43 | } else if param.In == model.ParameterInHeader { 44 | value := askForValue(param) 45 | 46 | if value == "" { 47 | continue 48 | } 49 | 50 | headers += fmt.Sprintf(" -H '%s: %s'", param.Name, value) 51 | } else if param.In == model.ParameterInCookie { 52 | value := askForValue(param) 53 | 54 | if value == "" { 55 | continue 56 | } 57 | 58 | headers += fmt.Sprintf(" -H 'Cookie: %s=%s'", param.Name, value) 59 | } 60 | } 61 | 62 | var body string 63 | if operation.RequestBody != nil { 64 | contentType := askForContentType(operation.RequestBody.Value.Content) 65 | if contentType != "" { 66 | headers += fmt.Sprintf(" -H 'Content-Type: %s'", contentType) 67 | } 68 | 69 | body = " -d ''" 70 | } 71 | 72 | return fmt.Sprintf(curlPattern, strings.ToUpper(method), endpoint+headers+body) 73 | } 74 | -------------------------------------------------------------------------------- /internal/converter/fetch.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/idsulik/swama/v2/internal/model" 8 | ) 9 | 10 | type Fetch struct { 11 | } 12 | 13 | const fetchPattern = "fetch('%s', { method: '%s', headers: %s, body: %s })" 14 | 15 | func NewFetchConverter() *Fetch { 16 | return &Fetch{} 17 | } 18 | 19 | func (c *Fetch) ConvertEndpoint(method string, endpoint string, operation *model.Operation) string { 20 | headers := make(map[string]string) 21 | 22 | for _, param := range operation.Parameters { 23 | if param.In == model.ParameterInPath { 24 | value := askForValue(param) 25 | 26 | if value == "" { 27 | continue 28 | } 29 | 30 | endpoint = strings.Replace(endpoint, fmt.Sprintf("{%s}", param.Name), value, 1) 31 | } else if param.In == model.ParameterInQuery { 32 | value := askForValue(param) 33 | 34 | if value == "" { 35 | continue 36 | } 37 | 38 | if strings.Contains(endpoint, "?") { 39 | endpoint = fmt.Sprintf("%s&%s=%s", endpoint, param.Name, value) 40 | } else { 41 | endpoint = fmt.Sprintf("%s?%s=%s", endpoint, param.Name, value) 42 | } 43 | } else if param.In == model.ParameterInHeader { 44 | value := askForValue(param) 45 | 46 | if value == "" { 47 | continue 48 | } 49 | 50 | headers[param.Name] = value 51 | } else if param.In == model.ParameterInCookie { 52 | value := askForValue(param) 53 | 54 | if value == "" { 55 | continue 56 | } 57 | 58 | headers["Cookie"] = fmt.Sprintf("%s=%s", param.Name, value) 59 | } 60 | } 61 | 62 | var body string 63 | if operation.RequestBody != nil { 64 | contentType := askForContentType(operation.RequestBody.Value.Content) 65 | if contentType != "" { 66 | headers["Content-Type"] = contentType 67 | } 68 | 69 | body = "''" 70 | } 71 | 72 | headersBuilder := strings.Builder{} 73 | headersBuilder.WriteString("{") 74 | for k, v := range headers { 75 | headersBuilder.WriteString(fmt.Sprintf("'%s': '%s',", k, v)) 76 | } 77 | headersBuilder.WriteString("}") 78 | 79 | return fmt.Sprintf(fetchPattern, endpoint, strings.ToUpper(method), headersBuilder.String(), body) 80 | } 81 | -------------------------------------------------------------------------------- /internal/mockserver/mockserver.go: -------------------------------------------------------------------------------- 1 | package mockserver 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "github.com/getkin/kin-openapi/openapi3" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | const ( 15 | responseCodeHeaderName = "X-Mock-Response-Code" 16 | responseCodeQueryParamName = "x-response-code" 17 | responseTypeQueryParamName = "x-response-type" 18 | availableResponsesHeaderName = "X-Available-Responses" 19 | ) 20 | 21 | type RunOptions struct { 22 | Host string 23 | Port int 24 | Delay int 25 | DefaultResponseCode string 26 | DefaultResponseType string 27 | } 28 | 29 | // XMLNode represents a generic XML node 30 | type XMLNode struct { 31 | XMLName xml.Name 32 | Attrs []xml.Attr `xml:"attr,omitempty"` 33 | Value string `xml:",chardata"` 34 | Children []*XMLNode `xml:",any"` 35 | } 36 | 37 | type MockServer struct { 38 | doc *openapi3.T 39 | } 40 | 41 | func NewMockServer(doc *openapi3.T) *MockServer { 42 | return &MockServer{ 43 | doc: doc, 44 | } 45 | } 46 | 47 | func (m *MockServer) Run(options RunOptions) error { 48 | gin.SetMode(gin.ReleaseMode) 49 | app := gin.Default() 50 | 51 | app.Use(m.availableResponsesMiddleware()) 52 | 53 | for _, path := range m.doc.Paths.InMatchingOrder() { 54 | for method, operation := range m.doc.Paths.Find(path).Operations() { 55 | app.Handle(method, convertPathToGinFormat(path), m.registerHandler(operation, options)) 56 | } 57 | } 58 | 59 | // Index route to list all registered routes 60 | app.GET( 61 | "/", func(c *gin.Context) { 62 | var routes []gin.H 63 | for _, route := range app.Routes() { 64 | if route.Path == "/" { 65 | continue 66 | } 67 | 68 | path := m.doc.Paths.Find(convertGinPathToOpenAPI(route.Path)) 69 | responseCodeToContentType := make(map[string]string) 70 | if op := path.GetOperation(route.Method); op != nil { 71 | for code, resp := range op.Responses.Map() { 72 | responseCodeToContentType[code] = "application/json" 73 | for contentType := range resp.Value.Content { 74 | responseCodeToContentType[code] = contentType 75 | } 76 | } 77 | } 78 | 79 | routes = append( 80 | routes, gin.H{ 81 | "method": route.Method, 82 | "path": route.Path, 83 | "availableResponses": responseCodeToContentType, 84 | }, 85 | ) 86 | } 87 | 88 | c.JSON( 89 | http.StatusOK, gin.H{ 90 | "routes": routes, 91 | "usage": gin.H{ 92 | "responseCode": gin.H{ 93 | "queryParam": fmt.Sprintf("?%s=", responseCodeQueryParamName), 94 | "header": fmt.Sprintf("%s: ", responseCodeHeaderName), 95 | "availableCodes": fmt.Sprintf("%s header in response", availableResponsesHeaderName), 96 | }, 97 | "responseType": gin.H{ 98 | "queryParam": fmt.Sprintf("?%s=", responseTypeQueryParamName), 99 | }, 100 | }, 101 | }, 102 | ) 103 | }, 104 | ) 105 | 106 | fmt.Printf("Mock server listening on http://%s:%d\n", options.Host, options.Port) 107 | return app.Run(fmt.Sprintf("%s:%d", options.Host, options.Port)) 108 | } 109 | 110 | func (m *MockServer) availableResponsesMiddleware() gin.HandlerFunc { 111 | return func(c *gin.Context) { 112 | c.Next() 113 | 114 | // After handler execution, add header with available response codes 115 | path := m.doc.Paths.Find(convertGinPathToOpenAPI(c.FullPath())) 116 | if path != nil { 117 | if op := path.GetOperation(c.Request.Method); op != nil { 118 | var codes []string 119 | for code := range op.Responses.Map() { 120 | codes = append(codes, code) 121 | } 122 | if len(codes) > 0 { 123 | c.Header(availableResponsesHeaderName, strings.Join(codes, ",")) 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | func (m *MockServer) registerHandler(op *openapi3.Operation, options RunOptions) gin.HandlerFunc { 131 | return func(c *gin.Context) { 132 | // If Delay is set in options, apply it to simulate latency 133 | if options.Delay > 0 { 134 | time.Sleep(time.Duration(options.Delay) * time.Millisecond) 135 | } 136 | 137 | // Get desired response code from query param or header 138 | desiredCode := c.Query(responseCodeQueryParamName) 139 | if desiredCode == "" { 140 | desiredCode = c.GetHeader(responseCodeHeaderName) 141 | } 142 | 143 | // If no code specified, use default 144 | if desiredCode == "" { 145 | desiredCode = options.DefaultResponseCode 146 | } 147 | 148 | // Get desired response type from query param 149 | desiredType := c.Query(responseTypeQueryParamName) 150 | if desiredType == "" { 151 | desiredType = options.DefaultResponseType 152 | } 153 | 154 | response := m.findSpecificResponse(op, desiredCode) 155 | if response == nil { 156 | body := gin.H{ 157 | "error": fmt.Sprintf("No response defined for status code %s", desiredCode), 158 | "availableCodes": m.getAvailableResponseCodes(op), 159 | } 160 | if desiredType == "xml" { 161 | c.XML(http.StatusBadRequest, body) 162 | return 163 | } else { 164 | c.JSON(http.StatusBadRequest, body) 165 | } 166 | return 167 | } 168 | 169 | status := parseStatusCode(desiredCode) 170 | acceptHeader := c.GetHeader("Accept") 171 | acceptedTypes := parseAcceptHeader(acceptHeader) 172 | 173 | var contentType string 174 | var schema *openapi3.Schema 175 | for mediaType, content := range response.Content { 176 | for _, acceptedType := range acceptedTypes { 177 | if desiredType != "" { 178 | if strings.HasSuffix(mediaType, desiredType) { 179 | contentType = mediaType 180 | schema = content.Schema.Value 181 | break 182 | } 183 | 184 | } else if strings.HasPrefix(mediaType, acceptedType) { 185 | contentType = mediaType 186 | schema = content.Schema.Value 187 | break 188 | } 189 | } 190 | 191 | if schema != nil { 192 | break 193 | } 194 | } 195 | 196 | if schema == nil { 197 | contentType = "application/json" 198 | if jsonContent, ok := response.Content["application/json"]; ok { 199 | schema = jsonContent.Schema.Value 200 | } 201 | } 202 | 203 | mockData := generateMockData(schema) 204 | 205 | switch { 206 | case strings.Contains(contentType, "application/xml"): 207 | c.XML(status, mapToXML(mockData, schema, "root")) 208 | default: 209 | c.JSON(status, mockData) 210 | } 211 | } 212 | } 213 | 214 | func (m *MockServer) findSpecificResponse(op *openapi3.Operation, code string) *openapi3.Response { 215 | if responseRef, ok := op.Responses.Map()[code]; ok { 216 | return responseRef.Value 217 | } 218 | return nil 219 | } 220 | 221 | func (m *MockServer) getAvailableResponseCodes(op *openapi3.Operation) []string { 222 | var codes []string 223 | for code := range op.Responses.Map() { 224 | codes = append(codes, code) 225 | } 226 | return codes 227 | } 228 | -------------------------------------------------------------------------------- /internal/mockserver/utils.go: -------------------------------------------------------------------------------- 1 | package mockserver 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/brianvoe/gofakeit/v7" 12 | "github.com/getkin/kin-openapi/openapi3" 13 | ) 14 | 15 | func generateMockData(schema *openapi3.Schema) interface{} { 16 | if schema == nil { 17 | return nil 18 | } 19 | 20 | switch strings.Join(schema.Type.Slice(), "") { 21 | case "object": 22 | obj := make(map[string]interface{}) 23 | for propName, propSchema := range schema.Properties { 24 | obj[propName] = generateMockData(propSchema.Value) 25 | } 26 | return obj 27 | case "array": 28 | arr := make([]interface{}, 0) 29 | count := gofakeit.Number(1, 5) 30 | for i := 0; i < count; i++ { 31 | arr = append(arr, generateMockData(schema.Items.Value)) 32 | } 33 | return arr 34 | case "string": 35 | switch schema.Format { 36 | case "date-time": 37 | return gofakeit.Date().Format(time.RFC3339) 38 | case "date": 39 | return gofakeit.Date().Format("2006-01-02") 40 | case "email": 41 | return gofakeit.Email() 42 | case "uuid": 43 | return gofakeit.UUID() 44 | default: 45 | if schema.Example != nil { 46 | return schema.Example 47 | } 48 | return gofakeit.Sentence(3) 49 | } 50 | case "number", "integer": 51 | if schema.Example != nil { 52 | return schema.Example 53 | } 54 | return gofakeit.Number(1, 1000) 55 | case "boolean": 56 | return gofakeit.Bool() 57 | default: 58 | return nil 59 | } 60 | } 61 | 62 | // mapToXML converts a map[string]interface{} to XML structure 63 | func mapToXML(data interface{}, schema *openapi3.Schema, name string) *XMLNode { 64 | if data == nil || schema == nil { 65 | return nil 66 | } 67 | 68 | // Get XML name from schema or use provided name 69 | xmlName := name 70 | if schema.XML != nil && schema.XML.Name != "" { 71 | xmlName = schema.XML.Name 72 | } 73 | 74 | node := &XMLNode{ 75 | XMLName: xml.Name{Local: xmlName}, 76 | } 77 | 78 | switch v := data.(type) { 79 | case map[string]interface{}: 80 | for key, value := range v { 81 | propSchema := schema.Properties[key].Value 82 | if propSchema == nil { 83 | continue 84 | } 85 | 86 | // Handle properties marked as XML attributes 87 | if propSchema.XML != nil && propSchema.XML.Attribute { 88 | node.Attrs = append( 89 | node.Attrs, xml.Attr{ 90 | Name: xml.Name{Local: key}, 91 | Value: fmt.Sprintf("%v", value), 92 | }, 93 | ) 94 | continue 95 | } 96 | 97 | // Get property name from schema or use key 98 | propName := key 99 | if propSchema.XML != nil && propSchema.XML.Name != "" { 100 | propName = propSchema.XML.Name 101 | } 102 | 103 | childNode := mapToXML(value, propSchema, propName) 104 | if childNode != nil { 105 | node.Children = append(node.Children, childNode) 106 | } 107 | } 108 | 109 | case []interface{}: 110 | // Handle array wrapping if specified in schema 111 | if schema.XML != nil && schema.XML.Wrapped { 112 | // For wrapped arrays, return the current node and append items as children 113 | for _, item := range v { 114 | childNode := mapToXML(item, schema.Items.Value, name) 115 | if childNode != nil { 116 | node.Children = append(node.Children, childNode) 117 | } 118 | } 119 | return node 120 | } else { 121 | // For unwrapped arrays, return an array of nodes 122 | nodes := make([]*XMLNode, 0) 123 | for _, item := range v { 124 | childNode := mapToXML(item, schema.Items.Value, name) 125 | if childNode != nil { 126 | nodes = append(nodes, childNode) 127 | } 128 | } 129 | // If this is the root node, wrap it 130 | if name != "" { 131 | node.Children = nodes 132 | return node 133 | } 134 | return &XMLNode{ 135 | XMLName: xml.Name{Local: "array"}, 136 | Children: nodes, 137 | } 138 | } 139 | 140 | default: 141 | // Handle primitive values 142 | node.Value = fmt.Sprintf("%v", v) 143 | } 144 | 145 | return node 146 | } 147 | 148 | func parseAcceptHeader(header string) []string { 149 | if header == "" { 150 | return []string{"application/json"} 151 | } 152 | 153 | types := strings.Split(header, ",") 154 | for i, t := range types { 155 | if idx := strings.Index(t, ";"); idx != -1 { 156 | types[i] = strings.TrimSpace(t[:idx]) 157 | } else { 158 | types[i] = strings.TrimSpace(t) 159 | } 160 | } 161 | return types 162 | } 163 | 164 | // convertPathToGinFormat Convert OpenAPI path params ({param}) to Gin format (:param) 165 | func convertPathToGinFormat(path string) string { 166 | path = strings.ReplaceAll(path, "{", ":") 167 | path = strings.ReplaceAll(path, "}", "") 168 | 169 | return path 170 | } 171 | 172 | // convertGinPathToOpenAPI Convert Gin path params (:param) back to OpenAPI format ({param}) 173 | func convertGinPathToOpenAPI(path string) string { 174 | parts := strings.Split(path, "/") 175 | for i, part := range parts { 176 | if strings.HasPrefix(part, ":") { 177 | parts[i] = "{" + strings.TrimPrefix(part, ":") + "}" 178 | } 179 | } 180 | return strings.Join(parts, "/") 181 | } 182 | 183 | // Helper function to parse the status code string to an integer 184 | func parseStatusCode(code string) int { 185 | status, err := strconv.Atoi(code) 186 | if err != nil { 187 | log.Printf("Invalid status code '%s', defaulting to 200", code) 188 | return 200 189 | } 190 | return status 191 | } 192 | -------------------------------------------------------------------------------- /internal/model/operation.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/getkin/kin-openapi/openapi3" 7 | ) 8 | 9 | type Operation struct { 10 | Method string 11 | Path string 12 | Parameters []*Parameter 13 | *openapi3.Operation 14 | } 15 | 16 | func NewOperation(method, path string, operation *openapi3.Operation) *Operation { 17 | return &Operation{ 18 | Method: method, 19 | Path: path, 20 | Parameters: createParameters(operation.Parameters), 21 | Operation: operation, 22 | } 23 | } 24 | 25 | func createParameters(parameters openapi3.Parameters) []*Parameter { 26 | params := make([]*Parameter, 0, len(parameters)) 27 | for _, p := range parameters { 28 | propertyType := "" 29 | if p.Value.Schema != nil { 30 | propertyType = strings.Join(p.Value.Schema.Value.Type.Slice(), ", ") 31 | } 32 | 33 | params = append( 34 | params, 35 | NewParameter(p.Value.In, p.Value.Name, propertyType, p.Value.Required, p.Value.Description), 36 | ) 37 | } 38 | return params 39 | } 40 | -------------------------------------------------------------------------------- /internal/model/parameter.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | const ( 4 | ParameterInPath = "path" 5 | ParameterInQuery = "query" 6 | ParameterInHeader = "header" 7 | ParameterInCookie = "cookie" 8 | ) 9 | 10 | type Parameter struct { 11 | In string 12 | Name string 13 | Type string 14 | Required bool 15 | Description string 16 | } 17 | 18 | func NewParameter(in string, name string, typ string, required bool, description string) *Parameter { 19 | return &Parameter{ 20 | In: in, 21 | Name: name, 22 | Type: typ, 23 | Required: required, 24 | Description: description, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/printer/common.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "fmt" 5 | "maps" 6 | "slices" 7 | "strings" 8 | 9 | "github.com/getkin/kin-openapi/openapi3" 10 | ) 11 | 12 | func getProperties(value *openapi3.Schema) []string { 13 | properties := make([]string, 0) 14 | 15 | if value.Properties != nil { 16 | sortedPropertiesName := slices.Sorted(maps.Keys(value.Properties)) 17 | for _, propertyName := range sortedPropertiesName { 18 | prop := value.Properties[propertyName] 19 | propertyName = enrichPropertyName(propertyName, prop) 20 | properties = append(properties, propertyName) 21 | } 22 | } 23 | 24 | return properties 25 | } 26 | 27 | func enrichPropertyName(propertyName string, prop *openapi3.SchemaRef) string { 28 | if prop == nil || prop.Value == nil { 29 | return propertyName 30 | } 31 | 32 | if prop.Value.Type != nil { 33 | if prop.Value.Format == "" { 34 | propertyName += fmt.Sprintf(" (%s)", strings.Join(prop.Value.Type.Slice(), ", ")) 35 | } else { 36 | propertyName += fmt.Sprintf(" (%s: %s)", strings.Join(prop.Value.Type.Slice(), ", "), prop.Value.Format) 37 | } 38 | 39 | if prop.Value.Properties != nil { 40 | properties := getProperties(prop.Value) 41 | propertyName += fmt.Sprintf(" {%s}", strings.Join(properties, ",")) 42 | } 43 | } 44 | 45 | return propertyName 46 | } 47 | -------------------------------------------------------------------------------- /internal/printer/definition.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "maps" 5 | "os" 6 | "slices" 7 | "strings" 8 | 9 | "github.com/olekukonko/tablewriter" 10 | ) 11 | 12 | func PrintDefinition(name string, definition map[string]interface{}) { 13 | table := tablewriter.NewWriter(os.Stdout) 14 | table.SetAutoWrapText(false) 15 | table.SetRowLine(true) 16 | table.SetHeader([]string{"Name", "Type", "Properties"}) 17 | t := definition["type"].(string) 18 | properties := slices.Sorted(maps.Keys(definition["properties"].(map[string]interface{}))) 19 | 20 | table.Append([]string{name, t, strings.Join(properties, ", ")}) 21 | 22 | table.Render() 23 | } 24 | -------------------------------------------------------------------------------- /internal/printer/parameters.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/idsulik/swama/v2/internal/model" 8 | "github.com/olekukonko/tablewriter" 9 | ) 10 | 11 | func PrintParameters(parameters []*model.Parameter) { 12 | table := tablewriter.NewWriter(os.Stdout) 13 | table.SetAutoWrapText(false) 14 | table.SetRowLine(true) 15 | table.SetHeader([]string{"In", "Parameter", "Type", "Required", "Description"}) 16 | for _, p := range parameters { 17 | description := "-" 18 | if p.Description != "" { 19 | description = p.Description 20 | } 21 | 22 | table.Append( 23 | []string{ 24 | p.In, 25 | p.Name, 26 | p.Type, 27 | fmt.Sprintf("%v", p.Required), 28 | description, 29 | }, 30 | ) 31 | } 32 | table.Render() 33 | } 34 | -------------------------------------------------------------------------------- /internal/printer/request_body.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/getkin/kin-openapi/openapi3" 8 | "github.com/olekukonko/tablewriter" 9 | ) 10 | 11 | func PrintRequestBody(requestBody *openapi3.RequestBody) { 12 | table := tablewriter.NewWriter(os.Stdout) 13 | table.SetAutoWrapText(false) 14 | table.SetHeader([]string{"Type", "Properties"}) 15 | propertiesToContentTypes := make(map[string][]string) 16 | for contentType, content := range requestBody.Content { 17 | properties := strings.Join(getProperties(content.Schema.Value), "\n") 18 | propertiesToContentTypes[properties] = append(propertiesToContentTypes[properties], contentType) 19 | } 20 | 21 | for properties, contentTypes := range propertiesToContentTypes { 22 | table.Append([]string{strings.Join(contentTypes, "\n"), properties}) 23 | } 24 | table.Render() 25 | } 26 | -------------------------------------------------------------------------------- /internal/printer/responses.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "maps" 5 | "os" 6 | "slices" 7 | "strings" 8 | 9 | "github.com/getkin/kin-openapi/openapi3" 10 | "github.com/olekukonko/tablewriter" 11 | ) 12 | 13 | func PrintResponses(responses *openapi3.Responses) { 14 | table := tablewriter.NewWriter(os.Stdout) 15 | table.SetAutoWrapText(false) 16 | table.SetAutoMergeCells(true) 17 | table.SetRowLine(true) 18 | table.SetHeader([]string{"Name", "Content Types", "Properties", "Description"}) 19 | sortedCodes := slices.Sorted(maps.Keys(responses.Map())) 20 | for _, code := range sortedCodes { 21 | response := responses.Value(code) 22 | description := "-" 23 | if response.Value.Description != nil { 24 | description = *response.Value.Description 25 | } 26 | 27 | if response.Value.Content != nil { 28 | propertiesToContentTypes := make(map[string][]string) 29 | for contentType := range response.Value.Content { 30 | properties := strings.Join(getProperties(response.Value.Content[contentType].Schema.Value), "\n") 31 | propertiesToContentTypes[properties] = append(propertiesToContentTypes[properties], contentType) 32 | } 33 | 34 | for properties, contentTypes := range propertiesToContentTypes { 35 | table.Append([]string{code, strings.Join(contentTypes, "\n"), properties, description}) 36 | } 37 | } else { 38 | table.Append([]string{code, "-", "-", description}) 39 | } 40 | 41 | } 42 | 43 | table.Render() 44 | } 45 | -------------------------------------------------------------------------------- /internal/printer/schema.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/getkin/kin-openapi/openapi3" 8 | "github.com/olekukonko/tablewriter" 9 | ) 10 | 11 | func PrintSchema(name string, schema *openapi3.Schema) { 12 | table := tablewriter.NewWriter(os.Stdout) 13 | table.SetAutoWrapText(false) 14 | table.SetRowLine(true) 15 | table.SetHeader([]string{"Name", "Type", "Properties", "Description"}) 16 | 17 | types := "-" 18 | description := "-" 19 | if schema.Description != "" { 20 | description = schema.Description 21 | } 22 | if schema.Type != nil { 23 | types = strings.Join(schema.Type.Slice(), ", ") 24 | } 25 | 26 | table.Append( 27 | []string{ 28 | name, 29 | types, 30 | strings.Join(getProperties(schema), "\n"), 31 | description, 32 | }, 33 | ) 34 | 35 | table.Render() 36 | } 37 | -------------------------------------------------------------------------------- /internal/swagger/components.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "fmt" 5 | "maps" 6 | "os" 7 | "slices" 8 | "strings" 9 | 10 | "github.com/getkin/kin-openapi/openapi3" 11 | "github.com/idsulik/swama/v2/internal/printer" 12 | "github.com/olekukonko/tablewriter" 13 | ) 14 | 15 | type Components interface { 16 | ListComponents() error 17 | ViewComponent(name string) error 18 | } 19 | 20 | type components struct { 21 | doc *openapi3.T 22 | } 23 | 24 | func NewComponents(doc *openapi3.T) Components { 25 | return &components{ 26 | doc: doc, 27 | } 28 | } 29 | 30 | // ListComponents lists all available API components in the Swagger/OpenAPI file. 31 | func (e *components) ListComponents() error { 32 | var sortedNames []string 33 | if e.doc.Components == nil { 34 | sortedNames = slices.Sorted(maps.Keys(e.doc.Extensions["definitions"].(map[string]interface{}))) 35 | } else { 36 | sortedNames = slices.Sorted(maps.Keys(e.doc.Components.Schemas)) 37 | } 38 | table := tablewriter.NewWriter(os.Stdout) 39 | table.SetAutoWrapText(false) 40 | table.SetRowLine(true) 41 | table.SetHeader([]string{"Name", "Type", "Description"}) 42 | for _, name := range sortedNames { 43 | types := "-" 44 | description := "-" 45 | if e.doc.Components == nil { 46 | definitions := e.doc.Extensions["definitions"].(map[string]interface{}) 47 | definition := definitions[name].(map[string]interface{}) 48 | if definition["type"] != nil { 49 | types = definition["type"].(string) 50 | } 51 | if definition["description"] != nil { 52 | description = definition["description"].(string) 53 | } 54 | } else { 55 | schema := e.doc.Components.Schemas[name] 56 | if schema.Value.Description != "" { 57 | description = schema.Value.Description 58 | } 59 | if schema.Value.Type != nil { 60 | types = strings.Join(schema.Value.Type.Slice(), ", ") 61 | } 62 | } 63 | 64 | table.Append([]string{name, types, description}) 65 | } 66 | table.Render() 67 | return nil 68 | } 69 | 70 | // ViewComponent shows details about a specific API component. 71 | func (e *components) ViewComponent(name string) error { 72 | if e.doc.Components != nil { 73 | for n, schema := range e.doc.Components.Schemas { 74 | if strings.ToLower(n) == strings.ToLower(name) { 75 | printer.PrintSchema(n, schema.Value) 76 | return nil 77 | } 78 | } 79 | } 80 | definitions, found := e.doc.Extensions["definitions"] 81 | if found { 82 | for n, definition := range definitions.(map[string]interface{}) { 83 | if strings.ToLower(n) == strings.ToLower(name) { 84 | printer.PrintDefinition(n, definition.(map[string]interface{})) 85 | return nil 86 | } 87 | } 88 | } 89 | fmt.Printf("Component %s not found\n", name) 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /internal/swagger/endpoints.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "fmt" 5 | "maps" 6 | "os" 7 | "regexp" 8 | "slices" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/getkin/kin-openapi/openapi3" 13 | converter2 "github.com/idsulik/swama/v2/internal/converter" 14 | "github.com/idsulik/swama/v2/internal/model" 15 | "github.com/idsulik/swama/v2/internal/printer" 16 | "github.com/idsulik/swama/v2/internal/util" 17 | "github.com/olekukonko/tablewriter" 18 | ) 19 | 20 | const ( 21 | GroupByTag = "tag" 22 | GroupByMethod = "method" 23 | GroupByNone = "none" 24 | ) 25 | 26 | type ListOptions struct { 27 | Method string 28 | Endpoint string 29 | Tag string 30 | Group string 31 | } 32 | 33 | type ViewOptions struct { 34 | Method string 35 | Endpoint string 36 | } 37 | 38 | type ConvertOptions struct { 39 | Method string 40 | Endpoint string 41 | ToType string 42 | } 43 | 44 | type Endpoints interface { 45 | ListEndpoints(options ListOptions) error 46 | ViewEndpoint(options ViewOptions) error 47 | ConvertEndpoint(options ConvertOptions) error 48 | } 49 | 50 | type endpoints struct { 51 | doc *openapi3.T 52 | } 53 | 54 | func NewEndpoints(doc *openapi3.T) Endpoints { 55 | return &endpoints{ 56 | doc: doc, 57 | } 58 | } 59 | 60 | // ListEndpoints lists all available API endpoints in the Swagger/OpenAPI file. 61 | func (e *endpoints) ListEndpoints(options ListOptions) error { 62 | type groupItem struct { 63 | method string 64 | path string 65 | summary string 66 | tags string 67 | } 68 | groupedEndpoints := make(map[string][]groupItem) 69 | for _, path := range e.doc.Paths.InMatchingOrder() { 70 | for m, operation := range e.doc.Paths.Find(path).Operations() { 71 | if options.Endpoint != "" { 72 | if matched, _ := regexp.MatchString(fmt.Sprintf("^%s$", options.Endpoint), path); !matched { 73 | continue 74 | } 75 | } 76 | 77 | if options.Method != "" && m != options.Method { 78 | continue 79 | } 80 | 81 | if options.Tag != "" && !slices.Contains(operation.Tags, options.Tag) { 82 | continue 83 | } 84 | 85 | tags := "" 86 | if len(operation.Tags) > 0 { 87 | tags = fmt.Sprintf("%v", operation.Tags) 88 | } 89 | 90 | if options.Group != "" { 91 | keys := make([]string, 0) 92 | if options.Group == GroupByTag { 93 | for _, tagName := range operation.Tags { 94 | tag := e.doc.Tags.Get(tagName) 95 | key := tagName 96 | if tag != nil { 97 | key += fmt.Sprintf(" (%s)", tag.Description) 98 | } 99 | keys = append(keys, key) 100 | } 101 | } else if options.Group == GroupByMethod { 102 | keys = append(keys, m) 103 | } else { 104 | keys = append(keys, "none") 105 | } 106 | 107 | for _, key := range keys { 108 | if _, ok := groupedEndpoints[key]; !ok { 109 | groupedEndpoints[key] = make([]groupItem, 0) 110 | } 111 | groupedEndpoints[key] = append( 112 | groupedEndpoints[key], 113 | groupItem{ 114 | method: m, 115 | path: path, 116 | summary: operation.Summary, 117 | tags: tags, 118 | }, 119 | ) 120 | } 121 | continue 122 | } 123 | } 124 | } 125 | 126 | // Sort and print the grouped endpoints 127 | sortedKeys := slices.Sorted(maps.Keys(groupedEndpoints)) 128 | var table *tablewriter.Table 129 | fmt.Println() 130 | for _, key := range sortedKeys { 131 | if key != GroupByNone { 132 | fmt.Printf("%s\n", key) 133 | } 134 | 135 | table = tablewriter.NewWriter(os.Stdout) 136 | table.SetAutoWrapText(false) 137 | table.SetRowLine(true) 138 | table.SetHeader([]string{"Method", "Path", "Summary", "Tags"}) 139 | values := groupedEndpoints[key] 140 | sort.Slice( 141 | values, func(i, j int) bool { 142 | if values[i].method != values[j].method { 143 | return values[i].method > values[j].method 144 | } 145 | return values[i].path < values[j].path 146 | }, 147 | ) 148 | for _, value := range values { 149 | table.Rich( 150 | []string{value.method, value.path, value.summary, value.tags}, []tablewriter.Colors{ 151 | {util.GetMethodColor(value.method)}, 152 | }, 153 | ) 154 | } 155 | 156 | table.Render() 157 | 158 | if key != GroupByNone { 159 | fmt.Println() 160 | } 161 | } 162 | 163 | return nil 164 | } 165 | 166 | // ViewEndpoint shows details about a specific API endpoint. 167 | func (e *endpoints) ViewEndpoint(options ViewOptions) error { 168 | operation, err := e.findOperation(options.Method, options.Endpoint) 169 | 170 | if err != nil { 171 | return err 172 | } 173 | 174 | table := tablewriter.NewWriter(os.Stdout) 175 | table.SetAutoWrapText(false) 176 | table.SetHeader([]string{"Method", "Path", "Summary", "Tags"}) 177 | table.Rich( 178 | []string{operation.Method, operation.Path, operation.Summary, fmt.Sprintf("%v", operation.Tags)}, 179 | []tablewriter.Colors{{util.GetMethodColor(operation.Method)}}, 180 | ) 181 | 182 | table.Render() 183 | 184 | if len(operation.Parameters) > 0 { 185 | fmt.Println("Parameters:") 186 | printer.PrintParameters(operation.Parameters) 187 | } 188 | 189 | if operation.RequestBody != nil { 190 | fmt.Println("Request Body:") 191 | printer.PrintRequestBody(operation.RequestBody.Value) 192 | } 193 | 194 | if operation.Responses != nil { 195 | fmt.Println("Responses:") 196 | printer.PrintResponses(operation.Responses) 197 | } 198 | 199 | return nil 200 | } 201 | 202 | // ConvertEndpoint converts an endpoint to curl or fetch. 203 | func (e *endpoints) ConvertEndpoint(options ConvertOptions) error { 204 | operation, err := e.findOperation(options.Method, options.Endpoint) 205 | 206 | if err != nil { 207 | return err 208 | } 209 | 210 | converter, err := converter2.NewConverter(options.ToType) 211 | 212 | if err != nil { 213 | return err 214 | } 215 | 216 | converted := converter.ConvertEndpoint(options.Method, options.Endpoint, operation) 217 | fmt.Println(converted) 218 | 219 | return nil 220 | } 221 | 222 | func (e *endpoints) findOperation(method, endpoint string) (*model.Operation, error) { 223 | path := e.doc.Paths.Find(endpoint) 224 | 225 | if path != nil { 226 | for m, operation := range path.Operations() { 227 | if method != "" && strings.ToLower(m) == strings.ToLower(method) { 228 | return model.NewOperation(m, endpoint, operation), nil 229 | } 230 | } 231 | } 232 | 233 | return nil, fmt.Errorf("endpoint %s not found", endpoint) 234 | } 235 | -------------------------------------------------------------------------------- /internal/swagger/loader.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/url" 7 | "os" 8 | 9 | "github.com/getkin/kin-openapi/openapi3" 10 | ) 11 | 12 | var defaultFiles = []string{ 13 | "swagger.yaml", 14 | "swagger.yml", 15 | "swagger.json", 16 | "openapi.yaml", 17 | "openapi.yml", 18 | "openapi.json", 19 | } 20 | 21 | // LoadSwaggerFile loads the Swagger/OpenAPI file into a parsed document. 22 | func LoadSwaggerFile(ctx context.Context, filepath string) (*openapi3.T, error) { 23 | swaggerLoader := &openapi3.Loader{ 24 | Context: ctx, 25 | IsExternalRefsAllowed: true, 26 | } 27 | 28 | url, err := url.Parse(filepath) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | doc, err := swaggerLoader.LoadFromURI(url) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return doc, nil 39 | } 40 | 41 | // LocateSwaggerFile tries to find the Swagger file in the current directory. 42 | func LocateSwaggerFile() string { 43 | for _, file := range defaultFiles { 44 | if _, err := os.Stat(file); err == nil { 45 | log.Printf("Using Swagger file: %s\n", file) 46 | return file 47 | } 48 | } 49 | 50 | log.Println("Swagger file not found in the current directory.") 51 | return "" 52 | } 53 | -------------------------------------------------------------------------------- /internal/swagger/servers.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/getkin/kin-openapi/openapi3" 7 | "github.com/olekukonko/tablewriter" 8 | ) 9 | 10 | type Servers interface { 11 | ListServers() error 12 | } 13 | 14 | type servers struct { 15 | doc *openapi3.T 16 | } 17 | 18 | func NewServers(doc *openapi3.T) Servers { 19 | return &servers{ 20 | doc: doc, 21 | } 22 | } 23 | 24 | // ListServers lists all available servers in the Swagger/OpenAPI file. 25 | func (e *servers) ListServers() error { 26 | table := tablewriter.NewWriter(os.Stdout) 27 | table.SetAutoWrapText(false) 28 | table.SetHeader([]string{"URL", "Description"}) 29 | for _, server := range e.doc.Servers { 30 | table.Append([]string{server.URL, server.Description}) 31 | } 32 | 33 | table.Render() 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/swagger/tags.go: -------------------------------------------------------------------------------- 1 | package swagger 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/getkin/kin-openapi/openapi3" 9 | "github.com/olekukonko/tablewriter" 10 | ) 11 | 12 | type Tags interface { 13 | ListTags() error 14 | ViewTag(name string) error 15 | } 16 | 17 | type tags struct { 18 | doc *openapi3.T 19 | } 20 | 21 | func NewTags(doc *openapi3.T) Tags { 22 | return &tags{ 23 | doc: doc, 24 | } 25 | } 26 | 27 | // ListTags lists all available tags in the Swagger/OpenAPI file. 28 | func (t *tags) ListTags() error { 29 | table := tablewriter.NewWriter(os.Stdout) 30 | table.SetAutoWrapText(false) 31 | table.SetHeader([]string{"Name", "Description", "External Docs"}) 32 | 33 | for _, tag := range t.doc.Tags { 34 | t.printTagDetails(table, tag) 35 | } 36 | 37 | table.Render() 38 | 39 | return nil 40 | } 41 | 42 | // ViewTag shows details about a specific API tag. 43 | func (t *tags) ViewTag(name string) error { 44 | for _, tag := range t.doc.Tags { 45 | if strings.ToLower(name) == strings.ToLower(tag.Name) { 46 | table := tablewriter.NewWriter(os.Stdout) 47 | table.SetAutoWrapText(false) 48 | table.SetHeader([]string{"Name", "Description", "External Docs"}) 49 | 50 | t.printTagDetails(table, tag) 51 | table.Render() 52 | return nil 53 | } 54 | } 55 | 56 | return fmt.Errorf("tag not found") 57 | } 58 | 59 | func (t *tags) printTagDetails(table *tablewriter.Table, tag *openapi3.Tag) { 60 | externalDocs := "-" 61 | if tag.ExternalDocs != nil { 62 | externalDocs = fmt.Sprintf("%s (%s)", tag.ExternalDocs.Description, tag.ExternalDocs.URL) 63 | } 64 | table.Append([]string{tag.Name, tag.Description, externalDocs}) 65 | } 66 | -------------------------------------------------------------------------------- /internal/util/color.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/olekukonko/tablewriter" 7 | ) 8 | 9 | func GetMethodColor(method string) int { 10 | method = strings.ToUpper(method) 11 | methodColor := tablewriter.FgHiWhiteColor 12 | if method == "GET" { 13 | methodColor = tablewriter.FgHiBlueColor 14 | } else if method == "POST" { 15 | methodColor = tablewriter.FgHiGreenColor 16 | } else if method == "PUT" { 17 | methodColor = tablewriter.FgHiYellowColor 18 | } else if method == "DELETE" { 19 | methodColor = tablewriter.FgHiRedColor 20 | } 21 | 22 | return methodColor 23 | } 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/idsulik/swama/v2/cmd" 11 | ) 12 | 13 | func main() { 14 | ctx, cancel := context.WithCancel(context.Background()) 15 | 16 | // Handle graceful shutdown with signal handling 17 | quit := make(chan os.Signal, 1) 18 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 19 | go func() { 20 | <-quit 21 | cancel() 22 | }() 23 | 24 | err := cmd.Execute(ctx) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | } 29 | --------------------------------------------------------------------------------