Current section
Files
Jump to
Current section
Files
lib/responses/options.ex
defmodule OpenAI.Responses.Options do
@moduledoc false
@typedoc "Normalized options map with string keys"
@type t :: %{optional(String.t()) => any}
@doc """
Normalize user-provided options into a map with string keys.
- Accepts keyword lists, maps with atom or string keys, or lists of {key, value} tuples.
- Recursively stringifies keys for maps and keyword lists.
- Preserves regular lists (arrays) as lists without converting to maps.
- Raises `ArgumentError` on invalid list entries that look like malformed options.
"""
@spec normalize(map | keyword) :: t
def normalize(options) when is_map(options) do
Map.new(options, fn {k, v} -> {to_string(k), normalize_value(v)} end)
end
def normalize(options) when is_list(options) do
options
|> Enum.map(fn
{k, v} -> {to_string(k), normalize_value(v)}
other -> raise ArgumentError, "Invalid option format: #{inspect(other)}"
end)
|> Map.new()
end
def normalize(other) do
raise ArgumentError, "Options must be a map or keyword list, got: #{inspect(other)}"
end
# Internal recursive normalization
defp normalize_value(value) when is_map(value) do
normalize(value)
end
defp normalize_value(value) when is_list(value) do
# If it's a keyword-like list (at least one tuple), try to normalize as options
if value != [] and is_tuple(hd(value)) do
try do
normalize(value)
rescue
ArgumentError -> Enum.map(value, &normalize_value/1)
end
else
# Regular list (array)
Enum.map(value, &normalize_value/1)
end
end
defp normalize_value(value), do: value
@doc """
Stringify top-level keys of a map. Does not descend into nested maps.
"""
@spec stringify_keys_shallow(map) :: t
def stringify_keys_shallow(map) when is_map(map) do
Map.new(map, fn {k, v} -> {to_string(k), v} end)
end
@doc """
Stringify keys of a map deeply.
"""
@spec stringify_keys_deep(map) :: t
def stringify_keys_deep(map) when is_map(map) do
map
|> Enum.map(fn {k, v} ->
v =
cond do
is_map(v) -> stringify_keys_deep(v)
is_list(v) -> Enum.map(v, &stringify_if_map/1)
true -> v
end
{to_string(k), v}
end)
|> Map.new()
end
defp stringify_if_map(v) when is_map(v), do: stringify_keys_deep(v)
defp stringify_if_map(v), do: v
@doc """
Merge additions into a named section (e.g., "text") of options without dropping existing fields.
- Section name can be atom or string; stored as string key.
- Additions are deep-stringified to ensure consistent string keys.
- Shallow merge at the section level (per-key) with additions taking precedence.
"""
@spec merge_option(t, atom | String.t(), map) :: t
def merge_option(options, section, additions) when is_map(options) and is_map(additions) do
section = to_string(section)
additions = stringify_keys_deep(additions)
existing = Map.get(options, section)
merged =
case existing do
%{} = map -> Map.merge(map, additions, fn _k, _old, new -> new end)
_ -> additions
end
Map.put(options, section, merged)
end
@doc """
Convenience wrapper to merge into the "text" section.
"""
@spec merge_text(t, map) :: t
def merge_text(options, additions) when is_map(options) and is_map(additions) do
merge_option(options, "text", additions)
end
@doc """
Preserve keys from a source map into options if they are not already present.
Keys must be strings. If a key is absent in `options` and present in `source`, it is copied.
"""
@spec preserve_from(t, map, [String.t()]) :: t
def preserve_from(options, source, keys) when is_map(options) and is_map(source) do
Enum.reduce(keys, options, fn key, acc ->
with false <- Map.has_key?(acc, key),
{:ok, value} <- Map.fetch(source, key) do
Map.put(acc, key, value)
else
_ -> acc
end
end)
end
end