defmodule TestcontainerEx.Util.PropertiesParser do
@moduledoc false
@user_file "~/.testcontainer_ex.properties"
@project_file ".testcontainer_ex.properties"
@env_prefix "TESTCONTAINERS_"
def read_property_file(file_path \\ @user_file) do
if File.exists?(Path.expand(file_path)) do
with {:ok, content} <- File.read(Path.expand(file_path)),
properties <- parse_properties(content) do
{:ok, properties}
else
error ->
{:error, properties: error}
end
else
# return empty map if file does not exist
{:ok, %{}}
end
end
@doc """
Reads properties from all sources with proper precedence.
Configuration is read from three sources with the following precedence
(highest to lowest):
1. Environment variables (TESTCONTAINERS_* prefix)
2. User file (~/.testcontainer_ex.properties)
3. Project file (.testcontainer_ex.properties)
Environment variables are converted from TESTCONTAINERS_PROPERTY_NAME format
to property.name format (uppercase to lowercase, underscores to dots, prefix removed).
## Options
- `:user_file` - path to user properties file (default: ~/.testcontainer_ex.properties)
- `:project_file` - path to project properties file (default: .testcontainer_ex.properties)
- `:env_prefix` - environment variable prefix (default: TESTCONTAINERS_)
## Returns
- `{:ok, map}` with merged properties.
"""
def read_property_sources(opts \\ []) do
user_file = Keyword.get(opts, :user_file, @user_file)
project_file = Keyword.get(opts, :project_file, @project_file)
env_prefix = Keyword.get(opts, :env_prefix, @env_prefix)
project_props = read_file_silent(project_file)
user_props = read_file_silent(user_file)
env_props = read_env_vars(env_prefix)
# Merge in order of lowest to highest precedence
merged =
project_props
|> Map.merge(user_props)
|> Map.merge(env_props)
{:ok, merged}
end
defp read_file_silent(file_path) do
expanded = Path.expand(file_path)
if File.exists?(expanded) do
case File.read(expanded) do
{:ok, content} -> parse_properties(content)
{:error, _} -> %{}
end
else
%{}
end
end
defp read_env_vars(prefix) do
System.get_env()
|> Enum.filter(fn {key, _value} -> String.starts_with?(key, prefix) end)
|> Enum.map(&env_to_property(&1, prefix))
|> Map.new()
end
# Converts TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED to ryuk.container.privileged
defp env_to_property({key, value}, prefix) do
property_key =
key
|> String.replace_prefix(prefix, "")
|> String.downcase()
|> String.replace("_", ".")
{property_key, value}
end
defp parse_properties(content) do
content
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == "" or String.starts_with?(&1, "#")))
|> Enum.flat_map(&extract_key_value_pair/1)
|> Enum.into(%{})
end
defp extract_key_value_pair(line) do
case String.split(line, "=", parts: 2) do
[key, value] when is_binary(value) ->
[{String.trim(key), String.trim(value)}]
_other ->
[]
end
end
end