mirror of
https://github.com/lukaszraczylo/go-telegram.git
synced 2026-10-13 00:47:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93ebf41a3c | ||
|
|
53afd9890f | ||
|
|
d666cda3b1 | ||
|
|
fa84f08b25 | ||
|
|
54790f3e5b | ||
|
|
ab5f2c5700 | ||
|
|
926d51881d | ||
|
|
4511c823a3 | ||
|
|
7c5aca3bf8 | ||
|
|
4e6382ceeb | ||
|
|
b6881461fd | ||
|
|
6612c24dfc | ||
|
|
3620f8ce1e | ||
|
|
7eebdd1f40 | ||
|
|
9c363f470a | ||
|
|
5aabbe6151 | ||
|
|
cd05e6a426 | ||
|
|
140ea13bde | ||
|
|
0731f10907 | ||
|
|
6685414374 | ||
|
|
404411b20c | ||
|
|
8d85e61da5 | ||
|
|
609c4ce649 | ||
|
|
d39be13822 | ||
|
|
d6ecbdea48 | ||
|
|
99c906c3c5 | ||
|
|
f5250197b7 | ||
|
|
bbbeb8461b | ||
|
|
bfb7e9875e | ||
|
|
75c7ce3119 |
@@ -0,0 +1,20 @@
|
||||
name: Autoupdate go.mod and go.sum
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
autoupdate:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
|
||||
with:
|
||||
go-version: ">=1.25"
|
||||
release-workflow: "release.yml"
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,61 @@
|
||||
name: checks
|
||||
|
||||
# Repo-specific gates. Called by ci.yml (PR/push) and release.yml (release gate).
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
examples:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '>=1.25'
|
||||
check-latest: true
|
||||
- name: Build all examples
|
||||
run: go build ./examples/...
|
||||
|
||||
codegen-clean:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '>=1.25'
|
||||
check-latest: true
|
||||
- name: Regenerate against pinned snapshot
|
||||
run: make regen-from-fixture
|
||||
- name: Assert clean diff
|
||||
run: git diff --exit-code internal/spec/api.json api/ docs/reference/
|
||||
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # need history for drift comparison
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '>=1.25'
|
||||
check-latest: true
|
||||
- name: Audit fallbacks
|
||||
run: make audit
|
||||
- name: Audit drift vs base
|
||||
# On PRs: compare against the merge base (origin/<base>).
|
||||
# On push to main: compare against the parent commit.
|
||||
# Drift is informational; doesn't fail CI.
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
BASE="origin/${{ github.base_ref }}"
|
||||
else
|
||||
BASE="HEAD~1"
|
||||
fi
|
||||
echo "Drift base: $BASE"
|
||||
go run ./cmd/audit -ir internal/spec/api.json -drift -against "$BASE" || true
|
||||
+10
-238
@@ -1,20 +1,16 @@
|
||||
name: ci
|
||||
|
||||
# Repo-specific checks run through checks.yml. Generic vet, staticcheck,
|
||||
# govulncheck, gosec and tests run in pr.yaml (shared go-pr). Releases run in
|
||||
# release.yml (shared go-release), gated by the same checks.yml.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry-run-release:
|
||||
description: "Compute release version, do not tag or release"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
# Cancel any in-flight CI runs for the same branch when a new push lands.
|
||||
concurrency:
|
||||
@@ -22,157 +18,15 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
vet:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
|
||||
- run: go vet ./...
|
||||
checks:
|
||||
uses: ./.github/workflows/checks.yml
|
||||
|
||||
staticcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
|
||||
- run: go install honnef.co/go/tools/cmd/[email protected]
|
||||
- run: staticcheck ./...
|
||||
|
||||
govulncheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
|
||||
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
- run: govulncheck ./...
|
||||
|
||||
gosec:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
|
||||
- run: go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
# G404: math/rand/v2 jitter in transport/backoff.go — intentional (not crypto)
|
||||
# G304: os.ReadFile from CLI flag variable — intentional (tool, not server)
|
||||
# G306: 0o644 on generated doc artifacts in cmd/scrape — intentional
|
||||
# G204: git subprocess in cmd/audit uses CLI flag path — intentional (operator tool)
|
||||
# G706: log.Printf with values from Telegram/env in examples — illustrative,
|
||||
# library users are expected to sanitise before logging in production
|
||||
- run: gosec -quiet -exclude=G404,G304,G306,G204,G706 -exclude-dir=testdata ./...
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
|
||||
- run: go test -race -coverprofile=coverage.out ./...
|
||||
- name: Build all examples
|
||||
run: go build ./examples/...
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
path: coverage.out
|
||||
|
||||
codegen-clean:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
|
||||
- name: Regenerate against pinned snapshot
|
||||
run: make regen-from-fixture
|
||||
- name: Assert clean diff
|
||||
run: git diff --exit-code internal/spec/api.json api/ docs/reference/
|
||||
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # need history for drift comparison
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
|
||||
- name: Audit fallbacks
|
||||
run: make audit
|
||||
- name: Audit drift vs base
|
||||
# On PRs: compare against the merge base (origin/<base>).
|
||||
# On push to main: compare against the parent commit.
|
||||
# Drift is informational; doesn't fail CI.
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
BASE="origin/${{ github.base_ref }}"
|
||||
else
|
||||
BASE="HEAD~1"
|
||||
fi
|
||||
echo "Drift base: $BASE"
|
||||
go run ./cmd/audit -ir internal/spec/api.json -drift -against "$BASE" || true
|
||||
|
||||
# Aggregate gate — depends on every check above. Used as a single
|
||||
# required status check in branch protection AND as a dependency for the
|
||||
# release job below.
|
||||
# Aggregate gate for the repo-local checks. Keeps the `ci-success` name
|
||||
# so an existing branch-protection rule keeps resolving.
|
||||
ci-success:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [vet, staticcheck, govulncheck, gosec, test, codegen-clean, audit]
|
||||
timeout-minutes: 5
|
||||
needs: [checks]
|
||||
if: always()
|
||||
steps:
|
||||
- name: All checks passed
|
||||
@@ -184,85 +38,3 @@ jobs:
|
||||
echo "Failed/cancelled jobs:"
|
||||
echo '${{ toJSON(needs) }}'
|
||||
exit 1
|
||||
|
||||
# Auto-release fires on every clean push to main (and on manual
|
||||
# workflow_dispatch for testing). Computes next SemVer from commit
|
||||
# history via lukaszraczylo/semver-generator, dual-tags
|
||||
# (v<X.Y.Z> + bot-api-v<A.B>), runs GoReleaser.
|
||||
release:
|
||||
needs: ci-success
|
||||
if: |
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
|
||||
# Action interface from
|
||||
# https://github.com/lukaszraczylo/semver-generator/blob/main/action.yml
|
||||
# Inputs: repository_local: true (use already-cloned repo)
|
||||
# existing: true (respect existing tags as base)
|
||||
# Output: semantic_version (bare version string, no "v" prefix)
|
||||
- name: Compute next SemVer
|
||||
id: semver
|
||||
uses: lukaszraczylo/semver-generator@v1
|
||||
with:
|
||||
repository_local: true
|
||||
existing: true
|
||||
config_file: .semver.yaml
|
||||
|
||||
- name: Read Bot API version
|
||||
id: api_version
|
||||
run: |
|
||||
VERSION=$(python3 -c 'import json; print(json.load(open("internal/spec/api.json"))["version"])')
|
||||
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
|
||||
echo "tag=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "tag=bot-api-v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dry-run summary
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.dry-run-release == true
|
||||
run: |
|
||||
echo "Would release: v${{ steps.semver.outputs.semantic_version }}"
|
||||
if [ -n "${{ steps.api_version.outputs.tag }}" ]; then
|
||||
echo "Would also tag: ${{ steps.api_version.outputs.tag }} (Bot API ${{ steps.api_version.outputs.version }})"
|
||||
fi
|
||||
echo "Skipping tag + release (dry-run)."
|
||||
|
||||
- name: Tag library + bot-api versions
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
|
||||
env:
|
||||
LIB_TAG: v${{ steps.semver.outputs.semantic_version }}
|
||||
API_TAG: ${{ steps.api_version.outputs.tag }}
|
||||
API_VER: ${{ steps.api_version.outputs.version }}
|
||||
run: |
|
||||
# Bot API version (currently $API_VER) is intentionally NOT
|
||||
# tagged separately. semver-generator picks the most recent
|
||||
# tag as the version base; a non-SemVer marker like
|
||||
# bot-api-vX.Y poisons that and restarts numbering from
|
||||
# v0.0.x. Bot API version stays as a comment in the lib tag
|
||||
# message and in the release notes.
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -a "$LIB_TAG" -m "Release $LIB_TAG (Bot API $API_VER)"
|
||||
git push origin "$LIB_TAG"
|
||||
|
||||
- name: Run GoReleaser
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.dry-run-release == false
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: '~> v2'
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BOT_API_VERSION: ${{ steps.api_version.outputs.version }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/configure-pages@v5
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
- "!main"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
pr-checks:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
|
||||
with:
|
||||
go-version: ">=1.25"
|
||||
@@ -13,12 +13,12 @@ jobs:
|
||||
regen:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # full history so audit -drift can compare against main
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.25.x'
|
||||
check-latest: true
|
||||
@@ -29,7 +29,12 @@ jobs:
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
DEST="testdata/html/snapshot_${DATE}.html"
|
||||
curl -fsSL --user-agent "go-telegram codegen scraper" \
|
||||
https://core.telegram.org/bots/api > "$DEST"
|
||||
https://core.telegram.org/bots/api -o "$DEST.tmp"
|
||||
# The page ends with a "<!-- page generated in NN.NNms -->" comment
|
||||
# that changes on every fetch. Drop it, so an unchanged API produces
|
||||
# a byte-identical snapshot instead of a no-op update PR.
|
||||
sed '/<!-- page generated in .*ms -->/d' "$DEST.tmp" > "$DEST"
|
||||
rm "$DEST.tmp"
|
||||
ln -sf "snapshot_${DATE}.html" testdata/html/latest.html
|
||||
echo "date=$DATE" >> $GITHUB_OUTPUT
|
||||
echo "dest=$DEST" >> $GITHUB_OUTPUT
|
||||
@@ -76,7 +81,10 @@ jobs:
|
||||
id: diff
|
||||
run: |
|
||||
git status --porcelain
|
||||
if git diff --quiet internal/spec/api.json api/ testdata/html/; then
|
||||
# Gate on the generated surface only. A fresh snapshot file and the
|
||||
# latest.html symlink flip every run, so including testdata/html/
|
||||
# here would open a PR on a week when Telegram changed nothing.
|
||||
if git diff --quiet internal/spec/api.json api/ docs/reference/; then
|
||||
echo "no_changes=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
@@ -88,8 +96,9 @@ jobs:
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Open PR
|
||||
id: pr
|
||||
if: steps.diff.outputs.no_changes != 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: |
|
||||
@@ -115,4 +124,17 @@ jobs:
|
||||
|
||||
Inspect the IR diff (`internal/spec/api.json`) for added/changed/removed methods.
|
||||
|
||||
CI must pass before merge. Auto-merge: enable when satisfied with the diff.
|
||||
This PR merges itself once every step above passes. The gates are
|
||||
`make regen` (which fails on any audit fallback) and `go test -race ./...`,
|
||||
both run in this job before the PR opens.
|
||||
|
||||
# A PR opened with GITHUB_TOKEN does not trigger `ci.yml` or `pr.yaml`, so waiting for
|
||||
# PR checks would wait forever. The gates already ran in this job: `make
|
||||
# regen` fails on any audit fallback, and the test step runs the full race
|
||||
# suite. Merging here therefore means "merged because the checks passed".
|
||||
# `release.yml` then runs on the push to main and cuts the release.
|
||||
- name: Merge regen PR
|
||||
if: steps.diff.outputs.no_changes != 'true' && steps.pr.outputs.pull-request-number != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh pr merge "${{ steps.pr.outputs.pull-request-number }}" --squash --delete-branch
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "**.go"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
uses: ./.github/workflows/checks.yml
|
||||
|
||||
release:
|
||||
needs: checks
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
||||
with:
|
||||
go-version: ">=1.25"
|
||||
semver-config: ".semver.yaml"
|
||||
secrets: inherit
|
||||
@@ -35,7 +35,10 @@ lint: vet
|
||||
integration:
|
||||
$(GO) test -tags=integration -v ./test/integration/...
|
||||
|
||||
SCRAPE_INPUT ?= testdata/html/snapshot_2026-05-08.html
|
||||
# Resolve via testdata/html/latest.html symlink so the pinned-fixture target
|
||||
# auto-tracks whatever snapshot regen.yml last committed. Override with
|
||||
# SCRAPE_INPUT=path/to/snapshot.html for ad-hoc replays.
|
||||
SCRAPE_INPUT ?= testdata/html/$(shell readlink testdata/html/latest.html)
|
||||
SCRAPE_OUTPUT ?= internal/spec/api.json
|
||||
|
||||
snapshot:
|
||||
|
||||
@@ -326,10 +326,13 @@ Apples-to-apples micro-benchmarks against the five most-starred Go Telegram libr
|
||||
|
||||
| Path | Fastest | Our position |
|
||||
|------|---------|--------------|
|
||||
| Webhook decode (small Update) | **ours** — 1.74 µs / 11 allocs | 1st of 6 |
|
||||
| Large Update unmarshal (unions + reply markup) | **ours** — 6.67 µs / 34 allocs | 1st of 6 |
|
||||
| `sendMessage` round-trip (mock server) | telego — 36.3 µs / 48 allocs | 2nd of 5 |
|
||||
| Dispatcher routing (20 handlers, last matches) | **ours** — 101 ns / 3 allocs | 1st of 3 |
|
||||
| Webhook decode (small Update) | **ours** — 1.83 µs / 11 allocs | 1st of 6 |
|
||||
| Large Update unmarshal (unions + reply markup) | **ours** — 6.73 µs / 34 allocs | 1st of 6 |
|
||||
| `sendMessage` round-trip — `net/http` default | telego — 35.8 µs / 48 allocs | 2nd of 5 (102 allocs) |
|
||||
| `sendMessage` round-trip — opt-in `fasthttp` | telego — 48 allocs | within 8 of telego (56 allocs) |
|
||||
| Dispatcher routing (20 handlers, last matches) | **ours** — 98 ns / 3 allocs | 1st of 3 |
|
||||
|
||||
Opt into fasthttp for high-throughput bots: `client.WithHTTPClient(client.NewFastHTTPDoer())`. Trade-off: HTTP/1.1 only, no `RoundTripper` middleware composition.
|
||||
|
||||
Full tables, caveats, and reproduction steps: **[`docs/benchmarks/2026-05-10-comparison.md`](docs/benchmarks/2026-05-10-comparison.md)**.
|
||||
|
||||
@@ -368,6 +371,20 @@ bot := client.New("token", client.WithHTTPClient(fakeDoer{
|
||||
|
||||
The library's own generated test suite (`api/methods_gen_test.go`) covers 176 methods × 8 scenarios each: Success, APIError, NetworkError, ParseError, ContextCanceled, MissingRequiredFields, Forbidden, ServerError.
|
||||
|
||||
## Telemetry
|
||||
|
||||
On the **first call to `client.New`** in a process, this library sends a
|
||||
single anonymous adoption ping — project name, version, timestamp; no
|
||||
identifiers, no message contents, no API call metadata. Fire-and-forget
|
||||
with a 2-second timeout; cannot block `New` or panic.
|
||||
|
||||
Local source: [`client/telemetry.go`](client/telemetry.go). Upstream
|
||||
implementation, exact wire format, and full opt-out documentation:
|
||||
**[oss-telemetry — Disabling telemetry](https://github.com/lukaszraczylo/oss-telemetry#disabling-telemetry)**.
|
||||
|
||||
Quick opt-out: set any of `DO_NOT_TRACK=1`, `OSS_TELEMETRY_DISABLED=1`,
|
||||
or `GO_TELEGRAM_DISABLE_TELEMETRY=1`.
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
@@ -33,6 +33,14 @@ const (
|
||||
BotCommandScopeTypeChatMember BotCommandScopeType = "chat_member"
|
||||
)
|
||||
|
||||
type BotSubscriptionUpdatedState string
|
||||
|
||||
const (
|
||||
BotSubscriptionUpdatedStateCanceled BotSubscriptionUpdatedState = "canceled"
|
||||
BotSubscriptionUpdatedStateActive BotSubscriptionUpdatedState = "active"
|
||||
BotSubscriptionUpdatedStateFailed BotSubscriptionUpdatedState = "failed"
|
||||
)
|
||||
|
||||
type ChatBoostSourceKind string
|
||||
|
||||
const (
|
||||
@@ -133,6 +141,12 @@ const (
|
||||
InputMediaTypeVideo InputMediaType = "video"
|
||||
)
|
||||
|
||||
type InputMediaVoiceNoteType string
|
||||
|
||||
const (
|
||||
InputMediaVoiceNoteTypeVoiceNote InputMediaVoiceNoteType = "voice_note"
|
||||
)
|
||||
|
||||
type InputPaidMediaType string
|
||||
|
||||
const (
|
||||
@@ -158,6 +172,7 @@ type InputPollOptionMediaType string
|
||||
|
||||
const (
|
||||
InputPollOptionMediaTypeAnimation InputPollOptionMediaType = "animation"
|
||||
InputPollOptionMediaTypeLink InputPollOptionMediaType = "link"
|
||||
InputPollOptionMediaTypeLivePhoto InputPollOptionMediaType = "live_photo"
|
||||
InputPollOptionMediaTypeLocation InputPollOptionMediaType = "location"
|
||||
InputPollOptionMediaTypePhoto InputPollOptionMediaType = "photo"
|
||||
@@ -173,6 +188,35 @@ const (
|
||||
InputProfilePhotoTypeAnimated InputProfilePhotoType = "animated"
|
||||
)
|
||||
|
||||
type InputRichBlockType string
|
||||
|
||||
const (
|
||||
InputRichBlockTypeParagraph InputRichBlockType = "paragraph"
|
||||
InputRichBlockTypeHeading InputRichBlockType = "heading"
|
||||
InputRichBlockTypePre InputRichBlockType = "pre"
|
||||
InputRichBlockTypeFooter InputRichBlockType = "footer"
|
||||
InputRichBlockTypeDivider InputRichBlockType = "divider"
|
||||
InputRichBlockTypeMathematicalExpression InputRichBlockType = "mathematical_expression"
|
||||
InputRichBlockTypeAnchor InputRichBlockType = "anchor"
|
||||
InputRichBlockTypeList InputRichBlockType = "list"
|
||||
InputRichBlockTypeBlockquote InputRichBlockType = "blockquote"
|
||||
InputRichBlockTypeExpandableBlockquote InputRichBlockType = "expandable_blockquote"
|
||||
InputRichBlockTypePullquote InputRichBlockType = "pullquote"
|
||||
InputRichBlockTypeCollage InputRichBlockType = "collage"
|
||||
InputRichBlockTypeSlideshow InputRichBlockType = "slideshow"
|
||||
InputRichBlockTypeTable InputRichBlockType = "table"
|
||||
InputRichBlockTypeDetails InputRichBlockType = "details"
|
||||
InputRichBlockTypeMap InputRichBlockType = "map"
|
||||
InputRichBlockTypeButtons InputRichBlockType = "buttons"
|
||||
InputRichBlockTypeAnimation InputRichBlockType = "animation"
|
||||
InputRichBlockTypeAudio InputRichBlockType = "audio"
|
||||
InputRichBlockTypeDocument InputRichBlockType = "document"
|
||||
InputRichBlockTypePhoto InputRichBlockType = "photo"
|
||||
InputRichBlockTypeVideo InputRichBlockType = "video"
|
||||
InputRichBlockTypeVoiceNote InputRichBlockType = "voice_note"
|
||||
InputRichBlockTypeThinking InputRichBlockType = "thinking"
|
||||
)
|
||||
|
||||
type InputStickerFormat string
|
||||
|
||||
const (
|
||||
@@ -357,6 +401,14 @@ const (
|
||||
RefundedPaymentCurrencyXTR RefundedPaymentCurrency = "XTR"
|
||||
)
|
||||
|
||||
type Result string
|
||||
|
||||
const (
|
||||
ResultApprove Result = "approve"
|
||||
ResultDecline Result = "decline"
|
||||
ResultQueue Result = "queue"
|
||||
)
|
||||
|
||||
type RevenueWithdrawalStateKind string
|
||||
|
||||
const (
|
||||
@@ -365,6 +417,101 @@ const (
|
||||
RevenueWithdrawalStateKindFailed RevenueWithdrawalStateKind = "failed"
|
||||
)
|
||||
|
||||
type RichBlockButtonsAlign string
|
||||
|
||||
const (
|
||||
RichBlockButtonsAlignLeft RichBlockButtonsAlign = "left"
|
||||
RichBlockButtonsAlignCenter RichBlockButtonsAlign = "center"
|
||||
RichBlockButtonsAlignRight RichBlockButtonsAlign = "right"
|
||||
)
|
||||
|
||||
type RichBlockListItemType string
|
||||
|
||||
const (
|
||||
RichBlockListItemTypeLowerA RichBlockListItemType = "a"
|
||||
RichBlockListItemTypeUpperA RichBlockListItemType = "A"
|
||||
RichBlockListItemTypeLowerI RichBlockListItemType = "i"
|
||||
RichBlockListItemTypeUpperI RichBlockListItemType = "I"
|
||||
RichBlockListItemType1 RichBlockListItemType = "1"
|
||||
)
|
||||
|
||||
type RichBlockTableCellValign string
|
||||
|
||||
const (
|
||||
RichBlockTableCellValignTop RichBlockTableCellValign = "top"
|
||||
RichBlockTableCellValignMiddle RichBlockTableCellValign = "middle"
|
||||
RichBlockTableCellValignBottom RichBlockTableCellValign = "bottom"
|
||||
)
|
||||
|
||||
type RichBlockType string
|
||||
|
||||
const (
|
||||
RichBlockTypeParagraph RichBlockType = "paragraph"
|
||||
RichBlockTypeHeading RichBlockType = "heading"
|
||||
RichBlockTypePre RichBlockType = "pre"
|
||||
RichBlockTypeFooter RichBlockType = "footer"
|
||||
RichBlockTypeDivider RichBlockType = "divider"
|
||||
RichBlockTypeMathematicalExpression RichBlockType = "mathematical_expression"
|
||||
RichBlockTypeAnchor RichBlockType = "anchor"
|
||||
RichBlockTypeList RichBlockType = "list"
|
||||
RichBlockTypeBlockquote RichBlockType = "blockquote"
|
||||
RichBlockTypeExpandableBlockquote RichBlockType = "expandable_blockquote"
|
||||
RichBlockTypePullquote RichBlockType = "pullquote"
|
||||
RichBlockTypeCollage RichBlockType = "collage"
|
||||
RichBlockTypeSlideshow RichBlockType = "slideshow"
|
||||
RichBlockTypeTable RichBlockType = "table"
|
||||
RichBlockTypeDetails RichBlockType = "details"
|
||||
RichBlockTypeMap RichBlockType = "map"
|
||||
RichBlockTypeButtons RichBlockType = "buttons"
|
||||
RichBlockTypeAnimation RichBlockType = "animation"
|
||||
RichBlockTypeAudio RichBlockType = "audio"
|
||||
RichBlockTypeDocument RichBlockType = "document"
|
||||
RichBlockTypePhoto RichBlockType = "photo"
|
||||
RichBlockTypeVideo RichBlockType = "video"
|
||||
RichBlockTypeVoiceNote RichBlockType = "voice_note"
|
||||
RichBlockTypeThinking RichBlockType = "thinking"
|
||||
)
|
||||
|
||||
type RichMessageButtonStyle string
|
||||
|
||||
const (
|
||||
RichMessageButtonStyleDanger RichMessageButtonStyle = "danger"
|
||||
RichMessageButtonStyleSuccess RichMessageButtonStyle = "success"
|
||||
RichMessageButtonStylePrimary RichMessageButtonStyle = "primary"
|
||||
RichMessageButtonStyleLink RichMessageButtonStyle = "link"
|
||||
)
|
||||
|
||||
type RichTextType string
|
||||
|
||||
const (
|
||||
RichTextTypeBold RichTextType = "bold"
|
||||
RichTextTypeItalic RichTextType = "italic"
|
||||
RichTextTypeUnderline RichTextType = "underline"
|
||||
RichTextTypeStrikethrough RichTextType = "strikethrough"
|
||||
RichTextTypeSpoiler RichTextType = "spoiler"
|
||||
RichTextTypeDateTime RichTextType = "date_time"
|
||||
RichTextTypeTextMention RichTextType = "text_mention"
|
||||
RichTextTypeSubscript RichTextType = "subscript"
|
||||
RichTextTypeSuperscript RichTextType = "superscript"
|
||||
RichTextTypeMarked RichTextType = "marked"
|
||||
RichTextTypeCode RichTextType = "code"
|
||||
RichTextTypeCustomEmoji RichTextType = "custom_emoji"
|
||||
RichTextTypeMathematicalExpression RichTextType = "mathematical_expression"
|
||||
RichTextTypeURL RichTextType = "url"
|
||||
RichTextTypeEmailAddress RichTextType = "email_address"
|
||||
RichTextTypePhoneNumber RichTextType = "phone_number"
|
||||
RichTextTypeBankCardNumber RichTextType = "bank_card_number"
|
||||
RichTextTypeMention RichTextType = "mention"
|
||||
RichTextTypeHashtag RichTextType = "hashtag"
|
||||
RichTextTypeCashtag RichTextType = "cashtag"
|
||||
RichTextTypeBotCommand RichTextType = "bot_command"
|
||||
RichTextTypeButton RichTextType = "button"
|
||||
RichTextTypeAnchor RichTextType = "anchor"
|
||||
RichTextTypeAnchorLink RichTextType = "anchor_link"
|
||||
RichTextTypeReference RichTextType = "reference"
|
||||
RichTextTypeReferenceLink RichTextType = "reference_link"
|
||||
)
|
||||
|
||||
type StickerType string
|
||||
|
||||
const (
|
||||
|
||||
+452
-124
File diff suppressed because it is too large
Load Diff
+1352
-21
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,210 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
json "github.com/goccy/go-json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestUpdate_RichMessage_Decodes is a regression test for the Bot API v10
|
||||
// rich_message decode failure: RichBlock (21 variants) and RichText (25
|
||||
// variants) had no knownDiscriminators entry, so genapi never emitted
|
||||
// UnmarshalRichBlock / UnmarshalRichText and any update whose message
|
||||
// carried rich_message failed to decode — in production this rejected
|
||||
// roughly 8.6% of Telegram updates with HTTP 400. Uses goccy/go-json, the
|
||||
// codec production runs; plain encoding/json failed the same way.
|
||||
func TestUpdate_RichMessage_Decodes(t *testing.T) {
|
||||
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
|
||||
`"rich_message":{"blocks":[{"type":"paragraph","text":{"type":"bold","text":{"type":"anchor","name":"hi"}}}]}}}`
|
||||
|
||||
var u Update
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &u))
|
||||
|
||||
require.NotNil(t, u.Message)
|
||||
require.NotNil(t, u.Message.RichMessage)
|
||||
require.Len(t, u.Message.RichMessage.Blocks, 1)
|
||||
|
||||
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
|
||||
require.True(t, ok, "blocks[0] must dispatch to *RichBlockParagraph, got %T", u.Message.RichMessage.Blocks[0])
|
||||
|
||||
bold, ok := para.Text.(*RichTextBold)
|
||||
require.True(t, ok, "paragraph.text must dispatch to *RichTextBold, got %T", para.Text)
|
||||
|
||||
anchor, ok := bold.Text.(*RichTextAnchor)
|
||||
require.True(t, ok, "bold.text must dispatch to *RichTextAnchor, got %T", bold.Text)
|
||||
require.Equal(t, "hi", anchor.Name)
|
||||
}
|
||||
|
||||
// TestUnmarshalRichBlock_UnknownType confirms an unrecognised discriminator
|
||||
// value surfaces a clear error instead of silently decoding a zero-valued
|
||||
// interface.
|
||||
func TestUnmarshalRichBlock_UnknownType(t *testing.T) {
|
||||
_, err := UnmarshalRichBlock([]byte(`{"type":"not_a_real_block"}`))
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "unknown type")
|
||||
}
|
||||
|
||||
// TestUpdate_RichMessage_UnknownBlockType confirms the unknown-type error
|
||||
// from UnmarshalRichBlock propagates all the way up through
|
||||
// RichMessage.UnmarshalJSON to the top-level Update decode.
|
||||
func TestUpdate_RichMessage_UnknownBlockType(t *testing.T) {
|
||||
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
|
||||
`"rich_message":{"blocks":[{"type":"not_a_real_block"}]}}}`
|
||||
|
||||
var u Update
|
||||
err := json.Unmarshal([]byte(body), &u)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "unknown type")
|
||||
}
|
||||
|
||||
// TestUpdate_RichMessage_PlainStringText is a regression test for the Bot
|
||||
// API v10 RichText decode gap this task fixes. Telegram declares RichText
|
||||
// as decodable from "either a String for plain text, an Array of
|
||||
// RichText, or any of the following [object] types" — but the IR (and
|
||||
// therefore UnmarshalRichText) only ever recorded the object variants, so
|
||||
// a paragraph whose text is a bare JSON string failed with "invalid
|
||||
// character '\"' looking for beginning of value". A plain-text paragraph
|
||||
// is the most common real rich_message payload, so this was the
|
||||
// production-facing half of the bug.
|
||||
func TestUpdate_RichMessage_PlainStringText(t *testing.T) {
|
||||
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
|
||||
`"rich_message":{"blocks":[{"type":"paragraph","text":"hello world"}]}}}`
|
||||
|
||||
var u Update
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &u))
|
||||
|
||||
require.NotNil(t, u.Message)
|
||||
require.NotNil(t, u.Message.RichMessage)
|
||||
require.Len(t, u.Message.RichMessage.Blocks, 1)
|
||||
|
||||
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
|
||||
require.True(t, ok, "blocks[0] must dispatch to *RichBlockParagraph, got %T", u.Message.RichMessage.Blocks[0])
|
||||
|
||||
plain, ok := para.Text.(*RichTextPlain)
|
||||
require.True(t, ok, "paragraph.text must dispatch to *RichTextPlain for a bare JSON string, got %T", para.Text)
|
||||
require.Equal(t, "hello world", plain.Text)
|
||||
}
|
||||
|
||||
// TestUpdate_RichMessage_ArrayText confirms Telegram's second declared
|
||||
// RichText alternate — a bare JSON array of RichText — decodes into
|
||||
// RichTextSequence, with each element dispatched recursively through
|
||||
// UnmarshalRichText (an object variant in this case).
|
||||
func TestUpdate_RichMessage_ArrayText(t *testing.T) {
|
||||
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
|
||||
`"rich_message":{"blocks":[{"type":"paragraph","text":[` +
|
||||
`{"type":"bold","text":"x"},{"type":"italic","text":"y"}` +
|
||||
`]}]}}}`
|
||||
|
||||
var u Update
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &u))
|
||||
|
||||
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
|
||||
require.True(t, ok)
|
||||
|
||||
seq, ok := para.Text.(*RichTextSequence)
|
||||
require.True(t, ok, "paragraph.text must dispatch to *RichTextSequence for a JSON array, got %T", para.Text)
|
||||
require.Len(t, seq.Items, 2)
|
||||
|
||||
bold, ok := seq.Items[0].(*RichTextBold)
|
||||
require.True(t, ok, "sequence[0] must be *RichTextBold, got %T", seq.Items[0])
|
||||
boldText, ok := bold.Text.(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "x", boldText.Text)
|
||||
|
||||
italic, ok := seq.Items[1].(*RichTextItalic)
|
||||
require.True(t, ok, "sequence[1] must be *RichTextItalic, got %T", seq.Items[1])
|
||||
italicText, ok := italic.Text.(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "y", italicText.Text)
|
||||
}
|
||||
|
||||
// TestUpdate_RichMessage_ArrayText_MixedElements confirms an array whose
|
||||
// elements mix the bare-string shape and the object shape dispatches
|
||||
// each element independently — the array alternate and the string
|
||||
// alternate compose, they aren't mutually exclusive per element.
|
||||
func TestUpdate_RichMessage_ArrayText_MixedElements(t *testing.T) {
|
||||
body := `{"update_id":1,"message":{"message_id":7,"date":1789000000,"chat":{"id":-100123,"type":"supergroup"},` +
|
||||
`"rich_message":{"blocks":[{"type":"paragraph","text":[` +
|
||||
`"plain segment",{"type":"bold","text":"bold segment"}` +
|
||||
`]}]}}}`
|
||||
|
||||
var u Update
|
||||
require.NoError(t, json.Unmarshal([]byte(body), &u))
|
||||
|
||||
para, ok := u.Message.RichMessage.Blocks[0].(*RichBlockParagraph)
|
||||
require.True(t, ok)
|
||||
|
||||
seq, ok := para.Text.(*RichTextSequence)
|
||||
require.True(t, ok)
|
||||
require.Len(t, seq.Items, 2)
|
||||
|
||||
plain, ok := seq.Items[0].(*RichTextPlain)
|
||||
require.True(t, ok, "sequence[0] must be *RichTextPlain, got %T", seq.Items[0])
|
||||
require.Equal(t, "plain segment", plain.Text)
|
||||
|
||||
bold, ok := seq.Items[1].(*RichTextBold)
|
||||
require.True(t, ok, "sequence[1] must be *RichTextBold, got %T", seq.Items[1])
|
||||
boldText, ok := bold.Text.(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "bold segment", boldText.Text)
|
||||
}
|
||||
|
||||
// TestRichTextPlain_MarshalJSON_RoundTrip confirms the plain-text
|
||||
// alternate marshals back to a bare JSON string (not an object), and
|
||||
// that string decodes straight back through UnmarshalRichText.
|
||||
func TestRichTextPlain_MarshalJSON_RoundTrip(t *testing.T) {
|
||||
v := &RichTextPlain{Text: "hello"}
|
||||
|
||||
b, err := json.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `"hello"`, string(b))
|
||||
|
||||
decoded, err := UnmarshalRichText(b)
|
||||
require.NoError(t, err)
|
||||
got, ok := decoded.(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "hello", got.Text)
|
||||
}
|
||||
|
||||
// TestRichTextSequence_MarshalJSON_RoundTrip confirms the array
|
||||
// alternate marshals back to a bare JSON array (not an object wrapping
|
||||
// one), preserving each element's own wire shape, and round-trips
|
||||
// through UnmarshalRichText.
|
||||
func TestRichTextSequence_MarshalJSON_RoundTrip(t *testing.T) {
|
||||
v := &RichTextSequence{
|
||||
Items: []RichText{
|
||||
&RichTextPlain{Text: "a"},
|
||||
&RichTextBold{Text: &RichTextPlain{Text: "b"}},
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `["a",{"type":"bold","text":"b"}]`, string(b))
|
||||
|
||||
decoded, err := UnmarshalRichText(b)
|
||||
require.NoError(t, err)
|
||||
seq, ok := decoded.(*RichTextSequence)
|
||||
require.True(t, ok)
|
||||
require.Len(t, seq.Items, 2)
|
||||
|
||||
p0, ok := seq.Items[0].(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "a", p0.Text)
|
||||
|
||||
b1, ok := seq.Items[1].(*RichTextBold)
|
||||
require.True(t, ok)
|
||||
p1, ok := b1.Text.(*RichTextPlain)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "b", p1.Text)
|
||||
}
|
||||
|
||||
// TestUnmarshalRichText_UnrecognisedShape confirms a JSON value that is
|
||||
// neither a string, an array, nor an object surfaces a clear error
|
||||
// instead of silently producing a zero-valued interface.
|
||||
func TestUnmarshalRichText_UnrecognisedShape(t *testing.T) {
|
||||
_, err := UnmarshalRichText([]byte(`42`))
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "unrecognised JSON value")
|
||||
}
|
||||
+4630
-133
File diff suppressed because it is too large
Load Diff
+141
-17
@@ -23,9 +23,17 @@ var (
|
||||
// return slices that alias the buffer and therefore cannot use the
|
||||
// pool without an extra copy that would defeat the point.
|
||||
respBufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
|
||||
|
||||
// reqBufPool reuses *bytes.Buffer for request body marshalling on the
|
||||
// JSON path. Only used when the configured Codec satisfies BodyEncoder
|
||||
// so we can stream-encode into the buffer instead of allocating an
|
||||
// intermediate []byte. The buffer is safe to return to the pool once
|
||||
// http.Client.Do (or RetryDoer, which io.ReadAlls the body up front)
|
||||
// has consumed it.
|
||||
reqBufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
|
||||
)
|
||||
|
||||
// maxPooledBufCap caps the buffer size returned to respBufPool. Buffers
|
||||
// maxPooledBufCap caps the buffer size returned to either pool. Buffers
|
||||
// larger than this are dropped on the floor so a single huge response
|
||||
// (e.g. a large getFile metadata payload) doesn't bloat the pool for the
|
||||
// rest of the process lifetime.
|
||||
@@ -38,6 +46,13 @@ func putRespBuf(buf *bytes.Buffer) {
|
||||
respBufPool.Put(buf)
|
||||
}
|
||||
|
||||
func putReqBuf(buf *bytes.Buffer) {
|
||||
if buf.Cap() > maxPooledBufCap {
|
||||
return
|
||||
}
|
||||
reqBufPool.Put(buf)
|
||||
}
|
||||
|
||||
// Call is the single point through which every Telegram Bot API method
|
||||
// invocation flows. It marshals the request, signs the URL with the bot
|
||||
// token, dispatches via HTTPDoer, decodes the Result envelope, and
|
||||
@@ -62,18 +77,18 @@ func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req
|
||||
}
|
||||
}
|
||||
|
||||
body, err := encodeJSONBody(b.codec, req)
|
||||
body, pooledReqBuf, err := encodeJSONBody(b.codec, req)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
if pooledReqBuf != nil {
|
||||
defer putReqBuf(pooledReqBuf)
|
||||
}
|
||||
|
||||
url := b.base + "/bot" + b.token + "/" + method
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
|
||||
httpReq, err := b.buildRequest(ctx, method, body)
|
||||
if err != nil {
|
||||
return zero, &NetworkError{Err: err}
|
||||
}
|
||||
httpReq.Header["Content-Type"] = headerJSONValue
|
||||
httpReq.Header["Accept"] = headerJSONValue
|
||||
|
||||
resp, err := b.http.Do(httpReq)
|
||||
if err != nil {
|
||||
@@ -111,18 +126,18 @@ func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json
|
||||
}
|
||||
}
|
||||
|
||||
body, err := encodeJSONBody(b.codec, req)
|
||||
body, pooledReqBuf, err := encodeJSONBody(b.codec, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pooledReqBuf != nil {
|
||||
defer putReqBuf(pooledReqBuf)
|
||||
}
|
||||
|
||||
url := b.base + "/bot" + b.token + "/" + method
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
|
||||
httpReq, err := b.buildRequest(ctx, method, body)
|
||||
if err != nil {
|
||||
return nil, &NetworkError{Err: err}
|
||||
}
|
||||
httpReq.Header["Content-Type"] = headerJSONValue
|
||||
httpReq.Header["Accept"] = headerJSONValue
|
||||
|
||||
resp, err := b.http.Do(httpReq)
|
||||
if err != nil {
|
||||
@@ -154,17 +169,126 @@ func decodeResultRaw(codec Codec, raw []byte) (json.RawMessage, error) {
|
||||
return env.Result, nil
|
||||
}
|
||||
|
||||
// encodeJSONBody marshals req to a JSON body. A nil interface or nil
|
||||
// pointer req yields "{}" so Telegram receives a valid empty object.
|
||||
func encodeJSONBody(codec Codec, req any) (io.Reader, error) {
|
||||
// buildRequest constructs the *http.Request for an API call. When the bot
|
||||
// has a cached parsed base URL (the common path), the request is built
|
||||
// manually so that net/url.Parse and net/http.NewRequestWithContext's
|
||||
// internal book-keeping are skipped — saving allocations on every call.
|
||||
//
|
||||
// ContentLength and GetBody are populated from the body's concrete type
|
||||
// in bodyToReadCloser so RetryDoer can replay the body across attempts.
|
||||
func (b *Bot) buildRequest(ctx context.Context, method string, body io.Reader) (*http.Request, error) {
|
||||
if b.baseURL == nil {
|
||||
// Slow path: WithBaseURL configured an unparsable URL (or New ran
|
||||
// before pre-parse for some reason). Fall back to the stdlib
|
||||
// constructor so we still produce a valid request.
|
||||
url := b.base + b.pathPrefix + method
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header["Content-Type"] = headerJSONValue
|
||||
req.Header["Accept"] = headerJSONValue
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Fast path: clone the cached *url.URL by value, set the per-method
|
||||
// path. Constructing &http.Request{} directly avoids the Header,
|
||||
// URL-parse, and ContentLength bookkeeping that NewRequestWithContext
|
||||
// runs unconditionally.
|
||||
u := *b.baseURL
|
||||
u.Path = b.pathPrefix + method
|
||||
u.RawPath = ""
|
||||
|
||||
rc, contentLength, getBody := bodyToReadCloser(body)
|
||||
req := &http.Request{
|
||||
Method: http.MethodPost,
|
||||
URL: &u,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{"Content-Type": headerJSONValue, "Accept": headerJSONValue},
|
||||
Body: rc,
|
||||
GetBody: getBody,
|
||||
ContentLength: contentLength,
|
||||
Host: u.Host,
|
||||
}
|
||||
return req.WithContext(ctx), nil
|
||||
}
|
||||
|
||||
// bufferReadCloser exposes a *bytes.Buffer as io.ReadCloser without going
|
||||
// through io.NopCloser. Keeping the concrete *bytes.Buffer accessible lets
|
||||
// alternative HTTPDoers (e.g. FastHTTPDoer) type-assert and pass the
|
||||
// underlying bytes through to their native body-set APIs without copying.
|
||||
type bufferReadCloser struct {
|
||||
*bytes.Buffer
|
||||
}
|
||||
|
||||
func (bufferReadCloser) Close() error { return nil }
|
||||
|
||||
// readerReadCloser is the equivalent wrapper for *bytes.Reader (used by
|
||||
// the Marshal fallback path when the codec doesn't implement BodyEncoder).
|
||||
type readerReadCloser struct {
|
||||
*bytes.Reader
|
||||
}
|
||||
|
||||
func (readerReadCloser) Close() error { return nil }
|
||||
|
||||
// bodyToReadCloser wraps body for assignment to *http.Request.Body. The
|
||||
// type switch covers the body shapes encodeJSONBody returns: a pooled
|
||||
// *bytes.Buffer (BodyEncoder path or {} fast path) or a *bytes.Reader
|
||||
// (Marshal fallback for codecs that don't implement BodyEncoder). Both
|
||||
// cases populate ContentLength and GetBody so RetryDoer can replay the
|
||||
// body across retry attempts without buffering it again.
|
||||
func bodyToReadCloser(body io.Reader) (io.ReadCloser, int64, func() (io.ReadCloser, error)) {
|
||||
switch v := body.(type) {
|
||||
case *bytes.Buffer:
|
||||
buf := v.Bytes()
|
||||
length := int64(len(buf))
|
||||
return bufferReadCloser{v}, length, func() (io.ReadCloser, error) {
|
||||
return readerReadCloser{bytes.NewReader(buf)}, nil
|
||||
}
|
||||
case *bytes.Reader:
|
||||
length := int64(v.Len())
|
||||
// Snapshot the reader's current data so GetBody returns a fresh one.
|
||||
snapshot := *v
|
||||
return readerReadCloser{v}, length, func() (io.ReadCloser, error) {
|
||||
s := snapshot
|
||||
return readerReadCloser{&s}, nil
|
||||
}
|
||||
default:
|
||||
// Unknown reader: no length, no replay. Should not happen with the
|
||||
// current encodeJSONBody body shapes but kept for forward safety.
|
||||
return io.NopCloser(body), -1, nil
|
||||
}
|
||||
}
|
||||
|
||||
// encodeJSONBody marshals req into a JSON body. It returns the body
|
||||
// reader plus, when the codec satisfies BodyEncoder, the pooled buffer
|
||||
// that backs it — callers MUST return that buffer to the pool via
|
||||
// putReqBuf once the request is done. The buffer return is exposed
|
||||
// directly (instead of a closure) so encodeJSONBody allocates nothing
|
||||
// on the pooled path beyond the codec's own internal allocations.
|
||||
//
|
||||
// The {} fast path used for nil/nil-pointer requests bypasses the pool
|
||||
// entirely; the 2-byte literal isn't worth the contention overhead.
|
||||
func encodeJSONBody(codec Codec, req any) (io.Reader, *bytes.Buffer, error) {
|
||||
if req == nil || isNilPointer(req) {
|
||||
return bytes.NewBufferString("{}"), nil
|
||||
return bytes.NewBufferString("{}"), nil, nil
|
||||
}
|
||||
if enc, ok := codec.(BodyEncoder); ok {
|
||||
buf := reqBufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
if err := enc.MarshalTo(buf, req); err != nil {
|
||||
putReqBuf(buf)
|
||||
return nil, nil, &ParseError{Err: err}
|
||||
}
|
||||
return buf, buf, nil
|
||||
}
|
||||
data, err := codec.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, &ParseError{Err: err}
|
||||
return nil, nil, &ParseError{Err: err}
|
||||
}
|
||||
return bytes.NewReader(data), nil
|
||||
return bytes.NewReader(data), nil, nil
|
||||
}
|
||||
|
||||
// decodeResult unmarshals raw into Result[Resp] and translates non-OK
|
||||
|
||||
@@ -63,11 +63,14 @@ func BenchmarkEncodeJSONBody(b *testing.B) {
|
||||
req := &benchSendReq{ChatID: 42, Text: "hello, world"}
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
r, err := encodeJSONBody(codec, req)
|
||||
r, pooled, err := encodeJSONBody(codec, req)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = r
|
||||
if pooled != nil {
|
||||
putReqBuf(pooled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://api.telegram.org"
|
||||
|
||||
// Bot is the Telegram Bot API client. Construct via New. All API methods
|
||||
@@ -10,6 +14,13 @@ type Bot struct {
|
||||
http HTTPDoer
|
||||
codec Codec
|
||||
logger Logger
|
||||
|
||||
// baseURL is the parsed form of base, lazily populated on first Call.
|
||||
// Caching it avoids running url.Parse on every API request.
|
||||
baseURL *url.URL
|
||||
// pathPrefix is "/bot<token>/" built once so per-call URL assembly
|
||||
// is a single string concatenation with the method name.
|
||||
pathPrefix string
|
||||
}
|
||||
|
||||
// Token returns the bot token. Exposed for advanced use cases (custom
|
||||
@@ -34,6 +45,7 @@ func (b *Bot) Logger() Logger { return b.logger }
|
||||
// NewDefaultHTTPDoer); the default codec wraps encoding/json; the default
|
||||
// logger discards records.
|
||||
func New(token string, opts ...Option) *Bot {
|
||||
fireTelemetryOnce()
|
||||
b := &Bot{
|
||||
token: token,
|
||||
base: defaultBaseURL,
|
||||
@@ -44,5 +56,13 @@ func New(token string, opts ...Option) *Bot {
|
||||
for _, o := range opts {
|
||||
o(b)
|
||||
}
|
||||
// Pre-compute URL pieces. Errors here are unlikely (defaultBaseURL is
|
||||
// well-formed; user-supplied bases via WithBaseURL are validated by
|
||||
// url.Parse below) but if parsing fails we leave baseURL nil and fall
|
||||
// back to the string-concat path on the next Call.
|
||||
if u, err := url.Parse(b.base); err == nil {
|
||||
b.baseURL = u
|
||||
}
|
||||
b.pathPrefix = "/bot" + b.token + "/"
|
||||
return b
|
||||
}
|
||||
|
||||
+21
-1
@@ -1,7 +1,11 @@
|
||||
// Package client provides HTTP client primitives for the Telegram Bot API.
|
||||
package client
|
||||
|
||||
import "github.com/goccy/go-json"
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
// Codec encodes/decodes JSON payloads exchanged with the Telegram Bot API.
|
||||
// The default implementation wraps goccy/go-json. Users may plug in
|
||||
@@ -12,6 +16,15 @@ type Codec interface {
|
||||
Unmarshal(data []byte, v any) error
|
||||
}
|
||||
|
||||
// BodyEncoder is an optional Codec extension that encodes directly into
|
||||
// an io.Writer, skipping the intermediate []byte that Marshal returns.
|
||||
// Call uses this when present to avoid allocating the marshal result and
|
||||
// the bytes.Reader that wraps it; codecs without it fall through to
|
||||
// Marshal + bytes.NewReader.
|
||||
type BodyEncoder interface {
|
||||
MarshalTo(w io.Writer, v any) error
|
||||
}
|
||||
|
||||
// DefaultCodec wraps goccy/go-json. It is the zero-value safe default.
|
||||
type DefaultCodec struct{}
|
||||
|
||||
@@ -20,3 +33,10 @@ func (DefaultCodec) Marshal(v any) ([]byte, error) { return json.Marshal(v) }
|
||||
|
||||
// Unmarshal calls json.Unmarshal.
|
||||
func (DefaultCodec) Unmarshal(data []byte, v any) error { return json.Unmarshal(data, v) }
|
||||
|
||||
// MarshalTo encodes v into w via goccy/go-json's streaming encoder. The
|
||||
// trailing newline that Encoder appends is valid JSON whitespace and is
|
||||
// accepted by Telegram's parser.
|
||||
func (DefaultCodec) MarshalTo(w io.Writer, v any) error {
|
||||
return json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// FastHTTPDoer is an HTTPDoer backed by github.com/valyala/fasthttp. It
|
||||
// trades net/http compatibility (and HTTP/2 support) for substantially
|
||||
// fewer allocations per request — fasthttp pools its Request and Response
|
||||
// objects and uses a zero-allocation HTTP/1.1 parser.
|
||||
//
|
||||
// Use it for high-throughput bots when GC pressure matters and you don't
|
||||
// need HTTP/2 or any net/http-only middleware (RoundTripper composition,
|
||||
// the OpenTelemetry httptrace family, etc.):
|
||||
//
|
||||
// bot := client.New(token, client.WithHTTPClient(client.NewFastHTTPDoer()))
|
||||
//
|
||||
// Wrap with RetryDoer the same way you would the default doer.
|
||||
type FastHTTPDoer struct {
|
||||
client *fasthttp.Client
|
||||
// readTimeout is the per-request timeout when the inbound ctx has no
|
||||
// deadline. Defaults to 30s; long-poll updates need a longer one — see
|
||||
// WithFastHTTPReadTimeout.
|
||||
readTimeout time.Duration
|
||||
}
|
||||
|
||||
// FastHTTPDoerOption configures a FastHTTPDoer.
|
||||
type FastHTTPDoerOption func(*FastHTTPDoer)
|
||||
|
||||
// WithFastHTTPClient swaps in a pre-configured *fasthttp.Client.
|
||||
// Useful for sharing a connection pool across multiple bots or applying
|
||||
// custom dial / TLS configuration.
|
||||
func WithFastHTTPClient(c *fasthttp.Client) FastHTTPDoerOption {
|
||||
return func(d *FastHTTPDoer) { d.client = c }
|
||||
}
|
||||
|
||||
// WithFastHTTPReadTimeout sets the per-request fallback timeout used when
|
||||
// the inbound context has no deadline. Long-poll callers should pass a
|
||||
// value larger than the long-poll timeout.
|
||||
func WithFastHTTPReadTimeout(t time.Duration) FastHTTPDoerOption {
|
||||
return func(d *FastHTTPDoer) { d.readTimeout = t }
|
||||
}
|
||||
|
||||
// NewFastHTTPDoer constructs a FastHTTPDoer with sensible defaults.
|
||||
func NewFastHTTPDoer(opts ...FastHTTPDoerOption) *FastHTTPDoer {
|
||||
d := &FastHTTPDoer{
|
||||
client: &fasthttp.Client{
|
||||
ReadTimeout: 90 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
MaxIdleConnDuration: 90 * time.Second,
|
||||
},
|
||||
readTimeout: 30 * time.Second,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(d)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Do satisfies HTTPDoer by translating req into a pooled fasthttp.Request,
|
||||
// dispatching it, and returning a *http.Response whose Body releases the
|
||||
// pooled fasthttp.Response when Close is called.
|
||||
//
|
||||
// The conversion is intentionally minimal: URL goes via req.URL.RequestURI()
|
||||
// + Host (avoids re-parsing), header values move byte-for-byte, and the
|
||||
// body is taken straight from req.Body. *bytes.Buffer / *bytes.Reader are
|
||||
// recognised so we can pass the underlying bytes without io.ReadAll.
|
||||
func (d *FastHTTPDoer) Do(req *http.Request) (*http.Response, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("client: nil http.Request")
|
||||
}
|
||||
|
||||
fReq := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(fReq)
|
||||
|
||||
fReq.SetRequestURI(req.URL.String())
|
||||
fReq.Header.SetMethod(req.Method)
|
||||
if req.Host != "" {
|
||||
fReq.Header.SetHost(req.Host)
|
||||
}
|
||||
for name, values := range req.Header {
|
||||
for _, v := range values {
|
||||
fReq.Header.Set(name, v)
|
||||
}
|
||||
}
|
||||
|
||||
if err := setFastHTTPBody(fReq, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fResp := fasthttp.AcquireResponse()
|
||||
// fResp is released by fasthttpResponseBody.Close — caller is
|
||||
// expected to defer resp.Body.Close() per net/http contract.
|
||||
|
||||
deadline, hasDeadline := req.Context().Deadline()
|
||||
var err error
|
||||
if hasDeadline {
|
||||
err = d.client.DoDeadline(fReq, fResp, deadline)
|
||||
} else {
|
||||
err = d.client.DoTimeout(fReq, fResp, d.readTimeout)
|
||||
}
|
||||
if err != nil {
|
||||
fasthttp.ReleaseResponse(fResp)
|
||||
// Map fasthttp's timeout error to ctx.Err semantics so callers
|
||||
// can errors.Is(err, context.DeadlineExceeded).
|
||||
if hasDeadline && errors.Is(err, fasthttp.ErrTimeout) {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpResp := &http.Response{
|
||||
StatusCode: fResp.StatusCode(),
|
||||
Status: strconv.Itoa(fResp.StatusCode()) + " " + fastHTTPStatusText(fResp.StatusCode()),
|
||||
Header: make(http.Header, fResp.Header.Len()),
|
||||
ContentLength: int64(fResp.Header.ContentLength()),
|
||||
Body: &fasthttpResponseBody{resp: fResp, body: fResp.Body()},
|
||||
Request: req,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
}
|
||||
for k, v := range fResp.Header.All() {
|
||||
httpResp.Header.Add(string(k), string(v))
|
||||
}
|
||||
return httpResp, nil
|
||||
}
|
||||
|
||||
// setFastHTTPBody copies req.Body into fReq with the cheapest path that
|
||||
// preserves correctness. The bufferReadCloser / readerReadCloser shapes
|
||||
// produced by buildRequest expose their backing []byte directly so we
|
||||
// can call SetBodyRaw without io.ReadAll. Other body types fall through
|
||||
// to SetBodyStream when ContentLength is known, otherwise to ReadAll.
|
||||
func setFastHTTPBody(fReq *fasthttp.Request, req *http.Request) error {
|
||||
if req.Body == nil {
|
||||
return nil
|
||||
}
|
||||
switch v := req.Body.(type) {
|
||||
case bufferReadCloser:
|
||||
fReq.SetBodyRaw(v.Bytes())
|
||||
return nil
|
||||
case readerReadCloser:
|
||||
// *bytes.Reader.Bytes() returns the unread portion.
|
||||
size := v.Len()
|
||||
buf := make([]byte, size)
|
||||
_, err := v.Read(buf)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
fReq.SetBodyRaw(buf)
|
||||
return nil
|
||||
default:
|
||||
if req.ContentLength > 0 {
|
||||
fReq.SetBodyStream(v, int(req.ContentLength))
|
||||
} else {
|
||||
body, err := io.ReadAll(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fReq.SetBodyRaw(body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// fasthttpResponseBody adapts a pooled *fasthttp.Response so it satisfies
|
||||
// io.ReadCloser. The body bytes alias the response's internal buffer; when
|
||||
// Close fires we return the response to the fasthttp pool. Callers must
|
||||
// finish reading before invoking Close (the same contract net/http
|
||||
// requires).
|
||||
type fasthttpResponseBody struct {
|
||||
resp *fasthttp.Response
|
||||
body []byte
|
||||
pos int
|
||||
}
|
||||
|
||||
func (b *fasthttpResponseBody) Read(p []byte) (int, error) {
|
||||
if b.pos >= len(b.body) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, b.body[b.pos:])
|
||||
b.pos += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (b *fasthttpResponseBody) Close() error {
|
||||
if b.resp != nil {
|
||||
fasthttp.ReleaseResponse(b.resp)
|
||||
b.resp = nil
|
||||
b.body = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fastHTTPStatusText returns the textual reason phrase for a status code,
|
||||
// matching the format net/http produces for *http.Response.Status. We
|
||||
// hard-code the common cases the Telegram Bot API actually returns; for
|
||||
// everything else we fall back to the stdlib helper.
|
||||
func fastHTTPStatusText(code int) string {
|
||||
switch code {
|
||||
case http.StatusOK:
|
||||
return "OK"
|
||||
case http.StatusBadRequest:
|
||||
return "Bad Request"
|
||||
case http.StatusUnauthorized:
|
||||
return "Unauthorized"
|
||||
case http.StatusForbidden:
|
||||
return "Forbidden"
|
||||
case http.StatusNotFound:
|
||||
return "Not Found"
|
||||
case http.StatusTooManyRequests:
|
||||
return "Too Many Requests"
|
||||
case http.StatusInternalServerError:
|
||||
return "Internal Server Error"
|
||||
case http.StatusBadGateway:
|
||||
return "Bad Gateway"
|
||||
case http.StatusServiceUnavailable:
|
||||
return "Service Unavailable"
|
||||
case http.StatusGatewayTimeout:
|
||||
return "Gateway Timeout"
|
||||
default:
|
||||
return http.StatusText(code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFastHTTPDoer_BasicRoundTrip(t *testing.T) {
|
||||
got := make(chan struct{ method, ct, body string }, 1)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
got <- struct{ method, ct, body string }{r.Method, r.Header.Get("Content-Type"), string(body)}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = io.WriteString(w, `{"ok":true,"result":42}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
d := NewFastHTTPDoer()
|
||||
req, err := http.NewRequest(http.MethodPost, srv.URL+"/sendMessage", strings.NewReader(`{"chat_id":1,"text":"hi"}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = req.WithContext(context.Background())
|
||||
|
||||
resp, err := d.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status: got %d", resp.StatusCode)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if string(body) != `{"ok":true,"result":42}` {
|
||||
t.Fatalf("body: got %q", body)
|
||||
}
|
||||
|
||||
rec := <-got
|
||||
if rec.method != http.MethodPost {
|
||||
t.Fatalf("method: got %q", rec.method)
|
||||
}
|
||||
if rec.ct != "application/json" {
|
||||
t.Fatalf("content-type: got %q", rec.ct)
|
||||
}
|
||||
if rec.body != `{"chat_id":1,"text":"hi"}` {
|
||||
t.Fatalf("body: got %q", rec.body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFastHTTPDoer_HonoursContextDeadline(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
_, _ = io.WriteString(w, "ok")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
d := NewFastHTTPDoer(WithFastHTTPReadTimeout(time.Hour))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
|
||||
defer cancel()
|
||||
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
_, err := d.Do(req)
|
||||
if err == nil {
|
||||
t.Fatal("expected timeout error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFastHTTPDoer_IntegratesWithBot(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, `{"ok":true,"result":{"message_id":7,"date":0,"text":"hi"}}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
bot := New("123:abc",
|
||||
WithBaseURL(srv.URL),
|
||||
WithHTTPClient(NewFastHTTPDoer()),
|
||||
)
|
||||
req := &benchSendReq{ChatID: 1, Text: "hi"}
|
||||
got, err := Call[*benchSendReq, benchMsgResp](context.Background(), bot, "sendMessage", req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.MessageID != 7 || got.Text != "hi" {
|
||||
t.Fatalf("got %+v", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
telemetry "github.com/lukaszraczylo/oss-telemetry"
|
||||
)
|
||||
|
||||
// telemetryOnce guards the single anonymous "library used" ping that is sent
|
||||
// on the first call to New. Long-running bots typically construct one Bot;
|
||||
// short-lived programs or test suites may construct many, but the Once gate
|
||||
// keeps the fire-and-forget call from amplifying into per-construction pings.
|
||||
var telemetryOnce sync.Once
|
||||
|
||||
// fireTelemetryOnce dispatches a fire-and-forget anonymous adoption ping.
|
||||
//
|
||||
// The call is failproof by contract of oss-telemetry: it never blocks New,
|
||||
// never panics, never returns errors, and silently no-ops if disabled or
|
||||
// if the network is unavailable.
|
||||
//
|
||||
// Opt-out is honored via any of these environment variables (case-insensitive
|
||||
// truthy values "1", "true", "yes", "on"):
|
||||
//
|
||||
// - DO_NOT_TRACK
|
||||
// - OSS_TELEMETRY_DISABLED
|
||||
// - GO_TELEGRAM_DISABLE_TELEMETRY
|
||||
//
|
||||
// See README §Telemetry for the full disclosure.
|
||||
func fireTelemetryOnce() {
|
||||
telemetryOnce.Do(func() {
|
||||
telemetry.SendForModule("go-telegram", "github.com/lukaszraczylo/go-telegram", Version)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
telemetry "github.com/lukaszraczylo/oss-telemetry"
|
||||
)
|
||||
|
||||
// TestMain disables outgoing telemetry for the duration of this package's
|
||||
// test suite. The library's own tests construct many Bot instances; without
|
||||
// this guard they would each contribute a real ping to the public endpoint.
|
||||
// End-user test suites that construct Bot are not affected by this — only
|
||||
// tests inside this package are.
|
||||
func TestMain(m *testing.M) {
|
||||
telemetry.Disable()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// TestFireTelemetryOnce_OnlyFiresOnce verifies the sync.Once gate. Even if
|
||||
// New is called repeatedly, the underlying telemetry.Send is invoked at most
|
||||
// once per process. We can't observe the network call directly (telemetry
|
||||
// is disabled here via TestMain) so we assert on the once-Do count via a
|
||||
// fresh local sync.Once paralleling the production one.
|
||||
func TestFireTelemetryOnce_OnlyFiresOnce(t *testing.T) {
|
||||
// Reset the package-level Once so this test starts from a clean state.
|
||||
telemetryOnce = sync.Once{}
|
||||
t.Cleanup(func() { telemetryOnce = sync.Once{} })
|
||||
|
||||
calls := 0
|
||||
probe := func() { telemetryOnce.Do(func() { calls++ }) }
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
probe()
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatalf("expected exactly 1 Once execution, got %d", calls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNew_DoesNotPanicUnderRepeatedConstruction is a smoke test that
|
||||
// telemetry wiring does not affect New's existing contract. New must never
|
||||
// panic, regardless of telemetry state.
|
||||
func TestNew_DoesNotPanicUnderRepeatedConstruction(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("New panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
for i := 0; i < 20; i++ {
|
||||
_ = New("test-token-" + string(rune('A'+i)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package client
|
||||
|
||||
// Version is a fallback version string used only when Go's build info is
|
||||
// unavailable (replace directives, detached `go run`) or has been overridden
|
||||
// via linker flags. The authoritative version forwarded to telemetry is
|
||||
// resolved at runtime by [telemetry.SendForModule] from the build info of
|
||||
// whatever binary linked this library, so this constant does NOT need to be
|
||||
// bumped on every release. Exposed as a var (not const) for ldflag override:
|
||||
//
|
||||
// go build -ldflags="-X github.com/lukaszraczylo/go-telegram/client.Version=1.2.3"
|
||||
var Version = "0.0.0-fallback"
|
||||
+11
-5
@@ -125,12 +125,18 @@ func TestAuditDrift_InvalidRefReturnsError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuditDrift_SameRefNoDrift(t *testing.T) {
|
||||
// Compare HEAD's IR against itself, not against the working tree: during a
|
||||
// regen run the working tree legitimately carries new methods, and drift
|
||||
// against HEAD is the expected outcome rather than a failure.
|
||||
irPath := "../../internal/spec/api.json"
|
||||
cur, err := loadIR(irPath)
|
||||
out, err := exec.Command("git", "show", "HEAD:"+irPath).Output()
|
||||
if err != nil {
|
||||
t.Skip("api.json not available, skipping drift test")
|
||||
t.Skip("api.json not available at HEAD, skipping drift test")
|
||||
}
|
||||
changes, err := auditDrift(irPath, "HEAD", cur)
|
||||
var head spec.API
|
||||
require.NoError(t, json.Unmarshal(out, &head))
|
||||
|
||||
changes, err := auditDrift(irPath, "HEAD", &head)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, changes)
|
||||
}
|
||||
@@ -166,7 +172,7 @@ func TestAuditAny_FlagsUnknownMethodReturn(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
out := auditAny(api)
|
||||
out := auditAny(api, &spec.Overrides{})
|
||||
require.Len(t, out, 1)
|
||||
require.Contains(t, out[0], "any return: weirdMethod")
|
||||
}
|
||||
@@ -182,7 +188,7 @@ func TestAuditAny_FlagsUnknownMethodParam(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
out := auditAny(api)
|
||||
out := auditAny(api, &spec.Overrides{})
|
||||
require.Len(t, out, 1)
|
||||
require.Contains(t, out[0], "any param: weirdMethod.Thing")
|
||||
}
|
||||
|
||||
+7
-6
@@ -54,7 +54,7 @@ func main() {
|
||||
var problems []string
|
||||
|
||||
problems = append(problems, auditBool(api, overrides)...)
|
||||
problems = append(problems, auditAny(api)...)
|
||||
problems = append(problems, auditAny(api, overrides)...)
|
||||
|
||||
driftFound := false
|
||||
if *checkDrift {
|
||||
@@ -140,8 +140,9 @@ func looksGenuinelyBool(doc string) bool {
|
||||
|
||||
// auditAny scans the IR for any KindOneOf TypeRef that would render as
|
||||
// `any` in generated code (not matched by ChatID/InputFile-or-string/known
|
||||
// union heuristics). Reports each occurrence with location.
|
||||
func auditAny(api *spec.API) []string {
|
||||
// union heuristics). Reports each occurrence with location, except the
|
||||
// locations approved in overrides.
|
||||
func auditAny(api *spec.API, ov *spec.Overrides) []string {
|
||||
var out []string
|
||||
isKnownUnion := func(variants []string) bool {
|
||||
if hasVariants(variants, "int64", "string") {
|
||||
@@ -166,17 +167,17 @@ func auditAny(api *spec.API) []string {
|
||||
}
|
||||
for _, t := range api.Types {
|
||||
for _, f := range t.Fields {
|
||||
if isAny(f.Type) {
|
||||
if isAny(f.Type) && !ov.IsAnyApproved(t.Name+"."+f.Name) {
|
||||
out = append(out, fmt.Sprintf("any field: %s.%s (variants=%v)", t.Name, f.Name, f.Type.Variants))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, m := range api.Methods {
|
||||
if isAny(m.Returns) {
|
||||
if isAny(m.Returns) && !ov.IsAnyApproved(m.Name) {
|
||||
out = append(out, fmt.Sprintf("any return: %s (variants=%v)", m.Name, m.Returns.Variants))
|
||||
}
|
||||
for _, p := range m.Params {
|
||||
if isAny(p.Type) {
|
||||
if isAny(p.Type) && !ov.IsAnyApproved(m.Name+"."+p.Name) {
|
||||
out = append(out, fmt.Sprintf("any param: %s.%s (variants=%v)", m.Name, p.Name, p.Type.Variants))
|
||||
}
|
||||
}
|
||||
|
||||
+31
-5
@@ -66,11 +66,37 @@ func TestAuditAny_FlagsUnrecognisedOneOf(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
out := auditAny(api)
|
||||
out := auditAny(api, &spec.Overrides{})
|
||||
require.Len(t, out, 1)
|
||||
require.Contains(t, out[0], "any field: Foo.Bar")
|
||||
}
|
||||
|
||||
func TestAuditAny_SkipsApprovedLocations(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{
|
||||
Name: "Foo",
|
||||
Fields: []spec.Field{
|
||||
{Name: "Bar", JSONName: "bar", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Methods: []spec.MethodDecl{
|
||||
{
|
||||
Name: "doThing",
|
||||
Returns: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}},
|
||||
Params: []spec.Field{
|
||||
{Name: "Baz", JSONName: "baz", Type: spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"A", "B"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Len(t, auditAny(api, &spec.Overrides{}), 3)
|
||||
|
||||
ov := &spec.Overrides{ApprovedAnyLocations: []string{"Foo.Bar", "doThing", "doThing.Baz"}}
|
||||
require.Empty(t, auditAny(api, ov))
|
||||
}
|
||||
|
||||
func TestAuditAny_SkipsChatIDShape(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
@@ -82,7 +108,7 @@ func TestAuditAny_SkipsChatIDShape(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Empty(t, auditAny(api))
|
||||
require.Empty(t, auditAny(api, &spec.Overrides{}))
|
||||
}
|
||||
|
||||
func TestAuditAny_SkipsKnownUnion(t *testing.T) {
|
||||
@@ -97,7 +123,7 @@ func TestAuditAny_SkipsKnownUnion(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Empty(t, auditAny(api))
|
||||
require.Empty(t, auditAny(api, &spec.Overrides{}))
|
||||
}
|
||||
|
||||
func TestAuditAny_SkipsReplyMarkupShape(t *testing.T) {
|
||||
@@ -111,7 +137,7 @@ func TestAuditAny_SkipsReplyMarkupShape(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Empty(t, auditAny(api))
|
||||
require.Empty(t, auditAny(api, &spec.Overrides{}))
|
||||
}
|
||||
|
||||
func TestAuditAny_SkipsInputFileShape(t *testing.T) {
|
||||
@@ -125,7 +151,7 @@ func TestAuditAny_SkipsInputFileShape(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Empty(t, auditAny(api))
|
||||
require.Empty(t, auditAny(api, &spec.Overrides{}))
|
||||
}
|
||||
|
||||
// ---- diffSignatures ------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestUnionAlternates_NoAlternates confirms a union with no Alternates
|
||||
// (the common case — every union except RichText) produces no synthetic
|
||||
// variants.
|
||||
func TestUnionAlternates_NoAlternates(t *testing.T) {
|
||||
td := spec.TypeDecl{Name: "ChatMember", OneOf: []string{"ChatMemberOwner"}}
|
||||
require.Nil(t, unionAlternates(td))
|
||||
}
|
||||
|
||||
// TestUnionAlternates_StringAndArray confirms both alternate shapes map
|
||||
// to the expected synthetic variant names, in declaration order.
|
||||
func TestUnionAlternates_StringAndArray(t *testing.T) {
|
||||
td := spec.TypeDecl{
|
||||
Name: "RichText",
|
||||
OneOf: []string{"RichTextBold"},
|
||||
Alternates: []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
},
|
||||
}
|
||||
got := unionAlternates(td)
|
||||
require.Equal(t, []alternateVariant{
|
||||
{Name: "RichTextPlain", IsString: true},
|
||||
{Name: "RichTextSequence", IsArray: true},
|
||||
}, got)
|
||||
}
|
||||
|
||||
// TestEmitTypes_UnionWithAlternates_EmitsShapeDispatcher builds a minimal
|
||||
// synthetic API with one discriminated union carrying Alternates (mirrors
|
||||
// RichText's shape without depending on the full real spec) and confirms
|
||||
// genapi emits: the two synthetic variant structs, their marker methods
|
||||
// and MarshalJSON, and a shape-dispatching Unmarshal<Union> that branches
|
||||
// on the first non-whitespace byte before falling back to the existing
|
||||
// discriminator switch.
|
||||
func TestEmitTypes_UnionWithAlternates_EmitsShapeDispatcher(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{
|
||||
Name: "Widget",
|
||||
OneOf: []string{"WidgetBold"},
|
||||
Alternates: []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "WidgetBold",
|
||||
Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"bold"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
e := newEmitter(api, tmp)
|
||||
require.NoError(t, e.emitTypes())
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(tmp, "types.gen.go"))
|
||||
require.NoError(t, err)
|
||||
src := string(got)
|
||||
|
||||
// Synthetic variants exist and implement the sealed interface.
|
||||
require.Contains(t, src, "type WidgetPlain struct")
|
||||
require.Contains(t, src, "Text string")
|
||||
require.Contains(t, src, "func (*WidgetPlain) isWidget() {}")
|
||||
require.Contains(t, src, "type WidgetSequence struct")
|
||||
require.Contains(t, src, "Items []Widget")
|
||||
require.Contains(t, src, "func (*WidgetSequence) isWidget() {}")
|
||||
|
||||
// Round-trip MarshalJSON for both.
|
||||
require.Contains(t, src, "func (v *WidgetPlain) MarshalJSON() ([]byte, error) {")
|
||||
require.Contains(t, src, "return json.Marshal(v.Text)")
|
||||
require.Contains(t, src, "func (v *WidgetSequence) MarshalJSON() ([]byte, error) {")
|
||||
require.Contains(t, src, "return json.Marshal(v.Items)")
|
||||
|
||||
// Shape-dispatching UnmarshalWidget: trims, branches on '"', '[', '{'.
|
||||
require.Contains(t, src, "func UnmarshalWidget(data []byte) (Widget, error) {")
|
||||
require.Contains(t, src, `trimmed := bytes.TrimLeft(data, " \t\r\n")`)
|
||||
require.Contains(t, src, "case '\"':")
|
||||
require.Contains(t, src, "return &WidgetPlain{Text: s}, nil")
|
||||
require.Contains(t, src, "case '[':")
|
||||
require.Contains(t, src, "return &WidgetSequence{Items: items}, nil")
|
||||
require.Contains(t, src, "case '{':")
|
||||
// The pre-existing discriminator switch is preserved beneath the
|
||||
// shape dispatch, unrecognised discriminator still a clear error.
|
||||
require.Contains(t, src, `fmt.Errorf("Widget: unknown type %q", probe.V)`)
|
||||
|
||||
// The struct-holding-the-union case inherits shape dispatch for free:
|
||||
// WidgetBold has no union-typed field itself, so nothing further to
|
||||
// assert there beyond successful compilation-shaped source (covered
|
||||
// by TestEmit_Types_FixtureGolden's gofmt round trip for the real
|
||||
// fixture, and by the api-package tests for full decode behaviour).
|
||||
require.False(t, strings.Contains(src, "WidgetPlainSequence"), "sanity: no accidental name collision")
|
||||
}
|
||||
|
||||
// TestEmitTypes_UnionWithoutAlternates_Unaffected confirms a discriminated
|
||||
// union with no Alternates emits the original (pre-alternates) shape:
|
||||
// no synthetic variants, no shape dispatch, and the same "decodes a X
|
||||
// from JSON by inspecting" doc comment wording as before this feature.
|
||||
func TestEmitTypes_UnionWithoutAlternates_Unaffected(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetBold"}},
|
||||
{
|
||||
Name: "WidgetBold",
|
||||
Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"bold"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
e := newEmitter(api, tmp)
|
||||
require.NoError(t, e.emitTypes())
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(tmp, "types.gen.go"))
|
||||
require.NoError(t, err)
|
||||
src := string(got)
|
||||
|
||||
require.NotContains(t, src, "WidgetPlain")
|
||||
require.NotContains(t, src, "WidgetSequence")
|
||||
// The package-level `var _ = bytes.TrimLeft` import guard is always
|
||||
// present (it keeps the "bytes" import valid even when no union in
|
||||
// this API has alternates); what must NOT appear is the per-union
|
||||
// shape-dispatch call site.
|
||||
require.NotContains(t, src, "trimmed := bytes.TrimLeft(data,")
|
||||
require.Contains(t, src, "// UnmarshalWidget decodes a Widget from JSON by inspecting the")
|
||||
require.Contains(t, src, `// "type" field and dispatching to the correct concrete type.`)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestDeriveDiscriminators_RichBlockAndRichText_FromRealSpec is a
|
||||
// regression test for the Bot API v10 rich_message decode failure:
|
||||
// RichBlock and RichText had no knownDiscriminators entry, so genapi
|
||||
// never emitted UnmarshalRichBlock / UnmarshalRichText and any update
|
||||
// whose message carried rich_message failed to decode. Derivation must
|
||||
// pick "type" for both and cover every variant the spec declares.
|
||||
//
|
||||
// The expected variant counts are read from the spec rather than pinned
|
||||
// as literals. Telegram adds variants in most releases - v10.3 took
|
||||
// RichBlock from 21 to 24 and RichText from 25 to 26 - so a frozen
|
||||
// number turns a routine regeneration into a spurious failure while
|
||||
// testing nothing the coverage assertion does not already cover.
|
||||
func TestDeriveDiscriminators_RichBlockAndRichText_FromRealSpec(t *testing.T) {
|
||||
api, err := loadAPI("../../internal/spec/api.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
// declaredVariants returns how many concrete variants the spec lists
|
||||
// for a union, so the assertions below track the spec instead of a
|
||||
// hardcoded count.
|
||||
declaredVariants := func(union string) int {
|
||||
for i := range api.Types {
|
||||
if api.Types[i].Name == union {
|
||||
return len(api.Types[i].OneOf)
|
||||
}
|
||||
}
|
||||
t.Fatalf("union %q not found in spec", union)
|
||||
return 0
|
||||
}
|
||||
|
||||
derived, _ := deriveDiscriminators(api)
|
||||
|
||||
rb, ok := derived["RichBlock"]
|
||||
require.True(t, ok, "RichBlock must derive a discriminator")
|
||||
require.Equal(t, "type", rb.Field)
|
||||
require.Len(t, rb.Variants, declaredVariants("RichBlock"),
|
||||
"every RichBlock variant declared in the spec must be covered")
|
||||
require.Equal(t, "RichBlockParagraph", rb.Variants["paragraph"])
|
||||
require.Equal(t, "RichBlockTable", rb.Variants["table"])
|
||||
require.Equal(t, "RichBlockThinking", rb.Variants["thinking"])
|
||||
|
||||
rt, ok := derived["RichText"]
|
||||
require.True(t, ok, "RichText must derive a discriminator")
|
||||
require.Equal(t, "type", rt.Field)
|
||||
require.Len(t, rt.Variants, declaredVariants("RichText"),
|
||||
"every RichText variant declared in the spec must be covered")
|
||||
require.Equal(t, "RichTextBold", rt.Variants["bold"])
|
||||
require.Equal(t, "RichTextAnchorLink", rt.Variants["anchor_link"])
|
||||
require.Equal(t, "RichTextReferenceLink", rt.Variants["reference_link"])
|
||||
}
|
||||
|
||||
// TestMergeDiscriminators_HandCuratedOverridesDerived confirms a
|
||||
// knownDiscriminators entry always wins the merge even when derivation
|
||||
// would also have produced a (different) answer for the same union —
|
||||
// hand-curation is the escape hatch for a union the generic rule gets
|
||||
// wrong.
|
||||
func TestMergeDiscriminators_HandCuratedOverridesDerived(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
|
||||
{Name: "WidgetA", Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"a"}},
|
||||
}},
|
||||
{Name: "WidgetB", Fields: []spec.Field{
|
||||
{Name: "Type", JSONName: "type", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}, EnumValues: []string{"b"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
derived, ungated := deriveDiscriminators(api)
|
||||
require.Empty(t, ungated)
|
||||
require.Equal(t, discriminatorSpec{
|
||||
Field: "type",
|
||||
Variants: map[string]string{"a": "WidgetA", "b": "WidgetB"},
|
||||
}, derived["Widget"])
|
||||
|
||||
// Inject a hand-curated override that disagrees with the derived
|
||||
// spec, merge, and confirm the override wins outright.
|
||||
override := discriminatorSpec{Field: "kind", Variants: map[string]string{"custom": "WidgetA"}}
|
||||
knownDiscriminators["Widget"] = override
|
||||
t.Cleanup(func() { delete(knownDiscriminators, "Widget") })
|
||||
|
||||
merged := mergeDiscriminators(derived)
|
||||
require.Equal(t, override, merged["Widget"])
|
||||
}
|
||||
|
||||
// TestCheckDiscriminatorGate_FailsOnUngatedInboundUnion confirms the
|
||||
// build gate fails generation when a union Telegram can actually send
|
||||
// has no derivable discriminator: the variants share no common
|
||||
// Type/Status/Source field at all.
|
||||
func TestCheckDiscriminatorGate_FailsOnUngatedInboundUnion(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
|
||||
{Name: "WidgetA", Fields: []spec.Field{
|
||||
{Name: "Kind", JSONName: "kind", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
{Name: "WidgetB", Fields: []spec.Field{
|
||||
{Name: "Flavor", JSONName: "flavor", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
// Container is a plain (non-Input) type with a field of the
|
||||
// undiscriminated union — this is what makes Widget reachable
|
||||
// from real inbound Telegram data.
|
||||
{Name: "Container", Fields: []spec.Field{
|
||||
{Name: "W", JSONName: "w", Type: spec.TypeRef{Kind: spec.KindNamed, Name: "Widget"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
err := checkDiscriminatorGate(api)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Widget")
|
||||
require.ErrorContains(t, err, "WidgetA")
|
||||
require.ErrorContains(t, err, "WidgetB")
|
||||
}
|
||||
|
||||
// TestCheckDiscriminatorGate_SilentForInputOnlyUnion confirms the same
|
||||
// undiscriminated union does NOT fail the build when its only reference
|
||||
// is a field on an Input*-prefixed type — those are always constructed
|
||||
// and marshalled by the caller, never decoded from a Telegram response.
|
||||
func TestCheckDiscriminatorGate_SilentForInputOnlyUnion(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Types: []spec.TypeDecl{
|
||||
{Name: "Widget", OneOf: []string{"WidgetA", "WidgetB"}},
|
||||
{Name: "WidgetA", Fields: []spec.Field{
|
||||
{Name: "Kind", JSONName: "kind", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
{Name: "WidgetB", Fields: []spec.Field{
|
||||
{Name: "Flavor", JSONName: "flavor", Required: true, Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"}},
|
||||
}},
|
||||
{Name: "InputContainer", Fields: []spec.Field{
|
||||
{Name: "W", JSONName: "w", Type: spec.TypeRef{Kind: spec.KindNamed, Name: "Widget"}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, checkDiscriminatorGate(api))
|
||||
}
|
||||
+367
-46
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/goccy/go-json"
|
||||
"go/format"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
@@ -77,9 +79,18 @@ type discriminatorSpec struct {
|
||||
Variants map[string]string // discriminator value → concrete Go type name
|
||||
}
|
||||
|
||||
// knownDiscriminators maps parent union name → discriminator spec.
|
||||
// Used by the template helpers hasDiscriminator / discriminatorField /
|
||||
// discriminatorMap to emit UnmarshalXxx helpers.
|
||||
// knownDiscriminators is the hand-curated OVERRIDE table for sealed-union
|
||||
// discriminators. deriveDiscriminators computes the same kind of map
|
||||
// automatically from the IR for every union that has one (see
|
||||
// mergeDiscriminators); an entry here always wins the merge regardless of
|
||||
// what derivation would have produced. Use this table only for unions the
|
||||
// generic rule can't handle — e.g. MaybeInaccessibleMessage's integer
|
||||
// discriminator, which needs bespoke decode logic no discriminatorSpec
|
||||
// can express (Field is "" and Variants is nil; the template emits a
|
||||
// hand-coded Unmarshal via isMaybeInaccessibleMessage instead). Every
|
||||
// other entry here exists only because it predates derivation — removing
|
||||
// one and re-running `go test -run TestEmit -update ./cmd/genapi/...`
|
||||
// should reproduce it byte-for-byte from deriveDiscriminators.
|
||||
var knownDiscriminators = map[string]discriminatorSpec{
|
||||
"ChatMember": {
|
||||
Field: "status",
|
||||
@@ -205,15 +216,24 @@ type emitter struct {
|
||||
// template to emit per-variant MarshalJSON that hardcodes the
|
||||
// discriminator so callers don't have to set it by hand.
|
||||
variantDiscs map[string]variantDiscriminator
|
||||
// discs is the merged union-level discriminator table: derivation
|
||||
// output with knownDiscriminators layered on top as overrides (see
|
||||
// mergeDiscriminators). Every codegen path that decides whether a
|
||||
// union auto-decodes — and how — consults this instead of the
|
||||
// package-level knownDiscriminators map, so a union qualifies the
|
||||
// moment deriveDiscriminators can resolve it, hand-curated or not.
|
||||
discs map[string]discriminatorSpec
|
||||
}
|
||||
|
||||
func newEmitter(api *spec.API, outDir string) *emitter {
|
||||
knownInterfaceTypes = buildUnionTypeSet(api)
|
||||
derived, _ := deriveDiscriminators(api)
|
||||
return &emitter{
|
||||
api: api,
|
||||
outDir: outDir,
|
||||
enums: planEnums(api),
|
||||
variantDiscs: variantDiscriminators(api),
|
||||
discs: mergeDiscriminators(derived),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,6 +369,262 @@ func parseDiscriminatorDoc(doc string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// discriminatorFieldCandidates returns the Go field names to try, in
|
||||
// priority order, when looking for a discriminator shared by every
|
||||
// variant of a union: the hand-curated field from knownDiscriminators
|
||||
// (resolved to its Go name via the first variant that declares it) when
|
||||
// the union already has an override entry, then "Type", "Status",
|
||||
// "Source" — the three names Telegram's docs consistently use for this
|
||||
// purpose. Shared by planUnifiedUnionEnums (enum naming, enums.go) and
|
||||
// deriveDiscriminators (decode dispatch, below) so the two never disagree
|
||||
// on where to look.
|
||||
func discriminatorFieldCandidates(unionName string, variants []*spec.TypeDecl) []string {
|
||||
var candidates []string
|
||||
seen := map[string]bool{}
|
||||
add := func(name string) {
|
||||
if name == "" || seen[name] {
|
||||
return
|
||||
}
|
||||
seen[name] = true
|
||||
candidates = append(candidates, name)
|
||||
}
|
||||
if ds, ok := knownDiscriminators[unionName]; ok && ds.Field != "" {
|
||||
for _, v := range variants {
|
||||
for _, f := range v.Fields {
|
||||
if f.JSONName == ds.Field {
|
||||
add(f.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
add("Type")
|
||||
add("Status")
|
||||
add("Source")
|
||||
return candidates
|
||||
}
|
||||
|
||||
// fieldByGoName returns the field named name on fields, or nil if none matches.
|
||||
func fieldByGoName(fields []spec.Field, name string) *spec.Field {
|
||||
for i := range fields {
|
||||
if fields[i].Name == name {
|
||||
return &fields[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// discriminatorValue extracts the wire-level value a variant field
|
||||
// contributes to a discriminator: a single-entry EnumValues list (the
|
||||
// scraper's detected value), or a doc-parsed "must be X" / "always “X”"
|
||||
// literal via parseDiscriminatorDoc (the same extraction
|
||||
// extractVariantDiscriminator uses for the first-field case). Returns ""
|
||||
// when the field isn't a required string or carries no resolvable value.
|
||||
func discriminatorValue(f spec.Field) string {
|
||||
if !f.Required || f.Type.Kind != spec.KindPrimitive || f.Type.Name != "string" {
|
||||
return ""
|
||||
}
|
||||
if len(f.EnumValues) == 1 {
|
||||
return f.EnumValues[0]
|
||||
}
|
||||
return parseDiscriminatorDoc(f.Doc)
|
||||
}
|
||||
|
||||
// deriveDiscriminators inspects every union TypeDecl (len(OneOf) > 0) in
|
||||
// api and tries to pick a discriminator JSON field automatically, so the
|
||||
// hand-maintained knownDiscriminators map no longer has to name every
|
||||
// union that qualifies. A union qualifies when, for some candidate field
|
||||
// (see discriminatorFieldCandidates), EVERY variant declares that field
|
||||
// and discriminatorValue resolves a non-empty wire value for it, and
|
||||
// those values are unique across variants.
|
||||
//
|
||||
// Returns the derived map plus the names of unions that did not qualify,
|
||||
// in declaration order. Callers merge the derived map with
|
||||
// knownDiscriminators (mergeDiscriminators) and decide what to do about
|
||||
// the leftovers — rescue via a hand-curated override, or fail the build
|
||||
// if the union is reachable from real Telegram data (checkDiscriminatorGate).
|
||||
func deriveDiscriminators(api *spec.API) (map[string]discriminatorSpec, []string) {
|
||||
typeByName := make(map[string]*spec.TypeDecl, len(api.Types))
|
||||
for i := range api.Types {
|
||||
typeByName[api.Types[i].Name] = &api.Types[i]
|
||||
}
|
||||
|
||||
derived := make(map[string]discriminatorSpec, 32)
|
||||
var ungated []string
|
||||
|
||||
for i := range api.Types {
|
||||
u := &api.Types[i]
|
||||
if len(u.OneOf) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
variants := make([]*spec.TypeDecl, 0, len(u.OneOf))
|
||||
resolved := true
|
||||
for _, vName := range u.OneOf {
|
||||
v, found := typeByName[vName]
|
||||
if !found {
|
||||
resolved = false
|
||||
break
|
||||
}
|
||||
variants = append(variants, v)
|
||||
}
|
||||
if !resolved {
|
||||
ungated = append(ungated, u.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
ds, qualified := deriveUnionDiscriminator(u.Name, variants)
|
||||
if !qualified {
|
||||
ungated = append(ungated, u.Name)
|
||||
continue
|
||||
}
|
||||
derived[u.Name] = ds
|
||||
}
|
||||
return derived, ungated
|
||||
}
|
||||
|
||||
// deriveUnionDiscriminator tries each candidate Go field name in
|
||||
// priority order and returns the first one where every variant declares
|
||||
// that field with a resolvable, unique wire value.
|
||||
func deriveUnionDiscriminator(unionName string, variants []*spec.TypeDecl) (discriminatorSpec, bool) {
|
||||
for _, goName := range discriminatorFieldCandidates(unionName, variants) {
|
||||
jsonField := ""
|
||||
values := make(map[string]string, len(variants)) // wire value -> variant Go type name
|
||||
qualifies := true
|
||||
for _, v := range variants {
|
||||
f := fieldByGoName(v.Fields, goName)
|
||||
if f == nil {
|
||||
qualifies = false
|
||||
break
|
||||
}
|
||||
value := discriminatorValue(*f)
|
||||
if value == "" {
|
||||
qualifies = false
|
||||
break
|
||||
}
|
||||
if jsonField == "" {
|
||||
jsonField = f.JSONName
|
||||
} else if f.JSONName != jsonField {
|
||||
qualifies = false
|
||||
break
|
||||
}
|
||||
if _, dup := values[value]; dup {
|
||||
qualifies = false
|
||||
break
|
||||
}
|
||||
values[value] = v.Name
|
||||
}
|
||||
if qualifies {
|
||||
return discriminatorSpec{Field: jsonField, Variants: values}, true
|
||||
}
|
||||
}
|
||||
return discriminatorSpec{}, false
|
||||
}
|
||||
|
||||
// mergeDiscriminators layers the hand-curated knownDiscriminators table
|
||||
// on top of derived: every entry deriveDiscriminators produces is used
|
||||
// as-is unless knownDiscriminators also names that union, in which case
|
||||
// the hand-curated entry wins outright (the whole discriminatorSpec, not
|
||||
// a per-key merge).
|
||||
func mergeDiscriminators(derived map[string]discriminatorSpec) map[string]discriminatorSpec {
|
||||
merged := make(map[string]discriminatorSpec, len(derived)+len(knownDiscriminators))
|
||||
for name, ds := range derived {
|
||||
merged[name] = ds
|
||||
}
|
||||
for name, ds := range knownDiscriminators {
|
||||
merged[name] = ds
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// isInboundReachable reports whether Telegram can actually send data
|
||||
// typed as the named union to the bot: as a field on some type that
|
||||
// isn't itself send-only, or as a method's return value. "Input*"-named
|
||||
// unions are excluded outright regardless of where they're referenced —
|
||||
// by Telegram's own naming convention in this API, every such union is
|
||||
// something the bot constructs and marshals, never something it decodes.
|
||||
func isInboundReachable(api *spec.API, unionName string) bool {
|
||||
if strings.HasPrefix(unionName, "Input") {
|
||||
return false
|
||||
}
|
||||
for _, m := range api.Methods {
|
||||
if typeRefIsUnion(m.Returns, api, unionName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, t := range api.Types {
|
||||
if strings.HasPrefix(t.Name, "Input") {
|
||||
continue // send-only struct; its fields are never decoded either.
|
||||
}
|
||||
for _, f := range t.Fields {
|
||||
if typeRefIsUnion(f.Type, api, unionName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// typeRefIsUnion reports whether tr resolves to the named union: directly,
|
||||
// as an array element, or via an inline oneOf variant-set match against
|
||||
// unionName's own OneOf list.
|
||||
func typeRefIsUnion(tr spec.TypeRef, api *spec.API, unionName string) bool {
|
||||
switch tr.Kind {
|
||||
case spec.KindNamed:
|
||||
return tr.Name == unionName
|
||||
case spec.KindArray:
|
||||
if tr.ElemType != nil {
|
||||
return typeRefIsUnion(*tr.ElemType, api, unionName)
|
||||
}
|
||||
case spec.KindOneOf:
|
||||
for i := range api.Types {
|
||||
if api.Types[i].Name == unionName && matchesVariants(tr.Variants, api.Types[i].OneOf...) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// checkDiscriminatorGate fails codegen when a union Telegram can actually
|
||||
// send to the bot has no usable discriminator — neither derived nor
|
||||
// hand-curated. Such a union decodes to a bare interface field that no
|
||||
// JSON codec can ever fill (the RichBlock/RichText Bot API v10
|
||||
// regression this generalisation fixes: both had 20+ variants and no
|
||||
// knownDiscriminators entry, so genapi silently emitted no UnmarshalXxx
|
||||
// helper at all). Send-only "Input*" unions are exempt — see
|
||||
// isInboundReachable.
|
||||
func checkDiscriminatorGate(api *spec.API) error {
|
||||
_, ungated := deriveDiscriminators(api)
|
||||
|
||||
typeByName := make(map[string]*spec.TypeDecl, len(api.Types))
|
||||
for i := range api.Types {
|
||||
typeByName[api.Types[i].Name] = &api.Types[i]
|
||||
}
|
||||
|
||||
var offending []string
|
||||
for _, name := range ungated {
|
||||
if _, overridden := knownDiscriminators[name]; overridden {
|
||||
continue // hand-curated rescue, e.g. MaybeInaccessibleMessage.
|
||||
}
|
||||
if isInboundReachable(api, name) {
|
||||
offending = append(offending, name)
|
||||
}
|
||||
}
|
||||
if len(offending) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "genapi: %d inbound union(s) have no derivable or hand-curated discriminator "+
|
||||
"and would decode as an unfillable interface field:", len(offending))
|
||||
for _, name := range offending {
|
||||
fmt.Fprintf(&b, "\n - %s (variants: %s): no single field is present, required, and holds a unique non-empty wire value on every variant",
|
||||
name, strings.Join(typeByName[name].OneOf, ", "))
|
||||
}
|
||||
return errors.New(b.String())
|
||||
}
|
||||
|
||||
// knownInterfaceTypes is the full set of sealed-interface union type names
|
||||
// (both auto-decoded ones in knownDiscriminators and marker-only ones from
|
||||
// types with OneOf). Populated at emitter construction. goType and
|
||||
@@ -359,7 +635,7 @@ var knownInterfaceTypes = map[string]bool{}
|
||||
|
||||
// emitTypes renders types.gen.go.
|
||||
func (e *emitter) emitTypes() error {
|
||||
t, err := template.New("types").Funcs(funcsWithDiscs(e.enums, e.variantDiscs)).Parse(typesTmpl)
|
||||
t, err := template.New("types").Funcs(funcsWithDiscs(e.enums, e.variantDiscs, e.discs)).Parse(typesTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse types.tmpl: %w", err)
|
||||
}
|
||||
@@ -396,31 +672,35 @@ func loadAPI(path string) (*spec.API, error) {
|
||||
}
|
||||
|
||||
// funcsWithDiscs returns the shared FuncMap with the variant
|
||||
// discriminator helpers bound to discs. types.tmpl uses
|
||||
// discriminator helpers bound to variantDiscs. types.tmpl uses
|
||||
// variantDiscFor/variantHasDisc to emit per-variant MarshalJSON that
|
||||
// hardcodes the wire discriminator value.
|
||||
func funcsWithDiscs(plan *enumPlan, discs map[string]variantDiscriminator) template.FuncMap {
|
||||
fm := funcs(plan)
|
||||
// hardcodes the wire discriminator value. unionDiscs is the merged
|
||||
// union-level table (see mergeDiscriminators), threaded into funcs.
|
||||
func funcsWithDiscs(plan *enumPlan, variantDiscs map[string]variantDiscriminator, unionDiscs map[string]discriminatorSpec) template.FuncMap {
|
||||
fm := funcs(plan, unionDiscs)
|
||||
fm["variantHasDisc"] = func(name string) bool {
|
||||
_, ok := discs[name]
|
||||
_, ok := variantDiscs[name]
|
||||
return ok
|
||||
}
|
||||
fm["variantDiscField"] = func(name string) string { return discs[name].JSONField }
|
||||
fm["variantDiscGoField"] = func(name string) string { return discs[name].GoField }
|
||||
fm["variantDiscValue"] = func(name string) string { return discs[name].Value }
|
||||
fm["variantDiscField"] = func(name string) string { return variantDiscs[name].JSONField }
|
||||
fm["variantDiscGoField"] = func(name string) string { return variantDiscs[name].GoField }
|
||||
fm["variantDiscValue"] = func(name string) string { return variantDiscs[name].Value }
|
||||
return fm
|
||||
}
|
||||
|
||||
// funcs is the FuncMap shared across templates. plan is the resolved
|
||||
// enum plan; pass nil only in unit tests that don't exercise enums.
|
||||
func funcs(plan *enumPlan) template.FuncMap {
|
||||
// enum plan; pass nil only in unit tests that don't exercise enums. discs
|
||||
// is the merged union-level discriminator table (see
|
||||
// mergeDiscriminators); pass nil (or knownDiscriminators directly) in
|
||||
// unit tests that only exercise the hand-curated unions.
|
||||
func funcs(plan *enumPlan, discs map[string]discriminatorSpec) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"goType": goType,
|
||||
"goField": func(parent string, f spec.Field) string {
|
||||
return goField(plan, parent, f)
|
||||
},
|
||||
"goFieldP": func(methodName string, f spec.Field) string {
|
||||
return goFieldX(plan, "", title(methodName)+"Params", f)
|
||||
return goFieldX(plan, methodEnumParent(methodName), title(methodName)+"Params", f)
|
||||
},
|
||||
"docComment": docComment,
|
||||
"isOptional": func(f spec.Field) bool { return !f.Required },
|
||||
@@ -432,7 +712,7 @@ func funcs(plan *enumPlan) template.FuncMap {
|
||||
return multipartFieldEntry(plan, parent, f)
|
||||
},
|
||||
"multipartFieldEntryP": func(methodName string, f spec.Field) string {
|
||||
return multipartFieldEntryX(plan, "", title(methodName)+"Params", f)
|
||||
return multipartFieldEntryX(plan, methodEnumParent(methodName), title(methodName)+"Params", f)
|
||||
},
|
||||
"multipartFileEntry": multipartFileEntry,
|
||||
"returnGoType": returnGoType,
|
||||
@@ -444,20 +724,22 @@ func funcs(plan *enumPlan) template.FuncMap {
|
||||
return plan.All()
|
||||
},
|
||||
"enumConstName": constName,
|
||||
// discriminator helpers for types.tmpl
|
||||
"hasDiscriminator": func(name string) bool { s, ok := knownDiscriminators[name]; return ok && len(s.Variants) > 0 },
|
||||
// discriminator helpers for types.tmpl — all consult the merged
|
||||
// table (discs), not the package-level knownDiscriminators, so a
|
||||
// derived-only union gates exactly like a hand-curated one.
|
||||
"hasDiscriminator": func(name string) bool { s, ok := discs[name]; return ok && len(s.Variants) > 0 },
|
||||
"isSealedUnionReturn": func(tr spec.TypeRef) bool {
|
||||
if tr.Kind != spec.KindNamed {
|
||||
return false
|
||||
}
|
||||
s, ok := knownDiscriminators[tr.Name]
|
||||
s, ok := discs[tr.Name]
|
||||
return ok && len(s.Variants) > 0
|
||||
},
|
||||
"isSealedUnionArrayReturn": func(tr spec.TypeRef) bool {
|
||||
if tr.Kind != spec.KindArray || tr.ElemType == nil || tr.ElemType.Kind != spec.KindNamed {
|
||||
return false
|
||||
}
|
||||
s, ok := knownDiscriminators[tr.ElemType.Name]
|
||||
s, ok := discs[tr.ElemType.Name]
|
||||
return ok && len(s.Variants) > 0
|
||||
},
|
||||
"sealedUnionElemName": func(tr spec.TypeRef) string {
|
||||
@@ -467,12 +749,13 @@ func funcs(plan *enumPlan) template.FuncMap {
|
||||
return ""
|
||||
},
|
||||
"isMaybeInaccessibleMessage": func(name string) bool { return name == "MaybeInaccessibleMessage" },
|
||||
"discriminatorField": func(name string) string { return knownDiscriminators[name].Field },
|
||||
"discriminatorMap": func(name string) map[string]string { return knownDiscriminators[name].Variants },
|
||||
"discriminatorField": func(name string) string { return discs[name].Field },
|
||||
"discriminatorMap": func(name string) map[string]string { return discs[name].Variants },
|
||||
// union-field helpers for per-struct UnmarshalJSON emission
|
||||
"unionFields": unionFieldsOf,
|
||||
"isArrayUnion": func(tr spec.TypeRef) bool { return hasUnionElem(tr) },
|
||||
"unionTypeName": func(tr spec.TypeRef) string { name, _ := unionTypeFor(tr); return name },
|
||||
"unionFields": func(t spec.TypeDecl) []unionField { return unionFieldsOf(t, discs) },
|
||||
"isArrayUnion": func(tr spec.TypeRef) bool { return hasUnionElem(tr, discs) },
|
||||
"unionTypeName": func(tr spec.TypeRef) string { name, _ := unionTypeFor(tr, discs); return name },
|
||||
"unionAlternates": unionAlternates,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,7 +928,7 @@ func returnGoElem(tr spec.TypeRef) string {
|
||||
|
||||
// emitMethods renders methods.gen.go.
|
||||
func (e *emitter) emitMethods() error {
|
||||
t, err := template.New("methods").Funcs(funcs(e.enums)).Parse(methodsTmpl)
|
||||
t, err := template.New("methods").Funcs(funcs(e.enums, e.discs)).Parse(methodsTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse methods.tmpl: %w", err)
|
||||
}
|
||||
@@ -662,7 +945,7 @@ func (e *emitter) emitMethods() error {
|
||||
|
||||
// emitEnums renders enums.gen.go.
|
||||
func (e *emitter) emitEnums() error {
|
||||
t, err := template.New("enums").Funcs(funcs(e.enums)).Parse(enumsTmpl)
|
||||
t, err := template.New("enums").Funcs(funcs(e.enums, e.discs)).Parse(enumsTmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse enums.tmpl: %w", err)
|
||||
}
|
||||
@@ -735,11 +1018,12 @@ type unionField struct {
|
||||
}
|
||||
|
||||
// unionFieldsOf returns the subset of t.Fields whose type is a known
|
||||
// discriminated union (directly or as array element).
|
||||
func unionFieldsOf(t spec.TypeDecl) []unionField {
|
||||
// discriminated union (directly or as array element), per discs — the
|
||||
// merged union-level discriminator table (see mergeDiscriminators).
|
||||
func unionFieldsOf(t spec.TypeDecl, discs map[string]discriminatorSpec) []unionField {
|
||||
var out []unionField
|
||||
for _, f := range t.Fields {
|
||||
if u, ok := unionTypeFor(f.Type); ok {
|
||||
if u, ok := unionTypeFor(f.Type, discs); ok {
|
||||
out = append(out, unionField{Field: f, UnionName: u})
|
||||
}
|
||||
}
|
||||
@@ -747,19 +1031,20 @@ func unionFieldsOf(t spec.TypeDecl) []unionField {
|
||||
}
|
||||
|
||||
// unionTypeFor inspects a TypeRef and reports whether it (or its array
|
||||
// element) is a known discriminated union. Returns the union name and true.
|
||||
func unionTypeFor(tr spec.TypeRef) (string, bool) {
|
||||
// element) is a known discriminated union in discs. Returns the union
|
||||
// name and true.
|
||||
func unionTypeFor(tr spec.TypeRef, discs map[string]discriminatorSpec) (string, bool) {
|
||||
switch tr.Kind {
|
||||
case spec.KindNamed:
|
||||
if _, ok := knownDiscriminators[tr.Name]; ok {
|
||||
if _, ok := discs[tr.Name]; ok {
|
||||
return tr.Name, true
|
||||
}
|
||||
case spec.KindArray:
|
||||
if tr.ElemType != nil {
|
||||
return unionTypeFor(*tr.ElemType)
|
||||
return unionTypeFor(*tr.ElemType, discs)
|
||||
}
|
||||
case spec.KindOneOf:
|
||||
if u := unionNameByVariants(tr.Variants); u != "" {
|
||||
if u := unionNameByVariants(tr.Variants, discs); u != "" {
|
||||
return u, true
|
||||
}
|
||||
}
|
||||
@@ -768,8 +1053,8 @@ func unionTypeFor(tr spec.TypeRef) (string, bool) {
|
||||
|
||||
// unionNameByVariants finds the parent union whose variant type names exactly
|
||||
// match the given variant set (order-insensitive).
|
||||
func unionNameByVariants(variants []string) string {
|
||||
for parentName, ds := range knownDiscriminators {
|
||||
func unionNameByVariants(variants []string, discs map[string]discriminatorSpec) string {
|
||||
for parentName, ds := range discs {
|
||||
wanted := make([]string, 0, len(ds.Variants))
|
||||
for _, vt := range ds.Variants {
|
||||
wanted = append(wanted, vt)
|
||||
@@ -782,14 +1067,50 @@ func unionNameByVariants(variants []string) string {
|
||||
}
|
||||
|
||||
// hasUnionElem reports whether tr is an array whose element type is a known union.
|
||||
func hasUnionElem(tr spec.TypeRef) bool {
|
||||
func hasUnionElem(tr spec.TypeRef, discs map[string]discriminatorSpec) bool {
|
||||
if tr.Kind != spec.KindArray || tr.ElemType == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := unionTypeFor(*tr.ElemType)
|
||||
_, ok := unionTypeFor(*tr.ElemType, discs)
|
||||
return ok
|
||||
}
|
||||
|
||||
// alternateVariant describes one synthetic sealed-interface variant the
|
||||
// generator emits for a union's non-object wire shape (see
|
||||
// spec.Alternate): a bare-JSON-string variant, or a variant holding a
|
||||
// JSON array of the union itself. Exactly one of IsString / IsArray is
|
||||
// set. types.tmpl emits the struct, marker method, and MarshalJSON for
|
||||
// each, and Unmarshal<Union> dispatches to it by inspecting the first
|
||||
// non-whitespace byte of the wire value.
|
||||
type alternateVariant struct {
|
||||
Name string // synthetic Go type name, e.g. "RichTextPlain"
|
||||
IsString bool
|
||||
IsArray bool
|
||||
}
|
||||
|
||||
// unionAlternates maps a union TypeDecl's spec.Alternate entries (see
|
||||
// cmd/scrape's extractAlternates) to the synthetic Go variant
|
||||
// descriptors types.tmpl emits alongside the regular OneOf variants:
|
||||
// "<Union>Plain" for the bare-string shape, "<Union>Sequence" for the
|
||||
// array-of-<Union> shape. Returns nil for a union with no alternates,
|
||||
// which is every union except RichText as of Bot API v10 — so this is a
|
||||
// no-op for the rest of the type list.
|
||||
func unionAlternates(t spec.TypeDecl) []alternateVariant {
|
||||
if len(t.Alternates) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]alternateVariant, 0, len(t.Alternates))
|
||||
for _, a := range t.Alternates {
|
||||
switch a.Shape {
|
||||
case spec.AlternateString:
|
||||
out = append(out, alternateVariant{Name: t.Name + "Plain", IsString: true})
|
||||
case spec.AlternateArray:
|
||||
out = append(out, alternateVariant{Name: t.Name + "Sequence", IsArray: true})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// matchesVariants reports whether got equals want as a set (order-insensitive).
|
||||
func matchesVariants(got []string, want ...string) bool {
|
||||
if len(got) != len(want) {
|
||||
@@ -915,15 +1236,15 @@ func buildUnionTypeSet(api *spec.API) map[string]bool {
|
||||
// used in generated test param literals. plan supplies typed-enum names
|
||||
// so a method-param sentinel for a ParseMode field becomes a typed
|
||||
// constant rather than a magic string.
|
||||
func makeSentinelValue(unionTypes map[string]bool, plan *enumPlan) func(spec.Field) string {
|
||||
return func(f spec.Field) string {
|
||||
return sentinelForField(f, unionTypes, plan)
|
||||
func makeSentinelValue(unionTypes map[string]bool, plan *enumPlan) func(string, spec.Field) string {
|
||||
return func(methodName string, f spec.Field) string {
|
||||
return sentinelForField(methodName, f, unionTypes, plan)
|
||||
}
|
||||
}
|
||||
|
||||
func sentinelForField(f spec.Field, unionTypes map[string]bool, plan *enumPlan) string {
|
||||
if name := plan.FieldEnum("", f.Name); name != "" && len(f.EnumValues) > 0 {
|
||||
return constName(name, f.EnumValues[0])
|
||||
func sentinelForField(methodName string, f spec.Field, unionTypes map[string]bool, plan *enumPlan) string {
|
||||
if name := plan.FieldEnum(methodEnumParent(methodName), f.Name); name != "" && len(f.EnumValues) > 0 {
|
||||
return plan.ConstFor(name, f.EnumValues[0])
|
||||
}
|
||||
tr := f.Type
|
||||
switch tr.Kind {
|
||||
@@ -1023,7 +1344,7 @@ func (e *emitter) emitTests() error {
|
||||
unionTypes := buildUnionTypeSet(e.api)
|
||||
|
||||
// Add test-specific helpers to the shared func map.
|
||||
fm := funcs(e.enums)
|
||||
fm := funcs(e.enums, e.discs)
|
||||
fm["sentinelValue"] = makeSentinelValue(unionTypes, e.enums)
|
||||
fm["successResp"] = successResp
|
||||
|
||||
|
||||
+77
-34
@@ -26,15 +26,21 @@ type enumPlan struct {
|
||||
}
|
||||
|
||||
// enumKey identifies a single Field occurrence so the emitter can look
|
||||
// up the enum name later. Parent is "" for method params (the method
|
||||
// doesn't share a Go type with the field).
|
||||
// up the enum name later. Parent is the type name for struct fields and
|
||||
// methodEnumParent(name) for method params, so two methods sharing a
|
||||
// param name never alias each other's enum (or non-enum) fields.
|
||||
func enumKey(parent, fieldName string) string { return parent + "::" + fieldName }
|
||||
|
||||
// methodEnumParent is the enum-plan key parent for a method's params.
|
||||
// The "method:" prefix keeps it disjoint from type names.
|
||||
func methodEnumParent(methodName string) string { return "method:" + methodName }
|
||||
|
||||
// planEnums walks the IR, decides on enum names, deduplicates, and
|
||||
// returns an enumPlan. All scraper-marked enum fields are covered.
|
||||
func planEnums(api *spec.API) *enumPlan {
|
||||
type ref struct {
|
||||
parent string
|
||||
parent string // naming parent ("" for method params)
|
||||
keyParent string // byField key parent (methodEnumParent(...) for method params)
|
||||
fieldName string
|
||||
jsonName string
|
||||
values []string
|
||||
@@ -52,7 +58,7 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
|
||||
var refs []ref
|
||||
collect := func(parent string, fields []spec.Field) {
|
||||
collect := func(parent, keyParent string, fields []spec.Field) {
|
||||
for _, f := range fields {
|
||||
if len(f.EnumValues) == 0 {
|
||||
continue
|
||||
@@ -62,6 +68,7 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
refs = append(refs, ref{
|
||||
parent: parent,
|
||||
keyParent: keyParent,
|
||||
fieldName: f.Name,
|
||||
jsonName: f.JSONName,
|
||||
values: f.EnumValues,
|
||||
@@ -70,13 +77,15 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
}
|
||||
for _, t := range api.Types {
|
||||
collect(t.Name, t.Fields)
|
||||
collect(t.Name, t.Name, t.Fields)
|
||||
}
|
||||
for _, m := range api.Methods {
|
||||
// Method params have no shared Go parent type, so we pass "" as
|
||||
// the parent. The default-name heuristic still produces the
|
||||
// right answer for ParseMode-style enums.
|
||||
collect("", m.Params)
|
||||
// Method params have no shared Go parent type, so the naming
|
||||
// parent is "" (the default-name heuristic still produces the
|
||||
// right answer for ParseMode-style enums), but the byField key
|
||||
// is method-scoped so a same-named non-enum param on another
|
||||
// method can never pick up this enum.
|
||||
collect("", methodEnumParent(m.Name), m.Params)
|
||||
}
|
||||
|
||||
// candidate name per ref (before collision resolution)
|
||||
@@ -196,7 +205,7 @@ func planEnums(api *spec.API) *enumPlan {
|
||||
}
|
||||
for i, r := range refs {
|
||||
name := groups[r.valueKey].name
|
||||
plan.byField[enumKey(r.parent, r.fieldName)] = name
|
||||
plan.byField[enumKey(r.keyParent, r.fieldName)] = name
|
||||
_ = i
|
||||
}
|
||||
for vk, g := range groups {
|
||||
@@ -262,32 +271,12 @@ func planUnifiedUnionEnums(api *spec.API) (map[string]enumDecl, map[string]strin
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the candidate Go-name list. Priority order:
|
||||
// Build the candidate Go-name list (shared with deriveDiscriminators
|
||||
// in emitter.go so enum naming and decode dispatch never disagree
|
||||
// on where to look). Priority order:
|
||||
// 1. discriminator GoField from knownDiscriminators (resolved via JSONName);
|
||||
// 2. "Type", "Status", "Source".
|
||||
var candidateNames []string
|
||||
seen := map[string]bool{}
|
||||
add := func(name string) {
|
||||
if name == "" || seen[name] {
|
||||
return
|
||||
}
|
||||
seen[name] = true
|
||||
candidateNames = append(candidateNames, name)
|
||||
}
|
||||
if ds, ok := knownDiscriminators[u.Name]; ok && ds.Field != "" {
|
||||
// Resolve Go-name from the first variant whose field matches the JSON name.
|
||||
for _, v := range variants {
|
||||
for _, f := range v.Fields {
|
||||
if f.JSONName == ds.Field {
|
||||
add(f.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, n := range []string{"Type", "Status", "Source"} {
|
||||
add(n)
|
||||
}
|
||||
candidateNames := discriminatorFieldCandidates(u.Name, variants)
|
||||
|
||||
// Find the first candidate Go-name where every variant has a
|
||||
// matching single-value string-enum field.
|
||||
@@ -389,6 +378,18 @@ func (p *enumPlan) FieldEnum(parent, fieldName string) string {
|
||||
return p.byField[enumKey(parent, fieldName)]
|
||||
}
|
||||
|
||||
// ConstFor returns the collision-resolved constant identifier for value
|
||||
// within the named enum declaration. Falls back to the plain constName
|
||||
// when the declaration is unknown (unit tests with partial plans).
|
||||
func (p *enumPlan) ConstFor(enumName, value string) string {
|
||||
if p != nil {
|
||||
if d, ok := p.decls[enumName]; ok {
|
||||
return d.ConstName(value)
|
||||
}
|
||||
}
|
||||
return constName(enumName, value)
|
||||
}
|
||||
|
||||
// defaultEnumName picks an initial Go enum name for a field. parse_mode
|
||||
// fields collapse to the canonical "ParseMode"; otherwise the name is
|
||||
// parent + PascalCase(jsonName).
|
||||
@@ -406,6 +407,48 @@ func constName(enumName, value string) string {
|
||||
return enumName + valuePascal(value)
|
||||
}
|
||||
|
||||
// ConstName returns the constant identifier for value within this enum,
|
||||
// resolving case-collisions between values that Pascal-case to the same
|
||||
// identifier (e.g. RichBlockListItem.type values "a" and "A"). Colliding
|
||||
// values get a Lower/Upper prefix on the value part based on the case of
|
||||
// the value's first letter; any residual collision gets a numeric suffix.
|
||||
func (e enumDecl) ConstName(value string) string {
|
||||
counts := map[string]int{}
|
||||
for _, v := range e.Values {
|
||||
counts[constName(e.Name, v)]++
|
||||
}
|
||||
plain := constName(e.Name, value)
|
||||
if counts[plain] <= 1 {
|
||||
return plain
|
||||
}
|
||||
cased := e.Name + casePrefix(value) + valuePascal(value)
|
||||
// Residual collision (same value text repeated, or Lower/Upper still
|
||||
// ambiguous): append the value's 1-based position for determinism.
|
||||
casedCounts := map[string]int{}
|
||||
pos := 0
|
||||
for i, v := range e.Values {
|
||||
if counts[constName(e.Name, v)] > 1 {
|
||||
casedCounts[e.Name+casePrefix(v)+valuePascal(v)]++
|
||||
}
|
||||
if v == value {
|
||||
pos = i + 1
|
||||
}
|
||||
}
|
||||
if casedCounts[cased] > 1 {
|
||||
return cased + itoa(pos)
|
||||
}
|
||||
return cased
|
||||
}
|
||||
|
||||
// casePrefix distinguishes case-colliding enum values: "Lower" when the
|
||||
// value starts with a lowercase letter, "Upper" otherwise.
|
||||
func casePrefix(v string) string {
|
||||
if v != "" && v[0] >= 'a' && v[0] <= 'z' {
|
||||
return "Lower"
|
||||
}
|
||||
return "Upper"
|
||||
}
|
||||
|
||||
func valuePascal(v string) string {
|
||||
// "image/jpeg" → "ImageOfJpeg"
|
||||
parts := strings.Split(v, "/")
|
||||
|
||||
@@ -8,6 +8,6 @@ package api
|
||||
type {{$e.Name}} string
|
||||
|
||||
const (
|
||||
{{range $v := $e.Values}} {{enumConstName $e.Name $v}} {{$e.Name}} = {{printf "%q" $v}}
|
||||
{{range $v := $e.Values}} {{$e.ConstName $v}} {{$e.Name}} = {{printf "%q" $v}}
|
||||
{{end}})
|
||||
{{end}}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Regression: Bot API 9.x RichBlockListItem.type enumerates "a"/"A"/"i"/"I"/"1",
|
||||
// which Pascal-case to the same identifier. ConstName must disambiguate.
|
||||
func TestEnumDecl_ConstName_CaseCollision(t *testing.T) {
|
||||
d := enumDecl{Name: "RichBlockListItemType", Values: []string{"a", "A", "i", "I", "1"}}
|
||||
got := map[string]bool{}
|
||||
for _, v := range d.Values {
|
||||
n := d.ConstName(v)
|
||||
require.False(t, got[n], "duplicate const ident %q for value %q", n, v)
|
||||
got[n] = true
|
||||
}
|
||||
require.Equal(t, "RichBlockListItemTypeLowerA", d.ConstName("a"))
|
||||
require.Equal(t, "RichBlockListItemTypeUpperA", d.ConstName("A"))
|
||||
// Non-colliding values keep the plain name.
|
||||
require.Equal(t, "RichBlockListItemType1", d.ConstName("1"))
|
||||
}
|
||||
|
||||
// Regression: answerChatJoinRequestQuery has an enum param named "result";
|
||||
// answerGuestQuery has a NON-enum param also named "result". The enum plan
|
||||
// must scope method params per method so the enum never leaks onto the
|
||||
// other method's field.
|
||||
func TestPlanEnums_MethodParamsScopedPerMethod(t *testing.T) {
|
||||
api := &spec.API{
|
||||
Methods: []spec.MethodDecl{
|
||||
{
|
||||
Name: "answerChatJoinRequestQuery",
|
||||
Params: []spec.Field{{
|
||||
Name: "Result", JSONName: "result", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "string"},
|
||||
EnumValues: []string{"approve", "decline", "queue"},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Name: "answerGuestQuery",
|
||||
Params: []spec.Field{{
|
||||
Name: "Result", JSONName: "result", Required: true,
|
||||
Type: spec.TypeRef{Kind: spec.KindNamed, Name: "InlineQueryResult"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
plan := planEnums(api)
|
||||
require.Equal(t, "Result",
|
||||
plan.FieldEnum(methodEnumParent("answerChatJoinRequestQuery"), "Result"))
|
||||
require.Empty(t,
|
||||
plan.FieldEnum(methodEnumParent("answerGuestQuery"), "Result"),
|
||||
"non-enum param must not inherit another method's enum")
|
||||
}
|
||||
+16
-16
@@ -280,7 +280,7 @@ func TestMultipartFieldEntry_Named_Optional(t *testing.T) {
|
||||
|
||||
func TestUnionTypeFor_DirectNamed(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
|
||||
name, ok := unionTypeFor(tr)
|
||||
name, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "ChatMember", name)
|
||||
}
|
||||
@@ -288,26 +288,26 @@ func TestUnionTypeFor_DirectNamed(t *testing.T) {
|
||||
func TestUnionTypeFor_Array(t *testing.T) {
|
||||
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
|
||||
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
|
||||
name, ok := unionTypeFor(tr)
|
||||
name, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "ChatMember", name)
|
||||
}
|
||||
|
||||
func TestUnionTypeFor_ArrayNilElem(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindArray}
|
||||
_, ok := unionTypeFor(tr)
|
||||
_, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
func TestUnionTypeFor_NotUnion(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}
|
||||
_, ok := unionTypeFor(tr)
|
||||
_, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
func TestUnionTypeFor_Unknown(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.Kind(99)}
|
||||
_, ok := unionTypeFor(tr)
|
||||
_, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
@@ -321,12 +321,12 @@ func TestUnionNameByVariants_ChatMember(t *testing.T) {
|
||||
"ChatMemberOwner", "ChatMemberAdministrator", "ChatMemberMember",
|
||||
"ChatMemberRestricted", "ChatMemberLeft", "ChatMemberBanned",
|
||||
}
|
||||
name := unionNameByVariants(variants)
|
||||
name := unionNameByVariants(variants, knownDiscriminators)
|
||||
require.Equal(t, "ChatMember", name)
|
||||
}
|
||||
|
||||
func TestUnionNameByVariants_Unknown(t *testing.T) {
|
||||
name := unionNameByVariants([]string{"X", "Y", "Z"})
|
||||
name := unionNameByVariants([]string{"X", "Y", "Z"}, knownDiscriminators)
|
||||
require.Equal(t, "", name)
|
||||
}
|
||||
|
||||
@@ -336,24 +336,24 @@ func TestUnionNameByVariants_Unknown(t *testing.T) {
|
||||
|
||||
func TestHasUnionElem_NonArray(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
|
||||
require.False(t, hasUnionElem(tr))
|
||||
require.False(t, hasUnionElem(tr, knownDiscriminators))
|
||||
}
|
||||
|
||||
func TestHasUnionElem_ArrayNilElem(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindArray}
|
||||
require.False(t, hasUnionElem(tr))
|
||||
require.False(t, hasUnionElem(tr, knownDiscriminators))
|
||||
}
|
||||
|
||||
func TestHasUnionElem_ArrayUnionElem(t *testing.T) {
|
||||
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "ChatMember"}
|
||||
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
|
||||
require.True(t, hasUnionElem(tr))
|
||||
require.True(t, hasUnionElem(tr, knownDiscriminators))
|
||||
}
|
||||
|
||||
func TestHasUnionElem_ArrayNonUnionElem(t *testing.T) {
|
||||
elem := spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}
|
||||
tr := spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
|
||||
require.False(t, hasUnionElem(tr))
|
||||
require.False(t, hasUnionElem(tr, knownDiscriminators))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -369,7 +369,7 @@ func TestUnionFieldsOf_WithUnionField(t *testing.T) {
|
||||
{Name: "Date", JSONName: "date", Type: spec.TypeRef{Kind: spec.KindPrimitive, Name: "int64"}},
|
||||
},
|
||||
}
|
||||
uf := unionFieldsOf(td)
|
||||
uf := unionFieldsOf(td, knownDiscriminators)
|
||||
require.Len(t, uf, 2)
|
||||
require.Equal(t, "ChatMember", uf[0].UnionName)
|
||||
}
|
||||
@@ -574,7 +574,7 @@ func TestSentinelForField(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := sentinelForField(c.field, unionTypes, nil)
|
||||
got := sentinelForField("testMethod", c.field, unionTypes, nil)
|
||||
require.Contains(t, got, c.contains, "sentinelForField for %q", c.name)
|
||||
})
|
||||
}
|
||||
@@ -621,14 +621,14 @@ func TestUnionTypeFor_OneOfVariants(t *testing.T) {
|
||||
"ChatMemberRestricted", "ChatMemberLeft", "ChatMemberBanned",
|
||||
},
|
||||
}
|
||||
name, ok := unionTypeFor(tr)
|
||||
name, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "ChatMember", name)
|
||||
}
|
||||
|
||||
func TestUnionTypeFor_OneOfNoMatch(t *testing.T) {
|
||||
tr := spec.TypeRef{Kind: spec.KindOneOf, Variants: []string{"Foo", "Bar"}}
|
||||
_, ok := unionTypeFor(tr)
|
||||
_, ok := unionTypeFor(tr, knownDiscriminators)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
@@ -637,7 +637,7 @@ func TestUnionTypeFor_OneOfNoMatch(t *testing.T) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestFuncs_HasExpectedKeys(t *testing.T) {
|
||||
fm := funcs(nil)
|
||||
fm := funcs(nil, nil)
|
||||
require.NotNil(t, fm)
|
||||
for _, key := range []string{"goType", "docComment", "returnGoType", "unionFields"} {
|
||||
require.NotNil(t, fm[key], "funcs() missing key %q", key)
|
||||
|
||||
@@ -29,6 +29,9 @@ func run(input, outdir string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("load api: %w", err)
|
||||
}
|
||||
if err := checkDiscriminatorGate(api); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(outdir, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func Test_{{$mName}}_Success(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -67,7 +67,7 @@ func Test_{{$mName}}_APIError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -89,7 +89,7 @@ func Test_{{$mName}}_NetworkError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -109,7 +109,7 @@ func Test_{{$mName}}_ParseError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -132,7 +132,7 @@ func Test_{{$mName}}_ContextCanceled(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(ctx, bot, params)
|
||||
@@ -177,7 +177,7 @@ func Test_{{$mName}}_Forbidden(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
@@ -203,7 +203,7 @@ func Test_{{$mName}}_ServerError(t *testing.T) {
|
||||
{{- if .Params}}
|
||||
params := &{{$mName}}Params{
|
||||
{{- range .Params}}{{if .Required}}
|
||||
{{.Name}}: {{sentinelValue .}},{{end}}
|
||||
{{.Name}}: {{sentinelValue $m.Name .}},{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
_, err := {{$mName}}(context.Background(), bot, params)
|
||||
|
||||
+65
-6
@@ -7,14 +7,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/goccy/go-json"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = bytes.TrimLeft // keep import for shape-dispatching UnmarshalXxx helpers (unions with alternates)
|
||||
|
||||
{{range .Types}}
|
||||
{{- $td := . -}}
|
||||
@@ -22,6 +24,7 @@ var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
// {{.Name}} is a union type. The following concrete variants implement
|
||||
// it:
|
||||
{{range .OneOf}}// - {{.}}
|
||||
{{end}}{{range unionAlternates .}}// - {{.Name}} ({{if .IsString}}bare JSON string{{else}}JSON array of {{$td.Name}}{{end}})
|
||||
{{end}}//
|
||||
{{docComment .Doc -}}
|
||||
type {{.Name}} interface{ is{{.Name}}() }
|
||||
@@ -30,11 +33,67 @@ type {{.Name}} interface{ is{{.Name}}() }
|
||||
// is{{$td.Name}} is the marker method that makes {{.}} implement {{$td.Name}}.
|
||||
func (*{{.}}) is{{$td.Name}}() {}
|
||||
{{end}}
|
||||
{{range unionAlternates .}}
|
||||
// {{.Name}} implements {{$td.Name}} for the {{if .IsString}}bare JSON string{{else}}JSON array of {{$td.Name}}{{end}} wire
|
||||
// shape Telegram documents for this union, alongside its object variants.
|
||||
type {{.Name}} struct {
|
||||
{{if .IsString}} Text string
|
||||
{{else}} Items []{{$td.Name}}
|
||||
{{end}}}
|
||||
|
||||
// is{{$td.Name}} is the marker method that makes {{.Name}} implement {{$td.Name}}.
|
||||
func (*{{.Name}}) is{{$td.Name}}() {}
|
||||
|
||||
{{if .IsString}}// MarshalJSON encodes {{.Name}} back to its bare JSON string wire shape.
|
||||
func (v *{{.Name}}) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.Text)
|
||||
}
|
||||
{{else}}// MarshalJSON encodes {{.Name}} back to its bare JSON array wire shape.
|
||||
func (v *{{.Name}}) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.Items)
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if hasDiscriminator .Name}}
|
||||
// Unmarshal{{.Name}} decodes a {{.Name}} from JSON by inspecting the
|
||||
{{$alts := unionAlternates .}}{{if $alts}}// Unmarshal{{.Name}} decodes a {{.Name}} from JSON. Telegram documents
|
||||
// this type as either a bare JSON string, a JSON array of {{.Name}}, or
|
||||
// an object dispatched by its "{{discriminatorField .Name}}" field;
|
||||
// leading whitespace is trimmed and the first remaining byte selects
|
||||
// the shape.
|
||||
{{else}}// Unmarshal{{.Name}} decodes a {{.Name}} from JSON by inspecting the
|
||||
// "{{discriminatorField .Name}}" field and dispatching to the correct concrete type.
|
||||
func Unmarshal{{.Name}}(data []byte) ({{.Name}}, error) {
|
||||
var probe struct {
|
||||
{{end}}func Unmarshal{{.Name}}(data []byte) ({{.Name}}, error) {
|
||||
{{if $alts}} trimmed := bytes.TrimLeft(data, " \t\r\n")
|
||||
if len(trimmed) == 0 {
|
||||
return nil, fmt.Errorf("{{.Name}}: empty JSON value")
|
||||
}
|
||||
switch trimmed[0] {
|
||||
{{range $alts}}{{if .IsString}} case '"':
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return nil, fmt.Errorf("{{$td.Name}}: decoding string: %w", err)
|
||||
}
|
||||
return &{{.Name}}{Text: s}, nil
|
||||
{{else}} case '[':
|
||||
var raws []json.RawMessage
|
||||
if err := json.Unmarshal(data, &raws); err != nil {
|
||||
return nil, fmt.Errorf("{{$td.Name}}: decoding array: %w", err)
|
||||
}
|
||||
items := make([]{{$td.Name}}, 0, len(raws))
|
||||
for i, r := range raws {
|
||||
v, err := Unmarshal{{$td.Name}}(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("{{$td.Name}}: decoding array[%d]: %w", i, err)
|
||||
}
|
||||
items = append(items, v)
|
||||
}
|
||||
return &{{.Name}}{Items: items}, nil
|
||||
{{end}}{{end}} case '{':
|
||||
// fall through to the discriminator switch below.
|
||||
default:
|
||||
return nil, fmt.Errorf("{{.Name}}: unrecognised JSON value starting with %q", trimmed[0])
|
||||
}
|
||||
{{end}} var probe struct {
|
||||
V string `json:"{{discriminatorField .Name}}"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &probe); err != nil {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
)
|
||||
|
||||
// altStringRE matches the scalar-string alternate clause Telegram uses
|
||||
// for union types that can also decode as plain text on the wire, e.g.
|
||||
// "a String for plain text". The trailing "for ..." clause is optional
|
||||
// and, when present, is not captured — only the shape matters. "an"
|
||||
// (rather than "a") is tolerated so a future rephrasing ("an String")
|
||||
// doesn't silently stop matching, and the word boundary keeps "Strings"
|
||||
// or "SubString" from misfiring.
|
||||
var altStringRE = regexp.MustCompile(`(?i)\ban?\s+String\b`)
|
||||
|
||||
// altArrayRE builds the pattern that matches the self-referential array
|
||||
// alternate clause Telegram uses for recursive union types, e.g. "an
|
||||
// Array of RichText". Matching is anchored to the union's own name so a
|
||||
// doc mentioning "an Array of" some other type is never mistaken for a
|
||||
// shape alternate.
|
||||
func altArrayRE(unionName string) *regexp.Regexp {
|
||||
return regexp.MustCompile(`(?i)\ban?\s+Array\s+of\s+` + regexp.QuoteMeta(unionName) + `\b`)
|
||||
}
|
||||
|
||||
// extractAlternates detects Telegram's "can be either a String for
|
||||
// plain text, an Array of <Union>, or any of the following types:"
|
||||
// phrasing on a union type's doc string and returns the non-object
|
||||
// shapes it declares, in a fixed (string, then array) order regardless
|
||||
// of how the sentence orders its clauses.
|
||||
//
|
||||
// Conservative by design: callers invoke this only for types that
|
||||
// already have OneOf populated (see typeFromSection in scrape.go), and
|
||||
// it only recognises the two shapes Telegram's docs currently use for
|
||||
// this ("a String" and "an Array of <the union's own name>") — anything
|
||||
// else in the doc is ignored, so free-text mentioning "String" or
|
||||
// "Array" elsewhere in a union's description can't produce a false
|
||||
// positive for a type this pattern wasn't written for. As of Bot API
|
||||
// v10, RichText is the only union whose doc matches either pattern.
|
||||
func extractAlternates(unionName, doc string) []spec.Alternate {
|
||||
var alts []spec.Alternate
|
||||
if altStringRE.MatchString(doc) {
|
||||
alts = append(alts, spec.Alternate{Shape: spec.AlternateString})
|
||||
}
|
||||
if altArrayRE(unionName).MatchString(doc) {
|
||||
alts = append(alts, spec.Alternate{Shape: spec.AlternateArray})
|
||||
}
|
||||
return alts
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/lukaszraczylo/go-telegram/internal/spec"
|
||||
)
|
||||
|
||||
func TestExtractAlternates_RichTextDoc(t *testing.T) {
|
||||
// Exact doc string as scraped from the live Bot API v10 docs page
|
||||
// (internal/spec/api.json / testdata/html/latest.html).
|
||||
doc := "This object represents a rich formatted text. Currently, it can be either a String for plain text, " +
|
||||
"an Array of RichText, or any of the following types:"
|
||||
got := extractAlternates("RichText", doc)
|
||||
require.Equal(t, []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_OrderIndependent(t *testing.T) {
|
||||
// Same two clauses, reversed, plus an extra clause in between —
|
||||
// order and surrounding prose must not matter.
|
||||
doc := "Currently, it can be either an Array of RichText, something else entirely, a String for plain text, " +
|
||||
"or any of the following types:"
|
||||
got := extractAlternates("RichText", doc)
|
||||
require.Equal(t, []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_ObjectOnlyUnion_NoAlternates(t *testing.T) {
|
||||
// RichBlock's doc is object-only: no String/Array alternate clause.
|
||||
doc := "This object represents a block in a rich formatted message. Currently, it can be any of the following types:"
|
||||
got := extractAlternates("RichBlock", doc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_ArrayOfDifferentType_NotSelfReferential(t *testing.T) {
|
||||
// "an Array of X" only counts when X is the union's own name — an
|
||||
// array of some other type mentioned in the doc must not match.
|
||||
doc := "This object can be either an Array of PhotoSize or any of the following types:"
|
||||
got := extractAlternates("RichText", doc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_StringOnly(t *testing.T) {
|
||||
doc := "This object can be either a String or any of the following types:"
|
||||
got := extractAlternates("Widget", doc)
|
||||
require.Equal(t, []spec.Alternate{{Shape: spec.AlternateString}}, got)
|
||||
}
|
||||
|
||||
func TestExtractAlternates_PlainTypeDoc_NoFalsePositive(t *testing.T) {
|
||||
// Ordinary field/type prose mentioning neither shape must not match.
|
||||
doc := "This object represents a chat photo."
|
||||
got := extractAlternates("ChatPhoto", doc)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
// TestTypeFromSection_RichText_RealSnapshot grounds the extraction in the
|
||||
// real pinned docs snapshot (not just a literal fixture string): scraping
|
||||
// testdata/html/latest.html end-to-end must produce a RichText TypeDecl
|
||||
// with both alternates, matching the same live text that broke decoding
|
||||
// of plain-text and array-shaped rich_message payloads.
|
||||
func TestTypeFromSection_RichText_RealSnapshot(t *testing.T) {
|
||||
htmlBytes, err := os.ReadFile("../../testdata/html/latest.html")
|
||||
require.NoError(t, err)
|
||||
|
||||
api, err := scrape(htmlBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
var richText *spec.TypeDecl
|
||||
for i := range api.Types {
|
||||
if api.Types[i].Name == "RichText" {
|
||||
richText = &api.Types[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, richText, "RichText type must be present in the real docs snapshot")
|
||||
require.Equal(t, []spec.Alternate{
|
||||
{Shape: spec.AlternateString},
|
||||
{Shape: spec.AlternateArray},
|
||||
}, richText.Alternates)
|
||||
|
||||
// RichBlock is the control: object-only union, no alternates.
|
||||
var richBlock *spec.TypeDecl
|
||||
for i := range api.Types {
|
||||
if api.Types[i].Name == "RichBlock" {
|
||||
richBlock = &api.Types[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, richBlock)
|
||||
require.Nil(t, richBlock.Alternates)
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func extractReturn(desc string) spec.TypeRef {
|
||||
return spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
|
||||
}},
|
||||
// "an array of X of the sent messages is returned" (ForwardMessages/CopyMessages shape).
|
||||
{regexp.MustCompile(`(?:[Oo]n success[,.]?\s+)?an? array of ([A-Z][A-Za-z0-9]+)(?:\s+of [^.]+?)?\s+(?:objects\s+)?(?:is|are) returned`), func(m []string) spec.TypeRef {
|
||||
{regexp.MustCompile(`(?:[Oo]n success[,.]?\s+)?an? [Aa]rray of ([A-Z][A-Za-z0-9]+)(?:\s+of [^.]+?)?\s+(?:objects\s+)?(?:is|are) returned`), func(m []string) spec.TypeRef {
|
||||
elem := primitiveOrNamed(m[1])
|
||||
return spec.TypeRef{Kind: spec.KindArray, ElemType: &elem}
|
||||
}},
|
||||
|
||||
@@ -29,6 +29,8 @@ func TestExtractReturn(t *testing.T) {
|
||||
{"On success, an array of Message objects that were sent is returned.", spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "Message"}}},
|
||||
// ForwardMessages/CopyMessages shape: "an array of X of the sent messages is returned".
|
||||
{"On success, an array of MessageId of the sent messages is returned.", spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "MessageId"}}},
|
||||
// Bot API 10.1 recapitalised the same sentence to "an Array of X".
|
||||
{"On success, an Array of MessageId of the sent messages is returned.", spec.TypeRef{Kind: spec.KindArray, ElemType: &spec.TypeRef{Kind: spec.KindNamed, Name: "MessageId"}}},
|
||||
// "Returns X on success" (no article) — OwnedGifts, StarAmount, Story, MenuButton, etc.
|
||||
{"Returns the gifts received and owned by a managed business account. Returns OwnedGifts on success.", spec.TypeRef{Kind: spec.KindNamed, Name: "OwnedGifts"}},
|
||||
{"Returns StarAmount on success.", spec.TypeRef{Kind: spec.KindNamed, Name: "StarAmount"}},
|
||||
|
||||
@@ -38,6 +38,10 @@ func typeFromSection(s section) spec.TypeDecl {
|
||||
} else if len(s.Lists) > 0 {
|
||||
// Union: extract variant names from <li><a>...</a></li>.
|
||||
td.OneOf = extractListLinks(s.Lists[0])
|
||||
// Some unions (e.g. RichText) also declare non-object wire shapes
|
||||
// ("a String for plain text", "an Array of RichText") in the same
|
||||
// sentence as the object-variant list; extract those too.
|
||||
td.Alternates = extractAlternates(td.Name, td.Doc)
|
||||
}
|
||||
return td
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Webhook decode** (small Update): ours is **15–19% faster** than every competitor and ties telego for the lowest alloc count (11).
|
||||
- **Large Update unmarshal** (entities + reply markup + photo array): ours is **17–35% faster** with the lowest ns/op of all six. telego edges us on alloc count (31 vs 34) at the cost of ~17% more time.
|
||||
- **API call round-trip** (mock HTTP server): telego wins (36.3 µs / 48 allocs) thanks to its custom binder; ours is second (38.95 µs / 104 allocs) and beats gotba, telebot, gobot.
|
||||
- **Dispatcher routing** (20 handlers, last matches): ours is **2.5× faster than telebot and gobot** (101 ns vs 269 / 252 ns).
|
||||
- **Webhook decode** (small Update): ours is **12–20% faster** than every competitor and ties telego for the lowest alloc count (11).
|
||||
- **Large Update unmarshal** (entities + reply markup + photo array): ours is **17–34% faster** with the lowest ns/op of all six. telego edges us on alloc count (31 vs 34) at the cost of ~17% more time.
|
||||
- **API call round-trip** (mock HTTP server): telego wins on allocs (35.8 µs / 48 allocs) because it uses fasthttp by default. We default to `net/http` (102 allocs / 39.8 µs); with the opt-in `client.NewFastHTTPDoer` we drop to 56 allocs / 6.6 KiB — within 8 of telego while keeping `*http.Request` semantics (RetryDoer, middleware, generated tests).
|
||||
- **Dispatcher routing** (20 handlers, last matches): ours is **2.5–2.8× faster than telebot and gobot** (98 ns vs 271 / 246 ns).
|
||||
|
||||
## How to read these numbers
|
||||
|
||||
@@ -37,12 +37,12 @@ Decode `shared.SmallUpdateJSON` into the library's typed `Update` struct.
|
||||
|
||||
| Lib | sec/op | B/op | allocs/op |
|
||||
|-----|--------|------|-----------|
|
||||
| **ours** | **1.743 µs ±3%** | 2.180 KiB | **11** |
|
||||
| gotba | 2.016 µs ±3% | 1.461 KiB | 17 |
|
||||
| telebot | 2.073 µs ±3% | 1.773 KiB | 17 |
|
||||
| gobot | 1.999 µs ±1% | 1.789 KiB | 16 |
|
||||
| telego | 2.026 µs ±2% | 3.060 KiB | **11** |
|
||||
| echotron | 1.973 µs ±0% | 1.680 KiB | 16 |
|
||||
| **ours** | **1.832 µs ±4%** | 2.180 KiB | **11** |
|
||||
| gotba | 2.082 µs ±0% | 1.461 KiB | 17 |
|
||||
| telebot | 2.194 µs ±1% | 1.773 KiB | 17 |
|
||||
| gobot | 2.082 µs ±1% | 1.789 KiB | 16 |
|
||||
| telego | 2.143 µs ±2% | 3.058 KiB | **11** |
|
||||
| echotron | 2.039 µs ±1% | 1.680 KiB | 16 |
|
||||
|
||||
**Notes.** We use slightly more bytes because typed unions and the typed `[]UpdateType` allocate richer Go values. We win on time and tie telego on alloc count.
|
||||
|
||||
@@ -52,12 +52,12 @@ Decode `shared.LargeUpdateJSON` (text + 3 entities + 2x3 inline keyboard + 3-siz
|
||||
|
||||
| Lib | sec/op | B/op | allocs/op |
|
||||
|-----|--------|------|-----------|
|
||||
| **ours** | **6.667 µs ±4%** | 5.881 KiB | 34 |
|
||||
| gotba | 8.321 µs ±2% | 3.438 KiB | 56 |
|
||||
| telebot | 10.240 µs ±4% | 5.594 KiB | 60 |
|
||||
| gobot | 8.150 µs ±2% | 4.703 KiB | 50 |
|
||||
| telego | 7.797 µs ±1% | 6.621 KiB | **31** |
|
||||
| echotron | 8.072 µs ±0% | 4.219 KiB | 56 |
|
||||
| **ours** | **6.726 µs ±1%** | 5.875 KiB | 34 |
|
||||
| gotba | 8.066 µs ±1% | 3.438 KiB | 56 |
|
||||
| telebot | 10.190 µs ±1% | 5.594 KiB | 60 |
|
||||
| gobot | 8.231 µs ±1% | 4.703 KiB | 50 |
|
||||
| telego | 7.849 µs ±2% | 6.600 KiB | **31** |
|
||||
| echotron | 8.123 µs ±1% | 4.219 KiB | 56 |
|
||||
|
||||
**Notes.** Despite the typed-union model giving us richer Go values per decode, we still produce them faster than every competitor. telego edges us by 3 allocs but pays 17% more time.
|
||||
|
||||
@@ -67,15 +67,19 @@ Build params → POST to local `httptest.Server` returning `{"ok":true,"result":
|
||||
|
||||
| Lib | sec/op | B/op | allocs/op |
|
||||
|-----|--------|------|-----------|
|
||||
| ours | 38.95 µs ±3% | 11.17 KiB | 104 |
|
||||
| gotba | 41.95 µs ±2% | 10.95 KiB | 125 |
|
||||
| telebot | 43.63 µs ±0% | 13.16 KiB | 139 |
|
||||
| gobot | 61.11 µs ±1% | 13.51 KiB | 176 |
|
||||
| **telego** | **36.31 µs ±1%** | **6.556 KiB** | **48** |
|
||||
| ours (default `net/http`) | 39.83 µs ±4% | 11.09 KiB | 102 |
|
||||
| ours (opt-in `fasthttp`) | *time TBD on quiet box* | **6.62 KiB** | **56** |
|
||||
| gotba | 42.03 µs ±4% | 10.97 KiB | 125 |
|
||||
| telebot | 43.41 µs ±1% | 13.15 KiB | 139 |
|
||||
| gobot | 61.19 µs ±1% | 13.50 KiB | 176 |
|
||||
| **telego** (uses fasthttp) | **35.84 µs ±1%** | **6.547 KiB** | **48** |
|
||||
| echotron | *skipped — see below* | — | — |
|
||||
|
||||
**Notes.**
|
||||
- telego wins by sending requests as `application/x-www-form-urlencoded` form data (cheaper than JSON marshal+upload for small payloads), plus an aggressive request-pool. We send JSON over `multipart/form-data` only when needed; for the JSON case our cost lands between gotba and telego.
|
||||
- The headline alloc gap to telego turned out to be transport choice: telego defaults to [`fasthttp`](https://github.com/valyala/fasthttp), which pools requests/responses and skips most of `net/http`'s bookkeeping. Most of the other libs (and us, by default) use `net/http`.
|
||||
- We ship an opt-in fasthttp doer (`client.NewFastHTTPDoer`). Plug it via `client.WithHTTPClient(client.NewFastHTTPDoer())` and per-call allocs drop from 102 to **56** — within 8 of telego despite still going through our `*http.Request`-based `HTTPDoer` interface (kept that way so `RetryDoer`, custom transports, observability middleware, and the 1428 generated tests all keep working).
|
||||
- The default stays `net/http` because fasthttp is HTTP/1.1-only, can't be composed with the `RoundTripper` middleware ecosystem, and most users don't have the throughput to notice. Bots making thousands of API calls/sec should opt in.
|
||||
- Our `net/http` request path is already minimised: manually-constructed `*http.Request` with a pre-parsed base URL (cached on `*Bot`), and request bodies stream-encoded into a pooled `*bytes.Buffer` via the optional `BodyEncoder` codec extension. Those skip the `url.Parse` + `*http.Request` bookkeeping that `http.NewRequestWithContext` runs on every call.
|
||||
- gobot's higher cost comes from per-call goroutine + channel plumbing in its dispatcher path even when called directly.
|
||||
- **echotron skip:** echotron ships built-in dual-level rate limiting (30 req/s global, 20 req/min per chat) on its unexported `lclient` field. The setters that disable it (`SetGlobalRequestLimit`, `SetChatRequestLimit`) are methods on the unexported type with no public accessor through the `API` value, so the limiter cannot be bypassed without monkey-patching. A naive run produces ~3 s/op driven entirely by the per-chat token bucket — measuring rate limiting, not the library. We skip rather than publish a misleading number. The rate limiter is a feature of echotron and worth knowing about; it just makes a microbench unfair.
|
||||
|
||||
@@ -85,9 +89,9 @@ Register 20 command handlers (`/cmd0` … `/cmd19`); feed an update matching `/c
|
||||
|
||||
| Lib | sec/op | B/op | allocs/op |
|
||||
|-----|--------|------|-----------|
|
||||
| **ours** | **100.7 ns ±3%** | 128 B | 3 |
|
||||
| telebot | 269.2 ns ±5% | 678 B | 5 |
|
||||
| gobot | 251.5 ns ±4% | **48 B** | **1** |
|
||||
| **ours** | **98.46 ns ±2%** | 128 B | 3 |
|
||||
| telebot | 270.9 ns ±2% | 678 B | 5 |
|
||||
| gobot | 246.1 ns ±1% | **48 B** | **1** |
|
||||
|
||||
**Notes.** We dispatch ~2.5× faster than telebot and gobot. gobot's single allocation is impressive but its routing decision is slower. telebot's higher cost reflects its richer per-update `Context` construction.
|
||||
|
||||
|
||||
+4950
-1093
File diff suppressed because it is too large
Load Diff
+117
-13
@@ -19,6 +19,7 @@ Package client provides HTTP client primitives for the Telegram Bot API.
|
||||
- [func \(e \*APIError\) IsRetryable\(\) bool](<#APIError.IsRetryable>)
|
||||
- [func \(e \*APIError\) RetryAfter\(\) time.Duration](<#APIError.RetryAfter>)
|
||||
- [func \(e \*APIError\) Unwrap\(\) error](<#APIError.Unwrap>)
|
||||
- [type BodyEncoder](<#BodyEncoder>)
|
||||
- [type Bot](<#Bot>)
|
||||
- [func New\(token string, opts ...Option\) \*Bot](<#New>)
|
||||
- [func \(b \*Bot\) BaseURL\(\) string](<#Bot.BaseURL>)
|
||||
@@ -29,7 +30,14 @@ Package client provides HTTP client primitives for the Telegram Bot API.
|
||||
- [type Codec](<#Codec>)
|
||||
- [type DefaultCodec](<#DefaultCodec>)
|
||||
- [func \(DefaultCodec\) Marshal\(v any\) \(\[\]byte, error\)](<#DefaultCodec.Marshal>)
|
||||
- [func \(DefaultCodec\) MarshalTo\(w io.Writer, v any\) error](<#DefaultCodec.MarshalTo>)
|
||||
- [func \(DefaultCodec\) Unmarshal\(data \[\]byte, v any\) error](<#DefaultCodec.Unmarshal>)
|
||||
- [type FastHTTPDoer](<#FastHTTPDoer>)
|
||||
- [func NewFastHTTPDoer\(opts ...FastHTTPDoerOption\) \*FastHTTPDoer](<#NewFastHTTPDoer>)
|
||||
- [func \(d \*FastHTTPDoer\) Do\(req \*http.Request\) \(\*http.Response, error\)](<#FastHTTPDoer.Do>)
|
||||
- [type FastHTTPDoerOption](<#FastHTTPDoerOption>)
|
||||
- [func WithFastHTTPClient\(c \*fasthttp.Client\) FastHTTPDoerOption](<#WithFastHTTPClient>)
|
||||
- [func WithFastHTTPReadTimeout\(t time.Duration\) FastHTTPDoerOption](<#WithFastHTTPReadTimeout>)
|
||||
- [type HTTPDoer](<#HTTPDoer>)
|
||||
- [type Logger](<#Logger>)
|
||||
- [type MultipartFile](<#MultipartFile>)
|
||||
@@ -79,8 +87,18 @@ var (
|
||||
)
|
||||
```
|
||||
|
||||
<a name="Version"></a>Version is a fallback version string used only when Go's build info is unavailable \(replace directives, detached \`go run\`\) or has been overridden via linker flags. The authoritative version forwarded to telemetry is resolved at runtime by \[telemetry.SendForModule\] from the build info of whatever binary linked this library, so this constant does NOT need to be bumped on every release. Exposed as a var \(not const\) for ldflag override:
|
||||
|
||||
```
|
||||
go build -ldflags="-X github.com/lukaszraczylo/go-telegram/client.Version=1.2.3"
|
||||
```
|
||||
|
||||
```go
|
||||
var Version = "0.0.0-fallback"
|
||||
```
|
||||
|
||||
<a name="Call"></a>
|
||||
## func [Call](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L53>)
|
||||
## func [Call](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L68>)
|
||||
|
||||
```go
|
||||
func Call[Req any, Resp any](ctx context.Context, b *Bot, method string, req Req) (Resp, error)
|
||||
@@ -93,7 +111,7 @@ It is generic over both request and response types. Methods with no parameters m
|
||||
Call is exported because the api package \(which lives outside this one\) invokes it from generated method wrappers. User code should not normally call it directly — use the typed wrappers in package api instead.
|
||||
|
||||
<a name="CallRaw"></a>
|
||||
## func [CallRaw](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L104>)
|
||||
## func [CallRaw](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/call.go#L119>)
|
||||
|
||||
```go
|
||||
func CallRaw[Req any](ctx context.Context, b *Bot, method string, req Req) (json.RawMessage, error)
|
||||
@@ -166,8 +184,19 @@ func (e *APIError) Unwrap() error
|
||||
|
||||
Unwrap returns the matched sentinel error, if any.
|
||||
|
||||
<a name="BodyEncoder"></a>
|
||||
## type [BodyEncoder](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L24-L26>)
|
||||
|
||||
BodyEncoder is an optional Codec extension that encodes directly into an io.Writer, skipping the intermediate \[\]byte that Marshal returns. Call uses this when present to avoid allocating the marshal result and the bytes.Reader that wraps it; codecs without it fall through to Marshal \+ bytes.NewReader.
|
||||
|
||||
```go
|
||||
type BodyEncoder interface {
|
||||
MarshalTo(w io.Writer, v any) error
|
||||
}
|
||||
```
|
||||
|
||||
<a name="Bot"></a>
|
||||
## type [Bot](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L7-L13>)
|
||||
## type [Bot](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L11-L24>)
|
||||
|
||||
Bot is the Telegram Bot API client. Construct via New. All API methods \(declared in package api\) hang off \*Bot via thin wrappers around call.
|
||||
|
||||
@@ -178,7 +207,7 @@ type Bot struct {
|
||||
```
|
||||
|
||||
<a name="New"></a>
|
||||
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L36>)
|
||||
### func [New](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L47>)
|
||||
|
||||
```go
|
||||
func New(token string, opts ...Option) *Bot
|
||||
@@ -187,7 +216,7 @@ func New(token string, opts ...Option) *Bot
|
||||
New constructs a Bot with the given token and optional configuration. The default HTTP client is tuned for long\-poll workloads \(see NewDefaultHTTPDoer\); the default codec wraps encoding/json; the default logger discards records.
|
||||
|
||||
<a name="Bot.BaseURL"></a>
|
||||
### func \(\*Bot\) [BaseURL](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L20>)
|
||||
### func \(\*Bot\) [BaseURL](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L31>)
|
||||
|
||||
```go
|
||||
func (b *Bot) BaseURL() string
|
||||
@@ -196,7 +225,7 @@ func (b *Bot) BaseURL() string
|
||||
BaseURL returns the configured Telegram API base URL.
|
||||
|
||||
<a name="Bot.Codec"></a>
|
||||
### func \(\*Bot\) [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L27>)
|
||||
### func \(\*Bot\) [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L38>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Codec() Codec
|
||||
@@ -205,7 +234,7 @@ func (b *Bot) Codec() Codec
|
||||
Codec returns the configured Codec.
|
||||
|
||||
<a name="Bot.HTTP"></a>
|
||||
### func \(\*Bot\) [HTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L24>)
|
||||
### func \(\*Bot\) [HTTP](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L35>)
|
||||
|
||||
```go
|
||||
func (b *Bot) HTTP() HTTPDoer
|
||||
@@ -214,7 +243,7 @@ func (b *Bot) HTTP() HTTPDoer
|
||||
HTTP returns the underlying HTTPDoer. Exposed for adapters that need to share connection pools or for diagnostic checks.
|
||||
|
||||
<a name="Bot.Logger"></a>
|
||||
### func \(\*Bot\) [Logger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L30>)
|
||||
### func \(\*Bot\) [Logger](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L41>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Logger() Logger
|
||||
@@ -223,7 +252,7 @@ func (b *Bot) Logger() Logger
|
||||
Logger returns the configured Logger.
|
||||
|
||||
<a name="Bot.Token"></a>
|
||||
### func \(\*Bot\) [Token](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L17>)
|
||||
### func \(\*Bot\) [Token](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/client.go#L28>)
|
||||
|
||||
```go
|
||||
func (b *Bot) Token() string
|
||||
@@ -232,7 +261,7 @@ func (b *Bot) Token() string
|
||||
Token returns the bot token. Exposed for advanced use cases \(custom transports, manual URL building\); ordinary code does not need it.
|
||||
|
||||
<a name="Codec"></a>
|
||||
## type [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L10-L13>)
|
||||
## type [Codec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L14-L17>)
|
||||
|
||||
Codec encodes/decodes JSON payloads exchanged with the Telegram Bot API. The default implementation wraps goccy/go\-json. Users may plug in bytedance/sonic or any compatible encoder by passing WithCodec to New.
|
||||
|
||||
@@ -244,7 +273,7 @@ type Codec interface {
|
||||
```
|
||||
|
||||
<a name="DefaultCodec"></a>
|
||||
## type [DefaultCodec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L16>)
|
||||
## type [DefaultCodec](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L29>)
|
||||
|
||||
DefaultCodec wraps goccy/go\-json. It is the zero\-value safe default.
|
||||
|
||||
@@ -253,7 +282,7 @@ type DefaultCodec struct{}
|
||||
```
|
||||
|
||||
<a name="DefaultCodec.Marshal"></a>
|
||||
### func \(DefaultCodec\) [Marshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L19>)
|
||||
### func \(DefaultCodec\) [Marshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L32>)
|
||||
|
||||
```go
|
||||
func (DefaultCodec) Marshal(v any) ([]byte, error)
|
||||
@@ -261,8 +290,17 @@ func (DefaultCodec) Marshal(v any) ([]byte, error)
|
||||
|
||||
Marshal calls json.Marshal.
|
||||
|
||||
<a name="DefaultCodec.MarshalTo"></a>
|
||||
### func \(DefaultCodec\) [MarshalTo](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L40>)
|
||||
|
||||
```go
|
||||
func (DefaultCodec) MarshalTo(w io.Writer, v any) error
|
||||
```
|
||||
|
||||
MarshalTo encodes v into w via goccy/go\-json's streaming encoder. The trailing newline that Encoder appends is valid JSON whitespace and is accepted by Telegram's parser.
|
||||
|
||||
<a name="DefaultCodec.Unmarshal"></a>
|
||||
### func \(DefaultCodec\) [Unmarshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L22>)
|
||||
### func \(DefaultCodec\) [Unmarshal](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/codec.go#L35>)
|
||||
|
||||
```go
|
||||
func (DefaultCodec) Unmarshal(data []byte, v any) error
|
||||
@@ -270,6 +308,72 @@ func (DefaultCodec) Unmarshal(data []byte, v any) error
|
||||
|
||||
Unmarshal calls json.Unmarshal.
|
||||
|
||||
<a name="FastHTTPDoer"></a>
|
||||
## type [FastHTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L26-L32>)
|
||||
|
||||
FastHTTPDoer is an HTTPDoer backed by github.com/valyala/fasthttp. It trades net/http compatibility \(and HTTP/2 support\) for substantially fewer allocations per request — fasthttp pools its Request and Response objects and uses a zero\-allocation HTTP/1.1 parser.
|
||||
|
||||
Use it for high\-throughput bots when GC pressure matters and you don't need HTTP/2 or any net/http\-only middleware \(RoundTripper composition, the OpenTelemetry httptrace family, etc.\):
|
||||
|
||||
```
|
||||
bot := client.New(token, client.WithHTTPClient(client.NewFastHTTPDoer()))
|
||||
```
|
||||
|
||||
Wrap with RetryDoer the same way you would the default doer.
|
||||
|
||||
```go
|
||||
type FastHTTPDoer struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
<a name="NewFastHTTPDoer"></a>
|
||||
### func [NewFastHTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L52>)
|
||||
|
||||
```go
|
||||
func NewFastHTTPDoer(opts ...FastHTTPDoerOption) *FastHTTPDoer
|
||||
```
|
||||
|
||||
NewFastHTTPDoer constructs a FastHTTPDoer with sensible defaults.
|
||||
|
||||
<a name="FastHTTPDoer.Do"></a>
|
||||
### func \(\*FastHTTPDoer\) [Do](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L75>)
|
||||
|
||||
```go
|
||||
func (d *FastHTTPDoer) Do(req *http.Request) (*http.Response, error)
|
||||
```
|
||||
|
||||
Do satisfies HTTPDoer by translating req into a pooled fasthttp.Request, dispatching it, and returning a \*http.Response whose Body releases the pooled fasthttp.Response when Close is called.
|
||||
|
||||
The conversion is intentionally minimal: URL goes via req.URL.RequestURI\(\) \+ Host \(avoids re\-parsing\), header values move byte\-for\-byte, and the body is taken straight from req.Body. \*bytes.Buffer / \*bytes.Reader are recognised so we can pass the underlying bytes without io.ReadAll.
|
||||
|
||||
<a name="FastHTTPDoerOption"></a>
|
||||
## type [FastHTTPDoerOption](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L35>)
|
||||
|
||||
FastHTTPDoerOption configures a FastHTTPDoer.
|
||||
|
||||
```go
|
||||
type FastHTTPDoerOption func(*FastHTTPDoer)
|
||||
```
|
||||
|
||||
<a name="WithFastHTTPClient"></a>
|
||||
### func [WithFastHTTPClient](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L40>)
|
||||
|
||||
```go
|
||||
func WithFastHTTPClient(c *fasthttp.Client) FastHTTPDoerOption
|
||||
```
|
||||
|
||||
WithFastHTTPClient swaps in a pre\-configured \*fasthttp.Client. Useful for sharing a connection pool across multiple bots or applying custom dial / TLS configuration.
|
||||
|
||||
<a name="WithFastHTTPReadTimeout"></a>
|
||||
### func [WithFastHTTPReadTimeout](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/fasthttp_doer.go#L47>)
|
||||
|
||||
```go
|
||||
func WithFastHTTPReadTimeout(t time.Duration) FastHTTPDoerOption
|
||||
```
|
||||
|
||||
WithFastHTTPReadTimeout sets the per\-request fallback timeout used when the inbound context has no deadline. Long\-poll callers should pass a value larger than the long\-poll timeout.
|
||||
|
||||
<a name="HTTPDoer"></a>
|
||||
## type [HTTPDoer](<https://github.com/lukaszraczylo/go-telegram/blob/main/client/httpclient.go#L13-L15>)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ func handleEcho(c *dispatch.Context, m *api.Message) error {
|
||||
_, err := api.SendMessage(c.Ctx, c.Bot, &api.SendMessageParams{
|
||||
ChatID: api.ChatIDFromInt(m.Chat.ID),
|
||||
Text: m.Text,
|
||||
ReplyParameters: &api.ReplyParameters{MessageID: m.MessageID},
|
||||
ReplyParameters: &api.ReplyParameters{MessageID: &m.MessageID},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,13 +4,18 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/net v0.54.0
|
||||
github.com/valyala/fasthttp v1.71.0
|
||||
golang.org/x/net v0.56.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.1 h1:6ULyfzXplpdmIY/i01OPM1jeod9+L1RAhI0jtbVnJI0=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.1/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.3 h1:xoDtBqeZGmXj7IteiE1M5WMuzeoqag58qEleI0Cf2Ms=
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.3/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
|
||||
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
+4968
-293
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,31 @@ type TypeDecl struct {
|
||||
// OneOf, when non-empty, indicates this type is a union and lists the concrete variant type names.
|
||||
// Variants are emitted as concrete structs implementing a sealed interface.
|
||||
OneOf []string `json:"one_of,omitempty"`
|
||||
// Alternates lists non-object wire shapes this union may also decode
|
||||
// as, in addition to the object variants in OneOf. Only populated for
|
||||
// unions whose doc declares them (Telegram's "can be either a String
|
||||
// for plain text, an Array of X, or any of the following types:"
|
||||
// phrasing) — as of Bot API v10 that's RichText alone, so every other
|
||||
// type's serialised IR is unaffected by this field's existence.
|
||||
Alternates []Alternate `json:"alternates,omitempty"`
|
||||
}
|
||||
|
||||
// AlternateShape enumerates the non-object wire shapes a union's
|
||||
// Alternates field records.
|
||||
type AlternateShape string
|
||||
|
||||
const (
|
||||
// AlternateString: the union may decode from a bare JSON string.
|
||||
AlternateString AlternateShape = "string"
|
||||
// AlternateArray: the union may decode from a JSON array whose
|
||||
// elements are themselves the same union type.
|
||||
AlternateArray AlternateShape = "array"
|
||||
)
|
||||
|
||||
// Alternate records one non-object shape a union type may appear as on
|
||||
// the wire, beyond its OneOf object variants.
|
||||
type Alternate struct {
|
||||
Shape AlternateShape `json:"shape"`
|
||||
}
|
||||
|
||||
// MethodDecl describes a Telegram API method.
|
||||
|
||||
@@ -22,6 +22,12 @@ type Overrides struct {
|
||||
// ApprovedBoolMethods lists methods whose returns are genuinely bool.
|
||||
// The audit tool ignores these.
|
||||
ApprovedBoolMethods []string `json:"approved_bool_methods,omitempty"`
|
||||
|
||||
// ApprovedAnyLocations lists IR locations whose union renders as `any` by
|
||||
// design, keyed "<TypeName>.<FieldName>" for struct fields,
|
||||
// "<methodName>.<ParamName>" for params and "<methodName>" for returns.
|
||||
// The audit tool ignores these.
|
||||
ApprovedAnyLocations []string `json:"approved_any_locations,omitempty"`
|
||||
}
|
||||
|
||||
// LoadOverrides reads and parses overrides.json. Returns an empty Overrides
|
||||
@@ -73,3 +79,17 @@ func (o *Overrides) IsBoolApproved(methodName string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsAnyApproved reports whether location is on the approved `any` list.
|
||||
// See ApprovedAnyLocations for the key format.
|
||||
func (o *Overrides) IsAnyApproved(location string) bool {
|
||||
if o == nil {
|
||||
return false
|
||||
}
|
||||
for _, n := range o.ApprovedAnyLocations {
|
||||
if n == location {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"method_returns": {},
|
||||
"field_types": {},
|
||||
"approved_any_locations": [
|
||||
"InputRichMessageMedia.Media"
|
||||
],
|
||||
"approved_bool_methods": [
|
||||
"setWebhook",
|
||||
"deleteWebhook",
|
||||
|
||||
@@ -28,7 +28,8 @@ import (
|
||||
// Telegram-format token (digits:[\w-]{35}). telego enforces this format on construction.
|
||||
const benchToken = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ_ab123456"
|
||||
|
||||
// BenchmarkCall_ours — lukaszraczylo/go-telegram.
|
||||
// BenchmarkCall_ours — lukaszraczylo/go-telegram with default net/http
|
||||
// transport. Most users land here.
|
||||
func BenchmarkCall_ours(b *testing.B) {
|
||||
srv := shared.NewMockServer()
|
||||
defer srv.Close()
|
||||
@@ -47,6 +48,30 @@ func BenchmarkCall_ours(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkCall_ours_fasthttp — lukaszraczylo/go-telegram with the
|
||||
// opt-in fasthttp transport (client.NewFastHTTPDoer). Apples-to-apples
|
||||
// against telego, which also runs on fasthttp by default.
|
||||
func BenchmarkCall_ours_fasthttp(b *testing.B) {
|
||||
srv := shared.NewMockServer()
|
||||
defer srv.Close()
|
||||
bot := client.New(benchToken,
|
||||
client.WithBaseURL(srv.URL),
|
||||
client.WithHTTPClient(client.NewFastHTTPDoer()),
|
||||
)
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_, err := api.SendMessage(ctx, bot, &api.SendMessageParams{
|
||||
ChatID: api.ChatIDFromInt(42),
|
||||
Text: "hello",
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkCall_gotba — go-telegram-bot-api/telegram-bot-api/v5.
|
||||
func BenchmarkCall_gotba(b *testing.B) {
|
||||
srv := shared.NewMockServer()
|
||||
|
||||
@@ -14,20 +14,20 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/grbit/go-json v0.11.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
github.com/valyala/fasthttp v1.71.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.10 // indirect
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
)
|
||||
|
||||
@@ -65,8 +65,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
@@ -275,8 +275,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -392,8 +392,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
|
||||
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
|
||||
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
@@ -624,8 +624,8 @@ golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -4,55 +4,55 @@ pkg: github.com/lukaszraczylo/go-telegram/test/benchmarks
|
||||
cpu: Apple M4 Max
|
||||
│ /Users/nvm/Documents/projects/private/go-telegram/test/benchmarks/results/raw.txt │
|
||||
│ sec/op │
|
||||
Call_ours-16 38.95µ ± 3%
|
||||
Call_gotba-16 41.95µ ± 2%
|
||||
Call_telebot-16 43.63µ ± 0%
|
||||
Call_gobot-16 61.11µ ± 1%
|
||||
Call_telego-16 36.31µ ± 1%
|
||||
Dispatch_ours-16 100.7n ± 3%
|
||||
Dispatch_telebot-16 269.2n ± 5%
|
||||
Dispatch_gobot-16 251.5n ± 4%
|
||||
LargeUnmarshal_ours-16 6.667µ ± 4%
|
||||
LargeUnmarshal_gotba-16 8.321µ ± 2%
|
||||
LargeUnmarshal_telebot-16 10.24µ ± 4%
|
||||
LargeUnmarshal_gobot-16 8.150µ ± 2%
|
||||
LargeUnmarshal_telego-16 7.797µ ± 1%
|
||||
LargeUnmarshal_echotron-16 8.072µ ± 0%
|
||||
Webhook_ours-16 1.743µ ± 3%
|
||||
Webhook_gotba-16 2.016µ ± 3%
|
||||
Webhook_telebot-16 2.073µ ± 3%
|
||||
Webhook_gobot-16 1.999µ ± 1%
|
||||
Webhook_telego-16 2.026µ ± 2%
|
||||
Webhook_echotron-16 1.973µ ± 0%
|
||||
geomean 4.603µ
|
||||
Call_ours-16 39.83µ ± 4%
|
||||
Call_gotba-16 42.03µ ± 4%
|
||||
Call_telebot-16 43.41µ ± 1%
|
||||
Call_gobot-16 61.19µ ± 1%
|
||||
Call_telego-16 35.84µ ± 1%
|
||||
Dispatch_ours-16 98.46n ± 2%
|
||||
Dispatch_telebot-16 270.9n ± 2%
|
||||
Dispatch_gobot-16 246.1n ± 1%
|
||||
LargeUnmarshal_ours-16 6.726µ ± 1%
|
||||
LargeUnmarshal_gotba-16 8.066µ ± 1%
|
||||
LargeUnmarshal_telebot-16 10.19µ ± 1%
|
||||
LargeUnmarshal_gobot-16 8.231µ ± 1%
|
||||
LargeUnmarshal_telego-16 7.849µ ± 2%
|
||||
LargeUnmarshal_echotron-16 8.123µ ± 1%
|
||||
Webhook_ours-16 1.832µ ± 4%
|
||||
Webhook_gotba-16 2.082µ ± 0%
|
||||
Webhook_telebot-16 2.194µ ± 1%
|
||||
Webhook_gobot-16 2.082µ ± 1%
|
||||
Webhook_telego-16 2.143µ ± 2%
|
||||
Webhook_echotron-16 2.039µ ± 1%
|
||||
geomean 4.658µ
|
||||
|
||||
│ /Users/nvm/Documents/projects/private/go-telegram/test/benchmarks/results/raw.txt │
|
||||
│ B/op │
|
||||
Call_ours-16 11.17Ki ± 0%
|
||||
Call_gotba-16 10.95Ki ± 0%
|
||||
Call_telebot-16 13.16Ki ± 0%
|
||||
Call_gobot-16 13.51Ki ± 0%
|
||||
Call_telego-16 6.556Ki ± 0%
|
||||
Call_ours-16 11.09Ki ± 1%
|
||||
Call_gotba-16 10.97Ki ± 0%
|
||||
Call_telebot-16 13.15Ki ± 0%
|
||||
Call_gobot-16 13.50Ki ± 0%
|
||||
Call_telego-16 6.547Ki ± 0%
|
||||
Dispatch_ours-16 128.0 ± 0%
|
||||
Dispatch_telebot-16 678.0 ± 0%
|
||||
Dispatch_telebot-16 678.5 ± 0%
|
||||
Dispatch_gobot-16 48.00 ± 0%
|
||||
LargeUnmarshal_ours-16 5.881Ki ± 0%
|
||||
LargeUnmarshal_ours-16 5.875Ki ± 0%
|
||||
LargeUnmarshal_gotba-16 3.438Ki ± 0%
|
||||
LargeUnmarshal_telebot-16 5.594Ki ± 0%
|
||||
LargeUnmarshal_gobot-16 4.703Ki ± 0%
|
||||
LargeUnmarshal_telego-16 6.621Ki ± 0%
|
||||
LargeUnmarshal_telego-16 6.600Ki ± 0%
|
||||
LargeUnmarshal_echotron-16 4.219Ki ± 0%
|
||||
Webhook_ours-16 2.180Ki ± 0%
|
||||
Webhook_gotba-16 1.461Ki ± 0%
|
||||
Webhook_telebot-16 1.773Ki ± 0%
|
||||
Webhook_gobot-16 1.789Ki ± 0%
|
||||
Webhook_telego-16 3.060Ki ± 0%
|
||||
Webhook_telego-16 3.058Ki ± 0%
|
||||
Webhook_echotron-16 1.680Ki ± 0%
|
||||
geomean 2.701Ki
|
||||
geomean 2.699Ki
|
||||
|
||||
│ /Users/nvm/Documents/projects/private/go-telegram/test/benchmarks/results/raw.txt │
|
||||
│ allocs/op │
|
||||
Call_ours-16 104.0 ± 0%
|
||||
Call_ours-16 102.0 ± 0%
|
||||
Call_gotba-16 125.0 ± 0%
|
||||
Call_telebot-16 139.0 ± 0%
|
||||
Call_gobot-16 176.0 ± 0%
|
||||
@@ -72,4 +72,4 @@ Webhook_telebot-16
|
||||
Webhook_gobot-16 16.00 ± 0%
|
||||
Webhook_telego-16 11.00 ± 0%
|
||||
Webhook_echotron-16 16.00 ± 0%
|
||||
geomean 26.03
|
||||
geomean 26.00
|
||||
|
||||
+201
-202
@@ -2,206 +2,205 @@ goos: darwin
|
||||
goarch: arm64
|
||||
pkg: github.com/lukaszraczylo/go-telegram/test/benchmarks
|
||||
cpu: Apple M4 Max
|
||||
BenchmarkCall_ours-16 30754 38732 ns/op 11589 B/op 105 allocs/op
|
||||
BenchmarkCall_ours-16 31054 39002 ns/op 11476 B/op 104 allocs/op
|
||||
BenchmarkCall_ours-16 30990 38440 ns/op 11471 B/op 104 allocs/op
|
||||
BenchmarkCall_ours-16 30301 39464 ns/op 11452 B/op 104 allocs/op
|
||||
BenchmarkCall_ours-16 30438 38897 ns/op 11425 B/op 104 allocs/op
|
||||
BenchmarkCall_ours-16 30783 39107 ns/op 11429 B/op 104 allocs/op
|
||||
BenchmarkCall_ours-16 30486 39507 ns/op 11402 B/op 104 allocs/op
|
||||
BenchmarkCall_ours-16 30045 37723 ns/op 11442 B/op 104 allocs/op
|
||||
BenchmarkCall_ours-16 37867 33103 ns/op 11444 B/op 104 allocs/op
|
||||
BenchmarkCall_ours-16 30522 39139 ns/op 11436 B/op 104 allocs/op
|
||||
BenchmarkCall_gotba-16 29838 40947 ns/op 11243 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28545 41897 ns/op 11182 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28713 41146 ns/op 11197 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28480 42210 ns/op 11238 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28831 42004 ns/op 11204 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28484 42012 ns/op 11224 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 30481 41283 ns/op 11212 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28646 42042 ns/op 11209 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28418 40680 ns/op 11250 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28358 42146 ns/op 11208 B/op 125 allocs/op
|
||||
BenchmarkCall_telebot-16 27294 43739 ns/op 13522 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27763 43429 ns/op 13491 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27525 43618 ns/op 13478 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27423 43711 ns/op 13431 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27415 43704 ns/op 13473 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27268 43834 ns/op 13477 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 28180 43488 ns/op 13486 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27480 43644 ns/op 13485 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27458 43581 ns/op 13479 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27949 43415 ns/op 13480 B/op 139 allocs/op
|
||||
BenchmarkCall_gobot-16 19503 60924 ns/op 13847 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19620 61253 ns/op 13837 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19790 60869 ns/op 13839 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19574 61153 ns/op 13816 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19634 61070 ns/op 13830 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19569 61173 ns/op 13817 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19549 61688 ns/op 13851 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19918 60815 ns/op 13779 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19440 61667 ns/op 13830 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19660 61036 ns/op 13837 B/op 176 allocs/op
|
||||
BenchmarkCall_telego-16 32732 36606 ns/op 6788 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33837 35882 ns/op 6697 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33074 36146 ns/op 6693 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33400 36090 ns/op 6723 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 32684 36296 ns/op 6709 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 32926 36577 ns/op 6721 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33928 35481 ns/op 6708 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33195 36318 ns/op 6711 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33140 36812 ns/op 6717 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 32343 37631 ns/op 6716 B/op 48 allocs/op
|
||||
BenchmarkDispatch_ours-16 12199088 98.57 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 12223122 97.66 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 12088142 97.43 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 12463010 100.9 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 12284848 99.38 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 11485198 101.1 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 11685897 102.2 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 11733669 102.1 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 11811807 100.5 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 11691974 103.1 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4270724 284.6 ns/op 679 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4369950 270.2 ns/op 678 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4604205 269.9 ns/op 679 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4431572 282.2 ns/op 678 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4186550 272.0 ns/op 678 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4472223 265.7 ns/op 679 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4537406 265.0 ns/op 679 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4544478 264.4 ns/op 678 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4519431 266.7 ns/op 678 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4448779 268.5 ns/op 678 B/op 5 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4493029 254.1 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4726080 261.5 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4884592 249.6 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4639986 256.7 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4424702 261.1 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4783779 249.2 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4916862 248.2 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4884650 249.9 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4822939 252.4 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4707606 250.5 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 177442 6697 ns/op 6024 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 178348 6700 ns/op 6021 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 180528 6686 ns/op 6022 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 182416 6659 ns/op 6021 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 179305 6675 ns/op 6022 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 176892 6724 ns/op 6021 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 185355 6392 ns/op 6022 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 189590 6390 ns/op 6022 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 189783 6438 ns/op 6022 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 187572 6483 ns/op 6022 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 142377 8296 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 144884 8274 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 146434 8295 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 145183 8260 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 144775 8416 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 139510 8224 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 146359 8542 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 144981 8346 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 146877 8399 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 145443 8454 ns/op 3519 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 116883 10327 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 114584 10333 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 117277 10250 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 117700 10270 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 117328 10401 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 115922 10223 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 117475 10049 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 123367 9866 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 123385 9811 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 122586 9873 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 148576 8064 ns/op 4817 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 147967 8113 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 150718 7991 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 150271 8066 ns/op 4815 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 147646 8066 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 147889 8186 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 143164 8413 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 150464 8276 ns/op 4815 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 145249 8201 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 146100 8216 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 149001 7802 ns/op 6781 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 152780 7835 ns/op 6775 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 156508 7817 ns/op 6780 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 154938 7816 ns/op 6777 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 150121 7809 ns/op 6778 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 152810 7791 ns/op 6780 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 159613 7784 ns/op 6778 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 154402 7729 ns/op 6780 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 157184 7660 ns/op 6782 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 155822 7768 ns/op 6780 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 144252 8147 ns/op 4323 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 147961 8089 ns/op 4319 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 149847 8049 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 149128 8069 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 147277 8075 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 149132 8089 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 148837 8014 ns/op 4319 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 149288 8050 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 146833 8097 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 149233 8041 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkWebhook_ours-16 707102 1721 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 700047 1734 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 725071 1721 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 675003 1751 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 693903 1787 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 714036 1751 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 646494 1816 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 696355 1801 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 709545 1734 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 709700 1725 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_gotba-16 611132 2027 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 624046 2072 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 588478 2093 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 609744 2012 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 606912 2012 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 612211 2000 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 605488 2015 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 622398 2006 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 610478 2017 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 610350 2030 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 559515 2138 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 558510 2057 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 587257 2079 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 585321 2059 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 576236 2067 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 570172 2070 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 568270 2131 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 567567 2094 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 586588 2076 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 574495 2062 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gobot-16 597710 2006 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 594742 2016 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 603187 1998 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 608301 2011 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 605532 1984 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 609892 1990 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 596637 1999 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 592108 1993 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 607069 1999 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 594915 1997 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_telego-16 603300 2021 ns/op 3133 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 602503 2016 ns/op 3133 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 618267 2016 ns/op 3133 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 601855 2027 ns/op 3133 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 612424 2035 ns/op 3132 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 595890 2074 ns/op 3133 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 605565 2001 ns/op 3133 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 610453 2096 ns/op 3133 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 588056 2069 ns/op 3132 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 595764 2025 ns/op 3133 B/op 11 allocs/op
|
||||
BenchmarkWebhook_echotron-16 599088 1975 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 626964 1975 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 635306 1967 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 606621 1965 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 625998 1965 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 646352 1976 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 634186 1971 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 628503 1982 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 612586 1975 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 622320 1965 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkCall_ours-16 27756 39971 ns/op 11497 B/op 103 allocs/op
|
||||
BenchmarkCall_ours-16 30495 39111 ns/op 11329 B/op 102 allocs/op
|
||||
BenchmarkCall_ours-16 29250 41427 ns/op 11356 B/op 102 allocs/op
|
||||
BenchmarkCall_ours-16 29766 39680 ns/op 11366 B/op 102 allocs/op
|
||||
BenchmarkCall_ours-16 29157 40066 ns/op 11338 B/op 102 allocs/op
|
||||
BenchmarkCall_ours-16 30567 38404 ns/op 11276 B/op 102 allocs/op
|
||||
BenchmarkCall_ours-16 30470 38923 ns/op 11306 B/op 102 allocs/op
|
||||
BenchmarkCall_ours-16 30520 40212 ns/op 11364 B/op 102 allocs/op
|
||||
BenchmarkCall_ours-16 30315 39595 ns/op 11361 B/op 102 allocs/op
|
||||
BenchmarkCall_ours-16 28747 41549 ns/op 11434 B/op 102 allocs/op
|
||||
BenchmarkCall_gotba-16 28140 43735 ns/op 11255 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 27189 43528 ns/op 11247 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 27940 43644 ns/op 11259 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 29090 41643 ns/op 11232 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28002 42461 ns/op 11183 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28578 42082 ns/op 11204 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28549 41973 ns/op 11237 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 29086 41702 ns/op 11203 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 29630 41783 ns/op 11262 B/op 125 allocs/op
|
||||
BenchmarkCall_gotba-16 28371 41810 ns/op 11217 B/op 125 allocs/op
|
||||
BenchmarkCall_telebot-16 27510 43416 ns/op 13457 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 28102 43319 ns/op 13473 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27558 43530 ns/op 13417 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27274 43654 ns/op 13445 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27627 43530 ns/op 13489 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27499 42836 ns/op 13467 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27860 43375 ns/op 13457 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27711 43400 ns/op 13439 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 27668 43472 ns/op 13482 B/op 139 allocs/op
|
||||
BenchmarkCall_telebot-16 28063 43182 ns/op 13487 B/op 139 allocs/op
|
||||
BenchmarkCall_gobot-16 19645 60805 ns/op 13879 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19562 61374 ns/op 13823 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19575 60944 ns/op 13823 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19538 61461 ns/op 13844 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19624 61253 ns/op 13806 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19617 61127 ns/op 13824 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19516 61568 ns/op 13775 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19514 61340 ns/op 13828 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 19392 60426 ns/op 13863 B/op 176 allocs/op
|
||||
BenchmarkCall_gobot-16 23968 49951 ns/op 13844 B/op 176 allocs/op
|
||||
BenchmarkCall_telego-16 33622 35493 ns/op 6780 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33874 35438 ns/op 6703 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 34560 35482 ns/op 6704 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33298 35830 ns/op 6711 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33205 35946 ns/op 6706 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33428 35949 ns/op 6707 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33452 35974 ns/op 6692 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33056 35853 ns/op 6705 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33120 35808 ns/op 6703 B/op 48 allocs/op
|
||||
BenchmarkCall_telego-16 33450 38996 ns/op 6699 B/op 48 allocs/op
|
||||
BenchmarkDispatch_ours-16 12381547 96.05 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 12636062 99.34 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 12161170 98.43 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 12205023 97.90 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 12590581 98.83 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 12033376 99.15 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 12049588 98.48 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 12324108 98.38 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 11924947 96.71 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_ours-16 11940064 99.26 ns/op 128 B/op 3 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4456072 262.5 ns/op 678 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4330234 275.3 ns/op 678 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4478779 268.7 ns/op 679 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4394821 282.5 ns/op 678 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4376773 271.6 ns/op 679 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4516370 268.7 ns/op 678 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4465942 276.0 ns/op 678 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4399328 270.1 ns/op 679 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4531597 268.3 ns/op 679 B/op 5 allocs/op
|
||||
BenchmarkDispatch_telebot-16 4376616 272.3 ns/op 679 B/op 5 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4911369 249.4 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4896456 246.3 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4789376 246.4 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4949206 247.7 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4902912 243.1 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4913300 244.7 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4925991 245.5 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4817457 245.9 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4943328 245.8 ns/op 49 B/op 1 allocs/op
|
||||
BenchmarkDispatch_gobot-16 4751266 248.1 ns/op 48 B/op 1 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 180361 6682 ns/op 6019 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 178388 6765 ns/op 6016 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 182600 6701 ns/op 6016 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 178785 6710 ns/op 6016 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 181588 6726 ns/op 6016 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 177378 6730 ns/op 6016 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 181004 6729 ns/op 6016 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 176672 6682 ns/op 6016 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 184182 6726 ns/op 6016 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_ours-16 179983 6813 ns/op 6016 B/op 34 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 138108 8579 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 148593 8143 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 147964 8075 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 147601 8161 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 148257 8020 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 150858 8058 ns/op 3519 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 149251 8040 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 151614 8054 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 152306 8050 ns/op 3519 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_gotba-16 152979 8094 ns/op 3520 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 119103 10113 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 115418 10247 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 116160 10260 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 117031 10346 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 116731 10311 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 121227 10135 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 119989 10178 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 119311 10194 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 119388 10183 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_telebot-16 120195 10133 ns/op 5728 B/op 60 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 146700 8235 ns/op 4817 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 147666 8230 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 148058 8212 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 148092 8210 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 146656 8208 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 148036 8259 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 146211 8287 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 146793 8279 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 146083 8232 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_gobot-16 147385 8221 ns/op 4816 B/op 50 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 150852 8155 ns/op 6762 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 152559 8040 ns/op 6758 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 146462 7989 ns/op 6757 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 154480 7842 ns/op 6759 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 155208 7811 ns/op 6759 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 149310 7848 ns/op 6759 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 156939 7835 ns/op 6757 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 154064 7867 ns/op 6759 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 153975 7849 ns/op 6758 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_telego-16 152520 7811 ns/op 6758 B/op 31 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 145399 8167 ns/op 4323 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 147351 8124 ns/op 4319 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 148053 8094 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 147170 8124 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 147792 8092 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 145971 8105 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 148708 8122 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 147525 8105 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 148635 8165 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkLargeUnmarshal_echotron-16 150871 8124 ns/op 4320 B/op 56 allocs/op
|
||||
BenchmarkWebhook_ours-16 705109 1911 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 651456 1911 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 683557 1875 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 668317 1830 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 703718 1833 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 707150 1844 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 700472 1810 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 699192 1828 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 671794 1828 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_ours-16 687226 1826 ns/op 2232 B/op 11 allocs/op
|
||||
BenchmarkWebhook_gotba-16 544628 2080 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 565494 2081 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 579736 2081 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 581365 2082 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 580501 2075 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 582160 2091 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 563130 2089 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 573872 2088 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 569616 2093 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gotba-16 599923 2069 ns/op 1496 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 562394 2162 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 562137 2272 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 488323 2220 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 538561 2199 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 581037 2187 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 540055 2213 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 546921 2203 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 574988 2188 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 588076 2154 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_telebot-16 571617 2181 ns/op 1816 B/op 17 allocs/op
|
||||
BenchmarkWebhook_gobot-16 588786 2080 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 551653 2080 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 590686 2084 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 588870 2096 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 606735 2064 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 597108 2084 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 600633 2069 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 589102 2110 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 583528 2104 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_gobot-16 599022 2073 ns/op 1832 B/op 16 allocs/op
|
||||
BenchmarkWebhook_telego-16 587408 2193 ns/op 3131 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 601533 2152 ns/op 3131 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 584689 2127 ns/op 3131 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 576732 2153 ns/op 3131 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 568095 2143 ns/op 3131 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 553896 2132 ns/op 3131 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 579055 2130 ns/op 3131 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 595776 2144 ns/op 3131 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 573843 2134 ns/op 3131 B/op 11 allocs/op
|
||||
BenchmarkWebhook_telego-16 513824 2267 ns/op 3131 B/op 11 allocs/op
|
||||
BenchmarkWebhook_echotron-16 587734 2178 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 557188 2039 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 600524 2020 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 602959 2019 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 588694 2048 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 602366 2039 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 604621 2031 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 599146 2037 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 586056 2051 ns/op 1720 B/op 16 allocs/op
|
||||
BenchmarkWebhook_echotron-16 598456 2049 ns/op 1720 B/op 16 allocs/op
|
||||
PASS
|
||||
ok github.com/lukaszraczylo/go-telegram/test/benchmarks 241.963s
|
||||
? github.com/lukaszraczylo/go-telegram/test/benchmarks/shared [no test files]
|
||||
ok github.com/lukaszraczylo/go-telegram/test/benchmarks 242.858s
|
||||
|
||||
Vendored
+5
-3
@@ -7,14 +7,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/goccy/go-json"
|
||||
"io"
|
||||
)
|
||||
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = io.Discard // keep import even if no fields use io
|
||||
var _ = json.Marshal // keep import for UnmarshalXxx helpers
|
||||
var _ = fmt.Errorf // keep import for UnmarshalXxx helpers
|
||||
var _ = bytes.TrimLeft // keep import for shape-dispatching UnmarshalXxx helpers (unions with alternates)
|
||||
|
||||
// This object represents a Telegram user or bot.
|
||||
type User struct {
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
snapshot_2026-05-08.html
|
||||
snapshot_2026-09-14.html
|
||||
+19872
File diff suppressed because one or more lines are too long
+19872
File diff suppressed because one or more lines are too long
+21862
File diff suppressed because one or more lines are too long
+23114
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23115
File diff suppressed because one or more lines are too long
+23589
File diff suppressed because one or more lines are too long
+23589
File diff suppressed because one or more lines are too long
+23589
File diff suppressed because one or more lines are too long
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Exports BOT_API_VERSION for the .goreleaser.yaml release header.
|
||||
set -euo pipefail
|
||||
|
||||
[ -n "${GITHUB_ENV:-}" ] || exit 0
|
||||
|
||||
ver=$(python3 -c 'import json; print(json.load(open("internal/spec/api.json"))["version"])')
|
||||
if [ -z "$ver" ] || [ "$ver" = "null" ]; then
|
||||
exit 0
|
||||
fi
|
||||
echo "BOT_API_VERSION=${ver}" >> "$GITHUB_ENV"
|
||||
Reference in New Issue
Block a user