Compare commits

..

238 Commits
v3.3.0 ... v4

Author SHA1 Message Date
vtolstov
9bb2f8cffa Apply Code Coverage Badge 2025-09-23 10:34:26 +00:00
24801750a7 integrate request builder into HTTP client for googleapis support (#157)
Some checks failed
coverage / build (push) Successful in 2m19s
test / test (push) Failing after 17m15s
2025-09-23 13:30:15 +03:00
vtolstov
b37fca95cf Apply Code Coverage Badge 2025-06-05 09:23:41 +00:00
4c4a024e19 fix for headers with hyphens (#155)
All checks were successful
coverage / build (push) Successful in 52s
test / test (push) Successful in 2m44s
2025-06-05 12:18:50 +03:00
b93e634873 fixed working with headers and cookies (#154)
Some checks failed
coverage / build (push) Successful in 3m33s
test / test (push) Failing after 16m33s
sync / sync (push) Successful in 11s
2025-06-04 23:12:06 +03:00
4a6414b2b8 changed embedded mutex to private field (#153)
Some checks failed
test / test (push) Failing after 16m53s
coverage / build (push) Failing after 17m7s
sync / sync (push) Successful in 13s
2025-05-25 01:14:50 +03:00
938ddd2ab3 added commit hash check to avoid unnecessary repository cloning (#152)
All checks were successful
sync / sync (push) Successful in 18s
2025-05-05 19:25:44 +03:00
04e0597b73 fixup workflows
All checks were successful
sync / sync (push) Successful in 35s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2025-05-04 22:52:28 +03:00
4d55c7b90a [v4] rename .gitea to .github (#148)
Some checks failed
sync / sync (push) Has been cancelled
* rename .gitea to .github

* attempt to fix lint/test job
2025-04-28 10:26:29 +03:00
fbe82a3905 improve sync
All checks were successful
sync / sync (push) Successful in 43s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2025-04-27 21:07:40 +03:00
vtolstov
14490a419e Apply Code Coverage Badge 2025-04-27 13:31:31 +00:00
d434261168 update deps
All checks were successful
coverage / build (push) Successful in 2m48s
sync / sync (push) Successful in 1m25s
test / test (push) Successful in 5m4s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2025-04-27 16:29:22 +03:00
2e384a5b75 Merge pull request 'prepare v4' (#125) from devstigneev/micro-client-http:prepare_v4_1 into v4
All checks were successful
test / test (push) Successful in 1m53s
move forward to v4

Reviewed-on: #125
2025-02-28 14:39:02 +03:00
d9e7ac75ef prepare v4
All checks were successful
lint / lint (pull_request) Successful in 1m38s
test / test (pull_request) Successful in 3m9s
2025-02-27 18:57:14 +03:00
a9829b5da0 Merge pull request 'move set content-type in client publish' (#123) from devstigneev/micro-client-http:v3 into v3
All checks were successful
test / test (push) Successful in 2m56s
Reviewed-on: #123
2025-01-18 15:38:23 +03:00
a770c498e7 move set content-type in client publish
All checks were successful
lint / lint (pull_request) Successful in 1m25s
test / test (pull_request) Successful in 2m56s
2025-01-17 17:57:44 +03:00
fa5602486a lintering && fix call hook (#122)
All checks were successful
test / test (push) Successful in 4m10s
Reviewed-on: #122
Co-authored-by: Evstigneev Denis <danteevstigneev@yandex.ru>
Co-committed-by: Evstigneev Denis <danteevstigneev@yandex.ru>
2024-12-18 20:43:01 +03:00
d5690ef2b7 Update workflows (#121)
All checks were successful
test / test (push) Successful in 2m31s
Co-authored-by: Aleksandr Tolstikhin <atolstikhin@mtsbank.ru>
Reviewed-on: #121
Co-authored-by: Александр Толстихин <tolstihin1996@mail.ru>
Co-committed-by: Александр Толстихин <tolstihin1996@mail.ru>
2024-12-11 00:35:07 +03:00
134031e18b update for latest micro logger changes
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-10-12 13:26:11 +03:00
9743a7f9b1 update deps
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-09-20 18:45:01 +03:00
2225b95e3e try to fixup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-05-20 08:55:24 +03:00
b76b8d3114 fixup path filling
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-05-04 15:05:21 +03:00
b92b2541e0 fixup path filling
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-05-04 13:41:59 +03:00
9d955fc079 lower deps
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-05-04 09:18:37 +03:00
1be75ea264 fill nested fields in path
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-05-02 14:40:12 +03:00
d4a07099a2 add metrics and tracing
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-04-23 06:51:47 +03:00
a648bf77f4 fixup logger
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-04-09 23:57:44 +03:00
0eb663fffe fixup logger
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2024-04-09 23:46:36 +03:00
Nurzhan Ilyassov
fd803b1fe0 update tests 2024-03-31 20:21:45 +03:00
Nurzhan Ilyassov
4c2178305d fix nil nmsg in case of empty request body 2024-03-31 20:21:45 +03:00
9149aeb3de Merge pull request 'fixup md' (#116) from pubmdfix into v3
Reviewed-on: #116
2023-12-21 00:16:22 +03:00
dcadd64941 fixup md
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-12-21 00:16:02 +03:00
8207d4154f Merge pull request 'fix MessageMetadata option' (#115) from client-metadata into v3
Reviewed-on: #115
2023-10-26 03:16:36 +03:00
d7524cbe01 fix MessageMetadata option
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-10-26 03:16:09 +03:00
0e7b8e73a8 Merge pull request 'fix request/response md handling' (#113) from reqrsp-md into v3
Reviewed-on: #113
2023-07-11 00:55:15 +03:00
35146aa717 fix request/response md handling
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-07-11 00:54:23 +03:00
3f44a41d30 Merge pull request 'simplify rsp parse' (#108) from logrsp into v3
Reviewed-on: #108
2023-03-16 19:17:12 +03:00
090100e522 simplify rsp parse
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-03-16 19:16:35 +03:00
0bf8053d72 Merge pull request 'logging: add debug logging for request and response' (#107) from logging into v3
Reviewed-on: #107
2023-03-15 22:26:58 +03:00
42bdd79b3d add debug logging for request and response
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2023-03-15 22:26:14 +03:00
5fa952c86a Merge pull request #107 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.16
Bump go.unistack.org/micro/v3 from 3.10.14 to 3.10.16
2023-03-14 22:00:27 +03:00
dependabot[bot]
d861c31a05 Bump go.unistack.org/micro/v3 from 3.10.14 to 3.10.16
Bumps go.unistack.org/micro/v3 from 3.10.14 to 3.10.16.

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-14 18:58:37 +00:00
e7dc59157d Merge pull request #106 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.14
Bump go.unistack.org/micro/v3 from 3.10.13 to 3.10.14
2023-02-27 22:08:00 +03:00
dependabot[bot]
7f7081e6e8 Bump go.unistack.org/micro/v3 from 3.10.13 to 3.10.14
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.10.13 to 3.10.14.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.10.13...v3.10.14)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-27 19:06:37 +00:00
d441f151e0 Merge pull request #105 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.13
Bump go.unistack.org/micro/v3 from 3.10.12 to 3.10.13
2023-02-22 21:13:36 +03:00
dependabot[bot]
6a6be26ef1 Bump go.unistack.org/micro/v3 from 3.10.12 to 3.10.13
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.10.12 to 3.10.13.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.10.12...v3.10.13)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-22 18:11:53 +00:00
d1d5e0f4ac Merge pull request #104 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.12
Bump go.unistack.org/micro/v3 from 3.10.11 to 3.10.12
2023-02-21 22:32:42 +03:00
dependabot[bot]
3f8976ee62 Bump go.unistack.org/micro/v3 from 3.10.11 to 3.10.12
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.10.11 to 3.10.12.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.10.11...v3.10.12)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-21 19:30:23 +00:00
2078f2e700 Merge pull request #103 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.11
Bump go.unistack.org/micro/v3 from 3.10.10 to 3.10.11
2023-02-14 22:00:49 +03:00
dependabot[bot]
007d45fc7b Bump go.unistack.org/micro/v3 from 3.10.10 to 3.10.11
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.10.10 to 3.10.11.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.10.10...v3.10.11)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-14 18:58:54 +00:00
6c84752ccd Merge pull request #102 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.10
Bump go.unistack.org/micro/v3 from 3.10.9 to 3.10.10
2023-02-13 22:17:58 +03:00
dependabot[bot]
54d21ba6e1 Bump go.unistack.org/micro/v3 from 3.10.9 to 3.10.10
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.10.9 to 3.10.10.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.10.9...v3.10.10)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-13 19:16:23 +00:00
5b1f1df17a Merge pull request #101 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.9
Bump go.unistack.org/micro/v3 from 3.10.6 to 3.10.9
2023-02-08 21:05:25 +03:00
dependabot[bot]
11d7ee54fd Bump go.unistack.org/micro/v3 from 3.10.6 to 3.10.9
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.10.6 to 3.10.9.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.10.6...v3.10.9)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-08 18:03:34 +00:00
41d606b4e8 Merge pull request #100 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.6
Bump go.unistack.org/micro/v3 from 3.10.5 to 3.10.6
2023-02-06 21:08:51 +03:00
dependabot[bot]
5f6f06c239 Bump go.unistack.org/micro/v3 from 3.10.5 to 3.10.6
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.10.5 to 3.10.6.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.10.5...v3.10.6)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-06 18:06:46 +00:00
dependabot[bot]
f80bb95119 Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 (#97)
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.3.5 to 1.3.6.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.3.5...v1.3.6)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-30 21:21:22 +03:00
dependabot[bot]
cd23f93d57 Bump golangci/golangci-lint-action from 3.3.1 to 3.4.0 (#96)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3.3.1 to 3.4.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3.3.1...v3.4.0)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-30 18:14:03 +00:00
f625d5c958 Merge pull request #98 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.5
Bump go.unistack.org/micro/v3 from 3.10.4 to 3.10.5
2023-01-30 21:08:56 +03:00
dependabot[bot]
8b068fb7ab Bump go.unistack.org/micro/v3 from 3.10.4 to 3.10.5
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.10.4 to 3.10.5.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.10.4...v3.10.5)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-30 18:06:50 +00:00
3a1e9ec707 Merge pull request #95 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.4
Bump go.unistack.org/micro/v3 from 3.10.1 to 3.10.4
2023-01-18 21:04:54 +03:00
dependabot[bot]
4d6a98782c Bump go.unistack.org/micro/v3 from 3.10.1 to 3.10.4
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.10.1 to 3.10.4.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.10.1...v3.10.4)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-18 18:03:06 +00:00
af838ec6e5 Merge pull request #94 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.1
Bump go.unistack.org/micro/v3 from 3.10.0 to 3.10.1
2023-01-17 21:05:06 +03:00
dependabot[bot]
0c90ea28d9 Bump go.unistack.org/micro/v3 from 3.10.0 to 3.10.1
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.10.0 to 3.10.1.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.10.0...v3.10.1)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-17 18:03:12 +00:00
90e327726b Merge pull request #93 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.10.0
Bump go.unistack.org/micro/v3 from 3.9.18 to 3.10.0
2023-01-09 21:21:55 +03:00
dependabot[bot]
6e29ac652b Bump go.unistack.org/micro/v3 from 3.9.18 to 3.10.0
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.18 to 3.10.0.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.18...v3.10.0)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-09 18:20:15 +00:00
f8b0fe881f Merge pull request #92 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.18
Bump go.unistack.org/micro/v3 from 3.9.17 to 3.9.18
2022-12-28 21:09:44 +03:00
dependabot[bot]
2941a335f0 Bump go.unistack.org/micro/v3 from 3.9.17 to 3.9.18
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.17 to 3.9.18.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.17...v3.9.18)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-28 18:07:29 +00:00
712ba79b55 Merge pull request #91 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.17
Bump go.unistack.org/micro/v3 from 3.9.15 to 3.9.17
2022-12-26 21:05:49 +03:00
dependabot[bot]
3df3d6bfa3 Bump go.unistack.org/micro/v3 from 3.9.15 to 3.9.17
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.15 to 3.9.17.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.15...v3.9.17)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-26 18:04:17 +00:00
bed23083cf Merge pull request #90 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.15
Bump go.unistack.org/micro/v3 from 3.9.13 to 3.9.15
2022-11-28 21:08:44 +03:00
dependabot[bot]
79175c96aa Bump go.unistack.org/micro/v3 from 3.9.13 to 3.9.15
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.13 to 3.9.15.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.13...v3.9.15)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-28 18:06:45 +00:00
dependabot[bot]
c23192f2b6 Bump hmarr/auto-approve-action from 2 to 3 (#89)
Bumps [hmarr/auto-approve-action](https://github.com/hmarr/auto-approve-action) from 2 to 3.
- [Release notes](https://github.com/hmarr/auto-approve-action/releases)
- [Commits](https://github.com/hmarr/auto-approve-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: hmarr/auto-approve-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-28 16:25:58 +03:00
2bf129d6ab Merge pull request #88 from unistack-org/ctx_dialer
add context dialer support
2022-11-17 00:06:59 +03:00
4bf60486e0 add context dialer support
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-11-17 00:05:09 +03:00
d048278b84 Merge pull request #87 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.13
Bump go.unistack.org/micro/v3 from 3.9.11 to 3.9.13
2022-11-14 21:05:48 +03:00
dependabot[bot]
1cf18ac2a9 Bump go.unistack.org/micro/v3 from 3.9.11 to 3.9.13
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.11 to 3.9.13.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.11...v3.9.13)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 18:03:58 +00:00
dependabot[bot]
251f6f70c4 Bump golangci/golangci-lint-action from 3.2.0 to 3.3.1 (#86)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3.2.0 to 3.3.1.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3.2.0...v3.3.1)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-14 07:22:37 +03:00
dependabot[bot]
26bf526741 Bump dependabot/fetch-metadata from 1.3.4 to 1.3.5 (#85)
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.3.4...v1.3.5)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-14 07:04:31 +03:00
dependabot[bot]
a86c824eb7 Bump dependabot/fetch-metadata from 1.3.3 to 1.3.4 (#83)
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.3.3 to 1.3.4.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.3.3...v1.3.4)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-04 11:43:33 +03:00
dependabot[bot]
f8cd67b8cd Bump dependabot/fetch-metadata from 1.3.1 to 1.3.3 (#81)
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.3.1 to 1.3.3.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.3.1...v1.3.3)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-28 17:45:04 +03:00
feb5fec0dd Merge pull request #82 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.11
Bump go.unistack.org/micro/v3 from 3.9.10 to 3.9.11
2022-07-11 21:20:31 +03:00
dependabot[bot]
37996fbe90 Bump go.unistack.org/micro/v3 from 3.9.10 to 3.9.11
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.10 to 3.9.11.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.10...v3.9.11)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-11 18:18:37 +00:00
08a22e02f9 Merge pull request #79 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.10
Bump go.unistack.org/micro/v3 from 3.9.8 to 3.9.10
2022-06-27 21:40:09 +03:00
dependabot[bot]
3358ce8aa2 Bump go.unistack.org/micro/v3 from 3.9.8 to 3.9.10
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.8 to 3.9.10.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.8...v3.9.10)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-27 18:38:09 +00:00
dependabot[bot]
74158bd60b Bump golangci/golangci-lint-action from 3.1.0 to 3.2.0 (#78)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3.1.0 to 3.2.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3.1.0...v3.2.0)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-25 22:52:35 +03:00
930530cbae Merge pull request #77 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.8
Bump go.unistack.org/micro/v3 from 3.9.7 to 3.9.8
2022-05-03 21:15:55 +03:00
dependabot[bot]
24a8dbdc4b Bump go.unistack.org/micro/v3 from 3.9.7 to 3.9.8
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.7 to 3.9.8.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.7...v3.9.8)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-03 18:14:10 +00:00
c3c580163d Merge pull request #76 from unistack-org/backoff
use backoff for lookup too
2022-05-01 23:34:28 +03:00
4ad526253a use backoff for lookup too
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-05-01 23:32:37 +03:00
dependabot[bot]
3eca811f81 Bump github/codeql-action from 1 to 2 (#75)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-25 21:44:56 +03:00
dependabot[bot]
367fe6d579 Bump dependabot/fetch-metadata from 1.3.0 to 1.3.1 (#74)
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-21 12:48:37 +03:00
b86a166d08 Merge pull request #73 from unistack-org/dependabot/github_actions/actions/setup-go-3
Bump actions/setup-go from 2 to 3
2022-04-11 22:58:06 +03:00
dependabot[bot]
ecd2711714 Bump actions/setup-go from 2 to 3
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-08 18:17:08 +00:00
0e82845989 Merge pull request #72 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.7
Bump go.unistack.org/micro/v3 from 3.9.5 to 3.9.7
2022-03-30 21:24:05 +03:00
dependabot[bot]
670a31c9ef Bump go.unistack.org/micro/v3 from 3.9.5 to 3.9.7
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.5 to 3.9.7.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.5...v3.9.7)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-30 18:22:04 +00:00
6f136a039b Merge pull request #71 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.5
Bump go.unistack.org/micro/v3 from 3.9.2 to 3.9.5
2022-03-28 21:29:16 +03:00
dependabot[bot]
c8516877ee Bump go.unistack.org/micro/v3 from 3.9.2 to 3.9.5
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.2 to 3.9.5.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.2...v3.9.5)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-28 18:27:27 +00:00
2075368ba3 Merge pull request #70 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.2
Bump go.unistack.org/micro/v3 from 3.9.1 to 3.9.2
2022-03-25 21:24:30 +03:00
dependabot[bot]
fbef71bae8 Bump go.unistack.org/micro/v3 from 3.9.1 to 3.9.2
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.1 to 3.9.2.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.1...v3.9.2)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-25 18:23:00 +00:00
a946d63cae Merge pull request #68 from unistack-org/dependabot/github_actions/actions/cache-3
Bump actions/cache from 2 to 3
2022-03-22 23:14:45 +03:00
dependabot[bot]
bc47181f45 Bump actions/cache from 2 to 3
Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-22 18:33:08 +00:00
e29671add6 Merge pull request #69 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.1
Bump go.unistack.org/micro/v3 from 3.9.0 to 3.9.1
2022-03-22 21:32:31 +03:00
dependabot[bot]
dede9e6488 Bump go.unistack.org/micro/v3 from 3.9.0 to 3.9.1
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.9.0 to 3.9.1.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.9.0...v3.9.1)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-22 18:26:02 +00:00
2f658787d4 Merge pull request #67 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.9.0
Bump go.unistack.org/micro/v3 from 3.8.21 to 3.9.0
2022-03-21 21:43:35 +03:00
dependabot[bot]
b8ad04a42a Bump go.unistack.org/micro/v3 from 3.8.21 to 3.9.0
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.21 to 3.9.0.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.21...v3.9.0)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-21 18:41:56 +00:00
766cbd8ae0 Merge pull request #66 from unistack-org/wrappers
add additional wrappers support
2022-03-10 12:30:10 +03:00
847352a8d3 Merge branch 'master' into wrappers 2022-03-10 12:28:56 +03:00
147c2d756e add additional wrappers support
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-10 12:27:37 +03:00
f42994e5c0 update go version
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-07 13:45:05 +03:00
08b3e7e23e Merge pull request #64 from unistack-org/message
add message metadata support
2022-03-05 19:04:45 +03:00
2f70890e6c Merge branch 'master' into message 2022-03-05 19:02:56 +03:00
e9be505c59 fix workflow
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 19:02:47 +03:00
13749e7a4d Merge branch 'master' into message 2022-03-05 19:01:26 +03:00
91cf06abbd fix workflow
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 19:01:13 +03:00
a1f29249d3 Merge branch 'master' into message 2022-03-05 18:58:02 +03:00
db9c686306 fix workflow
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 18:57:54 +03:00
b74add18c1 Merge branch 'master' into message 2022-03-05 18:54:53 +03:00
5d3cbfc7d3 fix workflow
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 18:54:28 +03:00
f42e8883ae Merge branch 'master' into message 2022-03-05 18:49:30 +03:00
93344a3f7b fix workflow
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 18:49:01 +03:00
2e218748e8 fix workflow
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 18:47:38 +03:00
178d68aae9 add message metadata support
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 18:46:00 +03:00
2061b2dbb6 update worklow
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 18:45:15 +03:00
fa9e204032 update
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 18:38:35 +03:00
bce6f8c3e7 fix workflows
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 18:32:28 +03:00
1ee46a0aac update workflow
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 18:25:01 +03:00
4f749c5bb1 change workflow
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-05 18:20:30 +03:00
8231f4f7f6 Merge pull request #57 from unistack-org/master
merge master
2022-03-03 17:47:50 +03:00
241114aeb1 Merge pull request #56 from unistack-org/fix_exported
fixup for older go
2022-03-03 17:47:30 +03:00
e4541d8b44 fixup for older go
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-03 17:47:12 +03:00
b658f5091a Merge pull request #55 from unistack-org/master
merge master
2022-03-02 17:52:34 +03:00
c21d30e4a0 Merge pull request #52 from unistack-org/dependabot/github_actions/dependabot/fetch-metadata-1.3.0
Bump dependabot/fetch-metadata from 1.2.1 to 1.3.0
2022-03-02 17:52:10 +03:00
1349df4d97 Merge pull request #53 from unistack-org/dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2 to 3
2022-03-02 17:52:01 +03:00
6dfc8b8a01 Merge pull request #54 from unistack-org/isExported
fixup for unexported fields
2022-03-02 17:51:49 +03:00
d280ea260e fixup for unexported fields
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-03-02 17:51:29 +03:00
dependabot[bot]
e9295b1d12 Bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 18:19:22 +00:00
dependabot[bot]
75b102666f Bump dependabot/fetch-metadata from 1.2.1 to 1.3.0
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.2.1 to 1.3.0.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.2.1...v1.3.0)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 18:19:19 +00:00
18b95d7dde Merge pull request #51 from unistack-org/dependabot/github_actions/golangci/golangci-lint-action-3.1.0
Bump golangci/golangci-lint-action from 2 to 3.1.0
2022-03-01 00:38:46 +03:00
dependabot[bot]
6d3d8ccd14 Bump golangci/golangci-lint-action from 2 to 3.1.0
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 2 to 3.1.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v2...v3.1.0)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-28 18:24:50 +00:00
f37c8fd5d4 Merge pull request #49 from unistack-org/dependabot/github_actions/dependabot/fetch-metadata-1.2.1
Bump dependabot/fetch-metadata from 1.1.1 to 1.2.1
2022-02-25 09:41:34 +03:00
dependabot[bot]
c0bfe416b1 Bump dependabot/fetch-metadata from 1.1.1 to 1.2.1
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.1.1 to 1.2.1.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.1.1...v1.2.1)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-23 18:16:56 +00:00
ce520397c4 Merge pull request #47 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.8.21
Bump go.unistack.org/micro/v3 from 3.8.20 to 3.8.21
2022-01-31 21:22:52 +03:00
dependabot[bot]
3cd4e16c36 Bump go.unistack.org/micro/v3 from 3.8.20 to 3.8.21
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.20 to 3.8.21.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.20...v3.8.21)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-31 18:21:09 +00:00
3dccbcba98 Merge pull request #46 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.8.20
Bump go.unistack.org/micro/v3 from 3.8.19 to 3.8.20
2022-01-26 21:17:26 +03:00
dependabot[bot]
7572ef631d Bump go.unistack.org/micro/v3 from 3.8.19 to 3.8.20
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.19 to 3.8.20.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.19...v3.8.20)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-26 18:15:32 +00:00
030d906f87 Merge pull request #45 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.8.19
Bump go.unistack.org/micro/v3 from 3.8.18 to 3.8.19
2022-01-25 21:17:30 +03:00
dependabot[bot]
bfefab250f Bump go.unistack.org/micro/v3 from 3.8.18 to 3.8.19
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.18 to 3.8.19.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.18...v3.8.19)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-25 18:15:35 +00:00
82b7b5e4fb Merge pull request #44 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.8.18
Bump go.unistack.org/micro/v3 from 3.8.15 to 3.8.18
2022-01-21 21:21:18 +03:00
dependabot[bot]
980bea6e4f Bump go.unistack.org/micro/v3 from 3.8.15 to 3.8.18
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.15 to 3.8.18.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.15...v3.8.18)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-21 18:19:33 +00:00
a7ce52d564 Merge pull request #43 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.8.15
Bump go.unistack.org/micro/v3 from 3.8.14 to 3.8.15
2022-01-19 21:19:09 +03:00
dependabot[bot]
4771d5e12b Bump go.unistack.org/micro/v3 from 3.8.14 to 3.8.15
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.14 to 3.8.15.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.14...v3.8.15)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-19 18:17:23 +00:00
6be01f2cb2 Merge branch 'master' into v3 2022-01-12 17:48:26 +03:00
3829766afd update for latest micro
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2022-01-12 17:47:58 +03:00
a2939066a5 Merge pull request #41 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.8.13
Bump go.unistack.org/micro/v3 from 3.8.12 to 3.8.13
2021-12-28 21:16:44 +03:00
dependabot[bot]
e6718f368a Bump go.unistack.org/micro/v3 from 3.8.12 to 3.8.13
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.12 to 3.8.13.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.12...v3.8.13)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-28 18:15:08 +00:00
381f3e051e Merge pull request #40 from unistack-org/master
merge master
2021-12-16 15:38:33 +03:00
757c458f60 fix for latest micro
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-12-16 15:37:49 +03:00
a2f089f890 Merge pull request #39 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.8.11
Bump go.unistack.org/micro/v3 from 3.8.10 to 3.8.11
2021-11-24 21:19:29 +03:00
dependabot[bot]
d85e3fdce2 Bump go.unistack.org/micro/v3 from 3.8.10 to 3.8.11
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.10 to 3.8.11.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.10...v3.8.11)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-24 18:17:26 +00:00
5f370d1a31 Merge pull request #38 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.8.10
Bump go.unistack.org/micro/v3 from 3.8.9 to 3.8.10
2021-11-19 21:17:58 +03:00
dependabot[bot]
2b47343e07 Bump go.unistack.org/micro/v3 from 3.8.9 to 3.8.10
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.9 to 3.8.10.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.9...v3.8.10)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-19 18:16:11 +00:00
00c1d3618f Merge pull request #37 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.8.9
Bump go.unistack.org/micro/v3 from 3.8.7 to 3.8.9
2021-11-18 21:23:11 +03:00
dependabot[bot]
0f80c9f7ba Bump go.unistack.org/micro/v3 from 3.8.7 to 3.8.9
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.7 to 3.8.9.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.7...v3.8.9)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-18 18:21:19 +00:00
a275ffa001 Merge pull request #36 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.8.7
Bump go.unistack.org/micro/v3 from 3.8.6 to 3.8.7
2021-10-28 21:22:57 +03:00
dependabot[bot]
2800d4c78b Bump go.unistack.org/micro/v3 from 3.8.6 to 3.8.7
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.6 to 3.8.7.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.6...v3.8.7)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-28 18:21:01 +00:00
a609f15425 Merge pull request #35 from unistack-org/dependabot/go_modules/go.unistack.org/micro/v3-3.8.6
Bump go.unistack.org/micro/v3 from 3.8.5 to 3.8.6
2021-10-27 16:34:53 +03:00
dependabot[bot]
83cc922771 Bump go.unistack.org/micro/v3 from 3.8.5 to 3.8.6
Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.5 to 3.8.6.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.5...v3.8.6)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-27 13:29:38 +00:00
2bba395ba3 fixup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 16:28:34 +03:00
a360ef0e8f fixup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 16:25:28 +03:00
5f0e4915f6 fixup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 16:18:11 +03:00
f02efaf18c fixup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 16:08:18 +03:00
279d111d39 fixup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 16:07:12 +03:00
928455a651 fixup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 15:56:36 +03:00
6df77d317d update automerge 2021-10-27 15:56:06 +03:00
e8f7fab079 Merge branch 'v3' 2021-10-27 15:49:00 +03:00
ae6f29e540 update
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 15:44:35 +03:00
de6cdb4e4d update
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 15:37:42 +03:00
0398d969d5 github automerge
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 15:37:42 +03:00
10419bc74a github automerge
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 15:37:42 +03:00
93ce60f59c fix Header and Cookie fill
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 15:37:42 +03:00
61c082cb2f github automerge
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 15:24:55 +03:00
22f1b6551a github automerge
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 15:23:26 +03:00
73b7d998c0 fix Header and Cookie fill
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-27 13:13:33 +03:00
4aca8e1ce0 merge v3 branch (#34)
* guard import

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>

* Bump go.unistack.org/micro/v3 from 3.8.4 to 3.8.5 (#32)

Bumps [go.unistack.org/micro/v3](https://github.com/unistack-org/micro) from 3.8.4 to 3.8.5.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.8.4...v3.8.5)

---
updated-dependencies:
- dependency-name: go.unistack.org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-26 19:01:46 +00:00
3592052ac9 guard import
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-25 20:35:02 +03:00
fd5479f6e7 add ability to send headers and cookies
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-10-25 19:59:37 +03:00
4fe11b430e fix path query requests with templates
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-09-23 08:58:55 +03:00
5dc57579c3 update workflow
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-09-21 21:46:43 +03:00
dependabot[bot]
f974460ff3 Bump github.com/unistack-org/micro/v3 from 3.7.0 to 3.7.1 (#28)
Bumps [github.com/unistack-org/micro/v3](https://github.com/unistack-org/micro) from 3.7.0 to 3.7.1.
- [Release notes](https://github.com/unistack-org/micro/releases)
- [Commits](https://github.com/unistack-org/micro/compare/v3.7.0...v3.7.1)

---
updated-dependencies:
- dependency-name: github.com/unistack-org/micro/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-09-17 08:27:55 +03:00
5c54b82aaf fixup workflow
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-09-17 08:22:44 +03:00
a5bf3e2edc update workflows
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-09-17 07:47:48 +03:00
3a2bac8723 enable automerge
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-09-16 11:03:10 +03:00
dependabot[bot]
9a72dba9df Bump github.com/unistack-org/micro/v3 from 3.6.2 to 3.7.0 (#27) 2021-09-06 15:03:55 +00:00
00e853e3ee add github stuff
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-09-06 15:23:36 +03:00
5aff0014bb add github stuff
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-09-06 10:32:59 +03:00
807b4dc41a fix for url query params in url
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-09-01 02:25:29 +03:00
a1f432fac9 fix parsing of url query with all methods
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-09-01 02:06:01 +03:00
dd7e872858 update micro
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-08-17 01:23:04 +03:00
57c2a5b95f nolint
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-31 15:01:44 +03:00
73fc3d34f0 minor tweaks
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-31 14:59:09 +03:00
09e7282b2b fix path template parsing
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-30 23:02:50 +03:00
5f029fd432 use metadata header names
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-23 12:22:27 +03:00
8555ebdd5c remove debug
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-14 22:16:01 +03:00
273da35b92 on error try to return original message to client
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-14 17:05:33 +03:00
556e8dd568 fix lint
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-09 23:10:48 +03:00
f3573e651b fix field setting
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-09 18:22:40 +03:00
8074f9f617 fix field setting
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-09 18:19:30 +03:00
e497b5fa89 rework newRequest
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-09 16:27:49 +03:00
520dc29f89 fixup header filling after making new request
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-09 12:25:26 +03:00
59d6c26003 support metadata option
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-09 11:00:19 +03:00
fade40754a update deps
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-05 16:23:00 +03:00
f39d449ca2 lint
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-05 16:10:38 +03:00
7cab3c18a7 add ability to wrap any struct to error interface
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-07-05 16:02:35 +03:00
7098c252dc allow to publish only body
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-06-11 15:21:36 +03:00
3cbc879769 fix stream timeout
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-29 22:38:38 +03:00
05add422d1 lint fixes (#25)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-25 16:26:36 +03:00
Renovate Bot
b4970ee807 Update module github.com/unistack-org/micro/v3 to v3.3.16 2021-04-20 13:42:24 +00:00
Renovate Bot
f4c91686f4 Update module github.com/unistack-org/micro/v3 to v3.3.15 2021-04-19 14:41:02 +00:00
ee9b7493e3 fix repeated detection
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-19 10:08:56 +03:00
Renovate Bot
0182d6ab56 Update module github.com/unistack-org/micro/v3 to v3.3.14 2021-04-19 04:33:26 +00:00
d99e97090c support repeated url values
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-19 02:10:24 +03:00
8de7912a91 detect response content-type
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-18 15:45:42 +03:00
6ccb40bab0 minor content-type fixes
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-16 17:09:29 +03:00
2b16a8a7a6 allow to set content type with charset
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-10 12:46:34 +03:00
Renovate Bot
d051256839 Update module github.com/unistack-org/micro/v3 to v3.3.13 2021-04-10 00:54:11 +00:00
68b32989fc allow to override content-type
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-09 23:12:25 +03:00
21a41a8e03 fix message body parsing
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-09 22:55:01 +03:00
9150958044 fix x-www-form-urlencoded requests
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-04-03 11:55:22 +03:00
Renovate Bot
e358db44ca Update module github.com/unistack-org/micro/v3 to v3.3.10 2021-04-01 00:36:29 +00:00
5cdd48329e metadata.FromContext => metadata.FromOutgoingContext
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-31 10:43:38 +03:00
Renovate Bot
01b5e1db54 Update module github.com/unistack-org/micro/v3 to v3.3.9 2021-03-30 01:17:00 +00:00
Renovate Bot
900e08458b Update module github.com/unistack-org/micro/v3 to v3.3.8 2021-03-29 01:30:57 +00:00
Renovate Bot
f4fff1c77a Update module github.com/unistack-org/micro/v3 to v3.3.4 2021-03-26 16:34:13 +00:00
4bbf97a309 add minor dev comment
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-26 15:52:52 +03:00
02f29b0ef3 fix tls issues
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-26 15:48:57 +03:00
Renovate Bot
ba69a7dfcd Update module github.com/unistack-org/micro/v3 to v3.3.2 2021-03-25 14:38:19 +00:00
Renovate Bot
45aee3c441 Update module github.com/unistack-org/micro/v3 to v3.3.1 2021-03-24 23:34:28 +00:00
48 changed files with 9871 additions and 1350 deletions

View File

@@ -1,6 +1,6 @@
---
name: Bug report
about: For reporting bugs in go-micro
about: For reporting bugs in micro
title: "[BUG]"
labels: ''
assignees: ''
@@ -16,9 +16,3 @@ assignees: ''
**How to reproduce the bug:**
If possible, please include a minimal code snippet here.
**Environment:**
Go Version: please paste `go version` output here
```
please paste `go env` output here
```

View File

@@ -1,6 +1,6 @@
---
name: Feature request / Enhancement
about: If you have a need not served by go-micro
about: If you have a need not served by micro
title: "[FEATURE]"
labels: ''
assignees: ''

View File

@@ -1,14 +1,8 @@
---
name: Question
about: Ask a question about go-micro
about: Ask a question about micro
title: ''
labels: ''
assignees: ''
---
Before asking, please check if your question has already been answered:
1. Check the documentation - https://micro.mu/docs/
2. Check the examples and plugins - https://github.com/micro/examples & https://github.com/micro/go-plugins
3. Search existing issues

28
.github/autoapprove.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: "autoapprove"
on:
pull_request_target:
types: [assigned, opened, synchronize, reopened]
workflow_run:
workflows: ["prbuild"]
types:
- completed
permissions:
pull-requests: write
contents: write
jobs:
autoapprove:
runs-on: ubuntu-latest
steps:
- name: approve
run: [ "curl -o tea https://dl.gitea.com/tea/main/tea-main-linux-amd64",
"chmod +x ./tea",
"./tea login add --name unistack --token ${{ secrets.GITHUB_TOKEN }} --url https://git.unistack.org",
"./tea pr --repo ${{ github.event.repository.name }}"
]
if: github.actor == 'vtolstov'
id: approve
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

20
.github/renovate.json vendored
View File

@@ -1,20 +0,0 @@
{
"extends": [
"config:base"
],
"postUpdateOptions": ["gomodTidy"],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"automerge": true
},
{
"groupName": "all deps",
"separateMajorMinor": true,
"groupSlug": "all",
"packagePatterns": [
"*"
]
}
]
}

13
.github/stale.sh vendored
View File

@@ -1,13 +0,0 @@
#!/bin/bash -ex
export PATH=$PATH:$(pwd)/bin
export GO111MODULE=on
export GOBIN=$(pwd)/bin
#go get github.com/rvflash/goup@v0.4.1
#goup -v ./...
#go get github.com/psampaz/go-mod-outdated@v0.6.0
go list -u -m -mod=mod -json all | go-mod-outdated -update -direct -ci || true
#go list -u -m -json all | go-mod-outdated -update

View File

@@ -1,46 +0,0 @@
name: build
on:
push:
branches:
- master
jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- name: setup
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: checkout
uses: actions/checkout@v2
- name: cache
uses: actions/cache@v2
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@v2
- name: lint
uses: golangci/golangci-lint-action@v2
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

53
.github/workflows/job_coverage.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
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 Normal file
View File

@@ -0,0 +1,29 @@
name: lint
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [ master, v3, v4 ]
paths-ignore:
- '.github/**'
- '.gitea/**'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v4
with:
filter: 'blob:none'
- name: setup go
uses: actions/setup-go@v5
with:
cache-dependency-path: "**/*.sum"
go-version: 'stable'
- name: setup deps
run: go get -v ./...
- name: run lint
uses: golangci/golangci-lint-action@v6
with:
version: 'latest'

94
.github/workflows/job_sync.yml vendored Normal file
View File

@@ -0,0 +1,94 @@
name: sync
on:
schedule:
- cron: '*/5 * * * *'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
sync:
if: github.server_url != 'https://github.com'
runs-on: ubuntu-latest
steps:
- name: init
run: |
git config --global user.email "vtolstov <vtolstov@users.noreply.github.com>"
git config --global user.name "github-actions[bot]"
echo "machine git.unistack.org login vtolstov password ${{ secrets.TOKEN_GITEA }}" >> /root/.netrc
echo "machine github.com login vtolstov password ${{ secrets.TOKEN_GITHUB }}" >> /root/.netrc
- name: check master
id: check_master
run: |
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/master | cut -f1)
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/master | cut -f1)
echo "src_hash=$src_hash"
echo "dst_hash=$dst_hash"
if [ "$src_hash" != "$dst_hash" ]; then
echo "sync_needed=true" >> $GITHUB_OUTPUT
else
echo "sync_needed=false" >> $GITHUB_OUTPUT
fi
- name: sync master
if: steps.check_master.outputs.sync_needed == 'true'
run: |
git clone --filter=blob:none --filter=tree:0 --branch master --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
cd repo
git remote add --no-tags --fetch --track master upstream https://github.com/${GITHUB_REPOSITORY}
git pull --rebase upstream master
git push upstream master --progress
git push origin master --progress
cd ../
rm -rf repo
- name: check v3
id: check_v3
run: |
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/v3 | cut -f1)
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/v3 | cut -f1)
echo "src_hash=$src_hash"
echo "dst_hash=$dst_hash"
if [ "$src_hash" != "$dst_hash" ]; then
echo "sync_needed=true" >> $GITHUB_OUTPUT
else
echo "sync_needed=false" >> $GITHUB_OUTPUT
fi
- name: sync v3
if: steps.check_v3.outputs.sync_needed == 'true'
run: |
git clone --filter=blob:none --filter=tree:0 --branch v3 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
cd repo
git remote add --no-tags --fetch --track v3 upstream https://github.com/${GITHUB_REPOSITORY}
git pull --rebase upstream v3
git push upstream v3 --progress
git push origin v3 --progress
cd ../
rm -rf repo
- name: check v4
id: check_v4
run: |
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/v4 | cut -f1)
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/v4 | cut -f1)
echo "src_hash=$src_hash"
echo "dst_hash=$dst_hash"
if [ "$src_hash" != "$dst_hash" ]; then
echo "sync_needed=true" >> $GITHUB_OUTPUT
else
echo "sync_needed=false" >> $GITHUB_OUTPUT
fi
- name: sync v4
if: steps.check_v4.outputs.sync_needed == 'true'
run: |
git clone --filter=blob:none --filter=tree:0 --branch v4 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
cd repo
git remote add --no-tags --fetch --track v4 upstream https://github.com/${GITHUB_REPOSITORY}
git pull --rebase upstream v4
git push upstream v4 --progress
git push origin v4 --progress
cd ../
rm -rf repo

31
.github/workflows/job_test.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
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 Normal file
View File

@@ -0,0 +1,50 @@
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

View File

@@ -1,46 +0,0 @@
name: prbuild
on:
pull_request:
branches:
- master
jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- name: setup
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: checkout
uses: actions/checkout@v2
- name: cache
uses: actions/cache@v2
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@v2
- name: lint
uses: golangci/golangci-lint-action@v2
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 Normal file
View File

@@ -0,0 +1,39 @@
# 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

5
.golangci.yml Normal file
View File

@@ -0,0 +1,5 @@
run:
concurrency: 8
timeout: 5m
issues-exit-code: 1
tests: true

150
README.md
View File

@@ -1,18 +1,26 @@
# HTTP Client
![Coverage](https://img.shields.io/badge/Coverage-42.7%25-yellow)
This plugin is a http client for micro.
This plugin is an HTTP client for [Micro](https://pkg.go.dev/go.unistack.org/micro/v4).
It implements the [micro.Client](https://pkg.go.dev/go.unistack.org/micro/v4/client#Client) interface.
## Overview
The http client wraps `net/http` to provide a robust micro client with service discovery, load balancing and streaming.
It complies with the [micro.Client](https://godoc.org/github.com/unistack-org/micro-client-http#Client) interface.
The HTTP client wraps `net/http` to provide a robust client with service discovery, load balancing and
implements HTTP rules defined in the [google/api/http.proto](https://github.com/googleapis/googleapis/blob/master/google/api/http.proto) specification.
## Limitations
* Streaming is not yet implemented.
* Only protobuf-generated messages are supported.
## Usage
### Use directly
```go
import "github.com/unistack-org/micro-client-http"
import (
"go.unistack.org/micro/v4"
http "go.unistack.org/micro-client-http/v4"
)
service := micro.NewService(
micro.Name("my.service"),
@@ -20,43 +28,127 @@ service := micro.NewService(
)
```
### Call Service
### Simple call
Assuming you have a http service "my.service" with path "/foo/bar"
```go
// new client
client := http.NewClient()
import (
"go.unistack.org/micro/v4/client"
http "go.unistack.org/micro-client-http/v4"
jsoncodec "go.unistack.org/micro-codec-json/v4"
)
// create request/response
request := client.NewRequest("my.service", "/foo/bar", protoRequest{})
response := new(protoResponse)
c := http.NewClient(
client.Codec("application/json", jsoncodec.NewCodec()),
)
// call service
err := client.Call(context.TODO(), request, response)
req := c.NewRequest(
"user-service",
"/user/{user_id}/order/{order_id}",
&protoReq{UserId: "123", OrderId: 456},
)
rsp := new(protoRsp)
err := c.Call(
ctx,
req,
rsp,
client.WithAddress("example.com"),
)
```
or you can call any rest api or site and unmarshal to response struct
### Call with specific options
```go
// new client
client := client.NewClientCallOptions(http.NewClient(), http.Address("https://api.github.com"))
import (
"go.unistack.org/micro/v4/client"
http "go.unistack.org/micro-client-http/v4"
)
req := client.NewRequest("github", "/users/vtolstov", nil)
rsp := make(map[string]interface{})
err := c.Call(context.TODO(), req, &rsp, mhttp.Method(http.MethodGet))
err := c.Call(
ctx,
req,
rsp,
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)
)
```
Look at http_test.go for detailed use.
### Call with request headers
### Encoding
Default protobuf with content-type application/proto
```go
client.NewRequest("service", "/path", protoRequest{})
import (
"go.unistack.org/micro/v4/metadata"
http "go.unistack.org/micro-client-http/v4"
)
ctx := metadata.NewOutgoingContext(ctx, metadata.Pairs(
"Authorization", "Bearer token",
"My-Header", "My-Header-Value",
))
err := c.Call(
ctx,
req,
rsp,
http.Header("Authorization", "true", "My-Header", "false"), // <- call option that declares required/optional headers
)
```
Json with content-type application/json
### Call with response headers
```go
client.NewJsonRequest("service", "/path", jsonRequest{})
import (
"go.unistack.org/micro/v4/metadata"
http "go.unistack.org/micro-client-http/v4"
)
respMetadata := metadata.Metadata{}
err := c.Call(
ctx,
req,
rsp,
client.WithResponseMetadata(&respMetadata), // <- metadata with response headers
)
```
### Call with cookies
```go
import (
"go.unistack.org/micro/v4/metadata"
http "go.unistack.org/micro-client-http/v4"
)
ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs(
"Cookie", "session_id=abc123; theme=dark",
))
err := c.Call(
ctx,
req,
rsp,
http.Cookie("session_id", "true", "theme", "false"), // <- call option that declares required/optional cookies
)
```
### Call with error mapping
```go
import (
http "go.unistack.org/micro-client-http/v4"
jsoncodec "go.unistack.org/micro-codec-json/v4"
)
err := c.Call(
ctx,
req,
rsp,
http.ErrorMap(map[string]any{
"default": &protoDefaultError{}, // <- default case
"403": &protoSpecialError{}, // <- key is the HTTP status code that is mapped to this error
}),
)
```

116
builder/body.go Normal file
View File

@@ -0,0 +1,116 @@
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
}

22
builder/body_option.go Normal file
View File

@@ -0,0 +1,22 @@
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
}

View File

@@ -0,0 +1,79 @@
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())
})
}
}

