Skip to content

Add mapbox directions route - #43

Open
mattpodwysocki wants to merge 3 commits into
mainfrom
feat/directions-api
Open

mattpodwysocki wants to merge 3 commits into
mainfrom
feat/directions-api

Conversation

@mattpodwysocki

@mattpodwysocki mattpodwysocki commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

What

mapbox directions, the first command for the Navigation API category
(Directions, Isochrone, Map Matching, Matrix). None of these had CLI
coverage before this. Hand-authored into custom-openapi/ since
openapi-specs doesn't publish a spec for the Directions API yet, following
the same convention search already uses.

No subcommand: this API has exactly one operation, so there's nothing a
second word (like the old route) would disambiguate. Same shape as
mapbox usage. See spec::FLATTENED_SERVICES.

Routes between 2-25 waypoints for driving (with or without live traffic),
walking, or cycling.

Excluded on purpose:

  • The ~30 electric-vehicle-routing parameters (engine=electric and
    everything under it). These describe one vehicle's charge/discharge
    curve down to the watt, which the Directions API needs to plan charging
    stops along the route. That's data an integration passes in from its own
    vehicle profile, not something to hand-type as a CLI flag on every call.
    Left for a follow-up.
  • POST. This spec format has no way to express "GET or POST, caller's
    choice" for the same operation. A real gap for a very long coordinate
    list, not a design choice; noted in the spec's own header for whoever
    picks it up.

Two path-parameter bugs found and fixed generically

Both surfaced from profile's value (mapbox/driving etc.):

  1. A spec parameter named profile silently collided with the global
    --profile credentials flag.
    Clap has one namespace of ids per
    command; the generated positional replaced the global one outright
    instead of erring, so mapbox directions mapbox/driving ... failed with
    Invalid profile name "mapbox/driving". Fixed with a small, documented
    override table (ARG_NAME_OVERRIDES in src/spec.rs), same shape as
    the existing BODY_CONTENT_TYPE_OVERRIDES.
  2. The routing profile's / was being percent-encoded. The same
    escaping that stops a free-text path parameter from smuggling in extra
    path segments (../../tokens/v2/victim etc.) was turning
    mapbox/driving's / into %2F, a 404. profile is a free-form value
    now, not a fixed enum (see below), so it needed its own opt-in signal
    rather than riding on "has an enum": UNESCAPED_PATH_PARAMS in
    src/spec.rs names the (service, parameter) pairs whose values are
    trusted to carry a structural character like / on purpose. New
    path_segment_for in src/executor.rs, unit-tested against both listed
    and unlisted parameters.

Routing profile is free-form, not a fixed list of four

Caught in review: an earlier version of this PR validated profile
against the four documented values (mapbox/driving-traffic,
mapbox/driving, mapbox/walking, mapbox/cycling) client-side, via a
clap enum. Some OEM accounts have additional profiles of their own that
are never published to docs.mapbox.com, so that validation would have
broken this command for exactly the accounts that most need it. Fixed:
profile is now sent exactly as typed, with the description saying so
instead of listing four values as if they were exhaustive.

Verification

Smoke-tested against production, not just unit tests: real routes for
mapbox/driving and mapbox/walking, with --steps, --geometries geojson, --overview full, and --annotations all verified to return the
documented shape. Full transcript and captured response are in
docs/commands.md's new section.

621 tests, cargo fmt --check and cargo clippy --all-targets -- -D warnings both clean.

Not done

Isochrone, Map Matching, and Matrix are tracked as separate follow-up PRs,
not bundled into this one.

🤖 Generated with Claude Code

The Navigation API category (Directions, Isochrone, Map Matching, Matrix,
Optimization, EV Charge Finder) has had no CLI coverage — this is the first
of those, hand-authored into custom-openapi/ since openapi-specs publishes
no spec for it yet.

