Skip to content

Commit e34c50b

Browse files
Util from converting protos to map
1 parent 16415e7 commit e34c50b

File tree

1 file changed

+119
-0
lines changed
  • ee/ephemeral_environments/lib/ephemeral_environments/utils

1 file changed

+119
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
defmodule EphemeralEnvironments.Utils.Proto do
2+
@moduledoc """
3+
Utility functions for converting protobuf structs to plain Elixir maps.
4+
"""
5+
6+
@doc """
7+
Recursively converts a protobuf struct to a plain Elixir map.
8+
- Converts Google.Protobuf.Timestamp to DateTime
9+
- Converts enums to their atom names (INSTANCE_STATE_PROVISIONING -> :provisioning)
10+
- Recursively processes nested structs
11+
"""
12+
def to_map(nil), do: nil
13+
14+
def to_map(%Google.Protobuf.Timestamp{} = timestamp) do
15+
DateTime.from_unix!(timestamp.seconds, :second)
16+
|> DateTime.add(timestamp.nanos, :nanosecond)
17+
end
18+
19+
def to_map(%module{} = struct) when is_atom(module) do
20+
struct
21+
|> Map.from_struct()
22+
|> Enum.map(fn {key, value} -> {key, convert_value(value, module, key)} end)
23+
|> Map.new()
24+
end
25+
26+
def to_map(value), do: value
27+
28+
defp convert_value(value, module, field) when is_list(value) do
29+
Enum.map(value, &to_map/1)
30+
end
31+
32+
defp convert_value(value, module, field) when is_struct(value) do
33+
to_map(value)
34+
end
35+
36+
defp convert_value(value, module, field) when is_integer(value) do
37+
# Check if this field is an enum by looking at the field definition
38+
case get_enum_module(module, field) do
39+
nil -> value
40+
enum_module -> integer_to_atom(enum_module, value)
41+
end
42+
end
43+
44+
defp convert_value(value, module, field) when is_atom(value) do
45+
# Check if this is an enum atom that needs normalization
46+
case get_enum_module(module, field) do
47+
nil -> value
48+
enum_module -> normalize_enum_name(value, enum_module)
49+
end
50+
end
51+
52+
defp convert_value(value, _module, _field), do: value
53+
54+
# If given field is of type enum inside the parend module, the name of the enum module
55+
# will be returned. Otherwise it will return nil.
56+
defp get_enum_module(module, field) do
57+
try do
58+
field_props = module.__message_props__().field_props
59+
60+
# Find the field by name_atom
61+
field_info =
62+
field_props
63+
|> Enum.find(fn {_num, props} -> props.name_atom == field end)
64+
|> case do
65+
{_num, props} -> props
66+
nil -> nil
67+
end
68+
69+
if field_info && field_info.enum? do
70+
case field_info.type do
71+
{:enum, enum_module} -> enum_module
72+
_ -> nil
73+
end
74+
else
75+
nil
76+
end
77+
rescue
78+
_ -> nil
79+
end
80+
end
81+
82+
defp integer_to_atom(enum_module, value) do
83+
try do
84+
enum_module.__message_props__()
85+
|> Map.get(:field_props, %{})
86+
|> Enum.find(fn {_name, props} -> props[:enum_value] == value end)
87+
|> case do
88+
{name, _} -> normalize_enum_name(name, enum_module)
89+
nil -> value
90+
end
91+
rescue
92+
_ -> value
93+
end
94+
end
95+
96+
# Normalize enum names by removing prefix and lowercasing
97+
# E.g., :INSTANCE_STATE_ZERO_STATE -> :zero_state (for InternalApi.EphemeralEnvironments.InstanceState)
98+
# :TYPE_STATE_DRAFT -> :draft (for InternalApi.EphemeralEnvironments.TypeState)
99+
defp normalize_enum_name(enum_atom, enum_module) do
100+
prefix = extract_enum_prefix(enum_module)
101+
102+
enum_atom
103+
|> Atom.to_string()
104+
|> String.replace_prefix(prefix <> "_", "")
105+
|> String.downcase()
106+
|> String.to_atom()
107+
end
108+
109+
# Extract the enum prefix from the module name
110+
# E.g., InternalApi.EphemeralEnvironments.InstanceState -> "INSTANCE_STATE"
111+
# InternalApi.EphemeralEnvironments.StateChangeActionType -> "STATE_CHANGE_ACTION_TYPE"
112+
defp extract_enum_prefix(enum_module) do
113+
enum_module
114+
|> Module.split()
115+
|> List.last()
116+
|> Macro.underscore()
117+
|> String.upcase()
118+
end
119+
end

0 commit comments

Comments
 (0)