114
builder/helpers.go Normal file
View File

@@ -0,0 +1,114 @@
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())
}
}

313
builder/path_template.go Normal file
View File

@@ -0,0 +1,313 @@
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
}

View File

@@ -0,0 +1,21 @@
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
}

View File

@@ -0,0 +1,15 @@
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

View File

@@ -0,0 +1,330 @@
syntax = "proto3";
package proto;
option go_package = "go.unistack.org/micro-client-http/v4/proto;proto";
message TestRequestBuilder {}
message Test_PathOnly {
message PrimitiveCase {
string userId = 1 [json_name = "user_id"];
int64 orderId = 2 [json_name = "order_id"];
}
message NestedCase {
User user = 1;
Order order = 2;
message User {
string id = 1;
}
message Order {
int64 id = 1;
Product product = 2;
message Product {
int64 id = 1;
}
}
}
message MultipleCase {
string userId = 1 [json_name = "user_id"];
Order order = 2;
message Order {
string id = 1;
}
}
message RepeatedCase {
repeated string userId = 1 [json_name = "user_id"];
int64 orderId = 2 [json_name = "order_id"];
}
message NonPrimitiveMessageCase {
User userId = 1 [json_name = "user_id"];
int64 orderId = 2 [json_name = "order_id"];
message User {
string id = 1;
}
}
message NonPrimitiveMapCase {
map<string, string> userId = 1 [json_name = "user_id"];
int64 orderId = 2 [json_name = "order_id"];
}
message PatternCase {
string pattern = 1;
}
message CompositePatternCase {
string pattern = 1;
string orderId = 2 [json_name = "order_id"];
string productId = 3 [json_name = "product_id"];
}
}
message Test_QueryOnly {
message PrimitiveCase {
string userId = 1 [json_name = "user_id"];
int64 orderId = 2 [json_name = "order_id"];
bool flag = 3;
}
message RepeatedCase {
repeated string strings = 1;
repeated int64 integers = 2;
}
message NestedMessageCase {
string userId = 1 [json_name = "user_id"];
Filter filter = 2;
message Filter {
int64 age = 1;
string name = 2;
SubFilter subFilter = 3 [json_name = "sub_filter"];
message SubFilter {
int64 subAge = 1 [json_name = "sub_age"];
string subName = 2 [json_name = "sub_name"];
}
}
}
message NestedMapCase {
string userId = 1 [json_name = "user_id"];
map<string, string> firstFilter = 2 [json_name = "first_filter"];
map<string, SubFilter> secondFilter = 4 [json_name = "second_filter"];
message SubFilter {
int64 subAge = 1 [json_name = "sub_age"];
string subName = 2 [json_name = "sub_name"];
}
}
message MultipleCase {
string userId = 1 [json_name = "user_id"];
repeated string strings = 2;
Filter firstFilter = 3 [json_name = "first_filter"];
map<string, SubFilter> secondFilter = 4 [json_name = "second_filter"];
message Filter {
int64 age = 1;
SubFilter subFilter = 2 [json_name = "sub_filter"];
}
message SubFilter {
int64 subAge = 1 [json_name = "sub_age"];
}
}
message RepeatedMessageCase {
repeated Filter filters = 1;
message Filter {
int64 age = 1;
}
}
}
message Test_BodyOnly {
message PrimitiveCase {
string userId = 1 [json_name = "user_id"];
int64 orderId = 2 [json_name = "order_id"];
bool flag = 3;
repeated string strings = 4;
Product product = 6;
message Product {
string id = 1;
string name = 2;
}
}
message NestedCase {
string userId = 1 [json_name = "user_id"];
Filter first_filter = 2 [json_name = "first_filter"];
Filter second_filter = 3 [json_name = "second_filter"];
message Filter {
int64 age = 1;
string name = 2;
SubFilter subFilter = 3 [json_name = "sub_filter"];
message SubFilter {
int64 subAge = 1 [json_name = "sub_age"];
string subName = 2 [json_name = "sub_name"];
}
}
}
message RepeatedMessageCase {
string userId = 1 [json_name = "user_id"];
repeated Product products = 2 [json_name = "products"];
message Product {
string id = 1;
string name = 2;
}
}
message MapCase {
map<string, string> firstFilter = 1 [json_name = "first_filter"];
map<string, SubFilter> secondFilter = 2 [json_name = "second_filter"];
message SubFilter {
int64 subAge = 1 [json_name = "sub_age"];
string subName = 2 [json_name = "sub_name"];
}
}
message MultipleCase {
string userId = 1 [json_name = "user_id"];
repeated SubFilter firstFilter = 2 [json_name = "first_filter"];
map<string, SubFilter> secondFilter = 3 [json_name = "second_filter"];
SubFilter thirdFilter = 4 [json_name = "third_filter"];
message SubFilter {
int64 subAge = 1 [json_name = "sub_age"];
string subName = 2 [json_name = "sub_name"];
}
}
}
message Test_Mixed {
message PrimitiveCase {
string userId = 1 [json_name = "user_id"];
int64 orderId = 2 [json_name = "order_id"];
Product product = 3;
message Product {
string id = 1;
string name = 2;
}
}
message NestedCase {
string userId = 1 [json_name = "user_id"];
Filter first_filter = 2 [json_name = "first_filter"];
Filter second_filter = 3 [json_name = "second_filter"];
message Filter {
int64 age = 1;
string name = 2;
SubFilter subFilter = 3 [json_name = "sub_filter"];
message SubFilter {
int64 subAge = 1 [json_name = "sub_age"];
string subName = 2 [json_name = "sub_name"];
}
}
}
message RepeatedMessageCase {
string userId = 1 [json_name = "user_id"];
repeated Product products = 2 [json_name = "products"];
message Product {
string id = 1;
string name = 2;
}
}
message MapCase {
map<string, string> firstFilter = 1 [json_name = "first_filter"];
map<string, SubFilter> secondFilter = 2 [json_name = "second_filter"];
message SubFilter {
int64 subAge = 1 [json_name = "sub_age"];
string subName = 2 [json_name = "sub_name"];
}
}
message MultipleCase {
string userId = 1 [json_name = "user_id"];
repeated SubFilter firstFilter = 2 [json_name = "first_filter"];
map<string, SubFilter> secondFilter = 3 [json_name = "second_filter"];
SubFilter thirdFilter = 4 [json_name = "third_filter"];
message SubFilter {
int64 subAge = 1 [json_name = "sub_age"];
string subName = 2 [json_name = "sub_name"];
}
}
}
message Benchmark {
message Case5 {
string field1 = 1;
string field2 = 2;
string field3 = 3;
string field4 = 4;
string field5 = 5;
}
message Case10 {
string field1 = 1;
string field2 = 2;
string field3 = 3;
string field4 = 4;
string field5 = 5;
string field6 = 6;
string field7 = 7;
string field8 = 8;
string field9 = 9;
string field10 = 10;
}
message Case30 {
string field1 = 1;
string field2 = 2;
string field3 = 3;
string field4 = 4;
string field5 = 5;
string field6 = 6;
string field7 = 7;
string field8 = 8;
string field9 = 9;
string field10 = 10;
string field11 = 11;
string field12 = 12;
string field13 = 13;
string field14 = 14;
string field15 = 15;
string field16 = 16;
string field17 = 17;
string field18 = 18;
string field19 = 19;
string field20 = 20;
string field21 = 21;
string field22 = 22;
string field23 = 23;
string field24 = 24;
string field25 = 25;
string field26 = 26;
string field27 = 27;
string field28 = 28;
string field29 = 29;
string field30 = 30;
}
}
message Test_Client_Call {
message Request {
string userId = 1 [json_name = "user_id"];
int64 orderId = 2 [json_name = "order_id"];
}
message Response {
string id = 1;
string name = 2;
}
message DefaultError {
string code = 1;
string msg = 2;
}
message SpecialError {
string code = 1;
string msg = 2;
string warning = 3;
}
}

