|
1 | 1 | defmodule EphemeralEnvironments.Utils.Proto do |
2 | 2 | @moduledoc """ |
3 | | - Utility functions for converting protobuf structs to plain Elixir maps. |
| 3 | + Utility functions for converting between protobuf structs and plain Elixir maps. |
4 | 4 | """ |
5 | 5 |
|
| 6 | + @doc """ |
| 7 | + Converts an Elixir map to a protobuf struct of the given module type. |
| 8 | + - Converts DateTime to Google.Protobuf.Timestamp |
| 9 | + - Converts normalized enum atoms (:ready) to protobuf enum atoms (:TYPE_STATE_READY) |
| 10 | + - Recursively processes nested maps to nested proto structs |
| 11 | +
|
| 12 | + ## Examples |
| 13 | +
|
| 14 | + from_map(%{name: "test", state: :ready}, InternalApi.EphemeralEnvironments.EphemeralEnvironmentType) |
| 15 | + """ |
| 16 | + def from_map(nil, _module), do: nil |
| 17 | + |
| 18 | + def from_map(map, module) when is_map(map) and is_atom(module) do |
| 19 | + field_props = module.__message_props__().field_props |
| 20 | + |
| 21 | + # Convert map to struct fields |
| 22 | + fields = |
| 23 | + map |
| 24 | + |> Enum.map(fn {key, value} -> |
| 25 | + # Find field info for this key |
| 26 | + field_info = find_field_info(field_props, key) |
| 27 | + converted_value = convert_value_from_map(value, field_info) |
| 28 | + {key, converted_value} |
| 29 | + end) |
| 30 | + |> Enum.into(%{}) |
| 31 | + |
| 32 | + struct(module, fields) |
| 33 | + end |
| 34 | + |
6 | 35 | @doc """ |
7 | 36 | Recursively converts a protobuf struct to a plain Elixir map. |
8 | 37 | - Converts Google.Protobuf.Timestamp to DateTime |
@@ -116,4 +145,80 @@ defmodule EphemeralEnvironments.Utils.Proto do |
116 | 145 | |> Macro.underscore() |
117 | 146 | |> String.upcase() |
118 | 147 | end |
| 148 | + |
| 149 | + # Find field info by field name atom |
| 150 | + defp find_field_info(field_props, field_name) do |
| 151 | + field_props |
| 152 | + |> Enum.find(fn {_num, props} -> props.name_atom == field_name end) |
| 153 | + |> case do |
| 154 | + {_num, props} -> props |
| 155 | + nil -> nil |
| 156 | + end |
| 157 | + end |
| 158 | + |
| 159 | + defp convert_value_from_map(nil, _field_info), do: nil |
| 160 | + |
| 161 | + defp convert_value_from_map(%DateTime{} = dt, _field_info) do |
| 162 | + %Google.Protobuf.Timestamp{ |
| 163 | + seconds: DateTime.to_unix(dt), |
| 164 | + nanos: 0 |
| 165 | + } |
| 166 | + end |
| 167 | + |
| 168 | + @unix_epoch ~N[1970-01-01 00:00:00] |
| 169 | + defp convert_value_from_map(%NaiveDateTime{} = ndt, _field_info) do |
| 170 | + %Google.Protobuf.Timestamp{ |
| 171 | + seconds: NaiveDateTime.diff(ndt, @unix_epoch) |
| 172 | + } |
| 173 | + end |
| 174 | + |
| 175 | + defp convert_value_from_map(value, nil), do: value |
| 176 | + |
| 177 | + defp convert_value_from_map(values, field_info) when is_list(values) do |
| 178 | + if field_info.embedded? do |
| 179 | + Enum.map(values, fn item -> |
| 180 | + if is_map(item) and not is_struct(item) do |
| 181 | + from_map(item, field_info.type) |
| 182 | + else |
| 183 | + item |
| 184 | + end |
| 185 | + end) |
| 186 | + else |
| 187 | + values |
| 188 | + end |
| 189 | + end |
| 190 | + |
| 191 | + # Handle nested maps (embedded messages) |
| 192 | + defp convert_value_from_map(value, field_info) when is_map(value) and not is_struct(value) do |
| 193 | + if field_info.embedded? do |
| 194 | + from_map(value, field_info.type) |
| 195 | + else |
| 196 | + value |
| 197 | + end |
| 198 | + end |
| 199 | + |
| 200 | + # Handle enum atoms - convert normalized atom back to proto enum |
| 201 | + defp convert_value_from_map(value, field_info) when is_atom(value) do |
| 202 | + if field_info.enum? do |
| 203 | + case field_info.type do |
| 204 | + {:enum, enum_module} -> denormalize_enum_name(value, enum_module) |
| 205 | + _ -> value |
| 206 | + end |
| 207 | + else |
| 208 | + value |
| 209 | + end |
| 210 | + end |
| 211 | + |
| 212 | + defp convert_value_from_map(value, _field_info), do: value |
| 213 | + |
| 214 | + # Denormalize enum: :ready -> :TYPE_STATE_READY |
| 215 | + defp denormalize_enum_name(normalized_atom, enum_module) do |
| 216 | + prefix = extract_enum_prefix(enum_module) |
| 217 | + |
| 218 | + normalized_atom |
| 219 | + |> Atom.to_string() |
| 220 | + |> String.upcase() |
| 221 | + |> then(&"#{prefix}_#{&1}") |
| 222 | + |> String.to_atom() |
| 223 | + end |
119 | 224 | end |
0 commit comments