Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert string coordinates to floats, or raise an error #218

Merged
merged 3 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 51 additions & 3 deletions lib/geo/json/decoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
)
Expand Down Expand Up @@ -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

Expand All @@ -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
63 changes: 62 additions & 1 deletion test/geo/json_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -522,7 +583,7 @@ defmodule Geo.JSON.Test do

geom = Jason.decode!(json) |> Geo.JSON.decode!()

assert %GeometryCollection{} = geom
assert %Geo.GeometryCollection{} = geom
assert length(geom.geometries) == 2
assert Enum.all?(geom.geometries, &match?(%Geo.MultiPolygon{}, &1))
assert geom.properties["id"] == "FLC017"
Expand Down
Loading