Excludes the ~30 electric-vehicle-routing parameters (engine=electric and
everything under it): those describe a vehicle's charging curve down to the
watt, which an integration passes in from a vehicle profile rather than
something to hand-type as a CLI flag. The EV Charge Finder API is a better
fit for that case. Also excludes POST, which this spec format has no way to
express alongside GET for the same operation — a real gap for a very long
coordinate list, not a design choice.

Two path-parameter bugs surfaced while wiring this up, fixed generically
rather than just for this command:

- A spec parameter literally named `profile` (the routing profile,
  mapbox/driving etc.) silently collided with the global --profile
  credentials flag — clap has one namespace of ids per command, and the
  generated positional replaced the global one outright instead of erring.
  `mapbox directions route mapbox/driving …` failed with `Invalid profile
  name "mapbox/driving"` before this. Fixed with a small, documented
  arg-name override table (ARG_NAME_OVERRIDES), the same shape
  BODY_CONTENT_TYPE_OVERRIDES already uses.
- An enum-constrained path parameter whose only valid values contain a
  literal `/` (again, mapbox/driving) was being percent-encoded to %2F by
  the same escaping that stops a free-text path parameter from smuggling in
  extra path segments or retargeting the request. Safe to skip once clap's
  PossibleValuesParser has already limited the value to one of the spec's
  own literal strings — there is nothing left for a caller to inject.

Smoke-tested against production: real routes for driving and walking
profiles, with --steps, --geometries geojson, --overview full, and
--annotations all verified to return the documented shape.

621 tests, fmt and clippy clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mattpodwysocki
mattpodwysocki requested a review from a team as a code owner September 23, 2026 21:49
first_sentence() in src/main.rs cuts a --help line at the first '.', which
is right after "`mapbox/driving` only" or "`mapbox/driving` and
`mapbox/driving-traffic`" on eight parameters here — a real sentence on its
own that was eating everything substantive that followed it in --help.
--schema and docs/commands.md were never affected; they show the full text.

Fixed by moving the profile-scope note to the end of each description
instead of the front, so the useful part survives the cut.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@danpat

danpat commented Sep 24, 2026

Copy link
Copy Markdown

The ~30 electric-vehicle-routing parameters (engine=electric and everything under it) — those describe a vehicle's charging curve down to the watt, which an integration passes in from a vehicle profile rather than something to hand type as a CLI flag. The EV Charge Finder API is a better fit for that case.

This sentence doesn't make a lot of sense. The EV Charge Finder API is for finding charging stations. The engine=electric parameter to the Directions API is for finding routes, and it requires the discharge/recharge profile for the vehicle to work.

Please make sure to validate that the vibe-coding here is reviewed by folks that know the APIs properly - it seems that the LLM is coming to some faulty conclusions somehow.

Comment thread docs/commands.md Outdated
Comment thread custom-openapi/directions/openapi/directions.yaml
Two pieces of review feedback landed on this PR (danpat, and product
guidance relayed separately), addressed together since both touch the same
lines:

1. Product guidance was to drop the verb for single-operation Nav
   commands: mapbox directions instead of mapbox directions route,
   matching the shape mapbox usage already has. Generalized rather than
   hand-coded per service: spec::FLATTENED_SERVICES names which services
   get this, build_service_command attaches the one operation's own args
   directly to the service-level command instead of nesting it, and run()'s
   dispatch skips the subcommand walk for one of these. Every reader of a
   command's identity (--schema, docs/commands.md, generate-skills,
   remedy's suggestions) goes through Operation::command(), which is now
   flattening-aware, so they all render correctly for free.

2. Real bug, not just wording: the routing profile's enum (the four
   documented values) rejected anything else client-side via clap's
   PossibleValuesParser. Some accounts — OEM agreements, mainly — have
   additional profiles never published to docs.mapbox.com, so the enum
   would have broken this command for exactly the customers who need a
   profile named beyond driving/walking/cycling. Reported in review.
   profile is now a free-form string, sent exactly as typed.

   Removing the enum broke the mechanism that kept profile's literal `/`
   from being percent-encoded — that fix was keyed on "does this parameter
   have an enum", which stopped being true. Replaced with a named table,
   spec::UNESCAPED_PATH_PARAMS, keyed on (service, parameter name) instead:
   the same outcome, but as an explicit decision rather than a side effect
   of a constraint that needed removing for an unrelated reason.

