Compare commits
	
		
			4 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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'
 | 
					 | 
				
			||||||
							
								
								
									
										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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										161
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										161
									
								
								README.md
									
									
									
									
									
								
							@@ -1,27 +1,18 @@
 | 
				
			|||||||
# HTTP Client
 | 
					# HTTP Client
 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
This plugin is an HTTP client for [Micro](https://pkg.go.dev/go.unistack.org/micro/v3).
 | 
					This plugin is a http client for micro.
 | 
				
			||||||
It implements the [micro.Client](https://pkg.go.dev/go.unistack.org/micro/v3/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.
 | 
					 | 
				
			||||||
* In `micro/v3`, metadata is implemented as `map[string]string`, which works for most headers but not for multiple `Set-Cookie` headers. The HTTP specification forbids the use of commas in `Set-Cookie` headers; therefore, their values cannot be parsed reliably. In `micro/v4`, metadata uses `map[string][]string`, resolving this issue.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Usage
 | 
					## Usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Use directly
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```go
 | 
					```go
 | 
				
			||||||
import (
 | 
					import "go.unistack.org/micro-client-http/v4"
 | 
				
			||||||
    "go.unistack.org/micro/v3"
 | 
					 | 
				
			||||||
    http "go.unistack.org/micro-client-http/v3"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
service := micro.NewService(
 | 
					service := micro.NewService(
 | 
				
			||||||
	micro.Name("my.service"),
 | 
						micro.Name("my.service"),
 | 
				
			||||||
@@ -29,137 +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/v3/client"
 | 
					client := http.NewClient()
 | 
				
			||||||
    http "go.unistack.org/micro-client-http/v3"
 | 
					 | 
				
			||||||
    jsoncodec "go.unistack.org/micro-codec-json/v3"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
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/v3/client"
 | 
					client := client.NewClientCallOptions(http.NewClient(), http.Address("https://api.github.com"))
 | 
				
			||||||
    http "go.unistack.org/micro-client-http/v3"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
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/v3/metadata"
 | 
					 | 
				
			||||||
    http "go.unistack.org/micro-client-http/v3"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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/v3/metadata"
 | 
					 | 
				
			||||||
    http "go.unistack.org/micro-client-http/v3"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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/v3/metadata"
 | 
					 | 
				
			||||||
    http "go.unistack.org/micro-client-http/v3"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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/v3"
 | 
					 | 
				
			||||||
    status "go.unistack.org/micro-client-http/v3/status"
 | 
					 | 
				
			||||||
    jsoncodec "go.unistack.org/micro-codec-json/v3"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if err != nil {
 | 
					 | 
				
			||||||
    s, ok := status.FromError(err)
 | 
					 | 
				
			||||||
    if !ok {...}
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    code    := s.Code()     // HTTP status code
 | 
					 | 
				
			||||||
    message := s.Message()  // HTTP status text
 | 
					 | 
				
			||||||
    details := s.Details()  // Error type mapped from ErrorMap
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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/v3/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/v3/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)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										176
									
								
								client.go
									
									
									
									
									
								
							
							
						
						
									
										176
									
								
								client.go
									
									
									
									
									
								
							@@ -1,176 +0,0 @@
 | 
				
			|||||||
package http
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"sync"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/client"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/errors"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/options"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/semconv"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/tracer"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"go.unistack.org/micro-client-http/v3/status"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var _ client.Client = (*Client)(nil)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var DefaultContentType = "application/json"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Client struct {
 | 
					 | 
				
			||||||
	funcPublish      client.FuncPublish
 | 
					 | 
				
			||||||
	funcBatchPublish client.FuncBatchPublish
 | 
					 | 
				
			||||||
	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.funcPublish = c.fnPublish
 | 
					 | 
				
			||||||
	c.funcBatchPublish = c.fnBatchPublish
 | 
					 | 
				
			||||||
	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)
 | 
					 | 
				
			||||||
		case client.HookPublish:
 | 
					 | 
				
			||||||
			c.funcPublish = h(c.funcPublish)
 | 
					 | 
				
			||||||
		case client.HookBatchPublish:
 | 
					 | 
				
			||||||
			c.funcBatchPublish = h(c.funcBatchPublish)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Client) Options() client.Options {
 | 
					 | 
				
			||||||
	return c.opts
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Client) NewMessage(topic string, msg interface{}, opts ...client.MessageOption) client.Message {
 | 
					 | 
				
			||||||
	msgOpts := client.NewMessageOptions(opts...)
 | 
					 | 
				
			||||||
	if msgOpts.ContentType == "" {
 | 
					 | 
				
			||||||
		msgOpts.ContentType = c.opts.ContentType
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return &httpMessage{
 | 
					 | 
				
			||||||
		topic:   topic,
 | 
					 | 
				
			||||||
		payload: msg,
 | 
					 | 
				
			||||||
		opts:    msgOpts,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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()),
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	defer sp.Finish()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		statusCode  int
 | 
					 | 
				
			||||||
		statusLabel string
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err == nil {
 | 
					 | 
				
			||||||
		statusCode = http.StatusOK
 | 
					 | 
				
			||||||
		statusLabel = "success"
 | 
					 | 
				
			||||||
	} else if st, ok := status.FromError(err); ok {
 | 
					 | 
				
			||||||
		statusCode = st.Code()
 | 
					 | 
				
			||||||
		statusLabel = "failure"
 | 
					 | 
				
			||||||
		sp.SetStatus(tracer.SpanStatusError, err.Error())
 | 
					 | 
				
			||||||
	} else if me := errors.FromError(err); me != nil {
 | 
					 | 
				
			||||||
		statusCode = int(me.Code)
 | 
					 | 
				
			||||||
		statusLabel = "failure"
 | 
					 | 
				
			||||||
		sp.SetStatus(tracer.SpanStatusError, err.Error())
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		statusCode = http.StatusInternalServerError
 | 
					 | 
				
			||||||
		statusLabel = "failure"
 | 
					 | 
				
			||||||
		sp.SetStatus(tracer.SpanStatusError, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", statusLabel, "code", strconv.Itoa(statusCode)).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) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
 | 
					 | 
				
			||||||
	return c.funcPublish(ctx, p, opts...)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Client) BatchPublish(ctx context.Context, ps []client.Message, opts ...client.PublishOption) error {
 | 
					 | 
				
			||||||
	return c.funcBatchPublish(ctx, ps, opts...)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Client) String() string {
 | 
					 | 
				
			||||||
	return "http"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,289 +0,0 @@
 | 
				
			|||||||
package http
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"bytes"
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
	"net/url"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/client"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/codec"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/logger"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/metadata"
 | 
					 | 
				
			||||||
	"google.golang.org/protobuf/proto"
 | 
					 | 
				
			||||||
	"google.golang.org/protobuf/reflect/protoreflect"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"go.unistack.org/micro-client-http/v3/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) {
 | 
					 | 
				
			||||||
		if shouldLogBody(ct) {
 | 
					 | 
				
			||||||
			log.Debug(
 | 
					 | 
				
			||||||
				ctx,
 | 
					 | 
				
			||||||
				fmt.Sprintf(
 | 
					 | 
				
			||||||
					"micro.client http request: method=%s url=%s headers=%v body=%s",
 | 
					 | 
				
			||||||
					method, u.String(), hreq.Header, body,
 | 
					 | 
				
			||||||
				),
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			log.Debug(
 | 
					 | 
				
			||||||
				ctx,
 | 
					 | 
				
			||||||
				fmt.Sprintf(
 | 
					 | 
				
			||||||
					"micro.client http request: method=%s url=%s headers=%v",
 | 
					 | 
				
			||||||
					method, u.String(), hreq.Header,
 | 
					 | 
				
			||||||
				),
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	tmp := http.Request{Header: http.Header{}}
 | 
					 | 
				
			||||||
	tmp.Header.Set("Cookie", rawCookies)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func shouldLogBody(contentType string) bool {
 | 
					 | 
				
			||||||
	ct := strings.ToLower(strings.Split(contentType, ";")[0])
 | 
					 | 
				
			||||||
	switch {
 | 
					 | 
				
			||||||
	case strings.HasPrefix(ct, "text/"): // => text/html, text/plain, text/csv etc.
 | 
					 | 
				
			||||||
		return true
 | 
					 | 
				
			||||||
	case ct == "application/json",
 | 
					 | 
				
			||||||
		ct == "application/xml",
 | 
					 | 
				
			||||||
		ct == "application/x-www-form-urlencoded",
 | 
					 | 
				
			||||||
		ct == "application/yaml":
 | 
					 | 
				
			||||||
		return true
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		return false
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,447 +0,0 @@
 | 
				
			|||||||
package http
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
	"net/http/httptest"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/stretchr/testify/require"
 | 
					 | 
				
			||||||
	jsoncodec "go.unistack.org/micro-codec-json/v3"
 | 
					 | 
				
			||||||
	"google.golang.org/protobuf/proto"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	pb "go.unistack.org/micro-client-http/v3/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: "",
 | 
					 | 
				
			||||||
			want:       []*http.Cookie{},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			name:       "single cookie",
 | 
					 | 
				
			||||||
			rawCookies: "session=abc123",
 | 
					 | 
				
			||||||
			want: []*http.Cookie{
 | 
					 | 
				
			||||||
				{Name: "session", Value: "abc123"},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			name:       "multiple cookies separate items",
 | 
					 | 
				
			||||||
			rawCookies: "session=abc123; user=john",
 | 
					 | 
				
			||||||
			want: []*http.Cookie{
 | 
					 | 
				
			||||||
				{Name: "session", Value: "abc123"},
 | 
					 | 
				
			||||||
				{Name: "user", Value: "john"},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			name:       "multiple cookies in one item",
 | 
					 | 
				
			||||||
			rawCookies: "a=1; b=2",
 | 
					 | 
				
			||||||
			want: []*http.Cookie{
 | 
					 | 
				
			||||||
				{Name: "a", Value: "1"},
 | 
					 | 
				
			||||||
				{Name: "b", Value: "2"},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			name:       "mix of combined and separate cookies",
 | 
					 | 
				
			||||||
			rawCookies: "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: "session=abc123; session=xyz",
 | 
					 | 
				
			||||||
			want: []*http.Cookie{
 | 
					 | 
				
			||||||
				{Name: "session", Value: "abc123"},
 | 
					 | 
				
			||||||
				{Name: "session", Value: "xyz"},
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			name:       "cookie with spaces",
 | 
					 | 
				
			||||||
			rawCookies: "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)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestShouldLogBody(t *testing.T) {
 | 
					 | 
				
			||||||
	tests := []struct {
 | 
					 | 
				
			||||||
		name        string
 | 
					 | 
				
			||||||
		contentType string
 | 
					 | 
				
			||||||
		want        bool
 | 
					 | 
				
			||||||
	}{
 | 
					 | 
				
			||||||
		// --- text/*
 | 
					 | 
				
			||||||
		{"plain text", "text/plain", true},
 | 
					 | 
				
			||||||
		{"html", "text/html", true},
 | 
					 | 
				
			||||||
		{"csv", "text/csv", true},
 | 
					 | 
				
			||||||
		{"yaml text", "text/yaml", true},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// --- application/*
 | 
					 | 
				
			||||||
		{"json", "application/json", true},
 | 
					 | 
				
			||||||
		{"xml", "application/xml", true},
 | 
					 | 
				
			||||||
		{"form-urlencoded", "application/x-www-form-urlencoded", true},
 | 
					 | 
				
			||||||
		{"yaml", "application/yaml", true},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// --- with parameters
 | 
					 | 
				
			||||||
		{"json with charset", "application/json; charset=utf-8", true},
 | 
					 | 
				
			||||||
		{"binary with charset", "application/octet-stream; charset=utf-8", false},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// --- binary
 | 
					 | 
				
			||||||
		{"multipart form", "multipart/form-data", false},
 | 
					 | 
				
			||||||
		{"binary stream", "application/octet-stream", false},
 | 
					 | 
				
			||||||
		{"pdf", "application/pdf", false},
 | 
					 | 
				
			||||||
		{"protobuf", "application/protobuf", false},
 | 
					 | 
				
			||||||
		{"image", "image/png", false},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// --- edge cases
 | 
					 | 
				
			||||||
		{"upper case type", "APPLICATION/JSON", true},
 | 
					 | 
				
			||||||
		{"mixed case type", "Text/HTML", true},
 | 
					 | 
				
			||||||
		{"unknown text prefix", "TEXT/FOO", true},
 | 
					 | 
				
			||||||
		{"weird semicolon only", ";", false},
 | 
					 | 
				
			||||||
		{"spaces only", "   ", false},
 | 
					 | 
				
			||||||
		{"empty content-type", "", false},
 | 
					 | 
				
			||||||
		{"missing main type", "/plain", false},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, tt := range tests {
 | 
					 | 
				
			||||||
		t.Run(tt.name, func(t *testing.T) {
 | 
					 | 
				
			||||||
			require.Equal(t, tt.want, shouldLogBody(tt.contentType))
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,84 +0,0 @@
 | 
				
			|||||||
package http
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/broker"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/client"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/codec"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/errors"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/metadata"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Client) publish(ctx context.Context, ps []client.Message, opts ...client.PublishOption) error {
 | 
					 | 
				
			||||||
	var body []byte
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	options := client.NewPublishOptions(opts...)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// get proxy
 | 
					 | 
				
			||||||
	exchange := ""
 | 
					 | 
				
			||||||
	if v, ok := os.LookupEnv("MICRO_PROXY"); ok {
 | 
					 | 
				
			||||||
		exchange = v
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	// get the exchange
 | 
					 | 
				
			||||||
	if len(options.Exchange) > 0 {
 | 
					 | 
				
			||||||
		exchange = options.Exchange
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	msgs := make([]*broker.Message, 0, len(ps))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	omd, ok := metadata.FromOutgoingContext(ctx)
 | 
					 | 
				
			||||||
	if !ok {
 | 
					 | 
				
			||||||
		omd = metadata.New(2)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, p := range ps {
 | 
					 | 
				
			||||||
		md := metadata.Copy(omd)
 | 
					 | 
				
			||||||
		topic := p.Topic()
 | 
					 | 
				
			||||||
		if len(exchange) > 0 {
 | 
					 | 
				
			||||||
			topic = exchange
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		md.Set(metadata.HeaderTopic, topic)
 | 
					 | 
				
			||||||
		iter := p.Metadata().Iterator()
 | 
					 | 
				
			||||||
		var k, v string
 | 
					 | 
				
			||||||
		for iter.Next(&k, &v) {
 | 
					 | 
				
			||||||
			md.Set(k, v)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		md[metadata.HeaderContentType] = p.ContentType()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// passed in raw data
 | 
					 | 
				
			||||||
		if d, ok := p.Payload().(*codec.Frame); ok {
 | 
					 | 
				
			||||||
			body = d.Data
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			// use codec for payload
 | 
					 | 
				
			||||||
			cf, err := c.newCodec(p.ContentType())
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return errors.InternalServerError("go.micro.client", "%+v", err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			// set the body
 | 
					 | 
				
			||||||
			b, err := cf.Marshal(p.Payload())
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return errors.InternalServerError("go.micro.client", "%+v", err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			body = b
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		msgs = append(msgs, &broker.Message{Header: md, Body: body})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return c.opts.Broker.BatchPublish(
 | 
					 | 
				
			||||||
		ctx,
 | 
					 | 
				
			||||||
		msgs,
 | 
					 | 
				
			||||||
		broker.PublishContext(options.Context),
 | 
					 | 
				
			||||||
		broker.PublishBodyOnly(options.BodyOnly),
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Client) fnPublish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
 | 
					 | 
				
			||||||
	return c.publish(ctx, []client.Message{p}, opts...)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (c *Client) fnBatchPublish(ctx context.Context, ps []client.Message, opts ...client.PublishOption) error {
 | 
					 | 
				
			||||||
	return c.publish(ctx, ps, opts...)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,13 +0,0 @@
 | 
				
			|||||||
package http
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/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,288 +0,0 @@
 | 
				
			|||||||
package http
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"net"
 | 
					 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
	"net/url"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"time"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/client"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/codec"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/errors"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/logger"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/metadata"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/selector"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"go.unistack.org/micro-client-http/v3/status"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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:
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var buf []byte
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if log.V(logger.DebugLevel) {
 | 
					 | 
				
			||||||
		if shouldLogBody(ct) {
 | 
					 | 
				
			||||||
			log.Debug(
 | 
					 | 
				
			||||||
				ctx,
 | 
					 | 
				
			||||||
				fmt.Sprintf(
 | 
					 | 
				
			||||||
					"micro.client http response: status=%s headers=%v body=%s",
 | 
					 | 
				
			||||||
					hrsp.Status, hrsp.Header, buf,
 | 
					 | 
				
			||||||
				),
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			log.Debug(
 | 
					 | 
				
			||||||
				ctx,
 | 
					 | 
				
			||||||
				fmt.Sprintf(
 | 
					 | 
				
			||||||
					"micro.client http response: status=%s headers=%v",
 | 
					 | 
				
			||||||
					hrsp.Status, hrsp.Header,
 | 
					 | 
				
			||||||
				),
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cf, err := c.newCodec(ct)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return errors.InternalServerError("go.micro.client", "unknown content-type %s: %v", ct, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	s := status.New(hrsp.StatusCode)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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 s.Err()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err = cf.Unmarshal(buf, mappedErr); err != nil {
 | 
					 | 
				
			||||||
		return errors.InternalServerError("go.micro.client", "unmarshal response: %v", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return s.WithDetails(mappedErr).Err()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										27
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								go.mod
									
									
									
									
									
								
							@@ -1,26 +1,5 @@
 | 
				
			|||||||
module go.unistack.org/micro-client-http/v3
 | 
					module go.unistack.org/micro-client-http/v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
go 1.24.0
 | 
					go 1.19
 | 
				
			||||||
 | 
					
 | 
				
			||||||
toolchain go1.24.3
 | 
					require go.unistack.org/micro/v4 v4.0.1
 | 
				
			||||||
 | 
					 | 
				
			||||||
require (
 | 
					 | 
				
			||||||
	github.com/stretchr/testify v1.11.1
 | 
					 | 
				
			||||||
	go.unistack.org/micro-codec-json/v3 v3.10.3
 | 
					 | 
				
			||||||
	go.unistack.org/micro/v3 v3.11.48
 | 
					 | 
				
			||||||
	google.golang.org/protobuf v1.36.10
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
require (
 | 
					 | 
				
			||||||
	github.com/ash3in/uuidv8 v1.2.0 // indirect
 | 
					 | 
				
			||||||
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 | 
					 | 
				
			||||||
	github.com/google/uuid v1.6.0 // indirect
 | 
					 | 
				
			||||||
	github.com/kr/pretty v0.3.1 // indirect
 | 
					 | 
				
			||||||
	github.com/matoous/go-nanoid v1.5.1 // indirect
 | 
					 | 
				
			||||||
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 | 
					 | 
				
			||||||
	go.unistack.org/micro-proto/v3 v3.4.1 // indirect
 | 
					 | 
				
			||||||
	golang.org/x/sys v0.36.0 // indirect
 | 
					 | 
				
			||||||
	google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
 | 
					 | 
				
			||||||
	google.golang.org/grpc v1.75.1 // indirect
 | 
					 | 
				
			||||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										52
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								go.sum
									
									
									
									
									
								
							@@ -1,50 +1,2 @@
 | 
				
			|||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
 | 
					go.unistack.org/micro/v4 v4.0.1 h1:xo1IxbVfgh8i0eY0VeYa3cbb13u5n/Mxnp3FOgWD4Jo=
 | 
				
			||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
 | 
					go.unistack.org/micro/v4 v4.0.1/go.mod h1:p/J5UcSJjfHsWGT31uKoghQ5rUQZzQJBAFy+Z4+ZVMs=
 | 
				
			||||||
github.com/ash3in/uuidv8 v1.2.0 h1:2oogGdtCPwaVtyvPPGin4TfZLtOGE5F+W++E880G6SI=
 | 
					 | 
				
			||||||
github.com/ash3in/uuidv8 v1.2.0/go.mod h1:BnU0wJBxnzdEKmVg4xckBkD+VZuecTFTUP3M0dWgyY4=
 | 
					 | 
				
			||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 | 
					 | 
				
			||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 | 
					 | 
				
			||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
					 | 
				
			||||||
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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 | 
					 | 
				
			||||||
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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 | 
					 | 
				
			||||||
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/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/v3 v3.10.3 h1:FwSBfJswov30Dyqxp1XfQW1EG4h77uTEe/VGflg6XlY=
 | 
					 | 
				
			||||||
go.unistack.org/micro-codec-json/v3 v3.10.3/go.mod h1:26OK5MizMNKhspGC6PRVwpDIp5w1GmRb0nE5eRWWDxA=
 | 
					 | 
				
			||||||
go.unistack.org/micro-proto/v3 v3.4.1 h1:UTjLSRz2YZuaHk9iSlVqqsA50JQNAEK2ZFboGqtEa9Q=
 | 
					 | 
				
			||||||
go.unistack.org/micro-proto/v3 v3.4.1/go.mod h1:okx/cnOhzuCX0ggl/vToatbCupi0O44diiiLLsZ93Zo=
 | 
					 | 
				
			||||||
go.unistack.org/micro/v3 v3.11.48 h1:lHJYSHU2z1TTcuswItGwG7cZXN6n04EFqY7lk/0gA7w=
 | 
					 | 
				
			||||||
go.unistack.org/micro/v3 v3.11.48/go.mod h1:fDQ8Mu9wubaFP0L8hNQlpzHiEnWN0wbOlawN9HYo0N4=
 | 
					 | 
				
			||||||
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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
 | 
					 | 
				
			||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 | 
					 | 
				
			||||||
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-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
 | 
					 | 
				
			||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
 | 
					 | 
				
			||||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
 | 
					 | 
				
			||||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
 | 
					 | 
				
			||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
 | 
					 | 
				
			||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
 | 
					 | 
				
			||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
					 | 
				
			||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 | 
					 | 
				
			||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 | 
					 | 
				
			||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
					 | 
				
			||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										677
									
								
								http.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										677
									
								
								http.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,677 @@
 | 
				
			|||||||
 | 
					// 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/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 ...client.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
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := h.opts.Transport.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 ...client.RequestOption) client.Request {
 | 
				
			||||||
 | 
						return newHTTPRequest(service, method, req, h.opts.ContentType, opts...)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *httpClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) 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
 | 
				
			||||||
 | 
							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 := 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 ...client.CallOption) (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
 | 
				
			||||||
 | 
							o := client.WithStreamTimeout(time.Until(d))
 | 
				
			||||||
 | 
							o(&callOpts)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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 ...client.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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// wrap in reverse
 | 
				
			||||||
 | 
						for i := len(options.Wrappers); i > 0; i-- {
 | 
				
			||||||
 | 
							c = options.Wrappers[i-1](c)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								message.go
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								message.go
									
									
									
									
									
								
							@@ -1,28 +0,0 @@
 | 
				
			|||||||
package http
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/client"
 | 
					 | 
				
			||||||
	"go.unistack.org/micro/v3/metadata"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type httpMessage struct {
 | 
					 | 
				
			||||||
	topic   string
 | 
					 | 
				
			||||||
	payload interface{}
 | 
					 | 
				
			||||||
	opts    client.MessageOptions
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *httpMessage) Topic() string {
 | 
					 | 
				
			||||||
	return m.topic
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *httpMessage) Payload() interface{} {
 | 
					 | 
				
			||||||
	return m.payload
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *httpMessage) ContentType() string {
 | 
					 | 
				
			||||||
	return m.opts.ContentType
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *httpMessage) Metadata() metadata.Metadata {
 | 
					 | 
				
			||||||
	return m.opts.Metadata
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										162
									
								
								options.go
									
									
									
									
									
								
							
							
						
						
									
										162
									
								
								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/v3/client"
 | 
						"go.unistack.org/micro/v4/client"
 | 
				
			||||||
 | 
						"go.unistack.org/micro/v4/metadata"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --------------------------------------------- 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) client.Option {
 | 
				
			||||||
 | 
						return client.SetOption(poolMaxStreams{}, n)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type poolMaxIdle struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PoolMaxIdle maximum idle conns of a pool
 | 
				
			||||||
 | 
					func PoolMaxIdle(d int) client.Option {
 | 
				
			||||||
 | 
						return client.SetOption(poolMaxIdle{}, d)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type maxRecvMsgSizeKey struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MaxRecvMsgSize set the maximum size of message that client can receive.
 | 
				
			||||||
 | 
					func MaxRecvMsgSize(s int) client.Option {
 | 
				
			||||||
 | 
						return client.SetOption(maxRecvMsgSizeKey{}, s)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type maxSendMsgSizeKey struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MaxSendMsgSize set the maximum size of message that client can send.
 | 
				
			||||||
 | 
					func MaxSendMsgSize(s int) client.Option {
 | 
				
			||||||
 | 
						return client.SetOption(maxSendMsgSizeKey{}, s)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type httpClientKey struct{}
 | 
					type httpClientKey struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// nolint: golint
 | 
				
			||||||
 | 
					// HTTPClient pass http.Client option to client Call
 | 
				
			||||||
func HTTPClient(c *http.Client) client.Option {
 | 
					func HTTPClient(c *http.Client) client.Option {
 | 
				
			||||||
	return client.SetOption(httpClientKey{}, c)
 | 
						return client.SetOption(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{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// nolint: golint
 | 
				
			||||||
 | 
					// HTTPDialer pass net.Dialer option to client
 | 
				
			||||||
func HTTPDialer(d *net.Dialer) client.Option {
 | 
					func HTTPDialer(d *net.Dialer) client.Option {
 | 
				
			||||||
	return client.SetOption(httpDialerKey{}, d)
 | 
						return client.SetOption(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{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Method pass method option to client Call
 | 
				
			||||||
func Method(m string) client.CallOption {
 | 
					func Method(m string) client.CallOption {
 | 
				
			||||||
	return client.SetCallOption(methodKey{}, m)
 | 
						return client.SetCallOption(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{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Path spcecifies path option to client Call
 | 
				
			||||||
func Path(p string) client.CallOption {
 | 
					func Path(p string) client.CallOption {
 | 
				
			||||||
	return client.SetCallOption(pathKey{}, p)
 | 
						return client.SetCallOption(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{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Body specifies body option to client Call
 | 
				
			||||||
func Body(b string) client.CallOption {
 | 
					func Body(b string) client.CallOption {
 | 
				
			||||||
	return client.SetCallOption(bodyKey{}, b)
 | 
						return client.SetCallOption(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{}) client.CallOption {
 | 
				
			||||||
	return client.SetCallOption(errorMapKey{}, m)
 | 
						return client.SetCallOption(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) client.CallOption {
 | 
				
			||||||
 | 
						return client.SetCallOption(structTagsKey{}, tags)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type metadataKey struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Metadata pass metadata to client Call
 | 
				
			||||||
 | 
					func Metadata(md metadata.Metadata) client.CallOption {
 | 
				
			||||||
 | 
						return client.SetCallOption(metadataKey{}, md)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ------------------------------------------------ Cookie option ------------------------------------------------------
 | 
					 | 
				
			||||||
type cookieKey struct{}
 | 
					type cookieKey struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Cookie pass cookie to client Call
 | 
				
			||||||
func Cookie(cookies ...string) client.CallOption {
 | 
					func Cookie(cookies ...string) client.CallOption {
 | 
				
			||||||
	return client.SetCallOption(cookieKey{}, cookies)
 | 
						return client.SetCallOption(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{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Header pass cookie to client Call
 | 
				
			||||||
func Header(headers ...string) client.CallOption {
 | 
					func Header(headers ...string) client.CallOption {
 | 
				
			||||||
	return client.SetCallOption(headerKey{}, headers)
 | 
						return client.SetCallOption(headerKey{}, headers)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func headerFromOpts(opts client.CallOptions) ([]string, bool) {
 | 
					 | 
				
			||||||
	h, ok := opts.Context.Value(headerKey{}).([]string)
 | 
					 | 
				
			||||||
	return h, ok
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								request.go
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								request.go
									
									
									
									
									
								
							@@ -1,17 +1,37 @@
 | 
				
			|||||||
package http
 | 
					package http
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"go.unistack.org/micro/v3/client"
 | 
						"go.unistack.org/micro/v4/client"
 | 
				
			||||||
	"go.unistack.org/micro/v3/codec"
 | 
						"go.unistack.org/micro/v4/codec"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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 ...client.RequestOption) 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 +44,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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +0,0 @@
 | 
				
			|||||||
package status
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Error is a thin wrapper around Status that implements the error interface.
 | 
					 | 
				
			||||||
type Error struct {
 | 
					 | 
				
			||||||
	s *Status
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (e *Error) Error() string {
 | 
					 | 
				
			||||||
	return e.s.String()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (e *Error) HTTPStatus() *Status {
 | 
					 | 
				
			||||||
	return e.s
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,54 +0,0 @@
 | 
				
			|||||||
package status
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Status represents the outcome of an HTTP request in a style similar to gRPC status.
 | 
					 | 
				
			||||||
type Status struct {
 | 
					 | 
				
			||||||
	code    int    // HTTP status code
 | 
					 | 
				
			||||||
	message string // HTTP status text
 | 
					 | 
				
			||||||
	details any    // parsed error object
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func New(statusCode int) *Status {
 | 
					 | 
				
			||||||
	return &Status{code: statusCode, message: http.StatusText(statusCode)}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func FromError(err error) (*Status, bool) {
 | 
					 | 
				
			||||||
	if err == nil {
 | 
					 | 
				
			||||||
		return nil, false
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	var e *Error
 | 
					 | 
				
			||||||
	if errors.As(err, &e) {
 | 
					 | 
				
			||||||
		return e.HTTPStatus(), true
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil, false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Status) Code() int {
 | 
					 | 
				
			||||||
	return s.code
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Status) Message() string {
 | 
					 | 
				
			||||||
	return s.message
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Status) Details() any {
 | 
					 | 
				
			||||||
	return s.details
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Status) WithDetails(details any) *Status {
 | 
					 | 
				
			||||||
	s.details = details
 | 
					 | 
				
			||||||
	return s
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Status) String() string {
 | 
					 | 
				
			||||||
	return fmt.Sprintf("http error: code = %d desc = %s", s.Code(), s.Message())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (s *Status) Err() error {
 | 
					 | 
				
			||||||
	return &Error{s: s}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,97 +0,0 @@
 | 
				
			|||||||
package status_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/stretchr/testify/require"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"go.unistack.org/micro-client-http/v3/status"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type fakeError struct{ s *status.Status }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (fe *fakeError) Error() string {
 | 
					 | 
				
			||||||
	return fe.s.String()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestNew(t *testing.T) {
 | 
					 | 
				
			||||||
	s := status.New(http.StatusNotFound)
 | 
					 | 
				
			||||||
	require.Equal(t, http.StatusNotFound, s.Code())
 | 
					 | 
				
			||||||
	require.Equal(t, "Not Found", s.Message())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestFromError(t *testing.T) {
 | 
					 | 
				
			||||||
	tests := []struct {
 | 
					 | 
				
			||||||
		name       string
 | 
					 | 
				
			||||||
		input      error
 | 
					 | 
				
			||||||
		wantStatus *status.Status
 | 
					 | 
				
			||||||
		wantOK     bool
 | 
					 | 
				
			||||||
	}{
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			name:       "nil error",
 | 
					 | 
				
			||||||
			input:      nil,
 | 
					 | 
				
			||||||
			wantStatus: nil,
 | 
					 | 
				
			||||||
			wantOK:     false,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			name:       "simple error",
 | 
					 | 
				
			||||||
			input:      errors.New("some error"),
 | 
					 | 
				
			||||||
			wantStatus: nil,
 | 
					 | 
				
			||||||
			wantOK:     false,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			name: "unexpected type of error",
 | 
					 | 
				
			||||||
			input: func() error {
 | 
					 | 
				
			||||||
				return &fakeError{s: status.New(http.StatusNotFound)}
 | 
					 | 
				
			||||||
			}(),
 | 
					 | 
				
			||||||
			wantStatus: nil,
 | 
					 | 
				
			||||||
			wantOK:     false,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			name:       "expected type of error",
 | 
					 | 
				
			||||||
			input:      status.New(http.StatusNotFound).Err(),
 | 
					 | 
				
			||||||
			wantStatus: status.New(http.StatusNotFound),
 | 
					 | 
				
			||||||
			wantOK:     true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, tt := range tests {
 | 
					 | 
				
			||||||
		t.Run(tt.name, func(t *testing.T) {
 | 
					 | 
				
			||||||
			result, ok := status.FromError(tt.input)
 | 
					 | 
				
			||||||
			require.Equal(t, tt.wantStatus, result)
 | 
					 | 
				
			||||||
			require.Equal(t, tt.wantOK, ok)
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestStatus_Code(t *testing.T) {
 | 
					 | 
				
			||||||
	s := status.New(http.StatusNotFound)
 | 
					 | 
				
			||||||
	require.Equal(t, http.StatusNotFound, s.Code())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestStatus_Message(t *testing.T) {
 | 
					 | 
				
			||||||
	s := status.New(http.StatusNotFound)
 | 
					 | 
				
			||||||
	require.Equal(t, "Not Found", s.Message())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestStatus_WithDetails(t *testing.T) {
 | 
					 | 
				
			||||||
	s := status.New(http.StatusNotFound).WithDetails(errors.New("some error"))
 | 
					 | 
				
			||||||
	require.Equal(t, errors.New("some error"), s.Details())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestStatus_String(t *testing.T) {
 | 
					 | 
				
			||||||
	s := status.New(http.StatusInternalServerError)
 | 
					 | 
				
			||||||
	expected := fmt.Sprintf("http error: code = %d desc = %s", 500, "Internal Server Error")
 | 
					 | 
				
			||||||
	require.Equal(t, expected, s.String())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestStatus_Err(t *testing.T) {
 | 
					 | 
				
			||||||
	var e *status.Error
 | 
					 | 
				
			||||||
	s := status.New(http.StatusForbidden)
 | 
					 | 
				
			||||||
	require.Error(t, s.Err())
 | 
					 | 
				
			||||||
	require.ErrorAs(t, s.Err(), &e)
 | 
					 | 
				
			||||||
	require.Equal(t, status.New(http.StatusForbidden), e.HTTPStatus())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										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