Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
76a0bdd67c | |||
8de525a8f8 | |||
ef36082f2e | |||
21c897be47 | |||
0b21dd6660 | |||
18eb0d9e5c |
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: For reporting bugs in micro
|
about: For reporting bugs in go-micro
|
||||||
title: "[BUG]"
|
title: "[BUG]"
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
@@ -16,3 +16,9 @@ assignees: ''
|
|||||||
**How to reproduce the bug:**
|
**How to reproduce the bug:**
|
||||||
|
|
||||||
If possible, please include a minimal code snippet here.
|
If possible, please include a minimal code snippet here.
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
Go Version: please paste `go version` output here
|
||||||
|
```
|
||||||
|
please paste `go env` output here
|
||||||
|
```
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Feature request / Enhancement
|
name: Feature request / Enhancement
|
||||||
about: If you have a need not served by micro
|
about: If you have a need not served by go-micro
|
||||||
title: "[FEATURE]"
|
title: "[FEATURE]"
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
8
.github/ISSUE_TEMPLATE/question.md
vendored
8
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,8 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: Question
|
name: Question
|
||||||
about: Ask a question about micro
|
about: Ask a question about go-micro
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
Before asking, please check if your question has already been answered:
|
||||||
|
|
||||||
|
1. Check the documentation - https://micro.mu/docs/
|
||||||
|
2. Check the examples and plugins - https://github.com/micro/examples & https://github.com/micro/go-plugins
|
||||||
|
3. Search existing issues
|
||||||
|
28
.github/autoapprove.yml
vendored
28
.github/autoapprove.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
name: "autoapprove"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [assigned, opened, synchronize, reopened]
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["prbuild"]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
autoapprove:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: approve
|
|
||||||
run: [ "curl -o tea https://dl.gitea.com/tea/main/tea-main-linux-amd64",
|
|
||||||
"chmod +x ./tea",
|
|
||||||
"./tea login add --name unistack --token ${{ secrets.GITHUB_TOKEN }} --url https://git.unistack.org",
|
|
||||||
"./tea pr --repo ${{ github.event.repository.name }}"
|
|
||||||
]
|
|
||||||
if: github.actor == 'vtolstov'
|
|
||||||
id: approve
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
19
.github/dependabot.yml
vendored
Normal file
19
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
|
||||||
|
# Maintain dependencies for Golang
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
20
.github/workflows/autoapprove.yml
vendored
Normal file
20
.github/workflows/autoapprove.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: "autoapprove"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [assigned, opened, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
autoapprove:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: approve
|
||||||
|
uses: hmarr/auto-approve-action@v3
|
||||||
|
if: github.actor == 'vtolstov' || github.actor == 'dependabot[bot]'
|
||||||
|
id: approve
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
21
.github/workflows/automerge.yml
vendored
Normal file
21
.github/workflows/automerge.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: "automerge"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [assigned, opened, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
automerge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.actor == 'vtolstov'
|
||||||
|
steps:
|
||||||
|
- name: merge
|
||||||
|
id: merge
|
||||||
|
run: gh pr merge --auto --merge "$PR_URL"
|
||||||
|
env:
|
||||||
|
PR_URL: ${{github.event.pull_request.html_url}}
|
||||||
|
GITHUB_TOKEN: ${{secrets.TOKEN}}
|
47
.github/workflows/build.yml
vendored
Normal file
47
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- v3
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: setup
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.17
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: ${{ runner.os }}-go-
|
||||||
|
- name: deps
|
||||||
|
run: go get -v -t -d ./...
|
||||||
|
- name: test
|
||||||
|
env:
|
||||||
|
INTEGRATION_TESTS: yes
|
||||||
|
run: go test -mod readonly -v ./...
|
||||||
|
lint:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: lint
|
||||||
|
uses: golangci/golangci-lint-action@v3.4.0
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||||
|
version: v1.30
|
||||||
|
# Optional: working directory, useful for monorepos
|
||||||
|
# working-directory: somedir
|
||||||
|
# Optional: golangci-lint command line arguments.
|
||||||
|
# args: --issues-exit-code=0
|
||||||
|
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||||
|
# only-new-issues: true
|
78
.github/workflows/codeql-analysis.yml
vendored
Normal file
78
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "codeql"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["prbuild"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
push:
|
||||||
|
branches: [ master, v3 ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ master, v3 ]
|
||||||
|
schedule:
|
||||||
|
- cron: '34 1 * * 0'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'go' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
|
# Learn more:
|
||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: setup
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.17
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: init
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: analyze
|
||||||
|
uses: github/codeql-action/analyze@v2
|
27
.github/workflows/dependabot-automerge.yml
vendored
Normal file
27
.github/workflows/dependabot-automerge.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: "dependabot-automerge"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [assigned, opened, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
automerge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.actor == 'dependabot[bot]'
|
||||||
|
steps:
|
||||||
|
- name: metadata
|
||||||
|
id: metadata
|
||||||
|
uses: dependabot/fetch-metadata@v1.3.6
|
||||||
|
with:
|
||||||
|
github-token: "${{ secrets.TOKEN }}"
|
||||||
|
- name: merge
|
||||||
|
id: merge
|
||||||
|
if: ${{contains(steps.metadata.outputs.dependency-names, 'go.unistack.org')}}
|
||||||
|
run: gh pr merge --auto --merge "$PR_URL"
|
||||||
|
env:
|
||||||
|
PR_URL: ${{github.event.pull_request.html_url}}
|
||||||
|
GITHUB_TOKEN: ${{secrets.TOKEN}}
|
53
.github/workflows/job_coverage.yml
vendored
53
.github/workflows/job_coverage.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: coverage
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main, v3, v4 ]
|
|
||||||
paths-ignore:
|
|
||||||
- '.github/**'
|
|
||||||
- '.gitea/**'
|
|
||||||
pull_request:
|
|
||||||
branches: [ main, v3, v4 ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
build:
|
|
||||||
if: github.server_url != 'https://github.com'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
filter: 'blob:none'
|
|
||||||
|
|
||||||
- name: setup go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
cache-dependency-path: "**/*.sum"
|
|
||||||
go-version: 'stable'
|
|
||||||
|
|
||||||
- name: test coverage
|
|
||||||
run: |
|
|
||||||
go test -v -cover ./... -covermode=count -coverprofile coverage.out -coverpkg ./...
|
|
||||||
go tool cover -func coverage.out -o coverage.out
|
|
||||||
|
|
||||||
- name: coverage badge
|
|
||||||
uses: tj-actions/coverage-badge-go@v2
|
|
||||||
with:
|
|
||||||
green: 80
|
|
||||||
filename: coverage.out
|
|
||||||
|
|
||||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
|
||||||
name: autocommit
|
|
||||||
with:
|
|
||||||
commit_message: Apply Code Coverage Badge
|
|
||||||
skip_fetch: false
|
|
||||||
skip_checkout: false
|
|
||||||
file_pattern: ./README.md
|
|
||||||
|
|
||||||
- name: push
|
|
||||||
if: steps.auto-commit-action.outputs.changes_detected == 'true'
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
github_token: ${{ github.token }}
|
|
||||||
branch: ${{ github.ref }}
|
|
29
.github/workflows/job_lint.yml
vendored
29
.github/workflows/job_lint.yml
vendored
@@ -1,29 +0,0 @@
|
|||||||
name: lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, synchronize]
|
|
||||||
branches: [ master, v3, v4 ]
|
|
||||||
paths-ignore:
|
|
||||||
- '.github/**'
|
|
||||||
- '.gitea/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
filter: 'blob:none'
|
|
||||||
- name: setup go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
cache-dependency-path: "**/*.sum"
|
|
||||||
go-version: 'stable'
|
|
||||||
- name: setup deps
|
|
||||||
run: go get -v ./...
|
|
||||||
- name: run lint
|
|
||||||
uses: golangci/golangci-lint-action@v6
|
|
||||||
with:
|
|
||||||
version: 'latest'
|
|
94
.github/workflows/job_sync.yml
vendored
94
.github/workflows/job_sync.yml
vendored
@@ -1,94 +0,0 @@
|
|||||||
name: sync
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '*/5 * * * *'
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
if: github.server_url != 'https://github.com'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: init
|
|
||||||
run: |
|
|
||||||
git config --global user.email "vtolstov <vtolstov@users.noreply.github.com>"
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
echo "machine git.unistack.org login vtolstov password ${{ secrets.TOKEN_GITEA }}" >> /root/.netrc
|
|
||||||
echo "machine github.com login vtolstov password ${{ secrets.TOKEN_GITHUB }}" >> /root/.netrc
|
|
||||||
|
|
||||||
- name: check master
|
|
||||||
id: check_master
|
|
||||||
run: |
|
|
||||||
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/master | cut -f1)
|
|
||||||
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/master | cut -f1)
|
|
||||||
echo "src_hash=$src_hash"
|
|
||||||
echo "dst_hash=$dst_hash"
|
|
||||||
if [ "$src_hash" != "$dst_hash" ]; then
|
|
||||||
echo "sync_needed=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "sync_needed=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: sync master
|
|
||||||
if: steps.check_master.outputs.sync_needed == 'true'
|
|
||||||
run: |
|
|
||||||
git clone --filter=blob:none --filter=tree:0 --branch master --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
|
||||||
cd repo
|
|
||||||
git remote add --no-tags --fetch --track master upstream https://github.com/${GITHUB_REPOSITORY}
|
|
||||||
git pull --rebase upstream master
|
|
||||||
git push upstream master --progress
|
|
||||||
git push origin master --progress
|
|
||||||
cd ../
|
|
||||||
rm -rf repo
|
|
||||||
|
|
||||||
- name: check v3
|
|
||||||
id: check_v3
|
|
||||||
run: |
|
|
||||||
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/v3 | cut -f1)
|
|
||||||
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/v3 | cut -f1)
|
|
||||||
echo "src_hash=$src_hash"
|
|
||||||
echo "dst_hash=$dst_hash"
|
|
||||||
if [ "$src_hash" != "$dst_hash" ]; then
|
|
||||||
echo "sync_needed=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "sync_needed=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: sync v3
|
|
||||||
if: steps.check_v3.outputs.sync_needed == 'true'
|
|
||||||
run: |
|
|
||||||
git clone --filter=blob:none --filter=tree:0 --branch v3 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
|
||||||
cd repo
|
|
||||||
git remote add --no-tags --fetch --track v3 upstream https://github.com/${GITHUB_REPOSITORY}
|
|
||||||
git pull --rebase upstream v3
|
|
||||||
git push upstream v3 --progress
|
|
||||||
git push origin v3 --progress
|
|
||||||
cd ../
|
|
||||||
rm -rf repo
|
|
||||||
|
|
||||||
- name: check v4
|
|
||||||
id: check_v4
|
|
||||||
run: |
|
|
||||||
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/v4 | cut -f1)
|
|
||||||
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/v4 | cut -f1)
|
|
||||||
echo "src_hash=$src_hash"
|
|
||||||
echo "dst_hash=$dst_hash"
|
|
||||||
if [ "$src_hash" != "$dst_hash" ]; then
|
|
||||||
echo "sync_needed=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "sync_needed=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: sync v4
|
|
||||||
if: steps.check_v4.outputs.sync_needed == 'true'
|
|
||||||
run: |
|
|
||||||
git clone --filter=blob:none --filter=tree:0 --branch v4 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
|
|
||||||
cd repo
|
|
||||||
git remote add --no-tags --fetch --track v4 upstream https://github.com/${GITHUB_REPOSITORY}
|
|
||||||
git pull --rebase upstream v4
|
|
||||||
git push upstream v4 --progress
|
|
||||||
git push origin v4 --progress
|
|
||||||
cd ../
|
|
||||||
rm -rf repo
|
|
31
.github/workflows/job_test.yml
vendored
31
.github/workflows/job_test.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
name: test
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, synchronize]
|
|
||||||
branches: [ master, v3, v4 ]
|
|
||||||
push:
|
|
||||||
branches: [ master, v3, v4 ]
|
|
||||||
paths-ignore:
|
|
||||||
- '.github/**'
|
|
||||||
- '.gitea/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
filter: 'blob:none'
|
|
||||||
- name: setup go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
cache-dependency-path: "**/*.sum"
|
|
||||||
go-version: 'stable'
|
|
||||||
- name: setup deps
|
|
||||||
run: go get -v ./...
|
|
||||||
- name: run test
|
|
||||||
env:
|
|
||||||
INTEGRATION_TESTS: yes
|
|
||||||
run: go test -mod readonly -v ./...
|
|
50
.github/workflows/job_tests.yml
vendored
50
.github/workflows/job_tests.yml
vendored
@@ -1,50 +0,0 @@
|
|||||||
name: test
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, synchronize]
|
|
||||||
branches: [ master, v3, v4 ]
|
|
||||||
push:
|
|
||||||
branches: [ master, v3, v4 ]
|
|
||||||
paths-ignore:
|
|
||||||
- '.github/**'
|
|
||||||
- '.gitea/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
filter: 'blob:none'
|
|
||||||
- name: checkout tests
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: master
|
|
||||||
filter: 'blob:none'
|
|
||||||
repository: unistack-org/micro-tests
|
|
||||||
path: micro-tests
|
|
||||||
- name: setup go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
cache-dependency-path: "**/*.sum"
|
|
||||||
go-version: 'stable'
|
|
||||||
- name: setup go work
|
|
||||||
env:
|
|
||||||
GOWORK: ${{ github.workspace }}/go.work
|
|
||||||
run: |
|
|
||||||
go work init
|
|
||||||
go work use .
|
|
||||||
go work use micro-tests
|
|
||||||
- name: setup deps
|
|
||||||
env:
|
|
||||||
GOWORK: ${{ github.workspace }}/go.work
|
|
||||||
run: go get -v ./...
|
|
||||||
- name: run tests
|
|
||||||
env:
|
|
||||||
INTEGRATION_TESTS: yes
|
|
||||||
GOWORK: ${{ github.workspace }}/go.work
|
|
||||||
run: |
|
|
||||||
cd micro-tests
|
|
||||||
go test -mod readonly -v ./... || true
|
|
47
.github/workflows/pr.yml
vendored
Normal file
47
.github/workflows/pr.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: prbuild
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- v3
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: setup
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.17
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: ${{ runner.os }}-go-
|
||||||
|
- name: deps
|
||||||
|
run: go get -v -t -d ./...
|
||||||
|
- name: test
|
||||||
|
env:
|
||||||
|
INTEGRATION_TESTS: yes
|
||||||
|
run: go test -mod readonly -v ./...
|
||||||
|
lint:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: lint
|
||||||
|
uses: golangci/golangci-lint-action@v3.4.0
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||||
|
version: v1.30
|
||||||
|
# Optional: working directory, useful for monorepos
|
||||||
|
# working-directory: somedir
|
||||||
|
# Optional: golangci-lint command line arguments.
|
||||||
|
# args: --issues-exit-code=0
|
||||||
|
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||||
|
# only-new-issues: true
|
39
.gitignore
vendored
39
.gitignore
vendored
@@ -1,39 +0,0 @@
|
|||||||
# Develop tools
|
|
||||||
/.vscode/
|
|
||||||
/.idea/
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
# Binaries for programs and plugins
|
|
||||||
*.exe
|
|
||||||
*.exe~
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
_build
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
# Test binary, build with `go test -c`
|
|
||||||
*.test
|
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
|
||||||
*.out
|
|
||||||
|
|
||||||
# vim temp files
|
|
||||||
*~
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
@@ -1,5 +1,44 @@
|
|||||||
run:
|
run:
|
||||||
concurrency: 8
|
concurrency: 4
|
||||||
timeout: 5m
|
deadline: 5m
|
||||||
issues-exit-code: 1
|
issues-exit-code: 1
|
||||||
tests: true
|
tests: true
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
govet:
|
||||||
|
check-shadowing: true
|
||||||
|
enable:
|
||||||
|
- fieldalignment
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- govet
|
||||||
|
- deadcode
|
||||||
|
- errcheck
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
|
- typecheck
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
|
- bodyclose
|
||||||
|
- gci
|
||||||
|
- goconst
|
||||||
|
- gocritic
|
||||||
|
- gosimple
|
||||||
|
- gofmt
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
- golint
|
||||||
|
- gosec
|
||||||
|
- makezero
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nilerr
|
||||||
|
- noctx
|
||||||
|
- prealloc
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
disable-all: false
|
||||||
|
150
README.md
150
README.md
@@ -1,26 +1,18 @@
|
|||||||
# HTTP Client
|
# HTTP Client
|
||||||