194
builder/query.go Normal file
View File

@@ -0,0 +1,194 @@
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
}

180
builder/request_builder.go Normal file
View File

@@ -0,0 +1,180 @@
// 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()
}

View File

@@ -0,0 +1,149 @@
package builder
import (
"fmt"
"math/rand"
"strings"
"testing"
"time"
"google.golang.org/protobuf/proto"
pb "go.unistack.org/micro-client-http/v4/builder/proto"
)
// sink prevents the compiler from optimizing away parsePathTemplate results.
var sink *pathTemplate
func BenchmarkParsePathTemplate(b *testing.B) {
r := rand.New(rand.NewSource(1))
benchInput := func(size int) string {
sb := strings.Builder{}
sb.Grow(size * 10)
for i := 0; i < size; i++ {
name := fmt.Sprintf("var%d", r.Intn(1000))
if r.Intn(5) == 0 {
sb.WriteString(fmt.Sprintf("{%s=**}", name))
} else {
sb.WriteString(fmt.Sprintf("{%s}", name))
}
}
return sb.String()
}
sizes := []int{1_000, 10_000, 50_000, 100_000}
for _, size := range sizes {
input := benchInput(size)
b.Run(fmt.Sprintf("N=%d", size), func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var err error
sink, err = parsePathTemplate(input)
if err != nil && testing.Verbose() {
b.Fatal(err)
}
}
})
}
}
func BenchmarkRequestBuilder(b *testing.B) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
makeMsg := func(fieldCount int) proto.Message {
switch fieldCount {
case 5:
return &pb.Benchmark_Case5{
Field1: fmt.Sprintf("value%d", r.Intn(1000)),
Field2: fmt.Sprintf("value%d", r.Intn(1000)),
Field3: fmt.Sprintf("value%d", r.Intn(1000)),
Field4: fmt.Sprintf("value%d", r.Intn(1000)),
Field5: fmt.Sprintf("value%d", r.Intn(1000)),
}
case 10:
return &pb.Benchmark_Case10{
Field1: fmt.Sprintf("value%d", r.Intn(1000)),
Field2: fmt.Sprintf("value%d", r.Intn(1000)),
Field3: fmt.Sprintf("value%d", r.Intn(1000)),
Field4: fmt.Sprintf("value%d", r.Intn(1000)),
Field5: fmt.Sprintf("value%d", r.Intn(1000)),
Field6: fmt.Sprintf("value%d", r.Intn(1000)),
Field7: fmt.Sprintf("value%d", r.Intn(1000)),
Field8: fmt.Sprintf("value%d", r.Intn(1000)),
Field9: fmt.Sprintf("value%d", r.Intn(1000)),
Field10: fmt.Sprintf("value%d", r.Intn(1000)),
}
case 30:
return &pb.Benchmark_Case30{
Field1: fmt.Sprintf("value%d", r.Intn(1000)),
Field2: fmt.Sprintf("value%d", r.Intn(1000)),
Field3: fmt.Sprintf("value%d", r.Intn(1000)),
Field4: fmt.Sprintf("value%d", r.Intn(1000)),
Field5: fmt.Sprintf("value%d", r.Intn(1000)),
Field6: fmt.Sprintf("value%d", r.Intn(1000)),
Field7: fmt.Sprintf("value%d", r.Intn(1000)),
Field8: fmt.Sprintf("value%d", r.Intn(1000)),
Field9: fmt.Sprintf("value%d", r.Intn(1000)),
Field10: fmt.Sprintf("value%d", r.Intn(1000)),
Field11: fmt.Sprintf("value%d", r.Intn(1000)),
Field12: fmt.Sprintf("value%d", r.Intn(1000)),
Field13: fmt.Sprintf("value%d", r.Intn(1000)),
Field14: fmt.Sprintf("value%d", r.Intn(1000)),
Field15: fmt.Sprintf("value%d", r.Intn(1000)),
Field16: fmt.Sprintf("value%d", r.Intn(1000)),
Field17: fmt.Sprintf("value%d", r.Intn(1000)),
Field18: fmt.Sprintf("value%d", r.Intn(1000)),
Field19: fmt.Sprintf("value%d", r.Intn(1000)),
Field20: fmt.Sprintf("value%d", r.Intn(1000)),
Field21: fmt.Sprintf("value%d", r.Intn(1000)),
Field22: fmt.Sprintf("value%d", r.Intn(1000)),
Field23: fmt.Sprintf("value%d", r.Intn(1000)),
Field24: fmt.Sprintf("value%d", r.Intn(1000)),
Field25: fmt.Sprintf("value%d", r.Intn(1000)),
Field26: fmt.Sprintf("value%d", r.Intn(1000)),
Field27: fmt.Sprintf("value%d", r.Intn(1000)),
Field28: fmt.Sprintf("value%d", r.Intn(1000)),
Field29: fmt.Sprintf("value%d", r.Intn(1000)),
Field30: fmt.Sprintf("value%d", r.Intn(1000)),
}
default:
b.Fatal("undefined field count")
return nil
}
}
tests := []struct {
name string
pathTmpl string
bodyOption string
}{
{"all fields in path", "/resource/{field1}/{field2}", ""},
{"single field body", "/resource/{field1}", "field4"},
{"full body", "/resource", "*"},
}
for _, fields := range []int{5, 10, 30} {
for _, tt := range tests {
b.Run(fmt.Sprintf("%s_%d_fields", tt.name, fields), func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
msg := makeMsg(fields)
rb, err := NewRequestBuilder(tt.pathTmpl, "POST", tt.bodyOption, msg)
if err != nil {
b.Fatalf("new request builder: %v", err)
}
_, _, err = rb.Build()
if err != nil {
b.Fatalf("build: %v", err)
}
}
})
}
}
}

