defmodule FunWithFlags.UI.Utils do @moduledoc false alias FunWithFlags.{Flag, Gate} def get_flag_status(%Flag{gates: gates} = flag) do case boolean_gate_open?(flag) do {:ok, true} -> :fully_open _ -> if any_other_gate_open?(gates) do :half_open else :closed end end end def boolean_gate_open?(%Flag{gates: gates}) do case Enum.find(gates, &Gate.boolean?/1) do %Gate{type: :boolean, enabled: enabled} -> {:ok, enabled} nil -> :missing end end defp any_other_gate_open?(gates) do gates |> Enum.filter(fn(gate) -> !Gate.boolean?(gate) end) |> Enum.any?(fn(%Gate{enabled: enabled}) -> enabled end) end def sort_flags(flags) do Enum.sort(flags, &sorter/2) end defp sorter(a, b) do sa = get_flag_status(a) sb = get_flag_status(b) if sa == sb do a.name < b.name else case sa do :fully_open -> true :half_open -> case sb do :fully_open -> false :closed -> true end :closed -> false end end end # Create new flags as disabled. # # Here we are converting a user-provided string to an atom, which is # potentially dangerous because atoms are not garbage collected. # Since we're going to persist it, however, the idea is that it's going # to be used anyway, and to be fair filling the persistent store with # unneeded data is a bigger concern. # def create_flag_with_name(name) do name |> String.to_atom() |> FunWithFlags.disable() end def get_flag(name) do if safe_flag_exists?(name) do case FunWithFlags.SimpleStore.lookup(String.to_existing_atom(name)) do {:ok, _flag} = result -> result {:error, _reason} = error -> error end else {:error, "not found"} end end def blank?(nil), do: true def blank?(""), do: true def blank?(" "), do: true def blank?(string) when is_binary(string) do length = string |> String.trim |> String.length length == 0 end def boolean_gate(%Flag{gates: gates}) do Enum.find(gates, &Gate.boolean?/1) end def actor_gates(%Flag{gates: gates}) do gates |> Enum.filter(&Gate.actor?/1) |> Enum.sort_by(&(&1.for)) end def group_gates(%Flag{gates: gates}) do gates |> Enum.filter(&Gate.group?/1) |> Enum.sort_by(&(&1.for)) end def percentage_gate(%Flag{gates: gates}) do Enum.find(gates, fn(g) -> Gate.percentage_of_time?(g) or Gate.percentage_of_actors?(g) end) end def parse_bool("true"), do: true def parse_bool("1"), do: true def parse_bool(1), do: true def parse_bool(true), do: true def parse_bool(_), do: false def validate_flag_name(conn, name) do if Regex.match?(~r/^\w+$/, name) do if safe_flag_exists?(name) do path = Path.join(conn.assigns[:namespace], "/flags/" <> name) {:fail, "A flag named '#{name}' already exists."} else :ok end else {:fail, "Invalid flag name, it must match /^\w+$/."} end end # We don't want to just convert any user provided string to an atom because # atoms are not garbage collected, and this could potentially leak memory # if some endpoint was abused and hammered with random non-existing flag names. # # Getting the list of the current flag names will reference and create all the # current atoms, and then `String.to_existing_atom/1` will simply raise an # error if the name doesn't match any existing atom. That implicitly proves # that there is no flag for that name # defp safe_flag_exists?(name) do try do {:ok, all} = FunWithFlags.all_flag_names() Enum.member?(all, String.to_existing_atom(name)) rescue ArgumentError -> false end end def sanitize(name) do name |> String.trim() end def validate(name) do string = to_string(name) cond do blank?(string) -> {:fail, "can't be blank"} String.match?(string, ~r/\?/) -> {:fail, "includes invalid characters: '?'"} true -> :ok end end def parse_and_validate_float(string) do if blank?(string) do {:fail, "can't be blank"} else case Float.parse(string) do {float, _} when float > 0 and float < 1 -> {:ok, float} {_float, _} -> {:fail, "is outside the '0.0 < x < 1.0' range"} :error -> {:fail, "is not a valid decimal number"} end end end # If we have an unexpected value here it's because people # are messing around with the s in the form. Just # use a default for unexpected values. # def parse_percentage_type(string) do case string do "time" -> :time "actors" -> :actors _ -> :time end end # Deal with floating point rounding errors without # losing precision. # # for example, to avoid these: # 0.421337 * 100 = 42.133700000000005 # 0.123457 * 100 = 12.345699999999999 # def as_percentage(float) when is_float(float) do percentage = float * 100 if round(percentage) == percentage do # our job is done percentage else # let's assume that here `percentage` has lots # of decimal digits with a rounding error. # # We know that this will be `> 2`, because # if it was `<= 2` then it would have # short-circuited in the if. decimal_digits = _decimal_digits(float) Float.round(percentage, decimal_digits - 2) end end defp _decimal_digits(float) do float |> Float.to_string() |> String.split(".") |> List.last() |> String.length() end end