From 64908017f8c438b72e626742589eb48b134cc54a Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Sun, 9 Aug 2026 23:43:45 +0900 Subject: [PATCH 1/5] docs: publish the v1.0 compatibility policy, and lock the surfaces it promises MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The only stability policy in the repo guaranteed `ServicePlugin`, `PluginConfig`, `Response`, `Resource` and the `ProtocolType` constants across v1.x. All five live in `internal/plugin`, which Go forbids importing from another module — so the sole documented v1.0 guarantee applied to nobody. docs/compatibility-policy.md states the promise for the surfaces a user is actually exposed to: config keys, environment variables, the CLI, admin API response keys, fidelity tier names, and wire behaviour. The wire guarantee is scoped to hand-verified operations covered by tests/compatibility/ — that suite, 775 tests run in CI and again against the tagged commit, is the enforcement rather than prose. Everything else is listed as explicitly not guaranteed, including auto-crud response content and all of `internal/`, which keeps the planned IR and ModelSource refactor unblocked. Two surface-lock tests keep the document honest. The admin ones matter most: the existing tests decode responses into the internal structs, so renaming a JSON tag renames both sides of the assertion and they stay green while every consumer breaks. Decoding into map[string]any and asserting key presence catches it — verified by renaming `resourceCount` and watching only the new test fail. Asserting presence rather than payload equality keeps the additive change the policy permits from failing the build. Also scopes the plugin-api.md guarantee to in-tree contributors, cross-links the policy from the release pre-flight checklist, and corrects the docs index, which still claimed 101 services. --- README.md | 4 +- changes/unreleased/Added-20260809-230000.yaml | 5 + docs/README.md | 9 +- docs/compatibility-policy.md | 131 ++++++++++++++++++ docs/plugin-api.md | 6 + docs/release.md | 6 +- internal/admin/api_test.go | 83 +++++++++++ internal/config/config_test.go | 72 ++++++++++ 8 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 changes/unreleased/Added-20260809-230000.yaml create mode 100644 docs/compatibility-policy.md diff --git a/README.md b/README.md index a6643fc..68051d9 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,9 @@ Start at the [docs index](docs/) for the full map. Quick links: - [Configuration](docs/configuration.md) — Config options, env-var overrides, tier shortcuts - [Architecture](docs/architecture.md) — System design, codegen pipeline, plugin model, multi-CSP vision - [Services Matrix](docs/services-matrix.md) — services, coverage status, boto3 compatibility -- [Plugin API](docs/plugin-api.md) — the stable `ServicePlugin` contract and v1.x stability policy +- [Compatibility Policy](docs/compatibility-policy.md) — what v1.0 guarantees across 1.x, and what it explicitly does not +- [Fidelity Manifest](docs/fidelity-manifest.md) — per-operation tiers: how much to trust any given call +- [Plugin API](docs/plugin-api.md) — the in-tree `ServicePlugin` contract for contributors - [Roadmap](docs/roadmap.md) — Phased plan toward multi-CSP support - [FAQ](docs/faq.md) / [Troubleshooting](docs/troubleshooting.md) — Common questions and errors - [Contributing](docs/contributing.md) — Development setup, adding new services diff --git a/changes/unreleased/Added-20260809-230000.yaml b/changes/unreleased/Added-20260809-230000.yaml new file mode 100644 index 0000000..afde9fc --- /dev/null +++ b/changes/unreleased/Added-20260809-230000.yaml @@ -0,0 +1,5 @@ +kind: Added +body: A published compatibility policy (docs/compatibility-policy.md) stating what v1.0 guarantees across the 1.x line — config keys, environment variables, the CLI, admin API response keys, fidelity tier names, and the response shape of hand-verified operations covered by the boto3 compatibility suite — what it explicitly does not guarantee, and the deprecation procedure that precedes any removal +time: 2026-08-09T23:00:00.000000+09:00 +custom: + Issue: "129" diff --git a/docs/README.md b/docs/README.md index 98f0dad..9a723a1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,14 @@ This directory holds DevCloud's technical documentation. Use the map below to ju - **[Configuration](configuration.md)** — YAML options, env-var overrides, tier shortcuts - **[Architecture](architecture.md)** — system design, plugin model, codegen pipeline, multi-CSP vision - **[Roadmap](roadmap.md)** — phased plan toward multi-CSP support -- **[Services Matrix](services-matrix.md)** — 101 services, coverage status, boto3 pass rate +- **[Services Matrix](services-matrix.md)** — 104 services, coverage status, boto3 pass rate + +## What you can rely on + +- **[Compatibility Policy](compatibility-policy.md)** — what v1.0 guarantees across 1.x, what it explicitly does not, and how deprecation works +- **[Fidelity Manifest](fidelity-manifest.md)** — per-operation tiers: how much to trust any given call +- **[CRUD Engine](crud-engine.md)** — how engine-served operations behave, and where they stop +- **[Plugin API](plugin-api.md)** — the in-tree `ServicePlugin` contract for contributors ## Per-service references diff --git a/docs/compatibility-policy.md b/docs/compatibility-policy.md new file mode 100644 index 0000000..c59464b --- /dev/null +++ b/docs/compatibility-policy.md @@ -0,0 +1,131 @@ +# Compatibility Policy + +What DevCloud **v1.0** promises, and what it deliberately does not. + +This document covers the surfaces you touch as a *user* of DevCloud — the config file, the +environment, the CLI, the admin API, and the AWS wire protocol. For the in-tree Go contract +that service implementations are written against, see +[plugin-api.md](plugin-api.md#api-stability); that surface lives under `internal/` and is not +importable from another module. + +Versions follow [Semantic Versioning](https://semver.org). "Across 1.x" below means every +release from v1.0.0 up to but not including v2.0.0. + +## Guaranteed across 1.x + +### Configuration file + +These keys keep their name, type and meaning. New keys may be added; existing ones are not +removed or repurposed. Defined in [`internal/config/config.go`](../internal/config/config.go). + +| Key | Type | Meaning | +|---|---|---| +| `server.port` | int | Listen port. Default `4747` when absent or `0`. | +| `services` | map | Presence of the block is authoritative — only the services it lists run. Absent means every registered service runs. | +| `services..enabled` | bool | Whether that service starts. | +| `services..data_dir` | string | Where that service stores data. | +| `admin.enabled` | bool | Whether the admin API is served. Default `false`. | +| `logging.level` | string | Log level. | +| `logging.format` | string | Log format. | + +### Environment variables + +| Variable | Meaning | +|---|---| +| `DEVCLOUD_PORT` | Overrides `server.port`. | +| `DEVCLOUD_SERVICES` | Service filter. `all`, a comma-separated list of service ids, or the `tier1` / `tier2` / `tier3` shortcuts. Unknown tokens are treated as literal service names. | +| `DEVCLOUD_DATA_DIR` | Base directory; each service stores under `/`. Overrides `data_dir`. | + +Environment overrides config file, and that precedence is guaranteed. + +### Command line + +`-config ` keeps its meaning. With no flag, DevCloud uses `./devcloud.yaml` if present +and the embedded defaults otherwise — zero-config startup keeps working. + +### Admin API + +Served at `/devcloud/api/` when `admin.enabled: true`. These routes keep responding, and their +JSON responses **only gain fields** — no documented key is removed or repurposed. + +| Route | Guaranteed response keys | +|---|---| +| `GET /devcloud/api/services` | array of `id`, `name`, `status`, `resourceCount` | +| `GET /devcloud/api/services/{id}/resources` | array of `type`, `id`, `name` | +| `GET /devcloud/api/logs` | array of `method`, `path`, `status`, `duration`, `timestamp`, `service`; newest first; `?limit=` honoured | +| `GET /devcloud/api/fidelity` | object keyed by service id, each with `modelBacked` and `counts`; `?service=` adds `operations` | + +### Fidelity tier names + +`hand-verified`, `auto-crud` and `unimplemented` keep the meanings given in +[fidelity-manifest.md](fidelity-manifest.md). The set does not shrink, and a name is never +reused for a different meaning. Every reachable operation carries one — enforced by +`TestFidelityManifestCoverage` in [`cmd/devcloud/fidelity_test.go`](../cmd/devcloud/fidelity_test.go), +which fails the build if an operation is unclassified. + +### Wire behaviour — scoped to the compatibility suite + +**A hand-verified operation covered by a test in [`tests/compatibility/`](../tests/compatibility/) +keeps its response shape across 1.x.** + +That suite — 775 tests driving real boto3 clients — *is* the guarantee. It runs in CI on every +push and again against the tagged commit before a release publishes, so the promise is enforced +by a failing build rather than by review discipline. If a response shape you depend on is not +covered there, it is not covered by this policy; adding a test is the way to bring it in scope, +and such contributions are welcome. + +## Not guaranteed + +Depending on any of the following will break, and breaking it is **not** a major-version event. + +- **`auto-crud` response content.** 948 operations are served by the + [generic CRUD engine](crud-engine.md) at fidelity that is deliberately *plausible, not + faithful*: store-backed responses echoing your input plus synthesized ids and ARNs, with no + validation, no cross-resource integrity, no pagination correctness and no business logic. + Their shape and content may change in any release. Use them to wire an SDK up, nothing more. +- **Hand-verified operations with no compatibility test.** Of 4,496 hand-verified operations, + only what the suite covers is promised. The rest are best-effort. +- **Data durability.** Stores are local development stores. Several are in-memory and + per-process; on-disk layouts under `data_dir` may change format between releases without a + migration. Do not treat DevCloud as a database. +- **`unimplemented` → served transitions.** An operation that returns an error today may start + returning a response. This is additive, and ships in a minor release. +- **Service coverage.** New services may be added in a minor release. The 104 services present + at v1.0 are a floor, not a ceiling. +- **Error message wording.** Error *codes* and HTTP status of `unimplemented` operations are + documented in [fidelity-manifest.md](fidelity-manifest.md); the human-readable message text + is not stable. +- **Log output.** Format, levels and wording of server logs are operational, not an API. +- **Everything under `internal/`.** Go forbids importing it from another module, and DevCloud + reserves the right to restructure it freely across 1.x — explicitly including the planned + intermediate representation and `ModelSource` work on the [roadmap](roadmap.md). Internal + churn is not a compatibility event. +- **Behavioural parity with AWS.** No release of DevCloud promises AWS's validation, business + logic, eventual-consistency timing, rate limits, or IAM enforcement. Credentials are accepted + without signature verification. + +## Deprecation procedure + +Removing anything from the guaranteed list is a **major** version bump. Before that can happen: + +1. **Deprecate in a minor release.** The old form keeps working and emits a runtime warning + naming its replacement. The precedent is the `dashboard` → `admin` config rename: the old key + still enables the admin API, warns, and yields to an explicit `admin` block + ([`config.go`](../internal/config/config.go)). +2. **Document it** — in the release notes for that version, and here. +3. **Remove no earlier than the next major.** At least one released version must have shipped + the warning. + +Silence is not deprecation. A removed key that YAML would otherwise drop without comment is +kept in the parser purely to warn — that is why `auth` still produces a message telling you +SigV4 is not enforced rather than being ignored. + +The pre-flight checklist in [release.md](release.md#pre-flight-checklist) makes this a step in +cutting a release, not a thing to remember. + +## Reporting a break + +If a 1.x release breaks something on the guaranteed list, that is a bug — please +[open an issue](https://github.com/skyoo2003/devcloud/issues) with the DevCloud version and a +reproducing snippet. If it breaks something on the not-guaranteed list, an issue is still +useful: it is evidence for tightening the policy in a future major. diff --git a/docs/plugin-api.md b/docs/plugin-api.md index 05db2e6..0c13319 100644 --- a/docs/plugin-api.md +++ b/docs/plugin-api.md @@ -127,6 +127,12 @@ The registry also exposes `RegisteredServices()` (all registered IDs) and ## API stability +This section is the **in-tree** contract — it constrains contributors writing +service plugins inside this repository. `internal/plugin` cannot be imported +from another Go module, so it is not the promise a *user* of DevCloud depends +on. That is [compatibility-policy.md](compatibility-policy.md), which covers the +config file, environment variables, CLI, admin API and wire behaviour. + Starting at **v1.0**, `ServicePlugin`, `PluginConfig`, `Response`, `Resource`, and the `ProtocolType` constants are stable within the `v1.x` series: diff --git a/docs/release.md b/docs/release.md index 68e1e40..c87ecb3 100644 --- a/docs/release.md +++ b/docs/release.md @@ -61,7 +61,11 @@ The rest are only caught here. with a warning first. The precedent is the `dashboard` → `admin` rename in [`internal/config/config.go`](../internal/config/config.go): the old key kept working, emitted a warning, and only then became removable. Removing without that overlap is a - major-version change. + major-version change. The full procedure, and the surfaces it applies to, is + [compatibility-policy.md](compatibility-policy.md). +- [ ] **Compatibility review.** If this release changes anything on the guaranteed list in + [compatibility-policy.md](compatibility-policy.md), it is a major bump — or it is a bug. + Additive change (a new config key, a new response field, a new service) is a minor bump. ## Cutting a release diff --git a/internal/admin/api_test.go b/internal/admin/api_test.go index c41bafa..f8ab711 100644 --- a/internal/admin/api_test.go +++ b/internal/admin/api_test.go @@ -49,6 +49,89 @@ func newTestRegistry(p *mockServicePlugin) *plugin.Registry { return reg } +// getJSON issues GET path against h, asserts the status and Content-Type the +// compatibility policy guarantees, and decodes the body into v. +func getJSON(t *testing.T, h http.Handler, path string, v any) { + t.Helper() + w := httptest.NewRecorder() + h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil)) + require.Equal(t, http.StatusOK, w.Code, path) + require.Equal(t, "application/json", w.Header().Get("Content-Type"), path) + require.NoError(t, json.NewDecoder(w.Body).Decode(v), path) +} + +// surfaceAPI builds an API with one service, one resource and one log entry — +// enough for every guaranteed route to return a non-empty body. +func surfaceAPI(t *testing.T) http.Handler { + t.Helper() + p := &mockServicePlugin{ + id: "s3", + name: "Amazon S3", + resources: []plugin.Resource{{Type: "bucket", ID: "my-bucket", Name: "my-bucket"}}, + } + lc := NewLogCollector(10) + lc.Add(RequestLog{ + Method: "GET", Path: "/s3/my-bucket", Status: 200, + Duration: "1.000ms", Timestamp: time.Now(), Service: "s3", + }) + return NewAPI(newTestRegistry(p), lc).Handler() +} + +// TestGuaranteedAdminSurface_Collections locks the wire keys of the three +// list-returning routes in docs/compatibility-policy.md. +// +// It decodes into map[string]any deliberately. The other tests in this file +// decode into the internal structs (serviceInfo, RequestLog), so renaming a +// JSON tag renames both sides of the assertion and they stay green while every +// consumer breaks. Asserting key *presence* rather than the whole payload keeps +// additive change — which the policy allows — from failing the build. +func TestGuaranteedAdminSurface_Collections(t *testing.T) { + h := surfaceAPI(t) + + for _, tc := range []struct { + route string + keys []string + }{ + {"/devcloud/api/services", []string{"id", "name", "status", "resourceCount"}}, + {"/devcloud/api/services/s3/resources", []string{"type", "id", "name"}}, + {"/devcloud/api/logs", []string{"method", "path", "status", "duration", "timestamp", "service"}}, + } { + var got []map[string]any + getJSON(t, h, tc.route, &got) + require.NotEmpty(t, got, "%s returned no entries to check", tc.route) + + for _, key := range tc.keys { + if _, ok := got[0][key]; !ok { + t.Errorf("%s: entry is missing guaranteed key %q — guaranteed by docs/compatibility-policy.md", + tc.route, key) + } + } + } +} + +// TestGuaranteedAdminSurface_Fidelity locks both shapes of the fidelity route: +// the summary carries counts only, and naming a service adds its operations. +func TestGuaranteedAdminSurface_Fidelity(t *testing.T) { + h := surfaceAPI(t) + + var summary map[string]map[string]any + getJSON(t, h, "/devcloud/api/fidelity", &summary) + require.Contains(t, summary, "s3") + for _, key := range []string{"modelBacked", "counts"} { + if _, ok := summary["s3"][key]; !ok { + t.Errorf("/devcloud/api/fidelity: missing guaranteed key %q — guaranteed by docs/compatibility-policy.md", key) + } + } + assert.NotContains(t, summary["s3"], "operations", + "the unfiltered summary must not carry every operation") + + var detail map[string]map[string]any + getJSON(t, h, "/devcloud/api/fidelity?service=s3", &detail) + require.Contains(t, detail, "s3") + assert.Contains(t, detail["s3"], "operations", + "?service= must add the per-operation tiers") +} + // TestAPI_Services registers a mock plugin and verifies the // /devcloud/api/services endpoint returns it. func TestAPI_Services(t *testing.T) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 93c2b0d..ec9bc3e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -239,6 +239,78 @@ func TestParse_AdminKeyWinsOverDeprecated(t *testing.T) { assert.False(t, cfg.Admin.Enabled, "explicit admin.enabled=false should win over deprecated dashboard.enabled=true") } +// TestGuaranteedConfigSurface locks the config keys docs/compatibility-policy.md +// promises will keep their name and meaning across 1.x. Adding a key is fine; +// removing or repurposing one of these is a major-version event, and this test +// is what makes that visible instead of silent. The deprecated 'dashboard' and +// removed 'auth' keys are covered by TestParse_DeprecatedDashboardKey and +// TestParse_RemovedAuthKeyWarns. +func TestGuaranteedConfigSurface(t *testing.T) { + isolateEnv(t) + + cfg, warnings, err := parse([]byte(` +server: + port: 5555 +services: + s3: + enabled: true + data_dir: ./custom/s3 +admin: + enabled: true +logging: + level: debug + format: json +`)) + require.NoError(t, err) + assert.Empty(t, warnings, "the guaranteed surface must parse without warnings") + + // t.Errorf per key, not require: one removed field should not hide the rest. + for _, tc := range []struct { + key string + got any + want any + }{ + {"server.port", cfg.Server.Port, 5555}, + {"services..enabled", cfg.Service("s3").Enabled, true}, + {"services..data_dir", cfg.Service("s3").DataDir, "./custom/s3"}, + {"services (block is authoritative)", cfg.Service("sqs").Enabled, false}, + {"admin.enabled", cfg.Admin.Enabled, true}, + {"logging.level", cfg.Logging.Level, "debug"}, + {"logging.format", cfg.Logging.Format, "json"}, + } { + if tc.got != tc.want { + t.Errorf("%s: got %v, want %v — guaranteed by docs/compatibility-policy.md", tc.key, tc.got, tc.want) + } + } +} + +// TestGuaranteedEnvSurface locks the three environment overrides the policy +// guarantees, including their precedence over the config file. +func TestGuaranteedEnvSurface(t *testing.T) { + isolateEnv(t) + t.Setenv("DEVCLOUD_PORT", "6060") + t.Setenv("DEVCLOUD_SERVICES", "s3") + t.Setenv("DEVCLOUD_DATA_DIR", "/tmp/dc") + + cfg, _, err := parse([]byte(` +server: + port: 4747 +services: + s3: + enabled: true + data_dir: ./custom/s3 + sqs: + enabled: true +`)) + require.NoError(t, err) + + assert.Equal(t, 6060, cfg.Server.Port, "DEVCLOUD_PORT must override server.port") + assert.True(t, cfg.Service("s3").Enabled, "DEVCLOUD_SERVICES must keep the service it names") + assert.False(t, cfg.Service("sqs").Enabled, "DEVCLOUD_SERVICES must filter out services it does not name") + assert.Equal(t, filepath.Join("/tmp/dc", "s3"), cfg.Service("s3").DataDir, + "DEVCLOUD_DATA_DIR must rebase data dirs, overriding data_dir") +} + // TestParse_EmptyData_FillsDefaults verifies that parsing an empty YAML // payload yields a Config with at least the default server port populated, // so downstream code sees a usable config rather than a zero-value one. From 7d2e1a15aff059b45cb3d2bc68afdedaf44fb152 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Mon, 10 Aug 2026 22:43:24 +0900 Subject: [PATCH 2/5] fix(config): make service selection do what the docs already promised MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on #129 found two cases where the published compatibility policy states a guarantee the code does not keep. Both are in Config.Service. An empty services block started every registered service. YAML decodes it to a non-nil empty map, and the len() > 0 check read that as "no block at all". An operator who writes an empty block gets 104 services instead of zero. A block is a block; only what it lists runs. DEVCLOUD_SERVICES could only narrow the YAML block, never widen it. With DEVCLOUD_SERVICES=sqs and a block listing only s3, the two filters intersect to the empty set and nothing starts at all. docs/configuration.md has said since before this PR that the variable disables everything it does not name "regardless of their enabled setting in YAML" — so the code was the bug, not the prose. It now names the running set outright: it starts a service the block omits, overrides a block entry the file disabled, and still takes that service's data_dir from the block. The existing TestGuaranteedEnvSurface masked the second case by naming only s3, which the fixture's YAML already listed — an intersecting filter passes that. It now names lambda too, which the YAML omits. All three new assertions fail against the previous Service() and pass against this one. Full Go suite green, 775 boto3 compatibility tests green. --- changes/unreleased/Fixed-20260810-093000.yaml | 5 +++ docs/compatibility-policy.md | 7 ++- docs/configuration.md | 4 +- internal/config/config.go | 25 +++++++---- internal/config/config_test.go | 45 ++++++++++++++++++- 5 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 changes/unreleased/Fixed-20260810-093000.yaml diff --git a/changes/unreleased/Fixed-20260810-093000.yaml b/changes/unreleased/Fixed-20260810-093000.yaml new file mode 100644 index 0000000..a0693a4 --- /dev/null +++ b/changes/unreleased/Fixed-20260810-093000.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: Service selection now matches what the docs promise. An empty `services` block is still a block, so it starts nothing rather than every registered service; and `DEVCLOUD_SERVICES` names the running set outright rather than intersecting with the `services` block, so it starts a service the block omits and overrides a block entry the file disabled. Previously `DEVCLOUD_SERVICES=sqs` alongside a block listing only `s3` started nothing at all +time: 2026-08-10T09:30:00.000000+09:00 +custom: + Issue: "129" diff --git a/docs/compatibility-policy.md b/docs/compatibility-policy.md index c59464b..aae9ce0 100644 --- a/docs/compatibility-policy.md +++ b/docs/compatibility-policy.md @@ -21,7 +21,7 @@ removed or repurposed. Defined in [`internal/config/config.go`](../internal/conf | Key | Type | Meaning | |---|---|---| | `server.port` | int | Listen port. Default `4747` when absent or `0`. | -| `services` | map | Presence of the block is authoritative — only the services it lists run. Absent means every registered service runs. | +| `services` | map | Presence of the block is authoritative — only the services it lists run, and an empty block runs nothing. Absent means every registered service runs. | | `services..enabled` | bool | Whether that service starts. | | `services..data_dir` | string | Where that service stores data. | | `admin.enabled` | bool | Whether the admin API is served. Default `false`. | @@ -33,10 +33,13 @@ removed or repurposed. Defined in [`internal/config/config.go`](../internal/conf | Variable | Meaning | |---|---| | `DEVCLOUD_PORT` | Overrides `server.port`. | -| `DEVCLOUD_SERVICES` | Service filter. `all`, a comma-separated list of service ids, or the `tier1` / `tier2` / `tier3` shortcuts. Unknown tokens are treated as literal service names. | +| `DEVCLOUD_SERVICES` | Names the running service set. `all`, a comma-separated list of service ids, or the `tier1` / `tier2` / `tier3` shortcuts. Unknown tokens are treated as literal service names. | | `DEVCLOUD_DATA_DIR` | Base directory; each service stores under `/`. Overrides `data_dir`. | Environment overrides config file, and that precedence is guaranteed. +`DEVCLOUD_SERVICES` decides membership on its own: it starts a service the +`services` block omits and stops one the block enables. It is a replacement for +the block's selection, not an intersection with it. ### Command line diff --git a/docs/configuration.md b/docs/configuration.md index 5cddca2..6682f53 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -29,6 +29,8 @@ Environment variables override YAML values for selected keys (see [Environment V The `services` block is **optional and authoritative**: omit it (as the embedded default does) and every registered service starts with `data_dir ./data/`; list any service and *only* the services you list start. +Writing `services: {}` lists nothing, so nothing starts — a block is a block +even when it is empty. | Key | Default | Description | |-----|---------|-------------| @@ -112,7 +114,7 @@ docker run -p 8080:8080 -e DEVCLOUD_PORT=8080 ghcr.io/skyoo2003/devcloud:latest ### `DEVCLOUD_SERVICES` -Comma-separated list of services to enable. When set, **only** the listed services are enabled — all others are disabled regardless of their `enabled` setting in YAML. When not set, each service uses its YAML `enabled` value (or the embedded default of `true`). An unknown `tierN` token is treated as a literal service name and logged as a warning. +Comma-separated list of services to enable. When set, it names the running set outright: **only** the listed services are enabled, all others are disabled regardless of their `enabled` setting in YAML, and a service the `services` block omits entirely still starts if you name it here. The block still supplies that service's `data_dir`. When not set, each service uses its YAML `enabled` value (or the embedded default of `true`). An unknown `tierN` token is treated as a literal service name and logged as a warning. **Tier shortcuts** (expand to predefined service groups — see [`internal/config/config.go`](https://github.com/skyoo2003/devcloud/blob/main/internal/config/config.go) for the exact list): diff --git a/internal/config/config.go b/internal/config/config.go index 8a58fa8..2402de2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -76,19 +76,26 @@ type LoggingConfig struct { // Service returns the effective configuration for serviceID. // // A YAML services block is authoritative: only the services it lists can run. -// When there is no block (the embedded default), every service is enabled with -// data_dir /. DEVCLOUD_SERVICES and DEVCLOUD_DATA_DIR are applied -// here so both paths agree. +// An empty block therefore runs nothing — it is a block, so it decides. Absent +// entirely (the embedded default), every service is enabled with data_dir +// /. DEVCLOUD_SERVICES and DEVCLOUD_DATA_DIR are applied here so both +// paths agree. func (c *Config) Service(serviceID string) ServiceConfig { - if c.allowed != nil && !c.allowed[serviceID] { - return ServiceConfig{} - } svc, listed := c.Services[serviceID] - if !listed { - if len(c.Services) > 0 { + switch { + case c.allowed != nil: + // DEVCLOUD_SERVICES names the running set outright, so it decides + // membership on its own: it can enable a service the YAML block omits + // as well as one the block lists with enabled: false. The block still + // supplies that service's data_dir. Environment beats file. + svc.Enabled = c.allowed[serviceID] + case !listed: + // nil means no block at all; a non-nil empty map means "services: {}", + // which lists nothing and therefore runs nothing. + if c.Services != nil { return ServiceConfig{} } - svc = ServiceConfig{Enabled: true} + svc.Enabled = true } if c.baseDir == "" && svc.DataDir != "" { return svc diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ec9bc3e..c1cc216 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -98,6 +98,44 @@ func TestService_ExplicitBlockIsAuthoritative(t *testing.T) { assert.False(t, cfg.Service("sqs").Enabled, "unlisted service must not start") } +// TestService_EmptyServicesBlock_RunsNothing covers "services: {}". YAML makes +// that a non-nil empty map, so it is a block that lists nothing — and a block +// is authoritative. Treating it like an absent block would start all 104 +// services, which is the opposite of what the operator wrote. +func TestService_EmptyServicesBlock_RunsNothing(t *testing.T) { + isolateEnv(t) + cfg, _, err := parse([]byte("services: {}\n")) + require.NoError(t, err) + require.NotNil(t, cfg.Services, "an empty block must survive parsing as a non-nil map") + assert.False(t, cfg.Service("s3").Enabled, "an empty services block lists nothing, so nothing runs") +} + +// TestService_EnvSelectionOverridesBlock covers the precedence +// docs/configuration.md states: DEVCLOUD_SERVICES names the running set +// outright. It must be able to add a service the block omits — not merely +// intersect with the block, which would leave "env names sqs, YAML lists s3" +// running nothing at all. +func TestService_EnvSelectionOverridesBlock(t *testing.T) { + isolateEnv(t) + t.Setenv("DEVCLOUD_SERVICES", "sqs,dynamodb") + + cfg, _, err := parse([]byte(` +services: + s3: + enabled: true + dynamodb: + enabled: false + data_dir: ./custom/ddb +`)) + require.NoError(t, err) + + assert.True(t, cfg.Service("sqs").Enabled, "env must enable a service the block omits") + assert.True(t, cfg.Service("dynamodb").Enabled, "env must override the block's enabled: false") + assert.False(t, cfg.Service("s3").Enabled, "env must disable a service it does not name") + assert.Equal(t, "./custom/ddb", cfg.Service("dynamodb").DataDir, + "the block still supplies data_dir for a service the env enables") +} + func TestService_DataDirOverride(t *testing.T) { isolateEnv(t) yaml := []byte("services:\n s3:\n enabled: true\n data_dir: ./custom/s3\n") @@ -151,7 +189,7 @@ func TestLoadOrDefault_FileMissing_UsesEmbedded(t *testing.T) { require.NoError(t, err) require.NotNil(t, cfg) assert.Equal(t, 4747, cfg.Server.Port, "embedded default port should be 4747") - assert.Empty(t, cfg.Services, "embedded default carries no services block") + assert.Nil(t, cfg.Services, "embedded default carries no services block — nil, not an empty one, which would run nothing") assert.True(t, cfg.Service("s3").Enabled, "s3 should be enabled in embedded default") } @@ -289,7 +327,9 @@ logging: func TestGuaranteedEnvSurface(t *testing.T) { isolateEnv(t) t.Setenv("DEVCLOUD_PORT", "6060") - t.Setenv("DEVCLOUD_SERVICES", "s3") + // s3 is listed in the YAML below, lambda deliberately is not: naming only a + // listed service would let a merely-intersecting filter pass this test. + t.Setenv("DEVCLOUD_SERVICES", "s3,lambda") t.Setenv("DEVCLOUD_DATA_DIR", "/tmp/dc") cfg, _, err := parse([]byte(` @@ -306,6 +346,7 @@ services: assert.Equal(t, 6060, cfg.Server.Port, "DEVCLOUD_PORT must override server.port") assert.True(t, cfg.Service("s3").Enabled, "DEVCLOUD_SERVICES must keep the service it names") + assert.True(t, cfg.Service("lambda").Enabled, "DEVCLOUD_SERVICES must enable a service the file omits") assert.False(t, cfg.Service("sqs").Enabled, "DEVCLOUD_SERVICES must filter out services it does not name") assert.Equal(t, filepath.Join("/tmp/dc", "s3"), cfg.Service("s3").DataDir, "DEVCLOUD_DATA_DIR must rebase data dirs, overriding data_dir") From 35fe3da4acffec5d1e2bedfefe51a530b6ef7ecb Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Mon, 10 Aug 2026 22:50:31 +0900 Subject: [PATCH 3/5] docs: narrow three guarantees to what the repo can actually keep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remaining Codex findings on #129. Each is the policy promising more than the code delivers, and each is fixed by shrinking the promise rather than by pretending the gap is not there. Wire behaviour. "A hand-verified operation covered by a test keeps its response shape" reads as the whole response. The suite does not check whole responses — test_lambda.py::test_create_function asserts FunctionName and the presence of FunctionArn, so dropping Runtime, Handler or MemorySize from CreateFunction stays green. The promise is now the asserted fields, which is exactly what a failing build defends, with that example spelled out. Unimplemented errors. The manifest claimed JSON and Query services answer InvalidAction (400). Only the 46 providers that return ErrUnhandledOp reach that fallback in gateway/router.go; 32 answer NotImplemented (501) from their own dispatch default, iot / iotwireless / apigatewayv2 / backup / bedrock / s3 use their own vocabulary, and sqs differs by protocol — Query 501, JSON 400. Verified against a running server: IAM AcceptDelegationRequest returns 501 NotImplemented today, which the published policy would have made a bug. Guaranteed now is only that the call fails and never fabricates a success; the code and status are documented, not promised, so normalizing them later is a minor release. The tier comment is generated, so the fix is in the codegen template with internal/generated regenerated from it. Plugin API. plugin-api.md required a major bump for a breaking ServicePlugin change while the policy reserved the right to restructure all of internal/ freely, leaving no answer for a maintainer holding both documents. Nothing outside this module can import internal/plugin, so the in-tree contract binds contributors, not release versioning; it now says so, and the policy's internal/ exclusion names it rather than silently contradicting it. Full Go suite green, lint clean, 775 boto3 compatibility tests green, codegen drift-free after rm -rf internal/generated && make codegen. --- .../Documentation-20260810-101500.yaml | 5 +++ docs/compatibility-policy.md | 34 +++++++++++++------ docs/fidelity-manifest.md | 21 +++++++++++- docs/plugin-api.md | 6 +++- .../templates/fidelity_manifest.go.tmpl | 13 ++++--- internal/generated/fidelity/manifest_gen.go | 13 ++++--- 6 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 changes/unreleased/Documentation-20260810-101500.yaml diff --git a/changes/unreleased/Documentation-20260810-101500.yaml b/changes/unreleased/Documentation-20260810-101500.yaml new file mode 100644 index 0000000..5af29ef --- /dev/null +++ b/changes/unreleased/Documentation-20260810-101500.yaml @@ -0,0 +1,5 @@ +kind: Documentation +body: Corrected what the fidelity manifest says an `unimplemented` operation returns. It claimed JSON and Query services answer `InvalidAction` (HTTP 400), but only the 46 providers that fall through to the CRUD engine do; 32 answer `NotImplemented` (HTTP 501) from their own dispatch default, several use their own vocabulary, and `sqs` differs by protocol. Only the failure itself is stable — the specific code and status are documented, not guaranteed +time: 2026-08-10T10:15:00.000000+09:00 +custom: + Issue: "129" diff --git a/docs/compatibility-policy.md b/docs/compatibility-policy.md index aae9ce0..6bfeb3f 100644 --- a/docs/compatibility-policy.md +++ b/docs/compatibility-policy.md @@ -68,14 +68,19 @@ which fails the build if an operation is unclassified. ### Wire behaviour — scoped to the compatibility suite -**A hand-verified operation covered by a test in [`tests/compatibility/`](../tests/compatibility/) -keeps its response shape across 1.x.** +**The response fields that a test in [`tests/compatibility/`](../tests/compatibility/) asserts +keep their name, type and meaning across 1.x.** -That suite — 775 tests driving real boto3 clients — *is* the guarantee. It runs in CI on every -push and again against the tagged commit before a release publishes, so the promise is enforced -by a failing build rather than by review discipline. If a response shape you depend on is not -covered there, it is not covered by this policy; adding a test is the way to bring it in scope, -and such contributions are welcome. +The promise is exactly as wide as the assertions — not as wide as the operation. `CreateFunction` +is covered by `test_lambda.py`, but that test asserts `FunctionName` and the presence of +`FunctionArn`, so those two are promised while `Runtime`, `Handler` and `MemorySize` are not, +even though today's response carries them. + +That narrowness is the point: it is the promise the repo can actually keep. The suite — 775 tests +driving real boto3 clients — runs in CI on every push and again against the tagged commit before +a release publishes, so breaking an asserted field fails the build rather than depending on review +discipline. Anything the suite does not assert rests on nothing but intent. Widening the promise +means adding assertions, and such contributions are welcome. ## Not guaranteed @@ -95,14 +100,21 @@ Depending on any of the following will break, and breaking it is **not** a major returning a response. This is additive, and ships in a minor release. - **Service coverage.** New services may be added in a minor release. The 104 services present at v1.0 are a floor, not a ceiling. -- **Error message wording.** Error *codes* and HTTP status of `unimplemented` operations are - documented in [fidelity-manifest.md](fidelity-manifest.md); the human-readable message text - is not stable. +- **Error codes, HTTP status and message wording.** What *is* guaranteed for an `unimplemented` + operation is that it **fails** — an AWS-shaped error, never a fabricated success. Which error + is not: it comes from whichever provider handles the request, so it is `InvalidAction` (400) + for services that fall through to the CRUD engine, `NotImplemented` (501) for the 32 providers + with their own dispatch default, and each path-routed provider's own vocabulary otherwise + (`s3` `MethodNotAllowed` 405, `bedrock` `UnsupportedOperation` 400). `sqs` even differs by + protocol. [fidelity-manifest.md](fidelity-manifest.md) records the current behaviour; + normalizing it is a minor release, not a major one. - **Log output.** Format, levels and wording of server logs are operational, not an API. - **Everything under `internal/`.** Go forbids importing it from another module, and DevCloud reserves the right to restructure it freely across 1.x — explicitly including the planned intermediate representation and `ModelSource` work on the [roadmap](roadmap.md). Internal - churn is not a compatibility event. + churn is not a compatibility event. The in-tree `ServicePlugin` contract in + [plugin-api.md](plugin-api.md#api-stability) is not an exception to this: it is a convention + that keeps in-tree plugins compiling, and it does not gate release versioning. - **Behavioural parity with AWS.** No release of DevCloud promises AWS's validation, business logic, eventual-consistency timing, rate limits, or IAM enforcement. Credentials are accepted without signature verification. diff --git a/docs/fidelity-manifest.md b/docs/fidelity-manifest.md index 1466c86..ceb607f 100644 --- a/docs/fidelity-manifest.md +++ b/docs/fidelity-manifest.md @@ -14,11 +14,30 @@ Smithy models, the CRUD registry, and each provider's dispatch code. |------|---------|--------------| | `hand-verified` | The service's provider implements the operation explicitly. | Behaviour, not just shape. Covered by the boto3 compatibility suite where tests exist. | | `auto-crud` | Served by the [generic CRUD engine](crud-engine.md) with plausible, store-backed responses. No validation, no business logic, no cross-resource integrity. | Wiring your SDK calls and round-tripping create → get → list → delete. Nothing else. | -| `unimplemented` | Not served — the call fails instead of inventing a success. The error is the provider's own: JSON and Query services return `InvalidAction` (HTTP 400), while the path-routed providers answer in their own vocabulary (`s3` `MethodNotAllowed` 405, `lambda` `ResourceNotFoundException` 404, `bedrock` `UnsupportedOperation` 400). | Knowing early that DevCloud will not serve this call. | +| `unimplemented` | Not served — the call fails instead of inventing a success. The error is whichever the handling provider emits, and that varies (see below). | Knowing early that DevCloud will not serve this call. | `hand-verified` always wins: the CRUD engine is reached only when a provider's dispatch falls through, so a hand-written implementation is never shadowed. +### What an `unimplemented` call actually returns + +There is no single error. Which one you get depends on how the owning provider +declines the operation: + +| How the provider declines | Error | Status | Providers | +|---|---|---|---| +| Returns `ErrUnhandledOp`, and the CRUD engine cannot classify the operation either — [`gateway/router.go`](../internal/gateway/router.go) emits the fallback | `InvalidAction` | 400 | 46 | +| Its dispatch `default:` answers directly | `NotImplemented` | 501 | 32 | +| Its dispatch `default:` answers in its own vocabulary | `UnsupportedOperation` / `MethodNotAllowed` | 400 / 405 | `iot`, `iotwireless`, `apigatewayv2`, `backup`, `bedrock`, `s3` | + +A service can even answer differently per protocol: `sqs` returns +`NotImplemented` (501) on the Query protocol and `InvalidAction` (400) on JSON. + +Only the *failure* is stable, and that is all +[compatibility-policy.md](compatibility-policy.md) promises — an `unimplemented` +operation never fabricates a success. The specific code and status are not +guaranteed across 1.x; normalizing them is a minor release. + ## Current coverage | Tier | Operations | diff --git a/docs/plugin-api.md b/docs/plugin-api.md index 0c13319..bb5246d 100644 --- a/docs/plugin-api.md +++ b/docs/plugin-api.md @@ -139,7 +139,11 @@ and the `ProtocolType` constants are stable within the `v1.x` series: - No method will be **removed** from `ServicePlugin` and no existing method **signature** will change in a `v1.x` release. - Struct fields will only be **added**, never removed or repurposed. -- Any breaking change to this contract requires a **major** version bump. +- Any breaking change to this contract must be called out in the release notes, + and every in-tree plugin updated in the same change. It is not on its own a + major-version event: nothing outside this module can import + `internal/plugin`, so no user can be depending on it. Release versioning is + governed by [compatibility-policy.md](compatibility-policy.md). New optional capability is introduced additively (new `Options` keys, new `ProtocolType` values) so existing plugins keep compiling and behaving. diff --git a/internal/codegen/templates/fidelity_manifest.go.tmpl b/internal/codegen/templates/fidelity_manifest.go.tmpl index 237e044..c42c336 100644 --- a/internal/codegen/templates/fidelity_manifest.go.tmpl +++ b/internal/codegen/templates/fidelity_manifest.go.tmpl @@ -21,11 +21,14 @@ const ( // store-backed responses: no validation, no business logic. TierAutoCRUD Tier = "auto-crud" // TierUnimplemented is not served: the call fails with an error rather than - // a made-up success. The error is the provider's own, not one shared code — - // JSON and Query services fall through to InvalidAction (HTTP 400), while - // the path-routed providers answer in their own vocabulary (s3 - // MethodNotAllowed, lambda ResourceNotFoundException, bedrock - // UnsupportedOperation). + // a made-up success. Only that failure is stable. The error itself is the + // declining provider's own and varies — InvalidAction (400) when the + // provider returns ErrUnhandledOp and the CRUD engine cannot classify the + // operation either, NotImplemented (501) from the providers that answer + // from their own dispatch default, and each path-routed provider's own + // vocabulary otherwise (s3 MethodNotAllowed 405, bedrock + // UnsupportedOperation 400). sqs even differs by protocol. See + // docs/fidelity-manifest.md; the codes are not guaranteed across 1.x. TierUnimplemented Tier = "unimplemented" ) diff --git a/internal/generated/fidelity/manifest_gen.go b/internal/generated/fidelity/manifest_gen.go index acaf3e3..a28cfb6 100644 --- a/internal/generated/fidelity/manifest_gen.go +++ b/internal/generated/fidelity/manifest_gen.go @@ -21,11 +21,14 @@ const ( // store-backed responses: no validation, no business logic. TierAutoCRUD Tier = "auto-crud" // TierUnimplemented is not served: the call fails with an error rather than - // a made-up success. The error is the provider's own, not one shared code — - // JSON and Query services fall through to InvalidAction (HTTP 400), while - // the path-routed providers answer in their own vocabulary (s3 - // MethodNotAllowed, lambda ResourceNotFoundException, bedrock - // UnsupportedOperation). + // a made-up success. Only that failure is stable. The error itself is the + // declining provider's own and varies — InvalidAction (400) when the + // provider returns ErrUnhandledOp and the CRUD engine cannot classify the + // operation either, NotImplemented (501) from the providers that answer + // from their own dispatch default, and each path-routed provider's own + // vocabulary otherwise (s3 MethodNotAllowed 405, bedrock + // UnsupportedOperation 400). sqs even differs by protocol. See + // docs/fidelity-manifest.md; the codes are not guaranteed across 1.x. TierUnimplemented Tier = "unimplemented" ) From 0846de20d094e4860e25a0f3fc3914511b9d318e Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Mon, 10 Aug 2026 23:14:06 +0900 Subject: [PATCH 4/5] fix(cloudfront): stop answering unimplemented operations with a success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three more Codex findings on #129, and the first one is a real bug rather than an over-broad promise. CloudFront's dispatch fallback returned HTTP 200 with `` and nothing else. boto3 parses that as a successful empty result, so GetPublicKey — which the fidelity manifest classifies as unimplemented — looked like it worked and returned nothing. That is true of all 122 CloudFront operations in that tier. A sweep of every provider's dispatch default found this was the only one; the other 32 already fail, and it now joins them with NotImplemented (501). Verified against a running server: GetPublicKey answers 501 where it answered 200, and ListDistributions still answers 200. TestDefaultOperations asserted the old behaviour in as many words — "Unimplemented operations should return 200 with empty XML" — so the test was holding the bug in place. It now asserts the failure, for a modelled operation and an unmatched path both. DEVCLOUD_SERVICES=all. expandTiers returns nil for "all", which Config.Service reads as "no filter", so a YAML block still restricts the running set. Here the code is right and the policy wording was wrong: configuration.md has always defined "all" as switching the filter off. The policy now carves it out instead of claiming the variable always names the set outright. Manifest coverage. The policy said an unclassified operation fails the build. TestFidelityManifestCoverage cannot see an operation that never reached the manifest — it iterates what is there and checks aggregate floors with 1,475 operations of slack. The claim is now what the tests actually enforce, plus the bound that makes the gap small: 93 of 104 services have an in-tree Smithy model, so their operation universe comes from the model and an operation losing its implementation reclassifies rather than vanishes. The 11 without one are reported by modelBacked on the fidelity endpoint. No new test — TestFidelityManifestCoversCRUDRegistry already cross-checks the engine-served half against the runtime registry; its comment now says so, and drops its reference to the InvalidAction claim this PR corrected. Full Go suite green, lint clean, 775 boto3 compatibility tests green, codegen drift-free. --- changes/unreleased/Fixed-20260810-140000.yaml | 5 ++++ cmd/devcloud/fidelity_test.go | 10 +++++-- docs/compatibility-policy.md | 28 ++++++++++++++----- docs/fidelity-manifest.md | 2 +- internal/services/cloudfront/provider.go | 15 ++++++---- internal/services/cloudfront/provider_test.go | 20 ++++++++++--- 6 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 changes/unreleased/Fixed-20260810-140000.yaml diff --git a/changes/unreleased/Fixed-20260810-140000.yaml b/changes/unreleased/Fixed-20260810-140000.yaml new file mode 100644 index 0000000..a57a73e --- /dev/null +++ b/changes/unreleased/Fixed-20260810-140000.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: CloudFront no longer fabricates a success for operations it does not implement. Its dispatch fallback answered HTTP 200 with an empty XML document, which boto3 parses as a successful empty result, on all 122 CloudFront operations the fidelity manifest classifies as `unimplemented` — including `GetPublicKey`. It now returns `NotImplemented` (HTTP 501), matching the other 32 providers that decline from their own dispatch default. It was the only provider in the tree that answered an unimplemented call with a success +time: 2026-08-10T14:00:00.000000+09:00 +custom: + Issue: "129" diff --git a/cmd/devcloud/fidelity_test.go b/cmd/devcloud/fidelity_test.go index 5c90415..1b4e108 100644 --- a/cmd/devcloud/fidelity_test.go +++ b/cmd/devcloud/fidelity_test.go @@ -73,9 +73,13 @@ func TestFidelityManifestCoversRegisteredServices(t *testing.T) { } } -// TestFidelityManifestCoversCRUDRegistry checks the other direction: every -// operation the CRUD engine would serve is declared, and never as unimplemented -// — that would promise an InvalidAction the runtime does not return. +// TestFidelityManifestCoversCRUDRegistry checks the other direction, against a +// source the manifest was not generated from: every operation the CRUD engine +// would serve at runtime is declared, and never as unimplemented — that would +// promise a failure the runtime does not deliver. This is also the only guard +// that can notice an engine-served operation going *missing* from the manifest; +// TestFidelityManifestCoverage can only inspect what the manifest already +// lists. func TestFidelityManifestCoversCRUDRegistry(t *testing.T) { for _, id := range plugin.DefaultRegistry.RegisteredServices() { for op := range crud.RegisteredOps(id) { diff --git a/docs/compatibility-policy.md b/docs/compatibility-policy.md index 6bfeb3f..1f141c5 100644 --- a/docs/compatibility-policy.md +++ b/docs/compatibility-policy.md @@ -37,9 +37,13 @@ removed or repurposed. Defined in [`internal/config/config.go`](../internal/conf | `DEVCLOUD_DATA_DIR` | Base directory; each service stores under `/`. Overrides `data_dir`. | Environment overrides config file, and that precedence is guaranteed. -`DEVCLOUD_SERVICES` decides membership on its own: it starts a service the -`services` block omits and stops one the block enables. It is a replacement for -the block's selection, not an intersection with it. + +When `DEVCLOUD_SERVICES` names services, it decides membership on its own: it +starts a service the `services` block omits and stops one the block enables. It +replaces the block's selection rather than intersecting with it. The literal +`all` is the exception — it switches the filter off and hands the decision back +to the `services` block, which is what "all" has always meant here +([configuration.md](configuration.md#devcloud_services)). ### Command line @@ -62,9 +66,19 @@ JSON responses **only gain fields** — no documented key is removed or repurpos `hand-verified`, `auto-crud` and `unimplemented` keep the meanings given in [fidelity-manifest.md](fidelity-manifest.md). The set does not shrink, and a name is never -reused for a different meaning. Every reachable operation carries one — enforced by -`TestFidelityManifestCoverage` in [`cmd/devcloud/fidelity_test.go`](../cmd/devcloud/fidelity_test.go), -which fails the build if an operation is unclassified. +reused for a different meaning. + +Every operation the manifest lists carries a tier from that set, every registered service +appears, and every operation the CRUD engine serves is present and not filed as +`unimplemented` — all three fail the build, in +[`cmd/devcloud/fidelity_test.go`](../cmd/devcloud/fidelity_test.go). + +What no test can catch is an operation that never reaches the manifest at all, and that is +bounded rather than eliminated: for the 93 services with an in-tree Smithy model the operation +universe comes from the model, so an operation losing its implementation reclassifies to +`unimplemented` instead of disappearing. For the 11 without one, the universe *is* what the +providers serve, so the manifest lists no unimplemented tail for them — `modelBacked` on +`GET /devcloud/api/fidelity` reports which is which. ### Wire behaviour — scoped to the compatibility suite @@ -103,7 +117,7 @@ Depending on any of the following will break, and breaking it is **not** a major - **Error codes, HTTP status and message wording.** What *is* guaranteed for an `unimplemented` operation is that it **fails** — an AWS-shaped error, never a fabricated success. Which error is not: it comes from whichever provider handles the request, so it is `InvalidAction` (400) - for services that fall through to the CRUD engine, `NotImplemented` (501) for the 32 providers + for services that fall through to the CRUD engine, `NotImplemented` (501) for the 33 providers with their own dispatch default, and each path-routed provider's own vocabulary otherwise (`s3` `MethodNotAllowed` 405, `bedrock` `UnsupportedOperation` 400). `sqs` even differs by protocol. [fidelity-manifest.md](fidelity-manifest.md) records the current behaviour; diff --git a/docs/fidelity-manifest.md b/docs/fidelity-manifest.md index ceb607f..348bfd5 100644 --- a/docs/fidelity-manifest.md +++ b/docs/fidelity-manifest.md @@ -27,7 +27,7 @@ declines the operation: | How the provider declines | Error | Status | Providers | |---|---|---|---| | Returns `ErrUnhandledOp`, and the CRUD engine cannot classify the operation either — [`gateway/router.go`](../internal/gateway/router.go) emits the fallback | `InvalidAction` | 400 | 46 | -| Its dispatch `default:` answers directly | `NotImplemented` | 501 | 32 | +| Its dispatch `default:` answers directly | `NotImplemented` | 501 | 33 | | Its dispatch `default:` answers in its own vocabulary | `UnsupportedOperation` / `MethodNotAllowed` | 400 / 405 | `iot`, `iotwireless`, `apigatewayv2`, `backup`, `bedrock`, `s3` | A service can even answer differently per protocol: `sqs` returns diff --git a/internal/services/cloudfront/provider.go b/internal/services/cloudfront/provider.go index 738a6c6..5ce683d 100644 --- a/internal/services/cloudfront/provider.go +++ b/internal/services/cloudfront/provider.go @@ -158,11 +158,16 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request return p.listTagsForResource(req) default: - return &plugin.Response{ - StatusCode: 200, - ContentType: "application/xml", - Body: []byte(``), - }, nil + // An operation this provider does not implement has to fail. Answering + // 200 with an empty document handed boto3 a parsed, empty success — + // exactly the fabricated success docs/compatibility-policy.md promises + // an unimplemented operation never returns, and what the fidelity + // manifest already classified these 122 operations as not doing. + detail := op + if detail == "" { + detail = req.Method + " " + req.URL.Path + } + return cfError("NotImplemented", "operation not implemented: "+detail, http.StatusNotImplemented), nil } } diff --git a/internal/services/cloudfront/provider_test.go b/internal/services/cloudfront/provider_test.go index 82e5efc..3e7912f 100644 --- a/internal/services/cloudfront/provider_test.go +++ b/internal/services/cloudfront/provider_test.go @@ -417,11 +417,23 @@ func TestTags(t *testing.T) { assert.NotContains(t, body, "team") } +// TestDefaultOperations pins the one thing docs/compatibility-policy.md +// guarantees about an unimplemented operation: it fails. This provider used to +// answer 200 with an empty XML document, which boto3 parses as a successful +// empty result — the fabricated success the policy rules out, on all 122 +// operations the fidelity manifest classifies as unimplemented for cloudfront. func TestDefaultOperations(t *testing.T) { p := newTestProvider(t) - // Unimplemented operations should return 200 with empty XML - resp := doRequest(t, p, http.MethodGet, "/2020-05-31/streaming-distribution", "") - assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.Contains(t, string(resp.Body), "xml") + for _, tc := range []struct{ name, method, path string }{ + {"modelled but unimplemented", http.MethodGet, "/2020-05-31/streaming-distribution"}, + {"unmatched path", http.MethodGet, "/2020-05-31/no-such-resource/abc"}, + } { + t.Run(tc.name, func(t *testing.T) { + resp := doRequest(t, p, tc.method, tc.path, "") + assert.GreaterOrEqual(t, resp.StatusCode, 400, "an unimplemented operation must not succeed") + assert.Equal(t, http.StatusNotImplemented, resp.StatusCode) + assert.Contains(t, string(resp.Body), "NotImplemented") + }) + } } From 0ffee430b216d6a81940c88ff412f7c81d8e346e Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Tue, 11 Aug 2026 01:01:48 +0900 Subject: [PATCH 5/5] fix(codegen): honour query constraints in generated route patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CloudFront TagResource and UntagResource were implemented, classified hand-verified, and unreachable. Both live at POST /2020-05-31/tagging and are told apart only by ?Operation=Tag or ?Operation=Untag, but matchURI split the entire pattern into path segments — so the final segment was the literal "tagging?Operation=Tag", which no real path segment can equal. Any route pattern carrying a query string was dead code. The previous commit turned that from a silent 200 with an empty XML body into a 501, which made it visible. This makes it work. matchURI now cuts pattern and request URI at "?" and requires the pattern's constraint to be satisfied: a bare key must be present ("?WithTags"), a key=value must be present with that value ("?Operation=Tag"). Because a constrained pattern is more specific than a bare one and OperationRoutes is ordered by operation name, MatchOperation makes two passes and tries constrained routes first — otherwise ".../distribution" would keep shadowing ".../distribution?WithTags". A request without the query still cannot match a constrained route, so no previously-dead pattern can hijack a working one. The fix is in the shared template, so all 92 generated routers carry it. MatchOperation's three callers — cloudfront, efs, route53 — now pass req.URL.RequestURI() rather than req.URL.Path; for efs and route53 that is a no-op, since neither service has a query-constrained route. TestTags passed "TagResource" straight to HandleRequest, so it exercised the handler and never the routing, which is why an unreachable operation looked covered. It now goes through doRequest, and TestMatchOperationHonoursQueryConstraints pins all four CloudFront routes plus the shadowing case and a non-matching query. Both fail against the old router. Also narrows the wire guarantee once more: an assertion that only checks a field is present cannot promise its type or meaning, so the policy now states the promise per assertion rather than per field, using CreateFunction's two assertions as the worked example. Verified end to end against a running server: TagResource returns 204, the tag reads back, UntagResource returns 204, and GetPublicKey still returns 501. Full Go suite green, lint clean, 775 boto3 compatibility tests green, codegen drift-free. --- changes/unreleased/Fixed-20260811-103000.yaml | 5 ++ docs/compatibility-policy.md | 23 +++-- internal/codegen/templates/router.go.tmpl | 89 +++++++++++++++---- internal/generated/acm/router.go | 89 +++++++++++++++---- internal/generated/acmpca/router.go | 89 +++++++++++++++---- internal/generated/amplify/router.go | 89 +++++++++++++++---- internal/generated/apigatewayv2/router.go | 89 +++++++++++++++---- internal/generated/appconfig/router.go | 89 +++++++++++++++---- .../applicationautoscaling/router.go | 89 +++++++++++++++---- internal/generated/appsync/router.go | 89 +++++++++++++++---- internal/generated/athena/router.go | 89 +++++++++++++++---- internal/generated/autoscaling/router.go | 89 +++++++++++++++---- internal/generated/backup/router.go | 89 +++++++++++++++---- internal/generated/batch/router.go | 89 +++++++++++++++---- internal/generated/bedrock/router.go | 89 +++++++++++++++---- internal/generated/cloudformation/router.go | 89 +++++++++++++++---- internal/generated/cloudfront/router.go | 89 +++++++++++++++---- internal/generated/cloudsearch/router.go | 89 +++++++++++++++---- internal/generated/cloudtrail/router.go | 89 +++++++++++++++---- internal/generated/cloudwatch/router.go | 89 +++++++++++++++---- internal/generated/cloudwatchlogs/router.go | 89 +++++++++++++++---- internal/generated/codeartifact/router.go | 89 +++++++++++++++---- internal/generated/codebuild/router.go | 89 +++++++++++++++---- internal/generated/codecommit/router.go | 89 +++++++++++++++---- internal/generated/codedeploy/router.go | 89 +++++++++++++++---- internal/generated/codepipeline/router.go | 89 +++++++++++++++---- internal/generated/cognitoidentity/router.go | 89 +++++++++++++++---- .../cognitoidentityprovider/router.go | 89 +++++++++++++++---- internal/generated/configservice/router.go | 89 +++++++++++++++---- internal/generated/costexplorer/router.go | 89 +++++++++++++++---- internal/generated/docdb/router.go | 89 +++++++++++++++---- internal/generated/dynamodb/router.go | 89 +++++++++++++++---- internal/generated/dynamodbstreams/router.go | 89 +++++++++++++++---- internal/generated/ebs/router.go | 89 +++++++++++++++---- internal/generated/ec2/router.go | 89 +++++++++++++++---- internal/generated/ecr/router.go | 89 +++++++++++++++---- internal/generated/ecs/router.go | 89 +++++++++++++++---- internal/generated/efs/router.go | 89 +++++++++++++++---- internal/generated/eks/router.go | 89 +++++++++++++++---- internal/generated/elasticache/router.go | 89 +++++++++++++++---- internal/generated/elasticbeanstalk/router.go | 89 +++++++++++++++---- .../elasticloadbalancingv2/router.go | 89 +++++++++++++++---- .../generated/elasticsearchservice/router.go | 89 +++++++++++++++---- internal/generated/emr/router.go | 89 +++++++++++++++---- internal/generated/eventbridge/router.go | 89 +++++++++++++++---- internal/generated/firehose/router.go | 89 +++++++++++++++---- internal/generated/fis/router.go | 89 +++++++++++++++---- internal/generated/glacier/router.go | 89 +++++++++++++++---- internal/generated/glue/router.go | 89 +++++++++++++++---- internal/generated/iam/router.go | 89 +++++++++++++++---- internal/generated/iot/router.go | 89 +++++++++++++++---- internal/generated/iotdataplane/router.go | 89 +++++++++++++++---- internal/generated/iotwireless/router.go | 89 +++++++++++++++---- internal/generated/kafka/router.go | 89 +++++++++++++++---- internal/generated/kinesis/router.go | 89 +++++++++++++++---- .../generated/kinesisanalyticsv2/router.go | 89 +++++++++++++++---- internal/generated/kms/router.go | 89 +++++++++++++++---- internal/generated/lakeformation/router.go | 89 +++++++++++++++---- internal/generated/lambda/router.go | 89 +++++++++++++++---- .../generated/managedblockchain/router.go | 89 +++++++++++++++---- internal/generated/memorydb/router.go | 89 +++++++++++++++---- internal/generated/mq/router.go | 89 +++++++++++++++---- internal/generated/mwaa/router.go | 89 +++++++++++++++---- internal/generated/neptune/router.go | 89 +++++++++++++++---- internal/generated/opensearch/router.go | 89 +++++++++++++++---- internal/generated/organizations/router.go | 89 +++++++++++++++---- internal/generated/pinpoint/router.go | 89 +++++++++++++++---- internal/generated/ram/router.go | 89 +++++++++++++++---- internal/generated/rds/router.go | 89 +++++++++++++++---- internal/generated/redshift/router.go | 89 +++++++++++++++---- internal/generated/resourcegroups/router.go | 89 +++++++++++++++---- .../resourcegroupstaggingapi/router.go | 89 +++++++++++++++---- internal/generated/route53/router.go | 89 +++++++++++++++---- internal/generated/route53resolver/router.go | 89 +++++++++++++++---- internal/generated/s3/router.go | 89 +++++++++++++++---- internal/generated/sagemaker/router.go | 89 +++++++++++++++---- internal/generated/secretsmanager/router.go | 89 +++++++++++++++---- internal/generated/servicediscovery/router.go | 89 +++++++++++++++---- internal/generated/ses/router.go | 89 +++++++++++++++---- internal/generated/sesv2/router.go | 89 +++++++++++++++---- internal/generated/sfn/router.go | 89 +++++++++++++++---- internal/generated/shield/router.go | 89 +++++++++++++++---- internal/generated/sns/router.go | 89 +++++++++++++++---- internal/generated/sqs/router.go | 89 +++++++++++++++---- internal/generated/ssm/router.go | 89 +++++++++++++++---- internal/generated/ssoadmin/router.go | 89 +++++++++++++++---- internal/generated/support/router.go | 89 +++++++++++++++---- internal/generated/swf/router.go | 89 +++++++++++++++---- internal/generated/textract/router.go | 89 +++++++++++++++---- internal/generated/timestreamwrite/router.go | 89 +++++++++++++++---- internal/generated/transcribe/router.go | 89 +++++++++++++++---- internal/generated/transfer/router.go | 89 +++++++++++++++---- internal/generated/waf/router.go | 89 +++++++++++++++---- internal/generated/wafv2/router.go | 89 +++++++++++++++---- internal/generated/xray/router.go | 89 +++++++++++++++---- internal/services/cloudfront/provider.go | 4 +- internal/services/cloudfront/provider_test.go | 44 ++++++--- internal/services/efs/provider.go | 4 +- internal/services/route53/provider.go | 2 +- 99 files changed, 6939 insertions(+), 1420 deletions(-) create mode 100644 changes/unreleased/Fixed-20260811-103000.yaml diff --git a/changes/unreleased/Fixed-20260811-103000.yaml b/changes/unreleased/Fixed-20260811-103000.yaml new file mode 100644 index 0000000..b00366f --- /dev/null +++ b/changes/unreleased/Fixed-20260811-103000.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: Generated routers now honour route patterns that constrain the query string, so operations distinguished only by a query parameter are reachable. `matchURI` split the whole pattern into path segments, which made `/2020-05-31/tagging?Operation=Tag` unable to match the path segment `tagging` — CloudFront `TagResource` and `UntagResource` were implemented but unroutable, and answered as unimplemented. A pattern's constraint must now be satisfied by the request query, and query-constrained routes are tried ahead of unconstrained ones so a bare path cannot shadow a more specific route +time: 2026-08-11T10:30:00.000000+09:00 +custom: + Issue: "129" diff --git a/docs/compatibility-policy.md b/docs/compatibility-policy.md index 1f141c5..64da296 100644 --- a/docs/compatibility-policy.md +++ b/docs/compatibility-policy.md @@ -82,19 +82,26 @@ providers serve, so the manifest lists no unimplemented tail for them — `model ### Wire behaviour — scoped to the compatibility suite -**The response fields that a test in [`tests/compatibility/`](../tests/compatibility/) asserts -keep their name, type and meaning across 1.x.** +**Whatever a test in [`tests/compatibility/`](../tests/compatibility/) asserts about a response +keeps holding across 1.x — that property, and nothing wider.** -The promise is exactly as wide as the assertions — not as wide as the operation. `CreateFunction` -is covered by `test_lambda.py`, but that test asserts `FunctionName` and the presence of -`FunctionArn`, so those two are promised while `Runtime`, `Handler` and `MemorySize` are not, -even though today's response carries them. +The promise is as wide as each individual assertion: not as wide as the field, and not as wide as +the operation. `CreateFunction` is covered by `test_lambda.py`, and its two assertions are worth +reading closely: + +- `FunctionName` is asserted **equal** to the name that was sent, so its key and its value are + both promised. +- `FunctionArn` is asserted only to be **present**, so its presence is promised and its type, + format and meaning are not. If it stopped being ARN-shaped the suite would stay green — so this + policy does not promise it stays ARN-shaped. +- `Runtime`, `Handler` and `MemorySize` are not asserted at all, so they carry no promise even + though today's response includes them. That narrowness is the point: it is the promise the repo can actually keep. The suite — 775 tests driving real boto3 clients — runs in CI on every push and again against the tagged commit before -a release publishes, so breaking an asserted field fails the build rather than depending on review +a release publishes, so breaking an assertion fails the build rather than depending on review discipline. Anything the suite does not assert rests on nothing but intent. Widening the promise -means adding assertions, and such contributions are welcome. +means adding or strengthening assertions, and such contributions are welcome. ## Not guaranteed diff --git a/internal/codegen/templates/router.go.tmpl b/internal/codegen/templates/router.go.tmpl index bc155b7..98b9ff6 100644 --- a/internal/codegen/templates/router.go.tmpl +++ b/internal/codegen/templates/router.go.tmpl @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package {{ .PkgName }} -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -22,25 +25,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -79,6 +100,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/acm/router.go b/internal/generated/acm/router.go index 35d32b8..7496e7c 100644 --- a/internal/generated/acm/router.go +++ b/internal/generated/acm/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package acm -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -36,25 +39,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -93,6 +114,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/acmpca/router.go b/internal/generated/acmpca/router.go index 79a353a..ea691b2 100644 --- a/internal/generated/acmpca/router.go +++ b/internal/generated/acmpca/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package acmpca -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -42,25 +45,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -99,6 +120,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/amplify/router.go b/internal/generated/amplify/router.go index 6ac6690..2178650 100644 --- a/internal/generated/amplify/router.go +++ b/internal/generated/amplify/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package amplify -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -56,25 +59,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -113,6 +134,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/apigatewayv2/router.go b/internal/generated/apigatewayv2/router.go index ba75b36..a8a7bce 100644 --- a/internal/generated/apigatewayv2/router.go +++ b/internal/generated/apigatewayv2/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package apigatewayv2 -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -122,25 +125,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -179,6 +200,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/appconfig/router.go b/internal/generated/appconfig/router.go index 0946938..015cbef 100644 --- a/internal/generated/appconfig/router.go +++ b/internal/generated/appconfig/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package appconfig -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -64,25 +67,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -121,6 +142,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/applicationautoscaling/router.go b/internal/generated/applicationautoscaling/router.go index 664558c..a8d4e81 100644 --- a/internal/generated/applicationautoscaling/router.go +++ b/internal/generated/applicationautoscaling/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package applicationautoscaling -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -33,25 +36,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -90,6 +111,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/appsync/router.go b/internal/generated/appsync/router.go index 4cd7db6..10937d6 100644 --- a/internal/generated/appsync/router.go +++ b/internal/generated/appsync/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package appsync -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -93,25 +96,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -150,6 +171,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/athena/router.go b/internal/generated/athena/router.go index bd4e958..a31c5ef 100644 --- a/internal/generated/athena/router.go +++ b/internal/generated/athena/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package athena -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -89,25 +92,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -146,6 +167,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/autoscaling/router.go b/internal/generated/autoscaling/router.go index 6624efc..8985621 100644 --- a/internal/generated/autoscaling/router.go +++ b/internal/generated/autoscaling/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package autoscaling -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -85,25 +88,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -142,6 +163,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/backup/router.go b/internal/generated/backup/router.go index 00f9683..3e5c1eb 100644 --- a/internal/generated/backup/router.go +++ b/internal/generated/backup/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package backup -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -127,25 +130,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -184,6 +205,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/batch/router.go b/internal/generated/batch/router.go index 5324ec0..c758518 100644 --- a/internal/generated/batch/router.go +++ b/internal/generated/batch/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package batch -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -64,25 +67,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -121,6 +142,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/bedrock/router.go b/internal/generated/bedrock/router.go index f1a2cb6..2b927b4 100644 --- a/internal/generated/bedrock/router.go +++ b/internal/generated/bedrock/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package bedrock -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -120,25 +123,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -177,6 +198,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/cloudformation/router.go b/internal/generated/cloudformation/router.go index b48cbc3..91a99f8 100644 --- a/internal/generated/cloudformation/router.go +++ b/internal/generated/cloudformation/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package cloudformation -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -109,25 +112,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -166,6 +187,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/cloudfront/router.go b/internal/generated/cloudfront/router.go index ce1d0a3..a53bc7e 100644 --- a/internal/generated/cloudfront/router.go +++ b/internal/generated/cloudfront/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package cloudfront -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -186,25 +189,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -243,6 +264,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/cloudsearch/router.go b/internal/generated/cloudsearch/router.go index 7f96015..ba837ff 100644 --- a/internal/generated/cloudsearch/router.go +++ b/internal/generated/cloudsearch/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package cloudsearch -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -45,25 +48,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -102,6 +123,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/cloudtrail/router.go b/internal/generated/cloudtrail/router.go index b4eba81..be6fd15 100644 --- a/internal/generated/cloudtrail/router.go +++ b/internal/generated/cloudtrail/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package cloudtrail -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -79,25 +82,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -136,6 +157,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/cloudwatch/router.go b/internal/generated/cloudwatch/router.go index f2fdc6e..7b69d71 100644 --- a/internal/generated/cloudwatch/router.go +++ b/internal/generated/cloudwatch/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package cloudwatch -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -65,25 +68,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -122,6 +143,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/cloudwatchlogs/router.go b/internal/generated/cloudwatchlogs/router.go index 00d0594..9d2141a 100644 --- a/internal/generated/cloudwatchlogs/router.go +++ b/internal/generated/cloudwatchlogs/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package cloudwatchlogs -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -132,25 +135,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -189,6 +210,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/codeartifact/router.go b/internal/generated/codeartifact/router.go index a034c57..f87416b 100644 --- a/internal/generated/codeartifact/router.go +++ b/internal/generated/codeartifact/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package codeartifact -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -67,25 +70,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -124,6 +145,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/codebuild/router.go b/internal/generated/codebuild/router.go index 6b3b4d3..a54ada0 100644 --- a/internal/generated/codebuild/router.go +++ b/internal/generated/codebuild/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package codebuild -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -78,25 +81,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -135,6 +156,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/codecommit/router.go b/internal/generated/codecommit/router.go index 2ddc205..859dc65 100644 --- a/internal/generated/codecommit/router.go +++ b/internal/generated/codecommit/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package codecommit -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -98,25 +101,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -155,6 +176,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/codedeploy/router.go b/internal/generated/codedeploy/router.go index 5669e62..8d4d8e5 100644 --- a/internal/generated/codedeploy/router.go +++ b/internal/generated/codedeploy/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package codedeploy -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -66,25 +69,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -123,6 +144,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/codepipeline/router.go b/internal/generated/codepipeline/router.go index 572b63e..ac55102 100644 --- a/internal/generated/codepipeline/router.go +++ b/internal/generated/codepipeline/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package codepipeline -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -63,25 +66,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -120,6 +141,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/cognitoidentity/router.go b/internal/generated/cognitoidentity/router.go index 285abf8..ac3cbdd 100644 --- a/internal/generated/cognitoidentity/router.go +++ b/internal/generated/cognitoidentity/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package cognitoidentity -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -42,25 +45,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -99,6 +120,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/cognitoidentityprovider/router.go b/internal/generated/cognitoidentityprovider/router.go index 1a90cdd..395d9fd 100644 --- a/internal/generated/cognitoidentityprovider/router.go +++ b/internal/generated/cognitoidentityprovider/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package cognitoidentityprovider -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -141,25 +144,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -198,6 +219,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/configservice/router.go b/internal/generated/configservice/router.go index 7a356f5..255d772 100644 --- a/internal/generated/configservice/router.go +++ b/internal/generated/configservice/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package configservice -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -116,25 +119,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -173,6 +194,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/costexplorer/router.go b/internal/generated/costexplorer/router.go index 6043e26..6d2973b 100644 --- a/internal/generated/costexplorer/router.go +++ b/internal/generated/costexplorer/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package costexplorer -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -66,25 +69,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -123,6 +144,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/docdb/router.go b/internal/generated/docdb/router.go index 1b25b0e..2a2b536 100644 --- a/internal/generated/docdb/router.go +++ b/internal/generated/docdb/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package docdb -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -74,25 +77,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -131,6 +152,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/dynamodb/router.go b/internal/generated/dynamodb/router.go index 7d3c18d..231c1c1 100644 --- a/internal/generated/dynamodb/router.go +++ b/internal/generated/dynamodb/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package dynamodb -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -76,25 +79,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -133,6 +154,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/dynamodbstreams/router.go b/internal/generated/dynamodbstreams/router.go index bb21e8c..ddd0c13 100644 --- a/internal/generated/dynamodbstreams/router.go +++ b/internal/generated/dynamodbstreams/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package dynamodbstreams -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -23,25 +26,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -80,6 +101,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/ebs/router.go b/internal/generated/ebs/router.go index e992674..57f1dc0 100644 --- a/internal/generated/ebs/router.go +++ b/internal/generated/ebs/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package ebs -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -25,25 +28,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -82,6 +103,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/ec2/router.go b/internal/generated/ec2/router.go index dd150f1..d1f6317 100644 --- a/internal/generated/ec2/router.go +++ b/internal/generated/ec2/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package ec2 -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -775,25 +778,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -832,6 +853,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/ecr/router.go b/internal/generated/ecr/router.go index 5e0ede6..13730ce 100644 --- a/internal/generated/ecr/router.go +++ b/internal/generated/ecr/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package ecr -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -77,25 +80,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -134,6 +155,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/ecs/router.go b/internal/generated/ecs/router.go index 839e5df..eeb46c9 100644 --- a/internal/generated/ecs/router.go +++ b/internal/generated/ecs/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package ecs -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -95,25 +98,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -152,6 +173,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/efs/router.go b/internal/generated/efs/router.go index f9d3367..6c3a442 100644 --- a/internal/generated/efs/router.go +++ b/internal/generated/efs/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package efs -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -50,25 +53,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -107,6 +128,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/eks/router.go b/internal/generated/eks/router.go index e94cc6b..4d4003f 100644 --- a/internal/generated/eks/router.go +++ b/internal/generated/eks/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package eks -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -83,25 +86,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -140,6 +161,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/elasticache/router.go b/internal/generated/elasticache/router.go index c3d0eb2..a227634 100644 --- a/internal/generated/elasticache/router.go +++ b/internal/generated/elasticache/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package elasticache -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -94,25 +97,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -151,6 +172,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/elasticbeanstalk/router.go b/internal/generated/elasticbeanstalk/router.go index 7e7dbd0..1cad386 100644 --- a/internal/generated/elasticbeanstalk/router.go +++ b/internal/generated/elasticbeanstalk/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package elasticbeanstalk -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -66,25 +69,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -123,6 +144,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/elasticloadbalancingv2/router.go b/internal/generated/elasticloadbalancingv2/router.go index b886f0b..8032286 100644 --- a/internal/generated/elasticloadbalancingv2/router.go +++ b/internal/generated/elasticloadbalancingv2/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package elasticloadbalancingv2 -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -70,25 +73,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -127,6 +148,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/elasticsearchservice/router.go b/internal/generated/elasticsearchservice/router.go index 6e5e359..1aea744 100644 --- a/internal/generated/elasticsearchservice/router.go +++ b/internal/generated/elasticsearchservice/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package elasticsearchservice -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -70,25 +73,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -127,6 +148,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/emr/router.go b/internal/generated/emr/router.go index da1403b..6d01d72 100644 --- a/internal/generated/emr/router.go +++ b/internal/generated/emr/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package emr -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -79,25 +82,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -136,6 +157,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/eventbridge/router.go b/internal/generated/eventbridge/router.go index eb3ac2f..3616563 100644 --- a/internal/generated/eventbridge/router.go +++ b/internal/generated/eventbridge/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package eventbridge -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -76,25 +79,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -133,6 +154,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/firehose/router.go b/internal/generated/firehose/router.go index 1e770be..274e125 100644 --- a/internal/generated/firehose/router.go +++ b/internal/generated/firehose/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package firehose -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -31,25 +34,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -88,6 +109,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/fis/router.go b/internal/generated/fis/router.go index b363584..d74d4ca 100644 --- a/internal/generated/fis/router.go +++ b/internal/generated/fis/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package fis -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -45,25 +48,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -102,6 +123,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/glacier/router.go b/internal/generated/glacier/router.go index 282bc46..869c218 100644 --- a/internal/generated/glacier/router.go +++ b/internal/generated/glacier/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package glacier -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -52,25 +55,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -109,6 +130,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/glue/router.go b/internal/generated/glue/router.go index 0eee9d1..34b279d 100644 --- a/internal/generated/glue/router.go +++ b/internal/generated/glue/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package glue -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -284,25 +287,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -341,6 +362,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/iam/router.go b/internal/generated/iam/router.go index 07d7ccd..65b3549 100644 --- a/internal/generated/iam/router.go +++ b/internal/generated/iam/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package iam -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -195,25 +198,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -252,6 +273,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/iot/router.go b/internal/generated/iot/router.go index c9c1d15..a148480 100644 --- a/internal/generated/iot/router.go +++ b/internal/generated/iot/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package iot -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -291,25 +294,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -348,6 +369,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/iotdataplane/router.go b/internal/generated/iotdataplane/router.go index c8dc79a..523df8b 100644 --- a/internal/generated/iotdataplane/router.go +++ b/internal/generated/iotdataplane/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package iotdataplane -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -27,25 +30,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -84,6 +105,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/iotwireless/router.go b/internal/generated/iotwireless/router.go index ab567cc..6ee6168 100644 --- a/internal/generated/iotwireless/router.go +++ b/internal/generated/iotwireless/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package iotwireless -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -131,25 +134,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -188,6 +209,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/kafka/router.go b/internal/generated/kafka/router.go index 13f0ce2..a2904c3 100644 --- a/internal/generated/kafka/router.go +++ b/internal/generated/kafka/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package kafka -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -78,25 +81,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -135,6 +156,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/kinesis/router.go b/internal/generated/kinesis/router.go index 572d214..42ded85 100644 --- a/internal/generated/kinesis/router.go +++ b/internal/generated/kinesis/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package kinesis -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -58,25 +61,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -115,6 +136,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/kinesisanalyticsv2/router.go b/internal/generated/kinesisanalyticsv2/router.go index 6c98ce4..2a2f4e2 100644 --- a/internal/generated/kinesisanalyticsv2/router.go +++ b/internal/generated/kinesisanalyticsv2/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package kinesisanalyticsv2 -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -52,25 +55,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -109,6 +130,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/kms/router.go b/internal/generated/kms/router.go index 6be6d0d..4f2a4e9 100644 --- a/internal/generated/kms/router.go +++ b/internal/generated/kms/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package kms -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -72,25 +75,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -129,6 +150,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/lakeformation/router.go b/internal/generated/lakeformation/router.go index 290db74..c0e009c 100644 --- a/internal/generated/lakeformation/router.go +++ b/internal/generated/lakeformation/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package lakeformation -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -80,25 +83,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -137,6 +158,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/lambda/router.go b/internal/generated/lambda/router.go index 80de47f..bcdb972 100644 --- a/internal/generated/lambda/router.go +++ b/internal/generated/lambda/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package lambda -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -104,25 +107,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -161,6 +182,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/managedblockchain/router.go b/internal/generated/managedblockchain/router.go index 2ebed06..62b7891 100644 --- a/internal/generated/managedblockchain/router.go +++ b/internal/generated/managedblockchain/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package managedblockchain -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -46,25 +49,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -103,6 +124,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/memorydb/router.go b/internal/generated/memorydb/router.go index 8712366..9ae1ebf 100644 --- a/internal/generated/memorydb/router.go +++ b/internal/generated/memorydb/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package memorydb -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -64,25 +67,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -121,6 +142,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/mq/router.go b/internal/generated/mq/router.go index 9e121ce..4720387 100644 --- a/internal/generated/mq/router.go +++ b/internal/generated/mq/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package mq -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -43,25 +46,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -100,6 +121,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/mwaa/router.go b/internal/generated/mwaa/router.go index 41ee524..72351c1 100644 --- a/internal/generated/mwaa/router.go +++ b/internal/generated/mwaa/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package mwaa -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -31,25 +34,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -88,6 +109,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/neptune/router.go b/internal/generated/neptune/router.go index dc60a57..499384d 100644 --- a/internal/generated/neptune/router.go +++ b/internal/generated/neptune/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package neptune -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -89,25 +92,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -146,6 +167,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/opensearch/router.go b/internal/generated/opensearch/router.go index 5c41ead..d95df00 100644 --- a/internal/generated/opensearch/router.go +++ b/internal/generated/opensearch/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package opensearch -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -106,25 +109,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -163,6 +184,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/organizations/router.go b/internal/generated/organizations/router.go index 228484a..7ac5674 100644 --- a/internal/generated/organizations/router.go +++ b/internal/generated/organizations/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package organizations -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -82,25 +85,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -139,6 +160,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/pinpoint/router.go b/internal/generated/pinpoint/router.go index 4b3de30..d2f1d86 100644 --- a/internal/generated/pinpoint/router.go +++ b/internal/generated/pinpoint/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package pinpoint -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -141,25 +144,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -198,6 +219,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/ram/router.go b/internal/generated/ram/router.go index 0758278..17fdfe2 100644 --- a/internal/generated/ram/router.go +++ b/internal/generated/ram/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package ram -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -54,25 +57,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -111,6 +132,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/rds/router.go b/internal/generated/rds/router.go index da0a52b..6bd85fa 100644 --- a/internal/generated/rds/router.go +++ b/internal/generated/rds/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package rds -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -182,25 +185,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -239,6 +260,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/redshift/router.go b/internal/generated/redshift/router.go index e6fa8ad..6361830 100644 --- a/internal/generated/redshift/router.go +++ b/internal/generated/redshift/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package redshift -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -160,25 +163,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -217,6 +238,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/resourcegroups/router.go b/internal/generated/resourcegroups/router.go index d9b71fb..a3ef894 100644 --- a/internal/generated/resourcegroups/router.go +++ b/internal/generated/resourcegroups/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package resourcegroups -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -42,25 +45,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -99,6 +120,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/resourcegroupstaggingapi/router.go b/internal/generated/resourcegroupstaggingapi/router.go index 7f51891..a20a0b8 100644 --- a/internal/generated/resourcegroupstaggingapi/router.go +++ b/internal/generated/resourcegroupstaggingapi/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package resourcegroupstaggingapi -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -28,25 +31,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -85,6 +106,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/route53/router.go b/internal/generated/route53/router.go index 4570525..0d33bac 100644 --- a/internal/generated/route53/router.go +++ b/internal/generated/route53/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package route53 -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -90,25 +93,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -147,6 +168,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/route53resolver/router.go b/internal/generated/route53resolver/router.go index 7b2c5af..012ac76 100644 --- a/internal/generated/route53resolver/router.go +++ b/internal/generated/route53resolver/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package route53resolver -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -87,25 +90,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -144,6 +165,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/s3/router.go b/internal/generated/s3/router.go index 6fb6e77..22c4dbc 100644 --- a/internal/generated/s3/router.go +++ b/internal/generated/s3/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package s3 -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -126,25 +129,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -183,6 +204,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/sagemaker/router.go b/internal/generated/sagemaker/router.go index 96f312e..b8d0326 100644 --- a/internal/generated/sagemaker/router.go +++ b/internal/generated/sagemaker/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package sagemaker -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -400,25 +403,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -457,6 +478,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/secretsmanager/router.go b/internal/generated/secretsmanager/router.go index 5e9147d..6a21c72 100644 --- a/internal/generated/secretsmanager/router.go +++ b/internal/generated/secretsmanager/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package secretsmanager -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -42,25 +45,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -99,6 +120,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/servicediscovery/router.go b/internal/generated/servicediscovery/router.go index 1f9b80b..55c3b9e 100644 --- a/internal/generated/servicediscovery/router.go +++ b/internal/generated/servicediscovery/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package servicediscovery -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -49,25 +52,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -106,6 +127,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/ses/router.go b/internal/generated/ses/router.go index 11e2d35..995d953 100644 --- a/internal/generated/ses/router.go +++ b/internal/generated/ses/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package ses -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -90,25 +93,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -147,6 +168,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/sesv2/router.go b/internal/generated/sesv2/router.go index a343814..a9de8d0 100644 --- a/internal/generated/sesv2/router.go +++ b/internal/generated/sesv2/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package sesv2 -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -129,25 +132,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -186,6 +207,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/sfn/router.go b/internal/generated/sfn/router.go index 097dd64..afcdc07 100644 --- a/internal/generated/sfn/router.go +++ b/internal/generated/sfn/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package sfn -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -56,25 +59,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -113,6 +134,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/shield/router.go b/internal/generated/shield/router.go index 3f3a1f9..016db79 100644 --- a/internal/generated/shield/router.go +++ b/internal/generated/shield/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package shield -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -55,25 +58,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -112,6 +133,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/sns/router.go b/internal/generated/sns/router.go index 216fbc0..538ff78 100644 --- a/internal/generated/sns/router.go +++ b/internal/generated/sns/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package sns -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -61,25 +64,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -118,6 +139,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/sqs/router.go b/internal/generated/sqs/router.go index 946d3c0..10845b1 100644 --- a/internal/generated/sqs/router.go +++ b/internal/generated/sqs/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package sqs -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -42,25 +45,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -99,6 +120,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/ssm/router.go b/internal/generated/ssm/router.go index 09cb92f..95fdcaf 100644 --- a/internal/generated/ssm/router.go +++ b/internal/generated/ssm/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package ssm -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -165,25 +168,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -222,6 +243,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/ssoadmin/router.go b/internal/generated/ssoadmin/router.go index 382b5a1..06534ff 100644 --- a/internal/generated/ssoadmin/router.go +++ b/internal/generated/ssoadmin/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package ssoadmin -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -98,25 +101,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -155,6 +176,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/support/router.go b/internal/generated/support/router.go index 9e11430..798fd37 100644 --- a/internal/generated/support/router.go +++ b/internal/generated/support/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package support -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -35,25 +38,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -92,6 +113,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/swf/router.go b/internal/generated/swf/router.go index 2d90edd..e23e1cc 100644 --- a/internal/generated/swf/router.go +++ b/internal/generated/swf/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package swf -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -58,25 +61,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -115,6 +136,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/textract/router.go b/internal/generated/textract/router.go index 7d5da26..4871026 100644 --- a/internal/generated/textract/router.go +++ b/internal/generated/textract/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package textract -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -44,25 +47,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -101,6 +122,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/timestreamwrite/router.go b/internal/generated/timestreamwrite/router.go index b9aacb4..7f506ea 100644 --- a/internal/generated/timestreamwrite/router.go +++ b/internal/generated/timestreamwrite/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package timestreamwrite -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -38,25 +41,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -95,6 +116,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/transcribe/router.go b/internal/generated/transcribe/router.go index f724477..8616414 100644 --- a/internal/generated/transcribe/router.go +++ b/internal/generated/transcribe/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package transcribe -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -62,25 +65,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -119,6 +140,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/transfer/router.go b/internal/generated/transfer/router.go index 500f49c..3b4c5b6 100644 --- a/internal/generated/transfer/router.go +++ b/internal/generated/transfer/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package transfer -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -90,25 +93,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -147,6 +168,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/waf/router.go b/internal/generated/waf/router.go index ee19c30..e5155e7 100644 --- a/internal/generated/waf/router.go +++ b/internal/generated/waf/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package waf -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -96,25 +99,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -153,6 +174,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/wafv2/router.go b/internal/generated/wafv2/router.go index adb17d7..a6e98f0 100644 --- a/internal/generated/wafv2/router.go +++ b/internal/generated/wafv2/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package wafv2 -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -74,25 +77,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -131,6 +152,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/generated/xray/router.go b/internal/generated/xray/router.go index 2793926..cf6425f 100644 --- a/internal/generated/xray/router.go +++ b/internal/generated/xray/router.go @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 package xray -import "strings" +import ( + "net/url" + "strings" +) // PathParams holds the URL path parameters extracted by the router. type PathParams map[string]string @@ -57,25 +60,43 @@ var OperationRoutes = []OperationRoute{ } // MatchOperation returns the operation name and extracted path parameters for -// the given HTTP method and URI path. It returns ("", nil) if no route matches. -func MatchOperation(method, path string) (string, PathParams) { - for _, route := range OperationRoutes { - if route.Method != method { - continue - } - if params, ok := matchURI(route.Pattern, path); ok { - return route.Operation, params +// the given HTTP method and request URI. It returns ("", nil) if no route +// matches. +// +// Pass req.URL.RequestURI(), not req.URL.Path: some AWS operations are +// distinguished only by a query string (".../tagging?Operation=Tag"), and a +// bare path can never satisfy those routes. +func MatchOperation(method, uri string) (string, PathParams) { + // Two passes. A route that constrains the query is more specific than one + // that does not, and OperationRoutes is ordered by operation name rather + // than specificity — so trying constrained routes first keeps + // ".../distribution" from shadowing ".../distribution?WithTags". + for _, constrained := range []bool{true, false} { + for _, route := range OperationRoutes { + if route.Method != method || strings.Contains(route.Pattern, "?") != constrained { + continue + } + if params, ok := matchURI(route.Pattern, uri); ok { + return route.Operation, params + } } } return "", nil } -// matchURI matches a URI path against a pattern that may contain {Label} and -// {Label+} (greedy) segments. Returns the extracted parameters and true on -// match, or nil and false otherwise. -func matchURI(pattern, path string) (PathParams, bool) { - patParts := splitPath(pattern) - pathParts := splitPath(path) +// matchURI matches a request URI against a pattern that may contain {Label} and +// {Label+} (greedy) segments, and may constrain the query string after a "?". +// Returns the extracted parameters and true on match, or nil and false +// otherwise. +func matchURI(pattern, uri string) (PathParams, bool) { + patPath, patQuery, _ := strings.Cut(pattern, "?") + uriPath, uriQuery, _ := strings.Cut(uri, "?") + if !matchQuery(patQuery, uriQuery) { + return nil, false + } + + patParts := splitPath(patPath) + pathParts := splitPath(uriPath) params := PathParams{} pi := 0 @@ -114,6 +135,44 @@ func matchURI(pattern, path string) (PathParams, bool) { return params, true } +// matchQuery reports whether a request's query string satisfies the constraint +// a route pattern carries after its "?". Each "&"-separated term is either a +// bare key that must be present ("?WithTags") or a key=value that must be +// present with that value ("?Operation=Tag"). An empty constraint matches +// anything, which is every route that has no "?". +func matchQuery(constraint, query string) bool { + if constraint == "" { + return true + } + // ParseQuery returns what it could parse alongside any error, so a + // malformed tail cannot hide a valid term earlier in the string. + values, _ := url.ParseQuery(query) + for _, term := range strings.Split(constraint, "&") { + if term == "" { + continue + } + key, want, hasValue := strings.Cut(term, "=") + got, present := values[key] + if !present { + return false + } + if !hasValue { + continue + } + matched := false + for _, v := range got { + if v == want { + matched = true + break + } + } + if !matched { + return false + } + } + return true +} + // splitPath splits a URI path into non-empty segments. func splitPath(path string) []string { parts := strings.Split(strings.Trim(path, "/"), "/") diff --git a/internal/services/cloudfront/provider.go b/internal/services/cloudfront/provider.go index 5ce683d..724fd9a 100644 --- a/internal/services/cloudfront/provider.go +++ b/internal/services/cloudfront/provider.go @@ -49,7 +49,9 @@ func (p *Provider) Shutdown(_ context.Context) error { func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request) (*plugin.Response, error) { if op == "" { var params generated.PathParams - op, params = generated.MatchOperation(req.Method, req.URL.Path) + // RequestURI, not Path: TagResource and UntagResource share + // POST /2020-05-31/tagging and are told apart only by ?Operation=. + op, params = generated.MatchOperation(req.Method, req.URL.RequestURI()) _ = params } switch op { diff --git a/internal/services/cloudfront/provider_test.go b/internal/services/cloudfront/provider_test.go index 3e7912f..252ffec 100644 --- a/internal/services/cloudfront/provider_test.go +++ b/internal/services/cloudfront/provider_test.go @@ -36,7 +36,7 @@ func doRequest(t *testing.T, p *Provider, method, path, body string) *plugin.Res bodyReader = bytes.NewBuffer(nil) } req := httptest.NewRequest(method, path, bodyReader) - op, _ := generated.MatchOperation(method, req.URL.Path) + op, _ := generated.MatchOperation(method, req.URL.RequestURI()) resp, err := p.HandleRequest(context.Background(), op, req) require.NoError(t, err) return resp @@ -381,15 +381,15 @@ func TestTags(t *testing.T) { teamplatform ` - req = httptest.NewRequest(http.MethodPost, "/2020-05-31/tagging?Operation=Tag&Resource="+arn, strings.NewReader(tagBody)) - resp, err = p.HandleRequest(context.Background(), "TagResource", req) - require.NoError(t, err) + // Routed through MatchOperation rather than hand-dispatched. Passing + // "TagResource" straight to HandleRequest is what hid the fact that no + // route could produce that name, leaving the handler unreachable in + // production while this test stayed green. + resp = doRequest(t, p, http.MethodPost, "/2020-05-31/tagging?Operation=Tag&Resource="+arn, tagBody) assert.Equal(t, http.StatusNoContent, resp.StatusCode) // ListTagsForResource - req = httptest.NewRequest(http.MethodGet, "/2020-05-31/tagging?Resource="+arn, nil) - resp, err = p.HandleRequest(context.Background(), "ListTagsForResource", req) - require.NoError(t, err) + resp = doRequest(t, p, http.MethodGet, "/2020-05-31/tagging?Resource="+arn, "") assert.Equal(t, http.StatusOK, resp.StatusCode) body := string(resp.Body) assert.Contains(t, body, "env") @@ -402,21 +402,39 @@ func TestTags(t *testing.T) { team ` - req = httptest.NewRequest(http.MethodPost, "/2020-05-31/tagging?Operation=Untag&Resource="+arn, strings.NewReader(untagBody)) - resp, err = p.HandleRequest(context.Background(), "UntagResource", req) - require.NoError(t, err) + resp = doRequest(t, p, http.MethodPost, "/2020-05-31/tagging?Operation=Untag&Resource="+arn, untagBody) assert.Equal(t, http.StatusNoContent, resp.StatusCode) // ListTagsForResource again - "team" should be gone - req = httptest.NewRequest(http.MethodGet, "/2020-05-31/tagging?Resource="+arn, nil) - resp, err = p.HandleRequest(context.Background(), "ListTagsForResource", req) - require.NoError(t, err) + resp = doRequest(t, p, http.MethodGet, "/2020-05-31/tagging?Resource="+arn, "") assert.Equal(t, http.StatusOK, resp.StatusCode) body = string(resp.Body) assert.Contains(t, body, "env") assert.NotContains(t, body, "team") } +// TestMatchOperationHonoursQueryConstraints guards the routing this provider +// depends on. Four CloudFront routes are told apart only by their query string, +// and matchURI used to split the whole pattern into path segments — so +// "tagging?Operation=Tag" could never equal the path segment "tagging" and both +// tag operations were unreachable no matter what the dispatch switch did. +func TestMatchOperationHonoursQueryConstraints(t *testing.T) { + for _, tc := range []struct{ method, uri, want string }{ + {http.MethodPost, "/2020-05-31/tagging?Operation=Tag&Resource=arn:aws:cloudfront::0:distribution/X", "TagResource"}, + {http.MethodPost, "/2020-05-31/tagging?Operation=Untag&Resource=arn:aws:cloudfront::0:distribution/X", "UntagResource"}, + {http.MethodGet, "/2020-05-31/tagging?Resource=arn:aws:cloudfront::0:distribution/X", "ListTagsForResource"}, + // A bare key constraint, and the unconstrained route it must not shadow. + {http.MethodPost, "/2020-05-31/distribution?WithTags", "CreateDistributionWithTags"}, + {http.MethodPost, "/2020-05-31/distribution", "CreateDistribution"}, + // A query the constraint does not accept must not match either tag route. + {http.MethodPost, "/2020-05-31/tagging?Operation=Sideways", ""}, + } { + if got, _ := generated.MatchOperation(tc.method, tc.uri); got != tc.want { + t.Errorf("%s %s: routed to %q, want %q", tc.method, tc.uri, got, tc.want) + } + } +} + // TestDefaultOperations pins the one thing docs/compatibility-policy.md // guarantees about an unimplemented operation: it fails. This provider used to // answer 200 with an empty XML document, which boto3 parses as a successful diff --git a/internal/services/efs/provider.go b/internal/services/efs/provider.go index 798a595..4a604d8 100644 --- a/internal/services/efs/provider.go +++ b/internal/services/efs/provider.go @@ -52,7 +52,7 @@ func (p *Provider) Shutdown(_ context.Context) error { func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request) (*plugin.Response, error) { if op == "" { var params generated.PathParams - op, params = generated.MatchOperation(req.Method, req.URL.Path) + op, params = generated.MatchOperation(req.Method, req.URL.RequestURI()) _ = params } @@ -66,7 +66,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request } // Extract path params - _, pathParams := generated.MatchOperation(req.Method, req.URL.Path) + _, pathParams := generated.MatchOperation(req.Method, req.URL.RequestURI()) q := req.URL.Query() switch op { diff --git a/internal/services/route53/provider.go b/internal/services/route53/provider.go index ee6ebd5..c7dc09e 100644 --- a/internal/services/route53/provider.go +++ b/internal/services/route53/provider.go @@ -52,7 +52,7 @@ func (p *Provider) HandleRequest(_ context.Context, op string, req *http.Request // If the router passes an empty op, resolve it ourselves. if op == "" { var params generated.PathParams - op, params = generated.MatchOperation(req.Method, req.URL.Path) + op, params = generated.MatchOperation(req.Method, req.URL.RequestURI()) _ = params } switch op {