|
|
||||||
|
|
||||||
This plugin is an HTTP client for [Micro](https://pkg.go.dev/go.unistack.org/micro/v4).
|
This plugin is a http client for micro.
|
||||||
It implements the [micro.Client](https://pkg.go.dev/go.unistack.org/micro/v4/client#Client) interface.
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The HTTP client wraps `net/http` to provide a robust client with service discovery, load balancing and
|
The http client wraps `net/http` to provide a robust micro client with service discovery, load balancing and streaming.
|
||||||
implements HTTP rules defined in the [google/api/http.proto](https://github.com/googleapis/googleapis/blob/master/google/api/http.proto) specification.
|
It complies with the [micro.Client](https://godoc.org/go.unistack.org/micro-client-http/v4#Client) interface.
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
* Streaming is not yet implemented.
|
|
||||||
* Only protobuf-generated messages are supported.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Use directly
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
import "go.unistack.org/micro-client-http/v4"
|
||||||
"go.unistack.org/micro/v4"
|
|
||||||
http "go.unistack.org/micro-client-http/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
service := micro.NewService(
|
service := micro.NewService(
|
||||||
micro.Name("my.service"),
|
micro.Name("my.service"),
|
||||||
@@ -28,127 +20,43 @@ service := micro.NewService(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Simple call
|
### Call Service
|
||||||
|
|
||||||
|
Assuming you have a http service "my.service" with path "/foo/bar"
|
||||||
```go
|
```go
|
||||||
import (
|
// new client
|
||||||
"go.unistack.org/micro/v4/client"
|
client := http.NewClient()
|
||||||
http "go.unistack.org/micro-client-http/v4"
|
|
||||||
jsoncodec "go.unistack.org/micro-codec-json/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
c := http.NewClient(
|
// create request/response
|
||||||
client.Codec("application/json", jsoncodec.NewCodec()),
|
request := client.NewRequest("my.service", "/foo/bar", protoRequest{})
|
||||||
)
|
response := new(protoResponse)
|
||||||
|
|
||||||
req := c.NewRequest(
|
// call service
|
||||||
"user-service",
|
err := client.Call(context.TODO(), request, response)
|
||||||
"/user/{user_id}/order/{order_id}",
|
|
||||||
&protoReq{UserId: "123", OrderId: 456},
|
|
||||||
)
|
|
||||||
rsp := new(protoRsp)
|
|
||||||
|
|
||||||
err := c.Call(
|
|
||||||
ctx,
|
|
||||||
req,
|
|
||||||
rsp,
|
|
||||||
client.WithAddress("example.com"),
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Call with specific options
|
or you can call any rest api or site and unmarshal to response struct
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
// new client
|
||||||
"go.unistack.org/micro/v4/client"
|
client := client.NewClientCallOptions(http.NewClient(), http.Address("https://api.github.com"))
|
||||||
http "go.unistack.org/micro-client-http/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
err := c.Call(
|
req := client.NewRequest("github", "/users/vtolstov", nil)
|
||||||
ctx,
|
rsp := make(map[string]interface{})
|
||||||
req,
|
|
||||||
rsp,
|
err := c.Call(context.TODO(), req, &rsp, mhttp.Method(http.MethodGet))
|
||||||
client.WithAddress("example.com"),
|
|
||||||
http.Method("POST"),
|
|
||||||
http.Path("/user/{user_id}/order/{order_id}"),
|
|
||||||
http.Body("*"), // <- use all fields from the proto request as HTTP request body or specify a single field name to use only that field (see Google API HTTP spec: google/api/http.proto)
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Call with request headers
|
Look at http_test.go for detailed use.
|
||||||
|
|
||||||
|
### Encoding
|
||||||
|
|
||||||
|
Default protobuf with content-type application/proto
|
||||||
```go
|
```go
|
||||||
import (
|
client.NewRequest("service", "/path", protoRequest{})
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
http "go.unistack.org/micro-client-http/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx := metadata.NewOutgoingContext(ctx, metadata.Pairs(
|
|
||||||
"Authorization", "Bearer token",
|
|
||||||
"My-Header", "My-Header-Value",
|
|
||||||
))
|
|
||||||
|
|
||||||
err := c.Call(
|
|
||||||
ctx,
|
|
||||||
req,
|
|
||||||
rsp,
|
|
||||||
http.Header("Authorization", "true", "My-Header", "false"), // <- call option that declares required/optional headers
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Call with response headers
|
Json with content-type application/json
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
client.NewJsonRequest("service", "/path", jsonRequest{})
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
http "go.unistack.org/micro-client-http/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
respMetadata := metadata.Metadata{}
|
|
||||||
|
|
||||||
err := c.Call(
|
|
||||||
ctx,
|
|
||||||
req,
|
|
||||||
rsp,
|
|
||||||
client.WithResponseMetadata(&respMetadata), // <- metadata with response headers
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Call with cookies
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
http "go.unistack.org/micro-client-http/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs(
|
|
||||||
"Cookie", "session_id=abc123; theme=dark",
|
|
||||||
))
|
|
||||||
|
|
||||||
err := c.Call(
|
|
||||||
ctx,
|
|
||||||
req,
|
|
||||||
rsp,
|
|
||||||
http.Cookie("session_id", "true", "theme", "false"), // <- call option that declares required/optional cookies
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Call with error mapping
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
http "go.unistack.org/micro-client-http/v4"
|
|
||||||
jsoncodec "go.unistack.org/micro-codec-json/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
err := c.Call(
|
|
||||||
ctx,
|
|
||||||
req,
|
|
||||||
rsp,
|
|
||||||
http.ErrorMap(map[string]any{
|
|
||||||
"default": &protoDefaultError{}, // <- default case
|
|
||||||
"403": &protoSpecialError{}, // <- key is the HTTP status code that is mapped to this error
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
116
builder/body.go
116
builder/body.go
@@ -1,116 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
func buildSingleFieldBody(msg proto.Message, fieldName string) (proto.Message, error) {
|
|
||||||
msgReflect := msg.ProtoReflect()
|
|
||||||
|
|
||||||
fd, found := findFieldByName(msgReflect, fieldName)
|
|
||||||
if !found || fd == nil {
|
|
||||||
return nil, fmt.Errorf("field %s not found", fieldName)
|
|
||||||
}
|
|
||||||
if !msgReflect.Has(fd) {
|
|
||||||
return nil, fmt.Errorf("field %s is not set", fieldName)
|
|
||||||
}
|
|
||||||
|
|
||||||
val := msgReflect.Get(fd)
|
|
||||||
|
|
||||||
if fd.Kind() == protoreflect.MessageKind {
|
|
||||||
return val.Message().Interface(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
newMsg := proto.Clone(msg)
|
|
||||||
newMsgReflect := newMsg.ProtoReflect()
|
|
||||||
newMsgReflect.Range(func(f protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
|
|
||||||
if f != fd {
|
|
||||||
newMsgReflect.Clear(f)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
return newMsg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildFullBody(msg proto.Message, usedFieldsPath *usedFields) (proto.Message, error) {
|
|
||||||
var (
|
|
||||||
msgReflect = msg.ProtoReflect()
|
|
||||||
newMsg = msgReflect.New().Interface()
|
|
||||||
newMsgReflect = newMsg.ProtoReflect()
|
|
||||||
)
|
|
||||||
|
|
||||||
fields := msgReflect.Descriptor().Fields()
|
|
||||||
for i := 0; i < fields.Len(); i++ {
|
|
||||||
fd := fields.Get(i)
|
|
||||||
fieldName := fd.JSONName()
|
|
||||||
|
|
||||||
if usedFieldsPath.hasTopLevelKey(fieldName) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val := msgReflect.Get(fd)
|
|
||||||
if !val.IsValid() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: order of the cases is important!
|
|
||||||
switch {
|
|
||||||
case fd.IsList():
|
|
||||||
list := val.List()
|
|
||||||
newList := newMsgReflect.Mutable(fd).List()
|
|
||||||
|
|
||||||
if fd.Kind() == protoreflect.MessageKind {
|
|
||||||
for j := 0; j < list.Len(); j++ {
|
|
||||||
elem, err := buildFullBody(list.Get(j).Message().Interface(), usedFieldsPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("recursive build full body: %w", err)
|
|
||||||
}
|
|
||||||
newList.Append(protoreflect.ValueOfMessage(elem.ProtoReflect()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for j := 0; j < list.Len(); j++ {
|
|
||||||
newList.Append(list.Get(j))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case fd.IsMap():
|
|
||||||
var (
|
|
||||||
m = val.Map()
|
|
||||||
newMap = newMsgReflect.Mutable(fd).Map()
|
|
||||||
rangeErr error
|
|
||||||
)
|
|
||||||
m.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
|
|
||||||
if fd.MapValue().Kind() == protoreflect.MessageKind {
|
|
||||||
elem, err := buildFullBody(v.Message().Interface(), usedFieldsPath)
|
|
||||||
if err != nil {
|
|
||||||
rangeErr = fmt.Errorf("recursive build full body: %w", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
newMap.Set(k, protoreflect.ValueOfMessage(elem.ProtoReflect()))
|
|
||||||
} else {
|
|
||||||
newMap.Set(k, v)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
if rangeErr != nil {
|
|
||||||
return nil, fmt.Errorf("map range error: %w", rangeErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
case fd.Kind() == protoreflect.MessageKind:
|
|
||||||
elem, err := buildFullBody(val.Message().Interface(), usedFieldsPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("recursive build full body: %w", err)
|
|
||||||
}
|
|
||||||
newMsgReflect.Set(fd, protoreflect.ValueOfMessage(elem.ProtoReflect()))
|
|
||||||
|
|
||||||
default:
|
|
||||||
newMsgReflect.Set(fd, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newMsg, nil
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
const (
|
|
||||||
singleWildcard string = "*"
|
|
||||||
doubleWildcard string = "**"
|
|
||||||
)
|
|
||||||
|
|
||||||
type bodyOption string
|
|
||||||
|
|
||||||
func (o bodyOption) String() string { return string(o) }
|
|
||||||
|
|
||||||
func (o bodyOption) isFullBody() bool {
|
|
||||||
return o.String() == singleWildcard
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o bodyOption) isWithoutBody() bool {
|
|
||||||
return o == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o bodyOption) isSingleField() bool {
|
|
||||||
return o != "" && o.String() != singleWildcard
|
|
||||||
}
|
|
@@ -1,79 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBodyOption_String(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
opt bodyOption
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{"empty", bodyOption(""), ""},
|
|
||||||
{"star", bodyOption("*"), "*"},
|
|
||||||
{"field", bodyOption("field"), "field"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
require.Equal(t, tt.want, tt.opt.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBodyOption_isFullBody(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
opt bodyOption
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"empty", bodyOption(""), false},
|
|
||||||
{"star", bodyOption("*"), true},
|
|
||||||
{"field", bodyOption("field"), false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
require.Equal(t, tt.want, tt.opt.isFullBody())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBodyOption_isWithoutBody(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
opt bodyOption
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"empty", bodyOption(""), true},
|
|
||||||
{"star", bodyOption("*"), false},
|
|
||||||
{"field", bodyOption("field"), false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
require.Equal(t, tt.want, tt.opt.isWithoutBody())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBodyOption_isSingleField(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
opt bodyOption
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"empty", bodyOption(""), false},
|
|
||||||
{"star", bodyOption("*"), false},
|
|
||||||
{"field", bodyOption("field"), true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
require.Equal(t, tt.want, tt.opt.isSingleField())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,114 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// findFieldByPath resolves a dot-separated field path in a protobuf message and returns the protoreflect value and its descriptor.
|
|
||||||
func findFieldByPath(msg protoreflect.Message, fieldPath string) (protoreflect.Value, protoreflect.FieldDescriptor, bool) {
|
|
||||||
var (
|
|
||||||
current = msg
|
|
||||||
parts = strings.Split(fieldPath, ".")
|
|
||||||
partsCount = len(parts) - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
for i, part := range parts {
|
|
||||||
fd, ok := findFieldByName(current, part)
|
|
||||||
if !ok {
|
|
||||||
return protoreflect.Value{}, nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
val := current.Get(fd)
|
|
||||||
if i == partsCount { // it's last part
|
|
||||||
return val, fd, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if fd.Kind() != protoreflect.MessageKind {
|
|
||||||
return protoreflect.Value{}, nil, false
|
|
||||||
}
|
|
||||||
current = val.Message()
|
|
||||||
}
|
|
||||||
|
|
||||||
return protoreflect.Value{}, nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// findFieldByName find a field name in a protobuf message and returns the protoreflect field descriptor.
|
|
||||||
func findFieldByName(msg protoreflect.Message, fieldName string) (protoreflect.FieldDescriptor, bool) {
|
|
||||||
fields := msg.Descriptor().Fields()
|
|
||||||
for i := 0; i < fields.Len(); i++ {
|
|
||||||
fd := fields.Get(i)
|
|
||||||
if fd.JSONName() == fieldName {
|
|
||||||
return fd, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// isZeroValue checks if protoreflect.Value is zero for the field.
|
|
||||||
func isZeroValue(val protoreflect.Value, fd protoreflect.FieldDescriptor) bool {
|
|
||||||
if fd.IsList() {
|
|
||||||
return val.List().Len() == 0
|
|
||||||
}
|
|
||||||
if fd.IsMap() {
|
|
||||||
return val.Map().Len() == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
switch fd.Kind() {
|
|
||||||
case protoreflect.StringKind:
|
|
||||||
return val.String() == ""
|
|
||||||
case protoreflect.BytesKind:
|
|
||||||
return len(val.Bytes()) == 0
|
|
||||||
case protoreflect.BoolKind:
|
|
||||||
return !val.Bool()
|
|
||||||
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
|
|
||||||
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
|
|
||||||
return val.Int() == 0
|
|
||||||
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind,
|
|
||||||
protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
|
|
||||||
return val.Uint() == 0
|
|
||||||
case protoreflect.FloatKind, protoreflect.DoubleKind:
|
|
||||||
return val.Float() == 0
|
|
||||||
case protoreflect.EnumKind:
|
|
||||||
return val.Enum() == 0
|
|
||||||
case protoreflect.MessageKind:
|
|
||||||
return !val.Message().IsValid()
|
|
||||||
default:
|
|
||||||
return !val.IsValid()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stringifyValue converts protoreflect.Value to string for path/query substitution.
|
|
||||||
func stringifyValue(val protoreflect.Value, fd protoreflect.FieldDescriptor) (string, error) {
|
|
||||||
switch fd.Kind() {
|
|
||||||
case protoreflect.StringKind:
|
|
||||||
return val.String(), nil
|
|
||||||
case protoreflect.BytesKind:
|
|
||||||
return base64.StdEncoding.EncodeToString(val.Bytes()), nil
|
|
||||||
case protoreflect.BoolKind:
|
|
||||||
if val.Bool() {
|
|
||||||
return "true", nil
|
|
||||||
}
|
|
||||||
return "false", nil
|
|
||||||
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
|
|
||||||
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
|
|
||||||
return fmt.Sprintf("%d", val.Int()), nil
|
|
||||||
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind,
|
|
||||||
protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
|
|
||||||
return fmt.Sprintf("%d", val.Uint()), nil
|
|
||||||
case protoreflect.FloatKind, protoreflect.DoubleKind:
|
|
||||||
return strconv.FormatFloat(val.Float(), 'g', -1, 64), nil
|
|
||||||
case protoreflect.EnumKind:
|
|
||||||
ed := fd.Enum().Values().ByNumber(val.Enum())
|
|
||||||
if ed != nil {
|
|
||||||
return string(ed.Name()), nil
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%d", val.Enum()), nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("unsupported field kind: %s", fd.Kind())
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,313 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// -------------------------- Path template representation -------------------------
|
|
||||||
|
|
||||||
// pathSegment is a helper interface for elements of a path.
|
|
||||||
type pathSegment interface {
|
|
||||||
isSegment() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// pathTemplate represents a parsed URL path template.
|
|
||||||
type pathTemplate struct {
|
|
||||||
// literalPrefix is the fixed part of the path before the first {var},
|
|
||||||
// e.g. "/v1/users/" for "/v1/users/{user_id}/orders:get".
|
|
||||||
// It is removed from segments, so segments contain only the remaining path literals and variables.
|
|
||||||
literalPrefix string
|
|
||||||
|
|
||||||
// segments is a sequence of pathLiteral or pathVar representing the rest of the path after literalPrefix.
|
|
||||||
segments []pathSegment
|
|
||||||
|
|
||||||
// customVerb is an optional ":verb" suffix, e.g. ":get".
|
|
||||||
customVerb string
|
|
||||||
}
|
|
||||||
|
|
||||||
// pathLiteral represents a fixed literal segment in a path template, e.g., "/v1/users/".
|
|
||||||
type pathLiteral struct {
|
|
||||||
text string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p pathLiteral) isSegment() bool { return true }
|
|
||||||
|
|
||||||
// pathVar represents a variable segment in a path template, e.g., "{user.id}".
|
|
||||||
type pathVar struct {
|
|
||||||
// fieldPath is the dotted path to the field in the struct, e.g., "user.id".
|
|
||||||
fieldPath string
|
|
||||||
|
|
||||||
// pattern is the optional pattern after '=', e.g., "*" or "**/orders".
|
|
||||||
// It specifies how the variable can match parts of the URL path.
|
|
||||||
pattern string
|
|
||||||
|
|
||||||
// multiSegment is true if the pattern can match multiple path segments
|
|
||||||
// (contains '/' or "**").
|
|
||||||
multiSegment bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p pathVar) isSegment() bool { return true }
|
|
||||||
|
|
||||||
// ----------------------------- Path template parsing -----------------------------
|
|
||||||
|
|
||||||
// parsePathTemplate parses a URL path template into a pathTemplate.
|
|
||||||
// It extracts:
|
|
||||||
// 1. literalPrefix — fixed part before the first variable,
|
|
||||||
// 2. segments — sequence of pathLiteral and pathVar,
|
|
||||||
// 3. customVerb — optional ":verb" suffix.
|
|
||||||
//
|
|
||||||
// Complexity: time O(n), memory O(n).
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
//
|
|
||||||
// input: "/v1/users/{user_id}/orders:get"
|
|
||||||
// output: pathTemplate{
|
|
||||||
// literalPrefix: "/v1/users/",
|
|
||||||
// segments: [{user_id}, "/orders"],
|
|
||||||
// customVerb: ":get",
|
|
||||||
// }
|
|
||||||
func parsePathTemplate(input string) (*pathTemplate, error) {
|
|
||||||
// Step 1: extract custom verb after the last colon, e.g. ":get"
|
|
||||||
var customVerb string
|
|
||||||
if i := strings.LastIndex(input, ":"); i >= 0 && i > strings.LastIndex(input, "/") {
|
|
||||||
customVerb = input[i:]
|
|
||||||
input = input[:i]
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
segments []pathSegment
|
|
||||||
buf strings.Builder
|
|
||||||
)
|
|
||||||
|
|
||||||
// Step 2: iterate over the input and split into segments
|
|
||||||
for i := 0; i < len(input); {
|
|
||||||
if input[i] != '{' {
|
|
||||||
buf.WriteByte(input[i])
|
|
||||||
i++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add literal before '{' if any
|
|
||||||
if buf.Len() > 0 {
|
|
||||||
segments = append(segments, pathLiteral{text: buf.String()})
|
|
||||||
buf.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find closing '}'
|
|
||||||
start := i + 1
|
|
||||||
offset := strings.IndexByte(input[start:], '}') // relative offset from start
|
|
||||||
if offset < 0 {
|
|
||||||
return nil, fmt.Errorf("unclosed '{' in path: %s", input)
|
|
||||||
}
|
|
||||||
end := start + offset
|
|
||||||
|
|
||||||
token := input[start:end]
|
|
||||||
i = end + 1 // jump past '}'
|
|
||||||
|
|
||||||
// Split field path and optional pattern
|
|
||||||
var fieldPath, pattern string
|
|
||||||
if k := strings.IndexByte(token, '='); k >= 0 {
|
|
||||||
fieldPath = strings.TrimSpace(token[:k])
|
|
||||||
pattern = strings.TrimSpace(token[k+1:])
|
|
||||||
} else {
|
|
||||||
fieldPath = strings.TrimSpace(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fieldPath == "" {
|
|
||||||
return nil, fmt.Errorf("empty variable in path: %s", input)
|
|
||||||
}
|
|
||||||
|
|
||||||
pv := pathVar{
|
|
||||||
fieldPath: fieldPath,
|
|
||||||
pattern: pattern,
|
|
||||||
multiSegment: isMultiSegmentPattern(pattern),
|
|
||||||
}
|
|
||||||
segments = append(segments, pv)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: add any trailing literal after last '}'
|
|
||||||
if buf.Len() > 0 {
|
|
||||||
segments = append(segments, pathLiteral{text: buf.String()})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: extract literalPrefix if the first segment is a literal
|
|
||||||
var literalPrefix string
|
|
||||||
if len(segments) > 0 {
|
|
||||||
if pl, ok := segments[0].(pathLiteral); ok {
|
|
||||||
literalPrefix = pl.text
|
|
||||||
segments = segments[1:] // remove from segments to avoid duplication
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: return fully parsed pathTemplate
|
|
||||||
return &pathTemplate{
|
|
||||||
literalPrefix: literalPrefix,
|
|
||||||
segments: segments,
|
|
||||||
customVerb: customVerb,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isMultiSegmentPattern returns true if pattern can match multiple path segments (contains '/' or '**').
|
|
||||||
// Examples:
|
|
||||||
// | Pattern | Result | Usecase |
|
|
||||||
// |----------------|--------|------------------------------------------|
|
|
||||||
// | "" | false | {var} => single segment |
|
|
||||||
// | "*" | false | {var=*} => single segment |
|
|
||||||
// | "**" | true | {var=**} => multiple segments |
|
|
||||||
// | "foo/*" | true | {var=foo/*} => multiple segments |
|
|
||||||
// | "foo/**" | true | {var=foo/**} => multiple segments |
|
|
||||||
// | "users/*/orders"| true | {users/*/orders} => multiple segments |
|
|
||||||
func isMultiSegmentPattern(pattern string) bool {
|
|
||||||
if pattern == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if pattern == singleWildcard {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.Contains(pattern, "/") || strings.Contains(pattern, doubleWildcard)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------- Path template resolving -----------------------------
|
|
||||||
|
|
||||||
// resolvePathPlaceholders expands placeholders in a path template using values from proto.Message.
|
|
||||||
// Placeholders must be bound to non-repeated scalar fields (not lists, maps, or messages).
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
//
|
|
||||||
// tmpl: "/v1/users/{user_id}/orders:get"
|
|
||||||
// msg: &pb.Message{UserId: 12345}
|
|
||||||
//
|
|
||||||
// path: "/v1/users/12345/orders:get"
|
|
||||||
// usedFields: {"user_id"}
|
|
||||||
func resolvePathPlaceholders(tmpl *pathTemplate, msg proto.Message) (path string, usedFields *usedFields, err error) {
|
|
||||||
usedFields = newUsedFields()
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
sb.WriteString(tmpl.literalPrefix)
|
|
||||||
|
|
||||||
msgReflect := msg.ProtoReflect()
|
|
||||||
|
|
||||||
for _, segment := range tmpl.segments {
|
|
||||||
switch s := segment.(type) {
|
|
||||||
case pathLiteral:
|
|
||||||
sb.WriteString(s.text)
|
|
||||||
|
|
||||||
case pathVar:
|
|
||||||
val, fd, ok := findFieldByPath(msgReflect, s.fieldPath)
|
|
||||||
if !ok {
|
|
||||||
return "", nil, fmt.Errorf("path placeholder %s not found", s.fieldPath)
|
|
||||||
}
|
|
||||||
if isZeroValue(val, fd) {
|
|
||||||
// it's the only case that allows zero-value matches.
|
|
||||||
if s.pattern == doubleWildcard {
|
|
||||||
usedFields.add(s.fieldPath)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return "", nil, fmt.Errorf("path placeholder %s has zero value", s.fieldPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// must be scalar (non-repeated, non-map, non-message)
|
|
||||||
if fd.IsList() || fd.IsMap() || fd.Kind() == protoreflect.MessageKind {
|
|
||||||
return "", nil, fmt.Errorf("path placeholder %s must be scalar", s.fieldPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
usedFields.add(s.fieldPath)
|
|
||||||
|
|
||||||
var strVal string
|
|
||||||
strVal, err = stringifyValue(val, fd)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("stringify placeholder %s: %w", s.fieldPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = validatePattern(s.pattern, strVal); err != nil {
|
|
||||||
return "", nil, fmt.Errorf("validate pattern, %s:%s: %w", s.fieldPath, strVal, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(strVal, "/")
|
|
||||||
for i := range parts {
|
|
||||||
parts[i] = url.PathEscape(parts[i])
|
|
||||||
}
|
|
||||||
sb.WriteString(strings.Join(parts, "/"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString(tmpl.customVerb)
|
|
||||||
return sb.String(), usedFields, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validatePattern checks whether input matches the given path pattern.
|
|
||||||
//
|
|
||||||
// Rules:
|
|
||||||
// - "" or "*" => exactly one segment, no "/" allowed
|
|
||||||
// - "**" => zero or more segments (may include "/")
|
|
||||||
// - composite patterns like "*/orders/*" must match literally
|
|
||||||
//
|
|
||||||
// Example for composite pattern case:
|
|
||||||
//
|
|
||||||
// pattern: "*/orders/*"
|
|
||||||
// input: "42/orders/123"
|
|
||||||
//
|
|
||||||
// patternSegments = ["*", "orders", "*"]
|
|
||||||
// valueParts = ["42", "orders", "123"]
|
|
||||||
//
|
|
||||||
// Match:
|
|
||||||
// "*" -> "42"
|
|
||||||
// "orders" -> "orders"
|
|
||||||
// "*" -> "123"
|
|
||||||
func validatePattern(pattern, input string) error {
|
|
||||||
var (
|
|
||||||
parts = strings.Split(input, "/")
|
|
||||||
lenParts = len(parts)
|
|
||||||
)
|
|
||||||
|
|
||||||
if pattern == "" || pattern == singleWildcard {
|
|
||||||
if lenParts != 1 {
|
|
||||||
return errors.New("must be a single path segment")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if pattern == doubleWildcard {
|
|
||||||
if lenParts < 1 {
|
|
||||||
return errors.New("must contain at least one segment")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
patternSegments = strings.Split(pattern, "/")
|
|
||||||
patternIndex int
|
|
||||||
)
|
|
||||||
|
|
||||||
for i := 0; i < len(patternSegments); i++ {
|
|
||||||
switch patternSegments[i] {
|
|
||||||
case singleWildcard:
|
|
||||||
if patternIndex >= lenParts || parts[patternIndex] == "" {
|
|
||||||
return fmt.Errorf("segment %d must not be empty", patternIndex)
|
|
||||||
}
|
|
||||||
patternIndex++
|
|
||||||
case doubleWildcard:
|
|
||||||
if patternIndex >= lenParts {
|
|
||||||
return fmt.Errorf("must contain at least one segment at position %d", patternIndex)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
if patternIndex >= lenParts || parts[patternIndex] != patternSegments[i] {
|
|
||||||
return fmt.Errorf("expected literal %s at position %d", patternSegments[i], patternIndex)
|
|
||||||
}
|
|
||||||
patternIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if patternIndex != lenParts {
|
|
||||||
return errors.New("extra segments in value")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,21 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
var (
|
|
||||||
pathTemplateCache = make(map[string]*pathTemplate)
|
|
||||||
pathTemplateCacheMu sync.RWMutex
|
|
||||||
)
|
|
||||||
|
|
||||||
func getCachedPathTemplate(path string) (*pathTemplate, bool) {
|
|
||||||
pathTemplateCacheMu.RLock()
|
|
||||||
defer pathTemplateCacheMu.RUnlock()
|
|
||||||
tmpl, ok := pathTemplateCache[path]
|
|
||||||
return tmpl, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func setPathTemplateCache(path string, tmpl *pathTemplate) {
|
|
||||||
pathTemplateCacheMu.Lock()
|
|
||||||
defer pathTemplateCacheMu.Unlock()
|
|
||||||
pathTemplateCache[path] = tmpl
|
|
||||||
}
|
|
@@ -1,15 +0,0 @@
|
|||||||
package proto
|
|
||||||
|
|
||||||
import "google.golang.org/protobuf/encoding/protojson"
|
|
||||||
|
|
||||||
var marshaler = protojson.MarshalOptions{}
|
|
||||||
|
|
||||||
func (m *Test_Client_Call_DefaultError) Error() string {
|
|
||||||
buf, _ := marshaler.Marshal(m)
|
|
||||||
return string(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Test_Client_Call_SpecialError) Error() string {
|
|
||||||
buf, _ := marshaler.Marshal(m)
|
|
||||||
return string(buf)
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,330 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package proto;
|
|
||||||
|
|
||||||
option go_package = "go.unistack.org/micro-client-http/v4/proto;proto";
|
|
||||||
|
|
||||||
message TestRequestBuilder {}
|
|
||||||
|
|
||||||
message Test_PathOnly {
|
|
||||||
message PrimitiveCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
int64 orderId = 2 [json_name = "order_id"];
|
|
||||||
}
|
|
||||||
|
|
||||||
message NestedCase {
|
|
||||||
User user = 1;
|
|
||||||
Order order = 2;
|
|
||||||
|
|
||||||
message User {
|
|
||||||
string id = 1;
|
|
||||||
}
|
|
||||||
message Order {
|
|
||||||
int64 id = 1;
|
|
||||||
Product product = 2;
|
|
||||||
|
|
||||||
message Product {
|
|
||||||
int64 id = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message MultipleCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
Order order = 2;
|
|
||||||
|
|
||||||
message Order {
|
|
||||||
string id = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message RepeatedCase {
|
|
||||||
repeated string userId = 1 [json_name = "user_id"];
|
|
||||||
int64 orderId = 2 [json_name = "order_id"];
|
|
||||||
}
|
|
||||||
|
|
||||||
message NonPrimitiveMessageCase {
|
|
||||||
User userId = 1 [json_name = "user_id"];
|
|
||||||
int64 orderId = 2 [json_name = "order_id"];
|
|
||||||
|
|
||||||
message User {
|
|
||||||
string id = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message NonPrimitiveMapCase {
|
|
||||||
map<string, string> userId = 1 [json_name = "user_id"];
|
|
||||||
int64 orderId = 2 [json_name = "order_id"];
|
|
||||||
}
|
|
||||||
|
|
||||||
message PatternCase {
|
|
||||||
string pattern = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CompositePatternCase {
|
|
||||||
string pattern = 1;
|
|
||||||
string orderId = 2 [json_name = "order_id"];
|
|
||||||
string productId = 3 [json_name = "product_id"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message Test_QueryOnly {
|
|
||||||
message PrimitiveCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
int64 orderId = 2 [json_name = "order_id"];
|
|
||||||
bool flag = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RepeatedCase {
|
|
||||||
repeated string strings = 1;
|
|
||||||
repeated int64 integers = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message NestedMessageCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
Filter filter = 2;
|
|
||||||
|
|
||||||
message Filter {
|
|
||||||
int64 age = 1;
|
|
||||||
string name = 2;
|
|
||||||
SubFilter subFilter = 3 [json_name = "sub_filter"];
|
|
||||||
|
|
||||||
message SubFilter {
|
|
||||||
int64 subAge = 1 [json_name = "sub_age"];
|
|
||||||
string subName = 2 [json_name = "sub_name"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message NestedMapCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
map<string, string> firstFilter = 2 [json_name = "first_filter"];
|
|
||||||
map<string, SubFilter> secondFilter = 4 [json_name = "second_filter"];
|
|
||||||
message SubFilter {
|
|
||||||
int64 subAge = 1 [json_name = "sub_age"];
|
|
||||||
string subName = 2 [json_name = "sub_name"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message MultipleCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
repeated string strings = 2;
|
|
||||||
Filter firstFilter = 3 [json_name = "first_filter"];
|
|
||||||
map<string, SubFilter> secondFilter = 4 [json_name = "second_filter"];
|
|
||||||
|
|
||||||
message Filter {
|
|
||||||
int64 age = 1;
|
|
||||||
SubFilter subFilter = 2 [json_name = "sub_filter"];
|
|
||||||
}
|
|
||||||
message SubFilter {
|
|
||||||
int64 subAge = 1 [json_name = "sub_age"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message RepeatedMessageCase {
|
|
||||||
repeated Filter filters = 1;
|
|
||||||
message Filter {
|
|
||||||
int64 age = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message Test_BodyOnly {
|
|
||||||
message PrimitiveCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
int64 orderId = 2 [json_name = "order_id"];
|
|
||||||
bool flag = 3;
|
|
||||||
repeated string strings = 4;
|
|
||||||
Product product = 6;
|
|
||||||
|
|
||||||
message Product {
|
|
||||||
string id = 1;
|
|
||||||
string name = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message NestedCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
Filter first_filter = 2 [json_name = "first_filter"];
|
|
||||||
Filter second_filter = 3 [json_name = "second_filter"];
|
|
||||||
|
|
||||||
message Filter {
|
|
||||||
int64 age = 1;
|
|
||||||
string name = 2;
|
|
||||||
SubFilter subFilter = 3 [json_name = "sub_filter"];
|
|
||||||
|
|
||||||
message SubFilter {
|
|
||||||
int64 subAge = 1 [json_name = "sub_age"];
|
|
||||||
string subName = 2 [json_name = "sub_name"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message RepeatedMessageCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
repeated Product products = 2 [json_name = "products"];
|
|
||||||
|
|
||||||
message Product {
|
|
||||||
string id = 1;
|
|
||||||
string name = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message MapCase {
|
|
||||||
map<string, string> firstFilter = 1 [json_name = "first_filter"];
|
|
||||||
map<string, SubFilter> secondFilter = 2 [json_name = "second_filter"];
|
|
||||||
|
|
||||||
message SubFilter {
|
|
||||||
int64 subAge = 1 [json_name = "sub_age"];
|
|
||||||
string subName = 2 [json_name = "sub_name"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message MultipleCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
repeated SubFilter firstFilter = 2 [json_name = "first_filter"];
|
|
||||||
map<string, SubFilter> secondFilter = 3 [json_name = "second_filter"];
|
|
||||||
SubFilter thirdFilter = 4 [json_name = "third_filter"];
|
|
||||||
|
|
||||||
message SubFilter {
|
|
||||||
int64 subAge = 1 [json_name = "sub_age"];
|
|
||||||
string subName = 2 [json_name = "sub_name"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message Test_Mixed {
|
|
||||||
message PrimitiveCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
int64 orderId = 2 [json_name = "order_id"];
|
|
||||||
Product product = 3;
|
|
||||||
|
|
||||||
message Product {
|
|
||||||
string id = 1;
|
|
||||||
string name = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message NestedCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
Filter first_filter = 2 [json_name = "first_filter"];
|
|
||||||
Filter second_filter = 3 [json_name = "second_filter"];
|
|
||||||
|
|
||||||
message Filter {
|
|
||||||
int64 age = 1;
|
|
||||||
string name = 2;
|
|
||||||
SubFilter subFilter = 3 [json_name = "sub_filter"];
|
|
||||||
|
|
||||||
message SubFilter {
|
|
||||||
int64 subAge = 1 [json_name = "sub_age"];
|
|
||||||
string subName = 2 [json_name = "sub_name"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message RepeatedMessageCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
repeated Product products = 2 [json_name = "products"];
|
|
||||||
|
|
||||||
message Product {
|
|
||||||
string id = 1;
|
|
||||||
string name = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message MapCase {
|
|
||||||
map<string, string> firstFilter = 1 [json_name = "first_filter"];
|
|
||||||
map<string, SubFilter> secondFilter = 2 [json_name = "second_filter"];
|
|
||||||
|
|
||||||
message SubFilter {
|
|
||||||
int64 subAge = 1 [json_name = "sub_age"];
|
|
||||||
string subName = 2 [json_name = "sub_name"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message MultipleCase {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
repeated SubFilter firstFilter = 2 [json_name = "first_filter"];
|
|
||||||
map<string, SubFilter> secondFilter = 3 [json_name = "second_filter"];
|
|
||||||
SubFilter thirdFilter = 4 [json_name = "third_filter"];
|
|
||||||
|
|
||||||
message SubFilter {
|
|
||||||
int64 subAge = 1 [json_name = "sub_age"];
|
|
||||||
string subName = 2 [json_name = "sub_name"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message Benchmark {
|
|
||||||
message Case5 {
|
|
||||||
string field1 = 1;
|
|
||||||
string field2 = 2;
|
|
||||||
string field3 = 3;
|
|
||||||
string field4 = 4;
|
|
||||||
string field5 = 5;
|
|
||||||
}
|
|
||||||
message Case10 {
|
|
||||||
string field1 = 1;
|
|
||||||
string field2 = 2;
|
|
||||||
string field3 = 3;
|
|
||||||
string field4 = 4;
|
|
||||||
string field5 = 5;
|
|
||||||
string field6 = 6;
|
|
||||||
string field7 = 7;
|
|
||||||
string field8 = 8;
|
|
||||||
string field9 = 9;
|
|
||||||
string field10 = 10;
|
|
||||||
}
|
|
||||||
message Case30 {
|
|
||||||
string field1 = 1;
|
|
||||||
string field2 = 2;
|
|
||||||
string field3 = 3;
|
|
||||||
string field4 = 4;
|
|
||||||
string field5 = 5;
|
|
||||||
string field6 = 6;
|
|
||||||
string field7 = 7;
|
|
||||||
string field8 = 8;
|
|
||||||
string field9 = 9;
|
|
||||||
string field10 = 10;
|
|
||||||
string field11 = 11;
|
|
||||||
string field12 = 12;
|
|
||||||
string field13 = 13;
|
|
||||||
string field14 = 14;
|
|
||||||
string field15 = 15;
|
|
||||||
string field16 = 16;
|
|
||||||
string field17 = 17;
|
|
||||||
string field18 = 18;
|
|
||||||
string field19 = 19;
|
|
||||||
string field20 = 20;
|
|
||||||
string field21 = 21;
|
|
||||||
string field22 = 22;
|
|
||||||
string field23 = 23;
|
|
||||||
string field24 = 24;
|
|
||||||
string field25 = 25;
|
|
||||||
string field26 = 26;
|
|
||||||
string field27 = 27;
|
|
||||||
string field28 = 28;
|
|
||||||
string field29 = 29;
|
|
||||||
string field30 = 30;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message Test_Client_Call {
|
|
||||||
message Request {
|
|
||||||
string userId = 1 [json_name = "user_id"];
|
|
||||||
int64 orderId = 2 [json_name = "order_id"];
|
|
||||||
}
|
|
||||||
message Response {
|
|
||||||
string id = 1;
|
|
||||||
string name = 2;
|
|
||||||
}
|
|
||||||
message DefaultError {
|
|
||||||
string code = 1;
|
|
||||||
string msg = 2;
|
|
||||||
}
|
|
||||||
message SpecialError {
|
|
||||||
string code = 1;
|
|
||||||
string msg = 2;
|
|
||||||
string warning = 3;
|
|
||||||
}
|
|
||||||
}
|
|
194
builder/query.go
194
builder/query.go
@@ -1,194 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
func buildQuery(msg proto.Message, usedFieldsPath *usedFields, usedFieldBody string) (url.Values, error) {
|
|
||||||
var (
|
|
||||||
query = url.Values{}
|
|
||||||
msgReflect = msg.ProtoReflect()
|
|
||||||
)
|
|
||||||
|
|
||||||
fields := msgReflect.Descriptor().Fields()
|
|
||||||
for i := 0; i < fields.Len(); i++ {
|
|
||||||
var (
|
|
||||||
fd = fields.Get(i)
|
|
||||||
fieldName = fd.JSONName()
|
|
||||||
)
|
|
||||||
|
|
||||||
if usedFieldsPath.hasFullKey(fieldName) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fieldName == usedFieldBody {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val := msgReflect.Get(fd)
|
|
||||||
if isZeroValue(val, fd) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: order of the cases is important!
|
|
||||||
switch {
|
|
||||||
case fd.IsList():
|
|
||||||
if fd.Kind() == protoreflect.MessageKind {
|
|
||||||
return nil, fmt.Errorf("repeated message field %s cannot be mapped to URL query parameters", fieldName)
|
|
||||||
}
|
|
||||||
list := val.List()
|
|
||||||
for j := 0; j < list.Len(); j++ {
|
|
||||||
strVal, err := stringifyValue(list.Get(j), fd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("stringify value for query %s: %w", fieldName, err)
|
|
||||||
}
|
|
||||||
query.Add(fieldName, strVal)
|
|
||||||
}
|
|
||||||
|
|
||||||
case fd.IsMap():
|
|
||||||
var (
|
|
||||||
m = val.Map()
|
|
||||||
rangeErr error
|
|
||||||
)
|
|
||||||
m.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
|
|
||||||
key := fmt.Sprintf("%s.%s", fieldName, k.String())
|
|
||||||
|
|
||||||
if fd.MapValue().Kind() == protoreflect.MessageKind {
|
|
||||||
flattened, err := flattenMsgForQuery(key, v.Message())
|
|
||||||
if err != nil {
|
|
||||||
rangeErr = fmt.Errorf("flatten msg for query %s: %w", fieldName, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, item := range flattened {
|
|
||||||
if item.val == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if usedFieldsPath.hasFullKey(item.key) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
query.Add(item.key, item.val)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
strVal, err := stringifyValue(v, fd.MapValue())
|
|
||||||
if err != nil {
|
|
||||||
rangeErr = fmt.Errorf("stringify value for map %s: %w", fieldName, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
query.Add(key, strVal)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
if rangeErr != nil {
|
|
||||||
return nil, fmt.Errorf("map range error: %w", rangeErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
case fd.Kind() == protoreflect.MessageKind:
|
|
||||||
flattened, err := flattenMsgForQuery(fieldName, val.Message())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("flatten msg for query %s: %w", fieldName, err)
|
|
||||||
}
|
|
||||||
for _, item := range flattened {
|
|
||||||
if item.val == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if usedFieldsPath.hasFullKey(item.key) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
query.Add(item.key, item.val)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
strVal, err := stringifyValue(val, fd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("stringify value for primitive %s: %w", fieldName, err)
|
|
||||||
}
|
|
||||||
query.Add(fieldName, strVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return query, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type flattenItem struct {
|
|
||||||
key string
|
|
||||||
val string
|
|
||||||
}
|
|
||||||
|
|
||||||
// flattenMsgForQuery flattens a non-repeated message value under a given prefix.
|
|
||||||
func flattenMsgForQuery(prefix string, msg protoreflect.Message) ([]flattenItem, error) {
|
|
||||||
var out []flattenItem
|
|
||||||
|
|
||||||
fields := msg.Descriptor().Fields()
|
|
||||||
for i := 0; i < fields.Len(); i++ {
|
|
||||||
var (
|
|
||||||
fd = fields.Get(i)
|
|
||||||
val = msg.Get(fd)
|
|
||||||
)
|
|
||||||
|
|
||||||
if isZeroValue(val, fd) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
key := fmt.Sprintf("%s.%s", prefix, fd.JSONName())
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case fd.IsList():
|
|
||||||
if fd.Kind() == protoreflect.MessageKind {
|
|
||||||
return nil, fmt.Errorf("repeated message field %s cannot be flattened for query", key)
|
|
||||||
}
|
|
||||||
list := val.List()
|
|
||||||
for j := 0; j < list.Len(); j++ {
|
|
||||||
strVal, err := stringifyValue(list.Get(j), fd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("stringify query %s: %w", key, err)
|
|
||||||
}
|
|
||||||
out = append(out, flattenItem{key: key, val: strVal})
|
|
||||||
}
|
|
||||||
|
|
||||||
case fd.Kind() == protoreflect.MessageKind:
|
|
||||||
nested, err := flattenMsgForQuery(key, val.Message())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("flatten msg for query %s: %w", key, err)
|
|
||||||
}
|
|
||||||
out = append(out, nested...)
|
|
||||||
|
|
||||||
case fd.IsMap():
|
|
||||||
var mapErr error
|
|
||||||
val.Map().Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
|
|
||||||
keyStr := k.String()
|
|
||||||
|
|
||||||
if fd.MapValue().Kind() == protoreflect.MessageKind {
|
|
||||||
child, err := flattenMsgForQuery(keyStr, v.Message())
|
|
||||||
if err != nil {
|
|
||||||
mapErr = fmt.Errorf("flatten map value %s: %w", key, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
out = append(out, child...)
|
|
||||||
} else {
|
|
||||||
strVal, err := stringifyValue(v, fd.MapValue())
|
|
||||||
if err != nil {
|
|
||||||
mapErr = fmt.Errorf("stringify query %s: %w", keyStr, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
out = append(out, flattenItem{key: keyStr, val: strVal})
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
if mapErr != nil {
|
|
||||||
return nil, mapErr
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
strVal, err := stringifyValue(val, fd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("stringify query %s: %w", key, err)
|
|
||||||
}
|
|
||||||
out = append(out, flattenItem{key: key, val: strVal})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
@@ -1,180 +0,0 @@
|
|||||||
// Package builder implements google.api.http-style request building (gRPC JSON transcoding)
|
|
||||||
// for HTTP requests, closely following the google.api.http spec.
|
|
||||||
// See full spec for details: https://github.com/googleapis/googleapis/blob/master/google/api/http.proto
|
|
||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RequestBuilder struct {
|
|
||||||
path string // e.g. "/v1/{name=projects/*/topics/*}:publish" or "/users/{user.id}"
|
|
||||||
method string // GET, POST, PATCH, etc. (not used in mapping rules, but convenient for callers)
|
|
||||||
bodyOption bodyOption // "", "*", or top-level field name
|
|
||||||
msg proto.Message // request struct
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRequestBuilder(
|
|
||||||
path string,
|
|
||||||
method string,
|
|
||||||
bodyOpt string,
|
|
||||||
msg proto.Message,
|
|
||||||
) (
|
|
||||||
*RequestBuilder,
|
|
||||||
error,
|
|
||||||
) {
|
|
||||||
rb := &RequestBuilder{
|
|
||||||
path: path,
|
|
||||||
method: method,
|
|
||||||
bodyOption: bodyOption(bodyOpt),
|
|
||||||
msg: msg,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := rb.validate(); err != nil {
|
|
||||||
return nil, fmt.Errorf("validate: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rb, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build applies mapping rules and returns:
|
|
||||||
//
|
|
||||||
// resolvedPath — path with placeholders substituted and query appended
|
|
||||||
// newMsg — same concrete type as input, filtered to contain only the body fields
|
|
||||||
// err — if mapping/validation failed
|
|
||||||
func (b *RequestBuilder) Build() (resolvedPath string, newMsg proto.Message, err error) {
|
|
||||||
tmpl, isCached := getCachedPathTemplate(b.path)
|
|
||||||
if !isCached {
|
|
||||||
tmpl, err = parsePathTemplate(b.path)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("parse path template: %w", err)
|
|
||||||
}
|
|
||||||
setPathTemplateCache(b.path, tmpl)
|
|
||||||
}
|
|
||||||
|
|
||||||
var usedFieldsPath *usedFields
|
|
||||||
resolvedPath, usedFieldsPath, err = resolvePathPlaceholders(tmpl, b.msg)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("resolve path placeholders: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if all set fields are already used in path, no need to process query/body
|
|
||||||
if allFieldsUsed(b.msg, usedFieldsPath) {
|
|
||||||
return resolvedPath, initZeroMsg(b.msg), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case b.bodyOption.isWithoutBody():
|
|
||||||
var query url.Values
|
|
||||||
query, err = buildQuery(b.msg, usedFieldsPath, "")
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("build query: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedPath + encodeQuery(query), initZeroMsg(b.msg), nil
|
|
||||||
|
|
||||||
case b.bodyOption.isSingleField():
|
|
||||||
fieldBody := b.bodyOption.String()
|
|
||||||
|
|
||||||
newMsg, err = buildSingleFieldBody(b.msg, fieldBody)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("build single field body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var query url.Values
|
|
||||||
query, err = buildQuery(b.msg, usedFieldsPath, fieldBody)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("build query: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedPath + encodeQuery(query), newMsg, nil
|
|
||||||
|
|
||||||
case b.bodyOption.isFullBody():
|
|
||||||
newMsg, err = buildFullBody(b.msg, usedFieldsPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("build full body: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedPath, newMsg, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return "", nil, fmt.Errorf("unsupported body option %s", b.bodyOption.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *RequestBuilder) validate() error {
|
|
||||||
if b.path == "" {
|
|
||||||
return errors.New("path is empty")
|
|
||||||
}
|
|
||||||
if err := validateHTTPMethod(b.method); err != nil {
|
|
||||||
return fmt.Errorf("validate http method: %w", err)
|
|
||||||
}
|
|
||||||
if err := validateHTTPMethodAndBody(b.method, b.bodyOption); err != nil {
|
|
||||||
return fmt.Errorf("validate http method and body: %w", err)
|
|
||||||
}
|
|
||||||
if b.msg == nil {
|
|
||||||
return errors.New("msg is nil")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateHTTPMethod(method string) error {
|
|
||||||
switch strings.ToUpper(method) {
|
|
||||||
case http.MethodGet,
|
|
||||||
http.MethodHead,
|
|
||||||
http.MethodPost,
|
|
||||||
http.MethodPut,
|
|
||||||
http.MethodPatch,
|
|
||||||
http.MethodDelete,
|
|
||||||
http.MethodConnect,
|
|
||||||
http.MethodOptions,
|
|
||||||
http.MethodTrace:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return errors.New("invalid http method")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateHTTPMethodAndBody(method string, bodyOpt bodyOption) error {
|
|
||||||
switch method {
|
|
||||||
case http.MethodGet, http.MethodDelete, http.MethodHead, http.MethodOptions:
|
|
||||||
if !bodyOpt.isWithoutBody() {
|
|
||||||
return fmt.Errorf("%s method must not have a body", method)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func allFieldsUsed(msg proto.Message, used *usedFields) bool {
|
|
||||||
if used.len() == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
count := 0
|
|
||||||
msg.ProtoReflect().Range(func(protoreflect.FieldDescriptor, protoreflect.Value) bool {
|
|
||||||
count++
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return used.len() == count
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeQuery(query url.Values) string {
|
|
||||||
if len(query) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
enc := query.Encode()
|
|
||||||
if enc == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "?" + enc
|
|
||||||
}
|
|
||||||
|
|
||||||
func initZeroMsg(msg proto.Message) proto.Message {
|
|
||||||
return msg.ProtoReflect().New().Interface()
|
|
||||||
}
|
|
@@ -1,149 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
|
|
||||||
pb "go.unistack.org/micro-client-http/v4/builder/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// sink prevents the compiler from optimizing away parsePathTemplate results.
|
|
||||||
var sink *pathTemplate
|
|
||||||
|
|
||||||
func BenchmarkParsePathTemplate(b *testing.B) {
|
|
||||||
r := rand.New(rand.NewSource(1))
|
|
||||||
|
|
||||||
benchInput := func(size int) string {
|
|
||||||
sb := strings.Builder{}
|
|
||||||
sb.Grow(size * 10)
|
|
||||||
|
|
||||||
for i := 0; i < size; i++ {
|
|
||||||
name := fmt.Sprintf("var%d", r.Intn(1000))
|
|
||||||
|
|
||||||
if r.Intn(5) == 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf("{%s=**}", name))
|
|
||||||
} else {
|
|
||||||
sb.WriteString(fmt.Sprintf("{%s}", name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
sizes := []int{1_000, 10_000, 50_000, 100_000}
|
|
||||||
for _, size := range sizes {
|
|
||||||
input := benchInput(size)
|
|
||||||
b.Run(fmt.Sprintf("N=%d", size), func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
var err error
|
|
||||||
sink, err = parsePathTemplate(input)
|
|
||||||
if err != nil && testing.Verbose() {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkRequestBuilder(b *testing.B) {
|
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
|
|
||||||
makeMsg := func(fieldCount int) proto.Message {
|
|
||||||
switch fieldCount {
|
|
||||||
case 5:
|
|
||||||
return &pb.Benchmark_Case5{
|
|
||||||
Field1: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field2: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field3: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field4: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field5: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
}
|
|
||||||
case 10:
|
|
||||||
return &pb.Benchmark_Case10{
|
|
||||||
Field1: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field2: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field3: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field4: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field5: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field6: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field7: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field8: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field9: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field10: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
}
|
|
||||||
case 30:
|
|
||||||
return &pb.Benchmark_Case30{
|
|
||||||
Field1: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field2: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field3: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field4: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field5: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field6: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field7: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field8: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field9: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field10: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field11: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field12: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field13: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field14: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field15: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field16: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field17: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field18: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field19: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field20: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field21: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field22: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field23: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field24: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field25: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field26: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field27: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field28: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field29: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
Field30: fmt.Sprintf("value%d", r.Intn(1000)),
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
b.Fatal("undefined field count")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
pathTmpl string
|
|
||||||
bodyOption string
|
|
||||||
}{
|
|
||||||
{"all fields in path", "/resource/{field1}/{field2}", ""},
|
|
||||||
{"single field body", "/resource/{field1}", "field4"},
|
|
||||||
{"full body", "/resource", "*"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fields := range []int{5, 10, 30} {
|
|
||||||
for _, tt := range tests {
|
|
||||||
b.Run(fmt.Sprintf("%s_%d_fields", tt.name, fields), func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
msg := makeMsg(fields)
|
|
||||||
rb, err := NewRequestBuilder(tt.pathTmpl, "POST", tt.bodyOption, msg)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("new request builder: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, err = rb.Build()
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("build: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,44 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
// usedFields stores keys and their top-level parts,
|
|
||||||
// turning top-level lookups from O(N) into O(1).
|
|
||||||
type usedFields struct {
|
|
||||||
full map[string]struct{}
|
|
||||||
top map[string]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUsedFields() *usedFields {
|
|
||||||
return &usedFields{
|
|
||||||
full: make(map[string]struct{}),
|
|
||||||
top: make(map[string]struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add inserts a new key and updates the top-level index.
|
|
||||||
func (u *usedFields) add(key string) {
|
|
||||||
u.full[key] = struct{}{}
|
|
||||||
top := key
|
|
||||||
if i := strings.IndexByte(key, '.'); i != -1 {
|
|
||||||
top = key[:i]
|
|
||||||
}
|
|
||||||
u.top[top] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// hasTopLevelKey checks if a top-level key exists.
|
|
||||||
func (u *usedFields) hasTopLevelKey(top string) bool {
|
|
||||||
_, ok := u.top[top]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// hasFullKey checks if an exact key exists.
|
|
||||||
func (u *usedFields) hasFullKey(key string) bool {
|
|
||||||
_, ok := u.full[key]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// len returns the number of full keys stored in the set.
|
|
||||||
func (u *usedFields) len() int {
|
|
||||||
return len(u.full)
|
|
||||||
}
|
|
@@ -1,78 +0,0 @@
|
|||||||
package builder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewUsedFields(t *testing.T) {
|
|
||||||
u := newUsedFields()
|
|
||||||
require.NotNil(t, u)
|
|
||||||
require.NotNil(t, u.full)
|
|
||||||
require.NotNil(t, u.top)
|
|
||||||
require.Len(t, u.full, 0)
|
|
||||||
require.Len(t, u.top, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsedFields_Add(t *testing.T) {
|
|
||||||
u := newUsedFields()
|
|
||||||
|
|
||||||
u.add("user.name")
|
|
||||||
u.add("profile")
|
|
||||||
|
|
||||||
_, ok := u.full["user.name"]
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
_, ok = u.full["profile"]
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
_, ok = u.top["user"]
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
_, ok = u.top["profile"]
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
require.Len(t, u.full, 2)
|
|
||||||
require.Len(t, u.top, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsedFields_HasFullKey(t *testing.T) {
|
|
||||||
u := newUsedFields()
|
|
||||||
u.add("user.name")
|
|
||||||
|
|
||||||
require.True(t, u.hasFullKey("user.name"))
|
|
||||||
require.False(t, u.hasFullKey("user.email"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsedFields_HasTopLevelKey(t *testing.T) {
|
|
||||||
u := newUsedFields()
|
|
||||||
u.add("user.name")
|
|
||||||
u.add("settings.theme")
|
|
||||||
|
|
||||||
require.True(t, u.hasTopLevelKey("user"))
|
|
||||||
require.True(t, u.hasTopLevelKey("settings"))
|
|
||||||
require.False(t, u.hasTopLevelKey("profile"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsedFields_AddDuplicate(t *testing.T) {
|
|
||||||
u := newUsedFields()
|
|
||||||
u.add("user.name")
|
|
||||||
u.add("user.name")
|
|
||||||
|
|
||||||
require.True(t, u.hasFullKey("user.name"))
|
|
||||||
require.True(t, u.hasTopLevelKey("user"))
|
|
||||||
require.Len(t, u.full, 1)
|
|
||||||
require.Len(t, u.top, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsedFields_Len(t *testing.T) {
|
|
||||||
u := newUsedFields()
|
|
||||||
|
|
||||||
u.add("user.name")
|
|
||||||
u.add("profile")
|
|
||||||
u.add("user.name")
|
|
||||||
u.add("profile")
|
|
||||||
|
|
||||||
require.Equal(t, u.len(), 2)
|
|
||||||
}
|
|
122
client.go
122
client.go
@@ -1,122 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/client"
|
|
||||||
"go.unistack.org/micro/v4/errors"
|
|
||||||
"go.unistack.org/micro/v4/options"
|
|
||||||
"go.unistack.org/micro/v4/semconv"
|
|
||||||
"go.unistack.org/micro/v4/tracer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var DefaultContentType = "application/json"
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
funcCall client.FuncCall
|
|
||||||
funcStream client.FuncStream
|
|
||||||
httpClient *http.Client
|
|
||||||
opts client.Options
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(opts ...client.Option) *Client {
|
|
||||||
clientOpts := client.NewOptions(opts...)
|
|
||||||
|
|
||||||
if len(clientOpts.ContentType) == 0 {
|
|
||||||
clientOpts.ContentType = DefaultContentType
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &Client{opts: clientOpts}
|
|
||||||
|
|
||||||
dialer, ok := httpDialerFromOpts(clientOpts)
|
|
||||||
if !ok {
|
|
||||||
dialer = defaultHTTPDialer()
|
|
||||||
}
|
|
||||||
|
|
||||||
c.httpClient, ok = httpClientFromOpts(clientOpts)
|
|
||||||
if !ok {
|
|
||||||
c.httpClient = defaultHTTPClient(dialer, clientOpts.TLSConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.funcCall = c.fnCall
|
|
||||||
c.funcStream = c.fnStream
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Name() string {
|
|
||||||
return c.opts.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Init(opts ...client.Option) error {
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&c.opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.opts.Hooks.EachPrev(func(hook options.Hook) {
|
|
||||||
switch h := hook.(type) {
|
|
||||||
case client.HookCall:
|
|
||||||
c.funcCall = h(c.funcCall)
|
|
||||||
case client.HookStream:
|
|
||||||
c.funcStream = h(c.funcStream)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Options() client.Options {
|
|
||||||
return c.opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) NewRequest(service, method string, req any, opts ...client.RequestOption) client.Request {
|
|
||||||
reqOpts := client.NewRequestOptions(opts...)
|
|
||||||
if reqOpts.ContentType == "" {
|
|
||||||
reqOpts.ContentType = c.opts.ContentType
|
|
||||||
}
|
|
||||||
|
|
||||||
return &httpRequest{
|
|
||||||
service: service,
|
|
||||||
method: method,
|
|
||||||
request: req,
|
|
||||||
opts: reqOpts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Call(ctx context.Context, req client.Request, rsp any, opts ...client.CallOption) error {
|
|
||||||
ts := time.Now()
|
|
||||||
c.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc()
|
|
||||||
var sp tracer.Span
|
|
||||||
ctx, sp = c.opts.Tracer.Start(ctx, req.Endpoint()+" rpc-client",
|
|
||||||
tracer.WithSpanKind(tracer.SpanKindClient),
|
|
||||||
tracer.WithSpanLabels("endpoint", req.Endpoint()),
|
|
||||||
)
|
|
||||||
err := c.funcCall(ctx, req, rsp, opts...)
|
|
||||||
c.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Dec()
|
|
||||||
te := time.Since(ts)
|
|
||||||
c.opts.Meter.Summary(semconv.ClientRequestLatencyMicroseconds, "endpoint", req.Endpoint()).Update(te.Seconds())
|
|
||||||
c.opts.Meter.Histogram(semconv.ClientRequestDurationSeconds, "endpoint", req.Endpoint()).Update(te.Seconds())
|
|
||||||
|
|
||||||
if me := errors.FromError(err); me == nil {
|
|
||||||
sp.Finish()
|
|
||||||
c.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "success", "code", strconv.Itoa(int(200))).Inc()
|
|
||||||
} else {
|
|
||||||
sp.SetStatus(tracer.SpanStatusError, err.Error())
|
|
||||||
c.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "failure", "code", strconv.Itoa(int(me.Code))).Inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
|
|
||||||
return c.funcStream(ctx, req, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) String() string {
|
|
||||||
return "http"
|
|
||||||
}
|
|
@@ -1,263 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/client"
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
|
||||||
"go.unistack.org/micro/v4/logger"
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
|
||||||
|
|
||||||
"go.unistack.org/micro-client-http/v4/builder"
|
|
||||||
)
|
|
||||||
|
|
||||||
func buildHTTPRequest(
|
|
||||||
ctx context.Context,
|
|
||||||
addr string,
|
|
||||||
path string,
|
|
||||||
ct string,
|
|
||||||
cf codec.Codec,
|
|
||||||
msg any,
|
|
||||||
opts client.CallOptions,
|
|
||||||
log logger.Logger,
|
|
||||||
) (
|
|
||||||
*http.Request,
|
|
||||||
error,
|
|
||||||
) {
|
|
||||||
protoMsg, ok := msg.(proto.Message)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("msg must be a proto message type")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
method = http.MethodPost
|
|
||||||
bodyOpt string
|
|
||||||
|
|
||||||
parameters = map[string]map[string]string{}
|
|
||||||
)
|
|
||||||
|
|
||||||
if opts.Context != nil {
|
|
||||||
if v, ok := methodFromOpts(opts); ok {
|
|
||||||
method = v
|
|
||||||
}
|
|
||||||
if v, ok := pathFromOpts(opts); ok {
|
|
||||||
path = v
|
|
||||||
}
|
|
||||||
if v, ok := bodyFromOpts(opts); ok {
|
|
||||||
bodyOpt = v
|
|
||||||
}
|
|
||||||
if h, ok := headerFromOpts(opts); ok && len(h) > 0 {
|
|
||||||
m, ok := parameters["header"]
|
|
||||||
if !ok {
|
|
||||||
m = make(map[string]string)
|
|
||||||
parameters["header"] = m
|
|
||||||
}
|
|
||||||
for idx := 0; idx+1 < len(h); idx += 2 {
|
|
||||||
m[h[idx]] = h[idx+1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c, ok := cookieFromOpts(opts); ok && len(c) > 0 {
|
|
||||||
m, ok := parameters["cookie"]
|
|
||||||
if !ok {
|
|
||||||
m = make(map[string]string)
|
|
||||||
parameters["cookie"] = m
|
|
||||||
}
|
|
||||||
for idx := 0; idx+1 < len(c); idx += 2 {
|
|
||||||
m[c[idx]] = c[idx+1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reqBuilder, err := builder.NewRequestBuilder(path, method, bodyOpt, protoMsg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("new request builder: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedPath, newMsg, err := reqBuilder.Build()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("build request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedURL := joinURL(addr, resolvedPath)
|
|
||||||
|
|
||||||
u, err := normalizeURL(resolvedURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("normalize url: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := marshallMsg(cf, newMsg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("marshal msg: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var hreq *http.Request
|
|
||||||
|
|
||||||
if len(body) > 0 {
|
|
||||||
hreq, err = http.NewRequestWithContext(ctx, method, u.String(), io.NopCloser(bytes.NewBuffer(body)))
|
|
||||||
hreq.ContentLength = int64(len(body))
|
|
||||||
} else {
|
|
||||||
hreq, err = http.NewRequestWithContext(ctx, method, u.String(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("new http request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
setHeadersAndCookies(ctx, hreq, ct, opts)
|
|
||||||
if err = validateHeadersAndCookies(hreq, parameters); err != nil {
|
|
||||||
return nil, fmt.Errorf("validate headers and cookies: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if log.V(logger.DebugLevel) {
|
|
||||||
log.Debug(
|
|
||||||
ctx,
|
|
||||||
fmt.Sprintf("request %s to %s with headers %v body %s", method, u.String(), hreq.Header, body),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hreq, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinURL(addr, resolvedPath string) string {
|
|
||||||
if addr == "" {
|
|
||||||
return resolvedPath
|
|
||||||
}
|
|
||||||
if resolvedPath == "" {
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.HasSuffix(addr, "/") && strings.HasPrefix(resolvedPath, "/"):
|
|
||||||
return addr + resolvedPath[1:]
|
|
||||||
case !strings.HasSuffix(addr, "/") && !strings.HasPrefix(resolvedPath, "/"):
|
|
||||||
return addr + "/" + resolvedPath
|
|
||||||
default:
|
|
||||||
return addr + resolvedPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeURL(raw string) (*url.URL, error) {
|
|
||||||
if !strings.Contains(raw, "://") {
|
|
||||||
raw = "http://" + raw
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(raw)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse url: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Scheme != "http" && u.Scheme != "https" {
|
|
||||||
return nil, fmt.Errorf("invalid scheme: %q (must be http or https)", u.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Host == "" {
|
|
||||||
return nil, errors.New("missing host in url")
|
|
||||||
}
|
|
||||||
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func marshallMsg(cf codec.Codec, msg proto.Message) ([]byte, error) {
|
|
||||||
if msg == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
isEmpty := true
|
|
||||||
msg.ProtoReflect().Range(func(protoreflect.FieldDescriptor, protoreflect.Value) bool {
|
|
||||||
isEmpty = false
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if isEmpty {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return cf.Marshal(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setHeadersAndCookies(ctx context.Context, r *http.Request, ct string, opts client.CallOptions) {
|
|
||||||
r.Header = make(http.Header)
|
|
||||||
|
|
||||||
r.Header.Set(metadata.HeaderContentType, ct)
|
|
||||||
r.Header.Set("Content-Length", fmt.Sprintf("%d", r.ContentLength))
|
|
||||||
|
|
||||||
if opts.AuthToken != "" {
|
|
||||||
r.Header.Set(metadata.HeaderAuthorization, opts.AuthToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.StreamTimeout > time.Duration(0) {
|
|
||||||
r.Header.Set(metadata.HeaderTimeout, fmt.Sprintf("%d", opts.StreamTimeout))
|
|
||||||
}
|
|
||||||
if opts.RequestTimeout > time.Duration(0) {
|
|
||||||
r.Header.Set(metadata.HeaderTimeout, fmt.Sprintf("%d", opts.RequestTimeout))
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.RequestMetadata != nil {
|
|
||||||
for k, v := range opts.RequestMetadata {
|
|
||||||
if k == "Cookie" {
|
|
||||||
applyCookies(r, v)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r.Header[k] = append(r.Header[k], v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if md, ok := metadata.FromOutgoingContext(ctx); ok {
|
|
||||||
for k, v := range md {
|
|
||||||
if k == "Cookie" {
|
|
||||||
applyCookies(r, v)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r.Header[k] = append(r.Header[k], v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyCookies(r *http.Request, rawCookies []string) {
|
|
||||||
if len(rawCookies) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
raw := strings.Join(rawCookies, "; ")
|
|
||||||
|
|
||||||
tmp := http.Request{Header: http.Header{}}
|
|
||||||
tmp.Header.Set("Cookie", raw)
|
|
||||||
|
|
||||||
for _, c := range tmp.Cookies() {
|
|
||||||
r.AddCookie(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateHeadersAndCookies(r *http.Request, parameters map[string]map[string]string) error {
|
|
||||||
if headers, ok := parameters["header"]; ok {
|
|
||||||
for name, required := range headers {
|
|
||||||
if required == "true" && r.Header.Get(name) == "" {
|
|
||||||
return fmt.Errorf("missing required header: %s", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cookies, ok := parameters["cookie"]; ok {
|
|
||||||
cookieMap := map[string]string{}
|
|
||||||
for _, c := range r.Cookies() {
|
|
||||||
cookieMap[c.Name] = c.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, required := range cookies {
|
|
||||||
if required == "true" {
|
|
||||||
if _, ok := cookieMap[name]; !ok {
|
|
||||||
return fmt.Errorf("missing required cookie: %s", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,401 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
jsoncodec "go.unistack.org/micro-codec-json/v4"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
|
|
||||||
pb "go.unistack.org/micro-client-http/v4/builder/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestJoinURL(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
addr string
|
|
||||||
path string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "both without slash",
|
|
||||||
addr: "http://example.com",
|
|
||||||
path: "api/v1",
|
|
||||||
want: "http://example.com/api/v1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "addr with slash, path without slash",
|
|
||||||
addr: "http://example.com/",
|
|
||||||
path: "api/v1",
|
|
||||||
want: "http://example.com/api/v1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "addr without slash, path with slash",
|
|
||||||
addr: "http://example.com",
|
|
||||||
path: "/api/v1",
|
|
||||||
want: "http://example.com/api/v1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "both with slash",
|
|
||||||
addr: "http://example.com/",
|
|
||||||
path: "/api/v1",
|
|
||||||
want: "http://example.com/api/v1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty addr",
|
|
||||||
addr: "",
|
|
||||||
path: "/api/v1",
|
|
||||||
want: "/api/v1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty path",
|
|
||||||
addr: "http://example.com",
|
|
||||||
path: "",
|
|
||||||
want: "http://example.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "both empty",
|
|
||||||
addr: "",
|
|
||||||
path: "",
|
|
||||||
want: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
require.Equal(t, tt.want, joinURL(tt.addr, tt.path))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeURL(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
want string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "host with port",
|
|
||||||
input: "localhost:8080",
|
|
||||||
want: "http://localhost:8080",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http with host",
|
|
||||||
input: "http://example.com",
|
|
||||||
want: "http://example.com",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http with no host",
|
|
||||||
input: "http://",
|
|
||||||
want: "",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "https with host",
|
|
||||||
input: "https://example.com",
|
|
||||||
want: "https://example.com",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "https with no host",
|
|
||||||
input: "https://",
|
|
||||||
want: "",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid scheme",
|
|
||||||
input: "ftp://example.com",
|
|
||||||
want: "",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv4 without scheme",
|
|
||||||
input: "127.0.0.1:9000",
|
|
||||||
want: "http://127.0.0.1:9000",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv4 with scheme",
|
|
||||||
input: "http://127.0.0.1:8080",
|
|
||||||
want: "http://127.0.0.1:8080",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv6 without scheme",
|
|
||||||
input: "[::1]:8080",
|
|
||||||
want: "http://[::1]:8080",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv6 with scheme",
|
|
||||||
input: "https://[::1]:443",
|
|
||||||
want: "https://[::1]:443",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "hostname only",
|
|
||||||
input: "my-service",
|
|
||||||
want: "http://my-service",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "hostname with path",
|
|
||||||
input: "service.local/api/v1",
|
|
||||||
want: "http://service.local/api/v1",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "hostname with dash and port",
|
|
||||||
input: "api-service.local:8080",
|
|
||||||
want: "http://api-service.local:8080",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "just path",
|
|
||||||
input: "/api/v1",
|
|
||||||
want: "",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty string",
|
|
||||||
input: "",
|
|
||||||
want: "",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http with query params",
|
|
||||||
input: "http://example.com?x=1&y=2",
|
|
||||||
want: "http://example.com?x=1&y=2",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http with fragment",
|
|
||||||
input: "http://example.com/path#section1",
|
|
||||||
want: "http://example.com/path#section1",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result, err := normalizeURL(tt.input)
|
|
||||||
if tt.wantErr {
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Nil(t, result)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tt.want, result.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarshallMsg(t *testing.T) {
|
|
||||||
type request = pb.Test_Client_Call_Request
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
msg proto.Message
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
msg: &request{},
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil",
|
|
||||||
msg: nil,
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid",
|
|
||||||
msg: &request{UserId: "123", OrderId: 456},
|
|
||||||
expected: `{"userId":"123","orderId":456}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result, err := marshallMsg(jsoncodec.NewCodec(), tt.msg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tt.expected, string(result))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApplyCookies(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
rawCookies []string
|
|
||||||
want []*http.Cookie
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
rawCookies: []string{},
|
|
||||||
want: []*http.Cookie{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single cookie",
|
|
||||||
rawCookies: []string{"session=abc123"},
|
|
||||||
want: []*http.Cookie{
|
|
||||||
{Name: "session", Value: "abc123"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple cookies separate items",
|
|
||||||
rawCookies: []string{"session=abc123", "user=john"},
|
|
||||||
want: []*http.Cookie{
|
|
||||||
{Name: "session", Value: "abc123"},
|
|
||||||
{Name: "user", Value: "john"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple cookies in one item",
|
|
||||||
rawCookies: []string{"a=1; b=2"},
|
|
||||||
want: []*http.Cookie{
|
|
||||||
{Name: "a", Value: "1"},
|
|
||||||
{Name: "b", Value: "2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mix of combined and separate cookies",
|
|
||||||
rawCookies: []string{"a=1; b=2", "c=3"},
|
|
||||||
want: []*http.Cookie{
|
|
||||||
{Name: "a", Value: "1"},
|
|
||||||
{Name: "b", Value: "2"},
|
|
||||||
{Name: "c", Value: "3"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate cookies",
|
|
||||||
rawCookies: []string{"session=abc123", "session=xyz"},
|
|
||||||
want: []*http.Cookie{
|
|
||||||
{Name: "session", Value: "abc123"},
|
|
||||||
{Name: "session", Value: "xyz"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cookie with spaces",
|
|
||||||
rawCookies: []string{"token=abc 123"},
|
|
||||||
want: []*http.Cookie{
|
|
||||||
{Name: "token", Value: "abc 123", Quoted: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
|
||||||
applyCookies(req, tt.rawCookies)
|
|
||||||
require.Equal(t, tt.want, req.Cookies())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateHeadersAndCookies(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
prepareRequest func() *http.Request
|
|
||||||
parameters map[string]map[string]string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "all required headers and cookies present",
|
|
||||||
prepareRequest: func() *http.Request {
|
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
|
||||||
req.Header.Set("My-Header-1", "Header-Value-1")
|
|
||||||
req.Header.Set("My-Header-2", "Header-Value-2")
|
|
||||||
req.AddCookie(&http.Cookie{Name: "session-1", Value: "abc-1"})
|
|
||||||
req.AddCookie(&http.Cookie{Name: "session-2", Value: "abc-2"})
|
|
||||||
return req
|
|
||||||
},
|
|
||||||
parameters: map[string]map[string]string{
|
|
||||||
"header": {"My-Header-1": "true", "My-Header-2": "true"},
|
|
||||||
"cookie": {"session-1": "true", "session-2": "true"},
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing required header",
|
|
||||||
prepareRequest: func() *http.Request {
|
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
|
||||||
req.Header.Set("My-Header-1", "Header-Value-1")
|
|
||||||
req.AddCookie(&http.Cookie{Name: "session-1", Value: "abc-1"})
|
|
||||||
req.AddCookie(&http.Cookie{Name: "session-2", Value: "abc-2"})
|
|
||||||
return req
|
|
||||||
},
|
|
||||||
parameters: map[string]map[string]string{
|
|
||||||
"header": {"My-Header-1": "true", "My-Header-2": "true"},
|
|
||||||
"cookie": {"session-1": "true", "session-2": "true"},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing required cookie",
|
|
||||||
prepareRequest: func() *http.Request {
|
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
|
||||||
req.Header.Set("My-Header-1", "Header-Value-1")
|
|
||||||
req.Header.Set("My-Header-2", "Header-Value-2")
|
|
||||||
req.AddCookie(&http.Cookie{Name: "session-1", Value: "abc-1"})
|
|
||||||
return req
|
|
||||||
},
|
|
||||||
parameters: map[string]map[string]string{
|
|
||||||
"header": {"My-Header-1": "true", "My-Header-2": "true"},
|
|
||||||
"cookie": {"session-1": "true", "session-2": "true"},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "optional header and cookie not provided partially",
|
|
||||||
prepareRequest: func() *http.Request {
|
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
|
||||||
req.Header.Set("My-Header-1", "Header-Value-1")
|
|
||||||
req.AddCookie(&http.Cookie{Name: "session-1", Value: "abc-1"})
|
|
||||||
return req
|
|
||||||
},
|
|
||||||
parameters: map[string]map[string]string{
|
|
||||||
"header": {"My-Header-1": "true", "My-Header-2": "false"},
|
|
||||||
"cookie": {"session-1": "true", "session-2": "false"},
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "optional header and cookie not provided",
|
|
||||||
prepareRequest: func() *http.Request {
|
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
|
||||||
return req
|
|
||||||
},
|
|
||||||
parameters: map[string]map[string]string{
|
|
||||||
"header": {"My-Header-1": "false", "My-Header-2": "false"},
|
|
||||||
"cookie": {"session-1": "false", "session-2": "false"},
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no headers or cookies required",
|
|
||||||
prepareRequest: func() *http.Request {
|
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
|
||||||
return req
|
|
||||||
},
|
|
||||||
parameters: map[string]map[string]string{},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
err := validateHeadersAndCookies(tt.prepareRequest(), tt.parameters)
|
|
||||||
if tt.wantErr {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,13 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: Add stream support in the future.
|
|
||||||
|
|
||||||
func (c *Client) fnStream(context.Context, client.Request, ...client.CallOption) (client.Stream, error) {
|
|
||||||
panic("not implemented")
|
|
||||||
}
|
|
@@ -1,273 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/client"
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
|
||||||
"go.unistack.org/micro/v4/errors"
|
|
||||||
"go.unistack.org/micro/v4/logger"
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
"go.unistack.org/micro/v4/selector"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Client) fnCall(ctx context.Context, req client.Request, rsp any, opts ...client.CallOption) error {
|
|
||||||
// make a copy of call opts
|
|
||||||
callOpts := c.opts.CallOptions
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(&callOpts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if we already have a deadline
|
|
||||||
d, ok := ctx.Deadline()
|
|
||||||
if !ok {
|
|
||||||
var cancel context.CancelFunc
|
|
||||||
// no deadline so we create a new one
|
|
||||||
ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout)
|
|
||||||
defer cancel()
|
|
||||||
} else {
|
|
||||||
// got a deadline so no need to setup context,
|
|
||||||
// but we need to set the timeout we pass along
|
|
||||||
opt := client.WithRequestTimeout(time.Until(d))
|
|
||||||
opt(&callOpts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// should we noop right here?
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// make copy of call method
|
|
||||||
hcall := c.call
|
|
||||||
|
|
||||||
// use the router passed as a call option, or fallback to the rpc clients router
|
|
||||||
if callOpts.Router == nil {
|
|
||||||
callOpts.Router = c.opts.Router
|
|
||||||
}
|
|
||||||
|
|
||||||
if callOpts.Selector == nil {
|
|
||||||
callOpts.Selector = c.opts.Selector
|
|
||||||
}
|
|
||||||
|
|
||||||
// inject proxy address
|
|
||||||
// TODO: don't even bother using Lookup/Select in this case
|
|
||||||
if len(c.opts.Proxy) > 0 {
|
|
||||||
callOpts.Address = []string{c.opts.Proxy}
|
|
||||||
}
|
|
||||||
|
|
||||||
var next selector.Next
|
|
||||||
|
|
||||||
call := func(i int) error {
|
|
||||||
// call backoff first. Someone may want an initial start delay
|
|
||||||
t, err := callOpts.Backoff(ctx, req, i)
|
|
||||||
if err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", "%+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// only sleep if greater than 0
|
|
||||||
if t.Seconds() > 0 {
|
|
||||||
time.Sleep(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
if next == nil {
|
|
||||||
var routes []string
|
|
||||||
// lookup the route to send the reques to
|
|
||||||
// TODO apply any filtering here
|
|
||||||
routes, err = c.opts.Lookup(ctx, req, callOpts)
|
|
||||||
if err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", "%+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// balance the list of nodes
|
|
||||||
next, err = callOpts.Selector.Select(routes)
|
|
||||||
if err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", "%+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node := next()
|
|
||||||
|
|
||||||
// make the call
|
|
||||||
err = hcall(ctx, node, req, rsp, callOpts)
|
|
||||||
|
|
||||||
// record the result of the call to inform future routing decisions
|
|
||||||
if verr := c.opts.Selector.Record(node, err); verr != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", "%+v", verr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// try and transform the error to micro error
|
|
||||||
if verr, ok := err.(*errors.Error); ok {
|
|
||||||
return verr
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := make(chan error, callOpts.Retries)
|
|
||||||
var gerr error
|
|
||||||
|
|
||||||
for i := 0; i <= callOpts.Retries; i++ {
|
|
||||||
go func() {
|
|
||||||
ch <- call(i)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
|
||||||
case err := <-ch:
|
|
||||||
// if the call succeeded lets bail early
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
retry, rerr := callOpts.Retry(ctx, req, i, err)
|
|
||||||
if rerr != nil {
|
|
||||||
return rerr
|
|
||||||
}
|
|
||||||
|
|
||||||
if !retry {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
gerr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gerr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) call(ctx context.Context, addr string, req client.Request, rsp any, opts client.CallOptions) error {
|
|
||||||
ct := req.ContentType()
|
|
||||||
if len(opts.ContentType) > 0 {
|
|
||||||
ct = opts.ContentType
|
|
||||||
}
|
|
||||||
|
|
||||||
cf, err := c.newCodec(ct)
|
|
||||||
if err != nil {
|
|
||||||
return errors.BadRequest("go.micro.client", "%+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hreq, err := buildHTTPRequest(ctx, addr, req.Endpoint(), ct, cf, req.Body(), opts, c.opts.Logger)
|
|
||||||
if err != nil {
|
|
||||||
return errors.BadRequest("go.micro.client", "%+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hrsp, err := c.httpClient.Do(hreq)
|
|
||||||
if err != nil {
|
|
||||||
switch err := err.(type) {
|
|
||||||
case *url.Error:
|
|
||||||
if err, ok := err.Err.(net.Error); ok && err.Timeout() {
|
|
||||||
return errors.Timeout("go.micro.client", "%+v", err)
|
|
||||||
}
|
|
||||||
case net.Error:
|
|
||||||
if err.Timeout() {
|
|
||||||
return errors.Timeout("go.micro.client", "%+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.InternalServerError("go.micro.client", "%+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer hrsp.Body.Close()
|
|
||||||
|
|
||||||
return c.parseRsp(ctx, hrsp, rsp, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newCodec(ct string) (codec.Codec, error) {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
|
|
||||||
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
|
|
||||||
ct = ct[:idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
if cf, ok := c.opts.Codecs[ct]; ok {
|
|
||||||
return cf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, codec.ErrUnknownContentType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) parseRsp(ctx context.Context, hrsp *http.Response, rsp any, opts client.CallOptions) error {
|
|
||||||
log := c.opts.Logger
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf []byte
|
|
||||||
|
|
||||||
if opts.ResponseMetadata != nil {
|
|
||||||
for k, v := range hrsp.Header {
|
|
||||||
opts.ResponseMetadata.Set(k, strings.Join(v, ","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hrsp.StatusCode == http.StatusNoContent {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ct := DefaultContentType
|
|
||||||
if htype := hrsp.Header.Get(metadata.HeaderContentType); htype != "" {
|
|
||||||
ct = htype
|
|
||||||
}
|
|
||||||
|
|
||||||
if hrsp.Body != nil {
|
|
||||||
var err error
|
|
||||||
buf, err = io.ReadAll(hrsp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", "read body: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cf, err := c.newCodec(ct)
|
|
||||||
if err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", "unknown content-type %s: %v", ct, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if log.V(logger.DebugLevel) {
|
|
||||||
log.Debug(ctx, fmt.Sprintf("response with headers: %v and body: %s", hrsp.Header, buf))
|
|
||||||
}
|
|
||||||
|
|
||||||
if hrsp.StatusCode < http.StatusBadRequest {
|
|
||||||
if err = cf.Unmarshal(buf, rsp); err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", "unmarshal response: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var mappedErr any
|
|
||||||
|
|
||||||
errMap, ok := errorMapFromOpts(opts)
|
|
||||||
if ok && errMap != nil {
|
|
||||||
mappedErr, ok = errMap[fmt.Sprintf("%d", hrsp.StatusCode)]
|
|
||||||
if !ok {
|
|
||||||
mappedErr, ok = errMap["default"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok || mappedErr == nil {
|
|
||||||
return errors.New("go.micro.client", string(buf), int32(hrsp.StatusCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = cf.Unmarshal(buf, mappedErr); err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", "unmarshal response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := mappedErr.(error); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the error map item does not implement the error interface, wrap it
|
|
||||||
return &Error{err: mappedErr}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
12
error.go
12
error.go
@@ -1,12 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// Error is used when items in the error map do not implement the error interface and need to be wrapped.
|
|
||||||
type Error struct {
|
|
||||||
err any
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
|
||||||
return fmt.Sprintf("%+v", e.err)
|
|
||||||
}
|
|
24
go.mod
24
go.mod
@@ -1,25 +1,5 @@
|
|||||||
module go.unistack.org/micro-client-http/v4
|
module go.unistack.org/micro-client-http/v4
|
||||||
|
|
||||||
go 1.23.0
|
go 1.19
|
||||||
|
|
||||||
toolchain go1.24.2
|
require go.unistack.org/micro/v4 v4.0.6
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/stretchr/testify v1.11.1
|
|
||||||
go.unistack.org/micro-codec-json/v4 v4.1.0
|
|
||||||
go.unistack.org/micro/v4 v4.1.19
|
|
||||||
google.golang.org/protobuf v1.36.9
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/matoous/go-nanoid v1.5.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
|
||||||
github.com/spf13/cast v1.9.2 // indirect
|
|
||||||
go.unistack.org/micro-proto/v4 v4.1.0 // indirect
|
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
|
|
||||||
google.golang.org/grpc v1.75.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
51
go.sum
51
go.sum
@@ -1,47 +1,4 @@
|
|||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
go.unistack.org/micro/v4 v4.0.1 h1:xo1IxbVfgh8i0eY0VeYa3cbb13u5n/Mxnp3FOgWD4Jo=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
go.unistack.org/micro/v4 v4.0.1/go.mod h1:p/J5UcSJjfHsWGT31uKoghQ5rUQZzQJBAFy+Z4+ZVMs=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
go.unistack.org/micro/v4 v4.0.6 h1:YFWvTh3VwyOd6NHYTQcf47n2TF5+p/EhpnbuBQX3qhk=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
go.unistack.org/micro/v4 v4.0.6/go.mod h1:bVEYTlPi0EsdgZZt311bIroDg9ict7ky3C87dSCCAGk=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
|
||||||
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/matoous/go-nanoid v1.5.1 h1:aCjdvTyO9LLnTIi0fgdXhOPPvOHjpXN6Ik9DaNjIct4=
|
|
||||||
github.com/matoous/go-nanoid v1.5.1/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
|
||||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
|
||||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
go.unistack.org/micro-codec-json/v4 v4.1.0 h1:iydeSkt3ee7IPU0dHHKlGN97lw+YFQasBk9rdv0woYA=
|
|
||||||
go.unistack.org/micro-codec-json/v4 v4.1.0/go.mod h1:aUg86elSlURSynTAetDAAXj/VzFDwwcg92QNrRzcvrM=
|
|
||||||
go.unistack.org/micro-proto/v4 v4.1.0 h1:qPwL2n/oqh9RE3RTTDgt28XK3QzV597VugQPaw9lKUk=
|
|
||||||
go.unistack.org/micro-proto/v4 v4.1.0/go.mod h1:ArmK7o+uFvxSY3dbJhKBBX4Pm1rhWdLEFf3LxBrMtec=
|
|
||||||
go.unistack.org/micro/v4 v4.1.19 h1:LKpmSPYvX5B9AkFD7JqMU/U06v5yEWn2bsCG/YKZtZI=
|
|
||||||
go.unistack.org/micro/v4 v4.1.19/go.mod h1:xleO2M5Yxh4s6I+RUcLrEpUjobefh+71ctrdIfn7TUs=
|
|
||||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
|
||||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
|
||||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
672
http.go
Normal file
672
http.go
Normal file
@@ -0,0 +1,672 @@
|
|||||||
|
// Package http provides a http client
|
||||||
|
package http // import "go.unistack.org/micro-client-http/v4"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/client"
|
||||||
|
"go.unistack.org/micro/v4/codec"
|
||||||
|
"go.unistack.org/micro/v4/errors"
|
||||||
|
"go.unistack.org/micro/v4/logger"
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
|
"go.unistack.org/micro/v4/options"
|
||||||
|
"go.unistack.org/micro/v4/selector"
|
||||||
|
rutil "go.unistack.org/micro/v4/util/reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DefaultContentType = "application/json"
|
||||||
|
|
||||||
|
/*
|
||||||
|
func filterLabel(r []router.Route) []router.Route {
|
||||||
|
// selector.FilterLabel("protocol", "http")
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
type httpClient struct {
|
||||||
|
httpcli *http.Client
|
||||||
|
opts client.Options
|
||||||
|
sync.RWMutex
|
||||||
|
init bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequest(ctx context.Context, log logger.Logger, addr string, req client.Request, ct string, cf codec.Codec, msg interface{}, opts client.CallOptions) (*http.Request, error) {
|
||||||
|
var tags []string
|
||||||
|
var parameters map[string]map[string]string
|
||||||
|
scheme := "http"
|
||||||
|
method := http.MethodPost
|
||||||
|
body := "*" // as like google api http annotation
|
||||||
|
host := addr
|
||||||
|
path := req.Endpoint()
|
||||||
|
|
||||||
|
u, err := url.Parse(addr)
|
||||||
|
if err == nil {
|
||||||
|
scheme = u.Scheme
|
||||||
|
path = u.Path
|
||||||
|
host = u.Host
|
||||||
|
} else {
|
||||||
|
u = &url.URL{Scheme: scheme, Path: path, Host: host}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: nestif
|
||||||
|
if opts.Context != nil {
|
||||||
|
if m, ok := opts.Context.Value(methodKey{}).(string); ok {
|
||||||
|
method = m
|
||||||
|
}
|
||||||
|
if p, ok := opts.Context.Value(pathKey{}).(string); ok {
|
||||||
|
path += p
|
||||||
|
}
|
||||||
|
if b, ok := opts.Context.Value(bodyKey{}).(string); ok {
|
||||||
|
body = b
|
||||||
|
}
|
||||||
|
if t, ok := opts.Context.Value(structTagsKey{}).([]string); ok && len(t) > 0 {
|
||||||
|
tags = t
|
||||||
|
}
|
||||||
|
if k, ok := opts.Context.Value(headerKey{}).([]string); ok && len(k) > 0 {
|
||||||
|
if parameters == nil {
|
||||||
|
parameters = make(map[string]map[string]string)
|
||||||
|
}
|
||||||
|
m, ok := parameters["header"]
|
||||||
|
if !ok {
|
||||||
|
m = make(map[string]string)
|
||||||
|
parameters["header"] = m
|
||||||
|
}
|
||||||
|
for idx := 0; idx < len(k)/2; idx += 2 {
|
||||||
|
m[k[idx]] = k[idx+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if k, ok := opts.Context.Value(cookieKey{}).([]string); ok && len(k) > 0 {
|
||||||
|
if parameters == nil {
|
||||||
|
parameters = make(map[string]map[string]string)
|
||||||
|
}
|
||||||
|
m, ok := parameters["cookie"]
|
||||||
|
if !ok {
|
||||||
|
m = make(map[string]string)
|
||||||
|
parameters["cookie"] = m
|
||||||
|
}
|
||||||
|
for idx := 0; idx < len(k)/2; idx += 2 {
|
||||||
|
m[k[idx]] = k[idx+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tags) == 0 {
|
||||||
|
switch ct {
|
||||||
|
default:
|
||||||
|
tags = append(tags, "json", "protobuf")
|
||||||
|
case "text/xml":
|
||||||
|
tags = append(tags, "xml")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
path = req.Endpoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err = u.Parse(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.BadRequest("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var nmsg interface{}
|
||||||
|
if len(u.Query()) > 0 {
|
||||||
|
path, nmsg, err = newPathRequest(u.Path+"?"+u.RawQuery, method, body, msg, tags, parameters)
|
||||||
|
} else {
|
||||||
|
path, nmsg, err = newPathRequest(u.Path, method, body, msg, tags, parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.BadRequest("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err = url.Parse(fmt.Sprintf("%s://%s%s", scheme, host, path))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.BadRequest("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookies []*http.Cookie
|
||||||
|
header := make(http.Header)
|
||||||
|
if opts.Context != nil {
|
||||||
|
if md, ok := opts.Context.Value(metadataKey{}).(metadata.Metadata); ok {
|
||||||
|
for k, v := range md {
|
||||||
|
header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.AuthToken != "" {
|
||||||
|
header.Set(metadata.HeaderAuthorization, opts.AuthToken)
|
||||||
|
}
|
||||||
|
if opts.RequestMetadata != nil {
|
||||||
|
for k, v := range opts.RequestMetadata {
|
||||||
|
header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if md, ok := metadata.FromOutgoingContext(ctx); ok {
|
||||||
|
for k, v := range md {
|
||||||
|
header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set timeout in nanoseconds
|
||||||
|
if opts.StreamTimeout > time.Duration(0) {
|
||||||
|
header.Set(metadata.HeaderTimeout, fmt.Sprintf("%d", opts.StreamTimeout))
|
||||||
|
}
|
||||||
|
if opts.RequestTimeout > time.Duration(0) {
|
||||||
|
header.Set(metadata.HeaderTimeout, fmt.Sprintf("%d", opts.RequestTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the content type for the request
|
||||||
|
header.Set(metadata.HeaderContentType, ct)
|
||||||
|
var v interface{}
|
||||||
|
|
||||||
|
for km, vm := range parameters {
|
||||||
|
for k, required := range vm {
|
||||||
|
v, err = rutil.StructFieldByPath(msg, k)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.BadRequest("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
if rutil.IsZero(v) {
|
||||||
|
if required == "true" {
|
||||||
|
return nil, errors.BadRequest("go.micro.client", fmt.Sprintf("required field %s not set", k))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch km {
|
||||||
|
case "header":
|
||||||
|
header.Set(k, fmt.Sprintf("%v", v))
|
||||||
|
case "cookie":
|
||||||
|
cookies = append(cookies, &http.Cookie{Name: k, Value: fmt.Sprintf("%v", v)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := cf.Marshal(nmsg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.BadRequest("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var hreq *http.Request
|
||||||
|
if len(b) > 0 {
|
||||||
|
hreq, err = http.NewRequestWithContext(ctx, method, u.String(), ioutil.NopCloser(bytes.NewBuffer(b)))
|
||||||
|
hreq.ContentLength = int64(len(b))
|
||||||
|
header.Set("Content-Length", fmt.Sprintf("%d", hreq.ContentLength))
|
||||||
|
} else {
|
||||||
|
hreq, err = http.NewRequestWithContext(ctx, method, u.String(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.BadRequest("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
hreq.Header = header
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
hreq.AddCookie(cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
if log.V(logger.DebugLevel) {
|
||||||
|
log.Debugf(ctx, "request %s to %s with headers %v body %s", method, u.String(), hreq.Header, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hreq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) call(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error {
|
||||||
|
ct := req.ContentType()
|
||||||
|
if len(opts.ContentType) > 0 {
|
||||||
|
ct = opts.ContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
cf, err := h.newCodec(ct)
|
||||||
|
if err != nil {
|
||||||
|
return errors.BadRequest("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
hreq, err := newRequest(ctx, h.opts.Logger, addr, req, ct, cf, req.Body(), opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// make the request
|
||||||
|
hrsp, err := h.httpcli.Do(hreq)
|
||||||
|
if err != nil {
|
||||||
|
switch err := err.(type) {
|
||||||
|
case *url.Error:
|
||||||
|
if err, ok := err.Err.(net.Error); ok && err.Timeout() {
|
||||||
|
return errors.Timeout("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
case net.Error:
|
||||||
|
if err.Timeout() {
|
||||||
|
return errors.Timeout("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
defer hrsp.Body.Close()
|
||||||
|
|
||||||
|
return h.parseRsp(ctx, hrsp, rsp, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) stream(ctx context.Context, addr string, req client.Request, opts client.CallOptions) (client.Stream, error) {
|
||||||
|
ct := req.ContentType()
|
||||||
|
if len(opts.ContentType) > 0 {
|
||||||
|
ct = opts.ContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// get codec
|
||||||
|
cf, err := h.newCodec(ct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.BadRequest("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
cc, err := (h.httpcli.Transport).(*http.Transport).DialContext(ctx, "tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.InternalServerError("go.micro.client", fmt.Sprintf("Error dialing: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &httpStream{
|
||||||
|
address: addr,
|
||||||
|
logger: h.opts.Logger,
|
||||||
|
context: ctx,
|
||||||
|
closed: make(chan bool),
|
||||||
|
opts: opts,
|
||||||
|
conn: cc,
|
||||||
|
ct: ct,
|
||||||
|
cf: cf,
|
||||||
|
reader: bufio.NewReader(cc),
|
||||||
|
request: req,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) newCodec(ct string) (codec.Codec, error) {
|
||||||
|
h.RLock()
|
||||||
|
defer h.RUnlock()
|
||||||
|
|
||||||
|
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
|
||||||
|
ct = ct[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, ok := h.opts.Codecs[ct]; ok {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, codec.ErrUnknownContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) Init(opts ...options.Option) error {
|
||||||
|
if len(opts) == 0 && h.init {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&h.opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.opts.Tracer.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := h.opts.Router.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := h.opts.Logger.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := h.opts.Meter.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) Options() client.Options {
|
||||||
|
return h.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) NewRequest(service, method string, req interface{}, opts ...options.Option) client.Request {
|
||||||
|
return newHTTPRequest(service, method, req, h.opts.ContentType, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...options.Option) error {
|
||||||
|
// make a copy of call opts
|
||||||
|
callOpts := h.opts.CallOptions
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&callOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we already have a deadline
|
||||||
|
d, ok := ctx.Deadline()
|
||||||
|
if !ok {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
// no deadline so we create a new one
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout)
|
||||||
|
defer cancel()
|
||||||
|
} else {
|
||||||
|
// got a deadline so no need to setup context
|
||||||
|
// but we need to set the timeout we pass along
|
||||||
|
if err := options.Set(&callOpts, time.Until(d), ".RequestTimeout"); err != nil {
|
||||||
|
return errors.New("go.micro.client", fmt.Sprintf("%v", err.Error()), 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// should we noop right here?
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// make copy of call method
|
||||||
|
hcall := h.call
|
||||||
|
|
||||||
|
// wrap the call in reverse
|
||||||
|
//for i := len(callOpts.CallWrappers); i > 0; i-- {
|
||||||
|
// hcall = callOpts.CallWrappers[i-1](hcall)
|
||||||
|
//}
|
||||||
|
|
||||||
|
// use the router passed as a call option, or fallback to the rpc clients router
|
||||||
|
if callOpts.Router == nil {
|
||||||
|
callOpts.Router = h.opts.Router
|
||||||
|
}
|
||||||
|
|
||||||
|
if callOpts.Selector == nil {
|
||||||
|
callOpts.Selector = h.opts.Selector
|
||||||
|
}
|
||||||
|
|
||||||
|
// inject proxy address
|
||||||
|
// TODO: don't even bother using Lookup/Select in this case
|
||||||
|
if len(h.opts.Proxy) > 0 {
|
||||||
|
callOpts.Address = []string{h.opts.Proxy}
|
||||||
|
}
|
||||||
|
|
||||||
|
var next selector.Next
|
||||||
|
|
||||||
|
// return errors.New("go.micro.client", "request timeout", 408)
|
||||||
|
call := func(i int) error {
|
||||||
|
// call backoff first. Someone may want an initial start delay
|
||||||
|
t, err := callOpts.Backoff(ctx, req, i)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// only sleep if greater than 0
|
||||||
|
if t.Seconds() > 0 {
|
||||||
|
time.Sleep(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
if next == nil {
|
||||||
|
var routes []string
|
||||||
|
// lookup the route to send the reques to
|
||||||
|
// TODO apply any filtering here
|
||||||
|
routes, err = h.opts.Lookup(ctx, req, callOpts)
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// balance the list of nodes
|
||||||
|
next, err = callOpts.Selector.Select(routes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node := next()
|
||||||
|
|
||||||
|
// make the call
|
||||||
|
err = hcall(ctx, node, req, rsp, callOpts)
|
||||||
|
// record the result of the call to inform future routing decisions
|
||||||
|
if verr := h.opts.Selector.Record(node, err); verr != nil {
|
||||||
|
return verr
|
||||||
|
}
|
||||||
|
|
||||||
|
// try and transform the error to a go-micro error
|
||||||
|
if verr, ok := err.(*errors.Error); ok {
|
||||||
|
return verr
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan error, callOpts.Retries)
|
||||||
|
var gerr error
|
||||||
|
|
||||||
|
for i := 0; i <= callOpts.Retries; i++ {
|
||||||
|
go func() {
|
||||||
|
ch <- call(i)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||||
|
case err := <-ch:
|
||||||
|
// if the call succeeded lets bail early
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
retry, rerr := callOpts.Retry(ctx, req, i, err)
|
||||||
|
if rerr != nil {
|
||||||
|
return rerr
|
||||||
|
}
|
||||||
|
|
||||||
|
if !retry {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gerr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gerr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) Stream(ctx context.Context, req client.Request, opts ...options.Option) (client.Stream, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// make a copy of call opts
|
||||||
|
callOpts := h.opts.CallOptions
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&callOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we already have a deadline
|
||||||
|
d, ok := ctx.Deadline()
|
||||||
|
if !ok && callOpts.StreamTimeout > time.Duration(0) {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
// no deadline so we create a new one
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, callOpts.StreamTimeout)
|
||||||
|
defer cancel()
|
||||||
|
} else {
|
||||||
|
// got a deadline so no need to setup context
|
||||||
|
// but we need to set the timeout we pass along
|
||||||
|
if err = options.Set(&callOpts, time.Until(d), ".StreamTimeout"); err != nil {
|
||||||
|
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", err.Error()), 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// should we noop right here?
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// make copy of call method
|
||||||
|
hstream := h.stream
|
||||||
|
// wrap the call in reverse
|
||||||
|
for i := len(callOpts.CallWrappers); i > 0; i-- {
|
||||||
|
hstream = callOpts.CallWrappers[i-1](hstream)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// use the router passed as a call option, or fallback to the rpc clients router
|
||||||
|
if callOpts.Router == nil {
|
||||||
|
callOpts.Router = h.opts.Router
|
||||||
|
}
|
||||||
|
|
||||||
|
if callOpts.Selector == nil {
|
||||||
|
callOpts.Selector = h.opts.Selector
|
||||||
|
}
|
||||||
|
|
||||||
|
// inject proxy address
|
||||||
|
// TODO: don't even bother using Lookup/Select in this case
|
||||||
|
if len(h.opts.Proxy) > 0 {
|
||||||
|
callOpts.Address = []string{h.opts.Proxy}
|
||||||
|
}
|
||||||
|
|
||||||
|
var next selector.Next
|
||||||
|
|
||||||
|
call := func(i int) (client.Stream, error) {
|
||||||
|
// call backoff first. Someone may want an initial start delay
|
||||||
|
t, cerr := callOpts.Backoff(ctx, req, i)
|
||||||
|
if cerr != nil {
|
||||||
|
return nil, errors.InternalServerError("go.micro.client", cerr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// only sleep if greater than 0
|
||||||
|
if t.Seconds() > 0 {
|
||||||
|
time.Sleep(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
if next == nil {
|
||||||
|
var routes []string
|
||||||
|
// lookup the route to send the reques to
|
||||||
|
// TODO apply any filtering here
|
||||||
|
routes, err = h.opts.Lookup(ctx, req, callOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// balance the list of nodes
|
||||||
|
next, err = callOpts.Selector.Select(routes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node := next()
|
||||||
|
|
||||||
|
stream, cerr := h.stream(ctx, node, req, callOpts)
|
||||||
|
|
||||||
|
// record the result of the call to inform future routing decisions
|
||||||
|
if verr := h.opts.Selector.Record(node, cerr); verr != nil {
|
||||||
|
return nil, verr
|
||||||
|
}
|
||||||
|
|
||||||
|
// try and transform the error to a go-micro error
|
||||||
|
if verr, ok := cerr.(*errors.Error); ok {
|
||||||
|
return nil, verr
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream, cerr
|
||||||
|
}
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
stream client.Stream
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan response, callOpts.Retries)
|
||||||
|
var grr error
|
||||||
|
|
||||||
|
for i := 0; i <= callOpts.Retries; i++ {
|
||||||
|
go func() {
|
||||||
|
s, cerr := call(i)
|
||||||
|
ch <- response{s, cerr}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||||
|
case rsp := <-ch:
|
||||||
|
// if the call succeeded lets bail early
|
||||||
|
if rsp.err == nil {
|
||||||
|
return rsp.stream, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
retry, rerr := callOpts.Retry(ctx, req, i, err)
|
||||||
|
if rerr != nil {
|
||||||
|
return nil, rerr
|
||||||
|
}
|
||||||
|
|
||||||
|
if !retry {
|
||||||
|
return nil, rsp.err
|
||||||
|
}
|
||||||
|
|
||||||
|
grr = rsp.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, grr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) String() string {
|
||||||
|
return "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) Name() string {
|
||||||
|
return h.opts.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(opts ...options.Option) client.Client {
|
||||||
|
options := client.NewOptions(opts...)
|
||||||
|
|
||||||
|
if len(options.ContentType) == 0 {
|
||||||
|
options.ContentType = DefaultContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
rc := &httpClient{
|
||||||
|
opts: options,
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialer func(context.Context, string) (net.Conn, error)
|
||||||
|
if v, ok := options.Context.Value(httpDialerKey{}).(*net.Dialer); ok {
|
||||||
|
dialer = func(ctx context.Context, addr string) (net.Conn, error) {
|
||||||
|
return v.DialContext(ctx, "tcp", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.ContextDialer != nil {
|
||||||
|
dialer = options.ContextDialer
|
||||||
|
}
|
||||||
|
if dialer == nil {
|
||||||
|
dialer = func(ctx context.Context, addr string) (net.Conn, error) {
|
||||||
|
return (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext(ctx, "tcp", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if httpcli, ok := options.Context.Value(httpClientKey{}).(*http.Client); ok {
|
||||||
|
rc.httpcli = httpcli
|
||||||
|
} else {
|
||||||
|
// TODO customTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
tr := &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return dialer(ctx, addr)
|
||||||
|
},
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
MaxConnsPerHost: 100,
|
||||||
|
MaxIdleConns: 20,
|
||||||
|
IdleConnTimeout: 60 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
TLSClientConfig: options.TLSConfig,
|
||||||
|
}
|
||||||
|
rc.httpcli = &http.Client{Transport: tr}
|
||||||
|
}
|
||||||
|
c := client.Client(rc)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
81
http_test.go
Normal file
81
http_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Field1 string `json:"field1"`
|
||||||
|
ClientID string
|
||||||
|
Field2 string
|
||||||
|
Field3 int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathWithHeader(t *testing.T) {
|
||||||
|
req := &Request{Name: "vtolstov", Field1: "field1", ClientID: "1234567890"}
|
||||||
|
p, m, err := newPathRequest(
|
||||||
|
"/api/v1/test?Name={name}&Field1={field1}",
|
||||||
|
"POST",
|
||||||
|
"*",
|
||||||
|
req,
|
||||||
|
nil,
|
||||||
|
map[string]map[string]string{"header": {"ClientID": "true"}},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
u, err := url.Parse(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if m != nil {
|
||||||
|
t.Fatal("new struct must be nil")
|
||||||
|
}
|
||||||
|
if u.Query().Get("Name") != "vtolstov" || u.Query().Get("Field1") != "field1" {
|
||||||
|
t.Fatalf("invalid values %v", u.Query())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathValues(t *testing.T) {
|
||||||
|
req := &Request{Name: "vtolstov", Field1: "field1"}
|
||||||
|
p, m, err := newPathRequest("/api/v1/test?Name={name}&Field1={field1}", "POST", "*", req, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
u, err := url.Parse(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_ = m
|
||||||
|
if u.Query().Get("Name") != "vtolstov" || u.Query().Get("Field1") != "field1" {
|
||||||
|
t.Fatalf("invalid values %v", u.Query())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidPath(t *testing.T) {
|
||||||
|
req := &Request{Name: "vtolstov", Field1: "field1", Field2: "field2", Field3: 10}
|
||||||
|
p, m, err := newPathRequest("/api/v1/{name}/list", "GET", "", req, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
u, err := url.Parse(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_ = m
|
||||||
|
parts := strings.Split(u.RawQuery, "&")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
t.Fatalf("invalid path: %v", parts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidPath(t *testing.T) {
|
||||||
|
req := &Request{Name: "vtolstov", Field1: "field1", Field2: "field2", Field3: 10}
|
||||||
|
_, _, err := newPathRequest("/api/v1/{xname}/list", "GET", "", req, nil, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("path param must not be filled")
|
||||||
|
}
|
||||||
|
}
|
192
options.go
192
options.go
@@ -1,148 +1,126 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/client"
|
"go.unistack.org/micro/v4/metadata"
|
||||||
|
"go.unistack.org/micro/v4/options"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --------------------------------------------- HTTPClient option -----------------------------------------------------
|
var (
|
||||||
|
// DefaultPoolMaxStreams maximum streams on a connectioin
|
||||||
|
// (20)
|
||||||
|
DefaultPoolMaxStreams = 20
|
||||||
|
|
||||||
|
// DefaultPoolMaxIdle maximum idle conns of a pool
|
||||||
|
// (50)
|
||||||
|
DefaultPoolMaxIdle = 50
|
||||||
|
|
||||||
|
// DefaultMaxRecvMsgSize maximum message that client can receive
|
||||||
|
// (4 MB).
|
||||||
|
DefaultMaxRecvMsgSize = 1024 * 1024 * 4
|
||||||
|
|
||||||
|
// DefaultMaxSendMsgSize maximum message that client can send
|
||||||
|
// (4 MB).
|
||||||
|
DefaultMaxSendMsgSize = 1024 * 1024 * 4
|
||||||
|
)
|
||||||
|
|
||||||
|
type poolMaxStreams struct{}
|
||||||
|
|
||||||
|
// PoolMaxStreams maximum streams on a connectioin
|
||||||
|
func PoolMaxStreams(n int) options.Option {
|
||||||
|
return options.ContextOption(poolMaxStreams{}, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
type poolMaxIdle struct{}
|
||||||
|
|
||||||
|
// PoolMaxIdle maximum idle conns of a pool
|
||||||
|
func PoolMaxIdle(d int) options.Option {
|
||||||
|
return options.ContextOption(poolMaxIdle{}, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
type maxRecvMsgSizeKey struct{}
|
||||||
|
|
||||||
|
// MaxRecvMsgSize set the maximum size of message that client can receive.
|
||||||
|
func MaxRecvMsgSize(s int) options.Option {
|
||||||
|
return options.ContextOption(maxRecvMsgSizeKey{}, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
type maxSendMsgSizeKey struct{}
|
||||||
|
|
||||||
|
// MaxSendMsgSize set the maximum size of message that client can send.
|
||||||
|
func MaxSendMsgSize(s int) options.Option {
|
||||||
|
return options.ContextOption(maxSendMsgSizeKey{}, s)
|
||||||
|
}
|
||||||
|
|
||||||
type httpClientKey struct{}
|
type httpClientKey struct{}
|
||||||
|
|
||||||
func HTTPClient(c *http.Client) client.Option {
|
// nolint: golint
|
||||||
return client.SetOption(httpClientKey{}, c)
|
// HTTPClient pass http.Client option to client Call
|
||||||
|
func HTTPClient(c *http.Client) options.Option {
|
||||||
|
return options.ContextOption(httpClientKey{}, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpClientFromOpts(opts client.Options) (*http.Client, bool) {
|
|
||||||
httpClient, ok := opts.Context.Value(httpClientKey{}).(*http.Client)
|
|
||||||
return httpClient, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultHTTPClient(
|
|
||||||
dialer func(ctx context.Context, addr string) (net.Conn, error),
|
|
||||||
tlsConfig *tls.Config,
|
|
||||||
) *http.Client {
|
|
||||||
tr := &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return dialer(ctx, addr)
|
|
||||||
},
|
|
||||||
ForceAttemptHTTP2: true,
|
|
||||||
MaxConnsPerHost: 100,
|
|
||||||
MaxIdleConns: 20,
|
|
||||||
IdleConnTimeout: 60 * time.Second,
|
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
|
||||||
TLSClientConfig: tlsConfig,
|
|
||||||
}
|
|
||||||
return &http.Client{Transport: tr}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------- HTTPDialer option -----------------------------------------------------
|
|
||||||
type httpDialerKey struct{}
|
type httpDialerKey struct{}
|
||||||
|
|
||||||
func HTTPDialer(d *net.Dialer) client.Option {
|
// nolint: golint
|
||||||
return client.SetOption(httpDialerKey{}, d)
|
// HTTPDialer pass net.Dialer option to client
|
||||||
|
func HTTPDialer(d *net.Dialer) options.Option {
|
||||||
|
return options.ContextOption(httpDialerKey{}, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
func httpDialerFromOpts(opts client.Options) (dialerFunc func(context.Context, string) (net.Conn, error), ok bool) {
|
|
||||||
var d *net.Dialer
|
|
||||||
|
|
||||||
if d, ok = opts.Context.Value(httpDialerKey{}).(*net.Dialer); ok {
|
|
||||||
dialerFunc = func(ctx context.Context, addr string) (net.Conn, error) {
|
|
||||||
return d.DialContext(ctx, "tcp", addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.ContextDialer != nil {
|
|
||||||
dialerFunc, ok = opts.ContextDialer, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return dialerFunc, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultHTTPDialer() func(ctx context.Context, addr string) (net.Conn, error) {
|
|
||||||
return func(ctx context.Context, addr string) (net.Conn, error) {
|
|
||||||
d := &net.Dialer{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}
|
|
||||||
return d.DialContext(ctx, "tcp", addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------- Method option -------------------------------------------------------
|
|
||||||
type methodKey struct{}
|
type methodKey struct{}
|
||||||
|
|
||||||
func Method(m string) client.CallOption {
|
// Method pass method option to client Call
|
||||||
return client.SetCallOption(methodKey{}, m)
|
func Method(m string) options.Option {
|
||||||
|
return options.ContextOption(methodKey{}, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func methodFromOpts(opts client.CallOptions) (string, bool) {
|
|
||||||
m, ok := opts.Context.Value(methodKey{}).(string)
|
|
||||||
return m, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------ Path option --------------------------------------------------------
|
|
||||||
type pathKey struct{}
|
type pathKey struct{}
|
||||||
|
|
||||||
func Path(p string) client.CallOption {
|
// Path spcecifies path option to client Call
|
||||||
return client.SetCallOption(pathKey{}, p)
|
func Path(p string) options.Option {
|
||||||
|
return options.ContextOption(pathKey{}, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathFromOpts(opts client.CallOptions) (string, bool) {
|
|
||||||
p, ok := opts.Context.Value(pathKey{}).(string)
|
|
||||||
return p, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------ Body option --------------------------------------------------------
|
|
||||||
type bodyKey struct{}
|
type bodyKey struct{}
|
||||||
|
|
||||||
func Body(b string) client.CallOption {
|
// Body specifies body option to client Call
|
||||||
return client.SetCallOption(bodyKey{}, b)
|
func Body(b string) options.Option {
|
||||||
|
return options.ContextOption(bodyKey{}, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func bodyFromOpts(opts client.CallOptions) (string, bool) {
|
|
||||||
b, ok := opts.Context.Value(bodyKey{}).(string)
|
|
||||||
return b, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------- ErrorMap option ------------------------------------------------------
|
|
||||||
type errorMapKey struct{}
|
type errorMapKey struct{}
|
||||||
|
|
||||||
func ErrorMap(m map[string]any) client.CallOption {
|
func ErrorMap(m map[string]interface{}) options.Option {
|
||||||
return client.SetCallOption(errorMapKey{}, m)
|
return options.ContextOption(errorMapKey{}, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorMapFromOpts(opts client.CallOptions) (map[string]any, bool) {
|
type structTagsKey struct{}
|
||||||
errMap, ok := opts.Context.Value(errorMapKey{}).(map[string]any)
|
|
||||||
return errMap, ok
|
// StructTags pass tags slice option to client Call
|
||||||
|
func StructTags(tags []string) options.Option {
|
||||||
|
return options.ContextOption(structTagsKey{}, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
type metadataKey struct{}
|
||||||
|
|
||||||
|
// Metadata pass metadata to client Call
|
||||||
|
func Metadata(md metadata.Metadata) options.Option {
|
||||||
|
return options.ContextOption(metadataKey{}, md)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------ Cookie option ------------------------------------------------------
|
|
||||||
type cookieKey struct{}
|
type cookieKey struct{}
|
||||||
|
|
||||||
func Cookie(cookies ...string) client.CallOption {
|
// Cookie pass cookie to client Call
|
||||||
return client.SetCallOption(cookieKey{}, cookies)
|
func Cookie(cookies ...string) options.Option {
|
||||||
|
return options.ContextOption(cookieKey{}, cookies)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cookieFromOpts(opts client.CallOptions) ([]string, bool) {
|
|
||||||
c, ok := opts.Context.Value(cookieKey{}).([]string)
|
|
||||||
return c, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------ Header option ------------------------------------------------------
|
|
||||||
type headerKey struct{}
|
type headerKey struct{}
|
||||||
|
|
||||||
func Header(headers ...string) client.CallOption {
|
// Header pass cookie to client Call
|
||||||
return client.SetCallOption(headerKey{}, headers)
|
func Header(headers ...string) options.Option {
|
||||||
}
|
return options.ContextOption(headerKey{}, headers)
|
||||||
|
|
||||||
func headerFromOpts(opts client.CallOptions) ([]string, bool) {
|
|
||||||
h, ok := opts.Context.Value(headerKey{}).([]string)
|
|
||||||
return h, ok
|
|
||||||
}
|
}
|
||||||
|
35
request.go
35
request.go
@@ -3,15 +3,36 @@ package http
|
|||||||
import (
|
import (
|
||||||
"go.unistack.org/micro/v4/client"
|
"go.unistack.org/micro/v4/client"
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
|
"go.unistack.org/micro/v4/options"
|
||||||
)
|
)
|
||||||
|
|
||||||
type httpRequest struct {
|
type httpRequest struct {
|
||||||
service string
|
service string
|
||||||
method string
|
method string
|
||||||
request any
|
contentType string
|
||||||
|
request interface{}
|
||||||
opts client.RequestOptions
|
opts client.RequestOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newHTTPRequest(service, method string, request interface{}, contentType string, opts ...options.Option) client.Request {
|
||||||
|
options := client.NewRequestOptions(opts...)
|
||||||
|
if len(options.ContentType) == 0 {
|
||||||
|
options.ContentType = contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
return &httpRequest{
|
||||||
|
service: service,
|
||||||
|
method: method,
|
||||||
|
request: request,
|
||||||
|
contentType: options.ContentType,
|
||||||
|
opts: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpRequest) ContentType() string {
|
||||||
|
return h.contentType
|
||||||
|
}
|
||||||
|
|
||||||
func (h *httpRequest) Service() string {
|
func (h *httpRequest) Service() string {
|
||||||
return h.service
|
return h.service
|
||||||
}
|
}
|
||||||
@@ -24,18 +45,14 @@ func (h *httpRequest) Endpoint() string {
|
|||||||
return h.method
|
return h.method
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *httpRequest) ContentType() string {
|
|
||||||
return h.opts.ContentType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpRequest) Body() any {
|
|
||||||
return h.request
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpRequest) Codec() codec.Codec {
|
func (h *httpRequest) Codec() codec.Codec {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *httpRequest) Body() interface{} {
|
||||||
|
return h.request
|
||||||
|
}
|
||||||
|
|
||||||
func (h *httpRequest) Stream() bool {
|
func (h *httpRequest) Stream() bool {
|
||||||
return h.opts.Stream
|
return h.opts.Stream
|
||||||
}
|
}
|
||||||
|
175
stream.go
Normal file
175
stream.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/client"
|
||||||
|
"go.unistack.org/micro/v4/codec"
|
||||||
|
"go.unistack.org/micro/v4/errors"
|
||||||
|
"go.unistack.org/micro/v4/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implements the streamer interface
|
||||||
|
type httpStream struct {
|
||||||
|
err error
|
||||||
|
conn net.Conn
|
||||||
|
cf codec.Codec
|
||||||
|
context context.Context
|
||||||
|
logger logger.Logger
|
||||||
|
request client.Request
|
||||||
|
closed chan bool
|
||||||
|
reader *bufio.Reader
|
||||||
|
address string
|
||||||
|
ct string
|
||||||
|
opts client.CallOptions
|
||||||
|
sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var errShutdown = fmt.Errorf("connection is shut down")
|
||||||
|
|
||||||
|
func (h *httpStream) isClosed() bool {
|
||||||
|
select {
|
||||||
|
case <-h.closed:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpStream) Context() context.Context {
|
||||||
|
return h.context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpStream) Request() client.Request {
|
||||||
|
return h.request
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpStream) Response() client.Response {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpStream) SendMsg(msg interface{}) error {
|
||||||
|
return h.Send(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpStream) Send(msg interface{}) error {
|
||||||
|
h.Lock()
|
||||||
|
defer h.Unlock()
|
||||||
|
|
||||||
|
if h.isClosed() {
|
||||||
|
h.err = errShutdown
|
||||||
|
return errShutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
hreq, err := newRequest(h.context, h.logger, h.address, h.request, h.ct, h.cf, msg, h.opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hreq.Write(h.conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpStream) RecvMsg(msg interface{}) error {
|
||||||
|
return h.Recv(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpStream) Recv(msg interface{}) error {
|
||||||
|
h.Lock()
|
||||||
|
defer h.Unlock()
|
||||||
|
|
||||||
|
if h.isClosed() {
|
||||||
|
h.err = errShutdown
|
||||||
|
return errShutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
hrsp, err := http.ReadResponse(h.reader, new(http.Request))
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
defer hrsp.Body.Close()
|
||||||
|
|
||||||
|
return h.parseRsp(h.context, h.logger, hrsp, h.cf, msg, h.opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpStream) Error() error {
|
||||||
|
h.RLock()
|
||||||
|
defer h.RUnlock()
|
||||||
|
return h.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpStream) CloseSend() error {
|
||||||
|
return h.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpStream) Close() error {
|
||||||
|
select {
|
||||||
|
case <-h.closed:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
close(h.closed)
|
||||||
|
return h.conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpStream) parseRsp(ctx context.Context, log logger.Logger, hrsp *http.Response, cf codec.Codec, rsp interface{}, opts client.CallOptions) error {
|
||||||
|
var err error
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
// fast path return
|
||||||
|
if hrsp.StatusCode == http.StatusNoContent {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
err = ctx.Err()
|
||||||
|
default:
|
||||||
|
if hrsp.Body != nil {
|
||||||
|
buf, err = io.ReadAll(hrsp.Body)
|
||||||
|
if err != nil {
|
||||||
|
if log.V(logger.ErrorLevel) {
|
||||||
|
log.Errorf(ctx, "failed to read body: %v", err)
|
||||||
|
}
|
||||||
|
return errors.InternalServerError("go.micro.client", string(buf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if log.V(logger.DebugLevel) {
|
||||||
|
log.Debugf(ctx, "response %s with %v", buf, hrsp.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hrsp.StatusCode < 400 {
|
||||||
|
if err = cf.Unmarshal(buf, rsp); err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rerr interface{}
|
||||||
|
errmap, ok := opts.Context.Value(errorMapKey{}).(map[string]interface{})
|
||||||
|
if ok && errmap != nil {
|
||||||
|
if rerr, ok = errmap[fmt.Sprintf("%d", hrsp.StatusCode)].(error); !ok {
|
||||||
|
rerr, ok = errmap["default"].(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok || rerr == nil {
|
||||||
|
return errors.New("go.micro.client", string(buf), int32(hrsp.StatusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cerr := cf.Unmarshal(buf, rerr); cerr != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", cerr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err, ok = rerr.(error); !ok {
|
||||||
|
err = &Error{rerr}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
356
util.go
Normal file
356
util.go
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/client"
|
||||||
|
"go.unistack.org/micro/v4/errors"
|
||||||
|
"go.unistack.org/micro/v4/logger"
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
|
rutil "go.unistack.org/micro/v4/util/reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
templateCache = make(map[string][]string)
|
||||||
|
mu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error struct holds error
|
||||||
|
type Error struct {
|
||||||
|
err interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error func for error interface
|
||||||
|
func (err *Error) Error() string {
|
||||||
|
return fmt.Sprintf("%v", err.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetError(err error) interface{} {
|
||||||
|
if rerr, ok := err.(*Error); ok {
|
||||||
|
return rerr.err
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPathRequest(path string, method string, body string, msg interface{}, tags []string, parameters map[string]map[string]string) (string, interface{}, error) {
|
||||||
|
// parse via https://github.com/googleapis/googleapis/blob/master/google/api/http.proto definition
|
||||||
|
tpl, err := newTemplate(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tpl) > 0 && msg == nil {
|
||||||
|
return "", nil, fmt.Errorf("nil message but path params requested: %v", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldsmapskip := make(map[string]struct{})
|
||||||
|
fieldsmap := make(map[string]string, len(tpl))
|
||||||
|
for _, v := range tpl {
|
||||||
|
var vs, ve int
|
||||||
|
for i := 0; i < len(v); i++ {
|
||||||
|
switch v[i] {
|
||||||
|
case '{':
|
||||||
|
vs = i + 1
|
||||||
|
case '}':
|
||||||
|
ve = i
|
||||||
|
}
|
||||||
|
if ve != 0 {
|
||||||
|
fieldsmap[v[vs:ve]] = ""
|
||||||
|
vs = 0
|
||||||
|
ve = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nmsg, err := rutil.Zero(msg)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// we cant switch on message and use proto helpers, to avoid dependency to protobuf
|
||||||
|
tmsg := reflect.ValueOf(msg)
|
||||||
|
if tmsg.Kind() == reflect.Ptr {
|
||||||
|
tmsg = tmsg.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
tnmsg := reflect.ValueOf(nmsg)
|
||||||
|
if tnmsg.Kind() == reflect.Ptr {
|
||||||
|
tnmsg = tnmsg.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
values := url.Values{}
|
||||||
|
// copy cycle
|
||||||
|
for i := 0; i < tmsg.NumField(); i++ {
|
||||||
|
val := tmsg.Field(i)
|
||||||
|
if val.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fld := tmsg.Type().Field(i)
|
||||||
|
// Skip unexported fields.
|
||||||
|
if fld.PkgPath != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
/* check for empty PkgPath can be replaced with new method IsExported
|
||||||
|
if !fld.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
t := &tag{}
|
||||||
|
for _, tn := range tags {
|
||||||
|
ts, ok := fld.Tag.Lookup(tn)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tp := strings.Split(ts, ",")
|
||||||
|
// special
|
||||||
|
switch tn {
|
||||||
|
case "protobuf": // special
|
||||||
|
for _, p := range tp {
|
||||||
|
if idx := strings.Index(p, "name="); idx > 0 {
|
||||||
|
t = &tag{key: tn, name: p[idx:]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t = &tag{key: tn, name: tp[0]}
|
||||||
|
}
|
||||||
|
if t.name != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cname := t.name
|
||||||
|
if cname == "" {
|
||||||
|
cname = fld.Name
|
||||||
|
// fallback to lowercase
|
||||||
|
t.name = strings.ToLower(fld.Name)
|
||||||
|
}
|
||||||
|
if _, ok := parameters["header"][cname]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := parameters["cookie"][cname]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !val.IsValid() || val.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: gocritic, nestif
|
||||||
|
if _, ok := fieldsmap[t.name]; ok {
|
||||||
|
switch val.Type().Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
for idx := 0; idx < val.Len(); idx++ {
|
||||||
|
values.Add(t.name, getParam(val.Index(idx)))
|
||||||
|
}
|
||||||
|
fieldsmapskip[t.name] = struct{}{}
|
||||||
|
default:
|
||||||
|
fieldsmap[t.name] = getParam(val)
|
||||||
|
}
|
||||||
|
} else if (body == "*" || body == t.name) && method != http.MethodGet {
|
||||||
|
if tnmsg.Field(i).CanSet() {
|
||||||
|
tnmsg.Field(i).Set(val)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if val.Type().Kind() == reflect.Slice {
|
||||||
|
for idx := 0; idx < val.Len(); idx++ {
|
||||||
|
values.Add(t.name, getParam(val.Index(idx)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
values.Add(t.name, getParam(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check not filled stuff
|
||||||
|
for k, v := range fieldsmap {
|
||||||
|
_, ok := fieldsmapskip[k]
|
||||||
|
if !ok && v == "" {
|
||||||
|
return "", nil, fmt.Errorf("path param %s not filled", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
for _, fld := range tpl {
|
||||||
|
_, _ = b.WriteRune('/')
|
||||||
|
// nolint: nestif
|
||||||
|
var vs, ve, vf int
|
||||||
|
var pholder bool
|
||||||
|
for i := 0; i < len(fld); i++ {
|
||||||
|
switch fld[i] {
|
||||||
|
case '{':
|
||||||
|
vs = i + 1
|
||||||
|
case '}':
|
||||||
|
ve = i
|
||||||
|
}
|
||||||
|
// nolint: nestif
|
||||||
|
if vs > 0 && ve != 0 {
|
||||||
|
if vm, ok := fieldsmap[fld[vs:ve]]; ok {
|
||||||
|
if vm != "" {
|
||||||
|
_, _ = b.WriteString(fld[vf : vs-1])
|
||||||
|
_, _ = b.WriteString(vm)
|
||||||
|
vf = ve + 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, _ = b.WriteString(fld)
|
||||||
|
}
|
||||||
|
vs = 0
|
||||||
|
ve = 0
|
||||||
|
pholder = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pholder {
|
||||||
|
_, _ = b.WriteString(fld)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(values) > 0 {
|
||||||
|
_, _ = b.WriteRune('?')
|
||||||
|
_, _ = b.WriteString(values.Encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
if rutil.IsZero(nmsg) {
|
||||||
|
return b.String(), nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String(), nmsg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTemplate(path string) ([]string, error) {
|
||||||
|
if len(path) == 0 || path[0] != '/' {
|
||||||
|
return nil, fmt.Errorf("path must starts with /")
|
||||||
|
}
|
||||||
|
mu.RLock()
|
||||||
|
tpl, ok := templateCache[path]
|
||||||
|
if ok {
|
||||||
|
mu.RUnlock()
|
||||||
|
return tpl, nil
|
||||||
|
}
|
||||||
|
mu.RUnlock()
|
||||||
|
|
||||||
|
tpl = strings.Split(path[1:], "/")
|
||||||
|
mu.Lock()
|
||||||
|
templateCache[path] = tpl
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
return tpl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpClient) parseRsp(ctx context.Context, hrsp *http.Response, rsp interface{}, opts client.CallOptions) error {
|
||||||
|
var err error
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
// fast path return
|
||||||
|
if hrsp.StatusCode == http.StatusNoContent {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.ResponseMetadata != nil {
|
||||||
|
*opts.ResponseMetadata = metadata.New(len(hrsp.Header))
|
||||||
|
for k, v := range hrsp.Header {
|
||||||
|
opts.ResponseMetadata.Set(k, strings.Join(v, ","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
err = ctx.Err()
|
||||||
|
default:
|
||||||
|
ct := DefaultContentType
|
||||||
|
|
||||||
|
if htype := hrsp.Header.Get("Content-Type"); htype != "" {
|
||||||
|
ct = htype
|
||||||
|
}
|
||||||
|
|
||||||
|
if hrsp.Body != nil {
|
||||||
|
buf, err = io.ReadAll(hrsp.Body)
|
||||||
|
if err != nil {
|
||||||
|
if h.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
h.opts.Logger.Errorf(ctx, "failed to read body: %v", err)
|
||||||
|
}
|
||||||
|
return errors.InternalServerError("go.micro.client", string(buf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cf, cerr := h.newCodec(ct)
|
||||||
|
if cerr != nil {
|
||||||
|
if h.opts.Logger.V(logger.DebugLevel) {
|
||||||
|
h.opts.Logger.Debugf(ctx, "response with %v unknown content-type %s %s", hrsp.Header, ct, buf)
|
||||||
|
}
|
||||||
|
return errors.InternalServerError("go.micro.client", cerr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.opts.Logger.V(logger.DebugLevel) {
|
||||||
|
h.opts.Logger.Debugf(ctx, "response %s with %v", buf, hrsp.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
// succeseful response
|
||||||
|
if hrsp.StatusCode < 400 {
|
||||||
|
if err = cf.Unmarshal(buf, rsp); err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// response with error
|
||||||
|
var rerr interface{}
|
||||||
|
errmap, ok := opts.Context.Value(errorMapKey{}).(map[string]interface{})
|
||||||
|
if ok && errmap != nil {
|
||||||
|
rerr, ok = errmap[fmt.Sprintf("%d", hrsp.StatusCode)]
|
||||||
|
if !ok {
|
||||||
|
rerr, ok = errmap["default"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok || rerr == nil {
|
||||||
|
return errors.New("go.micro.client", string(buf), int32(hrsp.StatusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cerr := cf.Unmarshal(buf, rerr); cerr != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", cerr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err, ok = rerr.(error); !ok {
|
||||||
|
err = &Error{rerr}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type tag struct {
|
||||||
|
key string
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getParam(val reflect.Value) string {
|
||||||
|
var v string
|
||||||
|
switch val.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
switch reflect.Indirect(val).Type().String() {
|
||||||
|
case
|
||||||
|
"wrapperspb.BoolValue",
|
||||||
|
"wrapperspb.BytesValue",
|
||||||
|
"wrapperspb.DoubleValue",
|
||||||
|
"wrapperspb.FloatValue",
|
||||||
|
"wrapperspb.Int32Value", "wrapperspb.Int64Value",
|
||||||
|
"wrapperspb.StringValue",
|
||||||
|
"wrapperspb.UInt32Value", "wrapperspb.UInt64Value":
|
||||||
|
if eva := reflect.Indirect(val).FieldByName("Value"); eva.IsValid() {
|
||||||
|
v = getParam(eva)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
v = fmt.Sprintf("%v", val.Interface())
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
99
util_test.go
Normal file
99
util_test.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParsing(t *testing.T) {
|
||||||
|
type Message struct {
|
||||||
|
IIN string `protobuf:"bytes,1,opt,name=iin,proto3" json:"iin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
omsg := &Message{IIN: "5555"}
|
||||||
|
|
||||||
|
for _, m := range []string{"POST"} {
|
||||||
|
body := ""
|
||||||
|
path, nmsg, err := newPathRequest("/users/iin/{iin}/push-notifications", m, body, omsg, []string{"protobuf", "json"}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
u, err := url.Parse(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_ = nmsg
|
||||||
|
if u.Path != "/users/iin/5555/push-notifications" {
|
||||||
|
t.Fatalf("newPathRequest invalid path %s", u.Path)
|
||||||
|
}
|
||||||
|
if nmsg != nil {
|
||||||
|
t.Fatalf("new message must be nil: %v\n", nmsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewPathRequest(t *testing.T) {
|
||||||
|
type Message struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Val1 string `protobuf:"bytes,1,opt,name=val1,proto3" json:"val1"`
|
||||||
|
Val3 []string
|
||||||
|
Val2 int64
|
||||||
|
}
|
||||||
|
|
||||||
|
omsg := &Message{Name: "test_name", Val1: "test_val1", Val2: 100, Val3: []string{"slice"}}
|
||||||
|
|
||||||
|
for _, m := range []string{"POST", "PUT", "PATCH", "GET", "DELETE"} {
|
||||||
|
body := ""
|
||||||
|
path, nmsg, err := newPathRequest("/v1/test", m, body, omsg, []string{"protobuf", "json"}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
u, err := url.Parse(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
vals := u.Query()
|
||||||
|
if v, ok := vals["name"]; !ok || v[0] != "test_name" {
|
||||||
|
t.Fatalf("invalid path: %v nmsg: %v", path, nmsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewPathVarRequest(t *testing.T) {
|
||||||
|
type Message struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Val1 string `protobuf:"bytes,1,opt,name=val1,proto3" json:"val1"`
|
||||||
|
Val3 []string
|
||||||
|
Val2 int64
|
||||||
|
}
|
||||||
|
|
||||||
|
omsg := &Message{Name: "test_name", Val1: "test_val1", Val2: 100, Val3: []string{"slice"}}
|
||||||
|
|
||||||
|
for _, m := range []string{"POST", "PUT", "PATCH", "GET", "DELETE"} {
|
||||||
|
body := ""
|
||||||
|
if m != "GET" {
|
||||||
|
body = "*"
|
||||||
|
}
|
||||||
|
path, nmsg, err := newPathRequest("/v1/test/{val1}", m, body, omsg, []string{"protobuf", "json"}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
u, err := url.Parse(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if m != "GET" {
|
||||||
|
if _, ok := nmsg.(*Message); !ok {
|
||||||
|
t.Fatalf("invalid nmsg: %#+v\n", nmsg)
|
||||||
|
}
|
||||||
|
if nmsg.(*Message).Name != "test_name" {
|
||||||
|
t.Fatalf("invalid nmsg: %v nmsg: %v", path, nmsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vals := u.Query()
|
||||||
|
if v, ok := vals["val2"]; !ok || v[0] != "100" {
|
||||||
|
t.Fatalf("invalid path: %v nmsg: %v", path, nmsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user