FunWithFlags: a feature toggle library plus its web GUI as a Plug

It depends on the use case, as always, but it sounds like all of them should be actors. I have the feeling that you are approaching the problem with the wrong assumption: that only users can be actors. As you’ve realized, this would make things a bit complicated in some scenarios. I’ll provide examples at the end of the post.

Yes. By design. Group names are meant to be human friendly labels that you define ahead of time. By contrast, actors are dynamic entities whose identity you resolve at runtime. In your case, yes, you can either declare groups for all of your regions and departments, or you can make them actors with namespaced IDs. It really depends on what makes more sense for your access patterns.

Oh, yes, that would be awkward. That’s what makes me think that modelling them as actors makes sense.

If I get what you mean, these categories could still be groups. You are free to represent them as you want. A user can have a literal list of group names (like roles, or tags), or you can infer the group dynamically.

I really think that you’re describing actors here.

Some examples

Here are some examples of how I would model it. I’m going to use users, departments and countries. To keep things simple I’m going to use plain structs instead of using Ecto, and I’m going to use some invented functions and just say what they would do.

Let’s say we have these structs:

defmodule User do
  defstruct [:id, :name]
end

defmodule Country do
  defstruct [:name, :iso]
end

defmodule Department do
  defstruct [:name]
end

Which are all actors:

defimpl FunWithFlags.Actor, for: User do
  def id(%{id: id}) do
    "user:#{id}"
  end
end

defimpl FunWithFlags.Actor, for: Country do
  def id(%{iso: iso}) do
    "country:#{iso}"
  end
end

defimpl FunWithFlags.Actor, for: Department do
  def id(%{name: name}) do
    "department:#{name}"
  end
end

With these simple building blocks, I can build a matrix of actor gates. Just remember that disabled gates take precedence over the enabled ones.

So, for example, if I want to enable something for engineers in Japan, I can:

japan = %Country{name: "Japan", iso: "jp"}
engineering = %Department{name: "Engineering"}

FunWithFlags.enable(:beta_features, for_actor: japan)
FunWithFlags.enable(:beta_features, for_actor: engineering)

And then I can check it for individual users with:

def beta_features_enabled_for?(user = %User{}) do
  country = Country.for(user)
  department = Department.for(user)

  FunWithFlags.enabled?(:beta_features, for: country) &&
    FunWithFlags.enabled?(:beta_features, for: department)
end

And later you can even add some groups to the Country struct, to add more flexibility. For example, let’s say that you want to enable the flag for all asian countries:

defimpl FunWithFlags.Group, for: Country do
  @asian_countries ~w{jp hk} # ...
  @european_countries ~w{it fr} # ...
  #...

  def in?(%{iso: iso}, :asian_countries) do
    iso in @asian_countries
  end

  def in?(%{iso: iso}, :european_countries) do
    iso in @european_countries
  end

  # ...
end

FunWithFlags.enable(:beta_features, for_group: :asian_countries)

For the “beta testers group designations”, then you can use a more traditional group approach. For example you could find a way to either set the the designations on the %User{} struct, or make them fetchable. Some simple examples:

defimpl FunWithFlags.Group, for: User do
  def in?(%{groups: groups}, group) when is_list(groups) do
    group in groups
  end
end

# or...
defimpl FunWithFlags.Group, for: User do
  def in?(user, group) do
    designations = BetaTesterGroupDesignation.get_list_for(user)
    group in designations
  end
end

# or even... if you feel fancy (think carefully before doing this because it's not optimal)
defimpl FunWithFlags.Group, for: User do
  def in?(user, group) do
    if Enum.member?(BetaTesterGroupDesignation.all(), group)
      designations = BetaTesterGroupDesignation.get_list_for(user)
      group in designations
    else
      # other group logic or false
      false
    end
  end
end

Done that, you can add User to the checks I showed above, or you can use different logic to give explicit settings on user actors higher priority. As you can see, this library just gives you the building blocks.

By the way, you can also model everything with groups, just like you initially imagined.
For example:

defimpl FunWithFlags.Group, for: User do
  def in?(user, group) do
    country = Country.for(user)
    department = Department.for(user)
    String.downcase("#{country.name}_#{department.name}") == to_string(group)
  end
end

FunWithFlags.enable(:beta_features, for_group: :japan_engineering)
FunWithFlags.enabled?(:beta_features, for: user)

I hope this clarifies your doubts, and I hope you’ll find it useful! :slight_smile:

3 Likes