diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6030012..6b924d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,8 @@ that may never merge. They are not releases and are not listed here.
## Unreleased
+- Clean up.
+
### Added
- The README now documents installing without the install script: the
diff --git a/Cargo.lock b/Cargo.lock
index ce69239..c2f138e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -751,7 +751,7 @@ checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
[[package]]
name = "mapbox-cli"
-version = "0.2.2"
+version = "0.3.0"
dependencies = [
"anyhow",
"base64",
diff --git a/Cargo.toml b/Cargo.toml
index 8cb4407..6a51d0b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "mapbox-cli"
-version = "0.2.2"
+version = "0.3.0"
edition = "2021"
description = "A command-line interface for Mapbox APIs, with commands generated at build time from OpenAPI specs."
repository = "https://github.com/mapbox/cli"
diff --git a/docs/commands.md b/docs/commands.md
index 525ed1b..03758e3 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -1,6 +1,6 @@
# Implemented commands
-Every command the CLI ships: four auth commands, 34 API operations across 11
+Every command the CLI ships: four auth commands, 33 API operations across 10
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,7 +10,7 @@ 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 34 were run against the live API and show what came back:** 26
+**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
@@ -29,12 +29,10 @@ commands and the flags they take — is held to `mapbox --schema` on every
`cargo test` run by `tests/docs_contract.rs`, so the half of this page that
can be checked cannot fall behind the binary.
-The remaining 5 give the response shape from the spec or the docs instead
-of a live capture, for two different reasons. `rasterarrays get-mrt-tile`
-needs a raster-array job this account does not have. The other four are
-`search`'s: read-only and safe to run, but the credentials used to write
-this page have no Search Box API access, so every call answers 401 rather
-than a result.
+The remaining 4 give the response shape from the spec or the docs instead
+of a live capture: they're all `search`'s — read-only and safe to run, but
+the credentials used to write this page have no Search Box API access, so
+every call answers 401 rather than a result.
Each **Parameters** section lists only what is specific to its command. The
globals every API command takes are
@@ -102,7 +100,6 @@ nests, and is typed `mapbox styles draft get`.
**[Tilesets](#tilesets)** —
[tilesets.get-tile](#mapbox-tilesets-get-tile) ·
[tilesets.get-mvt](#mapbox-tilesets-get-mvt) ·
-[tilesets.get-mrt](#mapbox-tilesets-get-mrt) ·
[tilesets.query](#mapbox-tilesets-query)
**[Tilesets CLI](#tilesets-cli)** — [tilesets-cli](#mapbox-tilesets-cli-args)
@@ -349,7 +346,7 @@ Docs: https://docs.mapbox.com/api/accounts/tokens/
## API command groups
-34 operations across 11 command groups. Ten are generated from the OpenAPI specs
+33 operations across 10 command groups. Nine are generated from the OpenAPI specs
vendored in `openapi/`; `search` is the one exception — a hand-authored
spec versioned in this repo's own `custom-openapi/`, see
`custom-openapi/README.md`.
@@ -367,7 +364,6 @@ operation moved to `tilesets`.
| [`accounts`](#accounts) | **3** |
| [`fonts`](#fonts) | **3** |
| [`geocoder`](#geocoder) | **3** |
-| [`rasterarrays`](#raster-arrays) | **1** |
| [`search`](#search) | **4** |
| [`sprites`](#sprites) | **5** |
| [`static-images`](#static-images) | **3** |
@@ -376,7 +372,7 @@ operation moved to `tilesets`.
| [`tilequery`](#tilequery) | **1** |
| [`tilesets`](#tilesets) | **2** |
-The [Contents](#contents) list above names every one of the 34.
+The [Contents](#contents) list above names every one of the 33.
**Everything else the Mapbox specs describe is not here at all.** Not
hidden, not shipped as a command that refuses: absent from the spec content
@@ -487,7 +483,7 @@ Three things worth knowing about the read forms:
A command that changes something takes `--dry-run`, which prints the request
it would send, on stdout, and sends nothing. Which commands those are is not
a list anyone keeps: it is every `POST`, `PUT`, `PATCH` and `DELETE` — 12 of
-the 34 operations — plus `auth login`, `auth logout`, `auth refresh` and
+the 33 operations — plus `auth login`, `auth logout`, `auth refresh` and
`generate-skills`. A read-only `GET` does not take it, so `mapbox styles
list --dry-run` is a usage error rather than a no-op. It rehearses
rather than describes: `--data` is parsed and every `--file` is read, so a
@@ -568,8 +564,8 @@ properties directly. A conforming response never reaches that case:
`geocoder` requires `name`/`feature_type` on every feature, `tilequery`
requires `tilequery.layer`.
-Four of the eleven command groups can answer with bytes — `rasterarrays`,
-`static-images`, `static-tiles` and `tilesets`. Those bypass `--output` in
+Three of the ten command groups can answer with bytes — `static-images`,
+`static-tiles` and `tilesets`. Those bypass `--output` in
both modes:
@@ -817,14 +813,8 @@ afterward.
### `mapbox fonts list`
-The font faces an account owns.
-
-#### Parameters
-
-`--fresh` skips the cache. Worth knowing even outside a script: a font
-just uploaded does not appear without it — the API's own `Cache-Control`
-otherwise serves a listing from before the upload for some window
-afterward.
+The font faces an account owns. Cached: a font just uploaded may not appear
+for some window afterward, per the API's own `Cache-Control`.
#### Examples
@@ -1915,8 +1905,7 @@ mapbox styles get ckstyle00000000000000001a --username user -o json > style.json
— is served through CloudFront with `max-age=900`, so a `styles get` right
after an `styles update` may still show the old document. The mutation
commands return the new state themselves, from the origin; trust that rather
-than reading back. `list-files --fresh true` is the one read that rebuilds
-instead of being served from cache.
+than reading back.
A style document is a nested tree of layers and sources, not rows, so it
stays JSON in both modes — indented under `text`, one line under `json`.
@@ -2247,18 +2236,17 @@ beforehand.
## Tilesets
-Tiles by tileset id, plus the raster-array and vector-tile lookups that key
-off one: raster from the Raster Tiles API, vector from the Vector Tiles
-API, MRT tiles from a raster-array job, and `query` from the Tilequery
-API. Four operations, out of four different specs, under one command
-group (#116) — a caller asking about a tileset is asking the same kind of
-question regardless of which API answers it.
+Tiles by tileset id, plus the vector-tile lookup that keys off one: raster
+from the Raster Tiles API, vector from the Vector Tiles API, and `query`
+from the Tilequery API. Three operations, out of three different specs,
+under one command group (#116) — a caller asking about a tileset is asking
+the same kind of question regardless of which API answers it.
Not to be confused with [`mapbox tilesets-cli`](#tilesets-cli), which
forwards to the separately installed Python Tilesets CLI and shares
nothing with this but the word.
-The three tile commands return bytes, so `--output` does not apply on
+The two tile commands return bytes, so `--output` does not apply on
them — redirect to a file. `query` returns GeoJSON.
### `mapbox tilesets get-tile`
@@ -2355,44 +2343,6 @@ $ ls -l tile.mvt
-### `mapbox tilesets get-mrt`
-
-One MRT tile from a raster-array job.
-
-#### Parameters
-
-`--jobid ` is required and comes from the raster-array job that
-produced the tiles.
-
-#### Examples
-
-```sh
-mapbox tilesets get-mrt --jobid 12 2048 1361 > tile.mrt
-```
-
-#### Outputs
-
-Not exercised: this account has no raster-array job. A `--jobid` that does
-not exist gets a 500 rather than a 404, so the error says nothing useful
-about what was wrong:
-
-
-
-```json
-{"code":"http_500","message":"Internal Server Error","status":500}
-```
-
-
-
-
### `mapbox tilesets query`
What features a vector tileset has at or near a point.
diff --git a/openapi/api-accounts/tokens-api.yaml b/openapi/api-accounts/tokens-api.yaml
index 9191eee..aba0233 100644
--- a/openapi/api-accounts/tokens-api.yaml
+++ b/openapi/api-accounts/tokens-api.yaml
@@ -217,14 +217,6 @@ components:
description: The username of the account that owns the tokens.
schema:
type: string
- TokenId:
- name: token_id
- in: path
- required: true
- description: The unique identifier of the token (the `id` field of a token object), not the token
- string itself.
- schema:
- type: string
responses:
BadRequest:
description: The request was malformed — for example, the token string was supplied where a token
diff --git a/openapi/api-fonts/fonts.production.v1.yaml b/openapi/api-fonts/fonts.production.v1.yaml
index c970fe1..0e53fb1 100644
--- a/openapi/api-fonts/fonts.production.v1.yaml
+++ b/openapi/api-fonts/fonts.production.v1.yaml
@@ -18,28 +18,14 @@ servers:
security:
- AccessTokenQuery: []
tags:
-- name: health
- description: API health check
- name: fonts
description: Manage and retrieve custom fonts
-- name: fontstacks
- description: Retrieve Mapbox fontstack glyphs
-- name: models
- description: Retrieve 3D model assets
components:
securitySchemes:
AccessTokenQuery:
type: apiKey
in: query
name: access_token
- parameters:
- fresh:
- name: fresh
- in: query
- required: false
- schema:
- type: boolean
- description: If true, sets Cache-Control to no-cache.
schemas:
Error:
type: object
@@ -113,7 +99,6 @@ paths:
security:
- AccessTokenQuery: []
parameters:
- - $ref: '#/components/parameters/fresh'
- name: owner
in: path
required: true
diff --git a/openapi/api-gl/static-images.production.v1.yaml b/openapi/api-gl/static-images.production.v1.yaml
index 8859e6d..dc402ef 100644
--- a/openapi/api-gl/static-images.production.v1.yaml
+++ b/openapi/api-gl/static-images.production.v1.yaml
@@ -158,22 +158,6 @@ components:
schema:
type: string
example: pin-s+ff0000(-118.2437,34.0522),pin-s+0000ff(-118.2537,34.0422)
- bbox:
- name: bbox
- in: path
- required: true
- description: |
- Bounding box as `[minlon,minlat,maxlon,maxlat]`. Coordinates are
- WGS84 longitude/latitude. `minlon` must be less than `maxlon` and
- `minlat` less than `maxlat`, otherwise the request returns `422`.
-
- The square brackets may be sent literally or percent-encoded as `%5B`
- and `%5D`; both are accepted. Note that `curl` treats `[` and `]` as
- glob characters, so a literal bounding box on the command line needs
- either the percent-encoded form or the `--globoff` flag.
- schema:
- type: string
- example: '[-77.043686,38.892035,-76.933086,38.942035]'
lon:
name: lon
in: path
@@ -311,24 +295,6 @@ components:
schema:
type: string
example: waterway-label
- padding:
- name: padding
- in: query
- required: false
- description: |
- Padding in pixels around the overlay or bounding box. Accepts 1 to 4
- comma-separated integers (CSS shorthand order: top, right, bottom,
- left). Only applies to `auto` and bounding box positioning; using it on
- any other endpoint returns `422`.
-
- Left plus right padding must be less than `width`, and top plus bottom
- padding must be less than `height`, otherwise the request returns
- `422`.
-
- Defaults to 5% of the smallest dimension, up to a maximum of 12 pixels.
- schema:
- type: string
- example: 50,20,50,20
addlayer:
name: addlayer
in: query
diff --git a/openapi/api-rasterarrays/rasterarrays.production.v1.yaml b/openapi/api-rasterarrays/rasterarrays.production.v1.yaml
deleted file mode 100644
index 9f8488d..0000000
--- a/openapi/api-rasterarrays/rasterarrays.production.v1.yaml
+++ /dev/null
@@ -1,163 +0,0 @@
-openapi: 3.1.0
-info:
- version: 1.0.0
- title: Rasterarray Tiles API
- description: Serves MRTs
- x-mapbox-docs-path: maps/raster-arrays
- x-mapbox-docs-products:
- - Rasterarray Tiles API
- x-mapbox-docs-description: |
- The Rasterarrays API serves multi-band raster tiles in MRT (Mapbox Raster Tile) format.
-tags:
-- name: Health
- description: Service health and uptime probes
-servers:
-- url: https://api.mapbox.com/rasterarrays/v1
- description: Production
-security:
-- AccessTokenQuery: []
-paths:
- /{tilesets-id}/{z}/{x}/{y}.mrt:
- get:
- summary: Get MRT tile
- description: "Retrieves a multi-band raster tile in MRT (Mapbox Raster Tile) format.\n\nThe tileset\
- \ ID follows the format `username.id`. The jobid parameter can be found \nin the mapdoc via the\
- \ api-tileset endpoint: \n`https://api.mapbox.com/v4/.json?secure&access_token=`\n"
- parameters:
- - name: tilesets-id
- in: path
- required: true
- description: The map id (tileset) to get tiles from - username.id format
- schema:
- type: string
- minLength: 1
- example: username.tileset_id
- - name: z
- in: path
- required: true
- description: The z value of the tile request
- schema:
- type: integer
- minimum: 0
- example: 14
- - name: x
- in: path
- required: true
- description: The x value of the tile request
- schema:
- type: integer
- minimum: 0
- example: 2535
- - name: y
- in: path
- required: true
- description: The y value of the tile request
- schema:
- type: integer
- minimum: 0
- example: 3142
- - name: access_token
- in: query
- required: true
- description: Required Mapbox API Access Token
- schema:
- type: string
- minLength: 1
- example: YOUR_MAPBOX_ACCESS_TOKEN
- - name: jobid
- in: query
- required: true
- description: Required job identifier used to locate the tile. Can be found in mapdoc via api-tileset
- endpoint.
- schema:
- type: string
- minLength: 1
- example: clnf3oau5005g08kzhr460vut
- responses:
- '200':
- description: Successfully retrieved MRT tile data
- content:
- application/octet-stream:
- schema:
- type: string
- format: binary
- description: Multi-band raster tile data in MRT format
- examples:
- getMrtTile:
- summary: MRT tile binary data
- externalValue: https://api.mapbox.com/rasterarrays/v1/username.tileset_id/14/2535/3142.mrt?access_token=YOUR_MAPBOX_ACCESS_TOKEN&jobid=clnf3oau5005g08kzhr460vut
- '400':
- description: Bad Request
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
- text/plain:
- schema:
- type: string
- example: Bad Request
- '403':
- description: Forbidden
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
- text/plain:
- schema:
- type: string
- example: Forbidden
- '404':
- description: Not Found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
- text/plain:
- schema:
- type: string
- example: Not Found
- x-mapbox-docs-examples:
- - example-id: getMrtTile
- title: Get an MRT tile
- request:
- tilesets-id: username.tileset_id
- z: 14
- x: 2535
- y: 3142
- access_token: YOUR_MAPBOX_ACCESS_TOKEN
- jobid: clnf3oau5005g08kzhr460vut
- operationId: getMrtTile
- x-mapbox-cli-command:
- - tilesets
- - get-mrt
-components:
- securitySchemes:
- AccessTokenQuery:
- type: apiKey
- in: query
- name: access_token
- description: Mapbox access token
- schemas:
- Error:
- type: object
- properties:
- message:
- type: string
- status:
- type: integer
- errors:
- type: array
- items:
- type: string
- code:
- type: string
- required:
- - message
- additionalProperties: true
- responses:
- ErrorResponse:
- description: Error response
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
diff --git a/openapi/api-rastertiles/rastertiles.production.v1.yaml b/openapi/api-rastertiles/rastertiles.production.v1.yaml
index 2c5daa8..3d91e3b 100644
--- a/openapi/api-rastertiles/rastertiles.production.v1.yaml
+++ b/openapi/api-rastertiles/rastertiles.production.v1.yaml
@@ -19,14 +19,8 @@ servers:
- url: https://api.mapbox.com
description: Production server
tags:
-- name: health
- description: Service-level health and uptime probes
- name: tiles
description: Raster tile delivery endpoints (v4)
-- name: grid
- description: UTFGrid JSON endpoints (v4 and legacy)
-- name: legacy
- description: Deprecated or legacy endpoints (API versions 1–3)
security:
- ApiKeyAuth: []
paths:
diff --git a/openapi/api-styles/styles.production.v1.yaml b/openapi/api-styles/styles.production.v1.yaml
index 2ff5179..7b534c4 100644
--- a/openapi/api-styles/styles.production.v1.yaml
+++ b/openapi/api-styles/styles.production.v1.yaml
@@ -34,10 +34,6 @@ tags:
description: Create, read, update, and delete map styles
- name: Sprites
description: Manage SVG images within a style's sprite
-- name: Folders
- description: Organize styles, tilesets, and datasets into folders
-- name: Sessions
- description: Billing session tracking endpoints
paths:
/styles/v1/{username}:
get:
diff --git a/src/executor.rs b/src/executor.rs
index b21a0a5..cfe6921 100644
--- a/src/executor.rs
+++ b/src/executor.rs
@@ -11,9 +11,10 @@ use crate::output::{self, CliError, Mode};
use crate::remedy::{self, Remedy};
use crate::spec::{Operation, Parameter, RequestBody, ACCOUNT_PLACEHOLDERS, MULTIPART};
-/// The query parameter the access token travels in, and what stands in for it
-/// anywhere the URL is shown. Named once because getting this wrong leaks a
-/// live token: `--debug` and the dry-run plan both render the same URL.
+/// The query parameter the access token travels in, and what stands in for
+/// it anywhere the URL is shown. Named once because getting this wrong
+/// leaks a live token — `--debug` and the dry-run plan both render the
+/// same URL.
const ACCESS_TOKEN: &str = "access_token";
const REDACTED: &str = "";
@@ -21,9 +22,9 @@ const REDACTED: &str = "";
const JSON_CONTENT_TYPE: &str = "application/json";
/// Name of the flag that stops short of sending. Declared per operation
-/// rather than globally: a `GET` has nothing to preview, and a flag offered
-/// where it cannot mean anything is worse than one you have to put after the
-/// operation name. See [`dry_run_arg`].
+/// rather than globally: a `GET` has nothing to preview, and offering a
+/// flag that means nothing is worse than requiring it after the operation
+/// name. See [`dry_run_arg`].
pub const DRY_RUN_ARG: &str = "dry-run";
/// The wording for a command whose mutation is an API call.
@@ -32,11 +33,11 @@ pub const DRY_RUN_REQUEST_HELP: &str =
/// The `--dry-run` flag, with the sentence that fits what the command does.
///
-/// The help text is a parameter because the two kinds of mutating command
-/// change different things: a generated operation would send a request, while
-/// `mapbox auth logout` deletes a file and sends nothing at all. A `--help`
-/// line that describes the wrong one is the small kind of lie that stops a
-/// safety flag from being reached for.
+/// The help text is a parameter because mutating commands change different
+/// things: a generated operation sends a request, while `mapbox auth
+/// logout` deletes a local file and sends nothing at all. A `--help` line
+/// that describes the wrong one is a small lie that makes people trust a
+/// safety flag less.
pub fn dry_run_arg(help: &'static str) -> clap::Arg {
clap::Arg::new(DRY_RUN_ARG)
.long(DRY_RUN_ARG)
@@ -46,8 +47,8 @@ pub fn dry_run_arg(help: &'static str) -> clap::Arg {
/// Whether `--dry-run` was given, for an operation that may not have it.
///
-/// `get_flag` panics on an argument that was never registered, and read-only
-/// operations never register this one.
+/// `get_flag` panics on an unregistered argument, and read-only operations
+/// never register this one — hence `try_get_one`.
pub fn wants_dry_run(matches: &ArgMatches) -> bool {
matches
.try_get_one::(DRY_RUN_ARG)
@@ -58,34 +59,34 @@ pub fn wants_dry_run(matches: &ArgMatches) -> bool {
}
/// The three switches that change whether and how a request goes out, as
-/// opposed to what is being requested.
+/// opposed to what's being requested.
///
-/// Grouped once there were three of them. Clippy's argument limit is the
-/// visible reason; the better one is that a call site handing over three bare
-/// `bool`s in a row is one transposition away from a silent swap — `--debug`
-/// for `--dry-run` would send the request it promised only to describe.
-/// Naming them at the call site makes that impossible.
+/// Grouped into a struct once there were three of them. Clippy's argument
+/// limit is the visible reason; the real one is that three bare `bool`s in
+/// a row at a call site is one transposition away from a silent swap —
+/// mixing up `--debug` and `--dry-run` would send a request that was only
+/// supposed to be described. Naming the fields makes that impossible.
#[derive(Clone, Copy)]
pub struct RunFlags {
pub debug: bool,
pub assume_yes: bool,
pub dry_run: bool,
/// The budget the caller asked for, from `crate::http::requested` —
- /// `--timeout` or `MAPBOX_TIMEOUT`. `None` does not mean "no timeout": it
- /// means nobody said, and [`crate::http::budget`] falls back to whichever
- /// default fits what the request is carrying.
+ /// `--timeout` or `MAPBOX_TIMEOUT`. `None` doesn't mean "no timeout",
+ /// it means "nobody said"; [`crate::http::budget`] then falls back to
+ /// whatever default fits the request.
pub timeout: Option,
}
-/// Runs one generated command — the request it would make, sent or, under
-/// `--dry-run`, described.
+/// Runs one generated command — the request it would make, either sent or
+/// described under `--dry-run`.
///
-/// A wrapper, so that every way this can reject an *argument* is pointed at
-/// `--schema` from one place instead of three: the choice between `--data`
-/// and `--file`, a `--data` that will not parse, and the same parse again on
-/// the dry-run path, which reads the body rather than trusting it.
-/// `with_schema_action` answers to a code, so every other failure — an HTTP
-/// status, an unreadable file — passes through untouched.
+/// A thin wrapper so every way this can reject an *argument* points at
+/// `--schema` from one place, not three: choosing between `--data` and
+/// `--file`, a `--data` that won't parse, and re-parsing it on the
+/// dry-run path (which reads the body instead of trusting it).
+/// `with_schema_action` reacts to a specific error code, so everything
+/// else — an HTTP status, an unreadable file — passes through untouched.
pub fn execute(
op: &Operation,
matches: &ArgMatches,
@@ -115,14 +116,12 @@ fn dispatch(
let mut path = op.path_template.clone();
- // Substitute {username}/{owner}/{account} from global --username
if let Some(u) = username {
for placeholder in ACCOUNT_PLACEHOLDERS {
path = path.replace(&format!("{{{placeholder}}}"), u);
}
}
- // Substitute other path params from positional args
for param in &op.path_params {
if let Some(val) = matches.get_one::(¶m.arg_name) {
let safe = path_segment(¶m.name, val)?;
@@ -131,7 +130,6 @@ fn dispatch(
}
if path.contains('{') {
- // Extract missing param names for a useful error
let missing: Vec<&str> = path
.split('{')
.skip(1)
@@ -144,8 +142,8 @@ fn dispatch(
missing.join(", ")
),
)
- // The message names the two ways to supply an account; this names
- // the command that says whether a login would supply it already.
+ // The message above names the two ways to supply an account; this
+ // adds the command that checks whether a login already supplies it.
.with_remedy(Remedy::default().with_action(Some("mapbox auth whoami".to_string())))
.into());
}
@@ -168,12 +166,12 @@ fn dispatch(
}
}
- // `--data` and `--file` are declared per-operation, so either may be
- // absent from this command entirely; `get_one` panics on an argument
- // that was never registered.
- // Resolved before anything reads it, `--dry-run` included, so a `@path`
- // that does not exist fails here rather than being described as a request
- // and then failing at send time. `resolve_data` says why.
+ // `--data`/`--file` are declared per-operation, so either may not exist
+ // on this command at all — `get_one` would panic on an unregistered
+ // argument, hence `try_get_one`. Resolved here, before `--dry-run` or
+ // anything else reads it, so a `@path` that doesn't exist fails now
+ // instead of after being described as a request that would then fail
+ // to send.
let data_argument = match matches.try_get_one::("data").ok().flatten() {
Some(value) => Some(resolve_data(value)?),
None => None,
@@ -188,19 +186,20 @@ fn dispatch(
.map(|values| values.map(String::as_str).collect())
.unwrap_or_default();
- // Resolved ahead of the dry-run branch on purpose: choosing between
+ // Resolved before the dry-run branch on purpose: choosing between
// `--data` and `--file` is a check the caller can fail, and a dry run
- // that skipped it would report a request that could not actually be
- // sent — which is the one thing it exists to rule out.
+ // that skipped it would describe a request that could never actually
+ // be sent.
let body_source = match &op.body {
Some(body) => Some(resolve_body_source(body, data, &files)?),
None => None,
};
if dry_run {
- // Worth saying out loud, and only here: a real call answers a missing
- // token with a 401 the caller cannot miss, while a dry run would
- // happily print a plausible-looking request that could never succeed.
+ // Worth saying here specifically: a real call would answer a
+ // missing token with an unmissable 401, but a dry run would just
+ // print a plausible-looking request that could never actually
+ // succeed.
if token.is_none() {
output::progress(
"Note: no access token was resolved, so the real request would be unauthenticated.",
@@ -209,21 +208,17 @@ fn dispatch(
return describe_request(op, mode, &url, &query, body_source.as_ref());
}
- // Below the dry-run branch, and that ordering is the point: a dry run
- // sends nothing, so asking whether to go ahead would be asking about
- // something that is not going to happen — and it would make the flag that
- // exists to be safe the one that blocks a script.
+ // Placed after the dry-run branch deliberately: a dry run sends
+ // nothing, so asking "go ahead?" about something that isn't going to
+ // happen would make the safety flag the thing that blocks a script.
//
- // `url` is passed rather than the rendered request because it carries no
- // query string, and the access token is a query parameter. The question is
- // printed. `redacted_url` above exists for the same reason on the same
- // string; this call must never be given its output.
+ // `url` here, not the redacted one — the access token is a query
+ // parameter and `url` carries no query string at all, so it's already
+ // safe to print. Never pass `redacted_url`'s output here.
confirm::destructive_request(&op.method, &url, assume_yes)?;
- // Below the dry-run branch, so a `[debug]` request line always means a
- // request that went out. The plan renders the same URL itself, and one
- // that reads as a call having been made is the last thing this flag
- // should print.
+ // Also after the dry-run branch, so a `[debug]` line always means a
+ // request actually went out — never one that only looks like it did.
if debug {
eprintln!("[debug] {} {}", op.method, redacted_url(&url, &query));
}
@@ -238,13 +233,13 @@ fn dispatch(
_ => client.get(&url),
}
.query(&query)
- // Named on the request rather than left to the client, because the client
- // cannot know what any one request is doing: a sprite upload sends a file
- // and a listing sends nothing, and a single budget that suits both is
- // either too short for the upload or too long to be a timeout at all.
- // `reqwest` prefers the request's own over the client's — 0.12.28's
- // `execute_request` reads `req.timeout().copied().or(self.timeout.0)` —
- // so this is what actually applies to everything sent from here.
+ // Set per-request, not on the client: the client has no way to know
+ // what a given request is doing, and a sprite upload (needs a long
+ // timeout) and a listing (needs a short one) can't share one budget.
+ // `reqwest` prefers the request's own timeout over the client's —
+ // 0.12.28's `execute_request` reads
+ // `req.timeout().copied().or(self.timeout.0)` — so this is what
+ // actually applies.
.timeout(http::budget(
timeout,
payload_of(
@@ -263,32 +258,32 @@ fn dispatch(
.send()
.map_err(|e| transport_failure("Request failed", e))?;
let status = response.status();
- // Read the headers before `bytes()` consumes the response — whatever is
- // not taken here is gone by the next line. `Content-Type` was for a long
- // time the only one that survived this point, which is what made
- // pagination and support escalation unreachable; see [`ResponseHeaders`].
+ // Must read headers before `bytes()` consumes the response — anything
+ // not taken here is gone after. For a long time only `Content-Type`
+ // survived this point, which is why pagination and support escalation
+ // used to be unreachable; see [`ResponseHeaders`].
let headers = ResponseHeaders::read(response.headers());
let content_type = headers.content_type.clone();
let body = response
.bytes()
.map_err(|e| transport_failure("Failed to read response", e))?;
- // Six of the twelve services answer with bytes, not text — images, vector
- // tiles, glyph PBFs, style ZIPs. Decoding those as UTF-8 replaces every
- // invalid sequence with U+FFFD, which silently corrupts the payload: a PNG
- // arrives with its leading 0x89 rewritten to EF BF BD and no longer opens.
- // An error response is worth reading whatever the endpoint normally
- // returns, so failures always take the text path.
+ // Six of the twelve services answer with bytes, not text — images,
+ // vector tiles, glyph PBFs, style ZIPs. Decoding those as UTF-8 would
+ // silently corrupt them (a PNG's leading 0x89 becomes EF BF BD and the
+ // file no longer opens). But an error response is worth reading no
+ // matter what the endpoint normally returns, so failures always take
+ // the text path.
let as_text = if status.is_success() && is_binary_content_type(&content_type) {
None
} else {
Some(String::from_utf8_lossy(&body))
};
- // A failure is an error, not a result: it belongs on stderr in whichever
- // shape the caller asked for, so it never lands in a redirected file
- // alongside real output. The body is carried into the error rather than
- // printed here, which keeps every detail the old dump-to-stdout showed.
+ // A failure is an error, not a result, so it belongs on stderr — never
+ // in a redirected file alongside real output. The body goes into the
+ // error object instead of being printed here, keeping every detail the
+ // old approach (dumping to stdout) showed.
if !status.is_success() {
let text = as_text
.as_deref()
@@ -299,11 +294,11 @@ fn dispatch(
.into());
}
- // A 204, or a 200 that carries nothing. `serde_json` cannot parse an
- // empty string, so this fell through to the text-body path and printed a
- // blank line — or `""` under `json`, a valid document that says nothing.
- // Every mutation in the CLI that succeeds without a body landed there,
- // which left a delete indistinguishable from a command that did not run.
+ // A 204, or a 200 with nothing in it. `serde_json` can't parse an empty
+ // string, so this used to fall through to the text-body path and print
+ // a blank line (or `""` under `json` — a valid document that says
+ // nothing). Every no-body mutation ended up there, which made a
+ // successful delete look the same as a command that never ran.
if as_text
.as_deref()
.is_some_and(|text| text.trim().is_empty())
@@ -319,8 +314,7 @@ fn dispatch(
);
}
- // `Some` only when the response is one page of several, which is the
- // listings and nothing else.
+ // `Some` only for a listing that's one page of several.
let next_page = headers
.next_page
.as_deref()
@@ -335,8 +329,8 @@ fn dispatch(
.map_err(|err| with_page_context(err, next_page.as_ref()))?,
None,
Some(&op.service),
- // The row asked for is in hand; where the *other* rows
- // are is not advice about it.
+ // The wanted row is already in hand, so no need for a
+ // pagination tip about the other rows.
None,
)?,
None => output::emit_value(
@@ -349,41 +343,42 @@ fn dispatch(
},
Err(_) => output::emit_text_body(mode, &text)?,
},
- // Bytes bypass the output contract entirely. A PNG cannot be wrapped
- // in a JSON envelope without destroying it, and `--output json` on a
- // tile endpoint is far more likely to be the global flag riding along
- // than a considered request to mangle the image.
+ // Bytes bypass the output contract entirely. A PNG can't be
+ // wrapped in a JSON envelope without destroying it, and
+ // `--output json` on a tile endpoint is far more likely to be a
+ // global flag riding along than a deliberate request to mangle
+ // the image.
None => write_binary(&body, &content_type)?,
}
Ok(())
}
-/// The headers a Mapbox response may identify itself with, in the order they
-/// are preferred.
+/// The headers a Mapbox response may identify itself with, checked in this
+/// order.
///
-/// Measured rather than assumed, and the measurement is the reason there are
-/// two. `x-request-id` is the name the convention would predict and is what
-/// this looked for first — but no Mapbox endpoint reachable from here sends
-/// it: styles, tokens, fonts and geocoding v6 all answer without one, on both
-/// success and failure. What every one of them does carry is `x-amz-cf-id`,
-/// the CloudFront request id, because the whole API is fronted by it — and
-/// that is the id support traces a request with.
+/// There are two because we measured, not assumed. `x-request-id` is the
+/// conventional name and what we checked first — but no Mapbox endpoint
+/// reachable from here actually sends it: styles, tokens, fonts, and
+/// geocoding v6 all answer without one, success or failure. What they all
+/// carry instead is `x-amz-cf-id`, the CloudFront request id, since the
+/// whole API sits behind CloudFront — and that's the id support uses to
+/// trace a request.
///
-/// `x-request-id` stays first because a service that does send one means it
-/// more specifically than the CDN in front of it does, and it costs a lookup
-/// in a map that is already in memory.
+/// `x-request-id` stays first anyway: a service that does send one is more
+/// specific than the CDN in front of it, and checking costs only a lookup
+/// in a map already in memory.
const REQUEST_ID_HEADERS: [&str; 2] = ["x-request-id", "x-amz-cf-id"];
/// The request id from a response, for a caller that reads the body itself.
///
-/// `text()` and `bytes()` both consume the response, so this has to be called
-/// before the body is read — which is the whole reason it is a named function
-/// rather than a line inlined at each of the three call sites.
+/// `text()` and `bytes()` both consume the response, so this must be
+/// called before reading the body — which is why it's a named function
+/// instead of being inlined at each of the three call sites.
///
-/// Mapbox-bound requests only. `agent_skills` talks to GitHub codeload, which
-/// identifies requests with `x-github-request-id` and is not something Mapbox
-/// support can look up, so it deliberately does not call this.
+/// For Mapbox-bound requests only. `agent_skills` talks to GitHub
+/// codeload, which uses `x-github-request-id` instead — something Mapbox
+/// support can't look up — so it deliberately doesn't call this.
pub fn request_id(headers: &reqwest::header::HeaderMap) -> Option {
REQUEST_ID_HEADERS.iter().find_map(|name| {
headers
@@ -397,16 +392,15 @@ pub fn request_id(headers: &reqwest::header::HeaderMap) -> Option {
/// What a response says about itself, past its body.
///
-/// A struct rather than three reads at the call site because `bytes()`
-/// consumes the response: whatever is not taken before it is unrecoverable.
-/// Taking only `Content-Type` is what left paginated listings truncating
-/// silently and left a 500 with nothing to quote to support.
+/// A struct instead of three reads at the call site, because `bytes()`
+/// consumes the response — whatever isn't taken before that is gone for
+/// good. Taking only `Content-Type` used to leave paginated listings
+/// truncating silently, and a 500 with nothing to quote to support.
struct ResponseHeaders {
/// Decides whether the body is read as text or written as bytes.
content_type: String,
- /// The request id — what support needs to find this one request in their
- /// logs. Carried into the error and never printed on success, because on
- /// a response that worked it is noise.
+ /// What support needs to find this request in their logs. Carried
+ /// into the error and never printed on success, where it's just noise.
request_id: Option,
/// The `rel="next"` target of a `Link` header, when this response is one
/// page of several.
@@ -415,8 +409,8 @@ struct ResponseHeaders {
impl ResponseHeaders {
fn read(headers: &reqwest::header::HeaderMap) -> Self {
- // A header present but empty says nothing, and an empty request id
- // would print as `Request ID:` with a blank after it.
+ // A present-but-empty header says nothing; treating it as missing
+ // avoids printing "Request ID:" with nothing after it.
let text = |name: &str| {
headers
.get(name)
@@ -437,31 +431,29 @@ impl ResponseHeaders {
}
}
-/// A response that is one page of several, and how to ask for the next one.
+/// A response that's one page of several, and how to ask for the next one.
///
-/// Holds the flags rather than the URL: following the `Link` target verbatim
-/// would mean re-sending a URL the API built, token and all, while the flags
-/// are something a caller can read, edit and run.
+/// Holds the flags, not the URL: following the `Link` target verbatim would
+/// mean re-sending a URL the API built (token and all), while flags are
+/// something a caller can read, edit, and run.
struct NextPage(Option);
impl NextPage {
- /// Derives the flags from the operation rather than hardcoding `--start`.
+ /// Derives the flags from the operation instead of hardcoding `--start`.
///
/// The next URL's query is matched against the parameters this command
/// declares, so the tip names whatever the spec calls its paging
- /// parameters, and a service that pages some other way needs no change
- /// here.
+ /// parameters — a service that pages differently needs no change here.
///
- /// **The access token cannot appear in the result.** It rides in the
- /// query string of every request, so the API echoes it back in this very
- /// URL. Two things keep it out, and the second is why the first is not
- /// enough: `dispatch` adds it directly rather than declaring it in
- /// `op.query_params`, so matching against the declared parameters
- /// excludes it today — but a spec is free to declare a parameter by that
- /// name, and then "by construction" would quietly stop being true, so it
- /// is also refused explicitly. `the_page_tip_never_names_the_access_token`
- /// and `a_declared_parameter_named_access_token_is_still_withheld` hold
- /// both halves.
+ /// **The access token can never appear in the result.** It rides in the
+ /// query string of every request, so the API echoes it back in this
+ /// very URL. Two safeguards, not one: `dispatch` adds the token
+ /// directly rather than declaring it in `op.query_params`, so matching
+ /// against declared parameters already excludes it — but a spec could
+ /// declare a parameter with that same name someday, so it's also
+ /// refused explicitly. `the_page_tip_never_names_the_access_token` and
+ /// `a_declared_parameter_named_access_token_is_still_withheld` test
+ /// both.
fn of(declared: &[Parameter], next: &str) -> Self {
let flags: Vec = query_pairs(next)
.into_iter()
@@ -481,10 +473,10 @@ impl NextPage {
fn tip(&self) -> String {
match &self.0 {
Some(flags) => format!("More results: add `{flags}` for the next page."),
- // Reachable only if the API pages an operation whose spec
- // declares no paging parameter — a spec gap, not a user error.
- // Saying so beats saying nothing, because the result is
- // incomplete either way and only this knows it.
+ // Only reachable if the API paginates an operation whose spec
+ // declares no paging parameter — a spec gap, not a user
+ // error. Still worth saying, since the result is incomplete
+ // either way.
None => "More results exist, but this command declares no parameter to reach them."
.to_string(),
}
@@ -504,11 +496,11 @@ impl NextPage {
/// A URL's query, decoded.
///
-/// Percent-decoded on purpose: the values go into a tip meant to be copied
-/// onto a command line, and the CLI re-encodes whatever it is given — so
-/// handing back `%2B` would round-trip to `%252B` and ask for the wrong page.
-/// An unparseable URL yields nothing rather than failing: a tip is not worth
-/// turning a successful request into an error.
+/// Percent-decoded on purpose: these values go into a tip meant to be
+/// copied onto a command line, and the CLI re-encodes whatever it's given
+/// — so handing back `%2B` would round-trip to `%252B` and ask for the
+/// wrong page. An unparseable URL yields nothing rather than an error; a
+/// pagination tip isn't worth turning a successful request into a failure.
fn query_pairs(url: &str) -> Vec<(String, String)> {
match reqwest::Url::parse(url) {
Ok(parsed) => parsed
@@ -521,9 +513,9 @@ fn query_pairs(url: &str) -> Vec<(String, String)> {
/// A query value as it would have to be typed into a shell.
///
-/// Paging cursors are opaque ids in practice, but the tip is advice a reader
-/// pastes, and an unquoted value with a space in it would silently become
-/// two arguments.
+/// Paging cursors are opaque ids in practice, but this tip gets pasted
+/// verbatim, and an unquoted value with a space in it would silently
+/// become two arguments.
fn shell_value(value: &str) -> String {
let safe = |c: char| c.is_ascii_alphanumeric() || "-_.~:@+,".contains(c);
if !value.is_empty() && value.chars().all(safe) {
@@ -534,17 +526,17 @@ fn shell_value(value: &str) -> String {
/// Adds "there are more pages" to a `--id` that matched nothing.
///
-/// `pick_row` searches the rows it was handed and says "No row has the id",
-/// which is true of the page and may well be false of the listing. On a
-/// paginated response that is the most misleading form of the truncation
-/// this whole path exists to stop, so the error says which it means.
+/// `pick_row` says "No row has the id", which is true of this page but
+/// might be false of the whole listing. On a paginated response, that's
+/// the most misleading version of the truncation problem this whole path
+/// exists to prevent — so the error clarifies which one it means.
fn with_page_context(err: anyhow::Error, next_page: Option<&NextPage>) -> anyhow::Error {
let Some(next_page) = next_page else {
return err;
};
match err.downcast::() {
- // `not_a_list` is about the shape of the response, which another
- // page would not change.
+ // `not_a_list` is about the shape of the response — another page
+ // wouldn't change that.
Ok(cli) if cli.code == "not_found" => cli
.with_remedy(Remedy::default().with_fix(&next_page.fix()))
.into(),
@@ -555,10 +547,10 @@ fn with_page_context(err: anyhow::Error, next_page: Option<&NextPage>) -> anyhow
/// The request line a reader may safely see: URL, query, token replaced.
///
-/// The access token rides in the query string, so every rendering of this URL
-/// has to strip it. `--debug` has always done so; the dry run needs it more,
-/// because the request *is* its output — the thing most likely to be pasted
-/// into an issue or captured whole by CI.
+/// The access token rides in the query string, so every rendering of this
+/// URL has to strip it. `--debug` has always done this; the dry run needs
+/// it even more, since the request *is* its output — the part most likely
+/// to get pasted into an issue or captured whole by CI.
fn redacted_url(url: &str, query: &[(String, String)]) -> String {
let rendered: Vec = query
.iter()
@@ -621,9 +613,9 @@ fn encode_query_value(value: &str) -> String {
/// One query parameter's value, or the stand-in when it is the token.
///
-/// The single place that decides what may be printed. Both renderings of the
-/// query — the URL line and the JSON object — go through it, so the rule
-/// cannot come to differ between the two.
+/// The single place deciding what may be printed. Both renderings of the
+/// query — the URL line and the JSON object — go through this, so the
+/// rule can't drift between the two.
fn shown_value<'a>(name: &str, value: &'a str) -> &'a str {
if name == ACCESS_TOKEN {
REDACTED
@@ -634,11 +626,11 @@ fn shown_value<'a>(name: &str, value: &'a str) -> &'a str {
/// The dry run's answer: what the real call would send, and nothing sent.
///
-/// It leaves through `output::emit` like any other result, and it goes to
-/// stdout for the same reason — the command was asked what it would do, and
-/// this is the answer to that question, not a note about it. So `-o json`
-/// gives a script an object to assert on before it lets a delete run for
-/// real, and a terminal gets one readable request line.
+/// Goes through `output::emit` and out to stdout like any other result —
+/// the command was asked what it would do, and this *is* the answer, not
+/// a note about it. So `-o json` gives a script an object to check before
+/// letting a real delete run, and a terminal gets one readable request
+/// line.
fn describe_request(
op: &Operation,
mode: Mode,
@@ -658,9 +650,9 @@ fn describe_request(
text.push_str(&described.text);
}
- // An object, not a list of pairs: no query parameter can be repeated
- // here, since each one comes from an argument clap keeps a single value
- // for, and `.query.access_token` is worth more to a caller than
+ // An object, not a list of pairs: no query parameter repeats here,
+ // since each comes from an argument clap keeps one value for, and
+ // `.query.access_token` is more useful to a caller than
// `.query[3].value`.
let shown_query: serde_json::Map = query
.iter()
@@ -677,9 +669,9 @@ fn describe_request(
"dry_run": true,
"command": op.command(),
"method": method,
- // Without the query string, which `query` carries broken out.
- // Rejoining the two would only produce a URL nobody can use, the
- // token in it being redacted.
+ // No query string here — `query` carries that separately.
+ // Rejoining them would produce a URL nobody can use, since the
+ // token in it is redacted.
"url": url,
"query": shown_query,
"body": described.map(|described| described.json),
@@ -696,12 +688,13 @@ struct DescribedBody {
/// Describes the body — and validates it exactly as sending would.
///
-/// Every check [`attach_body`] makes is made here too: the JSON is parsed,
-/// and each `--file` is read rather than stat-ed. Reading is the point. A dry
-/// run answers "would this work?", and a file that exists but cannot be read
-/// passes a metadata check and fails the real call — which is precisely the
-/// surprise the flag exists to rule out. The operations taking `--file` take
-/// sprites and upload chunks, so the read costs nothing worth saving.
+/// Every check [`attach_body`] makes happens here too: the JSON gets
+/// parsed, and each `--file` gets read, not just stat-ed. Reading matters
+/// because a dry run answers "would this work?", and a file that exists
+/// but can't be read would pass a metadata check and then fail the real
+/// call — exactly the surprise `--dry-run` is meant to rule out. The
+/// operations that take `--file` (sprites, upload chunks) are small, so
+/// the read costs nothing worth skipping.
fn describe_body(source: &BodySource<'_>) -> Result