thinwrap_location
thinwrap_location for Erlang, Elixir and Gleam — one Hex package, a unified API across 6 providers for routing, matrix, geocoding and isochrone, with built-in polyline utilities. Zero runtime deps (OTP httpc), stateless, BYO transport.
Erlang constraints
- Runtime dependencies
- Zero runtime dependencies
- BYO HTTP client
- transport behaviour: transport => {Module, Opts}, where Module exports request/2
- Provenance
- Hex registry signatures + lockfile checksums
- Minimum runtime
- Erlang/OTP 27+
- Truly zero dependencies — OTP only: httpc (inets + ssl) is the only HTTP surface, the stdlib json module parses JSON, and AWS SigV4 (SES, SNS, Bedrock) / JWT signing (FCM RS256, APNs ES256) are hand-rolled on crypto and public_key.
- Hex has no build-provenance attestation like npm provenance or PEP 740: the registry signs its index and your lockfile pins each tarball checksum, which proves integrity, not where the package was built.
- Stateless: no supervision tree, ETS tables or application env to configure. The default httpc transport is HTTPS-only and never follows redirects — a 3xx surfaces as an error rather than silently re-sending auth headers to another host.
30-second install
{deps, [{thinwrap_location, "~> 1.3"}]}.{:thinwrap_location, "~> 1.3"}gleam add thinwrap_location gleam_erlang envoy2-minute end-to-end example
Compute a route between two waypoints through any supported provider. The examples use Mapbox; switching providers means changing the config's provider and credentials — the input and the result shape stay identical. One package, three ways to call it:
Erlang
Paste into a function body or the shell. Reads the key from MAPBOX_TOKEN. More on calling it from Erlang: /erlang.
Config = #{provider => mapbox,
access_token => list_to_binary(os:getenv("MAPBOX_TOKEN"))},
case thinwrap_location:route(Config, #{
waypoints => [#{lat => 37.7749, lng => -122.4194},
#{lat => 37.3382, lng => -121.8863}]
}) of
{ok, #{total_distance_meters := M, total_duration_seconds := S, polyline := Polyline}} ->
io:format("~.1f km, ~B min~n", [M / 1000, round(S / 60)]),
io:format("~s~n", [Polyline]); % Google precision-5 encoded
{error, #{code := Code, message := Msg}} ->
io:format("~s: ~ts~n", [Code, Msg])
end.Elixir
Top-level script code (mix run). Reads the key from MAPBOX_TOKEN. More on calling it from Elixir: /elixir.
config = %{provider: :mapbox, access_token: System.fetch_env!("MAPBOX_TOKEN")}
case :thinwrap_location.route(config, %{
waypoints: [%{lat: 37.7749, lng: -122.4194}, %{lat: 37.3382, lng: -121.8863}]
}) do
{:ok, %{total_distance_meters: m, total_duration_seconds: s, polyline: polyline}} ->
IO.puts("#{Float.round(m / 1000, 1)} km, #{round(s / 60)} min")
# Google precision-5 encoded
IO.puts(polyline)
{:error, %{code: code, message: message}} ->
IO.puts("#{code}: #{message}")
endGleam
A complete module. Besides the package it uses gleam_erlang for atoms and envoy to read MAPBOX_TOKEN; rebar3 must be on PATH to build the package. More on calling it from Gleam: /gleam.
import envoy
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode
import gleam/erlang/atom
import gleam/float
import gleam/int
import gleam/io
import gleam/list
@external(erlang, "thinwrap_location", "route")
fn route(config: Dynamic, input: Dynamic) -> Result(Dynamic, Dynamic)
pub fn main() -> Nil {
let assert Ok(token) = envoy.get("MAPBOX_TOKEN")
let config =
obj([
#("provider", atom("mapbox")),
#("access_token", dynamic.string(token)),
])
let input =
obj([
#(
"waypoints",
dynamic.list([at(37.7749, -122.4194), at(37.3382, -121.8863)]),
),
])
case route(config, input) {
Ok(res) -> {
let summary = {
use m <- decode.field(key("total_distance_meters"), decode.float)
use s <- decode.field(key("total_duration_seconds"), decode.float)
use polyline <- decode.field(key("polyline"), decode.string)
decode.success(#(m, s, polyline))
}
let assert Ok(#(m, s, polyline)) = decode.run(res, summary)
io.println(
float.to_string(float.to_precision(m /. 1000.0, 1))
<> " km, "
<> int.to_string(float.round(s /. 60.0))
<> " min",
)
// Google precision-5 encoded
io.println(polyline)
}
Error(err) -> io.println(describe(err))
}
}
// The library speaks atom-keyed Erlang maps; these helpers build and read them.
fn key(name: String) -> atom.Atom {
atom.create(name)
}
fn atom(name: String) -> Dynamic {
atom.to_dynamic(atom.create(name))
}
fn obj(fields: List(#(String, Dynamic))) -> Dynamic {
dynamic.properties(list.map(fields, fn(f) { #(atom(f.0), f.1) }))
}
fn at(lat: Float, lng: Float) -> Dynamic {
obj([#("lat", dynamic.float(lat)), #("lng", dynamic.float(lng))])
}
fn describe(err: Dynamic) -> String {
let error = {
use code <- decode.field(key("code"), atom.decoder())
use message <- decode.field(key("message"), decode.string)
decode.success(atom.to_string(code) <> ": " <> message)
}
let assert Ok(line) = decode.run(err, error)
line
}Same call for every routing provider — setprovider to google, here,esri, tomtom, or osrm (with abase_url). Every connector returns the same result map: meters, seconds, and a Google precision-5 polyline.
Supported providers
| Provider | Operations | Auth | README |
|---|---|---|---|
google Google Maps Platform | routing, matrix, geocoding | query | docs |
mapbox Mapbox | routing, matrix, geocoding, isochrone | query | docs |
here HERE | routing, matrix, geocoding, isochrone | header | docs |
esri ESRI ArcGIS | routing, matrix, geocoding, isochrone | bearer | docs |
tomtom TomTom | routing, matrix, geocoding, isochrone | query | docs |
osrm OSRM (self-hosted) | routing, matrix | header | docs |
Migration paths
Built-in polyline utilities
Every routing connector returns a Google precision-5 encodedpolyline. thinwrap_location_polyline decodes and re-encodes it and converts HERE's flex-polyline and ESRI's path geometry without a third-party library: encode/1,decode/1, decode_flex/1 andencode_esri_paths/1 — the same four primitives as every sibling.
Operation coverage
Provider × operation gaps below are architectural, not bugs:
- OSRM ships only
route+matrix— geocoding and isochrone are out of scope by design for OSRM. Callingthinwrap_location:geocode/2with an OSRM config raises{badarg, <<"osrm does not support geocode">>}before any request. - Google does not expose a public isochrone API.
Bring your own transport
The default transport is OTP httpc: it never follows redirects and refuses cleartext http:// URLs. To reach a self-hosted OSRM over plain HTTP, or to use another HTTP client, pass a module implementing request/2 astransport => {Module, Opts}. The wrapper holds no state — the HERE/TomTom async-matrix submit-poll-retrieve cycle stays inside a single matrix call.
Cross-language siblings
The TypeScript (/packages/location-ts), PHP (/packages/location-php), Go (/packages/location-go), and Python (/packages/location-py) packages ship the same API shape and provider set — cross-language parity is a design goal, not a hard release gate.