File diff suppressed because it is too large Load Diff

44
builder/used_fields.go Normal file
View File

@@ -0,0 +1,44 @@
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)
}

View File

@@ -0,0 +1,78 @@
package builder
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestNewUsedFields(t *testing.T) {
u := newUsedFields()
require.NotNil(t, u)
require.NotNil(t, u.full)
require.NotNil(t, u.top)
require.Len(t, u.full, 0)
require.Len(t, u.top, 0)
}
func TestUsedFields_Add(t *testing.T) {
u := newUsedFields()
u.add("user.name")
u.add("profile")
_, ok := u.full["user.name"]
require.True(t, ok)
_, ok = u.full["profile"]
require.True(t, ok)
_, ok = u.top["user"]
require.True(t, ok)
_, ok = u.top["profile"]
require.True(t, ok)
require.Len(t, u.full, 2)
require.Len(t, u.top, 2)
}
func TestUsedFields_HasFullKey(t *testing.T) {
u := newUsedFields()
u.add("user.name")
require.True(t, u.hasFullKey("user.name"))
require.False(t, u.hasFullKey("user.email"))
}
func TestUsedFields_HasTopLevelKey(t *testing.T) {
u := newUsedFields()
u.add("user.name")
u.add("settings.theme")
require.True(t, u.hasTopLevelKey("user"))
require.True(t, u.hasTopLevelKey("settings"))
require.False(t, u.hasTopLevelKey("profile"))
}
func TestUsedFields_AddDuplicate(t *testing.T) {
u := newUsedFields()
u.add("user.name")
u.add("user.name")
require.True(t, u.hasFullKey("user.name"))
require.True(t, u.hasTopLevelKey("user"))
require.Len(t, u.full, 1)
require.Len(t, u.top, 1)
}
func TestUsedFields_Len(t *testing.T) {
u := newUsedFields()
u.add("user.name")
u.add("profile")
u.add("user.name")
u.add("profile")
require.Equal(t, u.len(), 2)
}

