Reputation: 2506
In a nutshell, I have a script that reads a .yaml file to get some configuration information at runtime such as what URL to contact, what shared secret to use, whether to use debug mode, etc.
The module that uses that config has a start function which later calls a loop and also calls a logdebug function that writes diagnostics but only if debug mode is set. What is irritating me, though is that I have to pass my configuration around to each of these functions every time I call them. It would be much easier if I could call the start function and have it set some variables that are available to all the other functions in the module. Can that be done? I can't seem to find anything about how to do that.
Is there a preferred way of setting runtime configuration like what I'm doing here? Maybe I'm overcomplicating things?
EDIT: Little more detail, I'm distributing this as an executable created with Escript.Build
and I don't want to make the end users edit a file and then rebuild the file. That's why I want the end user (who might not be super technical) to just be able to edit a .yaml file.
Upvotes: 3
Views: 3043
Reputation: 2506
I ended up using yamerl
in my main module to read the .yaml file and then I used Application.put_env/2
to put the values into a place where they were available for all my modules.
[ config | _ ] = :yamerl_constr.file("config.yaml")
Application.put_env(:osq_simulator, :base_url, :proplists.get_value('base_url', config))
Although based on some other feedback I got, it looks like maybe the answer from Chris Meyer is the "right" way to do what I'm trying to do.
Upvotes: 0
Reputation: 1631
Disclaimer: I'm interpreting your 'runtime config' more as arguments; if that's not the case, this answer may not be very useful.
A Module
is similar to a Class
.
Unfortunately, not similar enough for this common O-O approach to work; Elixir/Erlang modules have no "life" in them, and are just flat logic. What you are effectively trying to do is store state in the Module itself; in a functional language, state must be kept in variables, because the Module is shared across all callers from all processes- another process might need to store different state!
However, this is a common programming problem, and there is an idiomatic way to solve it in Elixir: a GenServer
.
If you aren't familiar with OTP, you owe it to yourself to learn it: it'll change the way you think about programming, it'll help you write better (read: more reliable) software, and it'll make you happy. Really.
I would store the config in the GenServer's state; If you create an internal struct to represent it, you can pass it around easily and set defaults; all the things we want in a pleasing API.
A sample implementation:
defmodule WebRequestor do
use GenServer
### API ###
# these functions execute in the CALLER's process
def start_link() do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def start do
# could use .call if you need synch, but then you'd want to look at
# delayed reply genserver calls, which is an advanced usage
GenServer.cast(__MODULE__, :start)
end
#could add other methods for enabling debug, setting secrets, etc.
def update_url(new_url) do
GenServer.call(__MODULE__, {:update_url, new_url})
end
defmodule State do
@doc false
defstruct [
url: "http://api.my.default",
secret: "mybadpassword",
debug: false,
status: :ready, # or whatever else you might need
]
end
### GenServer Callbacks ###
# These functions execute in the SERVER's process
def init([]) do
config = read_my_config_file
{:ok, config}
end
def handle_cast(:start, %{status: :ready} = state) do
if config.debug, do: IO.puts "Starting"
make_request(state.url)
{:noreply, %{state|status :running}}
end
def handle_cast(:state, state) do
#already running, so don't start again.
{:noreply, state}
end
def handle_call({:update_url, new_url}, _from, state) do
{:reply, :ok, %{state|url: new_url}}
end
### Internal Calls ###
defp make_request(config) do
# whatever you do here...
end
defp read_my_config_file do
# config reading...
%State{}
end
end
Upvotes: 5
Reputation: 51439
The answer depends on your constraints. Do you need to use .yaml? We discourage the use of YAML unless you really have non-Elixir programmers needing to touch those. If they are all being touched by programmers, then you can just use Elixir configuration:
# config/config.exs
config :my_app,
url: "...",
this: "...",
that: "..."
This will allow you to access and change the configuration by using functions like Application.get_env(:my_app, :url)
and Application.put_env(:my_app, :foo, :bar)
. In the future, if you want to build releases (shipping the whole application with the VM in a single directory), provide upgrades and so on, using Elixir configuration will prove the optimal workflow.
Upvotes: 3
Reputation: 136
Can you use an exs file instead ? Possibly load the yaml file there if necessary? Here's a blog post that seems quite similar: http://www.schmitty.me/taking-advantage-of-mix-config/
Upvotes: 1