feat: init swagger project
This commit is contained in:
commit
696d789686
|
@ -0,0 +1,33 @@
|
|||
# Binaries
|
||||
bin/
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary
|
||||
*.test
|
||||
|
||||
# Output coverage files
|
||||
*.out
|
||||
coverage.txt
|
||||
coverage.html
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Output files
|
||||
example/test_output/
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2025-09-30
|
||||
|
||||
### Added
|
||||
- Initial release as standalone tool
|
||||
- Extracted from go-zero project and made independent
|
||||
- Support for converting go-zero `.api` files to OpenAPI 2.0 (Swagger) specification
|
||||
- JSON and YAML output formats
|
||||
- Command-line interface using cobra
|
||||
- Support for all go-zero API features:
|
||||
- Info properties (title, description, version, host, basePath, etc.)
|
||||
- Type definitions (structs, arrays, maps, pointers)
|
||||
- Tag-based parameter handling (json, form, path, header)
|
||||
- Validation options (range, enum, default, example, optional)
|
||||
- Security definitions
|
||||
- Code-msg wrapper for responses
|
||||
- Definition references
|
||||
- Internal utility functions to replace go-zero dependencies
|
||||
- Comprehensive documentation
|
||||
- Example API files
|
||||
- Makefile for easy building
|
||||
- MIT License
|
||||
|
||||
### Changed
|
||||
- Module name from `github.com/zeromicro/go-zero` to `go-doc`
|
||||
- Removed dependency on `go-zero/tools/goctl/internal/version`
|
||||
- Removed dependency on `go-zero/tools/goctl/util/*`
|
||||
- Removed dependency on `google.golang.org/grpc/metadata`
|
||||
- Project structure reorganized to follow Go best practices
|
||||
- `cmd/go-doc/` for main entry point
|
||||
- `internal/swagger/` for core logic
|
||||
- `internal/util/` for utilities
|
||||
|
||||
### Removed
|
||||
- Dependency on go-zero runtime components
|
||||
- Go-zero specific version information
|
||||
|
||||
---
|
||||
|
||||
## Attribution
|
||||
|
||||
This project was originally part of the [go-zero](https://github.com/zeromicro/go-zero)
|
||||
project's swagger generation plugin. We are grateful to the go-zero team for their
|
||||
excellent work.
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Daniel Chan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
This project contains code originally from the go-zero project (https://github.com/zeromicro/go-zero),
|
||||
which is also licensed under the MIT License.
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
# 🔄 Migration Guide: From go-zero Plugin to Standalone Tool
|
||||
|
||||
This document explains how `go-doc` was extracted from go-zero and made independent.
|
||||
|
||||
## 📊 What Changed?
|
||||
|
||||
### 1. **Module Independence**
|
||||
|
||||
**Before (go-zero plugin):**
|
||||
```go
|
||||
// Part of go-zero's internal tools
|
||||
package swagger // in tools/goctl/api/plugin/swagger/
|
||||
```
|
||||
|
||||
**After (standalone):**
|
||||
```go
|
||||
module go-doc
|
||||
|
||||
package swagger // in internal/swagger/
|
||||
```
|
||||
|
||||
### 2. **Dependency Reduction**
|
||||
|
||||
#### Removed Internal Dependencies
|
||||
|
||||
| Dependency | Replaced With | Reason |
|
||||
|:-----------|:--------------|:-------|
|
||||
| `go-zero/tools/goctl/internal/version` | Custom version in `main.go` | Internal package not accessible |
|
||||
| `go-zero/tools/goctl/util` | `go-doc/internal/util` | Self-contained utilities |
|
||||
| `go-zero/tools/goctl/util/stringx` | `go-doc/internal/util` | Custom string manipulation |
|
||||
| `go-zero/tools/goctl/util/pathx` | `go-doc/internal/util` | Custom path utilities |
|
||||
| `google.golang.org/grpc/metadata` | Direct map access | Simplified KV retrieval |
|
||||
|
||||
#### Kept Essential Dependencies
|
||||
|
||||
```go
|
||||
require (
|
||||
github.com/go-openapi/spec v0.21.0 // Swagger spec
|
||||
github.com/spf13/cobra v1.8.1 // CLI framework
|
||||
github.com/zeromicro/go-zero/tools/goctl v1.9.0 // API parser only
|
||||
gopkg.in/yaml.v2 v2.4.0 // YAML output
|
||||
)
|
||||
```
|
||||
|
||||
### 3. **Project Structure**
|
||||
|
||||
**Before:**
|
||||
```
|
||||
go-zero/
|
||||
└── tools/
|
||||
└── goctl/
|
||||
└── api/
|
||||
└── plugin/
|
||||
└── swagger/
|
||||
├── swagger.go
|
||||
├── parameter.go
|
||||
└── ...
|
||||
```
|
||||
|
||||
**After:**
|
||||
```
|
||||
go-doc/
|
||||
├── cmd/
|
||||
│ └── go-doc/
|
||||
│ └── main.go # Standalone entry point
|
||||
├── internal/
|
||||
│ ├── swagger/ # Core logic (from go-zero)
|
||||
│ │ ├── swagger.go
|
||||
│ │ ├── parameter.go
|
||||
│ │ └── ...
|
||||
│ └── util/ # Self-contained utilities
|
||||
│ ├── util.go
|
||||
│ ├── stringx.go
|
||||
│ └── pathx.go
|
||||
└── example/
|
||||
```
|
||||
|
||||
### 4. **Import Path Changes**
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
import (
|
||||
"github.com/zeromicro/go-zero/tools/goctl/util"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/util/stringx"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/internal/version"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
import (
|
||||
"go-doc/internal/util"
|
||||
)
|
||||
```
|
||||
|
||||
### 5. **Metadata Handling**
|
||||
|
||||
**Before (using gRPC metadata):**
|
||||
```go
|
||||
func getStringFromKV(properties map[string]string, key string) string {
|
||||
md := metadata.New(properties)
|
||||
val := md.Get(key)
|
||||
if len(val) == 0 {
|
||||
return ""
|
||||
}
|
||||
return val[0]
|
||||
}
|
||||
```
|
||||
|
||||
**After (direct map access):**
|
||||
```go
|
||||
func getStringFromKVOrDefault(properties map[string]string, key string, def string) string {
|
||||
if len(properties) == 0 {
|
||||
return def
|
||||
}
|
||||
val, ok := properties[key]
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
str, err := strconv.Unquote(val)
|
||||
if err != nil || len(str) == 0 {
|
||||
return def
|
||||
}
|
||||
return str
|
||||
}
|
||||
```
|
||||
|
||||
### 6. **Version Information**
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
ext.Add("x-goctl-version", version.BuildVersion)
|
||||
ext.Add("x-github", "https://github.com/zeromicro/go-zero")
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
ext.Add("x-generator", "go-doc")
|
||||
ext.Add("x-github", "https://github.com/danielchan-25/go-doc")
|
||||
```
|
||||
|
||||
## 🔧 Implementation Details
|
||||
|
||||
### Custom Utilities Created
|
||||
|
||||
#### 1. **util.TrimWhiteSpace**
|
||||
```go
|
||||
// Replaces: go-zero/tools/goctl/util.TrimWhiteSpace
|
||||
func TrimWhiteSpace(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, s)
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. **util.FieldsAndTrimSpace**
|
||||
```go
|
||||
// Replaces: go-zero/tools/goctl/util.FieldsAndTrimSpace
|
||||
func FieldsAndTrimSpace(s string, fn func(rune) bool) []string {
|
||||
fields := strings.FieldsFunc(s, fn)
|
||||
result := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
trimmed := strings.TrimSpace(field)
|
||||
if len(trimmed) > 0 {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. **util.String (for ToCamel/Untitle)**
|
||||
```go
|
||||
// Replaces: go-zero/tools/goctl/util/stringx
|
||||
type String struct {
|
||||
source string
|
||||
}
|
||||
|
||||
func From(s string) String {
|
||||
return String{source: s}
|
||||
}
|
||||
|
||||
func (s String) ToCamel() string { /* implementation */ }
|
||||
func (s String) Untitle() string { /* implementation */ }
|
||||
```
|
||||
|
||||
#### 4. **util.MkdirIfNotExist**
|
||||
```go
|
||||
// Replaces: go-zero/tools/goctl/util/pathx.MkdirIfNotExist
|
||||
func MkdirIfNotExist(dir string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
return os.MkdirAll(dir, 0755)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## ✅ Compatibility
|
||||
|
||||
### What's Still Compatible?
|
||||
|
||||
✅ **API File Format** - 100% compatible with go-zero `.api` files
|
||||
✅ **Swagger Output** - Generates identical OpenAPI 2.0 specifications
|
||||
✅ **All Features** - All swagger generation features preserved
|
||||
✅ **Tag Syntax** - Same tag options (range, enum, default, example, etc.)
|
||||
|
||||
### What's Different?
|
||||
|
||||
⚠️ **Generated Metadata**
|
||||
- `x-generator: "go-doc"` instead of `x-goctl-version`
|
||||
- `x-github` points to go-doc repository
|
||||
- Build date format remains the same
|
||||
|
||||
⚠️ **Module Name**
|
||||
- Import as `go-doc` not part of go-zero
|
||||
|
||||
⚠️ **Binary Name**
|
||||
- `go-doc` instead of `goctl api plugin -plugin swagger`
|
||||
|
||||
## 🚀 Migration Steps for Users
|
||||
|
||||
If you were using go-zero's swagger plugin:
|
||||
|
||||
### Old Way (go-zero plugin):
|
||||
```bash
|
||||
goctl api plugin -plugin swagger -api example.api -dir output
|
||||
```
|
||||
|
||||
### New Way (standalone go-doc):
|
||||
```bash
|
||||
go-doc -a example.api -d output
|
||||
```
|
||||
|
||||
## 📝 Benefits of Independence
|
||||
|
||||
1. ✅ **Smaller Binary** - Only swagger generation, no full goctl
|
||||
2. ✅ **Faster Installation** - Fewer dependencies
|
||||
3. ✅ **Clearer Purpose** - Single responsibility
|
||||
4. ✅ **Easier Maintenance** - Self-contained codebase
|
||||
5. ✅ **Flexible Updates** - Independent release cycle
|
||||
|
||||
## 🙏 Attribution
|
||||
|
||||
This tool was extracted from the excellent [go-zero](https://github.com/zeromicro/go-zero)
|
||||
project. We are grateful to the go-zero team for their foundational work.
|
||||
|
||||
---
|
||||
|
||||
**Original Source:** https://github.com/zeromicro/go-zero/tree/master/tools/goctl/api/plugin/swagger
|
||||
**License:** MIT (both original and this project)
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
.PHONY: build clean test install run fmt lint help
|
||||
|
||||
# Variables
|
||||
BINARY_NAME=go-doc
|
||||
BUILD_DIR=bin
|
||||
MAIN_PATH=./cmd/go-doc
|
||||
VERSION?=1.0.0
|
||||
COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "dev")
|
||||
BUILD_DATE=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(BUILD_DATE)"
|
||||
|
||||
help: ## Display this help screen
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
build: ## Build the binary
|
||||
@echo "Building $(BINARY_NAME)..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PATH)
|
||||
@echo "✅ Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||
|
||||
clean: ## Remove build artifacts
|
||||
@echo "Cleaning..."
|
||||
@rm -rf $(BUILD_DIR)
|
||||
@rm -rf example/test_output
|
||||
@echo "✅ Clean complete"
|
||||
|
||||
test: ## Run tests
|
||||
@echo "Running tests..."
|
||||
@go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
@echo "✅ Tests complete"
|
||||
|
||||
install: ## Install the binary to $GOPATH/bin
|
||||
@echo "Installing $(BINARY_NAME)..."
|
||||
@go install $(LDFLAGS) $(MAIN_PATH)
|
||||
@echo "✅ Install complete"
|
||||
|
||||
run: build ## Build and run with example API file
|
||||
@echo "Running $(BINARY_NAME)..."
|
||||
@$(BUILD_DIR)/$(BINARY_NAME) -a example/example.api -d example/test_output
|
||||
@echo "✅ Generated: example/test_output/example.json"
|
||||
|
||||
fmt: ## Format code
|
||||
@echo "Formatting code..."
|
||||
@gofmt -s -w .
|
||||
@goimports -w .
|
||||
@echo "✅ Format complete"
|
||||
|
||||
lint: ## Run linters
|
||||
@echo "Running linters..."
|
||||
@golangci-lint run || (echo "golangci-lint not found, skipping..." && exit 0)
|
||||
@echo "✅ Lint complete"
|
||||
|
||||
deps: ## Download dependencies
|
||||
@echo "Downloading dependencies..."
|
||||
@go mod download
|
||||
@go mod tidy
|
||||
@echo "✅ Dependencies updated"
|
||||
|
||||
# Build for multiple platforms
|
||||
build-all: ## Build for all platforms
|
||||
@echo "Building for all platforms..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PATH)
|
||||
GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 $(MAIN_PATH)
|
||||
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PATH)
|
||||
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PATH)
|
||||
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PATH)
|
||||
@echo "✅ Multi-platform build complete"
|
||||
|
||||
example: build ## Generate example swagger files
|
||||
@echo "Generating examples..."
|
||||
@mkdir -p example/test_output
|
||||
@$(BUILD_DIR)/$(BINARY_NAME) -a example/example.api -d example/test_output -f example
|
||||
@$(BUILD_DIR)/$(BINARY_NAME) -a example/example.api -d example/test_output -f example -y
|
||||
@echo "✅ Examples generated in example/test_output/"
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
# go-doc
|
||||
|
||||
[](https://go.dev/)
|
||||
[](LICENSE)
|
||||
|
||||
**go-doc** is a standalone tool that converts [go-zero](https://github.com/zeromicro/go-zero) `.api` files into OpenAPI 2.0 (Swagger) specification.
|
||||
|
||||
Originally part of the go-zero project, go-doc is now an independent, easy-to-use command-line tool for generating API documentation.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🚀 **Standalone Binary** - No dependencies on go-zero runtime
|
||||
- 📝 **Full Swagger 2.0 Support** - Complete OpenAPI specification generation
|
||||
- 🎯 **Rich Type Support** - Handles structs, arrays, maps, pointers, and nested types
|
||||
- 🏷️ **Tag-based Configuration** - Support for `json`, `form`, `path`, `header` tags
|
||||
- 📊 **Advanced Validations** - Range, enum, default, example values
|
||||
- 🔐 **Security Definitions** - Custom authentication configurations
|
||||
- 📦 **Multiple Output Formats** - JSON or YAML output
|
||||
- 🎨 **Definition References** - Optional use of Swagger definitions for cleaner output
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/danielchan-25/go-doc.git
|
||||
cd go-doc
|
||||
go build -o bin/go-doc ./cmd/go-doc
|
||||
```
|
||||
|
||||
### Using Go Install
|
||||
|
||||
```bash
|
||||
go install github.com/danielchan-25/go-doc/cmd/go-doc@latest
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Generate JSON swagger file
|
||||
go-doc -a example/example.api -d output
|
||||
|
||||
# Generate YAML swagger file
|
||||
go-doc -a example/example.api -d output -y
|
||||
|
||||
# Specify custom filename
|
||||
go-doc -a example/example.api -d output -f my-api
|
||||
```
|
||||
|
||||
### Command Line Options
|
||||
|
||||
```
|
||||
Flags:
|
||||
-a, --api string API file path (required)
|
||||
-d, --dir string Output directory (required)
|
||||
-f, --filename string Output filename without extension (optional, defaults to API filename)
|
||||
-h, --help help for go-doc
|
||||
-v, --version version for go-doc
|
||||
-y, --yaml Generate YAML format instead of JSON
|
||||
```
|
||||
|
||||
## 📖 API File Format
|
||||
|
||||
### Basic Structure
|
||||
|
||||
```go
|
||||
syntax = "v1"
|
||||
|
||||
info (
|
||||
title: "My API"
|
||||
description: "API documentation"
|
||||
version: "v1.0.0"
|
||||
host: "api.example.com"
|
||||
basePath: "/v1"
|
||||
)
|
||||
|
||||
type (
|
||||
UserRequest {
|
||||
Id int `json:"id,range=[1:10000]"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
UserResponse {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "user"
|
||||
)
|
||||
service MyAPI {
|
||||
@handler getUser
|
||||
get /user/:id (UserRequest) returns (UserResponse)
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Info Properties
|
||||
|
||||
- `title` - API title
|
||||
- `description` - API description
|
||||
- `version` - API version
|
||||
- `host` - API host (e.g., "api.example.com")
|
||||
- `basePath` - Base path (e.g., "/v1")
|
||||
- `schemes` - Protocols (e.g., "http,https")
|
||||
- `consumes` - Request content types
|
||||
- `produces` - Response content types
|
||||
- `contactName`, `contactURL`, `contactEmail` - Contact information
|
||||
- `licenseName`, `licenseURL` - License information
|
||||
- `useDefinitions` - Use Swagger definitions (true/false)
|
||||
- `wrapCodeMsg` - Wrap response in `{code, msg, data}` structure
|
||||
- `securityDefinitionsFromJson` - Security definitions in JSON format
|
||||
|
||||
### Tag Options
|
||||
|
||||
#### JSON Tags
|
||||
|
||||
```go
|
||||
type Example {
|
||||
// Range validation
|
||||
Age int `json:"age,range=[1:150]"`
|
||||
|
||||
// Default value
|
||||
Status string `json:"status,default=active"`
|
||||
|
||||
// Example value
|
||||
Email string `json:"email,example=user@example.com"`
|
||||
|
||||
// Enum values
|
||||
Role string `json:"role,options=admin|user|guest"`
|
||||
|
||||
// Optional field
|
||||
Phone string `json:"phone,optional"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Form Tags
|
||||
|
||||
```go
|
||||
type QueryRequest {
|
||||
Keyword string `form:"keyword"`
|
||||
Page int `form:"page,default=1"`
|
||||
Size int `form:"size,range=[1:100]"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Path Tags
|
||||
|
||||
```go
|
||||
type PathRequest {
|
||||
UserId int `path:"userId,range=[1:999999]"`
|
||||
}
|
||||
```
|
||||
|
||||
#### Header Tags
|
||||
|
||||
```go
|
||||
type HeaderRequest {
|
||||
Token string `header:"Authorization"`
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Advanced Features
|
||||
|
||||
### Security Definitions
|
||||
|
||||
```go
|
||||
info (
|
||||
securityDefinitionsFromJson: `{
|
||||
"apiKey": {
|
||||
"type": "apiKey",
|
||||
"name": "x-api-key",
|
||||
"in": "header",
|
||||
"description": "API Key Authentication"
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
@server (
|
||||
authType: apiKey
|
||||
)
|
||||
service MyAPI {
|
||||
// Routes here will use apiKey authentication
|
||||
}
|
||||
```
|
||||
|
||||
### Code-Msg Wrapper
|
||||
|
||||
```go
|
||||
info (
|
||||
wrapCodeMsg: true
|
||||
bizCodeEnumDescription: "1001-User not found<br>1002-Permission denied"
|
||||
)
|
||||
```
|
||||
|
||||
Response will be wrapped as:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "ok",
|
||||
"data": { /* your actual response */ }
|
||||
}
|
||||
```
|
||||
|
||||
## 📂 Project Structure
|
||||
|
||||
```
|
||||
go-doc/
|
||||
├── cmd/
|
||||
│ └── go-doc/ # Main entry point
|
||||
│ └── main.go
|
||||
├── internal/
|
||||
│ ├── swagger/ # Core swagger generation logic
|
||||
│ │ ├── swagger.go
|
||||
│ │ ├── parameter.go
|
||||
│ │ ├── path.go
|
||||
│ │ └── ...
|
||||
│ └── util/ # Internal utilities
|
||||
│ ├── util.go
|
||||
│ ├── stringx.go
|
||||
│ └── pathx.go
|
||||
├── example/ # Example API files
|
||||
│ ├── example.api
|
||||
│ └── example_cn.api
|
||||
├── go.mod
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## 📝 License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Original code from [go-zero](https://github.com/zeromicro/go-zero) project
|
||||
- Built on top of [go-openapi/spec](https://github.com/go-openapi/spec)
|
||||
|
||||
## 📧 Contact
|
||||
|
||||
For questions or issues, please open an issue on GitHub.
|
||||
|
||||
---
|
||||
|
||||
Made with ❤️ by extracting and enhancing go-zero's swagger generation capabilities.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"go-doc/internal/swagger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "1.0.0"
|
||||
commit = "dev"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "go-doc",
|
||||
Short: "Generate Swagger documentation from go-zero API files",
|
||||
Long: `go-doc is a tool that converts go-zero .api files into OpenAPI 2.0 (Swagger) specification.`,
|
||||
Version: fmt.Sprintf("%s (commit: %s, built at: %s)", version, commit, date),
|
||||
RunE: swagger.Command,
|
||||
}
|
||||
|
||||
rootCmd.Flags().StringVarP(&swagger.VarStringAPI, "api", "a", "", "API file path (required)")
|
||||
rootCmd.Flags().StringVarP(&swagger.VarStringDir, "dir", "d", "", "Output directory (required)")
|
||||
rootCmd.Flags().StringVarP(&swagger.VarStringFilename, "filename", "f", "", "Output filename without extension (optional, defaults to API filename)")
|
||||
rootCmd.Flags().BoolVarP(&swagger.VarBoolYaml, "yaml", "y", false, "Generate YAML format instead of JSON")
|
||||
|
||||
if err := rootCmd.MarkFlagRequired("api"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := rootCmd.MarkFlagRequired("dir"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
# 🏛️ Cursor Rules: Go Kernel Mode 1.25.1
|
||||
|
||||
**最高指導原則:** 以 Linus Torvalds 的實用主義為核心,融合 Go 語言三巨頭 (Pike, Thompson, Griesemer) 的簡潔與併發哲學。
|
||||
|
||||
**核心目標:** 構建極致高效 (Performance-First)、極度簡潔 (Simplicity)、且完全受控 (Control) 的 Go 後端服務。
|
||||
|
||||
---
|
||||
|
||||
## 1. 最小化原則 (Minimization Principle)
|
||||
|
||||
**核心:保持規模的簡潔、專注、易於理解。**
|
||||
|
||||
| 規則 ID | 規則內容 (Go 實踐) | 哲學依據 |
|
||||
| :--- | :--- | :--- |
|
||||
| **M1** | **Stdlib-First:** **強制優先** 使用 Go 標準庫 (`net/http`, `context`, `sync`, `io`, `database/sql`)。任何外部依賴的引入必須有不可替代的理由。 | **Pike/Thompson:** 簡潔與正交性。 |
|
||||
| **M2** | **No-Fat-Frameworks:** **禁止** 使用大型、功能過載的 Web 框架。僅允許使用 Go 標準庫或極輕量的路由/工具庫 (如 `go-chi/chi`),以維持對 **性能瓶頸的完全控制**。 | **Linus Torvalds:** 實用與控制。 |
|
||||
| **M3** | **Small Interfaces:** 介面 (Interfaces) 必須 **小且專一** (`er` 慣例,如 `io.Reader`)。介面應定義在使用者 (Consumer) 端,以促進模組間的低耦合。 | **Robert Griesemer:** 語言設計的嚴謹。 |
|
||||
| **M4** | **Explicit Errors:** 錯誤處理必須使用 **Go 慣用的多回傳值 (`value, error`)** 模式。**嚴禁** 使用 `panic` 或 `recover` 來控制正常的業務流程錯誤。 | **Go 慣例:** 錯誤是流程的一部分。 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 結構化原則 (Structural Principle)
|
||||
|
||||
**核心:使用分層架構管理職責邊界,促進可測試性和可維護性。**
|
||||
|
||||
| 規則 ID | 規則內容 (Go 實踐) | 哲學依據 |
|
||||
| :--- | :--- | :--- |
|
||||
| **S1** | **Internal-First Layout:** 核心業務邏輯必須放置在 **`internal/`** 目錄中,不允許被外部專案直接導入。執行入口點必須在 **`cmd/{servicename}`**。 | **Griesemer/Go 慣例:** 嚴謹的專案封裝。 |
|
||||
| **S2** | **Clean Architecture Layers:** 服務應至少分為 **`transport`** (I/O, 如 `net/http` Handler)、**`service`** (核心業務邏輯) 和 **`repository`** (數據存取)。層次間透過 **介面** 隔離。 | **Pike/Thompson:** 關注點分離 (Separation of Concerns)。 |
|
||||
| **S3** | **Concurrency Management:** 併發操作 (如背景任務、Worker Pool) 必須明確使用 **Goroutine 和 Channel** 封裝。**Goroutine 的生命週期** 必須使用 **`context.Context`** 進行取消與超時控制。 | **Rob Pike:** Go 併發模型的正確實踐。 |
|
||||
| **S4** | **Config Isolation:** 設定檔 (`.env`, 環境變數) 只能在 **`cmd`** 或專門的 **`config`** 套件中讀取。核心業務邏輯 (Service Layer) 絕不允許直接存取環境變數。 | **Linus Torvalds:** 清晰的邊界控制。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 精準引用原則 (Precise Reference Principle)
|
||||
|
||||
**核心:所有依賴必須顯式、高效、可追溯。**
|
||||
|
||||
| 規則 ID | 規則內容 (Go 實踐) | 哲學依據 |
|
||||
| :--- | :--- | :--- |
|
||||
| **P1** | **Raw SQL Control:** 數據層存取必須使用 **原生 SQL** 語句,並搭配 **`sqlx`** 或 **`sqlc`** 輔助。**嚴禁** 使用大型、會自動生成複雜查詢的 ORM,以確保對效能的完全控制。 | **Linus Torvalds:** 控制數據流與效率。 |
|
||||
| **P2** | **Dependency Injection (DI):** 所有依賴(例如 `service` 依賴 `repository`)必須通過 **建構函式** (例如 `NewUserService(repo Repository)`) 顯式注入,避免使用全局變數。 | **Griesemer/Go 慣例:** 顯式的依賴關係。 |
|
||||
| **P3** | **I/O Context Passing:** 任何涉及 I/O (網路、DB) 的函式,其第一個參數必須是 **`context.Context`**,以便傳播截止時間和取消訊號。 | **Go 慣例:** 資源管理的標準。 |
|
||||
| **P4** | **Structured Logging:** 使用 **`log/slog`** 進行結構化日誌記錄。日誌中必須包含 Request ID 或 Trace ID,以實現 **可觀察性 (Observability)**。 | **Linus/實用主義:** 快速診斷問題的能力。 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 一致性原則 (Consistency Principle)
|
||||
|
||||
**核心:保持專案的程式碼風格、命名方式、結構的統一,減少團隊摩擦。**
|
||||
|
||||
| 規則 ID | 規則內容 (Go 實踐) | 哲學依據 |
|
||||
| :--- | :--- | :--- |
|
||||
| **C1** | **Mandatory Formatting:** 所有提交的程式碼必須通過 **`gofmt`** 或 **`goimports`** 格式化。**禁止** 違反官方程式碼風格的程式碼。 | **Robert Griesemer:** 語言設計的嚴謹性與工程規範。 |
|
||||
| **C2** | **Go Naming Conventions:** 變數和函式應**簡短**。未導出的私有成員使用**小寫開頭**。簡寫必須保持一致(如 HTTP 寫成 `http`,而非 `Http`)。 | **Rob Pike:** 簡潔與慣例。 |
|
||||
| **C3** | **Testing In-Package:** 單元測試程式碼 (e.g., `_test.go`) 應與被測試程式碼保持在 **同一套件**,以實現對私有函式和變數的全面測試。 | **Linus/Go 慣例:** 穩定性與可靠性。 |
|
||||
| **C4** | **Go Modules & Version:** 專案必須使用 **Go Modules** 進行依賴管理,並且針對 **Go 1.25** 版本進行編寫和測試。| **Linus/Griesemer:** 版本控制的嚴格性與相容性。 |
|
||||
|
||||
---
|
|
@ -0,0 +1,4 @@
|
|||
*.json
|
||||
*.yaml
|
||||
bin
|
||||
output
|
|
@ -0,0 +1,241 @@
|
|||
syntax = "v1"
|
||||
|
||||
info (
|
||||
title: "Demo API" // title corresponding to Swagger
|
||||
description: "Generating Swagger files using the API demo." // description corresponding to Swagger
|
||||
version: "v1" // version corresponding to Swagger
|
||||
termsOfService: "https://github.com/zeromicro/go-zero" // termsOfService corresponding to Swagger
|
||||
contactName: "keson.an" // contactName corresponding to Swagger
|
||||
contactURL: "https://github.com/zeromicro/go-zero" // contactURL corresponding to Swagger
|
||||
contactEmail: "example@gmail.com" // contactEmail corresponding to Swagger
|
||||
licenseName: "MIT" // licenseName corresponding to Swagger
|
||||
licenseURL: "https://github.com/zeromicro/go-zero" // licenseURL corresponding to Swagger
|
||||
consumes: "application/json" // consumes corresponding to Swagger,default value is `application/json`
|
||||
produces: "application/json" // produces corresponding to Swagger,default value is `application/json`
|
||||
schemes: "http,https" // schemes corresponding to Swagger,default value is `https``
|
||||
host: "example.com" // host corresponding to Swagger,default value is `127.0.0.1`
|
||||
basePath: "/v1" // basePath corresponding to Swagger,default value is `/`
|
||||
wrapCodeMsg: true // to wrap in the universal code-msg structure, like {"code":0,"msg":"OK","data":$data}
|
||||
bizCodeEnumDescription: "1001-User not login<br>1002-User permission denied" // enums of business error codes, in JSON format, with the key being the business error code and the value being the description of that error code. This only takes effect when wrapCodeMsg is set to true.
|
||||
// securityDefinitionsFromJson is a custom authentication configuration, and the JSON content will be directly inserted into the securityDefinitions of Swagger.
|
||||
// Format reference: https://swagger.io/specification/v2/#security-definitions-object
|
||||
// You can declare authType in the @server of the API to specify the authentication type used for its routes.
|
||||
securityDefinitionsFromJson: `{"apiKey":{"description":"apiKey type description","type":"apiKey","name":"x-api-key","in":"header"}}`
|
||||
useDefinitions: true // if set true, the definitions will be generated in the swagger.json for response body or json request body file, and the models will be referenced in the API.
|
||||
)
|
||||
|
||||
type (
|
||||
QueryReq {
|
||||
Id int `form:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
Avatar string `form:"avatar,optional,example=https://example.com/avatar.png"`
|
||||
}
|
||||
QueryResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
PathQueryReq {
|
||||
Id int `path:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
}
|
||||
PathQueryResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "query" // tags corresponding to Swagger
|
||||
summary: "query API set" // summary corresponding to Swagger
|
||||
prefix: v1
|
||||
authType: apiKey // Specifies the authentication type used for this route, which is the name defined in securityDefinitionsFromJson.
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "query demo"
|
||||
)
|
||||
@handler query
|
||||
get /query (QueryReq) returns (QueryResp)
|
||||
|
||||
@doc (
|
||||
description: "show path query demo"
|
||||
)
|
||||
@handler queryPath
|
||||
get /query/:id (PathQueryReq) returns (PathQueryResp)
|
||||
}
|
||||
|
||||
type (
|
||||
FormReq {
|
||||
Id int `form:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
}
|
||||
FormResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "form" // tags corresponding to Swagger
|
||||
summary: "form API set" // summary corresponding to Swagger
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "form demo"
|
||||
)
|
||||
@handler form
|
||||
post /form (FormReq) returns (FormResp)
|
||||
}
|
||||
|
||||
type (
|
||||
JsonReq {
|
||||
Id int `json:"id,range=[1:10000],example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
Avatar string `json:"avatar,optional"`
|
||||
Language string `json:"language,options=golang|java|python|typescript|rust"`
|
||||
Gender string `json:"gender,default=male,options=male|female,example=male"`
|
||||
}
|
||||
JsonResp {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Language string `json:"language"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
ComplexJsonLevel2 {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
}
|
||||
ComplexJsonLevel1 {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// Object
|
||||
Object ComplexJsonLevel2 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel2 `json:"pointerObject"`
|
||||
}
|
||||
ComplexJsonReq {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// basic array
|
||||
ArrayInteger []int `json:"arrayInteger"`
|
||||
ArrayNumber []float64 `json:"arrayNumber"`
|
||||
ArrayBoolean []bool `json:"arrayBoolean"`
|
||||
ArrayString []string `json:"arrayString"`
|
||||
// basic array array
|
||||
ArrayArrayInteger [][]int `json:"arrayArrayInteger"`
|
||||
ArrayArrayNumber [][]float64 `json:"arrayArrayNumber"`
|
||||
ArrayArrayBoolean [][]bool `json:"arrayArrayBoolean"`
|
||||
ArrayArrayString [][]string `json:"arrayArrayString"`
|
||||
// basic map
|
||||
MapInteger map[string]int `json:"mapInteger"`
|
||||
MapNumber map[string]float64 `json:"mapNumber"`
|
||||
MapBoolean map[string]bool `json:"mapBoolean"`
|
||||
MapString map[string]string `json:"mapString"`
|
||||
// basic map array
|
||||
MapArrayInteger map[string][]int `json:"mapArrayInteger"`
|
||||
MapArrayNumber map[string][]float64 `json:"mapArrayNumber"`
|
||||
MapArrayBoolean map[string][]bool `json:"mapArrayBoolean"`
|
||||
MapArrayString map[string][]string `json:"mapArrayString"`
|
||||
// basic map map
|
||||
MapMapInteger map[string]map[string]int `json:"mapMapInteger"`
|
||||
MapMapNumber map[string]map[string]float64 `json:"mapMapNumber"`
|
||||
MapMapBoolean map[string]map[string]bool `json:"mapMapBoolean"`
|
||||
MapMapString map[string]map[string]string `json:"mapMapString"`
|
||||
// Object
|
||||
Object ComplexJsonLevel1 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel1 `json:"pointerObject"`
|
||||
// Object array
|
||||
ArrayObject []ComplexJsonLevel1 `json:"arrayObject"`
|
||||
ArrayPointerObject []*ComplexJsonLevel1 `json:"arrayPointerObject"`
|
||||
// Object map
|
||||
MapObject map[string]ComplexJsonLevel1 `json:"mapObject"`
|
||||
MapPointerObject map[string]*ComplexJsonLevel1 `json:"mapPointerObject"`
|
||||
// Object array array
|
||||
ArrayArrayObject [][]ComplexJsonLevel1 `json:"arrayArrayObject"`
|
||||
ArrayArrayPointerObject [][]*ComplexJsonLevel1 `json:"arrayArrayPointerObject"`
|
||||
// Object array map
|
||||
ArrayMapObject []map[string]ComplexJsonLevel1 `json:"arrayMapObject"`
|
||||
ArrayMapPointerObject []map[string]*ComplexJsonLevel1 `json:"arrayMapPointerObject"`
|
||||
// Object map array
|
||||
MapArrayObject map[string][]ComplexJsonLevel1 `json:"mapArrayObject"`
|
||||
MapArrayPointerObject map[string][]*ComplexJsonLevel1 `json:"mapArrayPointerObject"`
|
||||
}
|
||||
ComplexJsonResp {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// basic array
|
||||
ArrayInteger []int `json:"arrayInteger"`
|
||||
ArrayNumber []float64 `json:"arrayNumber"`
|
||||
ArrayBoolean []bool `json:"arrayBoolean"`
|
||||
ArrayString []string `json:"arrayString"`
|
||||
// basic array array
|
||||
ArrayArrayInteger [][]int `json:"arrayArrayInteger"`
|
||||
ArrayArrayNumber [][]float64 `json:"arrayArrayNumber"`
|
||||
ArrayArrayBoolean [][]bool `json:"arrayArrayBoolean"`
|
||||
ArrayArrayString [][]string `json:"arrayArrayString"`
|
||||
// basic map
|
||||
MapInteger map[string]int `json:"mapInteger"`
|
||||
MapNumber map[string]float64 `json:"mapNumber"`
|
||||
MapBoolean map[string]bool `json:"mapBoolean"`
|
||||
MapString map[string]string `json:"mapString"`
|
||||
// basic map array
|
||||
MapArrayInteger map[string][]int `json:"mapArrayInteger"`
|
||||
MapArrayNumber map[string][]float64 `json:"mapArrayNumber"`
|
||||
MapArrayBoolean map[string][]bool `json:"mapArrayBoolean"`
|
||||
MapArrayString map[string][]string `json:"mapArrayString"`
|
||||
// basic map map
|
||||
MapMapInteger map[string]map[string]int `json:"mapMapInteger"`
|
||||
MapMapNumber map[string]map[string]float64 `json:"mapMapNumber"`
|
||||
MapMapBoolean map[string]map[string]bool `json:"mapMapBoolean"`
|
||||
MapMapString map[string]map[string]string `json:"mapMapString"`
|
||||
// Object
|
||||
Object ComplexJsonLevel1 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel1 `json:"pointerObject"`
|
||||
// Object array
|
||||
ArrayObject []ComplexJsonLevel1 `json:"arrayObject"`
|
||||
ArrayPointerObject []*ComplexJsonLevel1 `json:"arrayPointerObject"`
|
||||
// Object map
|
||||
MapObject map[string]ComplexJsonLevel1 `json:"mapObject"`
|
||||
MapPointerObject map[string]*ComplexJsonLevel1 `json:"mapPointerObject"`
|
||||
// Object array array
|
||||
ArrayArrayObject [][]ComplexJsonLevel1 `json:"arrayArrayObject"`
|
||||
ArrayArrayPointerObject [][]*ComplexJsonLevel1 `json:"arrayArrayPointerObject"`
|
||||
// Object array map
|
||||
ArrayMapObject []map[string]ComplexJsonLevel1 `json:"arrayMapObject"`
|
||||
ArrayMapPointerObject []map[string]*ComplexJsonLevel1 `json:"arrayMapPointerObject"`
|
||||
// Object map array
|
||||
MapArrayObject map[string][]ComplexJsonLevel1 `json:"mapArrayObject"`
|
||||
MapArrayPointerObject map[string][]*ComplexJsonLevel1 `json:"mapArrayPointerObject"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "postJson" // tags corresponding to Swagger
|
||||
summary: "json API set" // summary corresponding to Swagger
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "simple json request body API"
|
||||
)
|
||||
@handler jsonSimple
|
||||
post /json/simple (JsonReq) returns (JsonResp)
|
||||
|
||||
@doc (
|
||||
description: "complex json request body API"
|
||||
)
|
||||
@handler jsonComplex
|
||||
post /json/complex (ComplexJsonReq) returns (ComplexJsonResp)
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
syntax = "v1"
|
||||
|
||||
info (
|
||||
title: "演示 API" // 对应 swagger 的 title
|
||||
description: "演示 api 生成 swagger 文件的 api 完整写法" // 对应 swagger 的 description
|
||||
version: "v1" // 对应 swagger 的 version
|
||||
termsOfService: "https://github.com/zeromicro/go-zero" // 对应 swagger 的 termsOfService
|
||||
contactName: "keson.an" // 对应 swagger 的 contactName
|
||||
contactURL: "https://github.com/zeromicro/go-zero" // 对应 swagger 的 contactURL
|
||||
contactEmail: "example@gmail.com" // 对应 swagger 的 contactEmail
|
||||
licenseName: "MIT" // 对应 swagger 的 licenseName
|
||||
licenseURL: "https://github.com/zeromicro/go-zero" // 对应 swagger 的 licenseURL
|
||||
consumes: "application/json" // 对应 swagger 的 consumes,不填默认为 application/json
|
||||
produces: "application/json" // 对应 swagger 的 produces,不填默认为 application/json
|
||||
schemes: "http,https" // 对应 swagger 的 schemes,不填默认为 https
|
||||
host: "example.com" // 对应 swagger 的 host,不填默认为 127.0.0.1
|
||||
basePath: "/v1" // 对应 swagger 的 basePath,不填默认为 /
|
||||
wrapCodeMsg: true // 是否用 code-msg 通用响应体,如果开启,则以格式 {"code":0,"msg":"OK","data":$data} 包括响应体
|
||||
bizCodeEnumDescription: "1001-未登录<br>1002-无权限操作" // 全局业务错误码枚举描述,json 格式,key 为业务错误码,value 为该错误码的描述,仅当 wrapCodeMsg 为 true 时生效
|
||||
// securityDefinitionsFromJson 为自定义鉴权配置,json 内容将直接放入 swagger 的 securityDefinitions 中,
|
||||
// 格式参考 https://swagger.io/specification/v2/#security-definitions-object
|
||||
// 在 api 的 @server 中可声明 authType 来指定其路由使用的鉴权类型
|
||||
securityDefinitionsFromJson: `{"apiKey":{"description":"apiKey 类型鉴权自定义","type":"apiKey","name":"x-api-key","in":"header"}}`
|
||||
useDefinitions: true// 开启声明将生成models 进行关联,definitions 仅对响应体和 json 请求体生效
|
||||
)
|
||||
|
||||
type (
|
||||
QueryReq {
|
||||
Id int `form:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
Avatar string `form:"avatar,optional,example=https://example.com/avatar.png"`
|
||||
}
|
||||
QueryResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
PathQueryReq {
|
||||
Id int `path:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
}
|
||||
PathQueryResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "query 演示" // 对应 swagger 的 tags,可以对 swagger 中的 api 进行分组
|
||||
summary: "query 类型接口集合" // 对应 swagger 的 summary
|
||||
prefix: v1
|
||||
authType: apiKey // 指定该路由使用的鉴权类型,值为 securityDefinitionsFromJson 中定义的名称
|
||||
group:"demo"
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "query 接口"
|
||||
bizCodeEnumDescription: " 1003-用不存在<br>1004-非法操作" // 接口级别业务错误码枚举描述,会覆盖全局的业务错误码,json 格式,key 为业务错误码,value 为该错误码的描述,仅当 wrapCodeMsg 为 true 且 useDefinitions 为 false 时生效
|
||||
)
|
||||
@handler query
|
||||
get /query (QueryReq) returns (QueryResp)
|
||||
|
||||
@doc (
|
||||
description: "query path 中包含 id 字段接口"
|
||||
)
|
||||
@handler queryPath
|
||||
get /query/:id (PathQueryReq) returns (PathQueryResp)
|
||||
}
|
||||
|
||||
type (
|
||||
FormReq {
|
||||
Id int `form:"id,range=[1:10000],example=10"`
|
||||
Name string `form:"name,example=keson.an"`
|
||||
}
|
||||
FormResp {
|
||||
Id int `json:"id,example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "form 表单 api 演示" // 对应 swagger 的 tags,可以对 swagger 中的 api 进行分组
|
||||
summary: "form 表单类型接口集合" // 对应 swagger 的 summary
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "form 接口"
|
||||
)
|
||||
@handler form
|
||||
post /form (FormReq) returns (FormResp)
|
||||
}
|
||||
|
||||
type (
|
||||
JsonReq {
|
||||
Id int `json:"id,range=[1:10000],example=10"`
|
||||
Name string `json:"name,example=keson.an"`
|
||||
Avatar string `json:"avatar,optional"`
|
||||
Language string `json:"language,options=golang|java|python|typescript|rust"`
|
||||
Gender string `json:"gender,default=male,options=male|female,example=male"`
|
||||
}
|
||||
JsonResp {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Language string `json:"language"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
ComplexJsonLevel2 {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
}
|
||||
ComplexJsonLevel1 {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// Object
|
||||
Object ComplexJsonLevel2 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel2 `json:"pointerObject"`
|
||||
}
|
||||
ComplexJsonReq {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// basic array
|
||||
ArrayInteger []int `json:"arrayInteger"`
|
||||
ArrayNumber []float64 `json:"arrayNumber"`
|
||||
ArrayBoolean []bool `json:"arrayBoolean"`
|
||||
ArrayString []string `json:"arrayString"`
|
||||
// basic array array
|
||||
ArrayArrayInteger [][]int `json:"arrayArrayInteger"`
|
||||
ArrayArrayNumber [][]float64 `json:"arrayArrayNumber"`
|
||||
ArrayArrayBoolean [][]bool `json:"arrayArrayBoolean"`
|
||||
ArrayArrayString [][]string `json:"arrayArrayString"`
|
||||
// basic map
|
||||
MapInteger map[string]int `json:"mapInteger"`
|
||||
MapNumber map[string]float64 `json:"mapNumber"`
|
||||
MapBoolean map[string]bool `json:"mapBoolean"`
|
||||
MapString map[string]string `json:"mapString"`
|
||||
// basic map array
|
||||
MapArrayInteger map[string][]int `json:"mapArrayInteger"`
|
||||
MapArrayNumber map[string][]float64 `json:"mapArrayNumber"`
|
||||
MapArrayBoolean map[string][]bool `json:"mapArrayBoolean"`
|
||||
MapArrayString map[string][]string `json:"mapArrayString"`
|
||||
// basic map map
|
||||
MapMapInteger map[string]map[string]int `json:"mapMapInteger"`
|
||||
MapMapNumber map[string]map[string]float64 `json:"mapMapNumber"`
|
||||
MapMapBoolean map[string]map[string]bool `json:"mapMapBoolean"`
|
||||
MapMapString map[string]map[string]string `json:"mapMapString"`
|
||||
MapMapObject map[string]map[string]ComplexJsonLevel1 `json:"mapMapObject"`
|
||||
MapMapPointerObject map[string]map[string]*ComplexJsonLevel1 `json:"mapMapPointerObject"`
|
||||
// Object
|
||||
Object ComplexJsonLevel1 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel1 `json:"pointerObject"`
|
||||
// Object array
|
||||
ArrayObject []ComplexJsonLevel1 `json:"arrayObject"`
|
||||
ArrayPointerObject []*ComplexJsonLevel1 `json:"arrayPointerObject"`
|
||||
// Object map
|
||||
MapObject map[string]ComplexJsonLevel1 `json:"mapObject"`
|
||||
MapPointerObject map[string]*ComplexJsonLevel1 `json:"mapPointerObject"`
|
||||
// Object array array
|
||||
ArrayArrayObject [][]ComplexJsonLevel1 `json:"arrayArrayObject"`
|
||||
ArrayArrayPointerObject [][]*ComplexJsonLevel1 `json:"arrayArrayPointerObject"`
|
||||
// Object array map
|
||||
ArrayMapObject []map[string]ComplexJsonLevel1 `json:"arrayMapObject"`
|
||||
ArrayMapPointerObject []map[string]*ComplexJsonLevel1 `json:"arrayMapPointerObject"`
|
||||
// Object map array
|
||||
MapArrayObject map[string][]ComplexJsonLevel1 `json:"mapArrayObject"`
|
||||
MapArrayPointerObject map[string][]*ComplexJsonLevel1 `json:"mapArrayPointerObject"`
|
||||
}
|
||||
ComplexJsonResp {
|
||||
// basic
|
||||
Integer int `json:"integer,example=1"`
|
||||
Number float64 `json:"number,example=1.1"`
|
||||
Boolean bool `json:"boolean,options=true|false,example=true"`
|
||||
String string `json:"string,example=some text"`
|
||||
// basic array
|
||||
ArrayInteger []int `json:"arrayInteger"`
|
||||
ArrayNumber []float64 `json:"arrayNumber"`
|
||||
ArrayBoolean []bool `json:"arrayBoolean"`
|
||||
ArrayString []string `json:"arrayString"`
|
||||
// basic array array
|
||||
ArrayArrayInteger [][]int `json:"arrayArrayInteger"`
|
||||
ArrayArrayNumber [][]float64 `json:"arrayArrayNumber"`
|
||||
ArrayArrayBoolean [][]bool `json:"arrayArrayBoolean"`
|
||||
ArrayArrayString [][]string `json:"arrayArrayString"`
|
||||
// basic map
|
||||
MapInteger map[string]int `json:"mapInteger"`
|
||||
MapNumber map[string]float64 `json:"mapNumber"`
|
||||
MapBoolean map[string]bool `json:"mapBoolean"`
|
||||
MapString map[string]string `json:"mapString"`
|
||||
// basic map array
|
||||
MapArrayInteger map[string][]int `json:"mapArrayInteger"`
|
||||
MapArrayNumber map[string][]float64 `json:"mapArrayNumber"`
|
||||
MapArrayBoolean map[string][]bool `json:"mapArrayBoolean"`
|
||||
MapArrayString map[string][]string `json:"mapArrayString"`
|
||||
// basic map map
|
||||
MapMapInteger map[string]map[string]int `json:"mapMapInteger"`
|
||||
MapMapNumber map[string]map[string]float64 `json:"mapMapNumber"`
|
||||
MapMapBoolean map[string]map[string]bool `json:"mapMapBoolean"`
|
||||
MapMapString map[string]map[string]string `json:"mapMapString"`
|
||||
MapMapObject map[string]map[string]ComplexJsonLevel1 `json:"mapMapObject"`
|
||||
MapMapPointerObject map[string]map[string]*ComplexJsonLevel1 `json:"mapMapPointerObject"`
|
||||
// Object
|
||||
Object ComplexJsonLevel1 `json:"object"`
|
||||
PointerObject *ComplexJsonLevel1 `json:"pointerObject"`
|
||||
// Object array
|
||||
ArrayObject []ComplexJsonLevel1 `json:"arrayObject"`
|
||||
ArrayPointerObject []*ComplexJsonLevel1 `json:"arrayPointerObject"`
|
||||
// Object map
|
||||
MapObject map[string]ComplexJsonLevel1 `json:"mapObject"`
|
||||
MapPointerObject map[string]*ComplexJsonLevel1 `json:"mapPointerObject"`
|
||||
// Object array array
|
||||
ArrayArrayObject [][]ComplexJsonLevel1 `json:"arrayArrayObject"`
|
||||
ArrayArrayPointerObject [][]*ComplexJsonLevel1 `json:"arrayArrayPointerObject"`
|
||||
// Object array map
|
||||
ArrayMapObject []map[string]ComplexJsonLevel1 `json:"arrayMapObject"`
|
||||
ArrayMapPointerObject []map[string]*ComplexJsonLevel1 `json:"arrayMapPointerObject"`
|
||||
// Object map array
|
||||
MapArrayObject map[string][]ComplexJsonLevel1 `json:"mapArrayObject"`
|
||||
MapArrayPointerObject map[string][]*ComplexJsonLevel1 `json:"mapArrayPointerObject"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "post json api 演示" // 对应 swagger 的 tags,可以对 swagger 中的 api 进行分组
|
||||
summary: "json 请求类型接口集合" // 对应 swagger 的 summary
|
||||
)
|
||||
service Swagger {
|
||||
@doc (
|
||||
description: "简单的 json 请求体接口"
|
||||
)
|
||||
@handler jsonSimple
|
||||
post /json/simple (JsonReq) returns (JsonResp)
|
||||
|
||||
@doc (
|
||||
description: "复杂的 json 请求体接口"
|
||||
)
|
||||
@handler jsonComplex
|
||||
post /json/complex (ComplexJsonReq) returns (ComplexJsonResp)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 1. 检查并安装 swagger
|
||||
if ! command -v swagger &> /dev/null; then
|
||||
echo "swagger 未安装,正在从 GitHub 安装..."
|
||||
# 这里使用 go-swagger 的安装方式
|
||||
go install github.com/go-swagger/go-swagger/cmd/swagger@latest
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "安装 swagger 失败"
|
||||
exit 1
|
||||
fi
|
||||
echo "swagger 安装成功"
|
||||
else
|
||||
echo "swagger 已安装"
|
||||
fi
|
||||
|
||||
mkdir bin output
|
||||
|
||||
export GOBIN=$(pwd)/bin
|
||||
|
||||
# 2. 安装最新版 goctl
|
||||
go install ../../..
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "安装 goctl 失败"
|
||||
exit 1
|
||||
fi
|
||||
echo "goctl 安装成功"
|
||||
|
||||
# 3. 生成 swagger 文件
|
||||
echo "正在生成 swagger 文件..."
|
||||
./bin/goctl api swagger --api example_cn.api --dir output
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "生成 swagger 文件失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. 启动 swagger 服务
|
||||
echo "启动 swagger 服务..."
|
||||
swagger serve ./output/example_cn.json
|
Binary file not shown.
After Width: | Height: | Size: 3.3 MiB |
Binary file not shown.
After Width: | Height: | Size: 3.6 MiB |
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 1. Check and install swagger if not exists
|
||||
if ! command -v swagger &> /dev/null; then
|
||||
echo "swagger not found, installing from GitHub..."
|
||||
# Using go-swagger installation method
|
||||
go install github.com/go-swagger/go-swagger/cmd/swagger@latest
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install swagger"
|
||||
exit 1
|
||||
fi
|
||||
echo "swagger installed successfully"
|
||||
else
|
||||
echo "swagger already installed"
|
||||
fi
|
||||
|
||||
mkdir bin output
|
||||
|
||||
export GOBIN=$(pwd)/bin
|
||||
|
||||
# 2. Install latest goctl version
|
||||
go install ../../..
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install goctl"
|
||||
exit 1
|
||||
fi
|
||||
echo "goctl installed successfully"
|
||||
|
||||
# 3. Generate swagger files
|
||||
echo "Generating swagger files..."
|
||||
./bin/goctl api swagger --api example.api --dir output
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to generate swagger files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. Start swagger server
|
||||
echo "Starting swagger server..."
|
||||
swagger serve ./output/example.json
|
Binary file not shown.
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
After Width: | Height: | Size: 2.4 MiB |
|
@ -0,0 +1,91 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 检查Docker是否运行的函数
|
||||
is_docker_running() {
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
return 1 # Docker未运行
|
||||
else
|
||||
return 0 # Docker正在运行
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir bin output
|
||||
|
||||
export GOBIN=$(pwd)/bin
|
||||
|
||||
# 1. 检查并安装Docker(如果不存在)
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "未检测到Docker,正在尝试安装..."
|
||||
|
||||
# 使用官方脚本安装Docker
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
rm get-docker.sh
|
||||
|
||||
# 验证安装
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Docker安装失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 将当前用户加入docker组(可能需要重新登录)
|
||||
sudo usermod -aG docker $USER
|
||||
echo "Docker安装成功。您可能需要注销并重新登录使更改生效。"
|
||||
else
|
||||
echo "Docker已安装"
|
||||
fi
|
||||
|
||||
# 2. 安装最新版goctl
|
||||
go install ../../..
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "goctl安装失败"
|
||||
exit 1
|
||||
fi
|
||||
echo "goctl 安装成功"
|
||||
|
||||
# 3. 生成swagger文件
|
||||
echo "正在生成swagger文件..."
|
||||
./bin/goctl api swagger --api example_cn.api --dir output
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "swagger文件生成失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查Docker是否运行
|
||||
if ! is_docker_running; then
|
||||
echo "Docker未运行,请先启动Docker服务"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. 清理现有的swagger-ui容器
|
||||
echo "正在清理现有的swagger-ui容器..."
|
||||
docker rm -f swagger-ui 2>/dev/null && echo "已移除现有的swagger-ui容器"
|
||||
|
||||
# 5. 在Docker中运行swagger-ui
|
||||
echo "正在启动swagger-ui容器..."
|
||||
docker run -d --name swagger-ui -p 8080:8080 \
|
||||
-e SWAGGER_JSON=/tmp/example.json \
|
||||
-v $(pwd)/output/example_cn.json:/tmp/example.json \
|
||||
swaggerapi/swagger-ui
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "swagger-ui容器启动失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 等待1秒确保服务就绪
|
||||
echo "等待swagger-ui初始化..."
|
||||
sleep 1
|
||||
|
||||
# 显示访问信息并尝试打开浏览器
|
||||
SWAGGER_URL="http://localhost:8080"
|
||||
echo -e "\nSwagger UI 已准备就绪,访问地址: \033[1;34m${SWAGGER_URL}\033[0m"
|
||||
echo "正在尝试在默认浏览器中打开..."
|
||||
|
||||
# 跨平台打开浏览器
|
||||
case "$(uname -s)" in
|
||||
Linux*) xdg-open "$SWAGGER_URL";;
|
||||
Darwin*) open "$SWAGGER_URL";;
|
||||
CYGWIN*|MINGW*|MSYS*) start "$SWAGGER_URL";;
|
||||
*) echo "无法在当前操作系统自动打开浏览器";;
|
||||
esac
|
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
After Width: | Height: | Size: 2.4 MiB |
|
@ -0,0 +1,80 @@
|
|||
#!/bin/bash
|
||||
|
||||
is_docker_running() {
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
return 1 # Docker is not running
|
||||
else
|
||||
return 0 # Docker is running
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir bin output
|
||||
|
||||
export GOBIN=$(pwd)/bin
|
||||
|
||||
# 1. Check and install Docker if not exists
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Docker not found, attempting to install..."
|
||||
|
||||
# Install Docker using official installation script
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
rm get-docker.sh
|
||||
|
||||
# Verify installation
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Failed to install Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add current user to docker group (may require logout/login)
|
||||
sudo usermod -aG docker $USER
|
||||
echo "Docker installed successfully. You may need to logout and login again for changes to take effect."
|
||||
else
|
||||
echo "Docker already installed"
|
||||
fi
|
||||
|
||||
# 2. Install latest goctl version
|
||||
go install ../../..
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install goctl"
|
||||
exit 1
|
||||
fi
|
||||
echo "goctl installed successfully"
|
||||
|
||||
# 3. Generate swagger files
|
||||
echo "Generating swagger files..."
|
||||
./bin/goctl api swagger --api example.api --dir output
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to generate swagger files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! is_docker_running; then
|
||||
echo "Docker is not running, Pls start Docker first"
|
||||
fi
|
||||
|
||||
# 4. Clean up any existing swagger-ui container
|
||||
echo "Cleaning up existing swagger-ui containers..."
|
||||
docker rm -f swagger-ui 2>/dev/null && echo "Removed existing swagger-ui container"
|
||||
|
||||
# 5. Run swagger-ui in Docker
|
||||
echo "Starting swagger-ui in Docker..."
|
||||
docker run -d --name swagger-ui -p 8080:8080 -e SWAGGER_JSON=/tmp/example.json -v $(pwd)/output/example.json:/tmp/example.json swaggerapi/swagger-ui
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to start swagger-ui container"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for swagger-ui to initialize..."
|
||||
sleep 1
|
||||
SWAGGER_URL="http://localhost:8080"
|
||||
echo -e "\nSwagger UI is ready at: \033[1;34m${SWAGGER_URL}\033[0m"
|
||||
echo "Opening in default browser..."
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux*) xdg-open "$SWAGGER_URL";;
|
||||
Darwin*) open "$SWAGGER_URL";;
|
||||
CYGWIN*|MINGW*|MSYS*) start "$SWAGGER_URL";;
|
||||
*) echo "System not supported";;
|
||||
esac
|
|
@ -0,0 +1,30 @@
|
|||
module go-doc
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/zeromicro/go-zero/tools/goctl v1.9.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/gookit/color v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/zeromicro/go-zero v1.9.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
|
@ -0,0 +1,58 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e h1:auobAirzhPsLHMso0NVMqK0QunuLDYCK83KnaVUM/RU=
|
||||
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e/go.mod h1:NAKTe9SplQBxIUlHlsuId1jk1I7bWTVV/2q/GtdRi6g=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
||||
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
||||
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
|
||||
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/zeromicro/go-zero v1.9.0 h1:hlVtQCSHPszQdcwZTawzGwTej1G2mhHybYzMRLuwCt4=
|
||||
github.com/zeromicro/go-zero v1.9.0/go.mod h1:TMyCxiaOjLQ3YxyYlJrejaQZF40RlzQ3FVvFu5EbcV4=
|
||||
github.com/zeromicro/go-zero/tools/goctl v1.9.0 h1:Ro5YK1iTarQc5XO0BYysRr18+1seBY36YjnxmDkyKRg=
|
||||
github.com/zeromicro/go-zero/tools/goctl v1.9.0/go.mod h1:ypiu1QOOEMTHd0Ft8knzqmq4PWBI7+l3ozoi1stGVqo=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,79 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"go-doc/internal/util"
|
||||
)
|
||||
|
||||
func getBoolFromKVOrDefault(properties map[string]string, key string, def bool) bool {
|
||||
if len(properties) == 0 {
|
||||
return def
|
||||
}
|
||||
val, ok := properties[key]
|
||||
if !ok || len(val) == 0 {
|
||||
return def
|
||||
}
|
||||
//I think this function and those below should handle error, but they didn't.
|
||||
//Since a default value (def) is provided, any parsing errors will result in the default being returned.
|
||||
// Try to unquote if the value is quoted, otherwise use as-is
|
||||
str := val
|
||||
if unquoted, err := strconv.Unquote(val); err == nil {
|
||||
str = unquoted
|
||||
}
|
||||
if len(str) == 0 {
|
||||
return def
|
||||
}
|
||||
res, _ := strconv.ParseBool(str)
|
||||
return res
|
||||
}
|
||||
|
||||
func getStringFromKVOrDefault(properties map[string]string, key string, def string) string {
|
||||
if len(properties) == 0 {
|
||||
return def
|
||||
}
|
||||
val, ok := properties[key]
|
||||
if !ok || len(val) == 0 {
|
||||
return def
|
||||
}
|
||||
// Try to unquote if the value is quoted, otherwise use as-is
|
||||
str := val
|
||||
if unquoted, err := strconv.Unquote(val); err == nil {
|
||||
str = unquoted
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func getListFromInfoOrDefault(properties map[string]string, key string, def []string) []string {
|
||||
if len(properties) == 0 {
|
||||
return def
|
||||
}
|
||||
val, ok := properties[key]
|
||||
if !ok || len(val) == 0 {
|
||||
return def
|
||||
}
|
||||
|
||||
// Try to unquote if the value is quoted, otherwise use as-is
|
||||
str := val
|
||||
if unquoted, err := strconv.Unquote(val); err == nil {
|
||||
str = unquoted
|
||||
}
|
||||
resp := util.FieldsAndTrimSpace(str, commaRune)
|
||||
if len(resp) == 0 {
|
||||
return def
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func getFirstUsableString(def ...string) string {
|
||||
if len(def) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, val := range def {
|
||||
str, err := strconv.Unquote(val)
|
||||
if err == nil && len(str) != 0 {
|
||||
return str
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getBoolFromKVOrDefault(t *testing.T) {
|
||||
properties := map[string]string{
|
||||
"enabled": `"true"`,
|
||||
"disabled": `"false"`,
|
||||
"invalid": `"notabool"`,
|
||||
"empty_value": `""`,
|
||||
}
|
||||
|
||||
assert.True(t, getBoolFromKVOrDefault(properties, "enabled", false))
|
||||
assert.False(t, getBoolFromKVOrDefault(properties, "disabled", true))
|
||||
assert.False(t, getBoolFromKVOrDefault(properties, "invalid", false))
|
||||
assert.True(t, getBoolFromKVOrDefault(properties, "missing", true))
|
||||
assert.False(t, getBoolFromKVOrDefault(properties, "empty_value", false))
|
||||
assert.False(t, getBoolFromKVOrDefault(nil, "nil", false))
|
||||
assert.False(t, getBoolFromKVOrDefault(map[string]string{}, "empty", false))
|
||||
}
|
||||
|
||||
func Test_getStringFromKVOrDefault(t *testing.T) {
|
||||
properties := map[string]string{
|
||||
"name": `"example"`,
|
||||
"empty": `""`,
|
||||
}
|
||||
|
||||
assert.Equal(t, "example", getStringFromKVOrDefault(properties, "name", "default"))
|
||||
assert.Equal(t, "default", getStringFromKVOrDefault(properties, "empty", "default"))
|
||||
assert.Equal(t, "default", getStringFromKVOrDefault(properties, "missing", "default"))
|
||||
assert.Equal(t, "default", getStringFromKVOrDefault(nil, "nil", "default"))
|
||||
assert.Equal(t, "default", getStringFromKVOrDefault(map[string]string{}, "empty", "default"))
|
||||
}
|
||||
|
||||
func Test_getListFromInfoOrDefault(t *testing.T) {
|
||||
properties := map[string]string{
|
||||
"list": `"a, b, c"`,
|
||||
"empty": `""`,
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"a", " b", " c"}, getListFromInfoOrDefault(properties, "list", []string{"default"}))
|
||||
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(properties, "empty", []string{"default"}))
|
||||
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(properties, "missing", []string{"default"}))
|
||||
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(nil, "nil", []string{"default"}))
|
||||
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(map[string]string{}, "empty", []string{"default"}))
|
||||
assert.Equal(t, []string{"default"}, getListFromInfoOrDefault(map[string]string{
|
||||
"foo": ",,",
|
||||
}, "foo", []string{"default"}))
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package swagger
|
||||
|
||||
import "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
|
||||
func fillAllStructs(api *spec.ApiSpec) {
|
||||
var (
|
||||
tps []spec.Type
|
||||
structTypes = make(map[string]spec.DefineStruct)
|
||||
groups []spec.Group
|
||||
)
|
||||
for _, tp := range api.Types {
|
||||
structTypes[tp.Name()] = tp.(spec.DefineStruct)
|
||||
}
|
||||
|
||||
for _, tp := range api.Types {
|
||||
filledTP := fillStruct("", tp, structTypes)
|
||||
tps = append(tps, filledTP)
|
||||
structTypes[filledTP.Name()] = filledTP.(spec.DefineStruct)
|
||||
}
|
||||
|
||||
for _, group := range api.Service.Groups {
|
||||
routes := make([]spec.Route, 0, len(group.Routes))
|
||||
for _, route := range group.Routes {
|
||||
route.RequestType = fillStruct("", route.RequestType, structTypes)
|
||||
route.ResponseType = fillStruct("", route.ResponseType, structTypes)
|
||||
routes = append(routes, route)
|
||||
}
|
||||
group.Routes = routes
|
||||
groups = append(groups, group)
|
||||
}
|
||||
api.Service.Groups = groups
|
||||
api.Types = tps
|
||||
}
|
||||
|
||||
func fillStruct(parent string, tp spec.Type, allTypes map[string]spec.DefineStruct) spec.Type {
|
||||
switch val := tp.(type) {
|
||||
case spec.DefineStruct:
|
||||
var members []spec.Member
|
||||
for _, member := range val.Members {
|
||||
switch memberType := member.Type.(type) {
|
||||
case spec.PointerType:
|
||||
member.Type = spec.PointerType{
|
||||
RawName: memberType.RawName,
|
||||
Type: fillStruct(val.Name(), memberType.Type, allTypes),
|
||||
}
|
||||
case spec.ArrayType:
|
||||
member.Type = spec.ArrayType{
|
||||
RawName: memberType.RawName,
|
||||
Value: fillStruct(val.Name(), memberType.Value, allTypes),
|
||||
}
|
||||
case spec.MapType:
|
||||
member.Type = spec.MapType{
|
||||
RawName: memberType.RawName,
|
||||
Key: memberType.Key,
|
||||
Value: fillStruct(val.Name(), memberType.Value, allTypes),
|
||||
}
|
||||
case spec.DefineStruct:
|
||||
if parent != memberType.Name() { // avoid recursive struct
|
||||
if st, ok := allTypes[memberType.Name()]; ok {
|
||||
member.Type = fillStruct("", st, allTypes)
|
||||
}
|
||||
}
|
||||
case spec.NestedStruct:
|
||||
member.Type = fillStruct("", member.Type, allTypes)
|
||||
}
|
||||
members = append(members, member)
|
||||
}
|
||||
if len(members) == 0 {
|
||||
st, ok := allTypes[val.RawName]
|
||||
if ok {
|
||||
members = st.Members
|
||||
}
|
||||
}
|
||||
val.Members = members
|
||||
return val
|
||||
case spec.NestedStruct:
|
||||
members := make([]spec.Member, 0, len(val.Members))
|
||||
for _, member := range val.Members {
|
||||
switch memberType := member.Type.(type) {
|
||||
case spec.PointerType:
|
||||
member.Type = spec.PointerType{
|
||||
RawName: memberType.RawName,
|
||||
Type: fillStruct(val.Name(), memberType.Type, allTypes),
|
||||
}
|
||||
case spec.ArrayType:
|
||||
member.Type = spec.ArrayType{
|
||||
RawName: memberType.RawName,
|
||||
Value: fillStruct(val.Name(), memberType.Value, allTypes),
|
||||
}
|
||||
case spec.MapType:
|
||||
member.Type = spec.MapType{
|
||||
RawName: memberType.RawName,
|
||||
Key: memberType.Key,
|
||||
Value: fillStruct(val.Name(), memberType.Value, allTypes),
|
||||
}
|
||||
case spec.DefineStruct:
|
||||
if parent != memberType.Name() { // avoid recursive struct
|
||||
if st, ok := allTypes[memberType.Name()]; ok {
|
||||
member.Type = fillStruct("", st, allTypes)
|
||||
}
|
||||
}
|
||||
case spec.NestedStruct:
|
||||
if parent != memberType.Name() {
|
||||
if st, ok := allTypes[memberType.Name()]; ok {
|
||||
member.Type = fillStruct("", st, allTypes)
|
||||
}
|
||||
}
|
||||
}
|
||||
members = append(members, member)
|
||||
}
|
||||
if len(members) == 0 {
|
||||
st, ok := allTypes[val.RawName]
|
||||
if ok {
|
||||
members = st.Members
|
||||
}
|
||||
}
|
||||
val.Members = members
|
||||
return val
|
||||
case spec.PointerType:
|
||||
return spec.PointerType{
|
||||
RawName: val.RawName,
|
||||
Type: fillStruct(parent, val.Type, allTypes),
|
||||
}
|
||||
case spec.ArrayType:
|
||||
return spec.ArrayType{
|
||||
RawName: val.RawName,
|
||||
Value: fillStruct(parent, val.Value, allTypes),
|
||||
}
|
||||
case spec.MapType:
|
||||
return spec.MapType{
|
||||
RawName: val.RawName,
|
||||
Key: val.Key,
|
||||
Value: fillStruct(parent, val.Value, allTypes),
|
||||
}
|
||||
default:
|
||||
return tp
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go-doc/internal/util"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/pkg/parser/api/parser"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// VarStringAPI specifies the API filename.
|
||||
VarStringAPI string
|
||||
|
||||
// VarStringDir specifies the directory to generate swagger file.
|
||||
VarStringDir string
|
||||
|
||||
// VarStringFilename specifies the generated swagger file name without the extension.
|
||||
VarStringFilename string
|
||||
|
||||
// VarBoolYaml specifies whether to generate a YAML file.
|
||||
VarBoolYaml bool
|
||||
)
|
||||
|
||||
func Command(_ *cobra.Command, _ []string) error {
|
||||
if len(VarStringAPI) == 0 {
|
||||
return errors.New("missing -api")
|
||||
}
|
||||
|
||||
if len(VarStringDir) == 0 {
|
||||
return errors.New("missing -dir")
|
||||
}
|
||||
|
||||
api, err := parser.Parse(VarStringAPI, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fillAllStructs(api)
|
||||
|
||||
if err := api.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
swagger, err := spec2Swagger(api)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.MarshalIndent(swagger, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = util.MkdirIfNotExist(VarStringDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := VarStringFilename
|
||||
if filename == "" {
|
||||
base := filepath.Base(VarStringAPI)
|
||||
filename = strings.TrimSuffix(base, filepath.Ext(base))
|
||||
}
|
||||
|
||||
if VarBoolYaml {
|
||||
filePath := filepath.Join(VarStringDir, filename+".yaml")
|
||||
|
||||
var jsonObj interface{}
|
||||
if err := yaml.Unmarshal(data, &jsonObj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(jsonObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, data, 0644)
|
||||
}
|
||||
|
||||
// generate json swagger file
|
||||
filePath := filepath.Join(VarStringDir, filename+".json")
|
||||
return os.WriteFile(filePath, data, 0644)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package swagger
|
||||
|
||||
const (
|
||||
tagHeader = "header"
|
||||
tagPath = "path"
|
||||
tagForm = "form"
|
||||
tagJson = "json"
|
||||
defFlag = "default="
|
||||
enumFlag = "options="
|
||||
rangeFlag = "range="
|
||||
exampleFlag = "example="
|
||||
optionalFlag = "optional"
|
||||
|
||||
paramsInHeader = "header"
|
||||
paramsInPath = "path"
|
||||
paramsInQuery = "query"
|
||||
paramsInBody = "body"
|
||||
paramsInForm = "formData"
|
||||
|
||||
swaggerTypeInteger = "integer"
|
||||
swaggerTypeNumber = "number"
|
||||
swaggerTypeString = "string"
|
||||
swaggerTypeBoolean = "boolean"
|
||||
swaggerTypeArray = "array"
|
||||
swaggerTypeObject = "object"
|
||||
|
||||
swaggerVersion = "2.0"
|
||||
applicationJson = "application/json"
|
||||
applicationForm = "application/x-www-form-urlencoded"
|
||||
schemeHttps = "https"
|
||||
defaultBasePath = "/"
|
||||
)
|
||||
|
||||
const (
|
||||
propertyKeyUseDefinitions = "useDefinitions"
|
||||
propertyKeyExternalDocsDescription = "externalDocsDescription"
|
||||
propertyKeyExternalDocsURL = "externalDocsURL"
|
||||
propertyKeyTitle = "title"
|
||||
propertyKeyTermsOfService = "termsOfService"
|
||||
propertyKeyDescription = "description"
|
||||
propertyKeyVersion = "version"
|
||||
propertyKeyContactName = "contactName"
|
||||
propertyKeyContactURL = "contactURL"
|
||||
propertyKeyContactEmail = "contactEmail"
|
||||
propertyKeyLicenseName = "licenseName"
|
||||
propertyKeyLicenseURL = "licenseURL"
|
||||
propertyKeyProduces = "produces"
|
||||
propertyKeyConsumes = "consumes"
|
||||
propertyKeySchemes = "schemes"
|
||||
propertyKeyTags = "tags"
|
||||
propertyKeySummary = "summary"
|
||||
propertyKeyGroup = "group"
|
||||
propertyKeyOperationId = "operationId"
|
||||
propertyKeyDeprecated = "deprecated"
|
||||
propertyKeyPrefix = "prefix"
|
||||
propertyKeyAuthType = "authType"
|
||||
propertyKeyHost = "host"
|
||||
propertyKeyBasePath = "basePath"
|
||||
propertyKeyWrapCodeMsg = "wrapCodeMsg"
|
||||
propertyKeyBizCodeEnumDescription = "bizCodeEnumDescription"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultValueOfPropertyUseDefinition = false
|
||||
)
|
|
@ -0,0 +1,25 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func consumesFromTypeOrDef(ctx Context, method string, tp spec.Type) []string {
|
||||
if strings.EqualFold(method, http.MethodGet) {
|
||||
return []string{}
|
||||
}
|
||||
if tp == nil {
|
||||
return []string{}
|
||||
}
|
||||
structType, ok := tp.(spec.DefineStruct)
|
||||
if !ok {
|
||||
return []string{}
|
||||
}
|
||||
if typeContainsTag(ctx, structType, tagJson) {
|
||||
return []string{applicationJson}
|
||||
}
|
||||
return []string{applicationForm}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func TestConsumesFromTypeOrDef(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
tp spec.Type
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "GET method with nil type",
|
||||
method: http.MethodGet,
|
||||
tp: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "post nil",
|
||||
method: http.MethodPost,
|
||||
tp: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "json tag",
|
||||
method: http.MethodPost,
|
||||
tp: spec.DefineStruct{
|
||||
Members: []spec.Member{
|
||||
{
|
||||
Tag: `json:"example"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{applicationJson},
|
||||
},
|
||||
{
|
||||
name: "form tag",
|
||||
method: http.MethodPost,
|
||||
tp: spec.DefineStruct{
|
||||
Members: []spec.Member{
|
||||
{
|
||||
Tag: `form:"example"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{applicationForm},
|
||||
},
|
||||
{
|
||||
name: "Non struct type",
|
||||
method: http.MethodPost,
|
||||
tp: spec.ArrayType{},
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := consumesFromTypeOrDef(testingContext(t), tt.method, tt.tp)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
UseDefinitions bool
|
||||
WrapCodeMsg bool
|
||||
BizCodeEnumDescription string
|
||||
}
|
||||
|
||||
func testingContext(_ *testing.T) Context {
|
||||
return Context{}
|
||||
}
|
||||
|
||||
func contextFromApi(info spec.Info) Context {
|
||||
if len(info.Properties) == 0 {
|
||||
return Context{}
|
||||
}
|
||||
return Context{
|
||||
UseDefinitions: getBoolFromKVOrDefault(info.Properties, propertyKeyUseDefinitions, defaultValueOfPropertyUseDefinition),
|
||||
WrapCodeMsg: getBoolFromKVOrDefault(info.Properties, propertyKeyWrapCodeMsg, false),
|
||||
BizCodeEnumDescription: getStringFromKVOrDefault(info.Properties, propertyKeyBizCodeEnumDescription, "business code"),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/spec"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func definitionsFromTypes(ctx Context, types []apiSpec.Type) spec.Definitions {
|
||||
if !ctx.UseDefinitions {
|
||||
return nil
|
||||
}
|
||||
definitions := make(spec.Definitions)
|
||||
for _, tp := range types {
|
||||
typeName := tp.Name()
|
||||
definitions[typeName] = schemaFromType(ctx, tp)
|
||||
}
|
||||
return definitions
|
||||
}
|
||||
|
||||
func schemaFromType(ctx Context, tp apiSpec.Type) spec.Schema {
|
||||
p, r := propertiesFromType(ctx, tp)
|
||||
props := spec.SchemaProps{
|
||||
Type: typeFromGoType(ctx, tp),
|
||||
Properties: p,
|
||||
AdditionalProperties: mapFromGoType(ctx, tp),
|
||||
Items: itemsFromGoType(ctx, tp),
|
||||
Required: r,
|
||||
}
|
||||
return spec.Schema{
|
||||
SchemaProps: props,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go-doc/internal/util"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func rangeValueFromOptions(options []string) (minimum *float64, maximum *float64, exclusiveMinimum bool, exclusiveMaximum bool) {
|
||||
if len(options) == 0 {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
for _, option := range options {
|
||||
if strings.HasPrefix(option, rangeFlag) {
|
||||
val := option[6:]
|
||||
start, end := val[0], val[len(val)-1]
|
||||
if start != '[' && start != '(' {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
if end != ']' && end != ')' {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
exclusiveMinimum = start == '('
|
||||
exclusiveMaximum = end == ')'
|
||||
|
||||
content := val[1 : len(val)-1]
|
||||
idxColon := strings.Index(content, ":")
|
||||
if idxColon < 0 {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
var (
|
||||
minStr, maxStr string
|
||||
minVal, maxVal *float64
|
||||
)
|
||||
minStr = util.TrimWhiteSpace(content[:idxColon])
|
||||
if len(val) >= idxColon+1 {
|
||||
maxStr = util.TrimWhiteSpace(content[idxColon+1:])
|
||||
}
|
||||
|
||||
if len(minStr) > 0 {
|
||||
min, err := strconv.ParseFloat(minStr, 64)
|
||||
if err != nil {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
minVal = &min
|
||||
}
|
||||
|
||||
if len(maxStr) > 0 {
|
||||
max, err := strconv.ParseFloat(maxStr, 64)
|
||||
if err != nil {
|
||||
return nil, nil, false, false
|
||||
}
|
||||
maxVal = &max
|
||||
}
|
||||
|
||||
return minVal, maxVal, exclusiveMinimum, exclusiveMaximum
|
||||
}
|
||||
}
|
||||
return nil, nil, false, false
|
||||
}
|
||||
|
||||
func enumsValueFromOptions(options []string) []any {
|
||||
if len(options) == 0 {
|
||||
return []any{}
|
||||
}
|
||||
for _, option := range options {
|
||||
if strings.HasPrefix(option, enumFlag) {
|
||||
val := option[8:]
|
||||
fields := util.FieldsAndTrimSpace(val, func(r rune) bool {
|
||||
return r == '|'
|
||||
})
|
||||
var resp = make([]any, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
resp = append(resp, field)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
}
|
||||
return []any{}
|
||||
}
|
||||
|
||||
func defValueFromOptions(ctx Context, options []string, apiType spec.Type) any {
|
||||
tp := sampleTypeFromGoType(ctx, apiType)
|
||||
return valueFromOptions(ctx, options, defFlag, tp)
|
||||
}
|
||||
|
||||
func exampleValueFromOptions(ctx Context, options []string, apiType spec.Type) any {
|
||||
tp := sampleTypeFromGoType(ctx, apiType)
|
||||
val := valueFromOptions(ctx, options, exampleFlag, tp)
|
||||
if val != nil {
|
||||
return val
|
||||
}
|
||||
return defValueFromOptions(ctx, options, apiType)
|
||||
}
|
||||
|
||||
func valueFromOptions(_ Context, options []string, key string, tp string) any {
|
||||
if len(options) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, option := range options {
|
||||
if strings.HasPrefix(option, key) {
|
||||
s := option[len(key):]
|
||||
switch tp {
|
||||
case swaggerTypeInteger:
|
||||
val, _ := strconv.ParseInt(s, 10, 64)
|
||||
return val
|
||||
case swaggerTypeBoolean:
|
||||
val, _ := strconv.ParseBool(s)
|
||||
return val
|
||||
case swaggerTypeNumber:
|
||||
val, _ := strconv.ParseFloat(s, 64)
|
||||
return val
|
||||
case swaggerTypeArray:
|
||||
return s
|
||||
case swaggerTypeString:
|
||||
return s
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func TestRangeValueFromOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
expectedMin *float64
|
||||
expectedMax *float64
|
||||
expectedExclMin bool
|
||||
expectedExclMax bool
|
||||
}{
|
||||
{
|
||||
name: "Valid range with inclusive bounds",
|
||||
options: []string{"range=[1.0:10.0]"},
|
||||
expectedMin: floatPtr(1.0),
|
||||
expectedMax: floatPtr(10.0),
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: false,
|
||||
},
|
||||
{
|
||||
name: "Valid range with exclusive bounds",
|
||||
options: []string{"range=(1.0:10.0)"},
|
||||
expectedMin: floatPtr(1.0),
|
||||
expectedMax: floatPtr(10.0),
|
||||
expectedExclMin: true,
|
||||
expectedExclMax: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid range format",
|
||||
options: []string{"range=1.0:10.0"},
|
||||
expectedMin: nil,
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid range start",
|
||||
options: []string{"range=[a:1.0)"},
|
||||
expectedMin: nil,
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: false,
|
||||
},
|
||||
{
|
||||
name: "Missing range end",
|
||||
options: []string{"range=[1.0:)"},
|
||||
expectedMin: floatPtr(1.0),
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: true,
|
||||
},
|
||||
{
|
||||
name: "Missing range start and end",
|
||||
options: []string{"range=[:)"},
|
||||
expectedMin: nil,
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: true,
|
||||
},
|
||||
{
|
||||
name: "Missing range start",
|
||||
options: []string{"range=[:1.0)"},
|
||||
expectedMin: nil,
|
||||
expectedMax: floatPtr(1.0),
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid range end",
|
||||
options: []string{"range=[1.0:b)"},
|
||||
expectedMin: nil,
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: false,
|
||||
},
|
||||
{
|
||||
name: "Empty options",
|
||||
options: []string{},
|
||||
expectedMin: nil,
|
||||
expectedMax: nil,
|
||||
expectedExclMin: false,
|
||||
expectedExclMax: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
min, max, exclMin, exclMax := rangeValueFromOptions(tt.options)
|
||||
assert.Equal(t, tt.expectedMin, min)
|
||||
assert.Equal(t, tt.expectedMax, max)
|
||||
assert.Equal(t, tt.expectedExclMin, exclMin)
|
||||
assert.Equal(t, tt.expectedExclMax, exclMax)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumsValueFromOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
expected []any
|
||||
}{
|
||||
{
|
||||
name: "Valid enums",
|
||||
options: []string{"options=a|b|c"},
|
||||
expected: []any{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "Empty enums",
|
||||
options: []string{"options="},
|
||||
expected: []any{},
|
||||
},
|
||||
{
|
||||
name: "No enum option",
|
||||
options: []string{},
|
||||
expected: []any{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := enumsValueFromOptions(tt.options)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefValueFromOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
apiType spec.Type
|
||||
expected any
|
||||
}{
|
||||
{
|
||||
name: "Default integer value",
|
||||
options: []string{"default=42"},
|
||||
apiType: spec.PrimitiveType{RawName: "int"},
|
||||
expected: int64(42),
|
||||
},
|
||||
{
|
||||
name: "Default string value",
|
||||
options: []string{"default=hello"},
|
||||
apiType: spec.PrimitiveType{RawName: "string"},
|
||||
expected: "hello",
|
||||
},
|
||||
{
|
||||
name: "No default value",
|
||||
options: []string{},
|
||||
apiType: spec.PrimitiveType{RawName: "string"},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := defValueFromOptions(testingContext(t), tt.options, tt.apiType)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExampleValueFromOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
apiType spec.Type
|
||||
expected any
|
||||
}{
|
||||
{
|
||||
name: "Example value present",
|
||||
options: []string{"example=3.14"},
|
||||
apiType: spec.PrimitiveType{RawName: "float"},
|
||||
expected: 3.14,
|
||||
},
|
||||
{
|
||||
name: "Fallback to default value",
|
||||
options: []string{"default=42"},
|
||||
apiType: spec.PrimitiveType{RawName: "int"},
|
||||
expected: int64(42),
|
||||
},
|
||||
{
|
||||
name: "Fallback to default value",
|
||||
options: []string{"default="},
|
||||
apiType: spec.PrimitiveType{RawName: "int"},
|
||||
expected: int64(0),
|
||||
},
|
||||
{
|
||||
name: "No example or default value",
|
||||
options: []string{},
|
||||
apiType: spec.PrimitiveType{RawName: "string"},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
exampleValueFromOptions(testingContext(t), tt.options, tt.apiType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueFromOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options []string
|
||||
key string
|
||||
tp string
|
||||
expected any
|
||||
}{
|
||||
{
|
||||
name: "Integer value",
|
||||
options: []string{"default=42"},
|
||||
key: "default=",
|
||||
tp: "integer",
|
||||
expected: int64(42),
|
||||
},
|
||||
{
|
||||
name: "Boolean value",
|
||||
options: []string{"default=true"},
|
||||
key: "default=",
|
||||
tp: "boolean",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Number value",
|
||||
options: []string{"default=1.1"},
|
||||
key: "default=",
|
||||
tp: "number",
|
||||
expected: 1.1,
|
||||
},
|
||||
{
|
||||
name: "No matching key",
|
||||
options: []string{"example=42"},
|
||||
key: "default=",
|
||||
tp: "integer",
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := valueFromOptions(testingContext(t), tt.options, tt.key, tt.tp)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func floatPtr(f float64) *float64 {
|
||||
return &f
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func isPostJson(ctx Context, method string, tp apiSpec.Type) (string, bool) {
|
||||
if !strings.EqualFold(method, http.MethodPost) {
|
||||
return "", false
|
||||
}
|
||||
structType, ok := tp.(apiSpec.DefineStruct)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
var isPostJson bool
|
||||
rangeMemberAndDo(ctx, structType, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
|
||||
jsonTag, _ := tag.Get(tagJson)
|
||||
if !isPostJson {
|
||||
isPostJson = jsonTag != nil
|
||||
}
|
||||
})
|
||||
return structType.RawName, isPostJson
|
||||
}
|
||||
|
||||
func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Parameter {
|
||||
if tp == nil {
|
||||
return []spec.Parameter{}
|
||||
}
|
||||
structType, ok := tp.(apiSpec.DefineStruct)
|
||||
if !ok {
|
||||
return []spec.Parameter{}
|
||||
}
|
||||
|
||||
var (
|
||||
resp []spec.Parameter
|
||||
properties = map[string]spec.Schema{}
|
||||
requiredFields []string
|
||||
)
|
||||
rangeMemberAndDo(ctx, structType, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
|
||||
headerTag, _ := tag.Get(tagHeader)
|
||||
hasHeader := headerTag != nil
|
||||
|
||||
pathParameterTag, _ := tag.Get(tagPath)
|
||||
hasPathParameter := pathParameterTag != nil
|
||||
|
||||
formTag, _ := tag.Get(tagForm)
|
||||
hasForm := formTag != nil
|
||||
|
||||
jsonTag, _ := tag.Get(tagJson)
|
||||
hasJson := jsonTag != nil
|
||||
if hasHeader {
|
||||
minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(headerTag.Options)
|
||||
resp = append(resp, spec.Parameter{
|
||||
CommonValidations: spec.CommonValidations{
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enumsValueFromOptions(headerTag.Options),
|
||||
},
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(ctx, member.Type),
|
||||
Default: defValueFromOptions(ctx, headerTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(ctx, member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
In: paramsInHeader,
|
||||
Name: headerTag.Name,
|
||||
Description: formatComment(member.Comment),
|
||||
Required: required,
|
||||
},
|
||||
})
|
||||
}
|
||||
if hasPathParameter {
|
||||
minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(pathParameterTag.Options)
|
||||
resp = append(resp, spec.Parameter{
|
||||
CommonValidations: spec.CommonValidations{
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enumsValueFromOptions(pathParameterTag.Options),
|
||||
},
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(ctx, member.Type),
|
||||
Default: defValueFromOptions(ctx, pathParameterTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(ctx, member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
In: paramsInPath,
|
||||
Name: pathParameterTag.Name,
|
||||
Description: formatComment(member.Comment),
|
||||
Required: required,
|
||||
},
|
||||
})
|
||||
}
|
||||
if hasForm {
|
||||
minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(formTag.Options)
|
||||
if strings.EqualFold(method, http.MethodGet) {
|
||||
resp = append(resp, spec.Parameter{
|
||||
CommonValidations: spec.CommonValidations{
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enumsValueFromOptions(formTag.Options),
|
||||
},
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(ctx, member.Type),
|
||||
Default: defValueFromOptions(ctx, formTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(ctx, member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
In: paramsInQuery,
|
||||
Name: formTag.Name,
|
||||
Description: formatComment(member.Comment),
|
||||
Required: required,
|
||||
AllowEmptyValue: !required,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
resp = append(resp, spec.Parameter{
|
||||
CommonValidations: spec.CommonValidations{
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enumsValueFromOptions(formTag.Options),
|
||||
},
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(ctx, member.Type),
|
||||
Default: defValueFromOptions(ctx, formTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(ctx, member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
In: paramsInForm,
|
||||
Name: formTag.Name,
|
||||
Description: formatComment(member.Comment),
|
||||
Required: required,
|
||||
AllowEmptyValue: !required,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
if hasJson {
|
||||
minimum, maximum, exclusiveMinimum, exclusiveMaximum := rangeValueFromOptions(jsonTag.Options)
|
||||
if required {
|
||||
requiredFields = append(requiredFields, jsonTag.Name)
|
||||
}
|
||||
var schema = spec.Schema{
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: exampleValueFromOptions(ctx, jsonTag.Options, member.Type),
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: formatComment(member.Comment),
|
||||
Type: typeFromGoType(ctx, member.Type),
|
||||
Default: defValueFromOptions(ctx, jsonTag.Options, member.Type),
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enumsValueFromOptions(jsonTag.Options),
|
||||
AdditionalProperties: mapFromGoType(ctx, member.Type),
|
||||
},
|
||||
}
|
||||
switch sampleTypeFromGoType(ctx, member.Type) {
|
||||
case swaggerTypeArray:
|
||||
schema.Items = itemsFromGoType(ctx, member.Type)
|
||||
case swaggerTypeObject:
|
||||
p, r := propertiesFromType(ctx, member.Type)
|
||||
schema.Properties = p
|
||||
schema.Required = r
|
||||
}
|
||||
properties[jsonTag.Name] = schema
|
||||
}
|
||||
})
|
||||
if len(properties) > 0 {
|
||||
if ctx.UseDefinitions {
|
||||
structName, ok := isPostJson(ctx, method, tp)
|
||||
if ok {
|
||||
resp = append(resp, spec.Parameter{
|
||||
ParamProps: spec.ParamProps{
|
||||
In: paramsInBody,
|
||||
Name: paramsInBody,
|
||||
Required: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Ref: spec.MustCreateRef(getRefName(structName)),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
resp = append(resp, spec.Parameter{
|
||||
ParamProps: spec.ParamProps{
|
||||
In: paramsInBody,
|
||||
Name: paramsInBody,
|
||||
Required: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: typeFromGoType(ctx, structType),
|
||||
Properties: properties,
|
||||
Required: requiredFields,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func TestIsPostJson(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
hasJson bool
|
||||
expected bool
|
||||
}{
|
||||
{"POST with JSON", http.MethodPost, true, true},
|
||||
{"POST without JSON", http.MethodPost, false, false},
|
||||
{"GET with JSON", http.MethodGet, true, false},
|
||||
{"PUT with JSON", http.MethodPut, true, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testStruct := createTestStruct("TestStruct", tt.hasJson)
|
||||
_, result := isPostJson(testingContext(t), tt.method, testStruct)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParametersFromType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
useDefinitions bool
|
||||
hasJson bool
|
||||
expectedCount int
|
||||
expectedBody bool
|
||||
}{
|
||||
{"POST JSON with definitions", http.MethodPost, true, true, 1, true},
|
||||
{"POST JSON without definitions", http.MethodPost, false, true, 1, true},
|
||||
{"GET with form", http.MethodGet, false, false, 1, false},
|
||||
{"POST with form", http.MethodPost, false, false, 1, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := Context{UseDefinitions: tt.useDefinitions}
|
||||
testStruct := createTestStruct("TestStruct", tt.hasJson)
|
||||
params := parametersFromType(ctx, tt.method, testStruct)
|
||||
|
||||
assert.Equal(t, tt.expectedCount, len(params))
|
||||
if tt.expectedBody {
|
||||
assert.Equal(t, paramsInBody, params[0].In)
|
||||
} else if len(params) > 0 {
|
||||
assert.NotEqual(t, paramsInBody, params[0].In)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParametersFromType_EdgeCases(t *testing.T) {
|
||||
ctx := testingContext(t)
|
||||
|
||||
params := parametersFromType(ctx, http.MethodPost, nil)
|
||||
assert.Empty(t, params)
|
||||
|
||||
primitiveType := apiSpec.PrimitiveType{RawName: "string"}
|
||||
params = parametersFromType(ctx, http.MethodPost, primitiveType)
|
||||
assert.Empty(t, params)
|
||||
}
|
||||
|
||||
func createTestStruct(name string, hasJson bool) apiSpec.DefineStruct {
|
||||
tag := `form:"username"`
|
||||
if hasJson {
|
||||
tag = `json:"username"`
|
||||
}
|
||||
|
||||
return apiSpec.DefineStruct{
|
||||
RawName: name,
|
||||
Members: []apiSpec.Member{
|
||||
{
|
||||
Name: "Username",
|
||||
Type: apiSpec.PrimitiveType{RawName: "string"},
|
||||
Tag: tag,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
"go-doc/internal/util"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func spec2Paths(ctx Context, srv apiSpec.Service) *spec.Paths {
|
||||
paths := &spec.Paths{
|
||||
Paths: make(map[string]spec.PathItem),
|
||||
}
|
||||
for _, group := range srv.Groups {
|
||||
prefix := path.Clean(strings.TrimPrefix(group.GetAnnotation(propertyKeyPrefix), "/"))
|
||||
for _, route := range group.Routes {
|
||||
routPath := pathVariable2SwaggerVariable(ctx, route.Path)
|
||||
if len(prefix) > 0 && prefix != "." {
|
||||
routPath = "/" + path.Clean(prefix) + routPath
|
||||
}
|
||||
pathItem := spec2Path(ctx, group, route)
|
||||
existPathItem, ok := paths.Paths[routPath]
|
||||
if !ok {
|
||||
paths.Paths[routPath] = pathItem
|
||||
} else {
|
||||
paths.Paths[routPath] = mergePathItem(existPathItem, pathItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func mergePathItem(old, new spec.PathItem) spec.PathItem {
|
||||
if new.Get != nil {
|
||||
old.Get = new.Get
|
||||
}
|
||||
if new.Put != nil {
|
||||
old.Put = new.Put
|
||||
}
|
||||
if new.Post != nil {
|
||||
old.Post = new.Post
|
||||
}
|
||||
if new.Delete != nil {
|
||||
old.Delete = new.Delete
|
||||
}
|
||||
if new.Options != nil {
|
||||
old.Options = new.Options
|
||||
}
|
||||
if new.Head != nil {
|
||||
old.Head = new.Head
|
||||
}
|
||||
if new.Patch != nil {
|
||||
old.Patch = new.Patch
|
||||
}
|
||||
if new.Parameters != nil {
|
||||
old.Parameters = new.Parameters
|
||||
}
|
||||
return old
|
||||
}
|
||||
|
||||
func spec2Path(ctx Context, group apiSpec.Group, route apiSpec.Route) spec.PathItem {
|
||||
authType := getStringFromKVOrDefault(group.Annotation.Properties, propertyKeyAuthType, "")
|
||||
var security []map[string][]string
|
||||
if len(authType) > 0 {
|
||||
security = []map[string][]string{
|
||||
{
|
||||
authType: []string{},
|
||||
},
|
||||
}
|
||||
}
|
||||
groupName := getStringFromKVOrDefault(group.Annotation.Properties, propertyKeyGroup, "")
|
||||
operationId := route.Handler
|
||||
if len(groupName) > 0 {
|
||||
operationId = util.From(groupName + "_" + route.Handler).ToCamel()
|
||||
}
|
||||
operationId = util.From(operationId).Untitle()
|
||||
op := &spec.Operation{
|
||||
OperationProps: spec.OperationProps{
|
||||
Description: getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeyDescription, ""),
|
||||
Consumes: consumesFromTypeOrDef(ctx, route.Method, route.RequestType),
|
||||
Produces: getListFromInfoOrDefault(route.AtDoc.Properties, propertyKeyProduces, []string{applicationJson}),
|
||||
Schemes: getListFromInfoOrDefault(route.AtDoc.Properties, propertyKeySchemes, []string{schemeHttps}),
|
||||
Tags: getListFromInfoOrDefault(group.Annotation.Properties, propertyKeyTags, getListFromInfoOrDefault(group.Annotation.Properties, propertyKeySummary, []string{})),
|
||||
Summary: getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeySummary, getFirstUsableString(route.AtDoc.Text, route.Handler)),
|
||||
ID: operationId,
|
||||
Deprecated: getBoolFromKVOrDefault(route.AtDoc.Properties, propertyKeyDeprecated, false),
|
||||
Parameters: parametersFromType(ctx, route.Method, route.RequestType),
|
||||
Security: security,
|
||||
Responses: jsonResponseFromType(ctx, route.AtDoc, route.ResponseType),
|
||||
},
|
||||
}
|
||||
externalDocsDescription := getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeyExternalDocsDescription, "")
|
||||
externalDocsURL := getStringFromKVOrDefault(route.AtDoc.Properties, propertyKeyExternalDocsURL, "")
|
||||
if len(externalDocsDescription) > 0 || len(externalDocsURL) > 0 {
|
||||
op.ExternalDocs = &spec.ExternalDocumentation{
|
||||
Description: externalDocsDescription,
|
||||
URL: externalDocsURL,
|
||||
}
|
||||
|
||||
}
|
||||
item := spec.PathItem{}
|
||||
switch strings.ToUpper(route.Method) {
|
||||
case http.MethodGet:
|
||||
item.Get = op
|
||||
case http.MethodHead:
|
||||
item.Head = op
|
||||
case http.MethodPost:
|
||||
item.Post = op
|
||||
case http.MethodPut:
|
||||
item.Put = op
|
||||
case http.MethodPatch:
|
||||
item.Patch = op
|
||||
case http.MethodDelete:
|
||||
item.Delete = op
|
||||
case http.MethodOptions:
|
||||
item.Options = op
|
||||
default: // [http.MethodConnect,http.MethodTrace] not supported
|
||||
}
|
||||
return item
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/spec"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func propertiesFromType(ctx Context, tp apiSpec.Type) (spec.SchemaProperties, []string) {
|
||||
var (
|
||||
properties = map[string]spec.Schema{}
|
||||
requiredFields []string
|
||||
)
|
||||
switch val := tp.(type) {
|
||||
case apiSpec.PointerType:
|
||||
return propertiesFromType(ctx, val.Type)
|
||||
case apiSpec.ArrayType:
|
||||
return propertiesFromType(ctx, val.Value)
|
||||
case apiSpec.DefineStruct, apiSpec.NestedStruct:
|
||||
rangeMemberAndDo(ctx, val, func(tag *apiSpec.Tags, required bool, member apiSpec.Member) {
|
||||
var (
|
||||
jsonTagString = member.Name
|
||||
minimum, maximum *float64
|
||||
exclusiveMinimum, exclusiveMaximum bool
|
||||
example, defaultValue any
|
||||
enum []any
|
||||
)
|
||||
pathTag, _ := tag.Get(tagPath)
|
||||
if pathTag != nil {
|
||||
return
|
||||
}
|
||||
formTag, _ := tag.Get(tagForm)
|
||||
if formTag != nil {
|
||||
return
|
||||
}
|
||||
headerTag, _ := tag.Get(tagHeader)
|
||||
if headerTag != nil {
|
||||
return
|
||||
}
|
||||
|
||||
jsonTag, _ := tag.Get(tagJson)
|
||||
if jsonTag != nil {
|
||||
jsonTagString = jsonTag.Name
|
||||
minimum, maximum, exclusiveMinimum, exclusiveMaximum = rangeValueFromOptions(jsonTag.Options)
|
||||
example = exampleValueFromOptions(ctx, jsonTag.Options, member.Type)
|
||||
defaultValue = defValueFromOptions(ctx, jsonTag.Options, member.Type)
|
||||
enum = enumsValueFromOptions(jsonTag.Options)
|
||||
}
|
||||
|
||||
if required {
|
||||
requiredFields = append(requiredFields, jsonTagString)
|
||||
}
|
||||
|
||||
schema := spec.Schema{
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: example,
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: formatComment(member.Comment),
|
||||
Type: typeFromGoType(ctx, member.Type),
|
||||
Default: defaultValue,
|
||||
Maximum: maximum,
|
||||
ExclusiveMaximum: exclusiveMaximum,
|
||||
Minimum: minimum,
|
||||
ExclusiveMinimum: exclusiveMinimum,
|
||||
Enum: enum,
|
||||
AdditionalProperties: mapFromGoType(ctx, member.Type),
|
||||
},
|
||||
}
|
||||
|
||||
switch sampleTypeFromGoType(ctx, member.Type) {
|
||||
case swaggerTypeArray:
|
||||
schema.Items = itemsFromGoType(ctx, member.Type)
|
||||
case swaggerTypeObject:
|
||||
p, r := propertiesFromType(ctx, member.Type)
|
||||
schema.Properties = p
|
||||
schema.Required = r
|
||||
}
|
||||
if ctx.UseDefinitions {
|
||||
structName, containsStruct := containsStruct(member.Type)
|
||||
if containsStruct {
|
||||
schema.SchemaProps.Ref = spec.MustCreateRef(getRefName(structName))
|
||||
}
|
||||
}
|
||||
|
||||
properties[jsonTagString] = schema
|
||||
})
|
||||
}
|
||||
|
||||
return properties, requiredFields
|
||||
}
|
||||
|
||||
func containsStruct(tp apiSpec.Type) (string, bool) {
|
||||
switch val := tp.(type) {
|
||||
case apiSpec.PointerType:
|
||||
return containsStruct(val.Type)
|
||||
case apiSpec.ArrayType:
|
||||
return containsStruct(val.Value)
|
||||
case apiSpec.DefineStruct:
|
||||
return val.RawName, true
|
||||
case apiSpec.MapType:
|
||||
return containsStruct(val.Value)
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func getRefName(typeName string) string {
|
||||
return "#/definitions/" + typeName
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func jsonResponseFromType(ctx Context, atDoc apiSpec.AtDoc, tp apiSpec.Type) *spec.Responses {
|
||||
if tp == nil {
|
||||
return &spec.Responses{
|
||||
ResponsesProps: spec.ResponsesProps{
|
||||
StatusCodeResponses: map[int]spec.Response{
|
||||
http.StatusOK: {
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Description: "",
|
||||
Schema: &spec.Schema{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
props := spec.SchemaProps{
|
||||
AdditionalProperties: mapFromGoType(ctx, tp),
|
||||
Items: itemsFromGoType(ctx, tp),
|
||||
}
|
||||
if ctx.UseDefinitions {
|
||||
structName, ok := containsStruct(tp)
|
||||
if ok {
|
||||
props.Ref = spec.MustCreateRef(getRefName(structName))
|
||||
return &spec.Responses{
|
||||
ResponsesProps: spec.ResponsesProps{
|
||||
StatusCodeResponses: map[int]spec.Response{
|
||||
http.StatusOK: {
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: wrapCodeMsgProps(ctx, props, atDoc),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p, _ := propertiesFromType(ctx, tp)
|
||||
props.Type = typeFromGoType(ctx, tp)
|
||||
props.Properties = p
|
||||
return &spec.Responses{
|
||||
ResponsesProps: spec.ResponsesProps{
|
||||
StatusCodeResponses: map[int]spec.Response{
|
||||
http.StatusOK: {
|
||||
ResponseProps: spec.ResponseProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: wrapCodeMsgProps(ctx, props, atDoc),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
apiSpec "github.com/zeromicro/go-zero/tools/goctl/api/spec"
|
||||
)
|
||||
|
||||
func spec2Swagger(api *apiSpec.ApiSpec) (*spec.Swagger, error) {
|
||||
ctx := contextFromApi(api.Info)
|
||||
extensions, info := specExtensions(api.Info)
|
||||
var securityDefinitions spec.SecurityDefinitions
|
||||
securityDefinitionsFromJson := getStringFromKVOrDefault(api.Info.Properties, "securityDefinitionsFromJson", `{}`)
|
||||
_ = json.Unmarshal([]byte(securityDefinitionsFromJson), &securityDefinitions)
|
||||
swagger := &spec.Swagger{
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: extensions,
|
||||
},
|
||||
SwaggerProps: spec.SwaggerProps{
|
||||
Definitions: definitionsFromTypes(ctx, api.Types),
|
||||
Consumes: getListFromInfoOrDefault(api.Info.Properties, propertyKeyConsumes, []string{applicationJson}),
|
||||
Produces: getListFromInfoOrDefault(api.Info.Properties, propertyKeyProduces, []string{applicationJson}),
|
||||
Schemes: getListFromInfoOrDefault(api.Info.Properties, propertyKeySchemes, []string{schemeHttps}),
|
||||
Swagger: swaggerVersion,
|
||||
Info: info,
|
||||
Host: getStringFromKVOrDefault(api.Info.Properties, propertyKeyHost, ""),
|
||||
BasePath: getStringFromKVOrDefault(api.Info.Properties, propertyKeyBasePath, defaultBasePath),
|
||||
Paths: spec2Paths(ctx, api.Service),
|
||||
SecurityDefinitions: securityDefinitions,
|
||||
},
|
||||
}
|
||||
|
||||
return swagger, nil
|
||||
}
|
||||
|
||||
func formatComment(comment string) string {
|
||||
s := strings.TrimPrefix(comment, "//")
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func sampleItemsFromGoType(ctx Context, tp apiSpec.Type) *spec.Items {
|
||||
val, ok := tp.(apiSpec.ArrayType)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item := val.Value
|
||||
switch item.(type) {
|
||||
case apiSpec.PrimitiveType:
|
||||
return &spec.Items{
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(ctx, item),
|
||||
},
|
||||
}
|
||||
case apiSpec.ArrayType:
|
||||
return &spec.Items{
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(ctx, item),
|
||||
Items: sampleItemsFromGoType(ctx, item),
|
||||
},
|
||||
}
|
||||
default: // unsupported type
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// itemsFromGoType returns the schema or array of the type, just for non json body parameters.
|
||||
func itemsFromGoType(ctx Context, tp apiSpec.Type) *spec.SchemaOrArray {
|
||||
array, ok := tp.(apiSpec.ArrayType)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return itemFromGoType(ctx, array.Value)
|
||||
}
|
||||
|
||||
func mapFromGoType(ctx Context, tp apiSpec.Type) *spec.SchemaOrBool {
|
||||
mapType, ok := tp.(apiSpec.MapType)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var schema = &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: typeFromGoType(ctx, mapType.Value),
|
||||
AdditionalProperties: mapFromGoType(ctx, mapType.Value),
|
||||
},
|
||||
}
|
||||
switch sampleTypeFromGoType(ctx, mapType.Value) {
|
||||
case swaggerTypeArray:
|
||||
schema.Items = itemsFromGoType(ctx, mapType.Value)
|
||||
case swaggerTypeObject:
|
||||
p, r := propertiesFromType(ctx, mapType.Value)
|
||||
schema.Properties = p
|
||||
schema.Required = r
|
||||
}
|
||||
return &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: schema,
|
||||
}
|
||||
}
|
||||
|
||||
// itemFromGoType returns the schema or array of the type, just for non json body parameters.
|
||||
func itemFromGoType(ctx Context, tp apiSpec.Type) *spec.SchemaOrArray {
|
||||
switch itemType := tp.(type) {
|
||||
case apiSpec.PrimitiveType:
|
||||
return &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: typeFromGoType(ctx, tp),
|
||||
},
|
||||
},
|
||||
}
|
||||
case apiSpec.DefineStruct, apiSpec.NestedStruct, apiSpec.MapType:
|
||||
properties, requiredFields := propertiesFromType(ctx, itemType)
|
||||
return &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: typeFromGoType(ctx, itemType),
|
||||
Items: itemsFromGoType(ctx, itemType),
|
||||
Properties: properties,
|
||||
Required: requiredFields,
|
||||
AdditionalProperties: mapFromGoType(ctx, itemType),
|
||||
},
|
||||
},
|
||||
}
|
||||
case apiSpec.PointerType:
|
||||
return itemFromGoType(ctx, itemType.Type)
|
||||
case apiSpec.ArrayType:
|
||||
return &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: typeFromGoType(ctx, itemType),
|
||||
Items: itemsFromGoType(ctx, itemType),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func typeFromGoType(ctx Context, tp apiSpec.Type) []string {
|
||||
switch val := tp.(type) {
|
||||
case apiSpec.PrimitiveType:
|
||||
res, ok := tpMapper[val.RawName]
|
||||
if ok {
|
||||
return []string{res}
|
||||
}
|
||||
case apiSpec.ArrayType:
|
||||
return []string{swaggerTypeArray}
|
||||
case apiSpec.DefineStruct, apiSpec.MapType:
|
||||
return []string{swaggerTypeObject}
|
||||
case apiSpec.PointerType:
|
||||
return typeFromGoType(ctx, val.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sampleTypeFromGoType(ctx Context, tp apiSpec.Type) string {
|
||||
switch val := tp.(type) {
|
||||
case apiSpec.PrimitiveType:
|
||||
return tpMapper[val.RawName]
|
||||
case apiSpec.ArrayType:
|
||||
return swaggerTypeArray
|
||||
case apiSpec.DefineStruct, apiSpec.MapType, apiSpec.NestedStruct:
|
||||
return swaggerTypeObject
|
||||
case apiSpec.PointerType:
|
||||
return sampleTypeFromGoType(ctx, val.Type)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func typeContainsTag(ctx Context, structType apiSpec.DefineStruct, tag string) bool {
|
||||
members := expandMembers(ctx, structType)
|
||||
for _, member := range members {
|
||||
tags, _ := apiSpec.Parse(member.Tag)
|
||||
if _, err := tags.Get(tag); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func expandMembers(ctx Context, tp apiSpec.Type) []apiSpec.Member {
|
||||
var members []apiSpec.Member
|
||||
switch val := tp.(type) {
|
||||
case apiSpec.DefineStruct:
|
||||
for _, v := range val.Members {
|
||||
if v.IsInline {
|
||||
members = append(members, expandMembers(ctx, v.Type)...)
|
||||
continue
|
||||
}
|
||||
members = append(members, v)
|
||||
}
|
||||
case apiSpec.NestedStruct:
|
||||
for _, v := range val.Members {
|
||||
if v.IsInline {
|
||||
members = append(members, expandMembers(ctx, v.Type)...)
|
||||
continue
|
||||
}
|
||||
members = append(members, v)
|
||||
}
|
||||
}
|
||||
|
||||
return members
|
||||
}
|
||||
|
||||
func rangeMemberAndDo(ctx Context, structType apiSpec.Type, do func(tag *apiSpec.Tags, required bool, member apiSpec.Member)) {
|
||||
var members = expandMembers(ctx, structType)
|
||||
|
||||
for _, field := range members {
|
||||
tags, _ := apiSpec.Parse(field.Tag)
|
||||
required := isRequired(ctx, tags)
|
||||
do(tags, required, field)
|
||||
}
|
||||
}
|
||||
|
||||
func isRequired(ctx Context, tags *apiSpec.Tags) bool {
|
||||
tag, err := tags.Get(tagJson)
|
||||
if err == nil {
|
||||
return !isOptional(ctx, tag.Options)
|
||||
}
|
||||
tag, err = tags.Get(tagForm)
|
||||
if err == nil {
|
||||
return !isOptional(ctx, tag.Options)
|
||||
}
|
||||
tag, err = tags.Get(tagPath)
|
||||
if err == nil {
|
||||
return !isOptional(ctx, tag.Options)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isOptional(_ Context, options []string) bool {
|
||||
for _, option := range options {
|
||||
if option == optionalFlag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func pathVariable2SwaggerVariable(_ Context, path string) string {
|
||||
pathItems := strings.FieldsFunc(path, slashRune)
|
||||
resp := make([]string, 0, len(pathItems))
|
||||
for _, v := range pathItems {
|
||||
if strings.HasPrefix(v, ":") {
|
||||
resp = append(resp, "{"+v[1:]+"}")
|
||||
} else {
|
||||
resp = append(resp, v)
|
||||
}
|
||||
}
|
||||
return "/" + strings.Join(resp, "/")
|
||||
}
|
||||
|
||||
func wrapCodeMsgProps(ctx Context, properties spec.SchemaProps, atDoc apiSpec.AtDoc) spec.SchemaProps {
|
||||
if !ctx.WrapCodeMsg {
|
||||
return properties
|
||||
}
|
||||
globalCodeDesc := ctx.BizCodeEnumDescription
|
||||
methodCodeDesc := getStringFromKVOrDefault(atDoc.Properties, propertyKeyBizCodeEnumDescription, globalCodeDesc)
|
||||
return spec.SchemaProps{
|
||||
Type: []string{swaggerTypeObject},
|
||||
Properties: spec.SchemaProperties{
|
||||
"code": {
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: 0,
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{swaggerTypeInteger},
|
||||
Description: methodCodeDesc,
|
||||
},
|
||||
},
|
||||
"msg": {
|
||||
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
||||
Example: "ok",
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{swaggerTypeString},
|
||||
Description: "business message",
|
||||
},
|
||||
},
|
||||
"data": {
|
||||
SchemaProps: properties,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func specExtensions(api apiSpec.Info) (spec.Extensions, *spec.Info) {
|
||||
ext := spec.Extensions{}
|
||||
ext.Add("x-generator", "go-doc")
|
||||
ext.Add("x-description", "This is a go-doc generated swagger file.")
|
||||
ext.Add("x-date", time.Now().Format(time.DateTime))
|
||||
ext.Add("x-github", "https://github.com/danielchan-25/go-doc")
|
||||
ext.Add("x-source", "go-zero API specification")
|
||||
|
||||
info := &spec.Info{}
|
||||
info.Title = getStringFromKVOrDefault(api.Properties, propertyKeyTitle, "")
|
||||
info.Description = getStringFromKVOrDefault(api.Properties, propertyKeyDescription, "")
|
||||
info.TermsOfService = getStringFromKVOrDefault(api.Properties, propertyKeyTermsOfService, "")
|
||||
info.Version = getStringFromKVOrDefault(api.Properties, propertyKeyVersion, "1.0")
|
||||
|
||||
contactInfo := spec.ContactInfo{}
|
||||
contactInfo.Name = getStringFromKVOrDefault(api.Properties, propertyKeyContactName, "")
|
||||
contactInfo.URL = getStringFromKVOrDefault(api.Properties, propertyKeyContactURL, "")
|
||||
contactInfo.Email = getStringFromKVOrDefault(api.Properties, propertyKeyContactEmail, "")
|
||||
if len(contactInfo.Name) > 0 || len(contactInfo.URL) > 0 || len(contactInfo.Email) > 0 {
|
||||
info.Contact = &contactInfo
|
||||
}
|
||||
|
||||
license := &spec.License{}
|
||||
license.Name = getStringFromKVOrDefault(api.Properties, propertyKeyLicenseName, "")
|
||||
license.URL = getStringFromKVOrDefault(api.Properties, propertyKeyLicenseURL, "")
|
||||
if len(license.Name) > 0 || len(license.URL) > 0 {
|
||||
info.License = license
|
||||
}
|
||||
return ext, info
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package swagger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_pathVariable2SwaggerVariable(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{input: "/api/:id", expected: "/api/{id}"},
|
||||
{input: "/api/:id/details", expected: "/api/{id}/details"},
|
||||
{input: "/:version/api/:id", expected: "/{version}/api/{id}"},
|
||||
{input: "/api/v1", expected: "/api/v1"},
|
||||
{input: "/api/:id/:action", expected: "/api/{id}/{action}"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := pathVariable2SwaggerVariable(testingContext(t), tc.input)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package swagger
|
||||
|
||||
var (
|
||||
tpMapper = map[string]string{
|
||||
"uint8": swaggerTypeInteger,
|
||||
"uint16": swaggerTypeInteger,
|
||||
"uint32": swaggerTypeInteger,
|
||||
"uint64": swaggerTypeInteger,
|
||||
"int8": swaggerTypeInteger,
|
||||
"int16": swaggerTypeInteger,
|
||||
"int32": swaggerTypeInteger,
|
||||
"int64": swaggerTypeInteger,
|
||||
"int": swaggerTypeInteger,
|
||||
"uint": swaggerTypeInteger,
|
||||
"byte": swaggerTypeInteger,
|
||||
"float32": swaggerTypeNumber,
|
||||
"float64": swaggerTypeNumber,
|
||||
"string": swaggerTypeString,
|
||||
"bool": swaggerTypeBoolean,
|
||||
}
|
||||
commaRune = func(r rune) bool {
|
||||
return r == ','
|
||||
}
|
||||
slashRune = func(r rune) bool {
|
||||
return r == '/'
|
||||
}
|
||||
)
|
|
@ -0,0 +1,14 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// MkdirIfNotExist creates a directory if it doesn't exist
|
||||
func MkdirIfNotExist(dir string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
return os.MkdirAll(dir, 0755)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// String wraps a string for manipulation
|
||||
type String struct {
|
||||
source string
|
||||
}
|
||||
|
||||
// From creates a String instance
|
||||
func From(s string) String {
|
||||
return String{source: s}
|
||||
}
|
||||
|
||||
// ToCamel converts string to camelCase
|
||||
func (s String) ToCamel() string {
|
||||
if s.source == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
words := splitWords(s.source)
|
||||
if len(words) == 0 {
|
||||
return s.source
|
||||
}
|
||||
|
||||
result := strings.Builder{}
|
||||
for i, word := range words {
|
||||
if i == 0 {
|
||||
result.WriteString(strings.ToLower(word))
|
||||
} else {
|
||||
result.WriteString(title(word))
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// Untitle converts first character to lowercase
|
||||
func (s String) Untitle() string {
|
||||
if s.source == "" {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(s.source)
|
||||
runes[0] = unicode.ToLower(runes[0])
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
// splitWords splits a string into words by common separators
|
||||
func splitWords(s string) []string {
|
||||
var words []string
|
||||
var current strings.Builder
|
||||
|
||||
for i, r := range s {
|
||||
if r == '_' || r == '-' || r == ' ' || r == '.' {
|
||||
if current.Len() > 0 {
|
||||
words = append(words, current.String())
|
||||
current.Reset()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if i > 0 && unicode.IsUpper(r) && !unicode.IsUpper(rune(s[i-1])) {
|
||||
if current.Len() > 0 {
|
||||
words = append(words, current.String())
|
||||
current.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
current.WriteRune(r)
|
||||
}
|
||||
|
||||
if current.Len() > 0 {
|
||||
words = append(words, current.String())
|
||||
}
|
||||
|
||||
return words
|
||||
}
|
||||
|
||||
// title capitalizes the first character of a string
|
||||
func title(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(s)
|
||||
runes[0] = unicode.ToUpper(runes[0])
|
||||
return string(runes)
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// TrimWhiteSpace removes all whitespace characters from the string
|
||||
func TrimWhiteSpace(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, s)
|
||||
}
|
||||
|
||||
// FieldsAndTrimSpace splits string by the given separator function and trims space for each field
|
||||
func FieldsAndTrimSpace(s string, fn func(rune) bool) []string {
|
||||
fields := strings.FieldsFunc(s, fn)
|
||||
result := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
trimmed := strings.TrimSpace(field)
|
||||
if len(trimmed) > 0 {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
Loading…
Reference in New Issue