Shared module constants

Seems I’ve been spending too much time using things that live reload code, the files DO recompile if I change the JSON, I was just waiting for it to happen live :blush:

I’m sure there are things I can tweak in the below, but it seems to work fine for what I need right now.

  • Split over multiple JSON files
  • Custom Ecto field for converting when entering/retrieving from DB
  • Allow atoms to be used during dev/debug, but integers for storage/comms
  • Re-compiles when any JSON file changes
  • Dynamically create “nested modules” based on the JSON filename

So this is end result of all the suggestions (thanks again!)

defmodule API.Const do
  @moduledoc """
  A single point that we can use constants from. Rather than using something
  like a map to expose atoms => integers we create a set of functions that 
  let us encode/decode from either atom => integer or integer => atom. Doing this
  makes it a lot easier to debug things as we are basically just using atoms 
  everywhere in the code - we change to integers on the app boundary.

  We still break up the constants with regards to their respective areas, and 
  then we just include everything in here. We store the constatnts as JSON files
  so that we can export into frontend clients if needed,

  Discussion here: https://forum.elixirforum.com/t/shared-module-constants/2799/
  
  Eg:

  API.Const.System.encode(:ORG_STATUS_ACTIVE) => 100101
  API.Const.System.decode(100101) => :ORG_STATUS_ACTIVE
  """

  @root_dir       File.cwd!
  @const_dir      Path.join(~w(#{@root_dir} lib imsapi common constants))
  @const_files    Path.wildcard("#{@const_dir}/**/*.json")

  for filename <- @const_files do
    # Watch for changes and recompile
    @external_resource filename

    # This will be the new module: API.Const.<group>
    group = filename
            |> Path.basename(".json")
            |> String.capitalize

    file_consts = File.read!(filename)
                  |> Poison.decode!

    
    # Creates a module under the main API.Const module using the filename.
    # 
    # Eg a system.json file would end up as `API.Const.System`
    defmodule Module.concat([API, Const, group]) do
      for {key, value} <- file_consts do
        def encode(unquote(String.to_atom(key))),   do: unquote(value)
        def decode(unquote(value)), do: unquote(String.to_atom(key))
      end

      # Creates a new module that can be used with Ecto models to hsve s field
      # that is automatically encoded/decoded as data is retrieved/entered.
      # Name is the same as the outer module, with `Ecto` appended
      # 
      # Eg a system.json file would end up as `API.Const.System.EctoField`
      defmodule Module.concat([API, Const, group, EctoField]) do
        @behaviour Ecto.Type

        @group_mod Module.concat([API, Const, group])

        def type, do: :integer

        def cast(int) when is_integer(int), do: {:ok, apply(@group_mod, :decode, [int])}
        def cast(atom) when is_atom(atom), do: {:ok, atom}
        def cast(_), do: :error

        def load(int) when is_integer(int), do: {:ok, apply(@group_mod, :decode, [int])}
        def load(_), do: :error

        def dump(atom) when is_atom(atom), do: {:ok, apply(@group_mod, :encode, [atom])}
        def dump(_), do: :error
      end
    end
  end
end