Also corrected a factual error a reviewer caught in this PR's own
explanatory text: EV Charge Finder is a location-search product (charging
stations near a point), not related to the Directions API's own
engine=electric vehicle-profile parameters that were excluded — the two
are unrelated capabilities, not alternatives for the same thing. Fixed
everywhere that sentence appeared (custom-openapi/directions.yaml,
CHANGELOG.md).

487 tests, fmt and clippy clean. Two new regression tests
(a_flattened_service_takes_no_subcommand, command_only_drops_the_path_for_a_flattened_service)
plus the existing path-escaping tests rewritten for the table-based
mechanism instead of the enum-based one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mattpodwysocki

Copy link
Copy Markdown
Contributor Author

@danpat you're right, and it wasn't just clumsy wording, it was actually wrong. EV Charge Finder finds charging station locations. engine=electric is a Directions parameter that changes how the route itself gets calculated, using the vehicle's discharge/recharge curve to plan charging stops along the way. Those are two different jobs, and saying one is "a better fit" for the other's doesn't hold up.

I've fixed the wording in the spec header comment and updated the PR description to match: it now describes what engine=electric actually needs (a full vehicle profile handed in from an integration, not something to hand-type) without the incorrect EV Charge Finder claim.

Fair point on review too. Appreciate you catching it.

mattpodwysocki added a commit that referenced this pull request Sep 25, 2026
Second of the Navigation-category APIs with no prior CLI coverage. Same
shape as mapbox directions route (#43): hand-authored into
custom-openapi/ since openapi-specs has no spec for this API either, and
reuses that PR's fix for a spec parameter named `profile` colliding with the
global --profile flag (ARG_NAME_OVERRIDES gets a second row, not a second
mechanism).

`contours_minutes` and `contours_meters` are mutually exclusive but neither
is individually required by this CLI's own validation — same "not enforced
before the request goes out" precedent search category already uses for its
own proximity/near/bbox/route disjunction. The API answers 422 if both or
neither are given.

Smoke-tested against production: real contour polygons and linestrings for
driving and walking profiles, --polygons, --contours-minutes with multiple
values, verified to return the documented GeoJSON shape.

Also fixes a self-inflicted --help regression found while writing this:
first_sentence() in src/main.rs cuts a --help line at the first '.', and
several profile-scoped parameter descriptions in directions.yaml (already
merged in this branch) led with a complete sentence before the substantive
content, e.g. "`mapbox/driving` only." — eating everything after it in
--help. isochrone.yaml's own `denoise` had the same shape ("0.0-1.0:
...") and would have rendered as literally "0". Both fixed by moving the
qualifier to the end of the description instead of the front.

485 tests, fmt and clippy clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mattpodwysocki added a commit that referenced this pull request Sep 25, 2026
Third of the Navigation-category APIs with no prior CLI coverage. Same
shape as directions/isochrone (#43, #44): hand-authored
into custom-openapi/ since openapi-specs has no spec for this API either,
reusing ARG_NAME_OVERRIDES for the same profile-vs-global-flag collision
(third row, not a third mechanism).

Excludes POST, for the same documented reason directions route does: this
spec format can't express "GET or POST, caller's choice" for one
operationId, and the API's own POST exists specifically for a trace too
long for a URL (~8100 bytes) — a real gap, not a design choice.

Smoke-tested against production: a three-point San Francisco trace
returned a real match with legs/steps/geometry, including a null
tracepoint for a point too far from the road network to match — the
documented shape for that case, not a bug.

486 tests, fmt and clippy clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants