feat: init swagger project

This commit is contained in:
王性驊 2025-09-30 16:16:44 +08:00
commit 696d789686
45 changed files with 3692 additions and 0 deletions

33
.gitignore vendored Normal file
View File

@ -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/

51
CHANGELOG.md Normal file
View File

@ -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.

27
LICENSE Normal file
View File

@ -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.

255
MIGRATION.md Normal file
View File

@ -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)

76
Makefile Normal file
View File

@ -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/"

250
README.md Normal file
View File

@ -0,0 +1,250 @@
# go-doc
[![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.23-blue)](https://go.dev/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](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.

43
cmd/go-doc/main.go Normal file
View File

@ -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)
}
}

59
cursor.md Normal file
View File

@ -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** 版本控制的嚴格性與相容性。 |
---

4
example/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.json
*.yaml
bin
output

241
example/example.api Normal file
View File

@ -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)
}

247
example/example_cn.api Normal file
View File

@ -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)
}

39
example/go-swagger-cn.sh Normal file
View File

@ -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

BIN
example/go-swagger-ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

39
example/go-swagger.sh Normal file
View File

@ -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

91
example/swagger-ui-cn.sh Normal file
View File

@ -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

80
example/swagger-ui.sh Normal file
View File

@ -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

30
go.mod Normal file
View File

@ -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
)

58
go.sum Normal file
View File

@ -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=

View File

@ -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 ""
}

View File

@ -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"}))
}

138
internal/swagger/api.go Normal file
View File

@ -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
}
}

View File

@ -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)
}

65
internal/swagger/const.go Normal file
View File

@ -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
)

View File

@ -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}
}

View File

@ -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)
})
}
}

View File

@ -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"),
}
}

View File

@ -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,
}
}

125
internal/swagger/options.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
},
},
}
}

123
internal/swagger/path.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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),
},
},
},
},
},
}
}

320
internal/swagger/swagger.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

27
internal/swagger/vars.go Normal file
View File

@ -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 == '/'
}
)

14
internal/util/pathx.go Normal file
View File

@ -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
}

90
internal/util/stringx.go Normal file
View File

@ -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)
}

30
internal/util/util.go Normal file
View File

@ -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
}