Skip to content

Commit 99ad5d8

Browse files
authored
Reduce unsafe atom keys conversion (#30)
1 parent 240aa8b commit 99ad5d8

2 files changed

Lines changed: 168 additions & 9 deletions

File tree

lib/open_api_typesense/client.ex

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,19 +130,21 @@ defmodule OpenApiTypesense.Client do
130130
Enum.map_join(body, "\n", &Jason.encode_to_iodata!/1)
131131
end
132132

133+
defp parse_content_type({"application/json", {mod, :t}}, body) when is_atom(mod) do
134+
atom_keys = Map.keys(mod.__struct__()) |> Enum.reject(&(&1 == :__struct__))
135+
string_keys = Enum.map(atom_keys, &to_string/1)
136+
keys = atom_keys ++ string_keys
137+
138+
body
139+
|> Map.take(keys)
140+
|> OpenApiTypesense.Converter.to_atom_keys(safe: false)
141+
|> Jason.encode_to_iodata!()
142+
end
143+
133144
defp parse_content_type({"application/json", _}, body) do
134145
Jason.encode_to_iodata!(body)
135146
end
136147

137-
# defp parse_content_type({"application/json", {mod, :t}}, body) do
138-
# # Checks if map keys are atom or string
139-
# if Enum.all?(Map.keys(body), &is_atom/1) do
140-
# Jason.encode_to_iodata!(struct(mod, body))
141-
# else
142-
# Jason.encode_to_iodata!(body)
143-
# end
144-
# end
145-
146148
# Some resources are missing 4xx descriptions, hence we will set a default
147149
# instead so we can see the actual error message instead of stacktrace.
148150
# See https://github.com/typesense/typesense-api-spec/pull/84
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
defmodule OpenApiTypesense.Converter do
2+
@moduledoc """
3+
Converting your maps/structs that contains string or mix of atom keys into full atom keyed maps.
4+
"""
5+
6+
defstruct safe: true,
7+
underscore: true,
8+
high_perf: false,
9+
ignore: false
10+
11+
@spec to_atom_keys(
12+
map() | list() | tuple() | struct(),
13+
map() | list() | %__MODULE__{}
14+
) :: map() | list() | tuple()
15+
def to_atom_keys(value, opts \\ %{})
16+
17+
def to_atom_keys(%{__struct__: _type} = struct, %__MODULE__{} = opts) when is_struct(struct) do
18+
struct
19+
|> Map.from_struct()
20+
|> to_atom_keys(opts)
21+
end
22+
23+
def to_atom_keys(map, %__MODULE__{} = opts) when is_map(map) do
24+
Map.new(map, fn {k, v} -> {convert_key(k, opts), to_atom_keys(v, opts)} end)
25+
end
26+
27+
def to_atom_keys(list, %__MODULE__{} = opts) when is_list(list) do
28+
Enum.map(list, &to_atom_keys(&1, opts))
29+
end
30+
31+
def to_atom_keys(tuple, %__MODULE__{} = opts) when is_tuple(tuple) do
32+
tuple
33+
|> Tuple.to_list()
34+
|> to_atom_keys(opts)
35+
|> List.to_tuple()
36+
end
37+
38+
def to_atom_keys(val, opts = %__MODULE__{}) when is_struct(opts) do
39+
val
40+
end
41+
42+
def to_atom_keys(val, opts = %{}) when is_map(opts) do
43+
to_atom_keys(val, struct(__MODULE__, opts))
44+
end
45+
46+
def to_atom_keys(val, opts) when is_list(opts) do
47+
to_atom_keys(val, Enum.into(opts, %{}))
48+
end
49+
50+
defp convert_key(key, opts) do
51+
key
52+
|> as_underscore(opts.underscore, opts.high_perf)
53+
|> as_atom(opts.safe, opts.ignore)
54+
end
55+
56+
@spec as_atom(String.t() | atom(), safe? :: boolean(), ignore? :: boolean()) ::
57+
atom() | String.t()
58+
defp as_atom(key, true, true) when is_binary(key) do
59+
as_atom(key, true, false)
60+
rescue
61+
ArgumentError -> key
62+
end
63+
64+
defp as_atom(key, true, false) when is_binary(key) do
65+
String.to_existing_atom(key)
66+
end
67+
68+
defp as_atom(key, false, _) when is_binary(key) do
69+
String.to_atom(key)
70+
end
71+
72+
defp as_atom(key, _, _), do: key
73+
74+
@spec as_underscore(
75+
String.t() | atom(),
76+
underscore? :: boolean(),
77+
high_perf? :: boolean()
78+
) :: String.t()
79+
defp as_underscore(key, true, false) when is_binary(key) do
80+
underscore(key)
81+
end
82+
83+
defp as_underscore(key, true, false) when is_atom(key) do
84+
key
85+
|> Atom.to_string()
86+
|> as_underscore(true, false)
87+
end
88+
89+
defp as_underscore(key, true, true) when is_binary(key) do
90+
high_perf_underscore(key)
91+
end
92+
93+
defp as_underscore(key, true, true) when is_atom(key) do
94+
key
95+
|> Atom.to_string()
96+
|> as_underscore(true, true)
97+
end
98+
99+
defp as_underscore(key, _, _), do: key
100+
101+
@spec underscore(atom() | String.t()) :: String.t()
102+
defp underscore(key) do
103+
key
104+
|> Macro.underscore()
105+
|> String.replace(~r/-/, "_")
106+
end
107+
108+
@spec high_perf_underscore(String.t() | charlist()) :: String.t() | charlist()
109+
defp high_perf_underscore(key) when is_binary(key) do
110+
key
111+
|> String.to_charlist()
112+
|> high_perf_underscore()
113+
|> to_string()
114+
end
115+
116+
defp high_perf_underscore([h | t]) do
117+
[to_lower_char(h)] ++ to_underscore(t, h)
118+
end
119+
120+
defp high_perf_underscore(~c"") do
121+
~c""
122+
end
123+
124+
@spec to_underscore(charlist(), char()) :: charlist()
125+
defp to_underscore([h | t], prev) when prev == ?_ or prev == ?- do
126+
[to_lower_char(h)] ++ to_underscore(t, h)
127+
end
128+
129+
defp to_underscore([h0, h1 | t], _prev)
130+
when h0 >= ?A and h0 <= ?Z and not (h1 >= ?A and h1 <= ?Z) and h1 != ?_ and h1 != ?- do
131+
[?_, to_lower_char(h0), h1] ++ to_underscore(t, h1)
132+
end
133+
134+
defp to_underscore([h | t], prev)
135+
when h >= ?A and h <= ?Z and not (prev >= ?A and prev <= ?Z) and prev != ?_ and prev != ?- do
136+
[?_, to_lower_char(h)] ++ to_underscore(t, h)
137+
end
138+
139+
defp to_underscore([h | t], _prev) do
140+
[to_lower_char(h)] ++ to_underscore(t, h)
141+
end
142+
143+
defp to_underscore(~C"", _h) do
144+
~C""
145+
end
146+
147+
@spec to_lower_char(char()) :: char()
148+
defp to_lower_char(?-), do: ?_
149+
150+
defp to_lower_char(char) when char >= ?A and char <= ?Z do
151+
char + 32
152+
end
153+
154+
defp to_lower_char(char) do
155+
char
156+
end
157+
end

0 commit comments

Comments
 (0)