Skip to content

Commit b36a3b9

Browse files
committed
feat(json): accept atom keys in from_decoded/2
Closes #373 Without atom-key support, callers that build maps in Elixir or use `Jason.decode(..., keys: :atoms)` are forced to pre-convert keys to strings before feeding the data to `Protobuf.JSON.from_decoded/2`. Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent f402fee commit b36a3b9

3 files changed

Lines changed: 50 additions & 1 deletion

File tree

lib/protobuf/json.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,13 @@ defmodule Protobuf.JSON do
362362
iex> Protobuf.JSON.from_decoded(%{"color" => "GREEN","topSpeed" => 80.0}, Car)
363363
{:ok, %Car{color: :GREEN, top_speed: 80.0}}
364364
365+
Field names may also be supplied as atoms (matching either the proto field
366+
name or the JSON name), which is convenient when consuming data produced by
367+
`Jason.decode(..., keys: :atoms)` or built in Elixir:
368+
369+
iex> Protobuf.JSON.from_decoded(%{color: "RED"}, Car)
370+
{:ok, %Car{color: :RED, top_speed: 0.0}}
371+
365372
By default, an unknown enum string value is rejected:
366373
367374
iex> {:error, %Protobuf.JSON.DecodeError{}} =

lib/protobuf/json/decode.ex

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,27 @@ defmodule Protobuf.JSON.Decode do
270270
defp null_value?(nil, %Protobuf.FieldProps{type: {:enum, Google.Protobuf.NullValue}}), do: false
271271
defp null_value?(value, _props), do: is_nil(value)
272272

273-
defp fetch_field_value(%Protobuf.FieldProps{name: name_key, json_name: json_key}, data) do
273+
defp fetch_field_value(
274+
%Protobuf.FieldProps{name: name_key, json_name: json_key, name_atom: name_atom},
275+
data
276+
) do
274277
case data do
275278
%{^json_key => value} -> {:ok, value}
276279
%{^name_key => value} -> {:ok, value}
280+
%{^name_atom => value} -> {:ok, value}
281+
_ -> fetch_by_existing_atom(json_key, data)
282+
end
283+
end
284+
285+
defp fetch_by_existing_atom(json_key, data) do
286+
atom = :erlang.binary_to_existing_atom(json_key, :utf8)
287+
288+
case data do
289+
%{^atom => value} -> {:ok, value}
277290
_ -> :error
278291
end
292+
rescue
293+
ArgumentError -> :error
279294
end
280295

281296
defp decode_value(%{optional?: true, type: type}, nil, _opts)

test/protobuf/json/decode_test.exs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,33 @@ defmodule Protobuf.JSON.DecodeTest do
4343
end
4444
end
4545

46+
describe "atom keys" do
47+
test "proto field name as atom key" do
48+
data = %{string: "hello"}
49+
decoded = %Scalars{string: "hello"}
50+
assert decode(data, Scalars) == {:ok, decoded}
51+
end
52+
53+
test "JSON (camelCase) field name as atom key" do
54+
_ = :optionalString
55+
data = %{optionalString: "hello"}
56+
assert {:ok, decoded} = decode(data, TestAllTypesProto3)
57+
assert decoded.optional_string == "hello"
58+
end
59+
60+
test "string keys still win when both forms are present" do
61+
data = %{"string" => "from-string", :string => "from-atom"}
62+
decoded = %Scalars{string: "from-string"}
63+
assert decode(data, Scalars) == {:ok, decoded}
64+
end
65+
66+
test "mixed atom and string keys across fields" do
67+
data = %{:string => "hi", "bool" => true}
68+
decoded = %Scalars{string: "hi", bool: true}
69+
assert decode(data, Scalars) == {:ok, decoded}
70+
end
71+
end
72+
4673
describe "booleans" do
4774
test "actual boolean is valid" do
4875
data = %{"bool" => true}

0 commit comments

Comments
 (0)