122
client.go Normal file
View File

@@ -0,0 +1,122 @@
package http
import (
"context"
"net/http"
"strconv"
"sync"
"time"
"go.unistack.org/micro/v4/client"
"go.unistack.org/micro/v4/errors"
"go.unistack.org/micro/v4/options"
"go.unistack.org/micro/v4/semconv"
"go.unistack.org/micro/v4/tracer"
)
var DefaultContentType = "application/json"
type Client struct {
funcCall client.FuncCall
funcStream client.FuncStream
httpClient *http.Client
opts client.Options
mu sync.RWMutex
}
func NewClient(opts ...client.Option) *Client {
clientOpts := client.NewOptions(opts...)
if len(clientOpts.ContentType) == 0 {
clientOpts.ContentType = DefaultContentType
}
c := &Client{opts: clientOpts}
dialer, ok := httpDialerFromOpts(clientOpts)
if !ok {
dialer = defaultHTTPDialer()
}
c.httpClient, ok = httpClientFromOpts(clientOpts)
if !ok {
c.httpClient = defaultHTTPClient(dialer, clientOpts.TLSConfig)
}
c.funcCall = c.fnCall
c.funcStream = c.fnStream
return c
}
func (c *Client) Name() string {
return c.opts.Name
}
func (c *Client) Init(opts ...client.Option) error {
for _, o := range opts {
o(&c.opts)
}
c.opts.Hooks.EachPrev(func(hook options.Hook) {
switch h := hook.(type) {
case client.HookCall:
c.funcCall = h(c.funcCall)
case client.HookStream:
c.funcStream = h(c.funcStream)
}
})
return nil
}
func (c *Client) Options() client.Options {
return c.opts
}
func (c *Client) NewRequest(service, method string, req any, opts ...client.RequestOption) client.Request {
reqOpts := client.NewRequestOptions(opts...)
if reqOpts.ContentType == "" {
reqOpts.ContentType = c.opts.ContentType
}
return &httpRequest{
service: service,
method: method,
request: req,
opts: reqOpts,
}
}
func (c *Client) Call(ctx context.Context, req client.Request, rsp any, opts ...client.CallOption) error {
ts := time.Now()
c.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc()
var sp tracer.Span
ctx, sp = c.opts.Tracer.Start(ctx, req.Endpoint()+" rpc-client",
tracer.WithSpanKind(tracer.SpanKindClient),
tracer.WithSpanLabels("endpoint", req.Endpoint()),
)
err := c.funcCall(ctx, req, rsp, opts...)
c.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Dec()
te := time.Since(ts)
c.opts.Meter.Summary(semconv.ClientRequestLatencyMicroseconds, "endpoint", req.Endpoint()).Update(te.Seconds())
c.opts.Meter.Histogram(semconv.ClientRequestDurationSeconds, "endpoint", req.Endpoint()).Update(te.Seconds())
if me := errors.FromError(err); me == nil {
sp.Finish()
c.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "success", "code", strconv.Itoa(int(200))).Inc()
} else {
sp.SetStatus(tracer.SpanStatusError, err.Error())
c.opts.Meter.Counter(semconv.ClientRequestTotal, "endpoint", req.Endpoint(), "status", "failure", "code", strconv.Itoa(int(me.Code))).Inc()
}
return err
}
func (c *Client) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
return c.funcStream(ctx, req, opts...)
}
func (c *Client) String() string {
return "http"
}

263
client_helpers.go Normal file
View File

