Avoiding repeating and hardcoding keys across module definitions

I’ve just run into a very similar problem and my approach was to use structs to represent the data that was parsed out of the file at first, and having the output of the initial parse of the text file be a list of structs that represent the meaning extracted from the text. Then if you want a more standardized final product you can reduce this list into a final struct and the logic will be much cleaner because you can pattern match on structs, and even organize further logic into the struct modules.

You can also combined named regex captures with new/1 functions in your struct modules that accept maps with keys corresponding to your regex capture to cut down on repetition in your code, for example:

def ParseMod do
  @patterns [{~r/(?<some_name>\w+)/, StructMod}, ...]

  def parse(file_name) do
    file_name
    |> File.stream!()
    |> Stream.flat_map(fn line -> 
      Enum.reduce_while(@patterns, [], fn {pattern, mod}, [] -> 
       if match = Regex.named_captures(pattern, line) do
        {:halt, [apply(mod, :new, [match])]}
       else
        {:cont, []}
       end)
    end)
  end
end

Now when you have more data you want to extract you add a pattern entry and create a corresponding module with the appropriate new function and things stay nice and tidy.