From eadaacdebca371e9283b000a33d0f84f7d27046b Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Thu, 24 Sep 2026 16:13:27 -0400 Subject: [PATCH] Add mapbox optimization submit/get/list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sixth API this session, the first outside the naming doc's V1 scope (marked Support in V1: false there) — continuing per the user's own call to do all 8 anyway. Hand-authored into custom-openapi/ since openapi-specs has no spec for this API either. Solves a multi-vehicle, multi-stop routing problem as an asynchronous job: submit (POST, 202 with a job id), get (poll by id — 202 while solving, 200 with the routes once done), list (every submitted job and its status). submit's whole problem is one JSON document through --data, the same shape styles create already uses — locations, vehicles, and one or both of services/shipments, with no per-field flag for something this shape. No profile path parameter here, so no new ARG_NAME_OVERRIDES row. Smoke-tested against production end to end: submitted a real 3-stop problem, polled it to a solved 4-stop route (start, two services in the solver's chosen order, end) with real etas and a running odometer, and confirmed list reports it as a bare JSON array of {id, status}. 488 tests, fmt and clippy clean. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 10 + README.md | 1 + .../optimization/openapi/optimization.yaml | 127 ++++++++++++ docs/commands.md | 184 ++++++++++++++++++ src/remedy.rs | 4 + src/spec.rs | 4 + tests/fixtures/api_command_surface.txt | 3 + 7 files changed, 333 insertions(+) create mode 100644 custom-openapi/optimization/openapi/optimization.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index e62a375..9113cf4 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 optimization submit`/`get`/`list`, solving a multi-vehicle, + multi-stop routing problem (v2 — v1 is retired) as an asynchronous job: + submit the problem, poll `get` for the solved routes, `list` to see every + job's status without tracking ids yourself. Hand-authored into + `custom-openapi/` for the same reason the other commands added this + session were: no upstream spec exists yet. `submit`'s whole problem + (locations, vehicles, services/shipments, options) goes through `--data` + as one JSON document, the same shape `styles create` already uses for a + body with no sensible per-field flag. + - `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 diff --git a/README.md b/README.md index 861bb3b..ddbfd3b 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,7 @@ mapbox geocoder * mapbox isochrone * mapbox map-matching * mapbox matrix * +mapbox optimization * mapbox search * mapbox sprites * mapbox static * diff --git a/custom-openapi/optimization/openapi/optimization.yaml b/custom-openapi/optimization/openapi/optimization.yaml new file mode 100644 index 0000000..655e1dc --- /dev/null +++ b/custom-openapi/optimization/openapi/optimization.yaml @@ -0,0 +1,127 @@ +openapi: "3.0.0" +# `parse_spec` turns `info.description` below into this service's clap +# `long_about`, so it also reaches `mapbox optimization --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/navigation/optimization (v2 — v1 is retired). See +# `custom-openapi/README.md` for how a file like this is wired in. +info: + title: "Mapbox Optimization API v2" + description: >- + Solves a multi-vehicle, multi-stop routing problem — which vehicle + visits which stop, and in what order — as an asynchronous job: submit + it, then poll for the solved routes. + version: "0.0.0" +servers: + - url: https://api.mapbox.com + description: Optimization API v2 +paths: + /optimized-trips/v2: + post: + operationId: submit + summary: Submit a routing problem to solve. + description: >- + Accepted (202) with a job `id` and a `status`; poll `mapbox + optimization get ` until it stops answering 202. Up to 1000 + locations per problem. `--data` carries the whole problem as one + JSON document — there is no per-field flag for something this + shape. Top-level fields: `version` (always `1`), `locations` + (`name` + `[longitude, latitude]` `coordinates`, every name unique), + `vehicles` (`name`, and optionally `routing_profile` — + `mapbox/driving` by default — `start_location`/`end_location`, + `capacities`, `capabilities`, `earliest_start`/`latest_end`, + `breaks`, `loading_policy`: `any`/`fifo`/`lifo`), `services` — a + single stop to visit (`name`, `location`, `duration`, + `requirements`, `service_times`) — and/or `shipments` — a + pickup-then-dropoff pair (`name`, `from`, `to`, `size`, + `requirements`, `pickup_duration`/`dropoff_duration`, + `pickup_times`/`dropoff_times`); at least one of `services` or + `shipments` is required. `options.objectives` picks what to + optimize for: `min-total-travel-duration` or + `min-schedule-completion-time`. + parameters: + - name: "access_token" + in: query + required: true + description: "Mapbox API Access Token" + schema: + type: string + minLength: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + "202": + description: >- + Accepted: `{"id": "", "status": "ok|pending|processing"}`. + Not solved yet — poll `optimization get`. + "401": + description: Unauthorized + "422": + description: Unprocessable Entity — a malformed problem. + + get: + operationId: list + summary: List every submitted routing problem and its status. + description: >- + Every job this account has submitted, each as `{"id", "status"}` — + `status` is `pending`, `processing`, or `complete`. Does not return + the solved routes themselves; `optimization get ` does, once + `status` is `complete`. + parameters: + - name: "access_token" + in: query + required: true + description: "Mapbox API Access Token" + schema: + type: string + minLength: 1 + responses: + "200": + description: "A JSON array of `{id, status}` objects, one per submitted job." + "401": + description: Unauthorized + + /optimized-trips/v2/{id}: + get: + operationId: get + summary: Retrieve a routing problem's solved routes. + description: >- + Answers 202 with no body while still solving; 200 with the solved + routes once done — one entry per vehicle, each a list of stops + (`start`, `service`, `pickup`, `dropoff`, `break`, `end`) with an + ETA and running odometer, plus a `dropped` list of any + service/shipment the solver couldn't fit in. + parameters: + - name: "id" + in: path + required: true + description: "The job id `optimization submit` returned." + 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: >- + Solved: `routes` (one per vehicle, each a `vehicle` name and a + `stops` array — `type`, `location`, `eta`, `odometer`, and for + a `service`/`pickup`/`dropoff` stop the names it covers) and + `dropped` (`services`/`shipments` the solver left out). + "202": + description: "Still processing — no body. Poll again." + "401": + description: Unauthorized + "404": + description: Not Found — no job with that id. diff --git a/docs/commands.md b/docs/commands.md index ca460e7..2c10c90 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1691,6 +1691,190 @@ Neither output mode has a bespoke rendering for this response, same as `-o text` pretty-printed and `-o json` on one line. Dropped each waypoint's own snap `distance` for length; the real response carries it too. +--- +## Optimization + +Solves a multi-vehicle, multi-stop routing problem — which vehicle visits +which stop, and in what order — as an asynchronous job (v2; v1 is +retired). Curated by hand down to the parameters documented at +docs.mapbox.com/api/navigation/optimization — see `custom-openapi/README.md` +for why this command group doesn't come from the vendored specs the way +most others do. + +**vs. `matrix compute`**: that ranks reachability across many pairs; +this decides an actual visiting order for one or more vehicles, given +constraints (time windows, capacities, breaks) `matrix` knows nothing +about. + +### `mapbox optimization submit` + +Submits a routing problem. Accepted (202) with a job `id` and `status` — +`optimization get ` is the follow-up poll, not this command; solving +happens asynchronously and can take a while for a large problem. + +#### Parameters + +`--data`/`-d` carries the whole problem as one JSON document — there is no +per-field flag for something this shape. Top-level fields: + +| Field | Effect | +| --- | --- | +| `version` | Always `1`. | +| `locations` | `name` (unique) + `coordinates` (`[longitude, latitude]`) per stop. | +| `vehicles` | `name` (unique); optionally `routing_profile` (`mapbox/driving` by default), `start_location`/`end_location`, `capacities`, `capabilities`, `earliest_start`/`latest_end`, `breaks`, `loading_policy` (`any`/`fifo`/`lifo`). | +| `services` | A single stop to visit: `name`, `location`, `duration`, `requirements`, `service_times`. At least one of `services`/`shipments` is required. | +| `shipments` | A pickup-then-dropoff pair: `name`, `from`, `to`, `size`, `requirements`, `pickup_duration`/`dropoff_duration`, `pickup_times`/`dropoff_times`. | +| `options.objectives` | What to optimize for: `min-total-travel-duration` or `min-schedule-completion-time`. | + +#### Examples + +```sh +mapbox optimization submit -d '{ + "version": 1, + "locations": [ + {"name": "depot", "coordinates": [-122.42, 37.78]}, + {"name": "stop1", "coordinates": [-122.45, 37.91]}, + {"name": "stop2", "coordinates": [-122.41, 37.80]} + ], + "vehicles": [{"name": "van1", "start_location": "depot", "end_location": "depot"}], + "services": [ + {"name": "svc1", "location": "stop1"}, + {"name": "svc2", "location": "stop2"} + ] +}' +``` + +#### Outputs + +Captured live — one vehicle, two single-stop services: + + + + +
Terminal — -o textAgent — -o json
+ +```json +{ "id": "5f57b00c-a3a1-45de-89ca-1e24b3a23cc4.r1", "status": "ok" } +``` + + + +```json +{"id":"5f57b00c-a3a1-45de-89ca-1e24b3a23cc4.r1","status":"ok"} +``` + +
+ +### `mapbox optimization get` + +Retrieves a submitted problem's solved routes. Answers 202 with no body +while still solving; 200 with the routes once done. + +#### Parameters + +`` (positional) is required — the job id `submit` returned. + +#### Examples + +```sh +mapbox optimization get 5f57b00c-a3a1-45de-89ca-1e24b3a23cc4.r1 +``` + +#### Outputs + +Captured live — the job submitted above, once solved: one route, a start, +two service stops in the order the solver chose, and an end, each with a +running `odometer` (meters) and `eta`: + + + + +
Terminal — -o textAgent — -o json
+ +```json +{ + "dropped": { "services": [], "shipments": [] }, + "routes": [ + { + "vehicle": "van1", + "stops": [ + { "type": "start", "location": "depot", "eta": "1970-01-01T00:00:00Z", "odometer": 0 }, + { + "type": "service", + "location": "stop1", + "eta": "1970-01-01T00:39:41Z", + "odometer": 25766, + "services": ["svc1"] + }, + { + "type": "service", + "location": "stop2", + "eta": "1970-01-01T01:17:33Z", + "odometer": 51148, + "services": ["svc2"] + }, + { "type": "end", "location": "depot", "eta": "1970-01-01T01:34:07Z", "odometer": 54929 } + ] + } + ] +} +``` + + + +```json +{"dropped":{"services":[],"shipments":[]},"routes":[{"vehicle":"van1","stops":[{"type":"start","location":"depot","eta":"1970-01-01T00:00:00Z","odometer":0},{"type":"service","location":"stop1","eta":"1970-01-01T00:39:41Z","odometer":25766,"services":["svc1"]},{"type":"service","location":"stop2","eta":"1970-01-01T01:17:33Z","odometer":51148,"services":["svc2"]},{"type":"end","location":"depot","eta":"1970-01-01T01:34:07Z","odometer":54929}]}]} +``` + +
+ +Neither output mode has a bespoke rendering for this response — both print +the same JSON, `-o text` pretty-printed and `-o json` on one line. Dropped +each stop's `location_metadata` (snapped vs. supplied coordinate) and `wait` +for length; the real response carries them too. The `1970-01-01` dates are +this problem's own timestamps — no `earliest_start`/time window was given, +so the solver counted elapsed seconds from epoch rather than a real clock. + +### `mapbox optimization list` + +Every routing problem this account has submitted, with its status — +`pending`, `processing`, or `complete`. Does not return the solved routes +themselves; `optimization get ` does, once `status` is `complete`. + +#### Parameters + +None. + +#### Examples + +```sh +mapbox optimization list +``` + +#### Outputs + +Captured live, the same job as above, now complete: + + + + +
Terminal — -o textAgent — -o json
+ +```json +[{ "id": "5f57b00c-a3a1-45de-89ca-1e24b3a23cc4.r1", "status": "complete" }] +``` + + + +```json +[{"id":"5f57b00c-a3a1-45de-89ca-1e24b3a23cc4.r1","status":"complete"}] +``` + +
+ +A bare JSON array, not wrapped in an `items`/`results` field — every entry +is just `{id, status}`. + --- ## Search diff --git a/src/remedy.rs b/src/remedy.rs index 275186a..f5aff94 100644 --- a/src/remedy.rs +++ b/src/remedy.rs @@ -94,6 +94,10 @@ const SERVICE_DOCS: &[(&str, &str)] = &[ "https://docs.mapbox.com/api/navigation/map-matching/", ), ("matrix", "https://docs.mapbox.com/api/navigation/matrix/"), + ( + "optimization", + "https://docs.mapbox.com/api/navigation/optimization/", + ), ("search", "https://docs.mapbox.com/api/search/search-box/"), // Static Images and Static Tiles merged into one `static` command group // (#116); neither upstream page covers both, so this points at Static diff --git a/src/spec.rs b/src/spec.rs index 4a5347d..1b33b8b 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -682,6 +682,10 @@ pub const CUSTOM_SPEC_ENTRIES: &[SpecEntry] = &[ name: "feedback", yaml: include_str!("../custom-openapi/feedback/openapi/feedback.yaml"), }, + SpecEntry { + name: "optimization", + yaml: include_str!("../custom-openapi/optimization/openapi/optimization.yaml"), + }, ]; /// The list the CLI actually generates commands from: [`MAPBOX_SPEC_ENTRIES`], diff --git a/tests/fixtures/api_command_surface.txt b/tests/fixtures/api_command_surface.txt index 7161a4d..e62e469 100644 --- a/tests/fixtures/api_command_surface.txt +++ b/tests/fixtures/api_command_surface.txt @@ -13,6 +13,9 @@ mapbox geocoder reverse | aliases: (none) mapbox isochrone contours | aliases: (none) mapbox map-matching match | aliases: (none) mapbox matrix compute | aliases: (none) +mapbox optimization get | aliases: (none) +mapbox optimization list | aliases: (none) +mapbox optimization submit | aliases: (none) mapbox search category | aliases: (none) mapbox search forward | aliases: (none) mapbox search list-category | aliases: (none)