@@ -0,0 +1,263 @@
package http
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"go.unistack.org/micro/v4/client"
"go.unistack.org/micro/v4/codec"
"go.unistack.org/micro/v4/logger"
"go.unistack.org/micro/v4/metadata"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"go.unistack.org/micro-client-http/v4/builder"
)
func buildHTTPRequest(
ctx context.Context,
addr string,
path string,
ct string,
cf codec.Codec,
msg any,
opts client.CallOptions,
log logger.Logger,
) (
*http.Request,
error,
) {
protoMsg, ok := msg.(proto.Message)
if !ok {
return nil, errors.New("msg must be a proto message type")
}
var (
method = http.MethodPost
bodyOpt string
parameters = map[string]map[string]string{}
)
if opts.Context != nil {
if v, ok := methodFromOpts(opts); ok {
method = v
}
if v, ok := pathFromOpts(opts); ok {
path = v
}
if v, ok := bodyFromOpts(opts); ok {
bodyOpt = v
}
if h, ok := headerFromOpts(opts); ok && len(h) > 0 {
m, ok := parameters["header"]
if !ok {
m = make(map[string]string)
parameters["header"] = m
}
for idx := 0; idx+1 < len(h); idx += 2 {
m[h[idx]] = h[idx+1]
}
}
if c, ok := cookieFromOpts(opts); ok && len(c) > 0 {
m, ok := parameters["cookie"]
if !ok {
m = make(map[string]string)
parameters["cookie"] = m
}
for idx := 0; idx+1 < len(c); idx += 2 {
m[c[idx]] = c[idx+1]
}
}
}
reqBuilder, err := builder.NewRequestBuilder(path, method, bodyOpt, protoMsg)
if err != nil {
return nil, fmt.Errorf("new request builder: %w", err)
}
resolvedPath, newMsg, err := reqBuilder.Build()
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
resolvedURL := joinURL(addr, resolvedPath)
u, err := normalizeURL(resolvedURL)
if err != nil {
return nil, fmt.Errorf("normalize url: %w", err)
}
body, err := marshallMsg(cf, newMsg)
if err != nil {
return nil, fmt.Errorf("marshal msg: %w", err)
}
var hreq *http.Request
if len(body) > 0 {
hreq, err = http.NewRequestWithContext(ctx, method, u.String(), io.NopCloser(bytes.NewBuffer(body)))
hreq.ContentLength = int64(len(body))
} else {
hreq, err = http.NewRequestWithContext(ctx, method, u.String(), nil)
}
if err != nil {
return nil, fmt.Errorf("new http request: %w", err)
}
setHeadersAndCookies(ctx, hreq, ct, opts)
if err = validateHeadersAndCookies(hreq, parameters); err != nil {
return nil, fmt.Errorf("validate headers and cookies: %w", err)
}
if log.V(logger.DebugLevel) {
log.Debug(
ctx,
fmt.Sprintf("request %s to %s with headers %v body %s", method, u.String(), hreq.Header, body),
)
}
return hreq, nil
}
func joinURL(addr, resolvedPath string) string {
if addr == "" {
return resolvedPath
}
if resolvedPath == "" {
return addr
}
switch {
case strings.HasSuffix(addr, "/") && strings.HasPrefix(resolvedPath, "/"):
return addr + resolvedPath[1:]
case !strings.HasSuffix(addr, "/") && !strings.HasPrefix(resolvedPath, "/"):
return addr + "/" + resolvedPath
default:
return addr + resolvedPath
}
}
func normalizeURL(raw string) (*url.URL, error) {
if !strings.Contains(raw, "://") {
raw = "http://" + raw
}
u, err := url.Parse(raw)
if err != nil {
return nil, fmt.Errorf("parse url: %w", err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, fmt.Errorf("invalid scheme: %q (must be http or https)", u.Scheme)
}
if u.Host == "" {
return nil, errors.New("missing host in url")
}
return u, nil
}
func marshallMsg(cf codec.Codec, msg proto.Message) ([]byte, error) {
if msg == nil {
return nil, nil
}
isEmpty := true
msg.ProtoReflect().Range(func(protoreflect.FieldDescriptor, protoreflect.Value) bool {
isEmpty = false
return false
})
if isEmpty {
return nil, nil
}
return cf.Marshal(msg)
}
func setHeadersAndCookies(ctx context.Context, r *http.Request, ct string, opts client.CallOptions) {
r.Header = make(http.Header)
r.Header.Set(metadata.HeaderContentType, ct)
r.Header.Set("Content-Length", fmt.Sprintf("%d", r.ContentLength))
if opts.AuthToken != "" {
r.Header.Set(metadata.HeaderAuthorization, opts.AuthToken)
}
if opts.StreamTimeout > time.Duration(0) {
r.Header.Set(metadata.HeaderTimeout, fmt.Sprintf("%d", opts.StreamTimeout))
}
if opts.RequestTimeout > time.Duration(0) {
r.Header.Set(metadata.HeaderTimeout, fmt.Sprintf("%d", opts.RequestTimeout))
}
if opts.RequestMetadata != nil {
for k, v := range opts.RequestMetadata {
if k == "Cookie" {
applyCookies(r, v)
continue
}
r.Header[k] = append(r.Header[k], v...)
}
}
if md, ok := metadata.FromOutgoingContext(ctx); ok {
for k, v := range md {
if k == "Cookie" {
applyCookies(r, v)
continue
}
r.Header[k] = append(r.Header[k], v...)
}
}
}
func applyCookies(r *http.Request, rawCookies []string) {
if len(rawCookies) == 0 {
return
}
raw := strings.Join(rawCookies, "; ")
tmp := http.Request{Header: http.Header{}}
tmp.Header.Set("Cookie", raw)
for _, c := range tmp.Cookies() {
r.AddCookie(c)
}
}
func validateHeadersAndCookies(r *http.Request, parameters map[string]map[string]string) error {
if headers, ok := parameters["header"]; ok {
for name, required := range headers {
if required == "true" && r.Header.Get(name) == "" {
return fmt.Errorf("missing required header: %s", name)
}
}
}
if cookies, ok := parameters["cookie"]; ok {
cookieMap := map[string]string{}
for _, c := range r.Cookies() {
cookieMap[c.Name] = c.Value
}
for name, required := range cookies {
if required == "true" {
if _, ok := cookieMap[name]; !ok {
return fmt.Errorf("missing required cookie: %s", name)
}
}
}
}
return nil
}

401
client_helpers_test.go Normal file
View File

@@ -0,0 +1,401 @@
package http
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
jsoncodec "go.unistack.org/micro-codec-json/v4"
"google.golang.org/protobuf/proto"
pb "go.unistack.org/micro-client-http/v4/builder/proto"
)
func TestJoinURL(t *testing.T) {
tests := []struct {
name string
addr string
path string
want string
}{
{
name: "both without slash",
addr: "http://example.com",
path: "api/v1",
want: "http://example.com/api/v1",
},
{
name: "addr with slash, path without slash",
addr: "http://example.com/",
path: "api/v1",
want: "http://example.com/api/v1",
},
{
name: "addr without slash, path with slash",
addr: "http://example.com",
path: "/api/v1",
want: "http://example.com/api/v1",
},
{
name: "both with slash",
addr: "http://example.com/",
path: "/api/v1",
want: "http://example.com/api/v1",
},
{
name: "empty addr",
addr: "",
path: "/api/v1",
want: "/api/v1",
},
{
name: "empty path",
addr: "http://example.com",
path: "",
want: "http://example.com",
},
{
name: "both empty",
addr: "",
path: "",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, joinURL(tt.addr, tt.path))
})
}
}
func TestNormalizeURL(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "host with port",
input: "localhost:8080",
want: "http://localhost:8080",
wantErr: false,
},
{
name: "http with host",
input: "http://example.com",
want: "http://example.com",
wantErr: false,
},
{
name: "http with no host",
input: "http://",
want: "",
wantErr: true,
},
{
name: "https with host",
input: "https://example.com",
want: "https://example.com",
wantErr: false,
},
{
name: "https with no host",
input: "https://",
want: "",
wantErr: true,
},
{
name: "invalid scheme",
input: "ftp://example.com",
want: "",
wantErr: true,
},
{
name: "IPv4 without scheme",
input: "127.0.0.1:9000",
want: "http://127.0.0.1:9000",
wantErr: false,
},
{
name: "IPv4 with scheme",
input: "http://127.0.0.1:8080",
want: "http://127.0.0.1:8080",
wantErr: false,
},
{
name: "IPv6 without scheme",
input: "[::1]:8080",
want: "http://[::1]:8080",
wantErr: false,
},
{
name: "IPv6 with scheme",
input: "https://[::1]:443",
want: "https://[::1]:443",
wantErr: false,
},
{
name: "hostname only",
input: "my-service",
want: "http://my-service",
wantErr: false,
},
{
name: "hostname with path",
input: "service.local/api/v1",
want: "http://service.local/api/v1",
wantErr: false,
},
{
name: "hostname with dash and port",
input: "api-service.local:8080",
want: "http://api-service.local:8080",
wantErr: false,
},
{
name: "just path",
input: "/api/v1",
want: "",
wantErr: true,
},
{
name: "empty string",
input: "",
want: "",
wantErr: true,
},
{
name: "http with query params",
input: "http://example.com?x=1&y=2",
want: "http://example.com?x=1&y=2",
wantErr: false,
},
{
name: "http with fragment",
input: "http://example.com/path#section1",
want: "http://example.com/path#section1",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := normalizeURL(tt.input)
if tt.wantErr {
require.Error(t, err)
require.Nil(t, result)
} else {
require.NoError(t, err)
require.Equal(t, tt.want, result.String())
}
})
}
}
func TestMarshallMsg(t *testing.T) {
type request = pb.Test_Client_Call_Request
tests := []struct {
name string
msg proto.Message
expected string
}{
{
name: "empty",
msg: &request{},
expected: "",
},
{
name: "nil",
msg: nil,
expected: "",
},
{
name: "valid",
msg: &request{UserId: "123", OrderId: 456},
expected: `{"userId":"123","orderId":456}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := marshallMsg(jsoncodec.NewCodec(), tt.msg)
require.NoError(t, err)
require.Equal(t, tt.expected, string(result))
})
}
}
func TestApplyCookies(t *testing.T) {
tests := []struct {
name string
rawCookies []string
want []*http.Cookie
}{
{
name: "empty",
rawCookies: []string{},
want: []*http.Cookie{},
},
{
name: "single cookie",
rawCookies: []string{"session=abc123"},
want: []*http.Cookie{
{Name: "session", Value: "abc123"},
},
},
{
name: "multiple cookies separate items",
rawCookies: []string{"session=abc123", "user=john"},
want: []*http.Cookie{
{Name: "session", Value: "abc123"},
{Name: "user", Value: "john"},
},
},
{
name: "multiple cookies in one item",
rawCookies: []string{"a=1; b=2"},
want: []*http.Cookie{
{Name: "a", Value: "1"},
{Name: "b", Value: "2"},
},
},
{
name: "mix of combined and separate cookies",
rawCookies: []string{"a=1; b=2", "c=3"},
want: []*http.Cookie{
{Name: "a", Value: "1"},
{Name: "b", Value: "2"},
{Name: "c", Value: "3"},
},
},
{
name: "duplicate cookies",
rawCookies: []string{"session=abc123", "session=xyz"},
want: []*http.Cookie{
{Name: "session", Value: "abc123"},
{Name: "session", Value: "xyz"},
},
},
{
name: "cookie with spaces",
rawCookies: []string{"token=abc 123"},
want: []*http.Cookie{
{Name: "token", Value: "abc 123", Quoted: true},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
applyCookies(req, tt.rawCookies)
require.Equal(t, tt.want, req.Cookies())
})
}
}
func TestValidateHeadersAndCookies(t *testing.T) {
tests := []struct {
name string
prepareRequest func() *http.Request
parameters map[string]map[string]string
wantErr bool
}{
{
name: "all required headers and cookies present",
prepareRequest: func() *http.Request {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("My-Header-1", "Header-Value-1")
req.Header.Set("My-Header-2", "Header-Value-2")
req.AddCookie(&http.Cookie{Name: "session-1", Value: "abc-1"})
req.AddCookie(&http.Cookie{Name: "session-2", Value: "abc-2"})
return req
},
parameters: map[string]map[string]string{
"header": {"My-Header-1": "true", "My-Header-2": "true"},
"cookie": {"session-1": "true", "session-2": "true"},
},
wantErr: false,
},
{
name: "missing required header",
prepareRequest: func() *http.Request {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("My-Header-1", "Header-Value-1")
req.AddCookie(&http.Cookie{Name: "session-1", Value: "abc-1"})
req.AddCookie(&http.Cookie{Name: "session-2", Value: "abc-2"})
return req
},
parameters: map[string]map[string]string{
"header": {"My-Header-1": "true", "My-Header-2": "true"},
"cookie": {"session-1": "true", "session-2": "true"},
},
wantErr: true,
},
{
name: "missing required cookie",
prepareRequest: func() *http.Request {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("My-Header-1", "Header-Value-1")
req.Header.Set("My-Header-2", "Header-Value-2")
req.AddCookie(&http.Cookie{Name: "session-1", Value: "abc-1"})
return req
},
parameters: map[string]map[string]string{
"header": {"My-Header-1": "true", "My-Header-2": "true"},
"cookie": {"session-1": "true", "session-2": "true"},
},
wantErr: true,
},
{
name: "optional header and cookie not provided partially",
prepareRequest: func() *http.Request {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("My-Header-1", "Header-Value-1")
req.AddCookie(&http.Cookie{Name: "session-1", Value: "abc-1"})
return req
},
parameters: map[string]map[string]string{
"header": {"My-Header-1": "true", "My-Header-2": "false"},
"cookie": {"session-1": "true", "session-2": "false"},
},
wantErr: false,
},
{
name: "optional header and cookie not provided",
prepareRequest: func() *http.Request {
req := httptest.NewRequest("GET", "/", nil)
return req
},
parameters: map[string]map[string]string{
"header": {"My-Header-1": "false", "My-Header-2": "false"},
"cookie": {"session-1": "false", "session-2": "false"},
},
wantErr: false,
},
{
name: "no headers or cookies required",
prepareRequest: func() *http.Request {
req := httptest.NewRequest("GET", "/", nil)
return req
},
parameters: map[string]map[string]string{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateHeadersAndCookies(tt.prepareRequest(), tt.parameters)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

13
client_stream.go Normal file
View File

@@ -0,0 +1,13 @@
package http
import (
"context"
"go.unistack.org/micro/v4/client"
)
// TODO: Add stream support in the future.
func (c *Client) fnStream(context.Context, client.Request, ...client.CallOption) (client.Stream, error) {
panic("not implemented")
}

273
client_unary_call.go Normal file
View File

@@ -0,0 +1,273 @@
package http
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
"go.unistack.org/micro/v4/client"
"go.unistack.org/micro/v4/codec"
"go.unistack.org/micro/v4/errors"
"go.unistack.org/micro/v4/logger"
"go.unistack.org/micro/v4/metadata"
"go.unistack.org/micro/v4/selector"
)
func (c *Client) fnCall(ctx context.Context, req client.Request, rsp any, opts ...client.CallOption) error {
// make a copy of call opts
callOpts := c.opts.CallOptions
for _, opt := range opts {
opt(&callOpts)
}
// check if we already have a deadline
d, ok := ctx.Deadline()
if !ok {
var cancel context.CancelFunc
// no deadline so we create a new one
ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout)
defer cancel()
} else {
// got a deadline so no need to setup context,
// but we need to set the timeout we pass along
opt := client.WithRequestTimeout(time.Until(d))
opt(&callOpts)
}
// should we noop right here?
select {
case <-ctx.Done():
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
default:
}
// make copy of call method
hcall := c.call
// use the router passed as a call option, or fallback to the rpc clients router
if callOpts.Router == nil {
callOpts.Router = c.opts.Router
}
if callOpts.Selector == nil {
callOpts.Selector = c.opts.Selector
}
// inject proxy address
// TODO: don't even bother using Lookup/Select in this case
if len(c.opts.Proxy) > 0 {
callOpts.Address = []string{c.opts.Proxy}
}
var next selector.Next
call := func(i int) error {
// call backoff first. Someone may want an initial start delay
t, err := callOpts.Backoff(ctx, req, i)
if err != nil {
return errors.InternalServerError("go.micro.client", "%+v", err)
}
// only sleep if greater than 0
if t.Seconds() > 0 {
time.Sleep(t)
}
if next == nil {
var routes []string
// lookup the route to send the reques to
// TODO apply any filtering here
routes, err = c.opts.Lookup(ctx, req, callOpts)
if err != nil {
return errors.InternalServerError("go.micro.client", "%+v", err)
}
// balance the list of nodes
next, err = callOpts.Selector.Select(routes)
if err != nil {
return errors.InternalServerError("go.micro.client", "%+v", err)
}
}
node := next()
// make the call
err = hcall(ctx, node, req, rsp, callOpts)
// record the result of the call to inform future routing decisions
if verr := c.opts.Selector.Record(node, err); verr != nil {
return errors.InternalServerError("go.micro.client", "%+v", verr)
}
// try and transform the error to micro error
if verr, ok := err.(*errors.Error); ok {
return verr
}
return err
}
ch := make(chan error, callOpts.Retries)
var gerr error
for i := 0; i <= callOpts.Retries; i++ {
go func() {
ch <- call(i)
}()
select {
case <-ctx.Done():
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
case err := <-ch:
// if the call succeeded lets bail early
if err == nil {
return nil
}
retry, rerr := callOpts.Retry(ctx, req, i, err)
if rerr != nil {
return rerr
}
if !retry {
return err
}
gerr = err
}
}
return gerr
}
func (c *Client) call(ctx context.Context, addr string, req client.Request, rsp any, opts client.CallOptions) error {
ct := req.ContentType()
if len(opts.ContentType) > 0 {
ct = opts.ContentType
}
cf, err := c.newCodec(ct)
if err != nil {
return errors.BadRequest("go.micro.client", "%+v", err)
}
hreq, err := buildHTTPRequest(ctx, addr, req.Endpoint(), ct, cf, req.Body(), opts, c.opts.Logger)
if err != nil {
return errors.BadRequest("go.micro.client", "%+v", err)
}
hrsp, err := c.httpClient.Do(hreq)
if err != nil {
switch err := err.(type) {
case *url.Error:
if err, ok := err.Err.(net.Error); ok && err.Timeout() {
return errors.Timeout("go.micro.client", "%+v", err)
}
case net.Error:
if err.Timeout() {
return errors.Timeout("go.micro.client", "%+v", err)
}
}
return errors.InternalServerError("go.micro.client", "%+v", err)
}
defer hrsp.Body.Close()
return c.parseRsp(ctx, hrsp, rsp, opts)
}
func (c *Client) newCodec(ct string) (codec.Codec, error) {
c.mu.RLock()
defer c.mu.RUnlock()
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
ct = ct[:idx]
}
if cf, ok := c.opts.Codecs[ct]; ok {
return cf, nil
}
return nil, codec.ErrUnknownContentType
}
func (c *Client) parseRsp(ctx context.Context, hrsp *http.Response, rsp any, opts client.CallOptions) error {
log := c.opts.Logger
select {
case <-ctx.Done():
return ctx.Err()
default:
}
var buf []byte
if opts.ResponseMetadata != nil {
for k, v := range hrsp.Header {
opts.ResponseMetadata.Set(k, strings.Join(v, ","))
}
}
if hrsp.StatusCode == http.StatusNoContent {
return nil
}
ct := DefaultContentType
if htype := hrsp.Header.Get(metadata.HeaderContentType); htype != "" {
ct = htype
}
if hrsp.Body != nil {
var err error
buf, err = io.ReadAll(hrsp.Body)
if err != nil {
return errors.InternalServerError("go.micro.client", "read body: %v", err)
}
}
cf, err := c.newCodec(ct)
if err != nil {
return errors.InternalServerError("go.micro.client", "unknown content-type %s: %v", ct, err)
}
if log.V(logger.DebugLevel) {
log.Debug(ctx, fmt.Sprintf("response with headers: %v and body: %s", hrsp.Header, buf))
}
if hrsp.StatusCode < http.StatusBadRequest {
if err = cf.Unmarshal(buf, rsp); err != nil {
return errors.InternalServerError("go.micro.client", "unmarshal response: %v", err)
}
return nil
}
var mappedErr any
errMap, ok := errorMapFromOpts(opts)
if ok && errMap != nil {
mappedErr, ok = errMap[fmt.Sprintf("%d", hrsp.StatusCode)]
if !ok {
mappedErr, ok = errMap["default"]
}
}
if !ok || mappedErr == nil {
return errors.New("go.micro.client", string(buf), int32(hrsp.StatusCode))
}
if err = cf.Unmarshal(buf, mappedErr); err != nil {
return errors.InternalServerError("go.micro.client", "unmarshal response: %v", err)
}
if v, ok := mappedErr.(error); ok {
return v
}
// if the error map item does not implement the error interface, wrap it
return &Error{err: mappedErr}
}

1284
client_unary_call_test.go Normal file

File diff suppressed because it is too large Load Diff

12
error.go Normal file
View File

@@ -0,0 +1,12 @@
package http
import "fmt"
// Error is used when items in the error map do not implement the error interface and need to be wrapped.
type Error struct {
err any
}
func (e *Error) Error() string {
return fmt.Sprintf("%+v", e.err)
}

26
go.mod
View File

@@ -1,5 +1,25 @@
module github.com/unistack-org/micro-client-http/v3
module go.unistack.org/micro-client-http/v4
go 1.16
go 1.23.0
require github.com/unistack-org/micro/v3 v3.3.0
toolchain go1.24.2
require (
github.com/stretchr/testify v1.11.1
go.unistack.org/micro-codec-json/v4 v4.1.0
go.unistack.org/micro/v4 v4.1.19
google.golang.org/protobuf v1.36.9
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/matoous/go-nanoid v1.5.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spf13/cast v1.9.2 // indirect
go.unistack.org/micro-proto/v4 v4.1.0 // indirect
golang.org/x/sys v0.35.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/grpc v1.75.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

62
go.sum
View File

@@ -1,17 +1,47 @@
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/ef-ds/deque v1.0.4/go.mod h1:gXDnTC3yqvBcHbq2lcExjtAcVrOnJCbMcZXmuj8Z4tg=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/silas/dag v0.0.0-20210121180416-41cf55125c34/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
github.com/unistack-org/micro/v3 v3.3.0 h1:pEj/8QVFzMlNMEL//q/Te8qgG+XI6LTYIQrb6hMymgk=
github.com/unistack-org/micro/v3 v3.3.0/go.mod h1:iJwCWq2PECMxigfqe6TPC5GLWvj6P94Kk+PTVZGL3w8=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matoous/go-nanoid v1.5.1 h1:aCjdvTyO9LLnTIi0fgdXhOPPvOHjpXN6Ik9DaNjIct4=
github.com/matoous/go-nanoid v1.5.1/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.unistack.org/micro-codec-json/v4 v4.1.0 h1:iydeSkt3ee7IPU0dHHKlGN97lw+YFQasBk9rdv0woYA=
go.unistack.org/micro-codec-json/v4 v4.1.0/go.mod h1:aUg86elSlURSynTAetDAAXj/VzFDwwcg92QNrRzcvrM=
go.unistack.org/micro-proto/v4 v4.1.0 h1:qPwL2n/oqh9RE3RTTDgt28XK3QzV597VugQPaw9lKUk=
go.unistack.org/micro-proto/v4 v4.1.0/go.mod h1:ArmK7o+uFvxSY3dbJhKBBX4Pm1rhWdLEFf3LxBrMtec=
go.unistack.org/micro/v4 v4.1.19 h1:LKpmSPYvX5B9AkFD7JqMU/U06v5yEWn2bsCG/YKZtZI=
go.unistack.org/micro/v4 v4.1.19/go.mod h1:xleO2M5Yxh4s6I+RUcLrEpUjobefh+71ctrdIfn7TUs=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=

609
http.go
View File

@@ -1,609 +0,0 @@
// Package http provides a http client
package http
import (
"bufio"
"bytes"
"context"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"time"
"github.com/unistack-org/micro/v3/broker"
"github.com/unistack-org/micro/v3/client"
"github.com/unistack-org/micro/v3/codec"
"github.com/unistack-org/micro/v3/errors"
"github.com/unistack-org/micro/v3/metadata"
"github.com/unistack-org/micro/v3/router"
)
var (
DefaultContentType = "application/json"
)
func filterLabel(r []router.Route) []router.Route {
// selector.FilterLabel("protocol", "http")
return r
}
type httpClient struct {
opts client.Options
dialer *net.Dialer
httpcli *http.Client
init bool
}
func newRequest(addr string, req client.Request, ct string, cf codec.Codec, msg interface{}, opts client.CallOptions) (*http.Request, error) {
hreq := &http.Request{Method: http.MethodPost}
body := "*" // as like google api http annotation
var tags []string
var scheme string
u, err := url.Parse(addr)
if err != nil {
hreq.URL = &url.URL{
Scheme: "http",
Host: addr,
Path: req.Endpoint(),
}
hreq.Host = addr
scheme = "http"
} else {
ep := req.Endpoint()
if opts.Context != nil {
if m, ok := opts.Context.Value(methodKey{}).(string); ok {
hreq.Method = m
}
if p, ok := opts.Context.Value(pathKey{}).(string); ok {
ep = 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
}
}
hreq.URL, err = u.Parse(ep)
if err != nil {
return nil, errors.BadRequest("go.micro.client", err.Error())
}
}
if len(tags) == 0 {
switch ct {
default:
tags = append(tags, "json", "protobuf")
case "text/xml":
tags = append(tags, "xml")
}
}
path, nmsg, err := newPathRequest(hreq.URL.Path, hreq.Method, body, msg, tags)
if err != nil {
return nil, errors.BadRequest("go.micro.client", err.Error())
}
if scheme != "" {
hreq.URL, err = url.Parse(scheme + "://" + addr + path)
} else {
hreq.URL, err = url.Parse(addr + path)
}
if err != nil {
return nil, errors.BadRequest("go.micro.client", err.Error())
}
// marshal request is struct not empty
if nmsg != nil {
var b []byte
b, err = cf.Marshal(nmsg)
if err != nil {
return nil, errors.BadRequest("go.micro.client", err.Error())
}
hreq.Body = ioutil.NopCloser(bytes.NewBuffer(b))
hreq.ContentLength = int64(len(b))
}
return hreq, nil
}
func (h *httpClient) call(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error {
header := make(http.Header, 2)
if md, ok := metadata.FromContext(ctx); ok {
for k, v := range md {
header.Set(k, v)
}
}
ct := req.ContentType()
// set timeout in nanoseconds
header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout))
// set the content type for the request
header.Set("Content-Type", ct)
// get codec
cf, err := h.newCodec(ct)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
hreq, err := newRequest(addr, req, ct, cf, req.Body(), opts)
if err != nil {
return err
}
hreq.Header = header
// make the request
hrsp, err := h.httpcli.Do(hreq.WithContext(ctx))
if err != nil {
switch err := err.(type) {
case net.Error:
if err.Timeout() {
return errors.Timeout("go.micro.client", err.Error())
}
case *url.Error:
if err, ok := err.Err.(net.Error); ok && err.Timeout() {
return errors.Timeout("go.micro.client", err.Error())
}
}
return errors.InternalServerError("go.micro.client", err.Error())
}
defer hrsp.Body.Close()
return parseRsp(ctx, hrsp, cf, rsp, opts)
}
func (h *httpClient) stream(ctx context.Context, addr string, req client.Request, opts client.CallOptions) (client.Stream, error) {
var header http.Header
if md, ok := metadata.FromContext(ctx); ok {
header = make(http.Header, len(md)+2)
for k, v := range md {
header.Set(k, v)
}
} else {
header = make(http.Header, 2)
}
ct := req.ContentType()
// set timeout in nanoseconds
header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout))
// set the content type for the request
header.Set("Content-Type", ct)
// get codec
cf, err := h.newCodec(req.ContentType())
if err != nil {
return nil, errors.InternalServerError("go.micro.client", err.Error())
}
dialAddr := addr
u, err := url.Parse(dialAddr)
if err == nil && u.Scheme != "" && u.Host != "" {
dialAddr = u.Host
}
cc, err := h.dialer.DialContext(ctx, "tcp", addr)
if err != nil {
return nil, errors.InternalServerError("go.micro.client", fmt.Sprintf("Error dialing: %v", err))
}
return &httpStream{
address: addr,
context: ctx,
closed: make(chan bool),
opts: opts,
conn: cc,
ct: ct,
cf: cf,
header: header,
reader: bufio.NewReader(cc),
request: req,
}, nil
}
func (h *httpClient) newCodec(ct string) (codec.Codec, error) {
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.Broker.Init(); err != nil {
return err
}
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) NewMessage(topic string, msg interface{}, opts ...client.MessageOption) client.Message {
return newHTTPMessage(topic, msg, h.opts.ContentType, opts...)
}
func (h *httpClient) NewRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request {
return newHTTPRequest(service, method, req, h.opts.ContentType, reqOpts...)
}
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(d.Sub(time.Now()))
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}
}
// 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
}
// 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)
}
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) {
// 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(d.Sub(time.Now()))
opt(&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, err := h.stream()
if err != nil {
return nil, err
}
// 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}
}
// 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
}
call := func(i int) (client.Stream, error) {
// call backoff first. Someone may want an initial start delay
t, err := callOpts.Backoff(ctx, req, i)
if err != nil {
return nil, errors.InternalServerError("go.micro.client", err.Error())
}
// only sleep if greater than 0
if t.Seconds() > 0 {
time.Sleep(t)
}
node := next()
stream, err := h.stream(ctx, node, req, callOpts)
// record the result of the call to inform future routing decisions
if verr := h.opts.Selector.Record(node, err); verr != nil {
return nil, verr
}
// try and transform the error to a go-micro error
if verr, ok := err.(*errors.Error); ok {
return nil, verr
}
return stream, err
}
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, err := call(i)
ch <- response{s, err}
}()
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) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
options := client.NewPublishOptions(opts...)
md, ok := metadata.FromContext(ctx)
if !ok {
md = metadata.New(2)
}
md["Content-Type"] = p.ContentType()
md["Micro-Topic"] = p.Topic()
cf, err := h.newCodec(p.ContentType())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
var body []byte
// passed in raw data
if d, ok := p.Payload().(*codec.Frame); ok {
body = d.Data
} else {
b := bytes.NewBuffer(nil)
if err := cf.Write(b, &codec.Message{Type: codec.Event}, p.Payload()); err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
body = b.Bytes()
}
topic := p.Topic()
// get proxy
if prx := os.Getenv("MICRO_PROXY"); len(prx) > 0 {
options.Exchange = prx
}
// get the exchange
if len(options.Exchange) > 0 {
topic = options.Exchange
}
return h.opts.Broker.Publish(ctx, topic, &broker.Message{
Header: md,
Body: body,
}, broker.PublishContext(ctx))
}
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,
}
if httpcli, ok := options.Context.Value(httpClientKey{}).(*http.Client); ok {
rc.httpcli = httpcli
} else {
rc.httpcli = http.DefaultClient
}
if dialer, ok := options.Context.Value(httpDialerKey{}).(*net.Dialer); ok {
rc.dialer = dialer
} else {
rc.dialer = &net.Dialer{}
}
c := client.Client(rc)
// wrap in reverse
for i := len(options.Wrappers); i > 0; i-- {
c = options.Wrappers[i-1](c)
}
return c
}

View File

@@ -1,40 +0,0 @@
package http
import (
"net/url"
"strings"
"testing"
)
type Request struct {
Name string `json:"name"`
Field1 string
Field2 string
Field3 int64
}
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)
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}
p, m, err := newPathRequest("/api/v1/{xname}/list", "GET", "", req, nil)
if err == nil {
t.Fatalf("path param must not be filled")
}
_, _ = p, m
}

View File

@@ -1,36 +0,0 @@
package http
import (
"github.com/unistack-org/micro/v3/client"
)
type httpMessage struct {
topic string
contentType string
payload interface{}
}
func newHTTPMessage(topic string, payload interface{}, contentType string, opts ...client.MessageOption) client.Message {
options := client.NewMessageOptions(opts...)
if len(options.ContentType) == 0 {
options.ContentType = contentType
}
return &httpMessage{
payload: payload,
topic: topic,
contentType: options.ContentType,
}
}
func (h *httpMessage) ContentType() string {
return h.contentType
}
func (h *httpMessage) Topic() string {
return h.topic
}
func (h *httpMessage) Payload() interface{} {
return h.payload
}

View File

@@ -1,103 +1,148 @@
package http
import (
"context"
"crypto/tls"
"net"
"net/http"
"time"
"github.com/unistack-org/micro/v3/client"
"go.unistack.org/micro/v4/client"
)
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{}
type poolMaxIdle struct{}
type codecsKey struct{}
type tlsAuth struct{}
type maxRecvMsgSizeKey struct{}
type maxSendMsgSizeKey struct{}
// maximum streams on a connectioin
func PoolMaxStreams(n int) client.Option {
return client.SetOption(poolMaxStreams{}, n)
}
// maximum idle conns of a pool
func PoolMaxIdle(d int) client.Option {
return client.SetOption(poolMaxIdle{}, d)
}
// AuthTLS should be used to setup a secure authentication using TLS
func AuthTLS(t *tls.Config) client.Option {
return client.SetOption(tlsAuth{}, t)
}
//
// MaxRecvMsgSize set the maximum size of message that client can receive.
func MaxRecvMsgSize(s int) client.Option {
return client.SetOption(maxRecvMsgSizeKey{}, s)
}
//
// MaxSendMsgSize set the maximum size of message that client can send.
func MaxSendMsgSize(s int) client.Option {
return client.SetOption(maxSendMsgSizeKey{}, s)
}
// --------------------------------------------- HTTPClient option -----------------------------------------------------
type httpClientKey struct{}
func HTTPClient(c *http.Client) client.Option {
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{}
func HTTPDialer(d *net.Dialer) client.Option {
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{}
func Method(m string) client.CallOption {
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{}
func Path(p string) client.CallOption {
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{}
func Body(b string) client.CallOption {
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{}
func ErrorMap(m map[string]interface{}) client.CallOption {
func ErrorMap(m map[string]any) client.CallOption {
return client.SetCallOption(errorMapKey{}, m)
}
type structTagsKey struct{}
func StructTags(tags []string) client.CallOption {
return client.SetCallOption(structTagsKey{}, tags)
func errorMapFromOpts(opts client.CallOptions) (map[string]any, bool) {
errMap, ok := opts.Context.Value(errorMapKey{}).(map[string]any)
return errMap, ok
}
// ------------------------------------------------ Cookie option ------------------------------------------------------
type cookieKey struct{}
func Cookie(cookies ...string) client.CallOption {
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{}
func Header(headers ...string) client.CallOption {
return client.SetCallOption(headerKey{}, headers)
}
func headerFromOpts(opts client.CallOptions) ([]string, bool) {
h, ok := opts.Context.Value(headerKey{}).([]string)
return h, ok
}

View File

@@ -1,36 +1,15 @@
package http
import (
"github.com/unistack-org/micro/v3/client"
"github.com/unistack-org/micro/v3/codec"
"go.unistack.org/micro/v4/client"
"go.unistack.org/micro/v4/codec"
)
type httpRequest struct {
service string
method string
contentType string
request interface{}
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
service string
method string
request any
opts client.RequestOptions
}
func (h *httpRequest) Service() string {
@@ -45,14 +24,18 @@ func (h *httpRequest) Endpoint() string {
return h.method
}
func (h *httpRequest) Codec() codec.Codec {
return nil
func (h *httpRequest) ContentType() string {
return h.opts.ContentType
}
func (h *httpRequest) Body() interface{} {
func (h *httpRequest) Body() any {
return h.request
}
func (h *httpRequest) Codec() codec.Codec {
return nil
}
func (h *httpRequest) Stream() bool {
return h.opts.Stream
}

109
stream.go
View File

@@ -1,109 +0,0 @@
package http
import (
"bufio"
"context"
"fmt"
"net"
"net/http"
"sync"
"github.com/unistack-org/micro/v3/client"
"github.com/unistack-org/micro/v3/codec"
"github.com/unistack-org/micro/v3/errors"
)
// Implements the streamer interface
type httpStream struct {
sync.RWMutex
address string
opts client.CallOptions
ct string
cf codec.Codec
context context.Context
header http.Header
seq uint64
closed chan bool
err error
conn net.Conn
reader *bufio.Reader
request client.Request
}
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) Send(msg interface{}) error {
h.Lock()
defer h.Unlock()
if h.isClosed() {
h.err = errShutdown
return errShutdown
}
hreq, err := newRequest(h.address, h.request, h.ct, h.cf, msg, h.opts)
if err != nil {
return err
}
hreq.Header = h.header
return hreq.Write(h.conn)
}
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 parseRsp(h.context, hrsp, h.cf, msg, h.opts)
}
func (h *httpStream) Error() error {
h.RLock()
defer h.RUnlock()
return h.err
}
func (h *httpStream) Close() error {
select {
case <-h.closed:
return nil
default:
close(h.closed)
return h.conn.Close()
}
}

197
util.go
View File

@@ -1,197 +0,0 @@
package http
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"sync"
"github.com/unistack-org/micro/v3/client"
"github.com/unistack-org/micro/v3/codec"
"github.com/unistack-org/micro/v3/errors"
rutil "github.com/unistack-org/micro/v3/util/reflect"
util "github.com/unistack-org/micro/v3/util/router"
)
var (
templateCache = make(map[string]util.Template)
mu sync.RWMutex
)
func newPathRequest(path string, method string, body string, msg interface{}, tags []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.Fields) > 0 && msg == nil {
return "", nil, fmt.Errorf("nil message but path params requested: %v", path)
}
fieldsmap := make(map[string]string, len(tpl.Fields))
for _, v := range tpl.Fields {
fieldsmap[v] = ""
}
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 := make(map[string]string)
// copy cycle
for i := 0; i < tmsg.NumField(); i++ {
val := tmsg.Field(i)
if val.IsZero() {
continue
}
fld := tmsg.Type().Field(i)
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
t = &tag{key: tn, name: tp[3][5:], opts: append(tp[:3], tp[4:]...)}
default:
t = &tag{key: tn, name: tp[0], opts: tp[1:]}
}
if t.name != "" {
break
}
}
if t.name == "" {
// fallback to lowercase
t.name = strings.ToLower(fld.Name)
}
if _, ok := fieldsmap[t.name]; ok {
fieldsmap[t.name] = fmt.Sprintf("%v", val.Interface())
} else if (body == "*" || body == t.name) && method != http.MethodGet {
tnmsg.Field(i).Set(val)
} else {
values[t.name] = fmt.Sprintf("%v", val.Interface())
}
}
// check not filled stuff
for k, v := range fieldsmap {
if v == "" {
return "", nil, fmt.Errorf("path param %s not filled", k)
}
}
var b strings.Builder
for _, fld := range tpl.Pool {
_, _ = b.WriteRune('/')
if v, ok := fieldsmap[fld]; ok {
_, _ = b.WriteString(v)
} else {
_, _ = b.WriteString(fld)
}
}
idx := 0
for k, v := range values {
if idx == 0 {
_, _ = b.WriteRune('?')
} else {
_, _ = b.WriteRune('&')
}
_, _ = b.WriteString(k)
_, _ = b.WriteRune('=')
_, _ = b.WriteString(v)
idx++
}
if rutil.IsZero(nmsg) {
return b.String(), nil, nil
}
return b.String(), nmsg, nil
}
func newTemplate(path string) (util.Template, error) {
mu.RLock()
tpl, ok := templateCache[path]
if ok {
mu.RUnlock()
return tpl, nil
}
mu.RUnlock()
rule, err := util.Parse(path)
if err != nil {
return tpl, err
}
tpl = rule.Compile()
mu.Lock()
templateCache[path] = tpl
mu.Unlock()
return tpl, nil
}
func parseRsp(ctx context.Context, hrsp *http.Response, cf codec.Codec, rsp interface{}, opts client.CallOptions) error {
b, err := ioutil.ReadAll(hrsp.Body)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
if hrsp.StatusCode < 400 {
// unmarshal
if err := cf.Unmarshal(b, rsp); err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
return nil
}
errmap, ok := opts.Context.Value(errorMapKey{}).(map[string]interface{})
if !ok || errmap == nil {
// user not provide map of errors
// id: req.Service() ??
return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode))
}
if err, ok = errmap[fmt.Sprintf("%d", hrsp.StatusCode)].(error); !ok {
err, ok = errmap["default"].(error)
}
if !ok {
return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode))
}
if cerr := cf.Unmarshal(b, err); cerr != nil {
return errors.InternalServerError("go.micro.client", cerr.Error())
}
return err
}
type tag struct {
key string
name string
opts []string
}

View File

@@ -1,81 +0,0 @@
package http
import (
"net/url"
"testing"
)
func TestTemplate(t *testing.T) {
tpl, err := newTemplate("/v1/{ClientID}/list")
if err != nil {
t.Fatal(err)
}
_ = tpl
// fmt.Printf("%#+v\n", tpl.Pool)
}
func TestNewPathRequest(t *testing.T) {
type Message struct {
Name string `json:"name"`
Val1 string `protobuf:"bytes,1,opt,name=val1,proto3" json:"val1"`
Val2 int64
Val3 []string
}
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"})
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"`
Val2 int64
Val3 []string
}
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"})
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)
}
}
}
}