Skip to content

Return an error tuple from Geo.JSON.decode/1 for any decode failure - #254

Open
youdie006 wants to merge 1 commit into
felt:masterfrom
youdie006:fix/json-decode-error-tuple
Open

Return an error tuple from Geo.JSON.decode/1 for any decode failure#254
youdie006 wants to merge 1 commit into
felt:masterfrom
youdie006:fix/json-decode-error-tuple

Conversation

@youdie006

Copy link
Copy Markdown

The problem

Geo.JSON.decode/1 is the non-raising variant, but its rescue only catches DecodeError, so anything else raised while decoding escapes:

Geo.JSON.decode(%{"type" => "Point", "coordinates" => "1,2"})
** (ArgumentError) expected a numeric coordinate, got: "1,2"
    (geo 4.1.0) lib/geo/json/decoder.ex:308: Geo.JSON.Decoder.ensure_numeric/1
    (geo 4.1.0) lib/geo/json/decoder.ex:69: Geo.JSON.Decoder.decode!/1
    (geo 4.1.0) lib/geo/json/decoder.ex:113: Geo.JSON.Decoder.decode/1

Across a few inputs, all through the public Geo.JSON.decode/1:

Point coords is a string     -> RAISED ArgumentError
LineString coords string     -> RAISED ArgumentError
FeatureCollection nil        -> RAISED Protocol.UndefinedError
unknown type (control)       -> {:error, %DecodeError{}}
valid Point (control)        -> {:ok, ...}

ensure_numeric/1 (lib/geo/json/decoder.ex:302, :308) raises ArgumentError, and Enum.map over a nil "features" (:84) raises Protocol.UndefinedError. Neither is a DecodeError, so both escape a function whose spec says it returns {:error, _}.

Why this is the code and not the spec

The two sibling codecs already rescue everything:

# lib/geo/wkt/decoder.ex:29-36
@spec decode(binary) :: {:ok, Geo.geometry()} | {:error, Exception.t()}
def decode(wkb) do
  {:ok, decode!(wkb)}
rescue
  exception ->
    {:error, exception}
end

Geo.WKB.encode/1 (lib/geo/wkb.ex:37-44) is the same shape. The GeoJSON decoder is the only one with a narrowed rescue, so this brings it in line rather than inventing a pattern.

The ArgumentError path arrived with ensure_numeric/1 in #218, which made a new raise reachable from decode/1 without widening its rescue.

The change

Bare rescue in decode/1, and the spec widened to Exception.t() to match WKT and WKB — in lib/geo/json/decoder.ex and the delegating spec in lib/geo/json.ex.

The alternative would be to keep DecodeError.t() and wrap escapees in a DecodeError (it already carries a :value field for exactly that shape). I went with matching the siblings because it preserves the original exception, which is more useful when debugging, but I am happy to switch if you would rather the GeoJSON decoder keep the narrower return type.

Why the tests miss it

The coverage splits either side of the gap and never crosses:

  • test/geo/json_test.exs:330 is the only test driving non-bang decode/1, and it uses {"type": "random_type"} — a DecodeError, which is caught.
  • test/geo/json_test.exs:561-599 covers the ArgumentError path, but only through Geo.JSON.decode!/1 under assert_raise.

No test calls decode/1 with a non-numeric coordinate.

Verification

Added a test asserting {:error, _} for both escaping exception types, plus one pinning that a DecodeError still comes back as a DecodeError.

Reverting only the rescue clause and keeping the test fails with ** (ArgumentError) expected a numeric coordinate, got: "1,2".

Full suite: 3 doctests, 16 properties, 170 tests, 0 failures before — 171 tests, 0 failures after.


Disclosure: this patch was prepared with AI assistance. The reproduction, the red/green check and the suite runs above were executed against this branch; happy to adjust anything on request.

decode/1 is the non-raising variant, but its rescue only catches
DecodeError, so anything else raised while decoding escapes:

    Geo.JSON.decode(%{"type" => "Point", "coordinates" => "1,2"})
    ** (ArgumentError) expected a numeric coordinate, got: "1,2"

    Geo.JSON.decode(%{"type" => "FeatureCollection", "features" => nil})
    ** (Protocol.UndefinedError)

ensure_numeric/1 raises ArgumentError, and Enum.map on a nil "features"
raises Protocol.UndefinedError. Neither is a DecodeError.

The sibling codecs already rescue everything - Geo.WKT.decode/1 and
Geo.WKB.encode/1 both use a bare rescue with an Exception.t() spec - so
this brings the GeoJSON decoder in line with them rather than inventing
a pattern.

The ArgumentError path arrived with ensure_numeric/1 in felt#218 and is
reachable from decode/1, but decode/1's rescue was not widened with it.
The tests split either side of the gap: the only decode/1 test uses an
unknown type, which is a DecodeError and is caught, while the
ArgumentError cases are covered only through decode!/1 under
assert_raise.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant