diff --git a/lib/geo/json/decoder.ex b/lib/geo/json/decoder.ex index b42999d..673bc6a 100644 --- a/lib/geo/json/decoder.ex +++ b/lib/geo/json/decoder.ex @@ -46,7 +46,7 @@ defmodule Geo.JSON.Decoder do Enum.map(Map.get(geo_json, "geometries"), fn x -> do_decode( Map.get(x, "type"), - Map.get(x, "coordinates"), + ensure_numeric(x["coordinates"]), Map.get(x, "properties", %{}), crs ) @@ -62,7 +62,7 @@ defmodule Geo.JSON.Decoder do do_decode( Map.get(geo_json, "type"), - Map.get(geo_json, "coordinates"), + ensure_numeric(geo_json["coordinates"]), Map.get(geo_json, "properties", %{}), crs ) @@ -205,7 +205,12 @@ defmodule Geo.JSON.Decoder do properties: properties } else - do_decode(Map.get(geometry, "type"), Map.get(geometry, "coordinates"), properties, nil) + do_decode( + Map.get(geometry, "type"), + ensure_numeric(geometry["coordinates"]), + properties, + nil + ) end end @@ -231,4 +236,47 @@ defmodule Geo.JSON.Decoder do defp get_srid(nil) do nil end + + # Fast paths for the common (correct) cases + defp ensure_numeric(num) when is_number(num), do: num + defp ensure_numeric([x, y] = l) when is_number(x) and is_number(y), do: l + + defp ensure_numeric([x, y, z] = l) + when is_number(x) and is_number(y) and (is_number(z) or is_nil(z)) do + l + end + + defp ensure_numeric([x, y, z, m] = l) + when is_number(x) and is_number(y) and (is_number(z) or is_nil(z)) and + (is_number(m) or is_nil(z)) do + l + end + + defp ensure_numeric(l) when is_list(l) do + Enum.map(l, fn + num when is_number(num) -> + num + + str when is_binary(str) -> + try do + String.to_float(str) + catch + ArgumentError -> + raise ArgumentError, "expected a numeric coordinate, got the string #{inspect(str)}" + end + + nil -> + nil + + l when is_list(l) -> + Enum.map(l, &ensure_numeric/1) + + other -> + raise ArgumentError, "expected a numeric coordinate, got: #{inspect(other)}" + end) + end + + defp ensure_numeric(other) do + raise ArgumentError, "expected a numeric coordinate, got: #{inspect(other)}" + end end diff --git a/test/geo/json_test.exs b/test/geo/json_test.exs index 0dbbf5b..193be51 100644 --- a/test/geo/json_test.exs +++ b/test/geo/json_test.exs @@ -387,6 +387,67 @@ defmodule Geo.JSON.Test do assert geom.geometries == [] end + test "Decode seamlessly converts coordinates that are numbers-as-strings" do + check all( + x <- float(), + y <- float() + ) do + json = """ + { + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": ["#{x}", "#{y}"] + }, + "type": "Feature" + } + """ + + assert %Geo.Point{coordinates: {^x, ^y}} = Jason.decode!(json) |> Geo.JSON.decode!() + end + end + + test "Decode rejects geometries with non-numeric coordinates" do + for {bad_x, bad_y} <- [ + {" 100.0", "0.0"}, + {"100.0", "0.0?"}, + {"100.", "0.0"}, + {"100.0", nil, "0.0"} + ] do + json = """ + { + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [#{inspect(bad_x)}, #{inspect(bad_y)}] + }, + "type": "Feature" + } + """ + + assert_raise ArgumentError, fn -> + Jason.decode!(json) |> Geo.JSON.decode!() + end + end + end + + test "Decode rejects geometries with garbage coordinates" do + json = """ + { + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": {"x": 1.0, "y": 2.0} + }, + "type": "Feature" + } + """ + + assert_raise ArgumentError, fn -> + Jason.decode!(json) |> Geo.JSON.decode!() + end + end + property "encodes and decodes back to the correct Point struct" do check all( x <- float(),