From dd59c43d50c0989f8ece6da6b591189d381bf153 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Thu, 24 Sep 2026 15:09:48 -0400 Subject: [PATCH] Add mapbox feedback list/get MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fifth API this session with no prior CLI coverage — completes the naming doc's V1 scope (directions, isochrone, map matching, matrix, feedback). Hand-authored into custom-openapi/ since openapi-specs has no spec for this API either. feedback create, the write side, is declared in the spec but never reaches the command surface: confirmed directly against production that a POST /oauth/register requesting user-feedback:write silently drops it from the granted scope, the same unregistrable shape UNSUPPORTED_OPERATIONS already documents for accounts create-token/update-token/delete-token and styles download-style-zip. No profile path parameter here, so no new ARG_NAME_OVERRIDES row — this service was never at risk of that collision. Named the disabled operation createFeedbackItem rather than the shorter create: the withheld-operation guard in generate_skills.rs checks the bare command word against every generated file's text, and a bare "create" collides with the exposed, unrelated `styles create`. Since the operation is never reachable either way, the internal name doesn't affect anything user-facing. Also fixes a --help truncation on all six *_before/*_after time-window parameters ("ISO 8601." as a leading sentence, same class of bug fixed in directions.yaml and matrix.yaml earlier) by moving the format note to the end of each description. Updated docs/commands.md's own stale operation/group counts while here — 37 operations across 13 groups, not the 33/10 it still said (already stale by 2 before this branch, from drift this change didn't cause but was already touching the same sentence). Smoke-tested against production: list with comma-separated --status/ --category filters, pagination cursor, and get by id, all verified to return the documented shape. Confirmed create is absent from --help and errors as an unrecognized subcommand rather than reaching the API. 488 tests, fmt and clippy clean. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 10 + README.md | 1 + custom-openapi/feedback/openapi/feedback.yaml | 223 ++++++++++++++++++ docs/commands.md | 164 ++++++++++++- src/main.rs | 15 +- src/remedy.rs | 1 + src/spec.rs | 46 ++++ tests/fixtures/api_command_surface.txt | 2 + 8 files changed, 448 insertions(+), 14 deletions(-) create mode 100644 custom-openapi/feedback/openapi/feedback.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd34b9..2a9cab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,16 @@ that may never merge. They are not releases and are not listed here. ## Unreleased +- `mapbox feedback list`/`get`, reading feedback submitted against Mapbox + API responses — filterable, sortable, paginated. Hand-authored into + `custom-openapi/` for the same reason the Navigation commands were: no + upstream spec exists yet. `feedback create`, the write side, is not a + command — confirmed directly against production that `user-feedback:write` + is silently dropped from a `POST /oauth/register` grant, the same + unregistrable shape `accounts create-token` and `styles + download-style-zip` already document, so no `mapbox auth login` token can + ever carry it. + - `mapbox matrix`, travel time and/or distance between every pair in a set of up to 25 coordinates in one call, for driving (with or without live traffic), walking, or cycling. No subcommand: like `mapbox directions` diff --git a/README.md b/README.md index 71f5001..76afc9d 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ Each API is a top-level subcommand, one sub-subcommand per operation: ```sh mapbox accounts * +mapbox feedback * mapbox fonts * mapbox geocoder * mapbox search * diff --git a/custom-openapi/feedback/openapi/feedback.yaml b/custom-openapi/feedback/openapi/feedback.yaml new file mode 100644 index 0000000..64dcc57 --- /dev/null +++ b/custom-openapi/feedback/openapi/feedback.yaml @@ -0,0 +1,223 @@ +openapi: "3.0.0" +# `parse_spec` turns `info.description` below into this service's clap +# `long_about`, so it also reaches `mapbox feedback --help`, `--schema` and +# `generate-skills` output. Keep it to API prose only — the provenance below +# is for whoever edits this file, not for a CLI user: +# +# Hand-authored down to the parameters documented at +# docs.mapbox.com/api/feedback. See `custom-openapi/README.md` for how a +# file like this is wired in. `createFeedbackItem` is declared here but +# never reaches the command surface: it needs `user-feedback:write`, which +# `POST /oauth/register` silently drops from the granted scope — confirmed +# directly against production (`curl -X POST +# https://api.mapbox.com/oauth/register?scope=user-feedback:write ...` +# returns a registration with that scope missing from the response, the +# same shape `tokens:write` and `styles:download` already document in +# `src/spec.rs`'s `UNSUPPORTED_OPERATIONS`). No `mapbox auth login` token +# can ever carry it, so `feedback create` is not a command today. +info: + title: "Mapbox Feedback API" + description: >- + Feedback submitted against Mapbox API responses — geocoding, search, + directions and the rest — filterable, sortable, and paginated. + version: "0.0.0" +servers: + - url: https://api.mapbox.com + description: Feedback API +paths: + /user-feedback/v1/feedback: + get: + operationId: list + summary: List feedback items. + description: >- + Every feedback item on the account, newest received first by + default — filterable by id, status, category, a free-text search + phrase, trace id, or a time window on when it was created, + received, or last updated. Paginated with `--after`/`start_cursor`/ + `end_cursor` the way every other listing on this CLI is. + parameters: + - name: "access_token" + in: query + required: true + description: "Mapbox API Access Token" + schema: + type: string + minLength: 1 + # Prose rather than an `enum`: several of these are comma-separated + # lists, and the command builder turns a spec `enum` into a clap + # `PossibleValuesParser`, which accepts one value and would refuse + # a list. Same reasoning as `directions.yaml`'s `annotations`. + - name: "feedback_id" + in: query + required: false + description: "One or more feedback ids to filter by, comma-separated." + schema: + type: string + - name: "after" + in: query + required: false + description: "A cursor from a previous response's `end_cursor`, to page forward." + schema: + type: string + - name: "limit" + in: query + required: false + description: "Maximum items to return, up to 1000." + schema: + type: integer + minimum: 1 + maximum: 1000 + - name: "sort_by" + in: query + required: false + description: >- + Which timestamp to sort by. Defaults to `received_at`. + schema: + type: string + enum: ["received_at", "created_at", "updated_at"] + - name: "order" + in: query + required: false + description: "Sort direction. Defaults to `asc`." + schema: + type: string + enum: ["asc", "desc"] + - name: "status" + in: query + required: false + description: >- + Limit to one or more statuses, comma-separated. Options are + `received`, `fixed`, `reviewed`, `out_of_scope`. + schema: + type: string + example: "received,reviewed" + - name: "category" + in: query + required: false + description: >- + Limit to one or more feedback categories, comma-separated — + categories are account-specific, so there is no fixed list here. + schema: + type: string + - name: "search" + in: query + required: false + description: "A phrase to match against feedback text." + schema: + type: string + - name: "trace_id" + in: query + required: false + description: >- + Limit to one or more caller-provided trace ids, comma-separated. + schema: + type: string + # "ISO 8601" trails each of these six rather than leads them: + # `first_sentence` in `src/main.rs` cuts a `--help` line at the + # first `.`, and "ISO 8601." on its own left `--help` showing just + # that. `--schema` and `docs/commands.md` still show each in full. + - name: "created_before" + in: query + required: false + description: "Only items the caller created before this time, ISO 8601." + schema: + type: string + - name: "created_after" + in: query + required: false + description: "Only items the caller created after this time, ISO 8601." + schema: + type: string + - name: "received_before" + in: query + required: false + description: "Only items Mapbox received before this time, ISO 8601." + schema: + type: string + - name: "received_after" + in: query + required: false + description: "Only items Mapbox received after this time, ISO 8601." + schema: + type: string + - name: "updated_before" + in: query + required: false + description: "Only items last updated before this time, ISO 8601." + schema: + type: string + - name: "updated_after" + in: query + required: false + description: "Only items last updated after this time, ISO 8601." + schema: + type: string + responses: + "200": + description: >- + A JSON object with an `items` array (each a feedback item: `id`, + `status`, `category`, `feedback`, `location`, timestamps), plus + `has_after`/`end_cursor` and `has_before`/`start_cursor` for + paging either direction. + "401": + description: Unauthorized + "403": + description: Forbidden + + post: + # Not `create`: this operation is never reachable (see below), so the + # user-facing name doesn't matter — but the withheld-operation guard + # in `generate_skills.rs` checks the bare command word against every + # generated file's text, and a bare `create` collides with the + # exposed, unrelated `styles create`. A multi-word operationId, the + # same shape every other disabled operation already has + # (`createToken`, `downloadStyleZip`, …), sidesteps that. + operationId: createFeedbackItem + summary: Submit a new feedback item. + description: >- + Not a command — see this file's own header comment for why. + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + "201": + description: The created feedback item. + "401": + description: Unauthorized + "403": + description: Forbidden + + /user-feedback/v1/feedback/{feedback_id}: + get: + operationId: get + summary: Retrieve one feedback item by id. + parameters: + - name: "feedback_id" + in: path + required: true + description: "The feedback item's id." + schema: + type: string + minLength: 1 + - name: "access_token" + in: query + required: true + description: "Mapbox API Access Token" + schema: + type: string + minLength: 1 + responses: + "200": + description: >- + The feedback item: `id`, `status`, `category`, `feedback`, + `location` (`place_name`, `lon`, `lat`), and its + created/received/updated timestamps. + "401": + description: Unauthorized + "403": + description: Forbidden + "404": + description: Not Found — no feedback item with that id. diff --git a/docs/commands.md b/docs/commands.md index d8f240e..727d60f 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,6 +1,6 @@ # Implemented commands -Every command the CLI ships: four auth commands, 33 API operations across 10 +Every command the CLI ships: four auth commands, 37 API operations across 13 command groups, the tilesets-cli proxy, `completion` and `generate-skills`. Each is shown in both of its renderings. Which one you get is decided by `--output`, whose default (`auto`) reads stdout: a terminal gets the left column, a pipe or redirect @@ -10,13 +10,16 @@ gets the right one. See Account names, style ids and tokens in the examples are replaced; everything else is as the API sent it. -**29 of the 33 were run against the live API and show what came back:** 26 -on 2026-09-01, and `fonts list`, `fonts upload` and `fonts delete` on -2026-09-08, once `fonts:list`/`fonts:write` became -registrable. The write operations were exercised as round trips on -throwaway objects — a style created, updated, drafted and deleted; icons -uploaded to a sprite and taken out again; a font uploaded and deleted — -leaving the account as it was found. +**33 of the 37 were run against the live API and show what came back:** +`directions`, `isochrone`, `map-matching`, `matrix`, `feedback list` and +`feedback get` on 2026-09-24, once those +command groups existed at all, and the rest earlier — `fonts list`, +`fonts upload` and `fonts delete` on 2026-09-08 once +`fonts:list`/`fonts:write` became registrable, the remainder before that. +The write operations were exercised as round trips on throwaway objects — +a style created, updated, drafted and deleted; icons uploaded to a sprite +and taken out again; a font uploaded and deleted — leaving the account as +it was found. Every API command's **Outputs** block below is that snapshot rather than a live reading, and is re-taken by hand — nothing schedules it and nothing @@ -73,6 +76,9 @@ nests, and is typed `mapbox styles draft get`. **[Directions](#directions)** — [directions](#mapbox-directions) +**[Feedback](#feedback)** — [feedback.list](#mapbox-feedback-list) · +[feedback.get](#mapbox-feedback-get) + **[Fonts](#fonts)** — [fonts.list](#mapbox-fonts-list) · [fonts.upload](#mapbox-fonts-upload) · [fonts.delete](#mapbox-fonts-delete) @@ -939,6 +945,148 @@ Both trimmed to one leg for length — the real response also carries `admins` (administrative boundaries traversed) and `notifications` (three tunnel alerts, on this particular route) per leg. +--- +## Feedback + +Feedback submitted against Mapbox API responses — geocoding, search, +directions and the rest — filterable, sortable, and paginated. Curated by +hand down to the parameters documented at docs.mapbox.com/api/feedback — +see `custom-openapi/README.md` for why this command group doesn't come from +the vendored specs the way most others do. + +**`feedback create`, the write side of this API, is not a command.** It +needs a `user-feedback:write` scope that `POST /oauth/register` silently +drops from the granted set — confirmed directly against production, the +same shape `accounts create-token` and `styles download-style-zip` already +document. No `mapbox auth login` token can ever carry it. + +### `mapbox feedback list` + +Every feedback item on the account, newest received first by default. + +#### Parameters + +| Parameter | Effect | +| --- | --- | +| `--feedback-id ` | One or more feedback ids, comma-separated. | +| `--after ` | Page forward from a previous response's `end_cursor`. | +| `--limit ` | Maximum items to return, up to 1000. | +| `--sort-by ` | Which timestamp to sort by. Defaults to `received_at`. | +| `--order ` | Sort direction. Defaults to `asc`. | +| `--status ` | One or more of `received`, `fixed`, `reviewed`, `out_of_scope`, comma-separated. | +| `--category ` | One or more feedback categories, comma-separated — account-specific, no fixed list. | +| `--search ` | A phrase to match against feedback text. | +| `--trace-id ` | One or more caller-provided trace ids, comma-separated. | +| `--created-before` / `--created-after ` | Window on when the caller created the item. | +| `--received-before` / `--received-after ` | Window on when Mapbox received it. | +| `--updated-before` / `--updated-after ` | Window on when it was last updated. | + +#### Examples + +```sh +mapbox feedback list --limit 5 +mapbox feedback list --status received --category positioning_issue +``` + +#### Outputs + +Captured live, two items: + + + + +
Terminal — -o textAgent — -o json
+ +```json +{ + "items": [ + { + "id": "01a06d61-17e4-74aa-b824-13baaf272670", + "status": "received", + "category": "positioning_issue", + "feedback": "This is a test feedback. …", + "location": { "lat": 0, "lon": 0 }, + "received_at": "2026-09-04T17:04:34.818Z" + }, + { + "id": "01a06d61-77cc-7649-8db6-5beb2de0278d", + "status": "received", + "category": "application_issue", + "feedback": "This is a test feedback. …", + "location": { + "lat": 37.779238, + "lon": -122.419359, + "place_name": "400 Van Ness Avenue, San Francisco, California 94103, United States" + }, + "received_at": "2026-09-04T17:04:59.466Z" + } + ], + "has_after": true, + "has_before": false, + "start_cursor": "…", + "end_cursor": "…" +} +``` + + + +```json +{"items":[{"id":"01a06d61-17e4-74aa-b824-13baaf272670","status":"received","category":"positioning_issue","feedback":"This is a test feedback. …","location":{"lat":0,"lon":0},"received_at":"2026-09-04T17:04:34.818Z"},{"id":"01a06d61-77cc-7649-8db6-5beb2de0278d","status":"received","category":"application_issue","feedback":"This is a test feedback. …","location":{"lat":37.779238,"lon":-122.419359,"place_name":"400 Van Ness Avenue, San Francisco, California 94103, United States"},"received_at":"2026-09-04T17:04:59.466Z"}],"has_after":true,"has_before":false,"start_cursor":"…","end_cursor":"…"} +``` + +
+ +Neither output mode has a bespoke rendering for this response — it isn't +GeoJSON — so both print the same JSON, `-o text` pretty-printed and `-o +json` on one line. Feedback text trimmed and `created_at`/`updated_at`/ +`has_screenshot` dropped per item, for length; the real response carries +them too. + +### `mapbox feedback get` + +One feedback item by id. + +#### Parameters + +`` (positional) is required. + +#### Examples + +```sh +mapbox feedback get 01a06d61-17e4-74aa-b824-13baaf272670 +``` + +#### Outputs + +Captured live, the same item `list` returned above — a single object this +time, not wrapped in `items`: + + + + +
Terminal — -o textAgent — -o json
+ +```json +{ + "id": "01a06d61-17e4-74aa-b824-13baaf272670", + "status": "received", + "category": "positioning_issue", + "feedback": "This is a test feedback. …", + "location": { "lat": 0, "lon": 0 }, + "received_at": "2026-09-04T17:04:34.818Z" +} +``` + + + +```json +{"id":"01a06d61-17e4-74aa-b824-13baaf272670","status":"received","category":"positioning_issue","feedback":"This is a test feedback. …","location":{"lat":0,"lon":0},"received_at":"2026-09-04T17:04:34.818Z"} +``` + +
+ +Same trimming as `list` above. + --- ## Fonts diff --git a/src/main.rs b/src/main.rs index 1325e49..814bb51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1675,12 +1675,15 @@ mod tests { .filter(|op| op.disabled_scope.is_some()) .map(|op| op.command()) .collect(); - // Every current UNSUPPORTED_OPERATIONS entry is also `disabled` (or - // `tbd`, which strips the same way) in the maintainer-only decision - // record, so this is legitimately empty now rather than a broken guard — the - // operations are absent from the bundled specs, not merely filtered - // here. Nothing left to assert on `disabled` itself; the loop below - // still holds for whatever, if anything, shows up. + // Every current UNSUPPORTED_OPERATIONS entry sourced from + // `MAPBOX_SPEC_ENTRIES` is also `disabled` (or `tbd`, which strips + // the same way) in the maintainer-only decision record, so those + // are absent from the bundled specs rather than merely filtered + // here. A custom spec has no such record to strip it at the + // source — `feedback.yaml`'s `createFeedback` is UNSUPPORTED_OPERATIONS' + // first entry that actually reaches this list non-empty — so the + // loop below is doing real work for it, not just standing guard + // over an empty case. let app = build_app(&specs); for (path, _) in leaf_commands(&app, &[]) { diff --git a/src/remedy.rs b/src/remedy.rs index 4b979b7..275186a 100644 --- a/src/remedy.rs +++ b/src/remedy.rs @@ -79,6 +79,7 @@ const SERVICE_DOCS: &[(&str, &str)] = &[ "directions", "https://docs.mapbox.com/api/navigation/directions/", ), + ("feedback", "https://docs.mapbox.com/api/feedback/"), ("fonts", "https://docs.mapbox.com/api/maps/fonts/"), ( "geocoder", diff --git a/src/spec.rs b/src/spec.rs index d09c1c2..c0655df 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -605,6 +605,14 @@ const UNSUPPORTED_OPERATIONS: &[(&str, &str, &str)] = &[ // itself, which is why it wasn't registered alongside the other two // fonts scopes. ("styles", "downloadStyleZip", "styles:download"), + // Confirmed 2026-09-24 with a direct POST /oauth/register against + // production requesting `user-feedback:write` alongside two scopes + // already known registrable — the response's granted `scope` carried + // the other two and silently dropped this one, the same shape + // `tokens:write` and `styles:download` above already document. + // `user-feedback:read` (list/get) is unaffected and already in + // `DEFAULT_SCOPES_LIST`. + ("feedback", "createFeedbackItem", "user-feedback:write"), ]; fn unsupported_scope_for(service_name: &str, operation_id: &str) -> Option<&'static str> { @@ -730,6 +738,10 @@ pub const CUSTOM_SPEC_ENTRIES: &[SpecEntry] = &[ name: "matrix", yaml: include_str!("../custom-openapi/matrix/openapi/matrix.yaml"), }, + SpecEntry { + name: "feedback", + yaml: include_str!("../custom-openapi/feedback/openapi/feedback.yaml"), + }, ]; /// The list the CLI actually generates commands from: [`MAPBOX_SPEC_ENTRIES`], @@ -2279,6 +2291,40 @@ paths: assert_eq!(compute.command(), "matrix"); } + /// `createFeedbackItem` needs `user-feedback:write`, confirmed + /// unregistrable via a direct `POST /oauth/register` against + /// production — see `UNSUPPORTED_OPERATIONS`'s own comment for that. + /// `list` and `get` need only `user-feedback:read`, already in + /// `DEFAULT_SCOPES_LIST`, so they must stay reachable. + #[test] + fn feedback_create_is_unreachable_but_list_and_get_are_not() { + let spec = parse_spec( + "feedback", + include_str!("../custom-openapi/feedback/openapi/feedback.yaml"), + ) + .expect("feedback.yaml parses"); + + let create = spec + .operations + .iter() + .find(|op| op.command_path == ["create-feedback-item"]) + .expect("the create-feedback-item operation exists in the spec"); + assert!( + create.disabled_scope.is_some(), + "createFeedbackItem must be disabled — user-feedback:write isn't registrable" + ); + assert!(!create.is_exposed()); + + for path in [["list"], ["get"]] { + let op = spec + .operations + .iter() + .find(|op| op.command_path == path) + .unwrap_or_else(|| panic!("the {path:?} operation exists in the spec")); + assert!(op.is_exposed(), "{path:?} needs only user-feedback:read"); + } + } + #[test] fn arg_name_override_only_fires_for_the_row_it_names() { assert_eq!( diff --git a/tests/fixtures/api_command_surface.txt b/tests/fixtures/api_command_surface.txt index f16f224..8d5e344 100644 --- a/tests/fixtures/api_command_surface.txt +++ b/tests/fixtures/api_command_surface.txt @@ -2,6 +2,8 @@ mapbox accounts list-scopes | aliases: (none) mapbox accounts list-tokens | aliases: (none) mapbox accounts retrieve-token | aliases: (none) mapbox directions | aliases: (none) +mapbox feedback get | aliases: (none) +mapbox feedback list | aliases: (none) mapbox fonts delete | aliases: (none) mapbox fonts list | aliases: (none) mapbox fonts upload | aliases: (none)