Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow binaries as group names #13

Merged
merged 4 commits into from
Sep 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Unreleased

Possibly Breaking Changes:

* Allow binaries _and_ atoms as group gate names. Binaries are now preferred (atom group names are internally converted, stored and retrieved as binaries) and atoms are still allowed for retro-compatibility.
While calling `FunWithFlags.enable(:foo, for_group: :bar)` is still allowed and continues to work as before, this change will impact implementations of the `FunWithFlags.Group` protocol that assume
that the group name is passed as an atom.
To safely upgrade, these implementations should be changed to work with the group names passed as a binary instead. See the [update to the protocol implementation used in the tests](https://github.com/tompave/fun_with_flags/pull/13/files#diff-8c1bcfc3d51e8d863953ac5b57f0da2b) for an example.

Other changes:

* Compatibility updates for Ecto 2.2 (dev env, was fine in prod)

## v0.9.2
Expand Down
32 changes: 17 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,12 @@ end

### Group Gate

Group gates are similar to actor gates, but they apply to a category of entities rather than specific ones. They can be toggled on or off for the _name of the group_ (as an atom) instead of a specific term.
Group gates are similar to actor gates, but they apply to a category of entities rather than specific ones. They can be toggled on or off for the _name of the group_ instead of a specific term.

Group gates take precendence over boolean gates but are overridden by actor gates.

Group names can be binaries or atoms. Atoms are supported for retro-compatibility with versions `<= 0.9` and binaries are therefore preferred. In fact, atoms are internally converted to binaries and are then stored and later retrieved as binaries.

The semantics to determine which entities belong to which groups are application specific.
Entities could have an explicit list of groups they belong to, or the groups could be abstract and inferred from some other attribute. For example, an `:employee` group could comprise all `%User{}` structs with an email address matching the company domain, or an `:admin` group could be made of all users with `%User{admin: true}`.

Expand All @@ -183,35 +185,35 @@ defmodule MyApp.User do
end

defimpl FunWithFlags.Group, for: MyApp.User do
def in?(%{email: email}, :employee), do: Regex.match?(~r/@mycompany.com$/, email)
def in?(%{admin: is_admin}, :admin), do: !!is_admin
def in?(%{email: email}, "employee"), do: Regex.match?(~r/@mycompany.com$/, email)
def in?(%{admin: is_admin}, "admin"), do: !!is_admin
def in?(%{groups: list}, group_name), do: group_name in list
end

elisabeth = %User{email: "[email protected]", admin: true, groups: [:engineering, :product]}
FunWithFlags.Group.in?(elisabeth, :employee)
elisabeth = %User{email: "[email protected]", admin: true, groups: ["engineering", "product"]}
FunWithFlags.Group.in?(elisabeth, "employee")
true
FunWithFlags.Group.in?(elisabeth, :admin)
FunWithFlags.Group.in?(elisabeth, "admin")
true
FunWithFlags.Group.in?(elisabeth, :engineering)
FunWithFlags.Group.in?(elisabeth, "engineering")
true
FunWithFlags.Group.in?(elisabeth, :marketing)
FunWithFlags.Group.in?(elisabeth, "marketing")
false

defimpl FunWithFlags.Group, for: Map do
def in?(%{group: group_name}, group_name), do: true
def in?(_, _), do: false
end

FunWithFlags.Group.in?(%{group: :dumb_tests}, :dumb_tests)
FunWithFlags.Group.in?(%{group: "dumb_tests"}, "dumb_tests")
true
```

With the protocol implemented, actors can be used with the library functions:

```elixir
FunWithFlags.disable(:database_access)
FunWithFlags.enable(:database_access, for_group: :engineering)
FunWithFlags.enable(:database_access, for_group: "engineering")

FunWithFlags.enabled?(:database_access)
false
Expand All @@ -227,11 +229,11 @@ More examples:

```elixir
alias FunWithFlags.TestUser, as: User
harry = %User{id: 1, name: "Harry Potter", groups: [:wizards, :gryffindor]}
hagrid = %User{id: 2, name: "Rubeus Hagrid", groups: [:wizards, :gamekeeper]}
dudley = %User{id: 3, name: "Dudley Dursley", groups: [:muggles]}
harry = %User{id: 1, name: "Harry Potter", groups: ["wizards", "gryffindor"]}
hagrid = %User{id: 2, name: "Rubeus Hagrid", groups: ["wizards", "gamekeeper"]}
dudley = %User{id: 3, name: "Dudley Dursley", groups: ["muggles"]}
FunWithFlags.disable(:wands)
FunWithFlags.enable(:wands, for_group: :wizards)
FunWithFlags.enable(:wands, for_group: "wizards")
FunWithFlags.disable(:wands, for_actor: hagrid)

FunWithFlags.enabled?(:wands)
Expand All @@ -247,7 +249,7 @@ FunWithFlags.clear(:wands, for_actor: hagrid)
FunWithFlags.enabled?(:wands, for: hagrid)
true

FunWithFlags.clear(:wands, for_group: :wizards)
FunWithFlags.clear(:wands, for_group: "wizards")
FunWithFlags.enabled?(:wands, for: hagrid)
false
FunWithFlags.enabled?(:wands, for: harry)
Expand Down
42 changes: 24 additions & 18 deletions lib/fun_with_flags.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,18 @@ defmodule FunWithFlags do
used in the tests.

iex> alias FunWithFlags.TestUser, as: User
iex> harry = %User{id: 1, name: "Harry Potter", groups: [:wizards, :gryffindor]}
iex> harry = %User{id: 1, name: "Harry Potter", groups: ["wizards", "gryffindor"]}
iex> FunWithFlags.disable(:elder_wand)
iex> FunWithFlags.enable(:elder_wand, for_actor: harry)
iex> FunWithFlags.enabled?(:elder_wand)
false
iex> FunWithFlags.enabled?(:elder_wand, for: harry)
true
iex> voldemort = %User{id: 7, name: "Tom Riddle", groups: [:wizards, :slytherin]}
iex> voldemort = %User{id: 7, name: "Tom Riddle", groups: ["wizards", "slytherin"]}
iex> FunWithFlags.enabled?(:elder_wand, for: voldemort)
false
iex> filch = %User{id: 88, name: "Argus Filch", groups: [:staff]}
iex> FunWithFlags.enable(:magic_wands, for_group: :wizards)
iex> filch = %User{id: 88, name: "Argus Filch", groups: ["staff"]}
iex> FunWithFlags.enable(:magic_wands, for_group: "wizards")
iex> FunWithFlags.enabled?(:magic_wands, for: harry)
true
iex> FunWithFlags.enabled?(:magic_wands, for: voldemort)
Expand Down Expand Up @@ -103,7 +103,9 @@ defmodule FunWithFlags do
* `:for_actor` - used to enable the flag for a specific term only.
The value can be any term that implements the `Actor` protocol.
* `:for_group` - used to enable the flag for a specific group only.
The value should be an atom.
The value should be a binary or an atom (It's internally converted
to a binary and it's stored and retrieved as a binary. Atoms are
supported for retro-compatibility with versions <= 0.9)

## Examples

Expand Down Expand Up @@ -133,10 +135,10 @@ defmodule FunWithFlags do
used in the tests.

iex> alias FunWithFlags.TestUser, as: User
iex> marty = %User{name: "Marty McFly", groups: [:students, :time_travelers]}
iex> doc = %User{name: "Emmet Brown", groups: [:scientists, :time_travelers]}
iex> buford = %User{name: "Buford Tannen", groups: [:gunmen, :bandits]}
iex> FunWithFlags.enable(:delorean, for_group: :time_travelers)
iex> marty = %User{name: "Marty McFly", groups: ["students", "time_travelers"]}
iex> doc = %User{name: "Emmet Brown", groups: ["scientists", "time_travelers"]}
iex> buford = %User{name: "Buford Tannen", groups: ["gunmen", "bandits"]}
iex> FunWithFlags.enable(:delorean, for_group: "time_travelers")
{:ok, true}
iex> FunWithFlags.enabled?(:delorean)
false
Expand Down Expand Up @@ -187,7 +189,9 @@ defmodule FunWithFlags do
* `:for_actor` - used to disable the flag for a specific term only.
The value can be any term that implements the `Actor` protocol.
* `:for_group` - used to disable the flag for a specific group only.
The value should be an atom.
The value should be a binary or an atom (It's internally converted
to a binary and it's stored and retrieved as a binary. Atoms are
supported for retro-compatibility with versions <= 0.9)

## Examples

Expand Down Expand Up @@ -220,11 +224,11 @@ defmodule FunWithFlags do
used in the tests.

iex> alias FunWithFlags.TestUser, as: User
iex> harry = %User{name: "Harry Potter", groups: [:wizards, :gryffindor]}
iex> dudley = %User{name: "Dudley Dursley", groups: [:muggles]}
iex> harry = %User{name: "Harry Potter", groups: ["wizards", "gryffindor"]}
iex> dudley = %User{name: "Dudley Dursley", groups: ["muggles"]}
iex> FunWithFlags.enable(:hogwarts)
{:ok, true}
iex> FunWithFlags.disable(:hogwarts, for_group: :muggles)
iex> FunWithFlags.disable(:hogwarts, for_group: "muggles")
{:ok, false}
iex> FunWithFlags.enabled?(:hogwarts)
true
Expand Down Expand Up @@ -284,16 +288,18 @@ defmodule FunWithFlags do
* `:for_actor` - used to clear the flag for a specific term only.
The value can be any term that implements the `Actor` protocol.
* `:for_group` - used to clear the flag for a specific group only.
The value should be an atom.
The value should be a binary or an atom (It's internally converted
to a binary and it's stored and retrieved as a binary. Atoms are
supported for retro-compatibility with versions <= 0.9)

## Examples

iex> alias FunWithFlags.TestUser, as: User
iex> harry = %User{id: 1, name: "Harry Potter", groups: [:wizards, :gryffindor]}
iex> hagrid = %User{id: 2, name: "Rubeus Hagrid", groups: [:wizards, :gamekeeper]}
iex> dudley = %User{id: 3, name: "Dudley Dursley", groups: [:muggles]}
iex> harry = %User{id: 1, name: "Harry Potter", groups: ["wizards", "gryffindor"]}
iex> hagrid = %User{id: 2, name: "Rubeus Hagrid", groups: ["wizards", "gamekeeper"]}
iex> dudley = %User{id: 3, name: "Dudley Dursley", groups: ["muggles"]}
iex> FunWithFlags.disable(:wands)
iex> FunWithFlags.enable(:wands, for_group: :wizards)
iex> FunWithFlags.enable(:wands, for_group: "wizards")
iex> FunWithFlags.disable(:wands, for_actor: hagrid)
iex>
iex> FunWithFlags.enabled?(:wands)
Expand Down
6 changes: 3 additions & 3 deletions lib/fun_with_flags/gate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ defmodule FunWithFlags.Gate do

def new(:group, group_name, enabled) when is_boolean(enabled) do
validate_group_name(group_name)
%__MODULE__{type: :group, for: group_name, enabled: enabled}
%__MODULE__{type: :group, for: to_string(group_name), enabled: enabled}
end

defmodule InvalidGroupNameError do
defexception [:message]
end

defp validate_group_name(name) when is_atom(name), do: nil
defp validate_group_name(name) when is_binary(name) or is_atom(name), do: nil
defp validate_group_name(name) do
raise InvalidGroupNameError, "invalid group name '#{inspect(name)}', it should be an atom."
raise InvalidGroupNameError, "invalid group name '#{inspect(name)}', it should be a binary or an atom."
end


Expand Down
2 changes: 1 addition & 1 deletion lib/fun_with_flags/store/serializer/ecto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ defmodule FunWithFlags.Store.Serializer.Ecto do
end

defp do_deserialize_gate(%Record{gate_type: "group", enabled: enabled, target: target}) do
%Gate{type: :group, for: String.to_atom(target), enabled: enabled}
%Gate{type: :group, for: target, enabled: enabled}
end

def to_atom(atm) when is_atom(atm), do: atm
Expand Down
2 changes: 1 addition & 1 deletion lib/fun_with_flags/store/serializer/redis.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ defmodule FunWithFlags.Store.Serializer.Redis do
end

def deserialize_gate(["group/" <> group_name, enabled]) do
%Gate{type: :group, for: String.to_atom(group_name), enabled: parse_bool(enabled)}
%Gate{type: :group, for: group_name, enabled: parse_bool(enabled)}
end

def deserialize_flag(name, []), do: Flag.new(name, [])
Expand Down
15 changes: 10 additions & 5 deletions test/fun_with_flags/gate_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,18 @@ defmodule FunWithFlags.GateTest do
end
end

test "new(:group, group_name, true|false) returns a new Group Gate" do
assert %Gate{type: :group, for: :plants, enabled: true} = Gate.new(:group, :plants, true)
assert %Gate{type: :group, for: :animals, enabled: false} = Gate.new(:group, :animals, false)
test "new(:group, group_name, true|false) returns a new Group Gate, with atoms" do
assert %Gate{type: :group, for: "plants", enabled: true} = Gate.new(:group, :plants, true)
assert %Gate{type: :group, for: "animals", enabled: false} = Gate.new(:group, :animals, false)
end

test "new(:group, ...) with a name that is not an atom raises an exception" do
assert_raise FunWithFlags.Gate.InvalidGroupNameError, fn() -> Gate.new(:group, "a_binary", true) end
test "new(:group, group_name, true|false) returns a new Group Gate, with binaries" do
assert %Gate{type: :group, for: "plants", enabled: true} = Gate.new(:group, "plants", true)
assert %Gate{type: :group, for: "animals", enabled: false} = Gate.new(:group, "animals", false)
end

test "new(:group, ...) with a name that is not an atom or a binary raises an exception" do
assert_raise FunWithFlags.Gate.InvalidGroupNameError, fn() -> Gate.new(:group, 123, true) end
assert_raise FunWithFlags.Gate.InvalidGroupNameError, fn() -> Gate.new(:group, %{a: "map"}, false) end
end
end
Expand Down
4 changes: 2 additions & 2 deletions test/fun_with_flags/simple_store_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule FunWithFlags.SimpleStoreTest do

describe "delete(flag_name, gate)" do
setup do
group_gate = %Gate{type: :group, for: :muggles, enabled: false}
group_gate = %Gate{type: :group, for: "muggles", enabled: false}
bool_gate = %Gate{type: :boolean, enabled: true}
name = unique_atom()

Expand Down Expand Up @@ -76,7 +76,7 @@ defmodule FunWithFlags.SimpleStoreTest do

describe "delete(flag_name)" do
setup do
group_gate = %Gate{type: :group, for: :muggles, enabled: false}
group_gate = %Gate{type: :group, for: "muggles", enabled: false}
bool_gate = %Gate{type: :boolean, enabled: true}
name = unique_atom()

Expand Down
4 changes: 2 additions & 2 deletions test/fun_with_flags/store/persistent/ecto_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do
setup do
name = unique_atom()
bool_gate = %Gate{type: :boolean, enabled: false}
group_gate = %Gate{type: :group, for: :admins, enabled: true}
group_gate = %Gate{type: :group, for: "admins", enabled: true}
actor_gate = %Gate{type: :actor, for: "string_actor", enabled: true}
flag = %Flag{name: name, gates: sort_gates([bool_gate, group_gate, actor_gate])}

Expand Down Expand Up @@ -261,7 +261,7 @@ defmodule FunWithFlags.Store.Persistent.EctoTest do
setup do
name = unique_atom()
bool_gate = %Gate{type: :boolean, enabled: false}
group_gate = %Gate{type: :group, for: :admins, enabled: true}
group_gate = %Gate{type: :group, for: "admins", enabled: true}
actor_gate = %Gate{type: :actor, for: "string_actor", enabled: true}
flag = %Flag{name: name, gates: sort_gates([bool_gate, group_gate, actor_gate])}

Expand Down
4 changes: 2 additions & 2 deletions test/fun_with_flags/store/persistent/redis_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ defmodule FunWithFlags.Store.Persistent.RedisTest do
setup do
name = unique_atom()
bool_gate = %Gate{type: :boolean, enabled: false}
group_gate = %Gate{type: :group, for: :admins, enabled: true}
group_gate = %Gate{type: :group, for: "admins", enabled: true}
actor_gate = %Gate{type: :actor, for: "string_actor", enabled: true}
flag = %Flag{name: name, gates: [bool_gate, group_gate, actor_gate]}

Expand Down Expand Up @@ -428,7 +428,7 @@ defmodule FunWithFlags.Store.Persistent.RedisTest do
setup do
name = unique_atom()
bool_gate = %Gate{type: :boolean, enabled: false}
group_gate = %Gate{type: :group, for: :admins, enabled: true}
group_gate = %Gate{type: :group, for: "admins", enabled: true}
actor_gate = %Gate{type: :actor, for: "string_actor", enabled: true}
flag = %Flag{name: name, gates: [bool_gate, group_gate, actor_gate]}

Expand Down
10 changes: 5 additions & 5 deletions test/fun_with_flags/store/serializer/ecto_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ defmodule FunWithFlags.Store.Serializer.EctoTest do
flag = %Flag{name: flag_name, gates: [
%Gate{type: :actor, for: "user:123", enabled: true},
%Gate{type: :boolean, enabled: true},
%Gate{type: :group, for: :admins, enabled: false},
%Gate{type: :group, for: "admins", enabled: false},
]}
assert ^flag = Serializer.deserialize_flag(flag_name, [bool_record, actor_record, group_record])

flag = %Flag{name: flag_name, gates: [
%Gate{type: :actor, for: "user:123", enabled: true},
%Gate{type: :actor, for: "string:albicocca", enabled: false},
%Gate{type: :boolean, enabled: true},
%Gate{type: :group, for: :admins, enabled: false},
%Gate{type: :group, for: :penguins, enabled: true},
%Gate{type: :group, for: "admins", enabled: false},
%Gate{type: :group, for: "penguins", enabled: true},
]}

actor_record_2 = %{actor_record | id: 5, target: "string:albicocca", enabled: false}
Expand Down Expand Up @@ -82,10 +82,10 @@ defmodule FunWithFlags.Store.Serializer.EctoTest do

test "with group data", %{flag_name: flag_name, group_record: group_record} do
group_record = %{group_record | enabled: true}
assert %Gate{type: :group, for: :admins, enabled: true} = Serializer.deserialize_gate(flag_name, group_record)
assert %Gate{type: :group, for: "admins", enabled: true} = Serializer.deserialize_gate(flag_name, group_record)

group_record = %{group_record | enabled: false}
assert %Gate{type: :group, for: :admins, enabled: false} = Serializer.deserialize_gate(flag_name, group_record)
assert %Gate{type: :group, for: "admins", enabled: false} = Serializer.deserialize_gate(flag_name, group_record)
end
end
end
12 changes: 9 additions & 3 deletions test/fun_with_flags/store/serializer/redis_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ defmodule FunWithFlags.Store.Serializer.RedisTest do

gate = %Gate{type: :group, for: :swimmers, enabled: false}
assert ["group/swimmers", "false"] = Serializer.serialize(gate)

gate = %Gate{type: :group, for: "runners", enabled: true}
assert ["group/runners", "true"] = Serializer.serialize(gate)

gate = %Gate{type: :group, for: "swimmers", enabled: false}
assert ["group/swimmers", "false"] = Serializer.serialize(gate)
end
end

Expand Down Expand Up @@ -61,7 +67,7 @@ defmodule FunWithFlags.Store.Serializer.RedisTest do
%Gate{type: :actor, for: "string:albicocca", enabled: true},
%Gate{type: :boolean, enabled: false},
%Gate{type: :actor, for: "user:123", enabled: false},
%Gate{type: :group, for: :penguins, enabled: true},
%Gate{type: :group, for: "penguins", enabled: true},
]}

raw_redis_data = [
Expand All @@ -86,8 +92,8 @@ defmodule FunWithFlags.Store.Serializer.RedisTest do
end

test "with group data" do
assert %Gate{type: :group, for: :fishes, enabled: true} = Serializer.deserialize_gate(["group/fishes", "true"])
assert %Gate{type: :group, for: :cetacea, enabled: false} = Serializer.deserialize_gate(["group/cetacea", "false"])
assert %Gate{type: :group, for: "fishes", enabled: true} = Serializer.deserialize_gate(["group/fishes", "true"])
assert %Gate{type: :group, for: "cetacea", enabled: false} = Serializer.deserialize_gate(["group/cetacea", "false"])
end
end

Expand Down
Loading