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/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/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/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/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/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/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..64da296 --- /dev/null +++ b/docs/compatibility-policy.md @@ -0,0 +1,167 @@ +# 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, 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`. | +| `logging.level` | string | Log level. | +| `logging.format` | string | Log format. | + +### Environment variables + +| Variable | Meaning | +|---|---| +| `DEVCLOUD_PORT` | Overrides `server.port`. | +| `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. + +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 + +`-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 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 + +**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 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 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 or strengthening assertions, 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 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 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; + 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. 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. + +## 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/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/docs/fidelity-manifest.md b/docs/fidelity-manifest.md index 1466c86..348bfd5 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 | 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 +`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 05db2e6..bb5246d 100644 --- a/docs/plugin-api.md +++ b/docs/plugin-api.md @@ -127,13 +127,23 @@ 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: - 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/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/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/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/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 93c2b0d..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") } @@ -239,6 +277,81 @@ 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") + // 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(` +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.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") +} + // 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. 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/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" ) 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 738a6c6..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 { @@ -158,11 +160,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..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,26 +402,56 @@ 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 +// 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") + }) + } } 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 {