defmodule Squirrelix.Metadata do @moduledoc """ Loads Elixir query metadata files for the current code generation pipeline. Metadata files are evaluated with `Code.eval_string/3` and must return a map. Treat them like `mix.exs` / `config/*.exs`: only load trusted, project-local files. Prefer `--infer` in CI when you do not want to maintain evaluated Elixir metadata. """ alias Squirrelix.Error.CannotReadFile alias Squirrelix.Error.InvalidQueryMetadataFile @spec from_file(Path.t(), keyword()) :: {:ok, %{String.t() => keyword()}} | {:error, CannotReadFile.t() | InvalidQueryMetadataFile.t()} def from_file(file, opts \\ []) when is_binary(file) and is_list(opts) do root = Keyword.fetch!(opts, :root) with {:ok, content} <- read_metadata_file(file), {:ok, metadata} <- evaluate_metadata_file(file, content) do {:ok, expand_query_paths(metadata, root)} end end defp read_metadata_file(file) do case File.read(file) do {:ok, content} -> {:ok, content} {:error, reason} -> {:error, %CannotReadFile{file: file, reason: reason}} end end defp evaluate_metadata_file(file, content) do case Code.eval_string(content, [], file: file) do {metadata, _binding} when is_map(metadata) -> {:ok, metadata} {_metadata, _binding} -> {:error, %InvalidQueryMetadataFile{file: file, reason: :not_a_map}} end rescue error -> {:error, %InvalidQueryMetadataFile{file: file, reason: error}} end defp expand_query_paths(metadata, root) do Map.new(metadata, fn {query_file, query_metadata} -> {expand_query_path(query_file, root), query_metadata} end) end defp expand_query_path(query_file, root) when is_binary(query_file) do if Path.type(query_file) == :absolute do query_file else Path.expand(query_file, root) end end end