mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-07-08 04:22:23 +08:00
Compare commits
79 Commits
v1.9.4
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3db0b92c4 | ||
|
|
f910257ec9 | ||
|
|
d318de1212 | ||
|
|
99515480cf | ||
|
|
dbc71bb57b | ||
|
|
b2e3aa1587 | ||
|
|
726cd0141a | ||
|
|
c057ecd50a | ||
|
|
760e60c364 | ||
|
|
48ca7f03b5 | ||
|
|
d4882c1da0 | ||
|
|
1aecffc306 | ||
|
|
18305200d2 | ||
|
|
d9cb246301 | ||
|
|
4dd372914e | ||
|
|
a306964403 | ||
|
|
0431bce2b1 | ||
|
|
6f46837c05 | ||
|
|
ca8b50a7a5 | ||
|
|
0c79f5562e | ||
|
|
eb33a8bb36 | ||
|
|
34f16a857e | ||
|
|
6f45879f1b | ||
|
|
7b5e7b1c26 | ||
|
|
4ad4fd43b7 | ||
|
|
3f3dca6f72 | ||
|
|
4ea0d9e9ea | ||
|
|
52d2bdadcd | ||
|
|
3738be1945 | ||
|
|
5b74b9ab7b | ||
|
|
4a67261b7b | ||
|
|
22bdae0787 | ||
|
|
e8675d6a9a | ||
|
|
e441c44975 | ||
|
|
3f91a79a2b | ||
|
|
8c47c01739 | ||
|
|
f59a1cb0de | ||
|
|
d44ff6ddc8 | ||
|
|
6ffa9cabec | ||
|
|
0069721586 | ||
|
|
ba9c275853 | ||
|
|
9a6447ab5c | ||
|
|
004995f06a | ||
|
|
c12c82b2f6 | ||
|
|
85d770d340 | ||
|
|
8cd7f7a2d8 | ||
|
|
db3101361b | ||
|
|
eb2302b71e | ||
|
|
04ed637366 | ||
|
|
567087a715 | ||
|
|
4d2e64a417 | ||
|
|
b01831b4c5 | ||
|
|
d1a014955c | ||
|
|
ec802e25a6 | ||
|
|
8a2e09dfd1 | ||
|
|
220d438fe7 | ||
|
|
2cd96146fa | ||
|
|
7e96317fad | ||
|
|
70728ce2e2 | ||
|
|
6a72a735d4 | ||
|
|
b139a82c2e | ||
|
|
bdddf1f30c | ||
|
|
9b74b7e09e | ||
|
|
4d5ed2c45d | ||
|
|
a2310bf9d7 | ||
|
|
be846eba01 | ||
|
|
b20f0e3d60 | ||
|
|
e2bb65d43c | ||
|
|
94e2f5bd12 | ||
|
|
173f76acf9 | ||
|
|
6e1af75635 | ||
|
|
84ff755e61 | ||
|
|
4b9d23aef5 | ||
|
|
97b9aebe99 | ||
|
|
8e7e5695eb | ||
|
|
4b4751e76c | ||
|
|
fcec494ea8 | ||
|
|
42117c2dcc | ||
|
|
4b631f3785 |
105
.github/copilot-instructions.md
vendored
105
.github/copilot-instructions.md
vendored
@@ -172,6 +172,109 @@ err := c.QueryRowCtx(ctx, &dest, key, func(ctx context.Context, conn sqlx.SqlCon
|
||||
3. **API documentation**: Maintain API documentation in sync
|
||||
4. **README updates**: Update README for significant changes
|
||||
|
||||
## GitHub Issue Management
|
||||
|
||||
### Understanding and Categorizing Issues
|
||||
|
||||
When analyzing GitHub issues, consider these common categories:
|
||||
|
||||
1. **Bug Reports**: Stack traces, version info, reproduction steps
|
||||
2. **Feature Requests**: Use case, proposed solution, alternatives
|
||||
3. **Questions**: Usage, configuration, or architecture
|
||||
4. **Documentation Issues**: Missing, unclear, or incorrect docs
|
||||
5. **Performance Issues**: Benchmarks, profiling data, resource usage
|
||||
|
||||
### Issue Analysis Checklist
|
||||
|
||||
- Identify affected component (REST, RPC, Gateway, MCP, Core utilities, goctl)
|
||||
- Check versions (go-zero, Go)
|
||||
- Look for reproduction steps or code examples
|
||||
- Review code snippets, logs, or stack traces
|
||||
- Check if related to resilience features (breaker, load shedding, rate limiting)
|
||||
- Determine production impact
|
||||
|
||||
### Responding to Issues
|
||||
|
||||
Be helpful and professional. Ask clarifying questions when needed. Reference relevant documentation and code files. Provide code examples following project conventions. Suggest workarounds when applicable.
|
||||
|
||||
### Chinese to English Translation
|
||||
|
||||
go-zero has an international user base. When encountering issues or comments written in Chinese, translate them to English to ensure all contributors can participate in discussions.
|
||||
|
||||
#### Translation Guidelines
|
||||
|
||||
1. **Update issue titles**: Edit the issue title to include English translation only
|
||||
2. **Translate comments in place**: Add a comment with the English translation, followed by the original Chinese text
|
||||
3. **Keep original Chinese**: After translating, include the original Chinese text in a blockquote for verification
|
||||
4. **Encourage English communication**: Politely suggest users write in English for better collaboration
|
||||
5. **Maintain technical accuracy**: Preserve technical terms, component names, and code exactly
|
||||
6. **Translate naturally**: Avoid literal word-by-word translation; use idiomatic English
|
||||
7. **Preserve formatting**: Keep markdown formatting, code blocks, and links intact
|
||||
8. **Keep URLs unchanged**: Don't translate URLs or file paths
|
||||
|
||||
#### Common Technical Terms (Chinese → English)
|
||||
|
||||
- 框架 → **Framework** | 中间件 → **Middleware** | 负载均衡 → **Load Balancing**
|
||||
- 熔断器 → **Circuit Breaker** | 限流 → **Rate Limiting** | 降载/过载保护 → **Load Shedding**
|
||||
- 服务发现 → **Service Discovery** | 配置 → **Configuration** | 弹性/容错 → **Resilience** | 微服务 → **Microservices**
|
||||
|
||||
#### Translation Example
|
||||
|
||||
**Original Chinese Title:** `goctl 执行环境问题`
|
||||
**Updated Title:** `goctl Execution Environment Issue`
|
||||
|
||||
**Original Chinese Comment:** `我在项目中遇到熔断器配置问题`
|
||||
**Translation in Comment:**
|
||||
```markdown
|
||||
I encountered a circuit breaker configuration issue in my project.
|
||||
|
||||
> Original (原文): 我在项目中遇到熔断器配置问题
|
||||
```
|
||||
|
||||
### Common Issue Patterns and Solutions
|
||||
|
||||
#### Configuration Issues
|
||||
- Check `service.ServiceConf` embedding and struct tags
|
||||
- Verify YAML syntax, defaults, and validation rules
|
||||
- Reference: [rest/config.go](rest/config.go), [zrpc/config.go](zrpc/config.go)
|
||||
|
||||
#### Code Generation (goctl) Issues
|
||||
- Verify `.api` or `.proto` file syntax and goctl version
|
||||
- Reference: `tools/goctl/` directory
|
||||
|
||||
#### RPC Connection Issues
|
||||
- Check etcd configuration, service discovery, and endpoints
|
||||
- Verify load balancing settings (p2c_ewma)
|
||||
|
||||
#### Database/Cache Issues
|
||||
- Verify `sqlx.SqlConn` usage with context
|
||||
- Check cache key generation, invalidation, and connection pools
|
||||
- Use test helpers (`redistest`, `mongtest`)
|
||||
|
||||
#### Performance Issues
|
||||
- Check if load shedding is enabled (mode: `pre`/`pro`)
|
||||
- Review circuit breaker thresholds, rate limiting, and context timeouts
|
||||
|
||||
### Referencing Codebase
|
||||
|
||||
When explaining issues, reference specific files and patterns:
|
||||
- REST API: `rest/`, `rest/handler/`, `rest/httpx/`
|
||||
- RPC: `zrpc/`, `zrpc/internal/`
|
||||
- Core utilities: `core/breaker/`, `core/limit/`, `core/load/`, etc.
|
||||
- Gateway: `gateway/`
|
||||
- MCP: `mcp/`
|
||||
- Code generation: `tools/goctl/`
|
||||
- Examples: `adhoc/` directory contains various examples
|
||||
|
||||
### Encouraging Best Practices
|
||||
|
||||
When responding to issues, gently guide users toward:
|
||||
- Proper error handling with context
|
||||
- Using resilience features (breakers, rate limiters)
|
||||
- Following testing patterns with table-driven tests
|
||||
- Implementing proper resource cleanup
|
||||
- Reading existing documentation in `docs/` and `readme.md`
|
||||
|
||||
## Common Patterns to Follow
|
||||
|
||||
### Service Configuration
|
||||
@@ -203,7 +306,7 @@ Always implement proper resource cleanup using defer and context cancellation.
|
||||
## Build and Test Commands
|
||||
|
||||
- Build: `go build ./...`
|
||||
- Test: `go test ./...`
|
||||
- Test: `go test ./...`
|
||||
- Test with race detection: `go test -race ./...`
|
||||
- Format: `gofmt -w .`
|
||||
- Code generation:
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
6
.github/workflows/go.yml
vendored
6
.github/workflows/go.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v6
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
flags: unittests
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v6
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- goarch: "386"
|
||||
goos: darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: zeromicro/go-zero-release-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/reviewdog.yml
vendored
2
.github/workflows/reviewdog.yml
vendored
@@ -5,7 +5,7 @@ jobs:
|
||||
name: runner / staticcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
2
.github/workflows/version-check.yml
vendored
2
.github/workflows/version-check.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
version-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
// ErrPaddingSize indicates bad padding size.
|
||||
@@ -27,7 +25,8 @@ func newECB(b cipher.Block) *ecb {
|
||||
|
||||
type ecbEncrypter ecb
|
||||
|
||||
// NewECBEncrypter returns an ECB encrypter.
|
||||
// Deprecated: NewECBEncrypter returns an ECB encrypter.
|
||||
// ECB mode is insecure for multi-block data. Use AES-GCM instead.
|
||||
func NewECBEncrypter(b cipher.Block) cipher.BlockMode {
|
||||
return (*ecbEncrypter)(newECB(b))
|
||||
}
|
||||
@@ -39,12 +38,10 @@ func (x *ecbEncrypter) BlockSize() int { return x.blockSize }
|
||||
// the block size. Dst and src must overlap entirely or not at all.
|
||||
func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
|
||||
if len(src)%x.blockSize != 0 {
|
||||
logx.Error("crypto/cipher: input not full blocks")
|
||||
return
|
||||
panic("crypto/cipher: input not full blocks")
|
||||
}
|
||||
if len(dst) < len(src) {
|
||||
logx.Error("crypto/cipher: output smaller than input")
|
||||
return
|
||||
panic("crypto/cipher: output smaller than input")
|
||||
}
|
||||
|
||||
for len(src) > 0 {
|
||||
@@ -56,7 +53,8 @@ func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
|
||||
|
||||
type ecbDecrypter ecb
|
||||
|
||||
// NewECBDecrypter returns an ECB decrypter.
|
||||
// Deprecated: NewECBDecrypter returns an ECB decrypter.
|
||||
// ECB mode is insecure for multi-block data. Use AES-GCM instead.
|
||||
func NewECBDecrypter(b cipher.Block) cipher.BlockMode {
|
||||
return (*ecbDecrypter)(newECB(b))
|
||||
}
|
||||
@@ -70,12 +68,10 @@ func (x *ecbDecrypter) BlockSize() int {
|
||||
// the block size. Dst and src must overlap entirely or not at all.
|
||||
func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
|
||||
if len(src)%x.blockSize != 0 {
|
||||
logx.Error("crypto/cipher: input not full blocks")
|
||||
return
|
||||
panic("crypto/cipher: input not full blocks")
|
||||
}
|
||||
if len(dst) < len(src) {
|
||||
logx.Error("crypto/cipher: output smaller than input")
|
||||
return
|
||||
panic("crypto/cipher: output smaller than input")
|
||||
}
|
||||
|
||||
for len(src) > 0 {
|
||||
@@ -85,14 +81,18 @@ func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// EcbDecrypt decrypts src with the given key.
|
||||
// Deprecated: EcbDecrypt decrypts src with the given key.
|
||||
// ECB mode is insecure for multi-block data. Use AES-GCM instead.
|
||||
func EcbDecrypt(key, src []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
logx.Errorf("Decrypt key error: % x", key)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(src)%block.BlockSize() != 0 {
|
||||
return nil, ErrPaddingSize
|
||||
}
|
||||
|
||||
decrypter := NewECBDecrypter(block)
|
||||
decrypted := make([]byte, len(src))
|
||||
decrypter.CryptBlocks(decrypted, src)
|
||||
@@ -100,8 +100,9 @@ func EcbDecrypt(key, src []byte) ([]byte, error) {
|
||||
return pkcs5Unpadding(decrypted, decrypter.BlockSize())
|
||||
}
|
||||
|
||||
// EcbDecryptBase64 decrypts base64 encoded src with the given base64 encoded key.
|
||||
// Deprecated: EcbDecryptBase64 decrypts base64 encoded src with the given base64 encoded key.
|
||||
// The returned string is also base64 encoded.
|
||||
// ECB mode is insecure for multi-block data. Use AES-GCM instead.
|
||||
func EcbDecryptBase64(key, src string) (string, error) {
|
||||
keyBytes, err := getKeyBytes(key)
|
||||
if err != nil {
|
||||
@@ -121,11 +122,11 @@ func EcbDecryptBase64(key, src string) (string, error) {
|
||||
return base64.StdEncoding.EncodeToString(decryptedBytes), nil
|
||||
}
|
||||
|
||||
// EcbEncrypt encrypts src with the given key.
|
||||
// Deprecated: EcbEncrypt encrypts src with the given key.
|
||||
// ECB mode is insecure for multi-block data. Use AES-GCM instead.
|
||||
func EcbEncrypt(key, src []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
logx.Errorf("Encrypt key error: % x", key)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -137,8 +138,9 @@ func EcbEncrypt(key, src []byte) ([]byte, error) {
|
||||
return crypted, nil
|
||||
}
|
||||
|
||||
// EcbEncryptBase64 encrypts base64 encoded src with the given base64 encoded key.
|
||||
// Deprecated: EcbEncryptBase64 encrypts base64 encoded src with the given base64 encoded key.
|
||||
// The returned string is also base64 encoded.
|
||||
// ECB mode is insecure for multi-block data. Use AES-GCM instead.
|
||||
func EcbEncryptBase64(key, src string) (string, error) {
|
||||
keyBytes, err := getKeyBytes(key)
|
||||
if err != nil {
|
||||
@@ -179,10 +181,20 @@ func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
|
||||
|
||||
func pkcs5Unpadding(src []byte, blockSize int) ([]byte, error) {
|
||||
length := len(src)
|
||||
unpadding := int(src[length-1])
|
||||
if unpadding >= length || unpadding > blockSize {
|
||||
if length == 0 {
|
||||
return nil, ErrPaddingSize
|
||||
}
|
||||
|
||||
unpadding := int(src[length-1])
|
||||
if unpadding < 1 || unpadding > blockSize || unpadding > length {
|
||||
return nil, ErrPaddingSize
|
||||
}
|
||||
|
||||
for _, b := range src[length-unpadding:] {
|
||||
if int(b) != unpadding {
|
||||
return nil, ErrPaddingSize
|
||||
}
|
||||
}
|
||||
|
||||
return src[:length-unpadding], nil
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ func TestAesEcb(t *testing.T) {
|
||||
_, err = EcbDecrypt(badKey2, dst)
|
||||
assert.NotNil(t, err)
|
||||
_, err = EcbDecrypt(key, val)
|
||||
// not enough block, just nil
|
||||
assert.Nil(t, err)
|
||||
// not a multiple of block size
|
||||
assert.NotNil(t, err)
|
||||
src, err := EcbDecrypt(key, dst)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, val, src)
|
||||
@@ -41,33 +41,28 @@ func TestAesEcb(t *testing.T) {
|
||||
assert.Equal(t, 16, decrypter.BlockSize())
|
||||
|
||||
dst = make([]byte, 8)
|
||||
encrypter.CryptBlocks(dst, val)
|
||||
for _, b := range dst {
|
||||
assert.Equal(t, byte(0), b)
|
||||
}
|
||||
assert.Panics(t, func() {
|
||||
encrypter.CryptBlocks(dst, val)
|
||||
})
|
||||
|
||||
dst = make([]byte, 8)
|
||||
encrypter.CryptBlocks(dst, valLong)
|
||||
for _, b := range dst {
|
||||
assert.Equal(t, byte(0), b)
|
||||
}
|
||||
assert.Panics(t, func() {
|
||||
encrypter.CryptBlocks(dst, valLong)
|
||||
})
|
||||
|
||||
dst = make([]byte, 8)
|
||||
decrypter.CryptBlocks(dst, val)
|
||||
for _, b := range dst {
|
||||
assert.Equal(t, byte(0), b)
|
||||
}
|
||||
assert.Panics(t, func() {
|
||||
decrypter.CryptBlocks(dst, val)
|
||||
})
|
||||
|
||||
dst = make([]byte, 8)
|
||||
decrypter.CryptBlocks(dst, valLong)
|
||||
for _, b := range dst {
|
||||
assert.Equal(t, byte(0), b)
|
||||
}
|
||||
assert.Panics(t, func() {
|
||||
decrypter.CryptBlocks(dst, valLong)
|
||||
})
|
||||
|
||||
_, err = EcbEncryptBase64("cTR0N3dDKkYtSmFOZFJnVWpYbjJyNXU4eC9BP0QK", "aGVsbG93b3JsZGxvbmcuLgo=")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAesEcbBase64(t *testing.T) {
|
||||
const (
|
||||
val = "hello"
|
||||
@@ -98,3 +93,44 @@ func TestAesEcbBase64(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, val, string(b))
|
||||
}
|
||||
|
||||
func TestPkcs5UnpaddingEmptyInput(t *testing.T) {
|
||||
_, err := pkcs5Unpadding([]byte{}, 16)
|
||||
assert.Equal(t, ErrPaddingSize, err)
|
||||
}
|
||||
|
||||
func TestPkcs5UnpaddingMalformedPadding(t *testing.T) {
|
||||
// Valid PKCS5 padding of 3: last 3 bytes should all be 0x03
|
||||
// Here we corrupt one padding byte
|
||||
malformed := []byte{0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
|
||||
0x41, 0x41, 0x41, 0x41, 0x41, 0x02, 0x03, 0x03}
|
||||
_, err := pkcs5Unpadding(malformed, 16)
|
||||
assert.Equal(t, ErrPaddingSize, err)
|
||||
|
||||
// All padding bytes correct
|
||||
valid := []byte{0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
|
||||
0x41, 0x41, 0x41, 0x41, 0x41, 0x03, 0x03, 0x03}
|
||||
result, err := pkcs5Unpadding(valid, 16)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, valid[:13], result)
|
||||
}
|
||||
|
||||
func TestPkcs5UnpaddingInvalidPaddingValue(t *testing.T) {
|
||||
// padding value = 0 (< 1)
|
||||
_, err := pkcs5Unpadding([]byte{0x41, 0x00}, 16)
|
||||
assert.Equal(t, ErrPaddingSize, err)
|
||||
|
||||
// padding value > blockSize
|
||||
_, err = pkcs5Unpadding([]byte{0x41, 0x41, 0x41, 0x41, 17}, 4)
|
||||
assert.Equal(t, ErrPaddingSize, err)
|
||||
|
||||
// padding value > length
|
||||
_, err = pkcs5Unpadding([]byte{0x41, 0x03}, 16)
|
||||
assert.Equal(t, ErrPaddingSize, err)
|
||||
}
|
||||
|
||||
func TestEcbDecryptEmptyInput(t *testing.T) {
|
||||
key := []byte("q4t7w!z%C*F-JaNdRgUjXn2r5u8x/A?D")
|
||||
_, err := EcbDecrypt(key, []byte{})
|
||||
assert.Equal(t, ErrPaddingSize, err)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func ComputeKey(pubKey, priKey *big.Int) (*big.Int, error) {
|
||||
return nil, ErrInvalidPubKey
|
||||
}
|
||||
|
||||
if pubKey.Sign() <= 0 && p.Cmp(pubKey) <= 0 {
|
||||
if pubKey.Sign() <= 0 || p.Cmp(pubKey) <= 0 {
|
||||
return nil, ErrPubKeyOutOfBound
|
||||
}
|
||||
|
||||
|
||||
@@ -94,3 +94,32 @@ func TestDHOnErrors(t *testing.T) {
|
||||
|
||||
assert.NotNil(t, NewPublicKey([]byte("")))
|
||||
}
|
||||
|
||||
func TestDHPubKeyBoundary(t *testing.T) {
|
||||
key, err := GenerateKey()
|
||||
assert.Nil(t, err)
|
||||
|
||||
// pubKey = 0 should be rejected
|
||||
_, err = ComputeKey(big.NewInt(0), key.PriKey)
|
||||
assert.ErrorIs(t, err, ErrPubKeyOutOfBound)
|
||||
|
||||
// pubKey = -1 should be rejected
|
||||
_, err = ComputeKey(big.NewInt(-1), key.PriKey)
|
||||
assert.ErrorIs(t, err, ErrPubKeyOutOfBound)
|
||||
|
||||
// pubKey = p should be rejected
|
||||
_, err = ComputeKey(new(big.Int).Set(p), key.PriKey)
|
||||
assert.ErrorIs(t, err, ErrPubKeyOutOfBound)
|
||||
|
||||
// pubKey = p+1 should be rejected
|
||||
_, err = ComputeKey(new(big.Int).Add(p, big.NewInt(1)), key.PriKey)
|
||||
assert.ErrorIs(t, err, ErrPubKeyOutOfBound)
|
||||
|
||||
// pubKey = 1 should be accepted
|
||||
_, err = ComputeKey(big.NewInt(1), key.PriKey)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// pubKey = p-1 should be accepted
|
||||
_, err = ComputeKey(new(big.Int).Sub(p, big.NewInt(1)), key.PriKey)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package codec
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
@@ -46,7 +47,9 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// NewRsaDecrypter returns a RsaDecrypter with the given file.
|
||||
// Deprecated: NewRsaDecrypter returns a RsaDecrypter with the given file.
|
||||
// PKCS#1 v1.5 padding is vulnerable to padding oracle attacks.
|
||||
// Use NewRsaOAEPDecrypter instead.
|
||||
func NewRsaDecrypter(file string) (RsaDecrypter, error) {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
@@ -90,7 +93,9 @@ func (r *rsaDecrypter) DecryptBase64(input string) ([]byte, error) {
|
||||
return r.Decrypt(base64Decoded)
|
||||
}
|
||||
|
||||
// NewRsaEncrypter returns a RsaEncrypter with the given key.
|
||||
// Deprecated: NewRsaEncrypter returns a RsaEncrypter with the given key.
|
||||
// PKCS#1 v1.5 padding is vulnerable to padding oracle attacks.
|
||||
// Use NewRsaOAEPEncrypter instead.
|
||||
func NewRsaEncrypter(key []byte) (RsaEncrypter, error) {
|
||||
block, _ := pem.Decode(key)
|
||||
if block == nil {
|
||||
@@ -154,3 +159,90 @@ func rsaDecryptBlock(privateKey *rsa.PrivateKey, block []byte) ([]byte, error) {
|
||||
func rsaEncryptBlock(publicKey *rsa.PublicKey, msg []byte) ([]byte, error) {
|
||||
return rsa.EncryptPKCS1v15(rand.Reader, publicKey, msg)
|
||||
}
|
||||
|
||||
// NewRsaOAEPDecrypter returns a RsaDecrypter using OAEP with SHA-256.
|
||||
func NewRsaOAEPDecrypter(file string) (RsaDecrypter, error) {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(content)
|
||||
if block == nil {
|
||||
return nil, ErrPrivateKey
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &rsaOAEPDecrypter{
|
||||
rsaBase: rsaBase{
|
||||
bytesLimit: privateKey.N.BitLen() >> 3,
|
||||
},
|
||||
privateKey: privateKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewRsaOAEPEncrypter returns a RsaEncrypter using OAEP with SHA-256.
|
||||
func NewRsaOAEPEncrypter(key []byte) (RsaEncrypter, error) {
|
||||
block, _ := pem.Decode(key)
|
||||
if block == nil {
|
||||
return nil, ErrPublicKey
|
||||
}
|
||||
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch pubKey := pub.(type) {
|
||||
case *rsa.PublicKey:
|
||||
// OAEP overhead: 2*hash_size + 2
|
||||
hashSize := sha256.New().Size()
|
||||
return &rsaOAEPEncrypter{
|
||||
rsaBase: rsaBase{
|
||||
bytesLimit: (pubKey.N.BitLen() >> 3) - 2*hashSize - 2,
|
||||
},
|
||||
publicKey: pubKey,
|
||||
}, nil
|
||||
default:
|
||||
return nil, ErrNotRsaKey
|
||||
}
|
||||
}
|
||||
|
||||
type rsaOAEPDecrypter struct {
|
||||
rsaBase
|
||||
privateKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (r *rsaOAEPDecrypter) Decrypt(input []byte) ([]byte, error) {
|
||||
return r.crypt(input, func(block []byte) ([]byte, error) {
|
||||
return rsa.DecryptOAEP(sha256.New(), rand.Reader, r.privateKey, block, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *rsaOAEPDecrypter) DecryptBase64(input string) ([]byte, error) {
|
||||
if len(input) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
base64Decoded, err := base64.StdEncoding.DecodeString(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.Decrypt(base64Decoded)
|
||||
}
|
||||
|
||||
type rsaOAEPEncrypter struct {
|
||||
rsaBase
|
||||
publicKey *rsa.PublicKey
|
||||
}
|
||||
|
||||
func (r *rsaOAEPEncrypter) Encrypt(input []byte) ([]byte, error) {
|
||||
return r.crypt(input, func(block []byte) ([]byte, error) {
|
||||
return rsa.EncryptOAEP(sha256.New(), rand.Reader, r.publicKey, block, nil)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -58,3 +63,78 @@ func TestBadPubKey(t *testing.T) {
|
||||
_, err := NewRsaEncrypter([]byte("foo"))
|
||||
assert.Equal(t, ErrPublicKey, err)
|
||||
}
|
||||
|
||||
func TestOAEPCryption(t *testing.T) {
|
||||
enc, err := NewRsaOAEPEncrypter([]byte(pubKey))
|
||||
assert.Nil(t, err)
|
||||
ret, err := enc.Encrypt([]byte(testBody))
|
||||
assert.Nil(t, err)
|
||||
|
||||
file, err := fs.TempFilenameWithText(priKey)
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(file)
|
||||
dec, err := NewRsaOAEPDecrypter(file)
|
||||
assert.Nil(t, err)
|
||||
actual, err := dec.Decrypt(ret)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, testBody, string(actual))
|
||||
|
||||
actual, err = dec.DecryptBase64(base64.StdEncoding.EncodeToString(ret))
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, testBody, string(actual))
|
||||
|
||||
// empty input
|
||||
actual, err = dec.DecryptBase64("")
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, actual)
|
||||
}
|
||||
|
||||
func TestOAEPBadKeys(t *testing.T) {
|
||||
_, err := NewRsaOAEPEncrypter([]byte("bad"))
|
||||
assert.Equal(t, ErrPublicKey, err)
|
||||
|
||||
_, err = NewRsaOAEPDecrypter("nonexistent")
|
||||
assert.Error(t, err)
|
||||
|
||||
// valid PEM but invalid private key content
|
||||
badPem, err := fs.TempFilenameWithText("-----BEGIN RSA PRIVATE KEY-----\nYmFk\n-----END RSA PRIVATE KEY-----")
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(badPem)
|
||||
_, err = NewRsaOAEPDecrypter(badPem)
|
||||
assert.Error(t, err)
|
||||
|
||||
// not PEM content at all
|
||||
notPem, err := fs.TempFilenameWithText("not a pem file")
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(notPem)
|
||||
_, err = NewRsaOAEPDecrypter(notPem)
|
||||
assert.Equal(t, ErrPrivateKey, err)
|
||||
}
|
||||
|
||||
func TestOAEPEncrypterParseError(t *testing.T) {
|
||||
// valid PEM block but invalid public key content
|
||||
badPub := []byte("-----BEGIN PUBLIC KEY-----\nYmFk\n-----END PUBLIC KEY-----")
|
||||
_, err := NewRsaOAEPEncrypter(badPub)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestOAEPEncrypterNonRsaKey(t *testing.T) {
|
||||
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
assert.Nil(t, err)
|
||||
derBytes, err := x509.MarshalPKIXPublicKey(&ecKey.PublicKey)
|
||||
assert.Nil(t, err)
|
||||
ecPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: derBytes})
|
||||
_, err = NewRsaOAEPEncrypter(ecPem)
|
||||
assert.Equal(t, ErrNotRsaKey, err)
|
||||
}
|
||||
|
||||
func TestOAEPDecryptBase64Error(t *testing.T) {
|
||||
file, err := fs.TempFilenameWithText(priKey)
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(file)
|
||||
dec, err := NewRsaOAEPDecrypter(file)
|
||||
assert.Nil(t, err)
|
||||
|
||||
_, err = dec.DecryptBase64("not-valid-base64!!!")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -21,10 +21,11 @@ const (
|
||||
var (
|
||||
fillDefaultUnmarshaler = mapping.NewUnmarshaler(jsonTagKey, mapping.WithDefault())
|
||||
loaders = map[string]func([]byte, any) error{
|
||||
".json": LoadFromJsonBytes,
|
||||
".toml": LoadFromTomlBytes,
|
||||
".yaml": LoadFromYamlBytes,
|
||||
".yml": LoadFromYamlBytes,
|
||||
".json": LoadFromJsonBytes,
|
||||
".json5": LoadFromJson5Bytes,
|
||||
".toml": LoadFromTomlBytes,
|
||||
".yaml": LoadFromYamlBytes,
|
||||
".yml": LoadFromYamlBytes,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -41,7 +42,7 @@ func FillDefault(v any) error {
|
||||
return fillDefaultUnmarshaler.Unmarshal(map[string]any{}, v)
|
||||
}
|
||||
|
||||
// Load loads config into v from file, .json, .yaml and .yml are acceptable.
|
||||
// Load loads config into v from file, .json, .json5, .toml, .yaml and .yml are acceptable.
|
||||
func Load(file string, v any, opts ...Option) error {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
@@ -62,14 +63,10 @@ func Load(file string, v any, opts ...Option) error {
|
||||
return loader([]byte(os.ExpandEnv(string(content))), v)
|
||||
}
|
||||
|
||||
if err = loader(content, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return validate(v)
|
||||
return loader(content, v)
|
||||
}
|
||||
|
||||
// LoadConfig loads config into v from file, .json, .yaml and .yml are acceptable.
|
||||
// LoadConfig loads config into v from file, .json, .json5, .toml, .yaml and .yml are acceptable.
|
||||
// Deprecated: use Load instead.
|
||||
func LoadConfig(file string, v any, opts ...Option) error {
|
||||
return Load(file, v, opts...)
|
||||
@@ -123,6 +120,16 @@ func LoadFromYamlBytes(content []byte, v any) error {
|
||||
return LoadFromJsonBytes(b, v)
|
||||
}
|
||||
|
||||
// LoadFromJson5Bytes loads config into v from content json5 bytes.
|
||||
func LoadFromJson5Bytes(content []byte, v any) error {
|
||||
b, err := encoding.Json5ToJson(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return LoadFromJsonBytes(b, v)
|
||||
}
|
||||
|
||||
// LoadConfigFromYamlBytes loads config into v from content yaml bytes.
|
||||
// Deprecated: use LoadFromYamlBytes instead.
|
||||
func LoadConfigFromYamlBytes(content []byte, v any) error {
|
||||
|
||||
@@ -75,6 +75,160 @@ func TestLoadFromJsonBytesArray(t *testing.T) {
|
||||
assert.EqualValues(t, []string{"foo", "bar"}, expect)
|
||||
}
|
||||
|
||||
func TestConfigJson5(t *testing.T) {
|
||||
// JSON5 with comments, trailing commas, and unquoted keys
|
||||
text := `{
|
||||
// This is a comment
|
||||
a: 'foo', // single quotes
|
||||
b: 1,
|
||||
c: "${FOO}",
|
||||
d: "abcd!@#$112", // trailing comma
|
||||
}`
|
||||
t.Setenv("FOO", "2")
|
||||
|
||||
tmpfile, err := createTempFile(t, ".json5", text)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var val struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
C string `json:"c"`
|
||||
D string `json:"d"`
|
||||
}
|
||||
MustLoad(tmpfile, &val)
|
||||
assert.Equal(t, "foo", val.A)
|
||||
assert.Equal(t, 1, val.B)
|
||||
assert.Equal(t, "${FOO}", val.C)
|
||||
assert.Equal(t, "abcd!@#$112", val.D)
|
||||
}
|
||||
|
||||
func TestConfigJsonStandardParser(t *testing.T) {
|
||||
// Standard JSON uses standard JSON parser (not JSON5) for backward compatibility
|
||||
text := `{
|
||||
"a": "foo",
|
||||
"b": 1,
|
||||
"c": "${FOO}",
|
||||
"d": "abcd!@#$112"
|
||||
}`
|
||||
t.Setenv("FOO", "2")
|
||||
|
||||
tmpfile, err := createTempFile(t, ".json", text)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var val struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
C string `json:"c"`
|
||||
D string `json:"d"`
|
||||
}
|
||||
MustLoad(tmpfile, &val)
|
||||
assert.Equal(t, "foo", val.A)
|
||||
assert.Equal(t, 1, val.B)
|
||||
assert.Equal(t, "${FOO}", val.C)
|
||||
assert.Equal(t, "abcd!@#$112", val.D)
|
||||
}
|
||||
|
||||
func TestConfigJsonLargeIntegers(t *testing.T) {
|
||||
// Test that .json files preserve large integer precision (backward compatibility)
|
||||
text := `{
|
||||
"id": 1234567890123456789,
|
||||
"timestamp": 9223372036854775807
|
||||
}`
|
||||
|
||||
tmpfile, err := createTempFile(t, ".json", text)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var val struct {
|
||||
ID int64 `json:"id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
MustLoad(tmpfile, &val)
|
||||
assert.Equal(t, int64(1234567890123456789), val.ID)
|
||||
assert.Equal(t, int64(9223372036854775807), val.Timestamp)
|
||||
}
|
||||
|
||||
func TestConfigJson5Env(t *testing.T) {
|
||||
text := `{
|
||||
// Comment with env variable
|
||||
a: "foo",
|
||||
b: 1,
|
||||
c: "${FOO}",
|
||||
d: "abcd!@#$a12 3",
|
||||
}`
|
||||
t.Setenv("FOO", "2")
|
||||
|
||||
tmpfile, err := createTempFile(t, ".json5", text)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var val struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
C string `json:"c"`
|
||||
D string `json:"d"`
|
||||
}
|
||||
MustLoad(tmpfile, &val, UseEnv())
|
||||
assert.Equal(t, "foo", val.A)
|
||||
assert.Equal(t, 1, val.B)
|
||||
assert.Equal(t, "2", val.C)
|
||||
assert.Equal(t, "abcd!@# 3", val.D)
|
||||
}
|
||||
|
||||
func TestLoadFromJson5Bytes(t *testing.T) {
|
||||
// Test JSON5 features: comments, trailing commas, single quotes, unquoted keys
|
||||
input := []byte(`{
|
||||
// This is a comment
|
||||
users: [
|
||||
{name: 'foo'}, // trailing comma
|
||||
{Name: "bar"},
|
||||
],
|
||||
}`)
|
||||
var val struct {
|
||||
Users []struct {
|
||||
Name string
|
||||
}
|
||||
}
|
||||
|
||||
assert.NoError(t, LoadFromJson5Bytes(input, &val))
|
||||
var expect []string
|
||||
for _, user := range val.Users {
|
||||
expect = append(expect, user.Name)
|
||||
}
|
||||
assert.EqualValues(t, []string{"foo", "bar"}, expect)
|
||||
}
|
||||
|
||||
func TestLoadFromJson5BytesError(t *testing.T) {
|
||||
// Invalid JSON5 syntax
|
||||
input := []byte(`{a: foo}`) // unquoted string value (invalid)
|
||||
var val struct {
|
||||
A string
|
||||
}
|
||||
|
||||
assert.Error(t, LoadFromJson5Bytes(input, &val))
|
||||
}
|
||||
|
||||
func TestConfigJson5LargeIntegersLimitation(t *testing.T) {
|
||||
// Document that JSON5 has precision limitations for large integers (>2^53)
|
||||
// due to JavaScript number semantics. Users should use .json for configs with large IDs.
|
||||
text := `{
|
||||
// JSON5 converts numbers to float64, which loses precision for large integers
|
||||
id: 1234567890123456789
|
||||
}`
|
||||
|
||||
tmpfile, err := createTempFile(t, ".json5", text)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var val struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
// This will load; depending on the JSON5 implementation, large integers may lose precision.
|
||||
// This test documents that behavior without requiring loss of precision as an invariant.
|
||||
err = Load(tmpfile, &val)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Logf("loaded JSON5 large integer id=%d (original 1234567890123456789)", val.ID)
|
||||
}
|
||||
|
||||
func TestConfigToml(t *testing.T) {
|
||||
text := `a = "foo"
|
||||
b = 1
|
||||
@@ -1397,3 +1551,222 @@ func TestGetFullName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// validatorConfig is a test config that implements Validate() for testing validation behavior
|
||||
type validatorConfig struct {
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
func (v *validatorConfig) Validate() error {
|
||||
if v.Value < 10 {
|
||||
return errors.New("value must be >= 10")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestLoadValidation_WithoutEnv tests that validation is called correctly in normal loading path
|
||||
func TestLoadValidation_WithoutEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
extension string
|
||||
content string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "json valid value",
|
||||
extension: ".json",
|
||||
content: `{"value": 15}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json invalid value",
|
||||
extension: ".json",
|
||||
content: `{"value": 5}`,
|
||||
wantErr: true,
|
||||
errMsg: "value must be >= 10",
|
||||
},
|
||||
{
|
||||
name: "yaml valid value",
|
||||
extension: ".yaml",
|
||||
content: "value: 20\n",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "yaml invalid value",
|
||||
extension: ".yaml",
|
||||
content: "value: 3\n",
|
||||
wantErr: true,
|
||||
errMsg: "value must be >= 10",
|
||||
},
|
||||
{
|
||||
name: "toml valid value",
|
||||
extension: ".toml",
|
||||
content: "value = 100\n",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "toml invalid value",
|
||||
extension: ".toml",
|
||||
content: "value = 1\n",
|
||||
wantErr: true,
|
||||
errMsg: "value must be >= 10",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpfile, err := createTempFile(t, tt.extension, tt.content)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var cfg validatorConfig
|
||||
err = Load(tmpfile, &cfg)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadValidation_WithEnv tests that validation is called correctly with UseEnv() option
|
||||
func TestLoadValidation_WithEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
extension string
|
||||
content string
|
||||
envValue string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "json valid value with env",
|
||||
extension: ".json",
|
||||
content: `{"value": ${TEST_VALUE}}`,
|
||||
envValue: "25",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json invalid value with env",
|
||||
extension: ".json",
|
||||
content: `{"value": ${TEST_VALUE}}`,
|
||||
envValue: "7",
|
||||
wantErr: true,
|
||||
errMsg: "value must be >= 10",
|
||||
},
|
||||
{
|
||||
name: "yaml valid value with env",
|
||||
extension: ".yaml",
|
||||
content: "value: ${TEST_VALUE}\n",
|
||||
envValue: "50",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "yaml invalid value with env",
|
||||
extension: ".yaml",
|
||||
content: "value: ${TEST_VALUE}\n",
|
||||
envValue: "2",
|
||||
wantErr: true,
|
||||
errMsg: "value must be >= 10",
|
||||
},
|
||||
{
|
||||
name: "toml valid value with env",
|
||||
extension: ".toml",
|
||||
content: "value = ${TEST_VALUE}\n",
|
||||
envValue: "99",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "toml invalid value with env",
|
||||
extension: ".toml",
|
||||
content: "value = ${TEST_VALUE}\n",
|
||||
envValue: "8",
|
||||
wantErr: true,
|
||||
errMsg: "value must be >= 10",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv("TEST_VALUE", tt.envValue)
|
||||
|
||||
tmpfile, err := createTempFile(t, tt.extension, tt.content)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var cfg validatorConfig
|
||||
err = Load(tmpfile, &cfg, UseEnv())
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadValidation_Consistency verifies validation behavior is consistent between paths
|
||||
func TestLoadValidation_Consistency(t *testing.T) {
|
||||
// Test that both paths (with and without UseEnv) produce the same validation results
|
||||
const validValue = 15
|
||||
|
||||
formats := []struct {
|
||||
ext string
|
||||
invalid string
|
||||
valid string
|
||||
}{
|
||||
{".json", `{"value": 5}`, `{"value": 15}`},
|
||||
{".yaml", "value: 5\n", "value: 15\n"},
|
||||
{".toml", "value = 5\n", "value = 15\n"},
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
t.Run("invalid_"+format.ext, func(t *testing.T) {
|
||||
// Test without UseEnv()
|
||||
tmpfile1, err := createTempFile(t, format.ext, format.invalid)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var cfg1 validatorConfig
|
||||
err1 := Load(tmpfile1, &cfg1)
|
||||
|
||||
// Test with UseEnv()
|
||||
tmpfile2, err := createTempFile(t, format.ext, format.invalid)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var cfg2 validatorConfig
|
||||
err2 := Load(tmpfile2, &cfg2, UseEnv())
|
||||
|
||||
// Both should fail validation
|
||||
assert.Error(t, err1, "validation should fail without UseEnv()")
|
||||
assert.Error(t, err2, "validation should fail with UseEnv()")
|
||||
assert.Contains(t, err1.Error(), "value must be >= 10")
|
||||
assert.Contains(t, err2.Error(), "value must be >= 10")
|
||||
})
|
||||
|
||||
t.Run("valid_"+format.ext, func(t *testing.T) {
|
||||
// Test without UseEnv()
|
||||
tmpfile1, err := createTempFile(t, format.ext, format.valid)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var cfg1 validatorConfig
|
||||
err1 := Load(tmpfile1, &cfg1)
|
||||
|
||||
// Test with UseEnv()
|
||||
tmpfile2, err := createTempFile(t, format.ext, format.valid)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var cfg2 validatorConfig
|
||||
err2 := Load(tmpfile2, &cfg2, UseEnv())
|
||||
|
||||
// Both should pass validation
|
||||
assert.NoError(t, err1, "validation should pass without UseEnv()")
|
||||
assert.NoError(t, err2, "validation should pass with UseEnv()")
|
||||
assert.Equal(t, validValue, cfg1.Value)
|
||||
assert.Equal(t, validValue, cfg2.Value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func LoadProperties(filename string, opts ...Option) (Properties, error) {
|
||||
|
||||
raw := make(map[string]string)
|
||||
for i := range lines {
|
||||
pair := strings.Split(lines[i], "=")
|
||||
pair := strings.SplitN(lines[i], "=", 2)
|
||||
if len(pair) != 2 {
|
||||
// invalid property format
|
||||
return nil, &PropertyError{
|
||||
|
||||
@@ -92,3 +92,70 @@ func TestLoadBadFile(t *testing.T) {
|
||||
_, err := LoadProperties("nosuchfile")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestProperties_valueWithEqualSymbols(t *testing.T) {
|
||||
text := `# test with equal symbols in value
|
||||
db.url=postgres://localhost:5432/db?param=value
|
||||
math.equation=a=b=c
|
||||
base64.data=SGVsbG8=World=Test=
|
||||
url.with.params=http://example.com?foo=bar&baz=qux
|
||||
empty.value=
|
||||
key.with.space = value = with = equals`
|
||||
tmpfile, err := fs.TempFilenameWithText(text)
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(tmpfile)
|
||||
|
||||
props, err := LoadProperties(tmpfile)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "postgres://localhost:5432/db?param=value", props.GetString("db.url"))
|
||||
assert.Equal(t, "a=b=c", props.GetString("math.equation"))
|
||||
assert.Equal(t, "SGVsbG8=World=Test=", props.GetString("base64.data"))
|
||||
assert.Equal(t, "http://example.com?foo=bar&baz=qux", props.GetString("url.with.params"))
|
||||
assert.Equal(t, "", props.GetString("empty.value"))
|
||||
assert.Equal(t, "value = with = equals", props.GetString("key.with.space"))
|
||||
}
|
||||
|
||||
func TestProperties_edgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "no equal sign",
|
||||
content: "invalid line without equal",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "only equal sign",
|
||||
content: "=",
|
||||
wantErr: false, // "=" 会被解析为空 key 和空 value,len(pair) == 2,是合法的
|
||||
},
|
||||
{
|
||||
name: "empty key",
|
||||
content: "=value",
|
||||
wantErr: false, // 空 key 也会被 trim,但 len(pair) == 2 所以不会报错
|
||||
},
|
||||
{
|
||||
name: "equal at end",
|
||||
content: "key.name=",
|
||||
wantErr: false, // 空 value 是合法的
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpfile, err := fs.TempFilenameWithText(tt.content)
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(tmpfile)
|
||||
|
||||
_, err = LoadProperties(tmpfile)
|
||||
if tt.wantErr {
|
||||
assert.NotNil(t, err, "expected error for case: %s", tt.name)
|
||||
} else {
|
||||
assert.Nil(t, err, "unexpected error for case: %s", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,14 +263,24 @@ func (c *cluster) handleWatchEvents(ctx context.Context, key watchKey, events []
|
||||
for _, ev := range events {
|
||||
switch ev.Type {
|
||||
case clientv3.EventTypePut:
|
||||
evKey := string(ev.Kv.Key)
|
||||
evVal := string(ev.Kv.Value)
|
||||
c.lock.Lock()
|
||||
watcher.values[string(ev.Kv.Key)] = string(ev.Kv.Value)
|
||||
oldVal, exists := watcher.values[evKey]
|
||||
watcher.values[evKey] = evVal
|
||||
c.lock.Unlock()
|
||||
if exists && oldVal == evVal {
|
||||
// duplicate PUT with same value, skip to prevent unbounded growth
|
||||
continue
|
||||
}
|
||||
if exists {
|
||||
// key moved to a new value, notify delete of old entry first
|
||||
for _, l := range listeners {
|
||||
l.OnDelete(KV{Key: evKey, Val: oldVal})
|
||||
}
|
||||
}
|
||||
for _, l := range listeners {
|
||||
l.OnAdd(KV{
|
||||
Key: string(ev.Kv.Key),
|
||||
Val: string(ev.Kv.Value),
|
||||
})
|
||||
l.OnAdd(KV{Key: evKey, Val: evVal})
|
||||
}
|
||||
case clientv3.EventTypeDelete:
|
||||
c.lock.Lock()
|
||||
@@ -433,16 +443,16 @@ func (c *cluster) setupWatch(cli EtcdClient, key watchKey, rev int64) (context.C
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(cli.Ctx())
|
||||
|
||||
c.lock.Lock()
|
||||
if watcher, ok := c.watchers[key]; ok {
|
||||
watcher.cancel = cancel
|
||||
} else {
|
||||
val := newWatchValue()
|
||||
val.cancel = cancel
|
||||
|
||||
c.lock.Lock()
|
||||
c.watchers[key] = val
|
||||
c.lock.Unlock()
|
||||
}
|
||||
c.lock.Unlock()
|
||||
|
||||
rch = cli.Watch(clientv3.WithRequireLeader(ctx), wkey, ops...)
|
||||
|
||||
@@ -508,7 +518,7 @@ func makeKeyPrefix(key string) string {
|
||||
return fmt.Sprintf("%s%c", key, Delimiter)
|
||||
}
|
||||
|
||||
// NewClient returns a watchValue that make sure values are not nil.
|
||||
// newWatchValue returns a watchValue that make sure values are not nil.
|
||||
func newWatchValue() *watchValue {
|
||||
return &watchValue{
|
||||
values: make(map[string]string),
|
||||
|
||||
@@ -477,6 +477,116 @@ func TestRegistry_Unmonitor(t *testing.T) {
|
||||
assert.Nil(t, watchVals)
|
||||
}
|
||||
|
||||
// TestCluster_ConcurrentMonitor tests the race condition fix in setupWatch
|
||||
// This test specifically covers the scenario from issue #5394 where:
|
||||
// - addListener() writes to the watchers map (with lock)
|
||||
// - setupWatch() reads from the watchers map (now with lock after fix)
|
||||
// Running with -race flag will detect any race conditions
|
||||
func TestCluster_ConcurrentMonitor(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
cli := NewMockEtcdClient(ctrl)
|
||||
cli.EXPECT().Ctx().Return(context.Background()).AnyTimes()
|
||||
cli.EXPECT().Watch(gomock.Any(), gomock.Any(), gomock.Any()).Return(make(chan clientv3.WatchResponse)).AnyTimes()
|
||||
|
||||
c := &cluster{
|
||||
endpoints: []string{"localhost:2379"},
|
||||
key: "test-cluster",
|
||||
watchers: make(map[watchKey]*watchValue),
|
||||
watchGroup: threading.NewRoutineGroup(),
|
||||
done: make(chan lang.PlaceholderType),
|
||||
lock: sync.RWMutex{},
|
||||
}
|
||||
|
||||
// Spawn multiple concurrent operations that simulate the race condition:
|
||||
// - Some goroutines call addListener (write to map)
|
||||
// - Some goroutines call setupWatch (read from map)
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 20
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
keys := []watchKey{
|
||||
{key: "key-0", exactMatch: false},
|
||||
{key: "key-1", exactMatch: false},
|
||||
{key: "key-2", exactMatch: false},
|
||||
}
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
idx := i
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
key := keys[idx%len(keys)]
|
||||
|
||||
if idx%2 == 0 {
|
||||
// Half the goroutines add listeners (write operation)
|
||||
c.addListener(key, &mockListener{})
|
||||
} else {
|
||||
// Half the goroutines setup watches (read operation)
|
||||
_, _ = c.setupWatch(cli, key, 0)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
wg.Wait()
|
||||
|
||||
// Verify that watchers were correctly added
|
||||
c.lock.RLock()
|
||||
assert.True(t, len(c.watchers) > 0, "watchers should be added")
|
||||
for _, watcher := range c.watchers {
|
||||
assert.NotNil(t, watcher, "watcher should not be nil")
|
||||
}
|
||||
c.lock.RUnlock()
|
||||
|
||||
// Clean up
|
||||
close(c.done)
|
||||
}
|
||||
|
||||
func TestCluster_handleWatchEvents_DuplicatePut(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
listener := NewMockUpdateListener(ctrl)
|
||||
// OnAdd must be called exactly once despite two PUT events with the same key+value.
|
||||
listener.EXPECT().OnAdd(KV{Key: "hello", Val: "world"}).Times(1)
|
||||
|
||||
c := newCluster([]string{"any"})
|
||||
key := watchKey{key: "any"}
|
||||
c.watchers[key] = &watchValue{
|
||||
listeners: []UpdateListener{listener},
|
||||
values: make(map[string]string),
|
||||
}
|
||||
events := []*clientv3.Event{
|
||||
{Type: clientv3.EventTypePut, Kv: &mvccpb.KeyValue{Key: []byte("hello"), Value: []byte("world")}},
|
||||
{Type: clientv3.EventTypePut, Kv: &mvccpb.KeyValue{Key: []byte("hello"), Value: []byte("world")}},
|
||||
}
|
||||
c.handleWatchEvents(context.Background(), key, events)
|
||||
}
|
||||
|
||||
func TestCluster_handleWatchEvents_ValueChange(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
listener := NewMockUpdateListener(ctrl)
|
||||
gomock.InOrder(
|
||||
listener.EXPECT().OnAdd(KV{Key: "hello", Val: "world1"}),
|
||||
listener.EXPECT().OnDelete(KV{Key: "hello", Val: "world1"}),
|
||||
listener.EXPECT().OnAdd(KV{Key: "hello", Val: "world2"}),
|
||||
)
|
||||
|
||||
c := newCluster([]string{"any"})
|
||||
key := watchKey{key: "any"}
|
||||
c.watchers[key] = &watchValue{
|
||||
listeners: []UpdateListener{listener},
|
||||
values: make(map[string]string),
|
||||
}
|
||||
c.handleWatchEvents(context.Background(), key, []*clientv3.Event{
|
||||
{Type: clientv3.EventTypePut, Kv: &mvccpb.KeyValue{Key: []byte("hello"), Value: []byte("world1")}},
|
||||
})
|
||||
c.handleWatchEvents(context.Background(), key, []*clientv3.Event{
|
||||
{Type: clientv3.EventTypePut, Kv: &mvccpb.KeyValue{Key: []byte("hello"), Value: []byte("world2")}},
|
||||
})
|
||||
}
|
||||
|
||||
type mockListener struct {
|
||||
}
|
||||
|
||||
|
||||
@@ -141,12 +141,23 @@ func (c *container) addKv(key, value string) ([]string, bool) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
oldVal, alreadyMapped := c.mapping[key]
|
||||
if alreadyMapped && oldVal == value {
|
||||
// duplicate PUT with same key+value, nothing to do
|
||||
return nil, false
|
||||
}
|
||||
|
||||
c.dirty.Set(true)
|
||||
if alreadyMapped {
|
||||
// key moved to a new value; remove stale entry to prevent leak
|
||||
c.doRemoveKey(key)
|
||||
}
|
||||
|
||||
keys := c.values[value]
|
||||
previous := append([]string(nil), keys...)
|
||||
early := len(keys) > 0
|
||||
if c.exclusive && early {
|
||||
for _, each := range keys {
|
||||
for _, each := range previous {
|
||||
c.doRemoveKey(each)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +201,81 @@ func TestContainer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainer_DuplicateAdd(t *testing.T) {
|
||||
c := newContainer(false)
|
||||
// Simulate 100 duplicate PUT events for the same key+value.
|
||||
for i := 0; i < 100; i++ {
|
||||
c.OnAdd(internal.KV{Key: "etcd-key", Val: "host:1234"})
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"host:1234"}, c.GetValues())
|
||||
// Internal slice must not have grown beyond one entry.
|
||||
c.lock.Lock()
|
||||
assert.Len(t, c.values["host:1234"], 1)
|
||||
c.lock.Unlock()
|
||||
}
|
||||
|
||||
func TestContainer_KeyValueChange(t *testing.T) {
|
||||
c := newContainer(false)
|
||||
c.OnAdd(internal.KV{Key: "etcd-key", Val: "host:1234"})
|
||||
assert.ElementsMatch(t, []string{"host:1234"}, c.GetValues())
|
||||
|
||||
// Key moves to a different server value.
|
||||
c.OnAdd(internal.KV{Key: "etcd-key", Val: "host:5678"})
|
||||
assert.ElementsMatch(t, []string{"host:5678"}, c.GetValues())
|
||||
|
||||
// Old server must be fully removed; a subsequent delete must leave nothing.
|
||||
c.OnDelete(internal.KV{Key: "etcd-key", Val: "host:5678"})
|
||||
assert.Empty(t, c.GetValues())
|
||||
}
|
||||
|
||||
// TestContainer_ExclusiveMode verifies that adding successive keys for the same
|
||||
// value in exclusive mode leaves only the latest key and evicts all prior ones.
|
||||
func TestContainer_ExclusiveMode(t *testing.T) {
|
||||
c := newContainer(true)
|
||||
c.OnAdd(internal.KV{Key: "key1", Val: "server1"})
|
||||
c.OnAdd(internal.KV{Key: "key2", Val: "server1"})
|
||||
c.OnAdd(internal.KV{Key: "key3", Val: "server1"})
|
||||
|
||||
assert.ElementsMatch(t, []string{"server1"}, c.GetValues())
|
||||
c.lock.Lock()
|
||||
assert.Equal(t, []string{"key3"}, c.values["server1"], "only the latest key must remain")
|
||||
assert.NotContains(t, c.mapping, "key1", "key1 must have been evicted")
|
||||
assert.NotContains(t, c.mapping, "key2", "key2 must have been evicted")
|
||||
assert.Equal(t, "server1", c.mapping["key3"])
|
||||
c.lock.Unlock()
|
||||
}
|
||||
|
||||
// TestContainer_ExclusiveMode_MultipleEvictions injects 3 keys for the same
|
||||
// value directly into internal state and then triggers the exclusive eviction
|
||||
// loop via OnAdd. This exercises the range-over-previous fix: iterating over
|
||||
// the live slice (range keys) would corrupt iteration when doRemoveKey
|
||||
// compacts the shared underlying array in-place, causing the second and third
|
||||
// keys to be skipped; ranging over the deep copy (range previous) is safe.
|
||||
func TestContainer_ExclusiveMode_MultipleEvictions(t *testing.T) {
|
||||
c := newContainer(true)
|
||||
|
||||
// Bypass the exclusive invariant to simulate 3 pre-existing keys for the
|
||||
// same value — the state that would expose the in-place aliasing bug.
|
||||
c.lock.Lock()
|
||||
c.values["server1"] = []string{"key1", "key2", "key3"}
|
||||
c.mapping["key1"] = "server1"
|
||||
c.mapping["key2"] = "server1"
|
||||
c.mapping["key3"] = "server1"
|
||||
c.lock.Unlock()
|
||||
|
||||
// Adding key4 must evict all three existing keys via the exclusive loop.
|
||||
c.OnAdd(internal.KV{Key: "key4", Val: "server1"})
|
||||
|
||||
assert.ElementsMatch(t, []string{"server1"}, c.GetValues())
|
||||
c.lock.Lock()
|
||||
assert.Equal(t, []string{"key4"}, c.values["server1"], "all prior keys must be evicted")
|
||||
assert.NotContains(t, c.mapping, "key1", "key1 must be evicted")
|
||||
assert.NotContains(t, c.mapping, "key2", "key2 must be evicted")
|
||||
assert.NotContains(t, c.mapping, "key3", "key3 must be evicted")
|
||||
assert.Equal(t, "server1", c.mapping["key4"])
|
||||
c.lock.Unlock()
|
||||
}
|
||||
|
||||
func TestSubscriber(t *testing.T) {
|
||||
sub := new(Subscriber)
|
||||
Exclusive()(sub)
|
||||
|
||||
@@ -25,6 +25,29 @@ func TestMd5Hex(t *testing.T) {
|
||||
assert.Equal(t, md5Digest, actual)
|
||||
}
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
result := Hash([]byte(text))
|
||||
assert.NotEqual(t, uint64(0), result)
|
||||
}
|
||||
|
||||
func TestHash_Deterministic(t *testing.T) {
|
||||
data := []byte("consistent-hash-test")
|
||||
first := Hash(data)
|
||||
second := Hash(data)
|
||||
assert.Equal(t, first, second)
|
||||
}
|
||||
|
||||
func TestHash_Empty(t *testing.T) {
|
||||
// Hash should not panic on empty input.
|
||||
result := Hash([]byte{})
|
||||
_ = result
|
||||
}
|
||||
|
||||
func TestMd5Hex_Empty(t *testing.T) {
|
||||
result := Md5Hex([]byte{})
|
||||
assert.Equal(t, 32, len(result))
|
||||
}
|
||||
|
||||
func BenchmarkHashFnv(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
h := fnv.New32()
|
||||
|
||||
@@ -247,7 +247,7 @@ func TestStructedLogDebugf(t *testing.T) {
|
||||
defer writer.Store(old)
|
||||
|
||||
doTestStructedLog(t, levelDebug, w, func(v ...any) {
|
||||
Debugf(fmt.Sprint(v...))
|
||||
Debugf("%s", fmt.Sprint(v...))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -559,7 +559,7 @@ func TestStructedLogSlowf(t *testing.T) {
|
||||
defer writer.Store(old)
|
||||
|
||||
doTestStructedLog(t, levelSlow, w, func(v ...any) {
|
||||
Slowf(fmt.Sprint(v...))
|
||||
Slowf("%s", fmt.Sprint(v...))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -625,7 +625,7 @@ func TestStructedLogStatf(t *testing.T) {
|
||||
defer writer.Store(old)
|
||||
|
||||
doTestStructedLog(t, levelStat, w, func(v ...any) {
|
||||
Statf(fmt.Sprint(v...))
|
||||
Statf("%s", fmt.Sprint(v...))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -645,7 +645,7 @@ func TestStructedLogSeveref(t *testing.T) {
|
||||
defer writer.Store(old)
|
||||
|
||||
doTestStructedLog(t, levelSevere, w, func(v ...any) {
|
||||
Severef(fmt.Sprint(v...))
|
||||
Severef("%s", fmt.Sprint(v...))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# logx
|
||||
|
||||
[English](readme.md) | 简体中文
|
||||
[English](readme.md) | 简体中文 | [한국어](readme-ko.md)
|
||||
|
||||
## logx 配置
|
||||
|
||||
|
||||
205
core/logx/readme-ko.md
Normal file
205
core/logx/readme-ko.md
Normal file
@@ -0,0 +1,205 @@
|
||||
<img align="right" width="150px" src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/go-zero.png">
|
||||
|
||||
# logx
|
||||
|
||||
[English](readme.md) | [简体中文](readme-cn.md) | 한국어
|
||||
|
||||
## logx 설정
|
||||
|
||||
```go
|
||||
type LogConf struct {
|
||||
ServiceName string `json:",optional"`
|
||||
Mode string `json:",default=console,options=[console,file,volume]"`
|
||||
Encoding string `json:",default=json,options=[json,plain]"`
|
||||
TimeFormat string `json:",optional"`
|
||||
Path string `json:",default=logs"`
|
||||
Level string `json:",default=info,options=[info,error,severe]"`
|
||||
Compress bool `json:",optional"`
|
||||
KeepDays int `json:",optional"`
|
||||
StackCooldownMillis int `json:",default=100"`
|
||||
MaxBackups int `json:",default=0"`
|
||||
MaxSize int `json:",default=0"`
|
||||
Rotation string `json:",default=daily,options=[daily,size]"`
|
||||
}
|
||||
```
|
||||
|
||||
- `ServiceName`: 서비스 이름을 설정합니다. 선택 사항입니다. `volume` 모드에서는 이 이름이 로그 파일 생성에 사용됩니다. `rest/zrpc` 서비스에서는 이름이 `rest` 또는 `zrpc`의 이름으로 자동 설정됩니다.
|
||||
- `Mode`: 로그 출력 모드입니다. 기본값은 `console`입니다.
|
||||
- `console` 모드는 로그를 `stdout/stderr`에 씁니다.
|
||||
- `file` 모드는 `Path`로 지정한 파일에 로그를 씁니다.
|
||||
- `volume` 모드는 docker에서 사용하며, 마운트된 볼륨에 로그를 씁니다.
|
||||
- `Encoding`: 로그 인코딩 방식을 나타냅니다. 기본값은 `json`입니다.
|
||||
- `json` 모드는 로그를 json 형식으로 씁니다.
|
||||
- `plain` 모드는 터미널 색상이 활성화된 일반 텍스트로 로그를 씁니다.
|
||||
- `TimeFormat`: 시간 형식을 사용자 지정합니다. 선택 사항입니다. 기본값은 `2006-01-02T15:04:05.000Z07:00`입니다.
|
||||
- `Path`: 로그 경로를 설정합니다. 기본값은 `logs`입니다.
|
||||
- `Level`: 로그를 필터링할 로깅 레벨입니다. 기본값은 `info`입니다.
|
||||
- `info`: 모든 로그가 기록됩니다.
|
||||
- `error`: `info` 로그가 억제됩니다.
|
||||
- `severe`: `info`와 `error` 로그가 억제되고 `severe` 로그만 기록됩니다.
|
||||
- `Compress`: 로그 파일 압축 여부입니다. `file` 모드에서만 동작합니다.
|
||||
- `KeepDays`: 로그 파일을 보관할 일수입니다. 지정한 일수가 지나면 오래된 파일이 자동으로 삭제됩니다. `console` 모드에는 영향을 주지 않습니다.
|
||||
- `StackCooldownMillis`: 스택 트레이스를 다시 기록하기까지의 밀리초입니다. 스택 트레이스 로그 폭주를 방지하는 데 사용됩니다.
|
||||
- `MaxBackups`: 보관할 백업 로그 파일 개수입니다. 0은 모든 파일을 영구 보관한다는 의미입니다. `Rotation`이 `size`일 때만 적용됩니다. 참고: `KeepDays` 옵션의 우선순위가 더 높습니다. `MaxBackups`가 0이더라도 `KeepDays` 제한에 도달하면 로그 파일은 삭제됩니다.
|
||||
- `MaxSize`: 현재 기록 중인 로그 파일이 차지할 수 있는 최대 공간입니다. 0은 제한 없음을 의미합니다. 단위는 `MB`입니다. `Rotation`이 `size`일 때만 적용됩니다.
|
||||
- `Rotation`: 로그 로테이션 규칙의 유형입니다. 기본값은 `daily`입니다.
|
||||
- `daily`: 날짜 단위로 로그를 회전합니다.
|
||||
- `size`: 로그 크기 단위로 로그를 회전합니다.
|
||||
|
||||
## 로깅 메서드
|
||||
|
||||
```go
|
||||
type Logger interface {
|
||||
// Error logs a message at error level.
|
||||
Error(...any)
|
||||
// Errorf logs a message at error level.
|
||||
Errorf(string, ...any)
|
||||
// Errorv logs a message at error level.
|
||||
Errorv(any)
|
||||
// Errorw logs a message at error level.
|
||||
Errorw(string, ...LogField)
|
||||
// Info logs a message at info level.
|
||||
Info(...any)
|
||||
// Infof logs a message at info level.
|
||||
Infof(string, ...any)
|
||||
// Infov logs a message at info level.
|
||||
Infov(any)
|
||||
// Infow logs a message at info level.
|
||||
Infow(string, ...LogField)
|
||||
// Slow logs a message at slow level.
|
||||
Slow(...any)
|
||||
// Slowf logs a message at slow level.
|
||||
Slowf(string, ...any)
|
||||
// Slowv logs a message at slow level.
|
||||
Slowv(any)
|
||||
// Sloww logs a message at slow level.
|
||||
Sloww(string, ...LogField)
|
||||
// WithContext returns a new logger with the given context.
|
||||
WithContext(context.Context) Logger
|
||||
// WithDuration returns a new logger with the given duration.
|
||||
WithDuration(time.Duration) Logger
|
||||
}
|
||||
```
|
||||
|
||||
- `Error`, `Info`, `Slow`: `fmt.Sprint(…)`처럼 모든 종류의 메시지를 로그에 씁니다.
|
||||
- `Errorf`, `Infof`, `Slowf`: 지정한 형식으로 메시지를 로그에 씁니다.
|
||||
- `Errorv`, `Infov`, `Slowv`: 모든 종류의 메시지를 json 마샬링으로 인코딩해 로그에 씁니다.
|
||||
- `Errorw`, `Infow`, `Sloww`: 지정한 `key:value` 필드와 함께 문자열 메시지를 씁니다.
|
||||
- `WithContext`: 지정한 ctx를 로그 메시지에 주입합니다. 일반적으로 `trace-id`와 `span-id`를 기록하는 데 사용됩니다.
|
||||
- `WithDuration`: 경과 시간을 `duration` 키로 로그 메시지에 씁니다.
|
||||
|
||||
## 타사 로깅 라이브러리와 통합
|
||||
|
||||
- zap
|
||||
- 구현: [https://github.com/zeromicro/zero-contrib/blob/main/logx/zapx/zap.go](https://github.com/zeromicro/zero-contrib/blob/main/logx/zapx/zap.go)
|
||||
- 사용 예시: [https://github.com/zeromicro/zero-examples/blob/main/logx/zaplog/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/zaplog/main.go)
|
||||
- logrus
|
||||
- 구현: [https://github.com/zeromicro/zero-contrib/blob/main/logx/logrusx/logrus.go](https://github.com/zeromicro/zero-contrib/blob/main/logx/logrusx/logrus.go)
|
||||
- 사용 예시: [https://github.com/zeromicro/zero-examples/blob/main/logx/logrus/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/logrus/main.go)
|
||||
|
||||
더 많은 라이브러리는 직접 구현한 뒤 [https://github.com/zeromicro/zero-contrib](https://github.com/zeromicro/zero-contrib)에 PR을 보내주세요.
|
||||
|
||||
## 특정 저장소에 로그 쓰기
|
||||
|
||||
`logx`는 로그를 원하는 저장소에 쓸 수 있도록 사용자 지정할 수 있는 두 인터페이스를 정의합니다.
|
||||
|
||||
- `logx.NewWriter(w io.Writer)`
|
||||
- `logx.SetWriter(writer logx.Writer)`
|
||||
|
||||
예를 들어 로그를 콘솔이나 파일 대신 kafka에 쓰고 싶다면 아래처럼 할 수 있습니다.
|
||||
|
||||
```go
|
||||
type KafkaWriter struct {
|
||||
Pusher *kq.Pusher
|
||||
}
|
||||
|
||||
func NewKafkaWriter(pusher *kq.Pusher) *KafkaWriter {
|
||||
return &KafkaWriter{
|
||||
Pusher: pusher,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *KafkaWriter) Write(p []byte) (n int, err error) {
|
||||
// writing log with newlines, trim them.
|
||||
if err := w.Pusher.Push(strings.TrimSpace(string(p))); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
pusher := kq.NewPusher([]string{"localhost:9092"}, "go-zero")
|
||||
defer pusher.Close()
|
||||
|
||||
writer := logx.NewWriter(NewKafkaWriter(pusher))
|
||||
logx.SetWriter(writer)
|
||||
|
||||
// more code
|
||||
}
|
||||
```
|
||||
|
||||
전체 코드: [https://github.com/zeromicro/zero-examples/blob/main/logx/tokafka/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/tokafka/main.go)
|
||||
|
||||
## 민감한 필드 필터링
|
||||
|
||||
`password` 필드가 로그에 기록되지 않도록 하려면 아래처럼 할 수 있습니다.
|
||||
|
||||
```go
|
||||
type (
|
||||
Message struct {
|
||||
Name string
|
||||
Password string
|
||||
Message string
|
||||
}
|
||||
|
||||
SensitiveLogger struct {
|
||||
logx.Writer
|
||||
}
|
||||
)
|
||||
|
||||
func NewSensitiveLogger(writer logx.Writer) *SensitiveLogger {
|
||||
return &SensitiveLogger{
|
||||
Writer: writer,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *SensitiveLogger) Info(msg any, fields ...logx.LogField) {
|
||||
if m, ok := msg.(Message); ok {
|
||||
l.Writer.Info(Message{
|
||||
Name: m.Name,
|
||||
Password: "******",
|
||||
Message: m.Message,
|
||||
}, fields...)
|
||||
} else {
|
||||
l.Writer.Info(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// setup logx to make sure originalWriter not nil,
|
||||
// the injected writer is only for filtering, like a middleware.
|
||||
|
||||
originalWriter := logx.Reset()
|
||||
writer := NewSensitiveLogger(originalWriter)
|
||||
logx.SetWriter(writer)
|
||||
|
||||
logx.Infov(Message{
|
||||
Name: "foo",
|
||||
Password: "shouldNotAppear",
|
||||
Message: "bar",
|
||||
})
|
||||
|
||||
// more code
|
||||
}
|
||||
```
|
||||
|
||||
전체 코드: [https://github.com/zeromicro/zero-examples/blob/main/logx/filterfields/main.go](https://github.com/zeromicro/zero-examples/blob/main/logx/filterfields/main.go)
|
||||
|
||||
## 더 많은 예제
|
||||
|
||||
[https://github.com/zeromicro/zero-examples/tree/main/logx](https://github.com/zeromicro/zero-examples/tree/main/logx)
|
||||
|
||||
## 별을 눌러주세요! ⭐
|
||||
|
||||
이 프로젝트가 마음에 들거나 학습 또는 자체 솔루션을 시작하는 데 사용 중이라면 star를 눌러주세요. 감사합니다!
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# logx
|
||||
|
||||
English | [简体中文](readme-cn.md)
|
||||
English | [简体中文](readme-cn.md) | [한국어](readme-ko.md)
|
||||
|
||||
## logx configurations
|
||||
|
||||
|
||||
@@ -931,6 +931,113 @@ func TestUnmarshalJsonArray(t *testing.T) {
|
||||
assert.Equal(t, 18, v[0].Age)
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesPointerSliceUint64(t *testing.T) {
|
||||
t.Run("with values", func(t *testing.T) {
|
||||
var c struct {
|
||||
IDs *[]uint64 `json:"ids,optional"`
|
||||
}
|
||||
content := []byte(`{"ids":[9000,9001]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.IDs)
|
||||
assert.Equal(t, []uint64{9000, 9001}, *c.IDs)
|
||||
})
|
||||
|
||||
t.Run("omitted", func(t *testing.T) {
|
||||
var c struct {
|
||||
IDs *[]uint64 `json:"ids,optional"`
|
||||
}
|
||||
content := []byte(`{}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.Nil(t, c.IDs)
|
||||
})
|
||||
|
||||
t.Run("null", func(t *testing.T) {
|
||||
var c struct {
|
||||
IDs *[]uint64 `json:"ids,optional"`
|
||||
}
|
||||
content := []byte(`{"ids":null}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.Nil(t, c.IDs)
|
||||
})
|
||||
|
||||
t.Run("empty array", func(t *testing.T) {
|
||||
var c struct {
|
||||
IDs *[]uint64 `json:"ids,optional"`
|
||||
}
|
||||
content := []byte(`{"ids":[]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.IDs)
|
||||
assert.Equal(t, []uint64{}, *c.IDs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesPointerSliceOtherTypes(t *testing.T) {
|
||||
t.Run("pointer to []string", func(t *testing.T) {
|
||||
var c struct {
|
||||
Names *[]string `json:"names,optional"`
|
||||
}
|
||||
content := []byte(`{"names":["a","b"]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.Names)
|
||||
assert.Equal(t, []string{"a", "b"}, *c.Names)
|
||||
})
|
||||
|
||||
t.Run("pointer to []int", func(t *testing.T) {
|
||||
var c struct {
|
||||
Values *[]int `json:"values,optional"`
|
||||
}
|
||||
content := []byte(`{"values":[1,2,3]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.Values)
|
||||
assert.Equal(t, []int{1, 2, 3}, *c.Values)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesPointerSliceStruct(t *testing.T) {
|
||||
type Item struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
t.Run("with values", func(t *testing.T) {
|
||||
var c struct {
|
||||
Items *[]Item `json:"items,optional"`
|
||||
}
|
||||
content := []byte(`{"items":[{"name":"alice","age":30},{"name":"bob","age":25}]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.Items)
|
||||
assert.Equal(t, []Item{{Name: "alice", Age: 30}, {Name: "bob", Age: 25}}, *c.Items)
|
||||
})
|
||||
|
||||
t.Run("omitted", func(t *testing.T) {
|
||||
var c struct {
|
||||
Items *[]Item `json:"items,optional"`
|
||||
}
|
||||
content := []byte(`{}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.Nil(t, c.Items)
|
||||
})
|
||||
|
||||
t.Run("empty array", func(t *testing.T) {
|
||||
var c struct {
|
||||
Items *[]Item `json:"items,optional"`
|
||||
}
|
||||
content := []byte(`{"items":[]}`)
|
||||
|
||||
assert.Nil(t, UnmarshalJsonBytes(content, &c))
|
||||
assert.NotNil(t, c.Items)
|
||||
assert.Equal(t, []Item{}, *c.Items)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnmarshalJsonBytesError(t *testing.T) {
|
||||
var v []struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -142,11 +142,11 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
|
||||
return nil
|
||||
}
|
||||
|
||||
baseType := fieldType.Elem()
|
||||
baseType := Deref(fieldType).Elem()
|
||||
dereffedBaseType := Deref(baseType)
|
||||
dereffedBaseKind := dereffedBaseType.Kind()
|
||||
if refValue.Len() == 0 {
|
||||
value.Set(reflect.MakeSlice(reflect.SliceOf(baseType), 0, 0))
|
||||
SetValue(fieldType, value, reflect.MakeSlice(reflect.SliceOf(baseType), 0, 0))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func (u *Unmarshaler) fillSlice(fieldType reflect.Type, value reflect.Value,
|
||||
}
|
||||
|
||||
if valid {
|
||||
value.Set(conv)
|
||||
SetValue(fieldType, value, conv)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -201,7 +201,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
|
||||
return errUnsupportedType
|
||||
}
|
||||
|
||||
baseFieldType := fieldType.Elem()
|
||||
baseFieldType := Deref(fieldType).Elem()
|
||||
baseFieldKind := baseFieldType.Kind()
|
||||
conv := reflect.MakeSlice(reflect.SliceOf(baseFieldType), len(slice), cap(slice))
|
||||
|
||||
@@ -211,7 +211,7 @@ func (u *Unmarshaler) fillSliceFromString(fieldType reflect.Type, value reflect.
|
||||
}
|
||||
}
|
||||
|
||||
value.Set(conv)
|
||||
SetValue(fieldType, value, conv)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mapping
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -12,7 +13,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/lang"
|
||||
"github.com/zeromicro/go-zero/core/stringx"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -278,7 +278,7 @@ func parseKeyAndOptions(tagName string, field reflect.StructField) (string, *fie
|
||||
cache, ok := optionsCache[value]
|
||||
cacheLock.RUnlock()
|
||||
if ok {
|
||||
return stringx.TakeOne(cache.key, field.Name), cache.options, cache.err
|
||||
return cmp.Or(cache.key, field.Name), cache.options, cache.err
|
||||
}
|
||||
|
||||
key, options, err := doParseKeyAndOptions(field, value)
|
||||
@@ -290,7 +290,7 @@ func parseKeyAndOptions(tagName string, field reflect.StructField) (string, *fie
|
||||
}
|
||||
cacheLock.Unlock()
|
||||
|
||||
return stringx.TakeOne(key, field.Name), options, err
|
||||
return cmp.Or(key, field.Name), options, err
|
||||
}
|
||||
|
||||
// support below notations:
|
||||
|
||||
@@ -29,3 +29,10 @@ func TestCalcDiffEntropy(t *testing.T) {
|
||||
}
|
||||
assert.True(t, CalcEntropy(m) < .99)
|
||||
}
|
||||
|
||||
func TestCalcEntropySingleItem(t *testing.T) {
|
||||
m := map[any]int{
|
||||
"only": 42,
|
||||
}
|
||||
assert.Equal(t, float64(1), CalcEntropy(m))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package mathx
|
||||
|
||||
// Numerical is a constraint that permits any numeric type.
|
||||
type Numerical interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# mapreduce
|
||||
|
||||
[English](readme.md) | 简体中文
|
||||
[English](readme.md) | 简体中文 | [한국어](readme-ko.md)
|
||||
|
||||
## 为什么需要 MapReduce
|
||||
|
||||
在实际的业务场景中我们常常需要从不同的 rpc 服务中获取相应属性来组装成复杂对象。
|
||||
在实际的业务场景中我们常常需要从不同的 RPC 服务中获取相应属性来组装成复杂对象。
|
||||
|
||||
比如要查询商品详情:
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
3. 价格服务-查询价格属性
|
||||
4. 营销服务-查询营销属性
|
||||
|
||||
如果是串行调用的话响应时间会随着 rpc 调用次数呈线性增长,所以我们要优化性能一般会将串行改并行。
|
||||
如果是串行调用的话响应时间会随着 RPC 调用次数呈线性增长,所以我们要优化性能一般会将串行改并行。
|
||||
|
||||
简单的场景下使用 `WaitGroup` 也能够满足需求,但是如果我们需要对 rpc 调用返回的数据进行校验、数据加工转换、数据汇总呢?继续使用 `WaitGroup` 就有点力不从心了,go 的官方库中并没有这种工具(java 中提供了 CompleteFuture),我们依据 MapReduce 架构思想实现了进程内的数据批处理 MapReduce 并发工具类。
|
||||
简单的场景下使用 `WaitGroup` 也能够满足需求,但是如果我们需要对 RPC 调用返回的数据进行校验、数据加工转换、数据汇总呢?继续使用 `WaitGroup` 就有点力不从心了,go 的官方库中并没有这种工具(java 中提供了 CompletableFuture),我们依据 MapReduce 架构思想实现了进程内的数据批处理 MapReduce 并发工具类。
|
||||
|
||||
## 设计思路
|
||||
|
||||
|
||||
89
core/mr/readme-ko.md
Normal file
89
core/mr/readme-ko.md
Normal file
@@ -0,0 +1,89 @@
|
||||
<img align="right" width="150px" src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/go-zero.png">
|
||||
|
||||
# mapreduce
|
||||
|
||||
[English](readme.md) | [简体中文](readme-cn.md) | 한국어
|
||||
|
||||
## MapReduce가 필요한 이유
|
||||
|
||||
실제 비즈니스 시나리오에서는 서로 다른 RPC 서비스에서 속성을 가져와 복잡한 객체를 조립해야 하는 경우가 많습니다.
|
||||
|
||||
예를 들어 상품 상세 정보를 조회한다고 해봅시다.
|
||||
|
||||
1. 상품 서비스 - 상품 속성 조회
|
||||
2. 재고 서비스 - 재고 속성 조회
|
||||
3. 가격 서비스 - 가격 속성 조회
|
||||
4. 마케팅 서비스 - 마케팅 속성 조회
|
||||
|
||||
직렬 호출이라면 RPC 호출 횟수에 따라 응답 시간이 선형적으로 증가하므로, 일반적으로 응답 시간을 최적화하기 위해 직렬 호출을 병렬 호출로 바꿉니다.
|
||||
|
||||
단순한 시나리오에서는 `WaitGroup`만으로도 요구 사항을 충족할 수 있습니다. 하지만 RPC 호출이 반환한 데이터를 검증하거나, 데이터를 처리하거나, 데이터를 집계해야 한다면 어떻게 해야 할까요? Go 표준 라이브러리에는 이런 도구가 없습니다(Java에는 CompletableFuture가 제공됩니다). 그래서 우리는 MapReduce 아키텍처를 기반으로 프로세스 내부 데이터 배치 처리를 위한 MapReduce 동시성 도구를 구현했습니다.
|
||||
|
||||
## 설계 아이디어
|
||||
|
||||
동시성 도구가 필요한 비즈니스 시나리오를 작성자의 관점에서 정리해봅시다.
|
||||
|
||||
1. 상품 상세 조회: 여러 서비스를 동시에 호출해 상품 속성을 조합하고, 호출 오류가 발생하면 즉시 종료할 수 있어야 합니다.
|
||||
2. 상품 상세 페이지에서 사용자 쿠폰 자동 추천: 쿠폰을 동시에 검증하고, 검증에 실패한 쿠폰은 자동으로 제외하며, 나머지를 모두 반환할 수 있어야 합니다.
|
||||
|
||||
위 시나리오는 모두 입력 데이터를 처리한 뒤 정제된 데이터를 출력하는 과정입니다. 데이터 처리에는 아주 고전적인 비동기 패턴인 생산자-소비자 패턴이 있습니다. 따라서 데이터 배치 처리의 생명 주기를 추상화하면 대략 세 단계로 나눌 수 있습니다.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/mapreduce-serial-en.png" width="500">
|
||||
|
||||
1. 데이터 생성(generator)
|
||||
2. 데이터 처리(mapper)
|
||||
3. 데이터 집계(reducer)
|
||||
|
||||
데이터 생성은 필수 단계이고, 데이터 처리와 데이터 집계는 선택 단계입니다. 데이터 생성과 처리는 동시 호출을 지원하며, 데이터 집계는 기본적으로 순수 메모리 작업이므로 단일 고루틴으로 처리할 수 있습니다.
|
||||
|
||||
서로 다른 데이터 처리 단계가 서로 다른 고루틴에서 수행되므로, 고루틴 간 통신을 위해 채널을 사용하는 것이 자연스럽습니다.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/mapreduce-en.png" width="500">
|
||||
|
||||
언제든 프로세스를 종료하려면 어떻게 해야 할까요?
|
||||
|
||||
간단합니다. 고루틴에서 채널 또는 전달된 context의 완료 신호를 감시하면 됩니다.
|
||||
|
||||
## 간단한 예시
|
||||
|
||||
동시성을 시뮬레이션하며 제곱합을 계산합니다.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/mr"
|
||||
)
|
||||
|
||||
func main() {
|
||||
val, err := mr.MapReduce(func(source chan<- int) {
|
||||
// generator
|
||||
for i := 0; i < 10; i++ {
|
||||
source <- i
|
||||
}
|
||||
}, func(i int, writer mr.Writer[int], cancel func(error)) {
|
||||
// mapper
|
||||
writer.Write(i * i)
|
||||
}, func(pipe <-chan int, writer mr.Writer[int], cancel func(error)) {
|
||||
// reducer
|
||||
var sum int
|
||||
for i := range pipe {
|
||||
sum += i
|
||||
}
|
||||
writer.Write(sum)
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("result:", val)
|
||||
}
|
||||
```
|
||||
|
||||
더 많은 예제: [https://github.com/zeromicro/zero-examples/tree/main/mapreduce](https://github.com/zeromicro/zero-examples/tree/main/mapreduce)
|
||||
|
||||
## 별을 눌러주세요! ⭐
|
||||
|
||||
이 프로젝트가 마음에 들거나 학습 또는 자체 솔루션을 시작하는 데 사용 중이라면 star를 눌러주세요. 감사합니다!
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
# mapreduce
|
||||
|
||||
English | [简体中文](readme-cn.md)
|
||||
English | [简体中文](readme-cn.md) | [한국어](readme-ko.md)
|
||||
|
||||
## Why MapReduce is needed
|
||||
|
||||
In practical business scenarios we often need to get the corresponding properties from different rpc services to assemble complex objects.
|
||||
In practical business scenarios we often need to get the corresponding properties from different RPC services to assemble complex objects.
|
||||
|
||||
For example, to query product details.
|
||||
|
||||
@@ -15,9 +15,9 @@ For example, to query product details.
|
||||
3. price service - query price attributes
|
||||
4. marketing service - query marketing properties
|
||||
|
||||
If it is a serial call, the response time will increase linearly with the number of rpc calls, so we will generally change serial to parallel to optimize response time.
|
||||
If it is a serial call, the response time will increase linearly with the number of RPC calls, so we will generally change serial to parallel to optimize response time.
|
||||
|
||||
Simple scenarios using `WaitGroup` can also meet the needs, but what if we need to check the data returned by the rpc call, data processing, data aggregation? The official go library does not have such a tool (CompleteFuture is provided in java), so we implemented an in-process data batching MapReduce concurrent tool based on the MapReduce architecture.
|
||||
Simple scenarios using `WaitGroup` can also meet the needs, but what if we need to check the data returned by the RPC call, data processing, data aggregation? The official go library does not have such a tool (CompletableFuture is provided in java), so we implemented an in-process data batching MapReduce concurrent tool based on the MapReduce architecture.
|
||||
|
||||
## Design ideas
|
||||
|
||||
|
||||
@@ -542,7 +542,7 @@ func mustNewTestModel(collection mon.Collection, c cache.CacheConf, opts ...cach
|
||||
}
|
||||
}
|
||||
|
||||
// NewNodeModel returns a test Model with a cache node.
|
||||
// mustNewTestNodeModel returns a test Model with a cache node.
|
||||
func mustNewTestNodeModel(collection mon.Collection, rds *redis.Redis, opts ...cache.Option) *Model {
|
||||
c := cache.NewNode(rds, singleFlight, stats, mongo.ErrNoDocuments, opts...)
|
||||
return &Model{
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
var ignoreCmds = map[string]lang.PlaceholderType{
|
||||
"blpop": {},
|
||||
"hello": {},
|
||||
}
|
||||
|
||||
type breakerHook struct {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
red "github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/breaker"
|
||||
)
|
||||
@@ -75,6 +76,45 @@ func TestBreakerHook_ProcessHook(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, someError.Error(), err.Error())
|
||||
})
|
||||
|
||||
t.Run("breakerHook_ignoreHello", func(t *testing.T) {
|
||||
// hello is issued on connection init and is in ignoreCmds, so repeated
|
||||
// failures must never trip the breaker into ErrServiceUnavailable.
|
||||
h := breakerHook{brk: breaker.NewBreaker()}
|
||||
someError := errors.New("ERR some error")
|
||||
process := h.ProcessHook(func(_ context.Context, _ red.Cmder) error {
|
||||
return someError
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
var err error
|
||||
for i := 0; i < 1000; i++ {
|
||||
err = process(ctx, red.NewCmd(ctx, "hello", 3))
|
||||
if err != nil && err.Error() != someError.Error() {
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Equal(t, someError.Error(), err.Error())
|
||||
})
|
||||
|
||||
t.Run("breakerHook_notIgnored", func(t *testing.T) {
|
||||
// a regular command is not ignored, so repeated failures open the breaker.
|
||||
h := breakerHook{brk: breaker.NewBreaker()}
|
||||
someError := errors.New("ERR some error")
|
||||
process := h.ProcessHook(func(_ context.Context, _ red.Cmder) error {
|
||||
return someError
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
var err error
|
||||
for i := 0; i < 1000; i++ {
|
||||
err = process(ctx, red.NewCmd(ctx, "get", "key"))
|
||||
if err != nil && err.Error() != someError.Error() {
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Equal(t, breaker.ErrServiceUnavailable, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBreakerHook_ProcessPipelineHook(t *testing.T) {
|
||||
|
||||
@@ -23,6 +23,30 @@ type (
|
||||
Pass string `json:",optional"`
|
||||
Tls bool `json:",optional"`
|
||||
NonBlock bool `json:",default=true"`
|
||||
// DisableIdentity is used to disable CLIENT SETINFO command on connect.
|
||||
//
|
||||
// Some redis versions/proxies do not support CLIENT SETINFO and return an
|
||||
// error on connect; since that command runs through the breaker hook it can
|
||||
// trip the breaker. Set this to true to skip it on such servers. Together
|
||||
// with the default MaintNotifications=disabled (and the always-ignored
|
||||
// HELLO command), this keeps the connect-time commands from tripping the
|
||||
// breaker on incompatible servers, without forcing RESP2.
|
||||
//
|
||||
// default: false
|
||||
DisableIdentity bool `json:",default=false"`
|
||||
// Protocol 2 or 3. Use the version to negotiate RESP version with redis-server.
|
||||
//
|
||||
// default: 3.
|
||||
Protocol int `json:",default=3"`
|
||||
// MaintNotifications controls the CLIENT MAINT_NOTIFICATIONS handshake mode
|
||||
// (go-redis MaintNotificationsConfig.Mode):
|
||||
// - disabled: never send the command (avoids tripping the breaker on servers
|
||||
// that don't support it; keeps RESP3 intact)
|
||||
// - auto: try, silently fall back on error (go-redis default)
|
||||
// - enabled: force, fail the connection on error
|
||||
//
|
||||
// default: disabled
|
||||
MaintNotifications string `json:",default=disabled,options=disabled|enabled|auto"`
|
||||
// PingTimeout is the timeout for ping redis.
|
||||
PingTimeout time.Duration `json:",default=1s"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
red "github.com/redis/go-redis/v9"
|
||||
"github.com/redis/go-redis/v9/maintnotifications"
|
||||
"github.com/zeromicro/go-zero/core/breaker"
|
||||
"github.com/zeromicro/go-zero/core/errorx"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
@@ -53,18 +54,22 @@ type (
|
||||
|
||||
// Redis defines a redis node/cluster. It is thread-safe.
|
||||
Redis struct {
|
||||
Addr string
|
||||
Type string
|
||||
User string
|
||||
Pass string
|
||||
tls bool
|
||||
brk breaker.Breaker
|
||||
hooks []red.Hook
|
||||
Addr string
|
||||
Type string
|
||||
User string
|
||||
Pass string
|
||||
protocol int
|
||||
identity bool
|
||||
maintNotifications maintnotifications.Mode
|
||||
tls bool
|
||||
brk breaker.Breaker
|
||||
hooks []red.Hook
|
||||
}
|
||||
|
||||
// RedisNode interface represents a redis node.
|
||||
RedisNode interface {
|
||||
red.Cmdable
|
||||
Do(ctx context.Context, args ...any) *red.Cmd
|
||||
}
|
||||
|
||||
// GeoLocation is used with GeoAdd to add geospatial location.
|
||||
@@ -135,6 +140,15 @@ func NewRedis(conf RedisConf, opts ...Option) (*Redis, error) {
|
||||
if conf.Tls {
|
||||
opts = append([]Option{WithTLS()}, opts...)
|
||||
}
|
||||
if conf.Protocol > 0 {
|
||||
opts = append([]Option{WithProtocol(conf.Protocol)}, opts...)
|
||||
}
|
||||
if conf.DisableIdentity {
|
||||
opts = append([]Option{WithIdentity()}, opts...)
|
||||
}
|
||||
if len(conf.MaintNotifications) > 0 {
|
||||
opts = append([]Option{WithMaintNotifications(conf.MaintNotifications)}, opts...)
|
||||
}
|
||||
|
||||
rds := newRedis(conf.Host, opts...)
|
||||
if !conf.NonBlock {
|
||||
@@ -146,20 +160,6 @@ func NewRedis(conf RedisConf, opts ...Option) (*Redis, error) {
|
||||
return rds, nil
|
||||
}
|
||||
|
||||
func newRedis(addr string, opts ...Option) *Redis {
|
||||
r := &Redis{
|
||||
Addr: addr,
|
||||
Type: NodeType,
|
||||
brk: breaker.NewBreaker(),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// NewScript returns a new Script instance.
|
||||
func NewScript(script string) *Script {
|
||||
return red.NewScript(script)
|
||||
@@ -405,6 +405,25 @@ func (s *Redis) DelCtx(ctx context.Context, keys ...string) (int, error) {
|
||||
return int(v), nil
|
||||
}
|
||||
|
||||
// Do executes a generic redis command with given arguments.
|
||||
func (s *Redis) Do(args ...any) (any, error) {
|
||||
return s.DoCtx(context.Background(), args...)
|
||||
}
|
||||
|
||||
// DoCtx executes a generic redis command with given arguments using the provided context.
|
||||
func (s *Redis) DoCtx(ctx context.Context, args ...any) (any, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, errors.New("missing redis command")
|
||||
}
|
||||
|
||||
conn, err := getRedis(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn.Do(ctx, args...).Result()
|
||||
}
|
||||
|
||||
// Eval is the implementation of redis eval command.
|
||||
func (s *Redis) Eval(script string, keys []string, args ...any) (any, error) {
|
||||
return s.EvalCtx(context.Background(), script, keys, args...)
|
||||
@@ -1848,6 +1867,21 @@ func (s *Redis) XGroupCreateCtx(ctx context.Context, stream string, group string
|
||||
return conn.XGroupCreate(ctx, stream, group, start).Result()
|
||||
}
|
||||
|
||||
// XGroupSetID sets the last delivered ID for a Redis stream consumer group.
|
||||
func (s *Redis) XGroupSetID(stream, group, start string) (string, error) {
|
||||
return s.XGroupSetIDCtx(context.Background(), stream, group, start)
|
||||
}
|
||||
|
||||
// XGroupSetIDCtx is the context-aware version of XGroupSetID.
|
||||
func (s *Redis) XGroupSetIDCtx(ctx context.Context, stream, group, start string) (string, error) {
|
||||
conn, err := getRedis(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return conn.XGroupSetID(ctx, stream, group, start).Result()
|
||||
}
|
||||
|
||||
// XInfoConsumers returns information about consumers in a Redis stream consumer group.
|
||||
func (s *Redis) XInfoConsumers(stream string, group string) ([]red.XInfoConsumer, error) {
|
||||
return s.XInfoConsumersCtx(context.Background(), stream, group)
|
||||
@@ -2651,6 +2685,18 @@ func (s *Redis) checkConnection(pingTimeout time.Duration) error {
|
||||
return conn.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
// maintNotificationsConfig builds the go-redis maintenance notifications config
|
||||
// from the configured mode, defaulting to disabled when unset so that the
|
||||
// CLIENT MAINT_NOTIFICATIONS command is not issued on connect.
|
||||
func (r *Redis) maintNotificationsConfig() *maintnotifications.Config {
|
||||
mode := r.maintNotifications
|
||||
if len(mode) == 0 {
|
||||
mode = maintnotifications.ModeDisabled
|
||||
}
|
||||
|
||||
return &maintnotifications.Config{Mode: mode}
|
||||
}
|
||||
|
||||
// Cluster customizes the given Redis as a cluster.
|
||||
func Cluster() Option {
|
||||
return func(r *Redis) {
|
||||
@@ -2691,6 +2737,28 @@ func WithUser(user string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithProtocol customizes the given Redis with protocol.
|
||||
func WithProtocol(protocol int) Option {
|
||||
return func(r *Redis) {
|
||||
r.protocol = protocol
|
||||
}
|
||||
}
|
||||
|
||||
// WithIdentity customizes the given Redis with Identity enabled.
|
||||
func WithIdentity() Option {
|
||||
return func(r *Redis) {
|
||||
r.identity = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaintNotifications customizes the given Redis with the maintenance
|
||||
// notifications mode (disabled, enabled or auto).
|
||||
func WithMaintNotifications(mode string) Option {
|
||||
return func(r *Redis) {
|
||||
r.maintNotifications = maintnotifications.Mode(mode)
|
||||
}
|
||||
}
|
||||
|
||||
func acceptable(err error) bool {
|
||||
return err == nil || errorx.In(err, red.Nil, context.Canceled)
|
||||
}
|
||||
@@ -2706,6 +2774,20 @@ func getRedis(r *Redis) (RedisNode, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func newRedis(addr string, opts ...Option) *Redis {
|
||||
r := &Redis{
|
||||
Addr: addr,
|
||||
Type: NodeType,
|
||||
brk: breaker.NewBreaker(),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func toPairs(vals []red.Z) []Pair {
|
||||
pairs := make([]Pair, len(vals))
|
||||
for i, val := range vals {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
red "github.com/redis/go-redis/v9"
|
||||
"github.com/redis/go-redis/v9/maintnotifications"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/stringx"
|
||||
@@ -150,6 +151,82 @@ func TestNewRedis(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClientWithProtocolAndIdentity(t *testing.T) {
|
||||
r := miniredis.RunT(t)
|
||||
defer r.Close()
|
||||
c, err := getClient(&Redis{
|
||||
Addr: r.Addr(),
|
||||
Type: NodeType,
|
||||
protocol: 2,
|
||||
identity: true,
|
||||
})
|
||||
if assert.NoError(t, err) {
|
||||
assert.NotNil(t, c)
|
||||
assert.Equal(t, 2, c.Options().Protocol)
|
||||
assert.True(t, c.Options().DisableIdentity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRedis_ProtocolAndIdentity(t *testing.T) {
|
||||
logx.Disable()
|
||||
|
||||
s := miniredis.RunT(t)
|
||||
rds, err := NewRedis(RedisConf{
|
||||
Host: s.Addr(),
|
||||
Type: NodeType,
|
||||
Protocol: 2,
|
||||
DisableIdentity: true,
|
||||
})
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, 2, rds.protocol)
|
||||
assert.True(t, rds.identity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClientWithMaintNotifications(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mode maintnotifications.Mode
|
||||
want maintnotifications.Mode
|
||||
}{
|
||||
{name: "unset falls back to disabled", mode: "", want: maintnotifications.ModeDisabled},
|
||||
{name: "disabled", mode: maintnotifications.ModeDisabled, want: maintnotifications.ModeDisabled},
|
||||
{name: "enabled", mode: maintnotifications.ModeEnabled, want: maintnotifications.ModeEnabled},
|
||||
{name: "auto", mode: maintnotifications.ModeAuto, want: maintnotifications.ModeAuto},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
r := miniredis.RunT(t)
|
||||
defer r.Close()
|
||||
c, err := getClient(&Redis{
|
||||
Addr: r.Addr(),
|
||||
Type: NodeType,
|
||||
maintNotifications: test.mode,
|
||||
})
|
||||
if assert.NoError(t, err) {
|
||||
assert.NotNil(t, c)
|
||||
assert.NotNil(t, c.Options().MaintNotificationsConfig)
|
||||
assert.Equal(t, test.want, c.Options().MaintNotificationsConfig.Mode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRedis_MaintNotifications(t *testing.T) {
|
||||
logx.Disable()
|
||||
|
||||
s := miniredis.RunT(t)
|
||||
rds, err := NewRedis(RedisConf{
|
||||
Host: s.Addr(),
|
||||
Type: NodeType,
|
||||
MaintNotifications: string(maintnotifications.ModeAuto),
|
||||
})
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, maintnotifications.ModeAuto, rds.maintNotifications)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedis_NonBlock(t *testing.T) {
|
||||
logx.Disable()
|
||||
|
||||
@@ -275,6 +352,36 @@ func TestRedis_Eval(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedis_Do(t *testing.T) {
|
||||
runOnRedis(t, func(client *Redis) {
|
||||
_, err := newRedis(client.Addr, badType()).Do("PING")
|
||||
assert.NotNil(t, err)
|
||||
|
||||
pong, err := client.Do("PING")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "PONG", pong)
|
||||
|
||||
ok, err := client.Do("SET", "key1", "value1")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "OK", ok)
|
||||
|
||||
val, err := client.Do("GET", "key1")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "value1", val)
|
||||
|
||||
_, err = client.Do("GET", "not_exist")
|
||||
assert.Equal(t, Nil, err)
|
||||
|
||||
_, err = client.Do()
|
||||
assert.NotNil(t, err)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
_, err = client.DoCtx(ctx, "PING")
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedis_ScriptRun(t *testing.T) {
|
||||
runOnRedis(t, func(client *Redis) {
|
||||
sc := NewScript(`redis.call("EXISTS", KEYS[1])`)
|
||||
@@ -2264,6 +2371,31 @@ func TestRedisXGroupCreate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisXGroupSetID(t *testing.T) {
|
||||
runOnRedis(t, func(client *Redis) {
|
||||
_, err := newRedis(client.Addr, badType()).XGroupSetID("Source", "Destination", "0")
|
||||
assert.NotNil(t, err)
|
||||
|
||||
redisCli := newRedis(client.Addr)
|
||||
stream := "aa"
|
||||
group := "bb"
|
||||
|
||||
_, err = redisCli.XGroupCreateMkStream(stream, group, "0")
|
||||
assert.Nil(t, err)
|
||||
|
||||
res, err := redisCli.XGroupSetID(stream, group, "0")
|
||||
assert.Empty(t, res)
|
||||
assert.ErrorContains(t, err, "not supported")
|
||||
|
||||
_, err = newRedis(client.Addr, badType()).XGroupSetIDCtx(context.Background(), stream, group, "0")
|
||||
assert.NotNil(t, err)
|
||||
|
||||
res, err = redisCli.XGroupSetIDCtx(context.Background(), stream, group, "0")
|
||||
assert.Empty(t, res)
|
||||
assert.ErrorContains(t, err, "not supported")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRedisXInfo(t *testing.T) {
|
||||
runOnRedis(t, func(client *Redis) {
|
||||
_, err := newRedis(client.Addr, badType()).XInfoStream("Source")
|
||||
|
||||
@@ -50,25 +50,31 @@ func CreateBlockingNode(r *Redis) (ClosableNode, error) {
|
||||
switch r.Type {
|
||||
case NodeType:
|
||||
client := red.NewClient(&red.Options{
|
||||
Addr: r.Addr,
|
||||
Username: r.User,
|
||||
Password: r.Pass,
|
||||
DB: defaultDatabase,
|
||||
MaxRetries: maxRetries,
|
||||
PoolSize: 1,
|
||||
MinIdleConns: 1,
|
||||
ReadTimeout: timeout,
|
||||
Addr: r.Addr,
|
||||
Username: r.User,
|
||||
Password: r.Pass,
|
||||
DB: defaultDatabase,
|
||||
MaxRetries: maxRetries,
|
||||
PoolSize: 1,
|
||||
MinIdleConns: 1,
|
||||
ReadTimeout: timeout,
|
||||
Protocol: r.protocol,
|
||||
DisableIdentity: r.identity,
|
||||
MaintNotificationsConfig: r.maintNotificationsConfig(),
|
||||
})
|
||||
return &clientBridge{client}, nil
|
||||
case ClusterType:
|
||||
client := red.NewClusterClient(&red.ClusterOptions{
|
||||
Addrs: splitClusterAddrs(r.Addr),
|
||||
Username: r.User,
|
||||
Password: r.Pass,
|
||||
MaxRetries: maxRetries,
|
||||
PoolSize: 1,
|
||||
MinIdleConns: 1,
|
||||
ReadTimeout: timeout,
|
||||
Addrs: splitClusterAddrs(r.Addr),
|
||||
Username: r.User,
|
||||
Password: r.Pass,
|
||||
MaxRetries: maxRetries,
|
||||
PoolSize: 1,
|
||||
MinIdleConns: 1,
|
||||
ReadTimeout: timeout,
|
||||
Protocol: r.protocol,
|
||||
DisableIdentity: r.identity,
|
||||
MaintNotificationsConfig: r.maintNotificationsConfig(),
|
||||
})
|
||||
return &clusterBridge{client}, nil
|
||||
default:
|
||||
|
||||
@@ -43,4 +43,32 @@ func TestBlockingNode(t *testing.T) {
|
||||
_, err = CreateBlockingNode(New(r.Addr(), badType()))
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("test blocking node with protocol and identity", func(t *testing.T) {
|
||||
r, err := miniredis.Run()
|
||||
assert.NoError(t, err)
|
||||
defer r.Close()
|
||||
|
||||
node, err := CreateBlockingNode(New(r.Addr(), WithProtocol(2), WithIdentity()))
|
||||
assert.NoError(t, err)
|
||||
bridge, ok := node.(*clientBridge)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 2, bridge.Options().Protocol)
|
||||
assert.True(t, bridge.Options().DisableIdentity)
|
||||
node.Close()
|
||||
})
|
||||
|
||||
t.Run("test blocking node with cluster, protocol and identity", func(t *testing.T) {
|
||||
r, err := miniredis.Run()
|
||||
assert.NoError(t, err)
|
||||
defer r.Close()
|
||||
|
||||
node, err := CreateBlockingNode(New(r.Addr(), Cluster(), WithProtocol(2), WithIdentity()))
|
||||
assert.NoError(t, err)
|
||||
bridge, ok := node.(*clusterBridge)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 2, bridge.Options().Protocol)
|
||||
assert.True(t, bridge.Options().DisableIdentity)
|
||||
node.Close()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,13 +30,16 @@ func getClient(r *Redis) (*red.Client, error) {
|
||||
}
|
||||
}
|
||||
store := red.NewClient(&red.Options{
|
||||
Addr: r.Addr,
|
||||
Username: r.User,
|
||||
Password: r.Pass,
|
||||
DB: defaultDatabase,
|
||||
MaxRetries: maxRetries,
|
||||
MinIdleConns: idleConns,
|
||||
TLSConfig: tlsConfig,
|
||||
Addr: r.Addr,
|
||||
Username: r.User,
|
||||
Password: r.Pass,
|
||||
DB: defaultDatabase,
|
||||
MaxRetries: maxRetries,
|
||||
MinIdleConns: idleConns,
|
||||
TLSConfig: tlsConfig,
|
||||
Protocol: r.protocol,
|
||||
DisableIdentity: r.identity,
|
||||
MaintNotificationsConfig: r.maintNotificationsConfig(),
|
||||
})
|
||||
|
||||
hooks := append([]red.Hook{defaultDurationHook, breakerHook{
|
||||
|
||||
@@ -27,12 +27,15 @@ func getCluster(r *Redis) (*red.ClusterClient, error) {
|
||||
}
|
||||
}
|
||||
store := red.NewClusterClient(&red.ClusterOptions{
|
||||
Addrs: splitClusterAddrs(r.Addr),
|
||||
Username: r.User,
|
||||
Password: r.Pass,
|
||||
MaxRetries: maxRetries,
|
||||
MinIdleConns: idleConns,
|
||||
TLSConfig: tlsConfig,
|
||||
Addrs: splitClusterAddrs(r.Addr),
|
||||
Username: r.User,
|
||||
Password: r.Pass,
|
||||
MaxRetries: maxRetries,
|
||||
MinIdleConns: idleConns,
|
||||
TLSConfig: tlsConfig,
|
||||
Protocol: r.protocol,
|
||||
DisableIdentity: r.identity,
|
||||
MaintNotificationsConfig: r.maintNotificationsConfig(),
|
||||
})
|
||||
|
||||
hooks := append([]red.Hook{defaultDurationHook, breakerHook{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
red "github.com/redis/go-redis/v9"
|
||||
"github.com/redis/go-redis/v9/maintnotifications"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -57,3 +58,50 @@ func TestGetCluster(t *testing.T) {
|
||||
assert.NotNil(t, c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClusterWithProtocolAndIdentity(t *testing.T) {
|
||||
r := miniredis.RunT(t)
|
||||
defer r.Close()
|
||||
c, err := getCluster(&Redis{
|
||||
Addr: r.Addr(),
|
||||
Type: ClusterType,
|
||||
protocol: 2,
|
||||
identity: true,
|
||||
hooks: []red.Hook{defaultDurationHook},
|
||||
})
|
||||
if assert.NoError(t, err) {
|
||||
assert.NotNil(t, c)
|
||||
assert.Equal(t, 2, c.Options().Protocol)
|
||||
assert.True(t, c.Options().DisableIdentity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClusterWithMaintNotifications(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mode maintnotifications.Mode
|
||||
want maintnotifications.Mode
|
||||
}{
|
||||
{name: "unset falls back to disabled", mode: "", want: maintnotifications.ModeDisabled},
|
||||
{name: "disabled", mode: maintnotifications.ModeDisabled, want: maintnotifications.ModeDisabled},
|
||||
{name: "auto", mode: maintnotifications.ModeAuto, want: maintnotifications.ModeAuto},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
r := miniredis.RunT(t)
|
||||
defer r.Close()
|
||||
c, err := getCluster(&Redis{
|
||||
Addr: r.Addr(),
|
||||
Type: ClusterType,
|
||||
maintNotifications: test.mode,
|
||||
hooks: []red.Hook{defaultDurationHook},
|
||||
})
|
||||
if assert.NoError(t, err) {
|
||||
assert.NotNil(t, c)
|
||||
assert.NotNil(t, c.Options().MaintNotificationsConfig)
|
||||
assert.Equal(t, test.want, c.Options().MaintNotificationsConfig.Mode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package stringx
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/lang"
|
||||
@@ -21,24 +22,22 @@ func Contains(list []string, str string) bool {
|
||||
return slices.Contains(list, str)
|
||||
}
|
||||
|
||||
// Filter filters chars from s with given filter function.
|
||||
func Filter(s string, filter func(r rune) bool) string {
|
||||
var n int
|
||||
chars := []rune(s)
|
||||
for i, x := range chars {
|
||||
if n < i {
|
||||
chars[n] = x
|
||||
// Filter filters chars from s with given remove function.
|
||||
func Filter(s string, remove func(r rune) bool) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if remove(r) {
|
||||
return -1
|
||||
}
|
||||
if !filter(x) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
return string(chars[:n])
|
||||
return r
|
||||
}, s)
|
||||
}
|
||||
|
||||
// FirstN returns first n runes from s.
|
||||
func FirstN(s string, n int, ellipsis ...string) string {
|
||||
if n < 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var i int
|
||||
|
||||
for j := range s {
|
||||
@@ -133,7 +132,7 @@ func Substr(str string, start, stop int) (string, error) {
|
||||
return "", ErrInvalidStartPosition
|
||||
}
|
||||
|
||||
if stop < 0 || stop > length {
|
||||
if stop < 0 || stop > length || start > stop {
|
||||
return "", ErrInvalidStopPosition
|
||||
}
|
||||
|
||||
@@ -141,6 +140,7 @@ func Substr(str string, start, stop int) (string, error) {
|
||||
}
|
||||
|
||||
// TakeOne returns valid string if not empty or later one.
|
||||
// Deprecated: use cmp.Or instead.
|
||||
func TakeOne(valid, or string) string {
|
||||
if len(valid) > 0 {
|
||||
return valid
|
||||
|
||||
@@ -29,6 +29,40 @@ func TestContainsString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasEmpty(t *testing.T) {
|
||||
cases := []struct {
|
||||
args []string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
args: []string{"a", "b", "c"},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
args: []string{"a", "", "c"},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
args: []string{""},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
args: []string{},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
args: nil,
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, each := range cases {
|
||||
t.Run(path.Join(each.args...), func(t *testing.T) {
|
||||
assert.Equal(t, each.expect, HasEmpty(each.args...))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotEmpty(t *testing.T) {
|
||||
cases := []struct {
|
||||
args []string
|
||||
@@ -92,6 +126,24 @@ func TestFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilter(b *testing.B) {
|
||||
b.Run("true", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Filter(`ab,cd,ef`, func(r rune) bool { return r == ',' })
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("false", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Filter(`ab,cd,ef`, func(r rune) bool { return r == '!' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFirstN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -138,6 +190,18 @@ func TestFirstN(t *testing.T) {
|
||||
n: 10,
|
||||
expect: "我是中国人",
|
||||
},
|
||||
{
|
||||
name: "negative n returns empty string",
|
||||
input: "hello world",
|
||||
n: -1,
|
||||
expect: "",
|
||||
},
|
||||
{
|
||||
name: "negative n with utf8 returns empty string",
|
||||
input: "我是中国人",
|
||||
n: -5,
|
||||
expect: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -292,6 +356,13 @@ func TestSubstr(t *testing.T) {
|
||||
err: ErrInvalidStopPosition,
|
||||
expect: "",
|
||||
},
|
||||
{
|
||||
input: "hello",
|
||||
start: 3,
|
||||
stop: 2,
|
||||
err: ErrInvalidStopPosition,
|
||||
expect: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, each := range cases {
|
||||
|
||||
@@ -3,13 +3,11 @@ package trace
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/jaeger"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
@@ -20,12 +18,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
kindJaeger = "jaeger"
|
||||
kindZipkin = "zipkin"
|
||||
kindOtlpGrpc = "otlpgrpc"
|
||||
kindOtlpHttp = "otlphttp"
|
||||
kindFile = "file"
|
||||
protocolUdp = "udp"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -62,15 +58,7 @@ func StopAgent() {
|
||||
}
|
||||
|
||||
func createExporter(c Config) (sdktrace.SpanExporter, error) {
|
||||
// Just support jaeger and zipkin now, more for later
|
||||
switch c.Batcher {
|
||||
case kindJaeger:
|
||||
u, err := url.Parse(c.Endpoint)
|
||||
if err == nil && u.Scheme == protocolUdp {
|
||||
return jaeger.New(jaeger.WithAgentEndpoint(jaeger.WithAgentHost(u.Hostname()),
|
||||
jaeger.WithAgentPort(u.Port())))
|
||||
}
|
||||
return jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(c.Endpoint)))
|
||||
case kindZipkin:
|
||||
return zipkin.New(c.Endpoint)
|
||||
case kindOtlpGrpc:
|
||||
|
||||
@@ -27,21 +27,16 @@ func TestStartAgent(t *testing.T) {
|
||||
Name: "foo",
|
||||
}
|
||||
c2 := Config{
|
||||
Name: "bar",
|
||||
Endpoint: endpoint1,
|
||||
Batcher: kindJaeger,
|
||||
}
|
||||
c3 := Config{
|
||||
Name: "any",
|
||||
Endpoint: endpoint2,
|
||||
Batcher: kindZipkin,
|
||||
}
|
||||
c4 := Config{
|
||||
c3 := Config{
|
||||
Name: "bla",
|
||||
Endpoint: endpoint3,
|
||||
Batcher: "otlp",
|
||||
}
|
||||
c5 := Config{
|
||||
c4 := Config{
|
||||
Name: "otlpgrpc",
|
||||
Endpoint: endpoint3,
|
||||
Batcher: kindOtlpGrpc,
|
||||
@@ -49,7 +44,7 @@ func TestStartAgent(t *testing.T) {
|
||||
"uptrace-dsn": "http://project2_secret_token@localhost:14317/2",
|
||||
},
|
||||
}
|
||||
c6 := Config{
|
||||
c5 := Config{
|
||||
Name: "otlphttp",
|
||||
Endpoint: endpoint4,
|
||||
Batcher: kindOtlpHttp,
|
||||
@@ -58,22 +53,12 @@ func TestStartAgent(t *testing.T) {
|
||||
},
|
||||
OtlpHttpPath: "/v1/traces",
|
||||
}
|
||||
c7 := Config{
|
||||
Name: "UDP",
|
||||
Endpoint: endpoint5,
|
||||
Batcher: kindJaeger,
|
||||
}
|
||||
c8 := Config{
|
||||
Disabled: true,
|
||||
Endpoint: endpoint6,
|
||||
Batcher: kindJaeger,
|
||||
}
|
||||
c9 := Config{
|
||||
c6 := Config{
|
||||
Name: "file",
|
||||
Endpoint: endpoint71,
|
||||
Batcher: kindFile,
|
||||
}
|
||||
c10 := Config{
|
||||
c7 := Config{
|
||||
Name: "file",
|
||||
Endpoint: endpoint72,
|
||||
Batcher: kindFile,
|
||||
@@ -87,9 +72,6 @@ func TestStartAgent(t *testing.T) {
|
||||
StartAgent(c5)
|
||||
StartAgent(c6)
|
||||
StartAgent(c7)
|
||||
StartAgent(c8)
|
||||
StartAgent(c9)
|
||||
StartAgent(c10)
|
||||
defer StopAgent()
|
||||
|
||||
// With sync.Once, only the first non-disabled config (c1) takes effect.
|
||||
@@ -164,24 +146,6 @@ func TestCreateExporter_ValidExporters(t *testing.T) {
|
||||
wantErr: true,
|
||||
errMsg: "unknown exporter",
|
||||
},
|
||||
{
|
||||
name: "jaeger http",
|
||||
config: Config{
|
||||
Name: "jaeger-http",
|
||||
Endpoint: "http://localhost:14268/api/traces",
|
||||
Batcher: kindJaeger,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "jaeger udp",
|
||||
config: Config{
|
||||
Name: "jaeger-udp",
|
||||
Endpoint: "udp://localhost:6831",
|
||||
Batcher: kindJaeger,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "zipkin",
|
||||
config: Config{
|
||||
|
||||
@@ -8,7 +8,7 @@ type Config struct {
|
||||
Name string `json:",optional"`
|
||||
Endpoint string `json:",optional"`
|
||||
Sampler float64 `json:",default=1.0"`
|
||||
Batcher string `json:",default=jaeger,options=jaeger|zipkin|otlpgrpc|otlphttp|file"`
|
||||
Batcher string `json:",default=otlpgrpc,options=zipkin|otlpgrpc|otlphttp|file"`
|
||||
// OtlpHeaders represents the headers for OTLP gRPC or HTTP transport.
|
||||
// For example:
|
||||
// uptrace-dsn: 'http://project2_secret_token@localhost:14317/2'
|
||||
|
||||
@@ -329,8 +329,9 @@ func createDescriptorSource(cli zrpc.Client, up Upstream) (grpcurl.DescriptorSou
|
||||
return source, nil
|
||||
}
|
||||
|
||||
// withDialer sets a dialer to create a gRPC client.
|
||||
func withDialer(dialer func(conf zrpc.RpcClientConf) zrpc.Client) func(*Server) {
|
||||
// WithDialer sets a dialer to create a gRPC client.
|
||||
// This allows customization of gRPC client options, such as message size limits.
|
||||
func WithDialer(dialer func(conf zrpc.RpcClientConf) zrpc.Client) func(*Server) {
|
||||
return func(s *Server) {
|
||||
s.dialer = dialer
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestMustNewServer(t *testing.T) {
|
||||
c.Host = "localhost"
|
||||
c.Port = 18881
|
||||
|
||||
s := MustNewServer(c, withDialer(func(conf zrpc.RpcClientConf) zrpc.Client {
|
||||
s := MustNewServer(c, WithDialer(func(conf zrpc.RpcClientConf) zrpc.Client {
|
||||
return zrpc.MustNewClient(conf, zrpc.WithDialOption(grpc.WithContextDialer(dialer())))
|
||||
}), WithHeaderProcessor(func(header http.Header) []string {
|
||||
return []string{"foo"}
|
||||
|
||||
154
go.mod
154
go.mod
@@ -1,124 +1,138 @@
|
||||
module github.com/zeromicro/go-zero
|
||||
|
||||
go 1.21
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/alicebob/miniredis/v2 v2.35.0
|
||||
github.com/alicebob/miniredis/v2 v2.38.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fullstorydev/grpcurl v1.9.3
|
||||
github.com/go-sql-driver/mysql v1.9.0
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/golang/protobuf v1.5.4
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/grafana/pyroscope-go v1.2.7
|
||||
github.com/jackc/pgx/v5 v5.7.4
|
||||
github.com/jhump/protoreflect v1.17.0
|
||||
github.com/pelletier/go-toml/v2 v2.2.2
|
||||
github.com/prometheus/client_golang v1.21.1
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/grafana/pyroscope-go v1.3.0
|
||||
github.com/jackc/pgx/v5 v5.8.0
|
||||
github.com/jhump/protoreflect v1.18.0
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0
|
||||
github.com/pelletier/go-toml/v2 v2.4.3
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/redis/go-redis/v9 v9.21.0
|
||||
github.com/spaolacci/murmur3 v1.1.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.etcd.io/etcd/api/v3 v3.5.15
|
||||
go.etcd.io/etcd/client/v3 v3.5.15
|
||||
go.mongodb.org/mongo-driver/v2 v2.4.1
|
||||
go.opentelemetry.io/otel v1.24.0
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0
|
||||
go.opentelemetry.io/otel/sdk v1.24.0
|
||||
go.opentelemetry.io/otel/trace v1.24.0
|
||||
github.com/titanous/json5 v1.0.0
|
||||
go.etcd.io/etcd/api/v3 v3.5.21
|
||||
go.etcd.io/etcd/client/v3 v3.5.21
|
||||
go.mongodb.org/mongo-driver/v2 v2.7.0
|
||||
go.opentelemetry.io/otel v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.40.0
|
||||
go.opentelemetry.io/otel/sdk v1.40.0
|
||||
go.opentelemetry.io/otel/trace v1.40.0
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.uber.org/mock v0.4.0
|
||||
golang.org/x/net v0.35.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/time v0.10.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.36.5
|
||||
go.uber.org/mock v0.6.0
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/time v0.14.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409
|
||||
google.golang.org/grpc v1.80.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.29.3
|
||||
k8s.io/apimachinery v0.29.4
|
||||
k8s.io/client-go v0.29.3
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
||||
k8s.io/api v0.34.3
|
||||
k8s.io/apimachinery v0.34.3
|
||||
k8s.io/client-go v0.34.3
|
||||
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bufbuild/protocompile v0.14.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.12.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/jsonschema-go v0.4.2 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jhump/protoreflect/v2 v2.0.0-beta.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/openzipkin/zipkin-go v0.4.3 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.3 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/scram v1.2.0 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.110.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
344
go.sum
344
go.sum
@@ -1,9 +1,9 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -14,12 +14,12 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw=
|
||||
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
@@ -28,74 +28,78 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI=
|
||||
github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1ZkyVU3jk=
|
||||
github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
|
||||
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
|
||||
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
|
||||
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/grafana/pyroscope-go v1.3.0 h1:t3Jehad8vvqN4oRAB0LdmfQ5ZSUXQw3asoft+K4GAT8=
|
||||
github.com/grafana/pyroscope-go v1.3.0/go.mod h1:XA7I3usNx+UdjOZfQnl1WV8y924vsJo9KIVrKB+9jx4=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.10 h1:dvhndEbyavTb59vFCd6PsrAG5qi69/qZZtegh/TJKSY=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.10/go.mod h1:XnWRGg2XO5uxZdiz1rfeJH6w1eZ+YICCBVXNWOfH86g=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
|
||||
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
|
||||
github.com/jhump/protoreflect v1.18.0 h1:TOz0MSR/0JOZ5kECB/0ufGnC2jdsgZ123Rd/k4Z5/2w=
|
||||
github.com/jhump/protoreflect v1.18.0/go.mod h1:ezWcltJIVF4zYdIFM+D/sHV4Oh5LNU08ORzCGfwvTz8=
|
||||
github.com/jhump/protoreflect/v2 v2.0.0-beta.1 h1:Dw1rslK/VotaUGYsv53XVWITr+5RCPXfvvlGrM/+B6w=
|
||||
github.com/jhump/protoreflect/v2 v2.0.0-beta.1/go.mod h1:D9LBEowZyv8/iSu97FU2zmXG3JxVTmNw21mu63niFzU=
|
||||
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
@@ -103,8 +107,10 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -123,47 +129,62 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8=
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
|
||||
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
|
||||
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
|
||||
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
||||
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
|
||||
github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
|
||||
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
|
||||
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
|
||||
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -174,16 +195,20 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s=
|
||||
github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -191,54 +216,62 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
|
||||
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU=
|
||||
go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
|
||||
go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
|
||||
go.mongodb.org/mongo-driver/v2 v2.4.1 h1:hGDMngUao03OVQ6sgV5csk+RWOIkF+CuLsTPobNMGNI=
|
||||
go.mongodb.org/mongo-driver/v2 v2.4.1/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
|
||||
go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
|
||||
go.mongodb.org/mongo-driver/v2 v2.7.0 h1:RO+zqavD2/GCL3cxOMyZhx6R9Irzr8/6gsoqx5tcY/c=
|
||||
go.mongodb.org/mongo-driver/v2 v2.7.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.40.0 h1:zu+I4j+FdO6xIxBVPeuncQVbjxUM4LiMgv6GwGe9REE=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.40.0/go.mod h1:zS6cC4nFBYXbu18e7aLfMzubBjOiN7ZcROu477qtMf8=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -248,16 +281,16 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -267,69 +300,76 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
|
||||
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
|
||||
k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q=
|
||||
k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y=
|
||||
k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg=
|
||||
k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0=
|
||||
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
|
||||
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
|
||||
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
|
||||
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
|
||||
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
|
||||
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM=
|
||||
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
@@ -3,12 +3,63 @@ package encoding
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/titanous/json5"
|
||||
"github.com/zeromicro/go-zero/core/lang"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Json5ToJson converts JSON5 data into its JSON representation.
|
||||
func Json5ToJson(data []byte) ([]byte, error) {
|
||||
var val any
|
||||
if err := json5.Unmarshal(data, &val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate that there are no unsupported values like Infinity or NaN
|
||||
if err := validateJSONCompatible(val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return encodeToJSON(val)
|
||||
}
|
||||
|
||||
// validateJSONCompatible checks if the value can be represented in standard JSON.
|
||||
// JSON5 allows Infinity and NaN, but standard JSON does not support these values.
|
||||
func validateJSONCompatible(val any) error {
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
if math.IsInf(v, 0) {
|
||||
return fmt.Errorf("JSON5 value Infinity cannot be represented in standard JSON")
|
||||
}
|
||||
if math.IsNaN(v) {
|
||||
return fmt.Errorf("JSON5 value NaN cannot be represented in standard JSON")
|
||||
}
|
||||
case []any:
|
||||
for _, item := range v {
|
||||
if err := validateJSONCompatible(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case map[string]any:
|
||||
for _, value := range v {
|
||||
if err := validateJSONCompatible(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case map[any]any:
|
||||
for _, value := range v {
|
||||
if err := validateJSONCompatible(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TomlToJson converts TOML data into its JSON representation.
|
||||
func TomlToJson(data []byte) ([]byte, error) {
|
||||
var val any
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package encoding
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -116,3 +117,142 @@ func TestYamlToJsonSlice(t *testing.T) {
|
||||
assert.Equal(t, `{"foo":["bar","baz"]}
|
||||
`, string(b))
|
||||
}
|
||||
|
||||
func TestJson5ToJson(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "standard json",
|
||||
input: `{"a":"foo","b":1,"c":"${FOO}","d":"abcd!@#$112"}`,
|
||||
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
|
||||
},
|
||||
{
|
||||
name: "json5 with comments",
|
||||
input: `{/*comment*/"a":"foo","b":1}`,
|
||||
expect: "{\"a\":\"foo\",\"b\":1}\n",
|
||||
},
|
||||
{
|
||||
name: "json5 with trailing commas",
|
||||
input: `{"a":"foo","b":1,}`,
|
||||
expect: "{\"a\":\"foo\",\"b\":1}\n",
|
||||
},
|
||||
{
|
||||
name: "json5 with unquoted keys",
|
||||
input: `{a:"foo",b:1}`,
|
||||
expect: "{\"a\":\"foo\",\"b\":1}\n",
|
||||
},
|
||||
{
|
||||
name: "json5 with single quotes",
|
||||
input: `{"a":'foo',"b":1}`,
|
||||
expect: "{\"a\":\"foo\",\"b\":1}\n",
|
||||
},
|
||||
{
|
||||
name: "json5 with line comments",
|
||||
input: "{\n// This is a comment\n\"a\":\"foo\",\n\"b\":1\n}",
|
||||
expect: "{\"a\":\"foo\",\"b\":1}\n",
|
||||
},
|
||||
{
|
||||
name: "json5 all features combined",
|
||||
input: "{\n// comment\na: 'foo', // trailing comma\nb: 1,\n}",
|
||||
expect: "{\"a\":\"foo\",\"b\":1}\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, err := Json5ToJson([]byte(test.input))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.expect, string(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJson5ToJsonError(t *testing.T) {
|
||||
// Invalid JSON5: unquoted string value
|
||||
_, err := Json5ToJson([]byte("{a: foo}"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestJson5ToJsonInfinity(t *testing.T) {
|
||||
// JSON5 allows Infinity but standard JSON does not
|
||||
_, err := Json5ToJson([]byte(`{value: Infinity}`))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Infinity")
|
||||
|
||||
// Negative infinity
|
||||
_, err = Json5ToJson([]byte(`{value: -Infinity}`))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Infinity")
|
||||
|
||||
// Infinity in array
|
||||
_, err = Json5ToJson([]byte(`{values: [1, Infinity, 3]}`))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Infinity")
|
||||
}
|
||||
|
||||
func TestJson5ToJsonNaN(t *testing.T) {
|
||||
// JSON5 allows NaN but standard JSON does not
|
||||
_, err := Json5ToJson([]byte(`{value: NaN}`))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "NaN")
|
||||
|
||||
// NaN in nested structure
|
||||
_, err = Json5ToJson([]byte(`{nested: {value: NaN}}`))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "NaN")
|
||||
}
|
||||
|
||||
func TestJson5ToJsonSlice(t *testing.T) {
|
||||
b, err := Json5ToJson([]byte(`{
|
||||
// comment
|
||||
foo: [
|
||||
'bar',
|
||||
"baz", // trailing comma
|
||||
],
|
||||
}`))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `{"foo":["bar","baz"]}
|
||||
`, string(b))
|
||||
}
|
||||
|
||||
func TestValidateJSONCompatible(t *testing.T) {
|
||||
// Test float64 types
|
||||
assert.NoError(t, validateJSONCompatible(float64(1.5)))
|
||||
assert.Error(t, validateJSONCompatible(math.Inf(1)))
|
||||
assert.Error(t, validateJSONCompatible(math.Inf(-1)))
|
||||
assert.Error(t, validateJSONCompatible(math.NaN()))
|
||||
|
||||
// Test arrays with invalid values
|
||||
assert.Error(t, validateJSONCompatible([]any{1, math.Inf(1), 3}))
|
||||
assert.Error(t, validateJSONCompatible([]any{1, math.NaN(), 3}))
|
||||
assert.NoError(t, validateJSONCompatible([]any{1, 2, 3}))
|
||||
|
||||
// Test map[string]any with invalid values
|
||||
assert.Error(t, validateJSONCompatible(map[string]any{"value": math.Inf(1)}))
|
||||
assert.Error(t, validateJSONCompatible(map[string]any{"value": math.NaN()}))
|
||||
assert.NoError(t, validateJSONCompatible(map[string]any{"value": 1.5}))
|
||||
|
||||
// Test map[any]any with invalid values
|
||||
assert.Error(t, validateJSONCompatible(map[any]any{"value": math.Inf(1)}))
|
||||
assert.Error(t, validateJSONCompatible(map[any]any{"value": math.NaN()}))
|
||||
assert.NoError(t, validateJSONCompatible(map[any]any{"value": 1.5}))
|
||||
|
||||
// Test nested structures
|
||||
assert.Error(t, validateJSONCompatible(map[string]any{
|
||||
"nested": map[string]any{"value": math.Inf(1)},
|
||||
}))
|
||||
assert.Error(t, validateJSONCompatible([]any{
|
||||
map[string]any{"value": math.NaN()},
|
||||
}))
|
||||
|
||||
// Test valid values of various types
|
||||
assert.NoError(t, validateJSONCompatible("string"))
|
||||
assert.NoError(t, validateJSONCompatible(42))
|
||||
assert.NoError(t, validateJSONCompatible(true))
|
||||
assert.NoError(t, validateJSONCompatible(nil))
|
||||
}
|
||||
|
||||
166
mcp/MIGRATION.md
Normal file
166
mcp/MIGRATION.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Migration to Official MCP SDK
|
||||
|
||||
This document describes the migration from the custom MCP implementation to the official [go-sdk](https://github.com/modelcontextprotocol/go-sdk).
|
||||
|
||||
## Changes
|
||||
|
||||
### Dependencies
|
||||
|
||||
Added the official MCP SDK:
|
||||
```bash
|
||||
go get github.com/modelcontextprotocol/go-sdk@v1.2.0
|
||||
```
|
||||
|
||||
### Type System
|
||||
|
||||
All types are now re-exported from the official SDK:
|
||||
- `Tool` → `sdkmcp.Tool`
|
||||
- `CallToolRequest` → `sdkmcp.CallToolRequest`
|
||||
- `CallToolResult` → `sdkmcp.CallToolResult`
|
||||
- Content types (`TextContent`, `ImageContent`, etc.)
|
||||
- `Prompt`, `Resource`, `Server`, `ServerSession`
|
||||
|
||||
### Server Interface
|
||||
|
||||
The `McpServer` interface has been simplified:
|
||||
|
||||
```go
|
||||
type McpServer interface {
|
||||
Start()
|
||||
Stop()
|
||||
Server() *sdkmcp.Server // Returns underlying SDK server
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: The `AddTool`, `AddPrompt`, and `AddResource` methods have been removed. Use the SDK directly:
|
||||
|
||||
```go
|
||||
// Old (no longer supported)
|
||||
server.AddTool(tool, handler)
|
||||
|
||||
// New (use SDK directly)
|
||||
sdkmcp.AddTool(server.Server(), tool, handler)
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Updated configuration structure:
|
||||
- Removed: `ProtocolVersion`, `BaseUrl` (SDK manages these)
|
||||
- Added: `UseStreamable` (choose between SSE and Streamable HTTP transport)
|
||||
|
||||
```yaml
|
||||
mcp:
|
||||
name: my-server
|
||||
version: 1.0.0
|
||||
useStreamable: false # false = SSE (2024-11-05), true = Streamable HTTP (2025-03-26)
|
||||
sseEndpoint: /sse
|
||||
messageEndpoint: /message
|
||||
sseTimeout: 24h
|
||||
messageTimeout: 30s
|
||||
cors:
|
||||
- http://localhost:3000
|
||||
```
|
||||
|
||||
### Tool Registration
|
||||
|
||||
The SDK uses Go generics for type-safe tool registration:
|
||||
|
||||
```go
|
||||
import sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
type MyArgs struct {
|
||||
Value string `json:"value" jsonschema:"description=Input value"`
|
||||
}
|
||||
|
||||
tool := &mcp.Tool{
|
||||
Name: "my_tool",
|
||||
Description: "Description",
|
||||
}
|
||||
|
||||
handler := func(ctx context.Context, req *mcp.CallToolRequest, args MyArgs) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "Result"},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// Register with explicit type parameters
|
||||
sdkmcp.AddTool(server.Server(), tool, handler)
|
||||
```
|
||||
|
||||
The SDK automatically generates JSON schemas from struct tags.
|
||||
|
||||
### Transport Support
|
||||
|
||||
Two transports are supported:
|
||||
|
||||
1. **SSE (Server-Sent Events)**: 2024-11-05 MCP spec
|
||||
- Default (`UseStreamable: false`)
|
||||
- Endpoint: `/sse` (configurable)
|
||||
- Bidirectional: client sends messages to `/message`
|
||||
|
||||
2. **Streamable HTTP**: 2025-03-26 MCP spec
|
||||
- Opt-in (`UseStreamable: true`)
|
||||
- Endpoint: `/sse` (configurable)
|
||||
- Newer protocol with improved streaming
|
||||
|
||||
### Example Migration
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
server := mcp.NewMcpServer(c)
|
||||
|
||||
tool := &mcp.Tool{Name: "greet", Description: "Greet"}
|
||||
handler := func(ctx context.Context, req *mcp.CallToolRequest, args GreetArgs) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: "Hello"}},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
if err := server.AddTool(tool, handler); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
import sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
server := mcp.NewMcpServer(c)
|
||||
|
||||
tool := &mcp.Tool{Name: "greet", Description: "Greet"}
|
||||
handler := func(ctx context.Context, req *mcp.CallToolRequest, args GreetArgs) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: "Hello"}},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// Use SDK directly - no error return
|
||||
sdkmcp.AddTool(server.Server(), tool, handler)
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Official SDK**: Uses the official Model Context Protocol SDK
|
||||
2. **Type Safety**: Go generics provide compile-time type checking
|
||||
3. **Auto Schema**: JSON schemas generated automatically from struct tags
|
||||
4. **Dual Transport**: Supports both SSE and Streamable HTTP transports
|
||||
5. **Maintained**: SDK is actively maintained by the MCP team
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
1. `server.AddTool()` removed → use `sdkmcp.AddTool(server.Server(), ...)`
|
||||
2. `server.AddPrompt()` removed (SDK v1.2.0 limitation)
|
||||
3. `server.AddResource()` removed (SDK v1.2.0 limitation)
|
||||
4. Config fields `ProtocolVersion` and `BaseUrl` removed
|
||||
5. All types now come from SDK (re-exported for convenience)
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
- [ ] Update imports: add `sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"`
|
||||
- [ ] Replace `server.AddTool()` with `sdkmcp.AddTool(server.Server(), ...)`
|
||||
- [ ] Remove error handling for tool registration (SDK doesn't return errors)
|
||||
- [ ] Update config: remove `ProtocolVersion` and `BaseUrl`, add `UseStreamable`
|
||||
- [ ] Test with both SSE and Streamable transports
|
||||
- [ ] Update documentation/examples
|
||||
@@ -18,17 +18,16 @@ type McpConf struct {
|
||||
// Version is the server version reported in initialize responses
|
||||
Version string `json:",default=1.0.0"`
|
||||
|
||||
// ProtocolVersion is the MCP protocol version implemented
|
||||
ProtocolVersion string `json:",default=2024-11-05"`
|
||||
|
||||
// BaseUrl is the base URL for the server, used in SSE endpoint messages
|
||||
// If not set, defaults to http://localhost:{Port}
|
||||
BaseUrl string `json:",optional"`
|
||||
// UseStreamable when true uses Streamable HTTP transport (2025-03-26 spec),
|
||||
// otherwise uses SSE transport (2024-11-05 spec)
|
||||
UseStreamable bool `json:",default=false"`
|
||||
|
||||
// SseEndpoint is the path for Server-Sent Events connections
|
||||
// Used for SSE transport mode
|
||||
SseEndpoint string `json:",default=/sse"`
|
||||
|
||||
// MessageEndpoint is the path for JSON-RPC requests
|
||||
// Used for Streamable HTTP transport mode
|
||||
MessageEndpoint string `json:",default=/message"`
|
||||
|
||||
// Cors contains allowed CORS origins
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMcpConfDefaults(t *testing.T) {
|
||||
// Test default values are set correctly when unmarshalled from JSON
|
||||
// Test default values are set correctly
|
||||
jsonConfig := `name: test-service
|
||||
port: 8080
|
||||
mcp:
|
||||
@@ -23,41 +23,8 @@ mcp:
|
||||
|
||||
// Check default values
|
||||
assert.Equal(t, "test-mcp-server", c.Mcp.Name)
|
||||
assert.Equal(t, "1.0.0", c.Mcp.Version, "Default version should be 1.0.0")
|
||||
assert.Equal(t, "2024-11-05", c.Mcp.ProtocolVersion, "Default protocol version should be 2024-11-05")
|
||||
assert.Equal(t, "/sse", c.Mcp.SseEndpoint, "Default SSE endpoint should be /sse")
|
||||
assert.Equal(t, "/message", c.Mcp.MessageEndpoint, "Default message endpoint should be /message")
|
||||
assert.Equal(t, 30*time.Second, c.Mcp.MessageTimeout, "Default message timeout should be 30s")
|
||||
}
|
||||
|
||||
func TestMcpConfCustomValues(t *testing.T) {
|
||||
// Test custom values can be set
|
||||
jsonConfig := `{
|
||||
"Name": "test-service",
|
||||
"Port": 8080,
|
||||
"Mcp": {
|
||||
"Name": "test-mcp-server",
|
||||
"Version": "2.0.0",
|
||||
"ProtocolVersion": "2025-01-01",
|
||||
"BaseUrl": "http://example.com",
|
||||
"SseEndpoint": "/custom-sse",
|
||||
"MessageEndpoint": "/custom-message",
|
||||
"Cors": ["http://localhost:3000", "http://example.com"],
|
||||
"MessageTimeout": "60s"
|
||||
}
|
||||
}`
|
||||
|
||||
var c McpConf
|
||||
err := conf.LoadFromJsonBytes([]byte(jsonConfig), &c)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check custom values
|
||||
assert.Equal(t, "test-mcp-server", c.Mcp.Name, "Name should be inherited from RestConf")
|
||||
assert.Equal(t, "2.0.0", c.Mcp.Version, "Version should be customizable")
|
||||
assert.Equal(t, "2025-01-01", c.Mcp.ProtocolVersion, "Protocol version should be customizable")
|
||||
assert.Equal(t, "http://example.com", c.Mcp.BaseUrl, "BaseUrl should be customizable")
|
||||
assert.Equal(t, "/custom-sse", c.Mcp.SseEndpoint, "SSE endpoint should be customizable")
|
||||
assert.Equal(t, "/custom-message", c.Mcp.MessageEndpoint, "Message endpoint should be customizable")
|
||||
assert.Equal(t, []string{"http://localhost:3000", "http://example.com"}, c.Mcp.Cors, "CORS settings should be customizable")
|
||||
assert.Equal(t, 60*time.Second, c.Mcp.MessageTimeout, "Tool timeout should be customizable")
|
||||
assert.Equal(t, "1.0.0", c.Mcp.Version)
|
||||
assert.Equal(t, "/sse", c.Mcp.SseEndpoint)
|
||||
assert.Equal(t, "/message", c.Mcp.MessageEndpoint)
|
||||
assert.Equal(t, 30*time.Second, c.Mcp.MessageTimeout)
|
||||
}
|
||||
|
||||
@@ -1,443 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// syncResponseRecorder is a thread-safe wrapper around httptest.ResponseRecorder
|
||||
type syncResponseRecorder struct {
|
||||
*httptest.ResponseRecorder
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Create a new synchronized response recorder
|
||||
func newSyncResponseRecorder() *syncResponseRecorder {
|
||||
return &syncResponseRecorder{
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
}
|
||||
}
|
||||
|
||||
// Override Write method to synchronize access
|
||||
func (srr *syncResponseRecorder) Write(p []byte) (int, error) {
|
||||
srr.mu.Lock()
|
||||
defer srr.mu.Unlock()
|
||||
return srr.ResponseRecorder.Write(p)
|
||||
}
|
||||
|
||||
// Override WriteHeader method to synchronize access
|
||||
func (srr *syncResponseRecorder) WriteHeader(statusCode int) {
|
||||
srr.mu.Lock()
|
||||
defer srr.mu.Unlock()
|
||||
srr.ResponseRecorder.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
// Override Result method to synchronize access
|
||||
func (srr *syncResponseRecorder) Result() *http.Response {
|
||||
srr.mu.Lock()
|
||||
defer srr.mu.Unlock()
|
||||
return srr.ResponseRecorder.Result()
|
||||
}
|
||||
|
||||
// TestHTTPHandlerIntegration tests the HTTP handlers with a real server instance
|
||||
func TestHTTPHandlerIntegration(t *testing.T) {
|
||||
// Skip in short test mode
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Create a test configuration
|
||||
conf := McpConf{}
|
||||
conf.Mcp.Name = "test-integration"
|
||||
conf.Mcp.Version = "1.0.0-test"
|
||||
conf.Mcp.MessageTimeout = 1 * time.Second
|
||||
|
||||
// Create a mock server directly
|
||||
server := &sseMcpServer{
|
||||
conf: conf,
|
||||
clients: make(map[string]*mcpClient),
|
||||
tools: make(map[string]Tool),
|
||||
prompts: make(map[string]Prompt),
|
||||
resources: make(map[string]Resource),
|
||||
}
|
||||
|
||||
// Register a test tool
|
||||
err := server.RegisterTool(Tool{
|
||||
Name: "echo",
|
||||
Description: "Echo tool for testing",
|
||||
InputSchema: InputSchema{
|
||||
Properties: map[string]any{
|
||||
"message": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Message to echo",
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: func(ctx context.Context, params map[string]any) (any, error) {
|
||||
if msg, ok := params["message"].(string); ok {
|
||||
return fmt.Sprintf("Echo: %s", msg), nil
|
||||
}
|
||||
return "Echo: no message provided", nil
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a test HTTP request to the SSE endpoint
|
||||
req := httptest.NewRequest("GET", "/sse", nil)
|
||||
w := newSyncResponseRecorder()
|
||||
|
||||
// Create a done channel to signal completion of test
|
||||
done := make(chan bool)
|
||||
|
||||
// Start the SSE handler in a goroutine
|
||||
go func() {
|
||||
// lock.Lock()
|
||||
server.handleSSE(w, req)
|
||||
// lock.Unlock()
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Allow time for the handler to process
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// Expected - handler would normally block indefinitely
|
||||
case <-done:
|
||||
// This shouldn't happen immediately - the handler should block
|
||||
t.Error("SSE handler returned unexpectedly")
|
||||
}
|
||||
|
||||
// Check the initial headers
|
||||
resp := w.Result()
|
||||
assert.Equal(t, "chunked", resp.Header.Get("Transfer-Encoding"))
|
||||
resp.Body.Close()
|
||||
|
||||
// The handler creates a client and sends the endpoint message
|
||||
var sessionId string
|
||||
|
||||
// Give the handler time to set up the client
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Check that a client was created
|
||||
server.clientsLock.Lock()
|
||||
assert.Equal(t, 1, len(server.clients))
|
||||
for id := range server.clients {
|
||||
sessionId = id
|
||||
}
|
||||
server.clientsLock.Unlock()
|
||||
|
||||
require.NotEmpty(t, sessionId, "Expected a session ID to be created")
|
||||
|
||||
// Now that we have a session ID, we can test the message endpoint
|
||||
messageBody, _ := json.Marshal(Request{
|
||||
JsonRpc: "2.0",
|
||||
ID: 1,
|
||||
Method: methodInitialize,
|
||||
Params: json.RawMessage(`{}`),
|
||||
})
|
||||
|
||||
// Create a message request
|
||||
reqURL := fmt.Sprintf("/message?%s=%s", sessionIdKey, sessionId)
|
||||
msgReq := httptest.NewRequest("POST", reqURL, bytes.NewReader(messageBody))
|
||||
msgW := newSyncResponseRecorder()
|
||||
|
||||
// Process the message
|
||||
server.handleRequest(msgW, msgReq)
|
||||
|
||||
// Check the response
|
||||
msgResp := msgW.Result()
|
||||
assert.Equal(t, http.StatusAccepted, msgResp.StatusCode)
|
||||
msgResp.Body.Close() // Ensure response body is closed
|
||||
}
|
||||
|
||||
// TestHandlerResponseFlow tests the flow of a full request/response cycle
|
||||
func TestHandlerResponseFlow(t *testing.T) {
|
||||
// Create a mock server for testing
|
||||
server := &sseMcpServer{
|
||||
conf: McpConf{},
|
||||
clients: map[string]*mcpClient{
|
||||
"test-session": {
|
||||
id: "test-session",
|
||||
channel: make(chan string, 10),
|
||||
initialized: true,
|
||||
},
|
||||
},
|
||||
tools: make(map[string]Tool),
|
||||
prompts: make(map[string]Prompt),
|
||||
resources: make(map[string]Resource),
|
||||
}
|
||||
|
||||
// Register test resources
|
||||
server.RegisterTool(Tool{
|
||||
Name: "test.tool",
|
||||
Description: "Test tool",
|
||||
InputSchema: InputSchema{Type: "object"},
|
||||
Handler: func(ctx context.Context, params map[string]any) (any, error) {
|
||||
return "tool result", nil
|
||||
},
|
||||
})
|
||||
|
||||
server.RegisterPrompt(Prompt{
|
||||
Name: "test.prompt",
|
||||
Description: "Test prompt",
|
||||
})
|
||||
|
||||
server.RegisterResource(Resource{
|
||||
Name: "test.resource",
|
||||
URI: "http://example.com",
|
||||
Description: "Test resource",
|
||||
})
|
||||
|
||||
// Create a request with session ID parameter
|
||||
reqURL := fmt.Sprintf("/message?%s=%s", sessionIdKey, "test-session")
|
||||
|
||||
// Test tools/list request
|
||||
toolsListBody, _ := json.Marshal(Request{
|
||||
JsonRpc: "2.0",
|
||||
ID: 1,
|
||||
Method: methodToolsList,
|
||||
Params: json.RawMessage(`{}`),
|
||||
})
|
||||
|
||||
toolsReq := httptest.NewRequest("POST", reqURL, bytes.NewReader(toolsListBody))
|
||||
toolsW := newSyncResponseRecorder()
|
||||
|
||||
// Process the request
|
||||
server.handleRequest(toolsW, toolsReq)
|
||||
|
||||
// Check the response code
|
||||
toolsResp := toolsW.Result()
|
||||
assert.Equal(t, http.StatusAccepted, toolsResp.StatusCode)
|
||||
toolsResp.Body.Close()
|
||||
|
||||
// Check the channel message
|
||||
client := server.clients["test-session"]
|
||||
select {
|
||||
case message := <-client.channel:
|
||||
assert.Contains(t, message, `"tools":[{"name":"test.tool"`)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("Timed out waiting for tools/list response")
|
||||
}
|
||||
|
||||
// Test prompts/list request
|
||||
promptsListBody, _ := json.Marshal(Request{
|
||||
JsonRpc: "2.0",
|
||||
ID: 2,
|
||||
Method: methodPromptsList,
|
||||
Params: json.RawMessage(`{}`),
|
||||
})
|
||||
|
||||
promptsReq := httptest.NewRequest("POST", reqURL, bytes.NewReader(promptsListBody))
|
||||
promptsW := newSyncResponseRecorder()
|
||||
|
||||
// Process the request
|
||||
server.handleRequest(promptsW, promptsReq)
|
||||
|
||||
// Check the response code
|
||||
promptsResp := promptsW.Result()
|
||||
assert.Equal(t, http.StatusAccepted, promptsResp.StatusCode)
|
||||
promptsResp.Body.Close()
|
||||
|
||||
// Check the channel message
|
||||
select {
|
||||
case message := <-client.channel:
|
||||
assert.Contains(t, message, `"prompts":[{"name":"test.prompt"`)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("Timed out waiting for prompts/list response")
|
||||
}
|
||||
|
||||
// Test resources/list request
|
||||
resourcesListBody, _ := json.Marshal(Request{
|
||||
JsonRpc: "2.0",
|
||||
ID: 3,
|
||||
Method: methodResourcesList,
|
||||
Params: json.RawMessage(`{}`),
|
||||
})
|
||||
|
||||
resourcesReq := httptest.NewRequest("POST", reqURL, bytes.NewReader(resourcesListBody))
|
||||
resourcesW := newSyncResponseRecorder()
|
||||
|
||||
// Process the request
|
||||
server.handleRequest(resourcesW, resourcesReq)
|
||||
|
||||
// Check the response code
|
||||
resourcesResp := resourcesW.Result()
|
||||
assert.Equal(t, http.StatusAccepted, resourcesResp.StatusCode)
|
||||
resourcesResp.Body.Close()
|
||||
|
||||
// Check the channel message
|
||||
select {
|
||||
case message := <-client.channel:
|
||||
assert.Contains(t, message, `"name":"test.resource"`)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("Timed out waiting for resources/list response")
|
||||
}
|
||||
}
|
||||
|
||||
// TestProcessListMethods tests the list processing methods with pagination
|
||||
func TestProcessListMethods(t *testing.T) {
|
||||
server := &sseMcpServer{
|
||||
tools: make(map[string]Tool),
|
||||
prompts: make(map[string]Prompt),
|
||||
resources: make(map[string]Resource),
|
||||
}
|
||||
|
||||
// Add some test data
|
||||
for i := 1; i <= 5; i++ {
|
||||
tool := Tool{
|
||||
Name: fmt.Sprintf("tool%d", i),
|
||||
Description: fmt.Sprintf("Tool %d", i),
|
||||
InputSchema: InputSchema{Type: "object"},
|
||||
}
|
||||
server.tools[tool.Name] = tool
|
||||
|
||||
prompt := Prompt{
|
||||
Name: fmt.Sprintf("prompt%d", i),
|
||||
Description: fmt.Sprintf("Prompt %d", i),
|
||||
}
|
||||
server.prompts[prompt.Name] = prompt
|
||||
|
||||
resource := Resource{
|
||||
Name: fmt.Sprintf("resource%d", i),
|
||||
URI: fmt.Sprintf("http://example.com/%d", i),
|
||||
Description: fmt.Sprintf("Resource %d", i),
|
||||
}
|
||||
server.resources[resource.Name] = resource
|
||||
}
|
||||
|
||||
// Create a test client
|
||||
client := &mcpClient{
|
||||
id: "test-client",
|
||||
channel: make(chan string, 10),
|
||||
initialized: true,
|
||||
}
|
||||
|
||||
// Test processListTools
|
||||
req := Request{
|
||||
JsonRpc: "2.0",
|
||||
ID: 1,
|
||||
Method: methodToolsList,
|
||||
Params: json.RawMessage(`{"cursor": "", "_meta": {"progressToken": "token1"}}`),
|
||||
}
|
||||
|
||||
server.processListTools(context.Background(), client, req)
|
||||
|
||||
// Read response
|
||||
select {
|
||||
case response := <-client.channel:
|
||||
assert.Contains(t, response, `"tools":`)
|
||||
assert.Contains(t, response, `"progressToken":"token1"`)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("Timed out waiting for tools/list response")
|
||||
}
|
||||
|
||||
// Test processListPrompts
|
||||
req.ID = 2
|
||||
req.Method = methodPromptsList
|
||||
req.Params = json.RawMessage(`{"cursor": "next"}`)
|
||||
server.processListPrompts(context.Background(), client, req)
|
||||
|
||||
// Read response
|
||||
select {
|
||||
case response := <-client.channel:
|
||||
assert.Contains(t, response, `"prompts":`)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("Timed out waiting for prompts/list response")
|
||||
}
|
||||
|
||||
// Test processListResources
|
||||
req.ID = 3
|
||||
req.Method = methodResourcesList
|
||||
req.Params = json.RawMessage(`{"cursor": "next"}`)
|
||||
server.processListResources(context.Background(), client, req)
|
||||
|
||||
// Read response
|
||||
select {
|
||||
case response := <-client.channel:
|
||||
assert.Contains(t, response, `"resources":`)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("Timed out waiting for resources/list response")
|
||||
}
|
||||
}
|
||||
|
||||
// TestErrorResponseHandling tests error handling in the server
|
||||
func TestErrorResponseHandling(t *testing.T) {
|
||||
server := &sseMcpServer{
|
||||
tools: make(map[string]Tool),
|
||||
prompts: make(map[string]Prompt),
|
||||
resources: make(map[string]Resource),
|
||||
}
|
||||
|
||||
// Create a test client
|
||||
client := &mcpClient{
|
||||
id: "test-client",
|
||||
channel: make(chan string, 10),
|
||||
initialized: true,
|
||||
}
|
||||
|
||||
// Test invalid method
|
||||
req := Request{
|
||||
JsonRpc: "2.0",
|
||||
ID: 1,
|
||||
Method: "invalid_method",
|
||||
Params: json.RawMessage(`{}`),
|
||||
}
|
||||
|
||||
// Mock handleRequest by directly calling error handler
|
||||
server.sendErrorResponse(context.Background(), client, req.ID, "Method not found", errCodeMethodNotFound)
|
||||
|
||||
// Check response
|
||||
select {
|
||||
case response := <-client.channel:
|
||||
assert.Contains(t, response, `"error":{"code":-32601,"message":"Method not found"}`)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("Timed out waiting for error response")
|
||||
}
|
||||
|
||||
// Test invalid tool
|
||||
toolReq := Request{
|
||||
JsonRpc: "2.0",
|
||||
ID: 2,
|
||||
Method: methodToolsCall,
|
||||
Params: json.RawMessage(`{"name":"non_existent_tool"}`),
|
||||
}
|
||||
|
||||
// Call process method directly
|
||||
server.processToolCall(context.Background(), client, toolReq)
|
||||
|
||||
// Check response
|
||||
select {
|
||||
case response := <-client.channel:
|
||||
assert.Contains(t, response, `"error":{"code":-32602,"message":"Tool 'non_existent_tool' not found"}`)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("Timed out waiting for error response")
|
||||
}
|
||||
|
||||
// Test invalid prompt
|
||||
promptReq := Request{
|
||||
JsonRpc: "2.0",
|
||||
ID: 3,
|
||||
Method: methodPromptsGet,
|
||||
Params: json.RawMessage(`{"name":"non_existent_prompt"}`),
|
||||
}
|
||||
|
||||
// Call process method directly
|
||||
server.processGetPrompt(context.Background(), client, promptReq)
|
||||
|
||||
// Check response
|
||||
select {
|
||||
case response := <-client.channel:
|
||||
assert.Contains(t, response, `"error":{"code":-32602,"message":"Prompt 'non_existent_prompt' not found"}`)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("Timed out waiting for error response")
|
||||
}
|
||||
}
|
||||
33
mcp/options.go
Normal file
33
mcp/options.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package mcp
|
||||
|
||||
import "net/http"
|
||||
|
||||
// RequestMetadataExtractor extracts request metadata for downstream handlers.
|
||||
type RequestMetadataExtractor func(*http.Request) RequestMetadata
|
||||
|
||||
// McpOption customizes MCP server construction.
|
||||
type McpOption interface {
|
||||
apply(*serverOptions)
|
||||
}
|
||||
|
||||
type mcpOptionFunc func(*serverOptions)
|
||||
|
||||
func (f mcpOptionFunc) apply(opts *serverOptions) {
|
||||
f(opts)
|
||||
}
|
||||
|
||||
type serverOptions struct {
|
||||
requestMetadataExtractor RequestMetadataExtractor
|
||||
}
|
||||
|
||||
func defaultServerOptions() serverOptions {
|
||||
return serverOptions{}
|
||||
}
|
||||
|
||||
// WithRequestMetadataExtractor installs an extractor that runs for each incoming
|
||||
// MCP HTTP request, and stores the extracted metadata into handler context.
|
||||
func WithRequestMetadataExtractor(extractor RequestMetadataExtractor) McpOption {
|
||||
return mcpOptionFunc(func(opts *serverOptions) {
|
||||
opts.requestMetadataExtractor = extractor
|
||||
})
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/mapping"
|
||||
)
|
||||
|
||||
// ParseArguments parses the arguments and populates the request object
|
||||
func ParseArguments(args any, req any) error {
|
||||
switch arguments := args.(type) {
|
||||
case map[string]string:
|
||||
m := make(map[string]any, len(arguments))
|
||||
for k, v := range arguments {
|
||||
m[k] = v
|
||||
}
|
||||
return mapping.UnmarshalJsonMap(m, req, mapping.WithStringValues())
|
||||
case map[string]any:
|
||||
return mapping.UnmarshalJsonMap(arguments, req)
|
||||
default:
|
||||
return fmt.Errorf("unsupported argument type: %T", arguments)
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestParseArguments_MapStringString tests parsing map[string]string arguments
|
||||
func TestParseArguments_MapStringString(t *testing.T) {
|
||||
// Sample request struct to populate
|
||||
type requestStruct struct {
|
||||
Name string `json:"name"`
|
||||
Message string `json:"message"`
|
||||
Count int `json:"count"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// Create test arguments
|
||||
args := map[string]string{
|
||||
"name": "test-name",
|
||||
"message": "hello world",
|
||||
"count": "42",
|
||||
"enabled": "true",
|
||||
}
|
||||
|
||||
// Create a target object to populate
|
||||
var req requestStruct
|
||||
|
||||
// Parse the arguments
|
||||
err := ParseArguments(args, &req)
|
||||
|
||||
// Verify results
|
||||
assert.NoError(t, err, "Should parse map[string]string without error")
|
||||
assert.Equal(t, "test-name", req.Name, "Name should be correctly parsed")
|
||||
assert.Equal(t, "hello world", req.Message, "Message should be correctly parsed")
|
||||
assert.Equal(t, 42, req.Count, "Count should be correctly parsed to int")
|
||||
assert.True(t, req.Enabled, "Enabled should be correctly parsed to bool")
|
||||
}
|
||||
|
||||
// TestParseArguments_MapStringAny tests parsing map[string]any arguments
|
||||
func TestParseArguments_MapStringAny(t *testing.T) {
|
||||
// Sample request struct to populate
|
||||
type requestStruct struct {
|
||||
Name string `json:"name"`
|
||||
Message string `json:"message"`
|
||||
Count int `json:"count"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Tags []string `json:"tags"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// Create test arguments with mixed types
|
||||
args := map[string]any{
|
||||
"name": "test-name",
|
||||
"message": "hello world",
|
||||
"count": 42, // note: this is already an int
|
||||
"enabled": true, // note: this is already a bool
|
||||
"tags": []string{"tag1", "tag2"},
|
||||
"metadata": map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
}
|
||||
|
||||
// Create a target object to populate
|
||||
var req requestStruct
|
||||
|
||||
// Parse the arguments
|
||||
err := ParseArguments(args, &req)
|
||||
|
||||
// Verify results
|
||||
assert.NoError(t, err, "Should parse map[string]any without error")
|
||||
assert.Equal(t, "test-name", req.Name, "Name should be correctly parsed")
|
||||
assert.Equal(t, "hello world", req.Message, "Message should be correctly parsed")
|
||||
assert.Equal(t, 42, req.Count, "Count should be correctly parsed")
|
||||
assert.True(t, req.Enabled, "Enabled should be correctly parsed")
|
||||
assert.Equal(t, []string{"tag1", "tag2"}, req.Tags, "Tags should be correctly parsed")
|
||||
assert.Equal(t, map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
}, req.Metadata, "Metadata should be correctly parsed")
|
||||
}
|
||||
|
||||
// TestParseArguments_UnsupportedType tests parsing with an unsupported type
|
||||
func TestParseArguments_UnsupportedType(t *testing.T) {
|
||||
// Sample request struct to populate
|
||||
type requestStruct struct {
|
||||
Name string `json:"name"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Use an unsupported argument type (slice)
|
||||
args := []string{"not", "a", "map"}
|
||||
|
||||
// Create a target object to populate
|
||||
var req requestStruct
|
||||
|
||||
// Parse the arguments
|
||||
err := ParseArguments(args, &req)
|
||||
|
||||
// Verify error is returned with correct message
|
||||
assert.Error(t, err, "Should return error for unsupported type")
|
||||
assert.Contains(t, err.Error(), "unsupported argument type", "Error should mention unsupported type")
|
||||
assert.Contains(t, err.Error(), "[]string", "Error should include the actual type")
|
||||
}
|
||||
|
||||
// TestParseArguments_EmptyMap tests parsing with empty maps
|
||||
func TestParseArguments_EmptyMap(t *testing.T) {
|
||||
// Sample request struct to populate
|
||||
type requestStruct struct {
|
||||
Name string `json:"name,optional"`
|
||||
Message string `json:"message,optional"`
|
||||
}
|
||||
|
||||
// Test empty map[string]string
|
||||
t.Run("EmptyMapStringString", func(t *testing.T) {
|
||||
args := map[string]string{}
|
||||
var req requestStruct
|
||||
|
||||
err := ParseArguments(args, &req)
|
||||
|
||||
assert.NoError(t, err, "Should parse empty map[string]string without error")
|
||||
assert.Empty(t, req.Name, "Name should be empty string")
|
||||
assert.Empty(t, req.Message, "Message should be empty string")
|
||||
})
|
||||
|
||||
// Test empty map[string]any
|
||||
t.Run("EmptyMapStringAny", func(t *testing.T) {
|
||||
args := map[string]any{}
|
||||
var req requestStruct
|
||||
|
||||
err := ParseArguments(args, &req)
|
||||
|
||||
assert.NoError(t, err, "Should parse empty map[string]any without error")
|
||||
assert.Empty(t, req.Name, "Name should be empty string")
|
||||
assert.Empty(t, req.Message, "Message should be empty string")
|
||||
})
|
||||
}
|
||||
1042
mcp/readme.md
1042
mcp/readme.md
File diff suppressed because it is too large
Load Diff
150
mcp/request_metadata.go
Normal file
150
mcp/request_metadata.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/pathvar"
|
||||
)
|
||||
|
||||
// RequestMetadata carries selected request-scoped values into MCP handlers.
|
||||
type RequestMetadata struct {
|
||||
Headers map[string][]string
|
||||
Query map[string][]string
|
||||
Path map[string]string
|
||||
}
|
||||
|
||||
type requestMetadataCtxKey struct{}
|
||||
|
||||
// RequestMetadataFromContext returns metadata extracted at the transport boundary.
|
||||
func RequestMetadataFromContext(ctx context.Context) (RequestMetadata, bool) {
|
||||
metadata, ok := requestMetadataFromContext(ctx)
|
||||
if !ok {
|
||||
return RequestMetadata{}, false
|
||||
}
|
||||
|
||||
return normalizeRequestMetadata(metadata), true
|
||||
}
|
||||
|
||||
// HeaderFromContext returns the first header value for key.
|
||||
func HeaderFromContext(ctx context.Context, key string) (string, bool) {
|
||||
metadata, ok := requestMetadataFromContext(ctx)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
vals := metadata.Headers[http.CanonicalHeaderKey(key)]
|
||||
if len(vals) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return vals[0], true
|
||||
}
|
||||
|
||||
// QueryFromContext returns the first query value for key.
|
||||
func QueryFromContext(ctx context.Context, key string) (string, bool) {
|
||||
metadata, ok := requestMetadataFromContext(ctx)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
vals := metadata.Query[key]
|
||||
if len(vals) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return vals[0], true
|
||||
}
|
||||
|
||||
// PathFromContext returns the path variable value for key.
|
||||
func PathFromContext(ctx context.Context, key string) (string, bool) {
|
||||
metadata, ok := requestMetadataFromContext(ctx)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
val, ok := metadata.Path[key]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return val, true
|
||||
}
|
||||
|
||||
func requestMetadataFromContext(ctx context.Context) (RequestMetadata, bool) {
|
||||
metadata, ok := ctx.Value(requestMetadataCtxKey{}).(RequestMetadata)
|
||||
if !ok {
|
||||
return RequestMetadata{}, false
|
||||
}
|
||||
|
||||
return metadata, true
|
||||
}
|
||||
|
||||
// DefaultRequestMetadataExtractor extracts headers, query values, and path variables.
|
||||
func DefaultRequestMetadataExtractor(r *http.Request) RequestMetadata {
|
||||
metadata := RequestMetadata{
|
||||
Headers: make(map[string][]string, len(r.Header)),
|
||||
Query: make(map[string][]string),
|
||||
Path: clonePathVars(pathvar.Vars(r)),
|
||||
}
|
||||
|
||||
for key, vals := range r.Header {
|
||||
metadata.Headers[http.CanonicalHeaderKey(key)] = append([]string(nil), vals...)
|
||||
}
|
||||
|
||||
if r.URL != nil {
|
||||
for key, vals := range r.URL.Query() {
|
||||
metadata.Query[key] = append([]string(nil), vals...)
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
func normalizeRequestMetadata(metadata RequestMetadata) RequestMetadata {
|
||||
return RequestMetadata{
|
||||
Headers: cloneCanonicalHeaderValues(metadata.Headers),
|
||||
Query: cloneHeaderValues(metadata.Query),
|
||||
Path: clonePathVars(metadata.Path),
|
||||
}
|
||||
}
|
||||
|
||||
func cloneHeaderValues(values map[string][]string) map[string][]string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := make(map[string][]string, len(values))
|
||||
for key, vals := range values {
|
||||
cloned[key] = append([]string(nil), vals...)
|
||||
}
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneCanonicalHeaderValues(values map[string][]string) map[string][]string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := make(map[string][]string, len(values))
|
||||
for key, vals := range values {
|
||||
canonical := http.CanonicalHeaderKey(key)
|
||||
cloned[canonical] = append(cloned[canonical], vals...)
|
||||
}
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
func clonePathVars(values map[string]string) map[string]string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cloned := make(map[string]string, len(values))
|
||||
for key, val := range values {
|
||||
cloned[key] = val
|
||||
}
|
||||
|
||||
return cloned
|
||||
}
|
||||
185
mcp/request_metadata_test.go
Normal file
185
mcp/request_metadata_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/rest/pathvar"
|
||||
)
|
||||
|
||||
func TestDefaultRequestMetadataExtractor(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/sse?tenant=t1&trace=abc", nil)
|
||||
req.Header.Add("X-Tenant-Id", "tenant-from-header")
|
||||
req = pathvar.WithVars(req, map[string]string{"tool": "sum"})
|
||||
|
||||
metadata := DefaultRequestMetadataExtractor(req)
|
||||
header, ok := metadata.Headers["X-Tenant-Id"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, []string{"tenant-from-header"}, header)
|
||||
assert.Equal(t, []string{"t1"}, metadata.Query["tenant"])
|
||||
assert.Equal(t, "sum", metadata.Path["tool"])
|
||||
}
|
||||
|
||||
func TestRequestMetadataContextHelpers(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), requestMetadataCtxKey{}, RequestMetadata{
|
||||
Headers: map[string][]string{"X-Trace-Id": {"trace-1"}},
|
||||
Query: map[string][]string{"tenant": {"foo"}},
|
||||
Path: map[string]string{"scope": "prod"},
|
||||
})
|
||||
|
||||
metadata, ok := RequestMetadataFromContext(ctx)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, []string{"trace-1"}, metadata.Headers["X-Trace-Id"])
|
||||
|
||||
header, ok := HeaderFromContext(ctx, "x-trace-id")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "trace-1", header)
|
||||
|
||||
query, ok := QueryFromContext(ctx, "tenant")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "foo", query)
|
||||
|
||||
path, ok := PathFromContext(ctx, "scope")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "prod", path)
|
||||
}
|
||||
|
||||
func TestRequestMetadataContextHelpersMissingKeys(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), requestMetadataCtxKey{}, RequestMetadata{
|
||||
Headers: map[string][]string{"X-Trace-Id": {"trace-1"}},
|
||||
Query: map[string][]string{"tenant": {"foo"}},
|
||||
Path: map[string]string{"scope": "prod"},
|
||||
})
|
||||
|
||||
_, ok := HeaderFromContext(ctx, "x-missing")
|
||||
assert.False(t, ok)
|
||||
|
||||
_, ok = QueryFromContext(ctx, "missing")
|
||||
assert.False(t, ok)
|
||||
|
||||
_, ok = PathFromContext(ctx, "missing")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestRequestMetadataFromContextNotFound(t *testing.T) {
|
||||
_, ok := RequestMetadataFromContext(context.Background())
|
||||
assert.False(t, ok)
|
||||
|
||||
_, ok = HeaderFromContext(context.Background(), "x-test")
|
||||
assert.False(t, ok)
|
||||
|
||||
_, ok = QueryFromContext(context.Background(), "tenant")
|
||||
assert.False(t, ok)
|
||||
|
||||
_, ok = PathFromContext(context.Background(), "tenant")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestWrapRequestMetadata(t *testing.T) {
|
||||
s := &mcpServerImpl{
|
||||
options: serverOptions{
|
||||
requestMetadataExtractor: DefaultRequestMetadataExtractor,
|
||||
},
|
||||
}
|
||||
|
||||
called := false
|
||||
handler := s.wrapRequestMetadata(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
header, ok := HeaderFromContext(r.Context(), "x-tenant-id")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "tenant-1", header)
|
||||
|
||||
query, ok := QueryFromContext(r.Context(), "tenant")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "q-tenant", query)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/sse?tenant=q-tenant", nil)
|
||||
req.Header.Set("X-Tenant-Id", "tenant-1")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestWrapRequestMetadataNoExtractor(t *testing.T) {
|
||||
s := &mcpServerImpl{}
|
||||
|
||||
called := false
|
||||
handler := s.wrapRequestMetadata(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
_, ok := RequestMetadataFromContext(r.Context())
|
||||
assert.False(t, ok)
|
||||
}))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/sse", nil))
|
||||
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestWrapRequestMetadataCanonicalizesCustomHeaders(t *testing.T) {
|
||||
s := &mcpServerImpl{
|
||||
options: serverOptions{
|
||||
requestMetadataExtractor: func(*http.Request) RequestMetadata {
|
||||
return RequestMetadata{
|
||||
Headers: map[string][]string{
|
||||
"x-tenant-id": {"tenant-lower"},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
called := false
|
||||
handler := s.wrapRequestMetadata(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
header, ok := HeaderFromContext(r.Context(), "X-Tenant-Id")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "tenant-lower", header)
|
||||
}))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/sse", nil))
|
||||
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestRequestMetadataFromContextReturnsCopy(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), requestMetadataCtxKey{}, RequestMetadata{
|
||||
Headers: map[string][]string{"X-Trace-Id": {"trace-1"}},
|
||||
})
|
||||
|
||||
metadata, ok := RequestMetadataFromContext(ctx)
|
||||
assert.True(t, ok)
|
||||
metadata.Headers["X-Trace-Id"][0] = "mutated"
|
||||
metadata.Headers["X-New"] = []string{"new"}
|
||||
|
||||
fresh, ok := RequestMetadataFromContext(ctx)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, []string{"trace-1"}, fresh.Headers["X-Trace-Id"])
|
||||
assert.Nil(t, fresh.Headers["X-New"])
|
||||
}
|
||||
|
||||
func TestRequestMetadataFromContextWithEmptyAndCanonicalizedHeaders(t *testing.T) {
|
||||
emptyCtx := context.WithValue(context.Background(), requestMetadataCtxKey{}, RequestMetadata{})
|
||||
empty, ok := RequestMetadataFromContext(emptyCtx)
|
||||
assert.True(t, ok)
|
||||
assert.Nil(t, empty.Headers)
|
||||
assert.Nil(t, empty.Query)
|
||||
assert.Nil(t, empty.Path)
|
||||
|
||||
ctx := context.WithValue(context.Background(), requestMetadataCtxKey{}, RequestMetadata{
|
||||
Headers: map[string][]string{
|
||||
"x-tenant-id": {"a"},
|
||||
"X-Tenant-Id": {"b"},
|
||||
},
|
||||
})
|
||||
|
||||
metadata, ok := RequestMetadataFromContext(ctx)
|
||||
assert.True(t, ok)
|
||||
assert.ElementsMatch(t, []string{"a", "b"}, metadata.Headers["X-Tenant-Id"])
|
||||
}
|
||||
999
mcp/server.go
999
mcp/server.go
File diff suppressed because it is too large
Load Diff
3856
mcp/server_test.go
3856
mcp/server_test.go
File diff suppressed because it is too large
Load Diff
395
mcp/types.go
395
mcp/types.go
@@ -2,316 +2,99 @@ package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest"
|
||||
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
// Cursor is an opaque token used for pagination
|
||||
type Cursor string
|
||||
// Re-export commonly used SDK types for convenience
|
||||
type (
|
||||
// Tool types
|
||||
Tool = sdkmcp.Tool
|
||||
CallToolParams = sdkmcp.CallToolParams
|
||||
CallToolResult = sdkmcp.CallToolResult
|
||||
CallToolRequest = sdkmcp.CallToolRequest
|
||||
|
||||
// Request represents a generic MCP request following JSON-RPC 2.0 specification
|
||||
type Request struct {
|
||||
SessionId string `form:"session_id"` // Session identifier for client tracking
|
||||
JsonRpc string `json:"jsonrpc"` // Must be "2.0" per JSON-RPC spec
|
||||
ID any `json:"id"` // Request identifier for matching responses
|
||||
Method string `json:"method"` // Method name to invoke
|
||||
Params json.RawMessage `json:"params"` // Parameters for the method
|
||||
}
|
||||
// Content types
|
||||
Content = sdkmcp.Content
|
||||
TextContent = sdkmcp.TextContent
|
||||
ImageContent = sdkmcp.ImageContent
|
||||
AudioContent = sdkmcp.AudioContent
|
||||
|
||||
func (r Request) isNotification() (bool, error) {
|
||||
switch val := r.ID.(type) {
|
||||
case int:
|
||||
return val == 0, nil
|
||||
case int64:
|
||||
return val == 0, nil
|
||||
case float64:
|
||||
return val == 0.0, nil
|
||||
case string:
|
||||
return len(val) == 0, nil
|
||||
case nil:
|
||||
return true, nil
|
||||
default:
|
||||
return false, fmt.Errorf("invalid type %T", val)
|
||||
// Prompt types
|
||||
Prompt = sdkmcp.Prompt
|
||||
PromptMessage = sdkmcp.PromptMessage
|
||||
GetPromptParams = sdkmcp.GetPromptParams
|
||||
GetPromptResult = sdkmcp.GetPromptResult
|
||||
|
||||
// Resource types
|
||||
Resource = sdkmcp.Resource
|
||||
ResourceContents = sdkmcp.ResourceContents
|
||||
ReadResourceParams = sdkmcp.ReadResourceParams
|
||||
ReadResourceResult = sdkmcp.ReadResourceResult
|
||||
|
||||
// Session and server types
|
||||
Server = sdkmcp.Server
|
||||
ServerSession = sdkmcp.ServerSession
|
||||
ServerOptions = sdkmcp.ServerOptions
|
||||
Implementation = sdkmcp.Implementation
|
||||
|
||||
// Transport types
|
||||
SSEHandler = sdkmcp.SSEHandler
|
||||
StreamableHTTPHandler = sdkmcp.StreamableHTTPHandler
|
||||
)
|
||||
|
||||
// ToolHandler is a generic function signature for tool handlers.
|
||||
// Handlers should accept context, request, and typed arguments, and return
|
||||
// a result, metadata, and error.
|
||||
//
|
||||
// Deprecated: Use ToolHandlerFor directly from the SDK types.
|
||||
type ToolHandler[Args any, Meta any] func(
|
||||
ctx context.Context,
|
||||
req *CallToolRequest,
|
||||
args Args,
|
||||
) (*CallToolResult, Meta, error)
|
||||
|
||||
// PromptHandler is a function signature for prompt handlers.
|
||||
type PromptHandler func(
|
||||
ctx context.Context,
|
||||
req *sdkmcp.GetPromptRequest,
|
||||
args map[string]string,
|
||||
) (*GetPromptResult, error)
|
||||
|
||||
// ResourceHandler is a function signature for resource handlers.
|
||||
type ResourceHandler func(
|
||||
ctx context.Context,
|
||||
req *sdkmcp.ReadResourceRequest,
|
||||
uri string,
|
||||
) (*ReadResourceResult, error)
|
||||
|
||||
// AddTool registers a tool with the MCP server using type-safe generics.
|
||||
// The SDK automatically generates JSON schema from the Args struct tags.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type GreetArgs struct {
|
||||
// Name string `json:"name" jsonschema:"description=Name to greet"`
|
||||
// }
|
||||
//
|
||||
// tool := &mcp.Tool{
|
||||
// Name: "greet",
|
||||
// Description: "Greet someone",
|
||||
// }
|
||||
//
|
||||
// handler := func(ctx context.Context, req *mcp.CallToolRequest, args GreetArgs) (*mcp.CallToolResult, any, error) {
|
||||
// return &mcp.CallToolResult{
|
||||
// Content: []mcp.Content{&mcp.TextContent{Text: "Hello " + args.Name}},
|
||||
// }, nil, nil
|
||||
// }
|
||||
//
|
||||
// mcp.AddTool(server, tool, handler)
|
||||
func AddTool[In, Out any](server McpServer, tool *Tool, handler func(context.Context, *CallToolRequest, In) (*CallToolResult, Out, error)) {
|
||||
// Access internal server - only works with mcpServerImpl
|
||||
if impl, ok := server.(*mcpServerImpl); ok {
|
||||
sdkmcp.AddTool(impl.mcpServer, tool, handler)
|
||||
} else {
|
||||
logx.Error("AddTool: server must be of type *mcpServerImpl to use this helper")
|
||||
}
|
||||
}
|
||||
|
||||
type PaginatedParams struct {
|
||||
Cursor string `json:"cursor"`
|
||||
Meta struct {
|
||||
ProgressToken any `json:"progressToken"`
|
||||
} `json:"_meta"`
|
||||
}
|
||||
|
||||
// Result is the base interface for all results
|
||||
type Result struct {
|
||||
Meta map[string]any `json:"_meta,omitempty"` // Optional metadata
|
||||
}
|
||||
|
||||
// PaginatedResult is a base for results that support pagination
|
||||
type PaginatedResult struct {
|
||||
Result
|
||||
NextCursor Cursor `json:"nextCursor,omitempty"` // Opaque token for fetching next page
|
||||
}
|
||||
|
||||
// ListToolsResult represents the response to a tools/list request
|
||||
type ListToolsResult struct {
|
||||
PaginatedResult
|
||||
Tools []Tool `json:"tools"` // List of available tools
|
||||
}
|
||||
|
||||
// Message Content Types
|
||||
|
||||
// RoleType represents the sender or recipient of messages in a conversation
|
||||
type RoleType string
|
||||
|
||||
// PromptArgument defines a single argument that can be passed to a prompt
|
||||
type PromptArgument struct {
|
||||
Name string `json:"name"` // Argument name
|
||||
Description string `json:"description,omitempty"` // Human-readable description
|
||||
Required bool `json:"required,omitempty"` // Whether this argument is required
|
||||
}
|
||||
|
||||
// PromptHandler is a function that dynamically generates prompt content
|
||||
type PromptHandler func(ctx context.Context, args map[string]string) ([]PromptMessage, error)
|
||||
|
||||
// Prompt represents an MCP Prompt definition
|
||||
type Prompt struct {
|
||||
Name string `json:"name"` // Unique identifier for the prompt
|
||||
Description string `json:"description,omitempty"` // Human-readable description
|
||||
Arguments []PromptArgument `json:"arguments,omitempty"` // Arguments for customization
|
||||
Content string `json:"-"` // Static content (internal use only)
|
||||
Handler PromptHandler `json:"-"` // Handler for dynamic content generation
|
||||
}
|
||||
|
||||
// PromptMessage represents a message in a conversation
|
||||
type PromptMessage struct {
|
||||
Role RoleType `json:"role"` // Message sender role
|
||||
Content any `json:"content"` // Message content (TextContent, ImageContent, etc.)
|
||||
}
|
||||
|
||||
// TextContent represents text content in a message
|
||||
type TextContent struct {
|
||||
Text string `json:"text"` // The text content
|
||||
Annotations *Annotations `json:"annotations,omitempty"` // Optional annotations
|
||||
}
|
||||
|
||||
type typedTextContent struct {
|
||||
Type string `json:"type"`
|
||||
TextContent
|
||||
}
|
||||
|
||||
// ImageContent represents image data in a message
|
||||
type ImageContent struct {
|
||||
Data string `json:"data"` // Base64-encoded image data
|
||||
MimeType string `json:"mimeType"` // MIME type (e.g., "image/png")
|
||||
}
|
||||
|
||||
type typedImageContent struct {
|
||||
Type string `json:"type"`
|
||||
ImageContent
|
||||
}
|
||||
|
||||
// AudioContent represents audio data in a message
|
||||
type AudioContent struct {
|
||||
Data string `json:"data"` // Base64-encoded audio data
|
||||
MimeType string `json:"mimeType"` // MIME type (e.g., "audio/mp3")
|
||||
}
|
||||
|
||||
type typedAudioContent struct {
|
||||
Type string `json:"type"`
|
||||
AudioContent
|
||||
}
|
||||
|
||||
// FileContent represents file content
|
||||
type FileContent struct {
|
||||
URI string `json:"uri"` // URI identifying the file
|
||||
MimeType string `json:"mimeType"` // MIME type of the file
|
||||
Text string `json:"text"` // File content as text
|
||||
}
|
||||
|
||||
// EmbeddedResource represents a resource embedded in a message
|
||||
type EmbeddedResource struct {
|
||||
Type string `json:"type"` // Always "resource"
|
||||
Resource ResourceContent `json:"resource"` // The resource data
|
||||
}
|
||||
|
||||
// Annotations provides additional metadata for content
|
||||
type Annotations struct {
|
||||
Audience []RoleType `json:"audience,omitempty"` // Who should see this content
|
||||
Priority *float64 `json:"priority,omitempty"` // Optional priority (0-1)
|
||||
}
|
||||
|
||||
// Tool-related Types
|
||||
|
||||
// ToolHandler is a function that handles tool calls
|
||||
type ToolHandler func(ctx context.Context, params map[string]any) (any, error)
|
||||
|
||||
// Tool represents a Model Context Protocol Tool definition
|
||||
type Tool struct {
|
||||
Name string `json:"name"` // Unique identifier for the tool
|
||||
Description string `json:"description"` // Human-readable description
|
||||
InputSchema InputSchema `json:"inputSchema"` // JSON Schema for parameters
|
||||
Handler ToolHandler `json:"-"` // Not sent to clients
|
||||
}
|
||||
|
||||
// InputSchema represents tool's input schema in JSON Schema format
|
||||
type InputSchema struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]any `json:"properties"` // Property definitions
|
||||
Required []string `json:"required,omitempty"` // List of required properties
|
||||
}
|
||||
|
||||
// CallToolResult represents a tool call result that conforms to the MCP schema
|
||||
type CallToolResult struct {
|
||||
Result
|
||||
Content []any `json:"content"` // Content items (text, images, etc.)
|
||||
IsError bool `json:"isError,omitempty"` // True if tool execution failed
|
||||
}
|
||||
|
||||
// Resource represents a Model Context Protocol Resource definition
|
||||
type Resource struct {
|
||||
URI string `json:"uri"` // Unique resource identifier (RFC3986)
|
||||
Name string `json:"name"` // Human-readable name
|
||||
Description string `json:"description,omitempty"` // Optional description
|
||||
MimeType string `json:"mimeType,omitempty"` // Optional MIME type
|
||||
Handler ResourceHandler `json:"-"` // Internal handler not sent to clients
|
||||
}
|
||||
|
||||
// ResourceHandler is a function that handles resource read requests
|
||||
type ResourceHandler func(ctx context.Context) (ResourceContent, error)
|
||||
|
||||
// ResourceContent represents the content of a resource
|
||||
type ResourceContent struct {
|
||||
URI string `json:"uri"` // Resource URI (required)
|
||||
MimeType string `json:"mimeType,omitempty"` // MIME type of the resource
|
||||
Text string `json:"text,omitempty"` // Text content (if available)
|
||||
Blob string `json:"blob,omitempty"` // Base64 encoded blob data (if available)
|
||||
}
|
||||
|
||||
// ResourcesListResult represents the response to a resources/list request
|
||||
type ResourcesListResult struct {
|
||||
PaginatedResult
|
||||
Resources []Resource `json:"resources"` // List of available resources
|
||||
}
|
||||
|
||||
// ResourceReadParams contains parameters for a resources/read request
|
||||
type ResourceReadParams struct {
|
||||
URI string `json:"uri"` // URI of the resource to read
|
||||
}
|
||||
|
||||
// ResourceReadResult contains the result of a resources/read request
|
||||
type ResourceReadResult struct {
|
||||
Result
|
||||
Contents []ResourceContent `json:"contents"` // Array of resource content
|
||||
}
|
||||
|
||||
// ResourceSubscribeParams contains parameters for a resources/subscribe request
|
||||
type ResourceSubscribeParams struct {
|
||||
URI string `json:"uri"` // URI of the resource to subscribe to
|
||||
}
|
||||
|
||||
// ResourceUpdateNotification represents a notification about a resource update
|
||||
type ResourceUpdateNotification struct {
|
||||
URI string `json:"uri"` // URI of the updated resource
|
||||
Content ResourceContent `json:"content"` // New resource content
|
||||
}
|
||||
|
||||
// Client and Server Types
|
||||
|
||||
// mcpClient represents an SSE client connection
|
||||
type mcpClient struct {
|
||||
id string // Unique client identifier
|
||||
channel chan string // Channel for sending SSE messages
|
||||
initialized bool // Tracks if client has sent notifications/initialized
|
||||
}
|
||||
|
||||
// McpServer defines the interface for Model Context Protocol servers
|
||||
type McpServer interface {
|
||||
Start()
|
||||
Stop()
|
||||
RegisterTool(tool Tool) error
|
||||
RegisterPrompt(prompt Prompt)
|
||||
RegisterResource(resource Resource)
|
||||
}
|
||||
|
||||
// sseMcpServer implements the McpServer interface using SSE
|
||||
type sseMcpServer struct {
|
||||
conf McpConf
|
||||
server *rest.Server
|
||||
clients map[string]*mcpClient
|
||||
clientsLock sync.Mutex
|
||||
tools map[string]Tool
|
||||
toolsLock sync.Mutex
|
||||
prompts map[string]Prompt
|
||||
promptsLock sync.Mutex
|
||||
resources map[string]Resource
|
||||
resourcesLock sync.Mutex
|
||||
}
|
||||
|
||||
// Response Types
|
||||
|
||||
// errorObj represents a JSON-RPC error object
|
||||
type errorObj struct {
|
||||
Code int `json:"code"` // Error code
|
||||
Message string `json:"message"` // Error message
|
||||
}
|
||||
|
||||
// Response represents a JSON-RPC response
|
||||
type Response struct {
|
||||
JsonRpc string `json:"jsonrpc"` // Always "2.0"
|
||||
ID any `json:"id"` // Same as request ID
|
||||
Result any `json:"result"` // Result object (null if error)
|
||||
Error *errorObj `json:"error,omitempty"` // Error object (null if success)
|
||||
}
|
||||
|
||||
// Server Information Types
|
||||
|
||||
// serverInfo provides information about the server
|
||||
type serverInfo struct {
|
||||
Name string `json:"name"` // Server name
|
||||
Version string `json:"version"` // Server version
|
||||
}
|
||||
|
||||
// capabilities describes the server's capabilities
|
||||
type capabilities struct {
|
||||
Logging struct{} `json:"logging"`
|
||||
Prompts struct {
|
||||
ListChanged bool `json:"listChanged"` // Server will notify on prompt changes
|
||||
} `json:"prompts"`
|
||||
Resources struct {
|
||||
Subscribe bool `json:"subscribe"` // Server supports resource subscriptions
|
||||
ListChanged bool `json:"listChanged"` // Server will notify on resource changes
|
||||
} `json:"resources"`
|
||||
Tools struct {
|
||||
ListChanged bool `json:"listChanged"` // Server will notify on tool changes
|
||||
} `json:"tools"`
|
||||
}
|
||||
|
||||
// initializationResponse is sent in response to an initialize request
|
||||
type initializationResponse struct {
|
||||
ProtocolVersion string `json:"protocolVersion"` // Protocol version
|
||||
Capabilities capabilities `json:"capabilities"` // Server capabilities
|
||||
ServerInfo serverInfo `json:"serverInfo"` // Server information
|
||||
}
|
||||
|
||||
// ToolCallParams contains the parameters for a tool call
|
||||
type ToolCallParams struct {
|
||||
Name string `json:"name"` // Tool name
|
||||
Parameters map[string]any `json:"parameters"` // Tool parameters
|
||||
}
|
||||
|
||||
// ToolResult contains the result of a tool execution
|
||||
type ToolResult struct {
|
||||
Type string `json:"type"` // Content type (text, image, etc.)
|
||||
Content any `json:"content"` // Result content
|
||||
}
|
||||
|
||||
// errorMessage represents a detailed error message
|
||||
type errorMessage struct {
|
||||
Code int `json:"code"` // Error code
|
||||
Message string `json:"message"` // Error message
|
||||
Data any `json:",omitempty"` // Additional error data
|
||||
}
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestResponseMarshaling(t *testing.T) {
|
||||
// Test that the Response struct marshals correctly
|
||||
resp := Response{
|
||||
JsonRpc: "2.0",
|
||||
ID: 123,
|
||||
Result: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"jsonrpc":"2.0"`)
|
||||
assert.Contains(t, string(data), `"id":123`)
|
||||
assert.Contains(t, string(data), `"result":{"key":"value"}`)
|
||||
|
||||
// Test response with error
|
||||
respWithError := Response{
|
||||
JsonRpc: "2.0",
|
||||
ID: 456,
|
||||
Error: &errorObj{
|
||||
Code: errCodeInvalidRequest,
|
||||
Message: "Invalid Request",
|
||||
},
|
||||
}
|
||||
|
||||
data, err = json.Marshal(respWithError)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"jsonrpc":"2.0"`)
|
||||
assert.Contains(t, string(data), `"id":456`)
|
||||
assert.Contains(t, string(data), `"error":{"code":-32600,"message":"Invalid Request"}`)
|
||||
}
|
||||
|
||||
func TestRequestUnmarshaling(t *testing.T) {
|
||||
// Test that the Request struct unmarshals correctly
|
||||
jsonStr := `{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 789,
|
||||
"method": "test_method",
|
||||
"params": {"key": "value"}
|
||||
}`
|
||||
|
||||
var req Request
|
||||
err := json.Unmarshal([]byte(jsonStr), &req)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "2.0", req.JsonRpc)
|
||||
assert.Equal(t, float64(789), req.ID)
|
||||
assert.Equal(t, "test_method", req.Method)
|
||||
|
||||
// Check params unmarshaled correctly
|
||||
var params map[string]string
|
||||
err = json.Unmarshal(req.Params, ¶ms)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value", params["key"])
|
||||
}
|
||||
|
||||
func TestToolStructs(t *testing.T) {
|
||||
// Test Tool struct
|
||||
tool := Tool{
|
||||
Name: "test.tool",
|
||||
Description: "A test tool",
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"input": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Input parameter",
|
||||
},
|
||||
},
|
||||
Required: []string{"input"},
|
||||
},
|
||||
Handler: func(ctx context.Context, params map[string]any) (any, error) {
|
||||
return "result", nil
|
||||
},
|
||||
}
|
||||
|
||||
// Verify fields are correct
|
||||
assert.Equal(t, "test.tool", tool.Name)
|
||||
assert.Equal(t, "A test tool", tool.Description)
|
||||
assert.Equal(t, "object", tool.InputSchema.Type)
|
||||
assert.Contains(t, tool.InputSchema.Properties, "input")
|
||||
propMap, ok := tool.InputSchema.Properties["input"].(map[string]any)
|
||||
assert.True(t, ok, "Property should be a map")
|
||||
assert.Equal(t, "string", propMap["type"])
|
||||
assert.NotNil(t, tool.Handler)
|
||||
|
||||
// Verify JSON marshalling (which should exclude Handler function)
|
||||
data, err := json.Marshal(tool)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"name":"test.tool"`)
|
||||
assert.Contains(t, string(data), `"description":"A test tool"`)
|
||||
assert.Contains(t, string(data), `"inputSchema":`)
|
||||
assert.NotContains(t, string(data), `"Handler":`)
|
||||
}
|
||||
|
||||
func TestPromptStructs(t *testing.T) {
|
||||
// Test Prompt struct
|
||||
prompt := Prompt{
|
||||
Name: "test.prompt",
|
||||
Description: "A test prompt description",
|
||||
}
|
||||
|
||||
// Verify fields are correct
|
||||
assert.Equal(t, "test.prompt", prompt.Name)
|
||||
assert.Equal(t, "A test prompt description", prompt.Description)
|
||||
|
||||
// Verify JSON marshalling
|
||||
data, err := json.Marshal(prompt)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"name":"test.prompt"`)
|
||||
assert.Contains(t, string(data), `"description":"A test prompt description"`)
|
||||
}
|
||||
|
||||
func TestResourceStructs(t *testing.T) {
|
||||
// Test Resource struct
|
||||
resource := Resource{
|
||||
Name: "test.resource",
|
||||
URI: "http://example.com/resource",
|
||||
Description: "A test resource",
|
||||
}
|
||||
|
||||
// Verify fields are correct
|
||||
assert.Equal(t, "test.resource", resource.Name)
|
||||
assert.Equal(t, "http://example.com/resource", resource.URI)
|
||||
assert.Equal(t, "A test resource", resource.Description)
|
||||
|
||||
// Verify JSON marshalling
|
||||
data, err := json.Marshal(resource)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"name":"test.resource"`)
|
||||
assert.Contains(t, string(data), `"uri":"http://example.com/resource"`)
|
||||
assert.Contains(t, string(data), `"description":"A test resource"`)
|
||||
}
|
||||
|
||||
func TestContentTypes(t *testing.T) {
|
||||
// Test TextContent
|
||||
textContent := TextContent{
|
||||
Text: "Sample text",
|
||||
Annotations: &Annotations{
|
||||
Audience: []RoleType{RoleUser, RoleAssistant},
|
||||
Priority: ptr(1.0),
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(textContent)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"text":"Sample text"`)
|
||||
assert.Contains(t, string(data), `"audience":["user","assistant"]`)
|
||||
assert.Contains(t, string(data), `"priority":1`)
|
||||
|
||||
// Test ImageContent
|
||||
imageContent := ImageContent{
|
||||
Data: "base64data",
|
||||
MimeType: "image/png",
|
||||
}
|
||||
|
||||
data, err = json.Marshal(imageContent)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"data":"base64data"`)
|
||||
assert.Contains(t, string(data), `"mimeType":"image/png"`)
|
||||
|
||||
// Test AudioContent
|
||||
audioContent := AudioContent{
|
||||
Data: "base64audio",
|
||||
MimeType: "audio/mp3",
|
||||
}
|
||||
|
||||
data, err = json.Marshal(audioContent)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"data":"base64audio"`)
|
||||
assert.Contains(t, string(data), `"mimeType":"audio/mp3"`)
|
||||
}
|
||||
|
||||
func TestCallToolResult(t *testing.T) {
|
||||
// Test CallToolResult
|
||||
result := CallToolResult{
|
||||
Result: Result{
|
||||
Meta: map[string]any{
|
||||
"progressToken": "token123",
|
||||
},
|
||||
},
|
||||
Content: []interface{}{
|
||||
TextContent{
|
||||
Text: "Sample result",
|
||||
},
|
||||
},
|
||||
IsError: false,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(result)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"_meta":{"progressToken":"token123"}`)
|
||||
assert.Contains(t, string(data), `"content":[{"text":"Sample result"}]`)
|
||||
assert.NotContains(t, string(data), `"isError":`)
|
||||
}
|
||||
|
||||
func TestRequest_isNotification(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
id any
|
||||
want bool
|
||||
wantErr error
|
||||
}{
|
||||
// integer test cases
|
||||
{name: "int zero", id: 0, want: true, wantErr: nil},
|
||||
{name: "int non-zero", id: 1, want: false, wantErr: nil},
|
||||
{name: "int64 zero", id: int64(0), want: true, wantErr: nil},
|
||||
{name: "int64 max", id: int64(9223372036854775807), want: false, wantErr: nil},
|
||||
|
||||
// floating point number test cases
|
||||
{name: "float64 zero", id: float64(0.0), want: true, wantErr: nil},
|
||||
{name: "float64 positive", id: float64(0.000001), want: false, wantErr: nil},
|
||||
{name: "float64 negative", id: float64(-0.000001), want: false, wantErr: nil},
|
||||
{name: "float64 epsilon", id: float64(1e-300), want: false, wantErr: nil},
|
||||
|
||||
// string test cases
|
||||
{name: "empty string", id: "", want: true, wantErr: nil},
|
||||
{name: "non-empty string", id: "abc", want: false, wantErr: nil},
|
||||
{name: "space string", id: " ", want: false, wantErr: nil},
|
||||
{name: "unicode string", id: "こんにちは", want: false, wantErr: nil},
|
||||
|
||||
// special cases
|
||||
{name: "nil", id: nil, want: true, wantErr: nil},
|
||||
|
||||
// logical type test cases
|
||||
{name: "bool true", id: true, want: false, wantErr: errors.New("invalid type bool")},
|
||||
{name: "bool false", id: false, want: false, wantErr: errors.New("invalid type bool")},
|
||||
{name: "struct type", id: struct{}{}, want: false, wantErr: errors.New("invalid type struct {}")},
|
||||
{name: "slice type", id: []int{1, 2, 3}, want: false, wantErr: errors.New("invalid type []int")},
|
||||
{name: "map type", id: map[string]int{"a": 1}, want: false, wantErr: errors.New("invalid type map[string]int")},
|
||||
{name: "pointer type", id: new(int), want: false, wantErr: errors.New("invalid type *int")},
|
||||
{name: "func type", id: func() {}, want: false, wantErr: errors.New("invalid type func()")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := Request{
|
||||
SessionId: "test-session",
|
||||
JsonRpc: "2.0",
|
||||
ID: tt.id,
|
||||
Method: "testMethod",
|
||||
Params: json.RawMessage(`{}`),
|
||||
}
|
||||
|
||||
got, err := req.isNotification()
|
||||
|
||||
if (err != nil) != (tt.wantErr != nil) {
|
||||
t.Fatalf("error presence mismatch: got error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err != nil && tt.wantErr != nil && err.Error() != tt.wantErr.Error() {
|
||||
t.Fatalf("error message mismatch:\ngot %q\nwant %q", err.Error(), tt.wantErr.Error())
|
||||
}
|
||||
|
||||
if got != tt.want {
|
||||
t.Errorf("isNotification() = %v, want %v for ID %v (%T)", got, tt.want, tt.id, tt.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
107
mcp/util.go
107
mcp/util.go
@@ -1,107 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import "fmt"
|
||||
|
||||
// formatSSEMessage formats a Server-Sent Event message with proper CRLF line endings
|
||||
func formatSSEMessage(event string, data []byte) string {
|
||||
return fmt.Sprintf("event: %s\r\ndata: %s\r\n\r\n", event, string(data))
|
||||
}
|
||||
|
||||
// ptr is a helper function to get a pointer to a value
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func toTypedContents(contents []any) []any {
|
||||
typedContents := make([]any, len(contents))
|
||||
|
||||
for i, content := range contents {
|
||||
switch v := content.(type) {
|
||||
case TextContent:
|
||||
typedContents[i] = typedTextContent{
|
||||
Type: ContentTypeText,
|
||||
TextContent: v,
|
||||
}
|
||||
case ImageContent:
|
||||
typedContents[i] = typedImageContent{
|
||||
Type: ContentTypeImage,
|
||||
ImageContent: v,
|
||||
}
|
||||
case AudioContent:
|
||||
typedContents[i] = typedAudioContent{
|
||||
Type: ContentTypeAudio,
|
||||
AudioContent: v,
|
||||
}
|
||||
default:
|
||||
typedContents[i] = typedTextContent{
|
||||
Type: ContentTypeText,
|
||||
TextContent: TextContent{
|
||||
Text: fmt.Sprintf("Unknown content type: %T", v),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return typedContents
|
||||
}
|
||||
|
||||
func toTypedPromptMessages(messages []PromptMessage) []PromptMessage {
|
||||
typedMessages := make([]PromptMessage, len(messages))
|
||||
|
||||
for i, msg := range messages {
|
||||
switch v := msg.Content.(type) {
|
||||
case TextContent:
|
||||
typedMessages[i] = PromptMessage{
|
||||
Role: msg.Role,
|
||||
Content: typedTextContent{
|
||||
Type: ContentTypeText,
|
||||
TextContent: v,
|
||||
},
|
||||
}
|
||||
case ImageContent:
|
||||
typedMessages[i] = PromptMessage{
|
||||
Role: msg.Role,
|
||||
Content: typedImageContent{
|
||||
Type: ContentTypeImage,
|
||||
ImageContent: v,
|
||||
},
|
||||
}
|
||||
case AudioContent:
|
||||
typedMessages[i] = PromptMessage{
|
||||
Role: msg.Role,
|
||||
Content: typedAudioContent{
|
||||
Type: ContentTypeAudio,
|
||||
AudioContent: v,
|
||||
},
|
||||
}
|
||||
default:
|
||||
typedMessages[i] = PromptMessage{
|
||||
Role: msg.Role,
|
||||
Content: typedTextContent{
|
||||
Type: ContentTypeText,
|
||||
TextContent: TextContent{
|
||||
Text: fmt.Sprintf("Unknown content type: %T", v),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return typedMessages
|
||||
}
|
||||
|
||||
// validatePromptArguments checks if all required arguments are provided
|
||||
// Returns a list of missing required arguments
|
||||
func validatePromptArguments(prompt Prompt, providedArgs map[string]string) []string {
|
||||
var missingArgs []string
|
||||
|
||||
for _, arg := range prompt.Arguments {
|
||||
if arg.Required {
|
||||
if value, exists := providedArgs[arg.Name]; !exists || len(value) == 0 {
|
||||
missingArgs = append(missingArgs, arg.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return missingArgs
|
||||
}
|
||||
274
mcp/util_test.go
274
mcp/util_test.go
@@ -1,274 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Type string
|
||||
Data map[string]any
|
||||
}
|
||||
|
||||
func parseEvent(input string) (*Event, error) {
|
||||
var evt Event
|
||||
var dataStr string
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(input))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "event:") {
|
||||
evt.Type = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
|
||||
} else if strings.HasPrefix(line, "data:") {
|
||||
dataStr = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(dataStr) > 0 {
|
||||
if err := json.Unmarshal([]byte(dataStr), &evt.Data); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse data: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &evt, nil
|
||||
}
|
||||
|
||||
// TestToTypedPromptMessages tests the toTypedPromptMessages function
|
||||
func TestToTypedPromptMessages(t *testing.T) {
|
||||
// Test with multiple message types in one test
|
||||
t.Run("MixedContentTypes", func(t *testing.T) {
|
||||
// Create test data with different content types
|
||||
messages := []PromptMessage{
|
||||
{
|
||||
Role: RoleUser,
|
||||
Content: TextContent{
|
||||
Text: "Hello, this is a text message",
|
||||
Annotations: &Annotations{
|
||||
Audience: []RoleType{RoleUser, RoleAssistant},
|
||||
Priority: ptr(0.8),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: RoleAssistant,
|
||||
Content: ImageContent{
|
||||
Data: "base64ImageData",
|
||||
MimeType: "image/jpeg",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: RoleUser,
|
||||
Content: AudioContent{
|
||||
Data: "base64AudioData",
|
||||
MimeType: "audio/mp3",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "system",
|
||||
Content: "This is a simple string that should be handled as unknown type",
|
||||
},
|
||||
}
|
||||
|
||||
// Call the function
|
||||
result := toTypedPromptMessages(messages)
|
||||
|
||||
// Validate results
|
||||
require.Len(t, result, 4, "Should return the same number of messages")
|
||||
|
||||
// Validate first message (TextContent)
|
||||
msg := result[0]
|
||||
assert.Equal(t, RoleUser, msg.Role, "Role should be preserved")
|
||||
|
||||
// Type assertion using reflection since Content is an interface
|
||||
typed, ok := msg.Content.(typedTextContent)
|
||||
require.True(t, ok, "Should be typedTextContent")
|
||||
assert.Equal(t, ContentTypeText, typed.Type, "Type should be text")
|
||||
assert.Equal(t, "Hello, this is a text message", typed.Text, "Text content should be preserved")
|
||||
require.NotNil(t, typed.Annotations, "Annotations should be preserved")
|
||||
assert.Equal(t, []RoleType{RoleUser, RoleAssistant}, typed.Annotations.Audience, "Audience should be preserved")
|
||||
require.NotNil(t, typed.Annotations.Priority, "Priority should be preserved")
|
||||
assert.Equal(t, 0.8, *typed.Annotations.Priority, "Priority value should be preserved")
|
||||
|
||||
// Validate second message (ImageContent)
|
||||
msg = result[1]
|
||||
assert.Equal(t, RoleAssistant, msg.Role, "Role should be preserved")
|
||||
|
||||
// Type assertion for image content
|
||||
typedImg, ok := msg.Content.(typedImageContent)
|
||||
require.True(t, ok, "Should be typedImageContent")
|
||||
assert.Equal(t, ContentTypeImage, typedImg.Type, "Type should be image")
|
||||
assert.Equal(t, "base64ImageData", typedImg.Data, "Image data should be preserved")
|
||||
assert.Equal(t, "image/jpeg", typedImg.MimeType, "MimeType should be preserved")
|
||||
|
||||
// Validate third message (AudioContent)
|
||||
msg = result[2]
|
||||
assert.Equal(t, RoleUser, msg.Role, "Role should be preserved")
|
||||
|
||||
// Type assertion for audio content
|
||||
typedAudio, ok := msg.Content.(typedAudioContent)
|
||||
require.True(t, ok, "Should be typedAudioContent")
|
||||
assert.Equal(t, ContentTypeAudio, typedAudio.Type, "Type should be audio")
|
||||
assert.Equal(t, "base64AudioData", typedAudio.Data, "Audio data should be preserved")
|
||||
assert.Equal(t, "audio/mp3", typedAudio.MimeType, "MimeType should be preserved")
|
||||
|
||||
// Validate fourth message (unknown type converted to TextContent)
|
||||
msg = result[3]
|
||||
assert.Equal(t, RoleType("system"), msg.Role, "Role should be preserved")
|
||||
|
||||
// Should be converted to a typedTextContent with error message
|
||||
typedUnknown, ok := msg.Content.(typedTextContent)
|
||||
require.True(t, ok, "Unknown content should be converted to typedTextContent")
|
||||
assert.Equal(t, ContentTypeText, typedUnknown.Type, "Type should be text")
|
||||
assert.Contains(t, typedUnknown.Text, "Unknown content type:", "Should contain error about unknown type")
|
||||
assert.Contains(t, typedUnknown.Text, "string", "Should mention the actual type")
|
||||
})
|
||||
|
||||
// Test empty input
|
||||
t.Run("EmptyInput", func(t *testing.T) {
|
||||
messages := []PromptMessage{}
|
||||
result := toTypedPromptMessages(messages)
|
||||
assert.Empty(t, result, "Should return empty slice for empty input")
|
||||
})
|
||||
|
||||
// Test with nil annotations
|
||||
t.Run("NilAnnotations", func(t *testing.T) {
|
||||
messages := []PromptMessage{
|
||||
{
|
||||
Role: RoleUser,
|
||||
Content: TextContent{
|
||||
Text: "Text with nil annotations",
|
||||
Annotations: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := toTypedPromptMessages(messages)
|
||||
require.Len(t, result, 1, "Should return one message")
|
||||
|
||||
typed, ok := result[0].Content.(typedTextContent)
|
||||
require.True(t, ok, "Should be typedTextContent")
|
||||
assert.Equal(t, ContentTypeText, typed.Type, "Type should be text")
|
||||
assert.Equal(t, "Text with nil annotations", typed.Text, "Text content should be preserved")
|
||||
assert.Nil(t, typed.Annotations, "Nil annotations should be preserved as nil")
|
||||
})
|
||||
}
|
||||
|
||||
// TestToTypedContents tests the toTypedContents function
|
||||
func TestToTypedContents(t *testing.T) {
|
||||
// Test with multiple content types in one test
|
||||
t.Run("MixedContentTypes", func(t *testing.T) {
|
||||
// Create test data with different content types
|
||||
contents := []any{
|
||||
TextContent{
|
||||
Text: "Hello, this is a text content",
|
||||
Annotations: &Annotations{
|
||||
Audience: []RoleType{RoleUser, RoleAssistant},
|
||||
Priority: ptr(0.7),
|
||||
},
|
||||
},
|
||||
ImageContent{
|
||||
Data: "base64ImageData",
|
||||
MimeType: "image/png",
|
||||
},
|
||||
AudioContent{
|
||||
Data: "base64AudioData",
|
||||
MimeType: "audio/wav",
|
||||
},
|
||||
"This is a simple string that should be handled as unknown type",
|
||||
}
|
||||
|
||||
// Call the function
|
||||
result := toTypedContents(contents)
|
||||
|
||||
// Validate results
|
||||
require.Len(t, result, 4, "Should return the same number of contents")
|
||||
|
||||
// Validate first content (TextContent)
|
||||
typed, ok := result[0].(typedTextContent)
|
||||
require.True(t, ok, "Should be typedTextContent")
|
||||
assert.Equal(t, ContentTypeText, typed.Type, "Type should be text")
|
||||
assert.Equal(t, "Hello, this is a text content", typed.Text, "Text content should be preserved")
|
||||
require.NotNil(t, typed.Annotations, "Annotations should be preserved")
|
||||
assert.Equal(t, []RoleType{RoleUser, RoleAssistant}, typed.Annotations.Audience, "Audience should be preserved")
|
||||
require.NotNil(t, typed.Annotations.Priority, "Priority should be preserved")
|
||||
assert.Equal(t, 0.7, *typed.Annotations.Priority, "Priority value should be preserved")
|
||||
|
||||
// Validate second content (ImageContent)
|
||||
typedImg, ok := result[1].(typedImageContent)
|
||||
require.True(t, ok, "Should be typedImageContent")
|
||||
assert.Equal(t, ContentTypeImage, typedImg.Type, "Type should be image")
|
||||
assert.Equal(t, "base64ImageData", typedImg.Data, "Image data should be preserved")
|
||||
assert.Equal(t, "image/png", typedImg.MimeType, "MimeType should be preserved")
|
||||
|
||||
// Validate third content (AudioContent)
|
||||
typedAudio, ok := result[2].(typedAudioContent)
|
||||
require.True(t, ok, "Should be typedAudioContent")
|
||||
assert.Equal(t, ContentTypeAudio, typedAudio.Type, "Type should be audio")
|
||||
assert.Equal(t, "base64AudioData", typedAudio.Data, "Audio data should be preserved")
|
||||
assert.Equal(t, "audio/wav", typedAudio.MimeType, "MimeType should be preserved")
|
||||
|
||||
// Validate fourth content (unknown type converted to TextContent)
|
||||
typedUnknown, ok := result[3].(typedTextContent)
|
||||
require.True(t, ok, "Unknown content should be converted to typedTextContent")
|
||||
assert.Equal(t, ContentTypeText, typedUnknown.Type, "Type should be text")
|
||||
assert.Contains(t, typedUnknown.Text, "Unknown content type:", "Should contain error about unknown type")
|
||||
assert.Contains(t, typedUnknown.Text, "string", "Should mention the actual type")
|
||||
})
|
||||
|
||||
// Test empty input
|
||||
t.Run("EmptyInput", func(t *testing.T) {
|
||||
contents := []any{}
|
||||
result := toTypedContents(contents)
|
||||
assert.Empty(t, result, "Should return empty slice for empty input")
|
||||
})
|
||||
|
||||
// Test with nil annotations
|
||||
t.Run("NilAnnotations", func(t *testing.T) {
|
||||
contents := []any{
|
||||
TextContent{
|
||||
Text: "Text with nil annotations",
|
||||
Annotations: nil,
|
||||
},
|
||||
}
|
||||
|
||||
result := toTypedContents(contents)
|
||||
require.Len(t, result, 1, "Should return one content")
|
||||
|
||||
typed, ok := result[0].(typedTextContent)
|
||||
require.True(t, ok, "Should be typedTextContent")
|
||||
assert.Equal(t, ContentTypeText, typed.Type, "Type should be text")
|
||||
assert.Equal(t, "Text with nil annotations", typed.Text, "Text content should be preserved")
|
||||
assert.Nil(t, typed.Annotations, "Nil annotations should be preserved as nil")
|
||||
})
|
||||
|
||||
// Test with custom struct (should be handled as unknown type)
|
||||
t.Run("CustomStruct", func(t *testing.T) {
|
||||
type CustomContent struct {
|
||||
Data string
|
||||
}
|
||||
|
||||
contents := []any{
|
||||
CustomContent{
|
||||
Data: "custom data",
|
||||
},
|
||||
}
|
||||
|
||||
result := toTypedContents(contents)
|
||||
require.Len(t, result, 1, "Should return one content")
|
||||
|
||||
typed, ok := result[0].(typedTextContent)
|
||||
require.True(t, ok, "Custom struct should be converted to typedTextContent")
|
||||
assert.Equal(t, ContentTypeText, typed.Type, "Type should be text")
|
||||
assert.Contains(t, typed.Text, "Unknown content type:", "Should contain error about unknown type")
|
||||
assert.Contains(t, typed.Text, "CustomContent", "Should mention the actual type")
|
||||
})
|
||||
}
|
||||
149
mcp/vars.go
149
mcp/vars.go
@@ -1,149 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
)
|
||||
|
||||
// Protocol constants
|
||||
const (
|
||||
// JSON-RPC version as defined in the specification
|
||||
jsonRpcVersion = "2.0"
|
||||
|
||||
// Session identifier key used in request URLs
|
||||
sessionIdKey = "session_id"
|
||||
|
||||
// progressTokenKey is used to track progress of long-running tasks
|
||||
progressTokenKey = "progressToken"
|
||||
)
|
||||
|
||||
// Server-Sent Events (SSE) event types
|
||||
const (
|
||||
// Standard message event for JSON-RPC responses
|
||||
eventMessage = "message"
|
||||
|
||||
// Endpoint event for sending endpoint URL to clients
|
||||
eventEndpoint = "endpoint"
|
||||
)
|
||||
|
||||
// Content type identifiers
|
||||
const (
|
||||
// ContentTypeObject is object content type
|
||||
ContentTypeObject = "object"
|
||||
|
||||
// ContentTypeText is text content type
|
||||
ContentTypeText = "text"
|
||||
|
||||
// ContentTypeImage is image content type
|
||||
ContentTypeImage = "image"
|
||||
|
||||
// ContentTypeAudio is audio content type
|
||||
ContentTypeAudio = "audio"
|
||||
|
||||
// ContentTypeResource is resource content type
|
||||
ContentTypeResource = "resource"
|
||||
)
|
||||
|
||||
// Collection keys for broadcast events
|
||||
const (
|
||||
// Key for prompts collection
|
||||
keyPrompts = "prompts"
|
||||
|
||||
// Key for resources collection
|
||||
keyResources = "resources"
|
||||
|
||||
// Key for tools collection
|
||||
keyTools = "tools"
|
||||
)
|
||||
|
||||
// JSON-RPC error codes
|
||||
// Standard error codes from JSON-RPC 2.0 spec
|
||||
const (
|
||||
// Invalid JSON was received by the server
|
||||
errCodeInvalidRequest = -32600
|
||||
|
||||
// The method does not exist / is not available
|
||||
errCodeMethodNotFound = -32601
|
||||
|
||||
// Invalid method parameter(s)
|
||||
errCodeInvalidParams = -32602
|
||||
|
||||
// Internal JSON-RPC error
|
||||
errCodeInternalError = -32603
|
||||
|
||||
// Tool execution timed out
|
||||
errCodeTimeout = -32001
|
||||
|
||||
// Resource not found error
|
||||
errCodeResourceNotFound = -32002
|
||||
|
||||
// Client hasn't completed initialization
|
||||
errCodeClientNotInitialized = -32800
|
||||
)
|
||||
|
||||
// User and assistant role definitions
|
||||
const (
|
||||
// RoleUser is the "user" role - the entity asking questions
|
||||
RoleUser RoleType = "user"
|
||||
|
||||
// RoleAssistant is the "assistant" role - the entity providing responses
|
||||
RoleAssistant RoleType = "assistant"
|
||||
)
|
||||
|
||||
// Method names as defined in the MCP specification
|
||||
const (
|
||||
// Initialize the connection between client and server
|
||||
methodInitialize = "initialize"
|
||||
|
||||
// List available tools
|
||||
methodToolsList = "tools/list"
|
||||
|
||||
// Call a specific tool
|
||||
methodToolsCall = "tools/call"
|
||||
|
||||
// List available prompts
|
||||
methodPromptsList = "prompts/list"
|
||||
|
||||
// Get a specific prompt
|
||||
methodPromptsGet = "prompts/get"
|
||||
|
||||
// List available resources
|
||||
methodResourcesList = "resources/list"
|
||||
|
||||
// Read a specific resource
|
||||
methodResourcesRead = "resources/read"
|
||||
|
||||
// Subscribe to resource updates
|
||||
methodResourcesSubscribe = "resources/subscribe"
|
||||
|
||||
// Simple ping to check server availability
|
||||
methodPing = "ping"
|
||||
|
||||
// Notification that client is fully initialized
|
||||
methodNotificationsInitialized = "notifications/initialized"
|
||||
|
||||
// Notification that a request was canceled
|
||||
methodNotificationsCancelled = "notifications/cancelled"
|
||||
)
|
||||
|
||||
// Event names for Server-Sent Events (SSE)
|
||||
const (
|
||||
// Notification of tool list changes
|
||||
eventToolsListChanged = "tools/list_changed"
|
||||
|
||||
// Notification of prompt list changes
|
||||
eventPromptsListChanged = "prompts/list_changed"
|
||||
|
||||
// Notification of resource list changes
|
||||
eventResourcesListChanged = "resources/list_changed"
|
||||
)
|
||||
|
||||
var (
|
||||
// Default channel size for events
|
||||
eventChanSize = 10
|
||||
|
||||
// Default ping interval for checking connection availability
|
||||
// use syncx.ForAtomicDuration to ensure atomicity in test race
|
||||
pingInterval = syncx.ForAtomicDuration(30 * time.Second)
|
||||
)
|
||||
210
mcp/vars_test.go
210
mcp/vars_test.go
@@ -1,210 +0,0 @@
|
||||
// filepath: /Users/kevin/Develop/go/opensource/go-zero/mcp/vars_test.go
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestErrorCodes ensures error codes are applied correctly in error responses
|
||||
func TestErrorCodes(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
code int
|
||||
message string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "invalid request error",
|
||||
code: errCodeInvalidRequest,
|
||||
message: "Invalid request",
|
||||
expected: `"code":-32600`,
|
||||
},
|
||||
{
|
||||
name: "method not found error",
|
||||
code: errCodeMethodNotFound,
|
||||
message: "Method not found",
|
||||
expected: `"code":-32601`,
|
||||
},
|
||||
{
|
||||
name: "invalid params error",
|
||||
code: errCodeInvalidParams,
|
||||
message: "Invalid parameters",
|
||||
expected: `"code":-32602`,
|
||||
},
|
||||
{
|
||||
name: "internal error",
|
||||
code: errCodeInternalError,
|
||||
message: "Internal server error",
|
||||
expected: `"code":-32603`,
|
||||
},
|
||||
{
|
||||
name: "timeout error",
|
||||
code: errCodeTimeout,
|
||||
message: "Operation timed out",
|
||||
expected: `"code":-32001`,
|
||||
},
|
||||
{
|
||||
name: "resource not found error",
|
||||
code: errCodeResourceNotFound,
|
||||
message: "Resource not found",
|
||||
expected: `"code":-32002`,
|
||||
},
|
||||
{
|
||||
name: "client not initialized error",
|
||||
code: errCodeClientNotInitialized,
|
||||
message: "Client not initialized",
|
||||
expected: `"code":-32800`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resp := Response{
|
||||
JsonRpc: jsonRpcVersion,
|
||||
ID: int64(1),
|
||||
Error: &errorObj{
|
||||
Code: tc.code,
|
||||
Message: tc.message,
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), tc.expected, "Error code should match expected value")
|
||||
assert.Contains(t, string(data), tc.message, "Error message should be included")
|
||||
assert.Contains(t, string(data), jsonRpcVersion, "JSON-RPC version should be included")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestJsonRpcVersion ensures the correct JSON-RPC version is used
|
||||
func TestJsonRpcVersion(t *testing.T) {
|
||||
assert.Equal(t, "2.0", jsonRpcVersion, "JSON-RPC version should be 2.0")
|
||||
|
||||
// Test that it's used in responses
|
||||
resp := Response{
|
||||
JsonRpc: jsonRpcVersion,
|
||||
ID: int64(1),
|
||||
Result: "test",
|
||||
}
|
||||
data, err := json.Marshal(resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"jsonrpc":"2.0"`, "Response should use correct JSON-RPC version")
|
||||
|
||||
// Test that it's expected in requests
|
||||
reqStr := `{"jsonrpc":"2.0","id":1,"method":"test"}`
|
||||
var req Request
|
||||
err = json.Unmarshal([]byte(reqStr), &req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, jsonRpcVersion, req.JsonRpc, "Request should parse correct JSON-RPC version")
|
||||
}
|
||||
|
||||
// TestSessionIdKey ensures session ID extraction works correctly
|
||||
func TestSessionIdKey(t *testing.T) {
|
||||
// Create a mock server implementation
|
||||
mock := newMockMcpServer(t)
|
||||
defer mock.shutdown()
|
||||
|
||||
// Verify the key constant
|
||||
assert.Equal(t, "session_id", sessionIdKey, "Session ID key should be 'session_id'")
|
||||
|
||||
// Test that session ID is extracted correctly
|
||||
mockR := httptest.NewRequest("GET", "/?"+sessionIdKey+"=test-session", nil)
|
||||
|
||||
// Since the mock server is using the same session key logic,
|
||||
// we can test this by accessing the request query parameters directly
|
||||
sessionID := mockR.URL.Query().Get(sessionIdKey)
|
||||
assert.Equal(t, "test-session", sessionID, "Session ID should be extracted correctly")
|
||||
}
|
||||
|
||||
// TestEventTypes ensures event types are set correctly in SSE responses
|
||||
func TestEventTypes(t *testing.T) {
|
||||
// Test message event
|
||||
assert.Equal(t, "message", eventMessage, "Message event should be 'message'")
|
||||
|
||||
// Test endpoint event
|
||||
assert.Equal(t, "endpoint", eventEndpoint, "Endpoint event should be 'endpoint'")
|
||||
|
||||
// Verify them in an actual SSE format string
|
||||
messageEvent := "event: " + eventMessage + "\ndata: test\n\n"
|
||||
assert.Contains(t, messageEvent, "event: message", "Message event should format correctly")
|
||||
|
||||
endpointEvent := "event: " + eventEndpoint + "\ndata: test\n\n"
|
||||
assert.Contains(t, endpointEvent, "event: endpoint", "Endpoint event should format correctly")
|
||||
}
|
||||
|
||||
// TestCollectionKeys checks that collection keys are used correctly
|
||||
func TestCollectionKeys(t *testing.T) {
|
||||
// Verify collection key constants
|
||||
assert.Equal(t, "prompts", keyPrompts, "Prompts key should be 'prompts'")
|
||||
assert.Equal(t, "resources", keyResources, "Resources key should be 'resources'")
|
||||
assert.Equal(t, "tools", keyTools, "Tools key should be 'tools'")
|
||||
}
|
||||
|
||||
// TestRoleTypes checks that role types are used correctly
|
||||
func TestRoleTypes(t *testing.T) {
|
||||
// Test in annotations
|
||||
annotations := Annotations{
|
||||
Audience: []RoleType{RoleUser, RoleAssistant},
|
||||
}
|
||||
data, err := json.Marshal(annotations)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"audience":["user","assistant"]`, "Role types should marshal correctly")
|
||||
}
|
||||
|
||||
// TestMethodNames checks that method names are used correctly
|
||||
func TestMethodNames(t *testing.T) {
|
||||
// Verify method name constants
|
||||
methods := map[string]string{
|
||||
"initialize": methodInitialize,
|
||||
"tools/list": methodToolsList,
|
||||
"tools/call": methodToolsCall,
|
||||
"prompts/list": methodPromptsList,
|
||||
"prompts/get": methodPromptsGet,
|
||||
"resources/list": methodResourcesList,
|
||||
"resources/read": methodResourcesRead,
|
||||
"resources/subscribe": methodResourcesSubscribe,
|
||||
"ping": methodPing,
|
||||
"notifications/initialized": methodNotificationsInitialized,
|
||||
"notifications/cancelled": methodNotificationsCancelled,
|
||||
}
|
||||
|
||||
for expected, actual := range methods {
|
||||
assert.Equal(t, expected, actual, "Method name should be "+expected)
|
||||
}
|
||||
|
||||
// Test in a request
|
||||
for methodName := range methods {
|
||||
req := Request{
|
||||
JsonRpc: jsonRpcVersion,
|
||||
ID: int64(1),
|
||||
Method: methodName,
|
||||
}
|
||||
data, err := json.Marshal(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"method":"`+methodName+`"`, "Method name should be used in requests")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEventNames checks that event names are used correctly
|
||||
func TestEventNames(t *testing.T) {
|
||||
// Verify event name constants
|
||||
events := map[string]string{
|
||||
"tools/list_changed": eventToolsListChanged,
|
||||
"prompts/list_changed": eventPromptsListChanged,
|
||||
"resources/list_changed": eventResourcesListChanged,
|
||||
}
|
||||
|
||||
for expected, actual := range events {
|
||||
assert.Equal(t, expected, actual, "Event name should be "+expected)
|
||||
}
|
||||
|
||||
// Test event names in SSE format
|
||||
for _, eventName := range events {
|
||||
sseEvent := "event: " + eventName + "\ndata: test\n\n"
|
||||
assert.Contains(t, sseEvent, "event: "+eventName, "Event name should format correctly in SSE")
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
***缩短从需求到上线的距离***
|
||||
|
||||
[English](readme.md) | 简体中文
|
||||
[English](readme.md) | 简体中文 | [한국어](readme-ko.md)
|
||||
|
||||
[](https://goreportcard.com/report/github.com/zeromicro/go-zero)
|
||||
[](https://goproxy.cn/stats/github.com/zeromicro/go-zero/badges/download-count.svg)
|
||||
|
||||
278
readme-ko.md
Normal file
278
readme-ko.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# go-zero
|
||||
<p align="center">
|
||||
<img align="center" width="150px" src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/go-zero.png">
|
||||
</p>
|
||||
|
||||
go-zero는 다양한 엔지니어링 모범 사례가 내장된 웹 및 RPC 프레임워크입니다. 회복 탄력성 설계로 트래픽이 많은 서비스의 안정성을 보장하기 위해 만들어졌으며, 수년 동안 수천만 사용자를 보유한 사이트에서 운영되어 왔습니다.
|
||||
|
||||
<div align=center>
|
||||
|
||||
[](https://codecov.io/gh/zeromicro/go-zero)
|
||||
[](https://goreportcard.com/report/github.com/zeromicro/go-zero)
|
||||
[](https://github.com/zeromicro/go-zero)
|
||||
[](https://pkg.go.dev/github.com/zeromicro/go-zero)
|
||||
[](https://github.com/avelino/awesome-go)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://discord.gg/4JQvC5A4Fe)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## 🤷 go-zero란?
|
||||
[English](readme.md) | [简体中文](readme-cn.md) | 한국어
|
||||
|
||||
<a href="https://trendshift.io/repositories/3263" target="_blank"><img src="https://trendshift.io/api/badge/repositories/3263" alt="zeromicro%2Fgo-zero | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://www.producthunt.com/posts/go-zero?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-go-zero" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=334030&theme=light" alt="go-zero - A web & rpc framework written in Go. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
|
||||
go-zero는 [CNCF Landscape](https://landscape.cncf.io/?selected=go-zero)에 등재된, 다양한 엔지니어링 모범 사례가 내장된 웹 및 RPC 프레임워크입니다. 회복 탄력성 설계로 트래픽이 많은 서비스의 안정성을 보장하기 위해 만들어졌으며, 수년 동안 수천만 사용자를 보유한 사이트에서 운영되어 왔습니다.
|
||||
|
||||
go-zero에는 간단한 API 설명 문법과 `goctl`이라는 코드 생성 도구가 포함되어 있습니다. `goctl`을 사용하면 .api 파일에서 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 코드를 생성할 수 있습니다.
|
||||
|
||||
#### go-zero의 장점:
|
||||
|
||||
* 수천만 일간 활성 사용자를 처리하는 서비스의 안정성을 높입니다.
|
||||
* 체인형 타임아웃 제어, 동시성 제어, 속도 제한, 적응형 서킷 브레이커, 적응형 로드 셰딩이 내장되어 있으며, 별도 설정 없이도 사용할 수 있습니다.
|
||||
* 내장 미들웨어를 기존 프레임워크에 통합할 수 있습니다.
|
||||
* 간단한 API 문법과 한 번의 명령으로 여러 언어 코드를 생성할 수 있습니다.
|
||||
* 클라이언트 요청 파라미터를 자동으로 검증합니다.
|
||||
* 다양한 내장 마이크로서비스 관리 도구와 동시성 도구를 제공합니다.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/architecture-en.png" alt="Architecture" width="1500" />
|
||||
|
||||
## go-zero의 배경
|
||||
|
||||
2018년 초, 우리는 Java+MongoDB 기반 모놀리식 아키텍처에서 마이크로서비스로 전환하면서 다음을 선택했습니다.
|
||||
|
||||
* **Golang** - 높은 성능, 간결한 문법, 뛰어난 배포 경험, 낮은 리소스 사용량
|
||||
* **자체 설계한 마이크로서비스 프레임워크** - 더 나은 문제 격리, 더 쉬운 기능 확장, 더 빠른 이슈 해결
|
||||
|
||||
## go-zero의 설계 고려 사항
|
||||
|
||||
go-zero는 다음 핵심 설계 원칙을 따릅니다.
|
||||
|
||||
* **단순성** - 단순하게 유지하는 것을 제1원칙으로 삼습니다.
|
||||
* **고가용성** - 높은 동시성 상황에서도 안정적으로 동작합니다.
|
||||
* **회복 탄력성** - 적응형 보호를 갖춘 장애 지향 프로그래밍을 지향합니다.
|
||||
* **개발자 친화성** - 복잡성을 캡슐화하고 한 가지 일을 하는 한 가지 방법을 제공합니다.
|
||||
* **확장 용이성** - 성장을 위한 유연한 아키텍처를 제공합니다.
|
||||
|
||||
## go-zero의 구현과 기능
|
||||
|
||||
go-zero는 엔지니어링 모범 사례를 통합합니다.
|
||||
|
||||
* **코드 생성** - 보일러플레이트를 최소화하는 강력한 도구
|
||||
* **간단한 API** - 깔끔한 인터페이스와 net/http 완전 호환
|
||||
* **고성능** - 속도와 효율성을 위한 최적화
|
||||
* **회복 탄력성** - 내장 서킷 브레이커, 속도 제한, 로드 셰딩, 타임아웃 제어
|
||||
* **서비스 메시** - 서비스 디스커버리, 부하 분산, 호출 추적
|
||||
* **개발자 도구** - 자동 파라미터 검증, 캐시 관리, 메트릭과 모니터링
|
||||
|
||||

|
||||
|
||||
## go-zero 아키텍처
|
||||
|
||||
<img width="1067" alt="image" src="https://user-images.githubusercontent.com/1918356/171880372-5010d846-e8b1-4942-8fe2-e2bbb584f762.png">
|
||||
|
||||
## 설치
|
||||
|
||||
프로젝트에서 다음 명령을 실행하세요.
|
||||
|
||||
```shell
|
||||
go get -u github.com/zeromicro/go-zero
|
||||
```
|
||||
|
||||
## AI 네이티브 개발
|
||||
|
||||
go-zero 팀은 Claude, GitHub Copilot, Cursor가 프레임워크 규칙을 따르는 코드를 생성할 수 있도록 AI 도구를 제공합니다.
|
||||
|
||||
### 세 가지 핵심 프로젝트
|
||||
|
||||
**[ai-context](https://github.com/zeromicro/ai-context)** - AI 어시스턴트를 위한 워크플로 가이드
|
||||
|
||||
**[zero-skills](https://github.com/zeromicro/zero-skills)** - 예제가 포함된 패턴 라이브러리
|
||||
|
||||
**[mcp-zero](https://github.com/zeromicro/mcp-zero)** - Model Context Protocol을 통한 코드 생성 도구
|
||||
|
||||
### 빠른 설정
|
||||
|
||||
#### GitHub Copilot
|
||||
```bash
|
||||
git submodule add https://github.com/zeromicro/ai-context.git .github/ai-context
|
||||
ln -s ai-context/00-instructions.md .github/copilot-instructions.md # macOS/Linux
|
||||
# Windows: mklink .github\copilot-instructions.md .github\ai-context\00-instructions.md
|
||||
git submodule update --remote .github/ai-context # 업데이트
|
||||
```
|
||||
|
||||
#### Cursor
|
||||
```bash
|
||||
git submodule add https://github.com/zeromicro/ai-context.git .cursorrules
|
||||
git submodule update --remote .cursorrules # 업데이트
|
||||
```
|
||||
|
||||
#### Windsurf
|
||||
```bash
|
||||
git submodule add https://github.com/zeromicro/ai-context.git .windsurfrules
|
||||
git submodule update --remote .windsurfrules # 업데이트
|
||||
```
|
||||
|
||||
#### Claude Desktop
|
||||
```bash
|
||||
git clone https://github.com/zeromicro/mcp-zero.git && cd mcp-zero && go build
|
||||
# 설정: ~/Library/Application Support/Claude/claude_desktop_config.json
|
||||
# 또는: claude mcp add --transport stdio mcp-zero --env GOCTL_PATH=/path/to/goctl -- /path/to/mcp-zero
|
||||
```
|
||||
|
||||
### 동작 방식
|
||||
|
||||
AI 어시스턴트는 다음 도구를 함께 사용합니다.
|
||||
1. **ai-context** - 워크플로 안내
|
||||
2. **zero-skills** - 구현 패턴
|
||||
3. **mcp-zero** - 실시간 코드 생성
|
||||
|
||||
**예시**: REST API 생성 → AI가 **ai-context**에서 워크플로를 읽음 → **mcp-zero**를 호출해 코드 생성 → **zero-skills**에서 패턴 참조 → 프로덕션 준비가 된 코드 생성 ✅
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
1. 전체 예제:
|
||||
|
||||
[마이크로서비스 시스템 빠른 개발](https://github.com/zeromicro/zero-doc/blob/main/doc/shorturl-en.md)
|
||||
|
||||
[마이크로서비스 시스템 빠른 개발 - 다중 RPC](https://github.com/zeromicro/zero-doc/blob/main/docs/zero/bookstore-en.md)
|
||||
|
||||
2. goctl 설치
|
||||
|
||||
```shell
|
||||
# Go 사용
|
||||
go install github.com/zeromicro/go-zero/tools/goctl@latest
|
||||
|
||||
# Mac 사용
|
||||
brew install goctl
|
||||
|
||||
# 모든 플랫폼에서 docker 사용
|
||||
docker pull kevinwan/goctl
|
||||
# goctl 실행
|
||||
docker run --rm -it -v `pwd`:/app kevinwan/goctl --help
|
||||
```
|
||||
|
||||
goctl이 실행 가능하고 $PATH에 포함되어 있는지 확인하세요.
|
||||
|
||||
3. API 파일(greet.api) 생성:
|
||||
|
||||
```go
|
||||
type (
|
||||
Request {
|
||||
Name string `path:"name,options=[you,me]"` // 파라미터는 자동으로 검증됩니다.
|
||||
}
|
||||
|
||||
Response {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
)
|
||||
|
||||
service greet-api {
|
||||
@handler GreetHandler
|
||||
get /greet/from/:name(Request) returns (Response)
|
||||
}
|
||||
```
|
||||
|
||||
.api 템플릿 생성:
|
||||
|
||||
```shell
|
||||
goctl api -o greet.api
|
||||
```
|
||||
|
||||
4. Go 서버 코드 생성
|
||||
|
||||
```shell
|
||||
goctl api go -api greet.api -dir greet
|
||||
```
|
||||
|
||||
생성되는 구조:
|
||||
|
||||
```Plain Text
|
||||
├── greet
|
||||
│ ├── etc
|
||||
│ │ └── greet-api.yaml // 설정 파일
|
||||
│ ├── greet.go // main 파일
|
||||
│ └── internal
|
||||
│ ├── config
|
||||
│ │ └── config.go // 설정 정의
|
||||
│ ├── handler
|
||||
│ │ ├── greethandler.go // get/put/post/delete 라우트가 여기에 정의됩니다.
|
||||
│ │ └── routes.go // 라우트 목록
|
||||
│ ├── logic
|
||||
│ │ └── greetlogic.go // 요청 로직을 여기에 작성할 수 있습니다.
|
||||
│ ├── svc
|
||||
│ │ └── servicecontext.go // 서비스 컨텍스트, mysql/redis를 여기에 전달할 수 있습니다.
|
||||
│ └── types
|
||||
│ └── types.go // 요청/응답이 여기에 정의됩니다.
|
||||
└── greet.api // api 설명 파일
|
||||
```
|
||||
|
||||
서비스 실행:
|
||||
|
||||
```shell
|
||||
cd greet
|
||||
go mod tidy
|
||||
go run greet.go -f etc/greet-api.yaml
|
||||
```
|
||||
|
||||
기본 포트: 8888(etc/greet-api.yaml에서 변경 가능)
|
||||
|
||||
curl로 테스트:
|
||||
|
||||
```shell
|
||||
curl -i http://localhost:8888/greet/from/you
|
||||
```
|
||||
|
||||
응답:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Date: Sun, 30 Aug 2020 15:32:35 GMT
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
5. 비즈니스 로직 작성
|
||||
|
||||
* servicecontext.go를 통해 의존성(mysql, redis 등)을 전달합니다.
|
||||
* .api 정의에 따라 logic 패키지에 로직 코드를 추가합니다.
|
||||
|
||||
6. 여러 언어의 클라이언트 코드 생성
|
||||
|
||||
```shell
|
||||
goctl api java -api greet.api -dir greet
|
||||
goctl api dart -api greet.api -dir greet
|
||||
...
|
||||
```
|
||||
|
||||
## 벤치마크
|
||||
|
||||

|
||||
|
||||
[테스트 코드 보기](https://github.com/smallnest/go-web-framework-benchmark)
|
||||
|
||||
## 문서
|
||||
|
||||
* [문서](https://go-zero.dev/)
|
||||
* [마이크로서비스 시스템 빠른 개발](https://github.com/zeromicro/zero-doc/blob/main/doc/shorturl-en.md)
|
||||
* [마이크로서비스 시스템 빠른 개발 - 다중 RPC](https://github.com/zeromicro/zero-doc/blob/main/docs/zero/bookstore-en.md)
|
||||
* [예제](https://github.com/zeromicro/zero-examples)
|
||||
|
||||
## 채팅 그룹
|
||||
|
||||
다음 링크에서 채팅에 참여하세요: https://discord.gg/4JQvC5A4Fe
|
||||
|
||||
## Cloud Native Landscape
|
||||
|
||||
<p float="left">
|
||||
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/cncf-logo.svg" width="200"/>
|
||||
<img src="https://raw.githubusercontent.com/zeromicro/zero-doc/main/doc/images/cncf-landscape-logo.svg" width="150"/>
|
||||
</p>
|
||||
|
||||
go-zero는 [CNCF Cloud Native Landscape](https://landscape.cncf.io/?selected=go-zero)에 등재되었습니다.
|
||||
|
||||
## 별을 눌러주세요! ⭐
|
||||
|
||||
이 프로젝트가 마음에 들거나 학습 또는 자체 솔루션을 시작하는 데 사용 중이라면, 새 릴리스 업데이트를 받을 수 있도록 star를 눌러주세요. 여러분의 지원은 큰 힘이 됩니다!
|
||||
@@ -19,7 +19,7 @@ go-zero is a web and rpc framework with lots of builtin engineering practices. I
|
||||
|
||||
|
||||
## 🤷 What is go-zero?
|
||||
English | [简体中文](readme-cn.md)
|
||||
English | [简体中文](readme-cn.md) | [한국어](readme-ko.md)
|
||||
|
||||
<a href="https://trendshift.io/repositories/3263" target="_blank"><img src="https://trendshift.io/api/badge/repositories/3263" alt="zeromicro%2Fgo-zero | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://www.producthunt.com/posts/go-zero?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-go-zero" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=334030&theme=light" alt="go-zero - A web & rpc framework written in Go. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
@@ -21,10 +21,11 @@ import (
|
||||
|
||||
func TestOtelHandler(t *testing.T) {
|
||||
ztrace.StartAgent(ztrace.Config{
|
||||
Name: "go-zero-test",
|
||||
Endpoint: "http://localhost:14268/api/traces",
|
||||
Batcher: "jaeger",
|
||||
Sampler: 1.0,
|
||||
Name: "go-zero-test",
|
||||
Endpoint: "http://localhost:14268",
|
||||
OtlpHttpPath: "/v1/traces",
|
||||
Batcher: "otlphttp",
|
||||
Sampler: 1.0,
|
||||
})
|
||||
defer ztrace.StopAgent()
|
||||
|
||||
@@ -84,10 +85,11 @@ func TestTraceHandler(t *testing.T) {
|
||||
|
||||
func TestDontTracingSpan(t *testing.T) {
|
||||
ztrace.StartAgent(ztrace.Config{
|
||||
Name: "go-zero-test",
|
||||
Endpoint: "http://localhost:14268/api/traces",
|
||||
Batcher: "jaeger",
|
||||
Sampler: 1.0,
|
||||
Name: "go-zero-test",
|
||||
Endpoint: "http://localhost:14268",
|
||||
OtlpHttpPath: "/v1/traces",
|
||||
Batcher: "otlphttp",
|
||||
Sampler: 1.0,
|
||||
})
|
||||
defer ztrace.StopAgent()
|
||||
|
||||
@@ -129,10 +131,11 @@ func TestDontTracingSpan(t *testing.T) {
|
||||
|
||||
func TestTraceResponseWriter(t *testing.T) {
|
||||
ztrace.StartAgent(ztrace.Config{
|
||||
Name: "go-zero-test",
|
||||
Endpoint: "http://localhost:14268/api/traces",
|
||||
Batcher: "jaeger",
|
||||
Sampler: 1.0,
|
||||
Name: "go-zero-test",
|
||||
Endpoint: "http://localhost:14268",
|
||||
OtlpHttpPath: "/v1/traces",
|
||||
Batcher: "otlphttp",
|
||||
Sampler: 1.0,
|
||||
})
|
||||
defer ztrace.StopAgent()
|
||||
|
||||
|
||||
@@ -84,8 +84,11 @@ func buildRequest(ctx context.Context, method, url string, data any) (*http.Requ
|
||||
var reader io.Reader
|
||||
jsonVars, hasJsonBody := val[jsonKey]
|
||||
if hasJsonBody {
|
||||
if method == http.MethodGet {
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
return nil, ErrGetWithBody
|
||||
case http.MethodHead:
|
||||
return nil, ErrHeadWithBody
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
@@ -21,10 +21,11 @@ import (
|
||||
|
||||
func TestDoRequest(t *testing.T) {
|
||||
ztrace.StartAgent(ztrace.Config{
|
||||
Name: "go-zero-test",
|
||||
Endpoint: "http://localhost:14268/api/traces",
|
||||
Batcher: "jaeger",
|
||||
Sampler: 1.0,
|
||||
Name: "go-zero-test",
|
||||
Endpoint: "http://localhost:14268",
|
||||
OtlpHttpPath: "/v1/traces",
|
||||
Batcher: "otlphttp",
|
||||
Sampler: 1.0,
|
||||
})
|
||||
defer ztrace.StopAgent()
|
||||
|
||||
@@ -228,3 +229,106 @@ func TestDo_WithClientHttpTrace(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, enter)
|
||||
}
|
||||
|
||||
func TestBuildRequestWithBody(t *testing.T) {
|
||||
testBody := struct {
|
||||
Key string `json:"key"`
|
||||
Value int `json:"value"`
|
||||
}{
|
||||
Key: "foo",
|
||||
Value: 10,
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
testName string
|
||||
method string
|
||||
url string
|
||||
body any
|
||||
wantedErr error
|
||||
}{
|
||||
{
|
||||
testName: "GET Request with Body",
|
||||
method: http.MethodGet,
|
||||
url: "/ping",
|
||||
body: testBody,
|
||||
wantedErr: ErrGetWithBody,
|
||||
},
|
||||
{
|
||||
testName: "GET Request without Body",
|
||||
method: http.MethodGet,
|
||||
url: "/ping",
|
||||
body: nil,
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
testName: "HEAD Request with Body",
|
||||
method: http.MethodHead,
|
||||
url: "/ping",
|
||||
body: testBody,
|
||||
wantedErr: ErrHeadWithBody,
|
||||
},
|
||||
{
|
||||
testName: "HEAD Request without Body",
|
||||
method: http.MethodHead,
|
||||
url: "/ping",
|
||||
body: nil,
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
testName: "POST Request with Body",
|
||||
method: http.MethodPost,
|
||||
url: "/ping",
|
||||
body: testBody,
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
testName: "PUT Request with Body",
|
||||
method: http.MethodPut,
|
||||
url: "/ping",
|
||||
body: testBody,
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
testName: "PATCH Request with Body",
|
||||
method: http.MethodPatch,
|
||||
url: "/ping",
|
||||
body: testBody,
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
testName: "DELETE Request with Body",
|
||||
method: http.MethodDelete,
|
||||
url: "/ping",
|
||||
body: testBody,
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
testName: "CONNECT Request with Body",
|
||||
method: http.MethodConnect,
|
||||
url: "/ping",
|
||||
body: testBody,
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
testName: "OPTIONS Request with Body",
|
||||
method: http.MethodOptions,
|
||||
url: "/ping",
|
||||
body: testBody,
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
testName: "TRACE Request with Body",
|
||||
method: http.MethodTrace,
|
||||
url: "/ping",
|
||||
body: testBody,
|
||||
wantedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
_, err := buildRequest(context.Background(), tc.method, tc.url, tc.body)
|
||||
assert.Equal(t, tc.wantedErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package httpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/breaker"
|
||||
)
|
||||
@@ -67,8 +70,44 @@ func (s namedService) do(r *http.Request) (resp *http.Response, err error) {
|
||||
resp, err = s.cli.Do(r)
|
||||
return err
|
||||
}, func(err error) bool {
|
||||
return err == nil && resp.StatusCode < http.StatusInternalServerError
|
||||
return acceptable(resp, err)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// acceptable determines whether the HTTP request/response should be considered
|
||||
// successful for circuit breaker purposes.
|
||||
//
|
||||
// Returns true (acceptable) for:
|
||||
// - HTTP status codes < 500 (2xx, 3xx, 4xx)
|
||||
// - Context cancellation (user-initiated)
|
||||
// - Non-network errors (application-level errors)
|
||||
//
|
||||
// Returns false (not acceptable, triggers breaker) for:
|
||||
// - HTTP status codes >= 500 (server errors)
|
||||
// - context.DeadlineExceeded (timeout)
|
||||
// - Network errors (connection refused, DNS failures, etc.)
|
||||
func acceptable(resp *http.Response, err error) bool {
|
||||
if err == nil {
|
||||
return resp.StatusCode < http.StatusInternalServerError
|
||||
}
|
||||
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return false
|
||||
}
|
||||
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Unwrap url.Error if present
|
||||
var ue *url.Error
|
||||
if errors.As(err, &ue) {
|
||||
err = ue.Unwrap()
|
||||
}
|
||||
|
||||
// Network errors are not acceptable
|
||||
var ne net.Error
|
||||
return !errors.As(err, &ne)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@ package httpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/rest/internal/header"
|
||||
@@ -84,3 +88,256 @@ func TestNamedService_DoBadRequest(t *testing.T) {
|
||||
_, err := service.Do(context.Background(), http.MethodPost, "/nodes/:key", val)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
// mockNetError implements net.Error interface for testing
|
||||
type mockNetError struct {
|
||||
msg string
|
||||
timeout bool
|
||||
temporary bool
|
||||
}
|
||||
|
||||
func (e *mockNetError) Error() string { return e.msg }
|
||||
func (e *mockNetError) Timeout() bool { return e.timeout }
|
||||
func (e *mockNetError) Temporary() bool { return e.temporary }
|
||||
|
||||
func TestAcceptable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
resp *http.Response
|
||||
err error
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "no error with 2xx status code",
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
},
|
||||
err: nil,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no error with 3xx status code",
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusMovedPermanently,
|
||||
},
|
||||
err: nil,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no error with 4xx status code",
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
},
|
||||
err: nil,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no error with 499 status code (just below 500)",
|
||||
resp: &http.Response{
|
||||
StatusCode: 499,
|
||||
},
|
||||
err: nil,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no error with 500 status code",
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
err: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no error with 503 status code",
|
||||
resp: &http.Response{
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
err: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "context deadline exceeded",
|
||||
resp: nil,
|
||||
err: context.DeadlineExceeded,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "context canceled",
|
||||
resp: nil,
|
||||
err: context.Canceled,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wrapped context deadline exceeded",
|
||||
resp: nil,
|
||||
err: errors.Join(context.DeadlineExceeded, errors.New("timeout")),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrapped context canceled",
|
||||
resp: nil,
|
||||
err: errors.Join(context.Canceled, errors.New("canceled")),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "network error - timeout",
|
||||
resp: nil,
|
||||
err: &mockNetError{msg: "network timeout", timeout: true, temporary: false},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "network error - temporary",
|
||||
resp: nil,
|
||||
err: &mockNetError{msg: "temporary network error", timeout: false, temporary: true},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "network error - connection refused",
|
||||
resp: nil,
|
||||
err: &net.OpError{Op: "dial", Net: "tcp", Err: errors.New("connection refused")},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "url.Error wrapping network error",
|
||||
resp: nil,
|
||||
err: &url.Error{
|
||||
Op: "Get",
|
||||
URL: "http://example.com",
|
||||
Err: &mockNetError{msg: "network error", timeout: true},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "url.Error wrapping non-network error",
|
||||
resp: nil,
|
||||
err: &url.Error{
|
||||
Op: "Get",
|
||||
URL: "http://example.com",
|
||||
Err: errors.New("some other error"),
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "url.Error wrapping context.DeadlineExceeded",
|
||||
resp: nil,
|
||||
err: &url.Error{
|
||||
Op: "Get",
|
||||
URL: "http://example.com",
|
||||
Err: context.DeadlineExceeded,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "url.Error wrapping context.Canceled",
|
||||
resp: nil,
|
||||
err: &url.Error{
|
||||
Op: "Get",
|
||||
URL: "http://example.com",
|
||||
Err: context.Canceled,
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "generic error (non-network)",
|
||||
resp: nil,
|
||||
err: errors.New("some random error"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "EOF error (non-network)",
|
||||
resp: nil,
|
||||
err: errors.New("EOF"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "nil response with nil error (edge case)",
|
||||
resp: nil,
|
||||
err: nil,
|
||||
expected: false, // Will panic in real code, but resp.StatusCode access
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Handle the edge case where resp is nil and err is nil
|
||||
if tt.resp == nil && tt.err == nil {
|
||||
// This would panic in real code, so we skip the actual test
|
||||
// In production, this should never happen
|
||||
return
|
||||
}
|
||||
|
||||
result := acceptable(tt.resp, tt.err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptable_RealNetworkTimeout(t *testing.T) {
|
||||
// Create a client with very short timeout
|
||||
client := &http.Client{
|
||||
Timeout: 1 * time.Nanosecond, // Extremely short timeout to force timeout error
|
||||
}
|
||||
|
||||
service := NewServiceWithClient("test", client)
|
||||
|
||||
// Create a server that delays response
|
||||
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer svr.Close()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, svr.URL, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// This should timeout and trigger the circuit breaker
|
||||
resp, err := service.DoRequest(req)
|
||||
|
||||
// The error should be present due to timeout
|
||||
assert.Error(t, err)
|
||||
// Response might be nil due to timeout
|
||||
if resp != nil {
|
||||
t.Logf("Response status: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptable_Integration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
expectBreaker bool // Whether breaker should consider this as failure
|
||||
}{
|
||||
{"200 OK should not trigger breaker", http.StatusOK, false},
|
||||
{"201 Created should not trigger breaker", http.StatusCreated, false},
|
||||
{"400 Bad Request should not trigger breaker", http.StatusBadRequest, false},
|
||||
{"404 Not Found should not trigger breaker", http.StatusNotFound, false},
|
||||
{"500 Internal Server Error should trigger breaker", http.StatusInternalServerError, true},
|
||||
{"502 Bad Gateway should trigger breaker", http.StatusBadGateway, true},
|
||||
{"503 Service Unavailable should trigger breaker", http.StatusServiceUnavailable, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(tt.statusCode)
|
||||
}))
|
||||
defer svr.Close()
|
||||
|
||||
service := NewService("test-service-" + tt.name)
|
||||
req, err := http.NewRequest(http.MethodGet, svr.URL, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
resp, err := service.DoRequest(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.statusCode, resp.StatusCode)
|
||||
|
||||
// The actual breaker behavior is tested implicitly through the acceptable function
|
||||
result := acceptable(resp, nil)
|
||||
if tt.expectBreaker {
|
||||
assert.False(t, result, "Status %d should not be acceptable", tt.statusCode)
|
||||
} else {
|
||||
assert.True(t, result, "Status %d should be acceptable", tt.statusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,9 @@ const (
|
||||
colon = ':'
|
||||
)
|
||||
|
||||
// ErrGetWithBody indicates that GET request with body.
|
||||
var ErrGetWithBody = errors.New("HTTP GET should not have body")
|
||||
var (
|
||||
// ErrGetWithBody indicates that GET request with body.
|
||||
ErrGetWithBody = errors.New("HTTP GET should not have body")
|
||||
// ErrHeadWithBody indicates that HEAD request with body.
|
||||
ErrHeadWithBody = errors.New("HTTP HEAD should not have body")
|
||||
)
|
||||
|
||||
34
tools/goctl/api/swagger/issue5496/issue5496.api
Normal file
34
tools/goctl/api/swagger/issue5496/issue5496.api
Normal file
@@ -0,0 +1,34 @@
|
||||
syntax = "v1"
|
||||
|
||||
info (
|
||||
title: "Issue 5496 Reproduce"
|
||||
version: "v1"
|
||||
host: "localhost"
|
||||
)
|
||||
|
||||
// Reproduces issue #5496:
|
||||
// The `example` tag option in `path` and `form` tags does not appear in generated swagger JSON.
|
||||
type (
|
||||
Request {
|
||||
// path param with example — should appear as example in swagger
|
||||
Name string `path:"name,options=you|me,example=nihao"`
|
||||
// form param with example and range — should appear as example in swagger
|
||||
Age int `form:"age,optional,range=[1:200],example=18"`
|
||||
}
|
||||
Response {
|
||||
Name string `json:"name,example=nihao"`
|
||||
Age int `json:"age,example=18"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
tags: "issue5496"
|
||||
summary: "Issue 5496 - example field missing for path/form params"
|
||||
)
|
||||
service issue5496 {
|
||||
@doc (
|
||||
description: "Demonstrate that example is missing in path/form swagger params"
|
||||
)
|
||||
@handler getUser
|
||||
get /user/:name (Request) returns (Response)
|
||||
}
|
||||
81
tools/goctl/api/swagger/issue5496/issue5496.json
Normal file
81
tools/goctl/api/swagger/issue5496/issue5496.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "Issue 5496 Reproduce",
|
||||
"version": "v1"
|
||||
},
|
||||
"host": "localhost",
|
||||
"basePath": "/",
|
||||
"paths": {
|
||||
"/user/{name}": {
|
||||
"get": {
|
||||
"description": "Demonstrate that example is missing in path/form swagger params",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"issue5496"
|
||||
],
|
||||
"summary": "getUser",
|
||||
"operationId": "getUser",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"you",
|
||||
"me"
|
||||
],
|
||||
"type": "string",
|
||||
"example": "nihao",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"example": 18,
|
||||
"name": "age",
|
||||
"in": "query",
|
||||
"allowEmptyValue": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"age": {
|
||||
"type": "integer",
|
||||
"example": 18
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "nihao"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-date": "2026-03-19 22:52:34",
|
||||
"x-description": "This is a goctl generated swagger file.",
|
||||
"x-github": "https://github.com/zeromicro/go-zero",
|
||||
"x-go-zero-doc": "https://go-zero.dev/",
|
||||
"x-goctl-version": "1.10.0"
|
||||
}
|
||||
@@ -72,6 +72,7 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(ctx, member.Type),
|
||||
Default: defValueFromOptions(ctx, headerTag.Options, member.Type),
|
||||
Example: exampleValueFromOptions(ctx, headerTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(ctx, member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
@@ -96,6 +97,7 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(ctx, member.Type),
|
||||
Default: defValueFromOptions(ctx, pathParameterTag.Options, member.Type),
|
||||
Example: exampleValueFromOptions(ctx, pathParameterTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(ctx, member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
@@ -121,6 +123,7 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(ctx, member.Type),
|
||||
Default: defValueFromOptions(ctx, formTag.Options, member.Type),
|
||||
Example: exampleValueFromOptions(ctx, formTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(ctx, member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
@@ -143,6 +146,7 @@ func parametersFromType(ctx Context, method string, tp apiSpec.Type) []spec.Para
|
||||
SimpleSchema: spec.SimpleSchema{
|
||||
Type: sampleTypeFromGoType(ctx, member.Type),
|
||||
Default: defValueFromOptions(ctx, formTag.Options, member.Type),
|
||||
Example: exampleValueFromOptions(ctx, formTag.Options, member.Type),
|
||||
Items: sampleItemsFromGoType(ctx, member.Type),
|
||||
},
|
||||
ParamProps: spec.ParamProps{
|
||||
|
||||
@@ -83,6 +83,41 @@ func TestParametersFromType_EdgeCases(t *testing.T) {
|
||||
assert.Empty(t, params)
|
||||
}
|
||||
|
||||
// TestParametersFromType_ExampleField reproduces issue #5496:
|
||||
// example= in path/form tags was not emitted in the generated swagger JSON.
|
||||
func TestParametersFromType_ExampleField(t *testing.T) {
|
||||
ctx := testingContext(t)
|
||||
|
||||
testStruct := apiSpec.DefineStruct{
|
||||
RawName: "Request",
|
||||
Members: []apiSpec.Member{
|
||||
{
|
||||
Name: "Name",
|
||||
Type: apiSpec.PrimitiveType{RawName: "string"},
|
||||
Tag: `path:"name,options=you|me,example=nihao"`,
|
||||
},
|
||||
{
|
||||
Name: "Age",
|
||||
Type: apiSpec.PrimitiveType{RawName: "int"},
|
||||
Tag: `form:"age,optional,range=[1:200],example=18"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
params := parametersFromType(ctx, http.MethodGet, testStruct)
|
||||
assert.Len(t, params, 2)
|
||||
|
||||
// path param should have example
|
||||
pathParam := params[0]
|
||||
assert.Equal(t, paramsInPath, pathParam.In)
|
||||
assert.Equal(t, "nihao", pathParam.SimpleSchema.Example, "path param example should be set")
|
||||
|
||||
// form/query param should have example
|
||||
queryParam := params[1]
|
||||
assert.Equal(t, paramsInQuery, queryParam.In)
|
||||
assert.EqualValues(t, int64(18), queryParam.SimpleSchema.Example, "form param example should be set")
|
||||
}
|
||||
|
||||
func createTestStruct(name string, hasJson bool) apiSpec.DefineStruct {
|
||||
tag := `form:"username"`
|
||||
if hasJson {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
module github.com/zeromicro/go-zero/tools/goctl
|
||||
|
||||
go 1.21
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/emicklei/proto v1.14.2
|
||||
github.com/emicklei/proto v1.14.3
|
||||
github.com/fatih/structtag v1.2.0
|
||||
github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e
|
||||
github.com/go-sql-driver/mysql v1.9.0
|
||||
github.com/gookit/color v1.6.0
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
@@ -16,103 +16,108 @@ require (
|
||||
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1
|
||||
github.com/zeromicro/antlr v0.0.1
|
||||
github.com/zeromicro/ddl-parser v1.0.5
|
||||
github.com/zeromicro/go-zero v1.9.3
|
||||
golang.org/x/text v0.22.0
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.36.5
|
||||
github.com/zeromicro/go-zero v1.10.2
|
||||
golang.org/x/text v0.34.0
|
||||
google.golang.org/grpc v1.80.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/alicebob/miniredis/v2 v2.38.0 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // 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/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafana/pyroscope-go v1.2.7 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||
github.com/grafana/pyroscope-go v1.3.0 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.10 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.4 // indirect
|
||||
github.com/jackc/pgx/v5 v5.8.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/openzipkin/zipkin-go v0.4.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.21.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/redis/go-redis/v9 v9.16.0 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/redis/go-redis/v9 v9.19.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/titanous/json5 v1.0.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.15 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.15 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.21 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/time v0.10.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.29.3 // indirect
|
||||
k8s.io/apimachinery v0.29.4 // indirect
|
||||
k8s.io/client-go v0.29.3 // indirect
|
||||
k8s.io/klog/v2 v2.110.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
k8s.io/api v0.34.3 // indirect
|
||||
k8s.io/apimachinery v0.34.3 // indirect
|
||||
k8s.io/client-go v0.34.3 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec h1:EEyRvzmpEUZ+I8WmD5cw/vY8EqhambkOqy5iFr0908A=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521184019-c5ad59b459ec/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
@@ -14,8 +14,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
@@ -26,20 +26,19 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I=
|
||||
github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/proto v1.14.3 h1:zEhlzNkpP8kN6utonKMzlPfIvy82t5Kb9mufaJxSe1Q=
|
||||
github.com/emicklei/proto v1.14.3/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||
@@ -50,37 +49,35 @@ github.com/go-openapi/spec v0.21.1-0.20250328170532-a3928469592e h1:auobAirzhPsL
|
||||
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/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
|
||||
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
|
||||
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/grafana/pyroscope-go v1.3.0 h1:t3Jehad8vvqN4oRAB0LdmfQ5ZSUXQw3asoft+K4GAT8=
|
||||
github.com/grafana/pyroscope-go v1.3.0/go.mod h1:XA7I3usNx+UdjOZfQnl1WV8y924vsJo9KIVrKB+9jx4=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.10 h1:dvhndEbyavTb59vFCd6PsrAG5qi69/qZZtegh/TJKSY=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.10/go.mod h1:XnWRGg2XO5uxZdiz1rfeJH6w1eZ+YICCBVXNWOfH86g=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||
@@ -91,8 +88,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
@@ -102,8 +99,10 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
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=
|
||||
@@ -122,36 +121,39 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
|
||||
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
|
||||
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
||||
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
|
||||
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
|
||||
github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
@@ -161,78 +163,81 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s=
|
||||
github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c=
|
||||
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1 h1:+dBg5k7nuTE38VVdoroRsT0Z88fmvdYrI2EjzJst35I=
|
||||
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1/go.mod h1:nmuySobZb4kFgFy6BptpXp/BBw+xFSyvVPP6auoJB4k=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
github.com/zeromicro/antlr v0.0.1 h1:CQpIn/dc0pUjgGQ81y98s/NGOm2Hfru2NNio2I9mQgk=
|
||||
github.com/zeromicro/antlr v0.0.1/go.mod h1:nfpjEwFR6Q4xGDJMcZnCL9tEfQRgszMwu3rDz2Z+p5M=
|
||||
github.com/zeromicro/ddl-parser v1.0.5 h1:LaVqHdzMTjasua1yYpIYaksxKqRzFrEukj2Wi2EbWaQ=
|
||||
github.com/zeromicro/ddl-parser v1.0.5/go.mod h1:ISU/8NuPyEpl9pa17Py9TBPetMjtsiHrb9f5XGiYbo8=
|
||||
github.com/zeromicro/go-zero v1.9.3 h1:dJ568uUoRJY0RUxo4aH4htSglbEUF60WiM1MZVkTK9A=
|
||||
github.com/zeromicro/go-zero v1.9.3/go.mod h1:JBAtfXQvErk+V7pxzcySR0mW6m2I4KPhNQZGASltDRQ=
|
||||
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
|
||||
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU=
|
||||
go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
|
||||
go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
github.com/zeromicro/go-zero v1.10.2 h1:XVxs4tGi4dkNE08iZP0BoqlCuof4iAnCdZ424mz8yyM=
|
||||
github.com/zeromicro/go-zero v1.10.2/go.mod h1:Qn1kdpoQfj9DzTtYUlv5pXIFAij6gNAwmkZ+w2ldr2Q=
|
||||
go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
|
||||
go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.40.0 h1:zu+I4j+FdO6xIxBVPeuncQVbjxUM4LiMgv6GwGe9REE=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.40.0/go.mod h1:zS6cC4nFBYXbu18e7aLfMzubBjOiN7ZcROu477qtMf8=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
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/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -241,76 +246,83 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
|
||||
k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
|
||||
k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q=
|
||||
k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y=
|
||||
k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg=
|
||||
k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0=
|
||||
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
|
||||
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
|
||||
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
|
||||
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
|
||||
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
|
||||
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM=
|
||||
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// BuildVersion is the version of goctl.
|
||||
const BuildVersion = "1.9.2"
|
||||
const BuildVersion = "1.10.1"
|
||||
|
||||
var tag = map[string]int{"pre-alpha": 0, "alpha": 1, "pre-beta": 2, "beta": 3, "released": 4, "": 5}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ func genFindOne(table Table, withCache, postgreSql bool) (string, string, error)
|
||||
|
||||
output, err := util.With("findOne").
|
||||
Parse(text).
|
||||
AddFunc("hasField", hasField(table)).
|
||||
Execute(map[string]any{
|
||||
"withCache": withCache,
|
||||
"upperStartCamelObject": camel,
|
||||
|
||||
@@ -22,7 +22,7 @@ func genFindOneByField(table Table, withCache, postgreSql bool) (*findOneCode, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := util.With("findOneByField").Parse(text)
|
||||
t := util.With("findOneByField").Parse(text).AddFunc("hasField", hasField(table))
|
||||
var list []string
|
||||
camelTableName := table.Name.ToCamel()
|
||||
for _, key := range table.UniqueCacheKey {
|
||||
@@ -54,7 +54,7 @@ func genFindOneByField(table Table, withCache, postgreSql bool) (*findOneCode, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t = util.With("findOneByFieldMethod").Parse(text)
|
||||
t = util.With("findOneByFieldMethod").Parse(text).AddFunc("hasField", hasField(table))
|
||||
var listMethod []string
|
||||
for _, key := range table.UniqueCacheKey {
|
||||
var inJoin, paramJoin Join
|
||||
@@ -88,7 +88,7 @@ func genFindOneByField(table Table, withCache, postgreSql bool) (*findOneCode, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out, err := util.With("findOneByFieldExtraMethod").Parse(text).Execute(map[string]any{
|
||||
out, err := util.With("findOneByFieldExtraMethod").AddFunc("hasField", hasField(table)).Parse(text).Execute(map[string]any{
|
||||
"upperStartCamelObject": camelTableName,
|
||||
"primaryKeyLeft": table.PrimaryCacheKey.VarLeft,
|
||||
"lowerStartCamelObject": stringx.From(camelTableName).Untitle(),
|
||||
|
||||
@@ -360,6 +360,7 @@ func (g *defaultGenerator) genModelCustom(in parser.Table, withCache bool) (stri
|
||||
|
||||
t := util.With("model-custom").
|
||||
Parse(text).
|
||||
AddFunc("hasField", hasField(Table{Table: in})).
|
||||
GoFmt(true)
|
||||
output, err := t.Execute(map[string]any{
|
||||
"pkg": g.pkg,
|
||||
@@ -381,6 +382,7 @@ func (g *defaultGenerator) executeModel(table Table, code *code) (*bytes.Buffer,
|
||||
}
|
||||
t := util.With("model").
|
||||
Parse(text).
|
||||
AddFunc("hasField", hasField(table)).
|
||||
GoFmt(true)
|
||||
output, err := t.Execute(map[string]any{
|
||||
"pkg": g.pkg,
|
||||
|
||||
@@ -28,7 +28,7 @@ func genImports(table Table, withCache, timeImport bool) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buffer, err := util.With("import").Parse(text).Execute(map[string]any{
|
||||
buffer, err := util.With("import").Parse(text).AddFunc("hasField", hasField(table)).Execute(map[string]any{
|
||||
"time": timeImport,
|
||||
"containsPQ": table.ContainsPQ,
|
||||
"data": table,
|
||||
|
||||
@@ -94,3 +94,16 @@ func Update() error {
|
||||
|
||||
return pathx.InitTemplates(category, templates)
|
||||
}
|
||||
|
||||
// hasField returns a function that checks if a field exists in the table.
|
||||
// It uses a pre-built map for O(1) lookup performance.
|
||||
func hasField(table Table) func(string) bool {
|
||||
fieldSet := make(map[string]struct{}, len(table.Fields))
|
||||
for _, field := range table.Fields {
|
||||
fieldSet[field.NameOriginal] = struct{}{}
|
||||
}
|
||||
return func(f string) bool {
|
||||
_, ok := fieldSet[f]
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/model/sql/parser"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/model/sql/template"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
|
||||
"github.com/zeromicro/go-zero/tools/goctl/util/stringx"
|
||||
)
|
||||
|
||||
func TestGenTemplates(t *testing.T) {
|
||||
@@ -91,3 +93,151 @@ func TestUpdate(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, template.New, string(data))
|
||||
}
|
||||
|
||||
func TestHasField(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
table Table
|
||||
fieldName string
|
||||
wantResult bool
|
||||
}{
|
||||
{
|
||||
name: "field exists",
|
||||
table: Table{
|
||||
Table: parser.Table{
|
||||
Fields: []*parser.Field{
|
||||
{NameOriginal: "id"},
|
||||
{NameOriginal: "name"},
|
||||
{NameOriginal: "created_at"},
|
||||
},
|
||||
},
|
||||
},
|
||||
fieldName: "name",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "field does not exist",
|
||||
table: Table{
|
||||
Table: parser.Table{
|
||||
Fields: []*parser.Field{
|
||||
{NameOriginal: "id"},
|
||||
{NameOriginal: "name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
fieldName: "email",
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "empty table",
|
||||
table: Table{
|
||||
Table: parser.Table{
|
||||
Fields: []*parser.Field{},
|
||||
},
|
||||
},
|
||||
fieldName: "id",
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "case sensitive match",
|
||||
table: Table{
|
||||
Table: parser.Table{
|
||||
Fields: []*parser.Field{
|
||||
{NameOriginal: "ID"},
|
||||
{NameOriginal: "Name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
fieldName: "id",
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "exact match required",
|
||||
table: Table{
|
||||
Table: parser.Table{
|
||||
Fields: []*parser.Field{
|
||||
{NameOriginal: "user_name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
fieldName: "user_name",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "partial match should fail",
|
||||
table: Table{
|
||||
Table: parser.Table{
|
||||
Fields: []*parser.Field{
|
||||
{NameOriginal: "user_name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
fieldName: "user",
|
||||
wantResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fn := hasField(tt.table)
|
||||
result := fn(tt.fieldName)
|
||||
assert.Equal(t, tt.wantResult, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasFieldWithRealTable(t *testing.T) {
|
||||
// Create a realistic table structure
|
||||
table := Table{
|
||||
Table: parser.Table{
|
||||
Name: stringx.From("users"),
|
||||
Fields: []*parser.Field{
|
||||
{NameOriginal: "id", DataType: "int64"},
|
||||
{NameOriginal: "username", DataType: "string"},
|
||||
{NameOriginal: "email", DataType: "string"},
|
||||
{NameOriginal: "password", DataType: "string"},
|
||||
{NameOriginal: "created_at", DataType: "time.Time"},
|
||||
{NameOriginal: "updated_at", DataType: "time.Time"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fn := hasField(table)
|
||||
|
||||
// Test all existing fields
|
||||
assert.True(t, fn("id"))
|
||||
assert.True(t, fn("username"))
|
||||
assert.True(t, fn("email"))
|
||||
assert.True(t, fn("password"))
|
||||
assert.True(t, fn("created_at"))
|
||||
assert.True(t, fn("updated_at"))
|
||||
|
||||
// Test non-existing fields
|
||||
assert.False(t, fn("deleted_at"))
|
||||
assert.False(t, fn("ID"))
|
||||
assert.False(t, fn("Username"))
|
||||
assert.False(t, fn(""))
|
||||
}
|
||||
|
||||
func TestHasFieldPerformance(t *testing.T) {
|
||||
// Create a table with many fields to test performance optimization
|
||||
var fields []*parser.Field
|
||||
for i := 0; i < 1000; i++ {
|
||||
fields = append(fields, &parser.Field{
|
||||
NameOriginal: "field_" + string(rune('0'+i%10)) + string(rune('a'+i%26)),
|
||||
})
|
||||
}
|
||||
|
||||
table := Table{
|
||||
Table: parser.Table{
|
||||
Fields: fields,
|
||||
},
|
||||
}
|
||||
|
||||
fn := hasField(table)
|
||||
|
||||
// Verify the function works correctly
|
||||
assert.True(t, fn(fields[0].NameOriginal))
|
||||
assert.True(t, fn(fields[999].NameOriginal))
|
||||
assert.False(t, fn("non_existent_field"))
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func genUpdate(table Table, withCache, postgreSql bool) (
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
output, err := util.With("update").Parse(text).Execute(
|
||||
output, err := util.With("update").Parse(text).AddFunc("hasField", hasField(table)).Execute(
|
||||
map[string]any{
|
||||
"withCache": withCache,
|
||||
"containsIndexCache": table.ContainsUniqueCacheKey,
|
||||
@@ -94,7 +94,7 @@ func genUpdate(table Table, withCache, postgreSql bool) (
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
updateMethodOutput, err := util.With("updateMethod").Parse(text).Execute(
|
||||
updateMethodOutput, err := util.With("updateMethod").Parse(text).AddFunc("hasField", hasField(table)).Execute(
|
||||
map[string]any{
|
||||
"upperStartCamelObject": camelTableName,
|
||||
"data": table,
|
||||
|
||||
@@ -197,7 +197,7 @@ func (p *printer) print(x reflect.Value) {
|
||||
|
||||
case reflect.Struct:
|
||||
if val, ok := x.Interface().(apitoken.Position); ok {
|
||||
p.printf(val.String())
|
||||
p.printf("%s", val.String())
|
||||
return
|
||||
}
|
||||
t := x.Type()
|
||||
|
||||
@@ -357,7 +357,7 @@ func (w *Writer) Write(opts ...Option) {
|
||||
|
||||
// WriteText writes the text.
|
||||
func (w *Writer) WriteText(text string) {
|
||||
_, _ = fmt.Fprintf(w.tw, text)
|
||||
_, _ = fmt.Fprint(w.tw, text)
|
||||
}
|
||||
|
||||
func (w *Writer) write(opt *option) {
|
||||
|
||||
@@ -284,7 +284,7 @@ func (api *API) parseImportedAPI(imports []ast.ImportStmt) ([]*API, error) {
|
||||
}
|
||||
// import cycle check
|
||||
if err := api.importManager.Push(impPath); err != nil {
|
||||
return nil, ast.SyntaxError(tok.Position, err.Error())
|
||||
return nil, ast.SyntaxError(tok.Position, "%s", err.Error())
|
||||
}
|
||||
|
||||
if _, ok := api.importSet[impPath]; ok {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -24,5 +24,5 @@ func (e *errorManager) error() error {
|
||||
if len(e.errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(strings.Join(e.errors, "\n"))
|
||||
return errors.New(strings.Join(e.errors, "\n"))
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user