diff --git a/.credo.exs b/.credo.exs index 6f868f8..edaf1de 100644 --- a/.credo.exs +++ b/.credo.exs @@ -46,7 +46,7 @@ # If you want to enforce a style guide and need a more traditional linting # experience, you can change `strict` to `true` below: # - strict: true, # false, + strict: true, # # To modify the timeout for parsing files, change this value: # diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..2f21c47 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,3 @@ +[ + inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] +] diff --git a/README.md b/README.md index f921683..6e6aab8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # FunWithFlags.UI [](https://github.com/tompave/fun_with_flags_ui/actions?query=branch%3Amaster) -[](https://github.com/tompave/fun_with_flags_ui/actions/workflows/quality.yml?query=branch%3Amaster) +[](https://github.com/tompave/fun_with_flags_ui/actions/workflows/quality.yml?query=branch%3Amaster) [](https://hex.pm/packages/fun_with_flags_ui) A Web dashboard for the [FunWithFlags](https://github.com/tompave/fun_with_flags) Elixir package. @@ -56,7 +56,7 @@ Again, because it's just a plug, it can be run [standalone](https://hexdocs.pm/p If you clone the repository, the library comes with two convenience functions to accomplish this: ```elixir -# Simple, let Cowboy sort out the supervision tree: +# Simple, let Bandit sort out the supervision tree: {:ok, pid} = FunWithFlags.UI.run_standalone() # Uses some explicit supervision configuration: @@ -115,7 +115,7 @@ For this reason this library enforces some stricter rules when creating flags an ## Installation -The package can be installed by adding `fun_with_flags_ui` to your list of dependencies in `mix.exs`. +The package can be installed by adding `fun_with_flags_ui` to your list of dependencies in `mix.exs`. It requires [`fun_with_flags`](https://hex.pm/packages/fun_with_flags), see its [installation documentation](https://github.com/tompave/fun_with_flags#installation) for more details. ```elixir diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..3f9e78e --- /dev/null +++ b/compose.yaml @@ -0,0 +1,5 @@ +services: + redis: + image: redis:8 + ports: + - "6379:6379" diff --git a/config/config.exs b/config/config.exs index 8d927ae..36c4fc3 100644 --- a/config/config.exs +++ b/config/config.exs @@ -10,5 +10,5 @@ config :fun_with_flags, :cache_bust_notifications, enabled: false case config_env() do :test -> import_config "test.exs" - _ -> nil + _ -> nil end diff --git a/lib/fun_with_flags/ui.ex b/lib/fun_with_flags/ui.ex index 4b38d03..42aba37 100644 --- a/lib/fun_with_flags/ui.ex +++ b/lib/fun_with_flags/ui.ex @@ -9,19 +9,18 @@ defmodule FunWithFlags.UI do @doc false def start(_type, _args) do - check_cowboy() + check_bandit() children = [ - {Plug.Cowboy, scheme: :http, plug: FunWithFlags.UI.Router, options: [port: 8080]} + {Bandit, scheme: :http, plug: FunWithFlags.UI.Router, options: [port: 8080]} ] opts = [strategy: :one_for_one, name: FunWithFlags.UI.Supervisor] Supervisor.start_link(children, opts) end - - # Since :cowboy is an optional dependency, if we want to run this - # standalone we want to return a clear error message if Cowboy is + # Since :bandit is an optional dependency, if we want to run this + # standalone we want to return a clear error message if Bandit is # missing. # # On the other hand, if :fun_with_flags_ui is run as a Plug in a @@ -29,28 +28,30 @@ defmodule FunWithFlags.UI do # here, as the responsibility of managing the HTTP layer belongs # to the host app. # - defp check_cowboy do - with :ok <- Application.ensure_started(:ranch), - :ok <- Application.ensure_started(:cowlib), - :ok <- Application.ensure_started(:cowboy) do - :ok - else + defp check_bandit do + case Application.ensure_started(:bandit) do + :ok -> + :ok + {:error, _} -> - raise "You need to add :cowboy to your Mix dependencies to run FunWithFlags.UI standalone." + raise "You need to add :bandit to your Mix dependencies to run FunWithFlags.UI standalone." end end - @doc """ - Convenience function to simply run the Plug in Cowboy. + Convenience function to simply run the Plug in Bandit. This _will_ be supervided, but in the private supervsion tree - of :cowboy and :ranch. + of :bandit. """ def run_standalone do - Plug.Cowboy.http FunWithFlags.UI.Router, [], port: 8080 - end + server_opts = [ + plug: FunWithFlags.UI.Router, + port: 8080 + ] + Bandit.start_link(server_opts) + end @doc """ Convenience function to run the Plug in a custom supervision tree. diff --git a/lib/fun_with_flags/ui/router.ex b/lib/fun_with_flags/ui/router.ex index 3251925..4e8554d 100644 --- a/lib/fun_with_flags/ui/router.ex +++ b/lib/fun_with_flags/ui/router.ex @@ -8,25 +8,26 @@ defmodule FunWithFlags.UI.Router do use Plug.Router alias FunWithFlags.UI.{SimpleActor, Templates, Utils} - if Mix.env == :dev do + if Mix.env() == :dev do use Plug.Debugger, otp_app: :fun_with_flags_ui end - plug Plug.Logger, log: :debug + plug(Plug.Logger, log: :debug) - plug Plug.Static, + plug(Plug.Static, gzip: true, at: "/assets", from: :fun_with_flags_ui + ) - plug :protect_from_forgery, Plug.CSRFProtection.init([]) + plug(:protect_from_forgery, Plug.CSRFProtection.init([])) - plug Plug.Parsers, parsers: [:urlencoded] - plug Plug.MethodOverride + plug(Plug.Parsers, parsers: [:urlencoded]) + plug(Plug.MethodOverride) - plug :assign_csrf_token - plug :match - plug :dispatch + plug(:assign_csrf_token) + plug(:match) + plug(:dispatch) @doc false def call(conn, opts) do @@ -34,13 +35,11 @@ defmodule FunWithFlags.UI.Router do super(conn, opts) end - get "/" do conn |> redirect_to("/flags") end - # form to create a new flag # get "/new" do @@ -48,7 +47,6 @@ defmodule FunWithFlags.UI.Router do |> html_resp(200, Templates.new(%{conn: conn})) end - # endpoint to create a new flag # post "/flags" do @@ -57,19 +55,26 @@ defmodule FunWithFlags.UI.Router do case Utils.validate_flag_name(conn, name) do :ok -> case Utils.create_flag_with_name(name) do - {:ok, _} -> redirect_to conn, "/flags/#{name}" - _ -> html_resp(conn, 400, Templates.new(%{conn: conn, error_message: "Something went wrong!"})) + {:ok, _} -> + redirect_to(conn, "/flags/#{name}") + + _ -> + html_resp( + conn, + 400, + Templates.new(%{conn: conn, error_message: "Something went wrong!"}) + ) end + {:fail, reason} -> html_resp(conn, 400, Templates.new(%{conn: conn, error_message: reason})) end end - # get a list of the flags # get "/flags" do - {:ok, flags} = FunWithFlags.all_flags + {:ok, flags} = FunWithFlags.all_flags() flags = Utils.sort_flags(flags) body = Templates.index(conn: conn, flags: flags) @@ -77,7 +82,6 @@ defmodule FunWithFlags.UI.Router do |> html_resp(200, body) end - # flag details # get "/flags/:name" do @@ -85,13 +89,13 @@ defmodule FunWithFlags.UI.Router do {:ok, flag} -> body = Templates.details(conn: conn, flag: flag) html_resp(conn, 200, body) + {:error, _} -> body = Templates.not_found(conn: conn, name: name) html_resp(conn, 404, body) end end - # to clear an entire flag # delete "/flags/:name" do @@ -99,10 +103,9 @@ defmodule FunWithFlags.UI.Router do |> String.to_existing_atom() |> FunWithFlags.clear() - redirect_to conn, "/flags" + redirect_to(conn, "/flags") end - # to toggle the default state of a flag # patch "/flags/:name/boolean" do @@ -115,19 +118,17 @@ defmodule FunWithFlags.UI.Router do FunWithFlags.disable(flag_name) end - redirect_to conn, "/flags/#{name}" + redirect_to(conn, "/flags/#{name}") end - # to clear a boolean gate # delete "/flags/:name/boolean" do flag_name = String.to_existing_atom(name) FunWithFlags.clear(flag_name, boolean: true) - redirect_to conn, "/flags/#{name}" + redirect_to(conn, "/flags/#{name}") end - # to toggle an actor gate # patch "/flags/:name/actors/:actor_id" do @@ -141,10 +142,9 @@ defmodule FunWithFlags.UI.Router do FunWithFlags.disable(flag_name, for_actor: actor) end - redirect_to conn, "/flags/#{name}#actor_#{actor_id}" + redirect_to(conn, "/flags/#{name}#actor_#{actor_id}") end - # to clear an actor gate # delete "/flags/:name/actors/:actor_id" do @@ -152,10 +152,9 @@ defmodule FunWithFlags.UI.Router do actor = %SimpleActor{id: actor_id} FunWithFlags.clear(flag_name, for_actor: actor) - redirect_to conn, "/flags/#{name}#actor_gates" + redirect_to(conn, "/flags/#{name}#actor_gates") end - # to toggle a group gate # patch "/flags/:name/groups/:group_name" do @@ -169,10 +168,9 @@ defmodule FunWithFlags.UI.Router do FunWithFlags.disable(flag_name, for_group: group_name) end - redirect_to conn, "/flags/#{name}#group_#{group_name}" + redirect_to(conn, "/flags/#{name}#group_#{group_name}") end - # to clear a group gate # delete "/flags/:name/groups/:group_name" do @@ -181,19 +179,17 @@ defmodule FunWithFlags.UI.Router do FunWithFlags.clear(flag_name, for_group: group_name) - redirect_to conn, "/flags/#{name}#group_gates" + redirect_to(conn, "/flags/#{name}#group_gates") end - # to clear a percentage gate # delete "/flags/:name/percentage" do flag_name = String.to_existing_atom(name) FunWithFlags.clear(flag_name, for_percentage: true) - redirect_to conn, "/flags/#{name}" + redirect_to(conn, "/flags/#{name}") end - # to add a new actor to a flag # post "/flags/:name/actors" do @@ -204,20 +200,29 @@ defmodule FunWithFlags.UI.Router do :ok -> enabled = Utils.parse_bool(conn.params["enabled"]) actor = %SimpleActor{id: actor_id} + if enabled do FunWithFlags.enable(flag_name, for_actor: actor) else FunWithFlags.disable(flag_name, for_actor: actor) end - redirect_to conn, "/flags/#{name}#actor_#{actor_id}" + + redirect_to(conn, "/flags/#{name}#actor_#{actor_id}") + {:fail, reason} -> {:ok, flag} = Utils.get_flag(name) - body = Templates.details(conn: conn, flag: flag, actor_error_message: "The actor ID #{reason}.") + + body = + Templates.details( + conn: conn, + flag: flag, + actor_error_message: "The actor ID #{reason}." + ) + html_resp(conn, 400, body) end end - # to add a new group to a flag # post "/flags/:name/groups" do @@ -227,20 +232,29 @@ defmodule FunWithFlags.UI.Router do case Utils.validate(group_name) do :ok -> enabled = Utils.parse_bool(conn.params["enabled"]) + if enabled do FunWithFlags.enable(flag_name, for_group: group_name) else FunWithFlags.disable(flag_name, for_group: group_name) end - redirect_to conn, "/flags/#{name}#group_#{group_name}" + + redirect_to(conn, "/flags/#{name}#group_#{group_name}") + {:fail, reason} -> {:ok, flag} = Utils.get_flag(name) - body = Templates.details(conn: conn, flag: flag, group_error_message: "The group name #{reason}.") + + body = + Templates.details( + conn: conn, + flag: flag, + group_error_message: "The group name #{reason}." + ) + html_resp(conn, 400, body) end end - # to add or replace a percentage gate # post "/flags/:name/percentage" do @@ -250,49 +264,54 @@ defmodule FunWithFlags.UI.Router do case Utils.parse_and_validate_float(conn.params["percent_value"]) do {:ok, float} -> FunWithFlags.enable(flag_name, for_percentage_of: {type, float}) - redirect_to conn, "/flags/#{name}#percentage_gate" + redirect_to(conn, "/flags/#{name}#percentage_gate") + {:fail, reason} -> {:ok, flag} = Utils.get_flag(name) - body = Templates.details(conn: conn, flag: flag, percentage_error_message: "The percentage value #{reason}.") + + body = + Templates.details( + conn: conn, + flag: flag, + percentage_error_message: "The percentage value #{reason}." + ) + html_resp(conn, 400, body) end end - match _ do send_resp(conn, 404, "") end - defp html_resp(conn, status, body) do conn |> put_resp_content_type("text/html") |> send_resp(status, body) end - defp redirect_to(conn, uri) do path = Path.join(conn.assigns[:namespace], uri) conn |> put_resp_header("location", path) |> put_resp_content_type("text/html") - |> send_resp(302, "
You are being redirected.") + |> send_resp( + 302, + "You are being redirected." + ) end - defp extract_namespace(conn, opts) do ns = opts[:namespace] || "" Plug.Conn.assign(conn, :namespace, "/" <> ns) end - defp assign_csrf_token(conn, _opts) do csrf_token = Plug.CSRFProtection.get_csrf_token() Plug.Conn.assign(conn, :csrf_token, csrf_token) end - # Custom CSRF protection plug. It wraps the default plug provided # by `Plug`, it calls `Plug.Conn.fetch_session/1` (no-op if already # fetched), and it bails out gracefully if no session is configured. @@ -304,7 +323,10 @@ defmodule FunWithFlags.UI.Router do |> Plug.CSRFProtection.call(opts) rescue _e in ArgumentError -> - Logger.warning("CSRF protection won't work unless your host application uses the session plug") + Logger.warning( + "CSRF protection won't work unless your host application uses the session plug" + ) + conn end end diff --git a/lib/fun_with_flags/ui/templates.ex b/lib/fun_with_flags/ui/templates.ex index 8b0f6cb..2598543 100644 --- a/lib/fun_with_flags/ui/templates.ex +++ b/lib/fun_with_flags/ui/templates.ex @@ -18,20 +18,26 @@ defmodule FunWithFlags.UI.Templates do _percentage_row: "rows/_percentage", _new_actor_row: "rows/_new_actor", _new_group_row: "rows/_new_group", - _percentage_form_row: "rows/_percentage_form", + _percentage_form_row: "rows/_percentage_form" ] for {fn_name, file_name} <- @templates do - EEx.function_from_file :def, fn_name, Path.expand("./templates/#{file_name}.html.eex", __DIR__), [:assigns] + EEx.function_from_file( + :def, + fn_name, + Path.expand("./templates/#{file_name}.html.eex", __DIR__), + [:assigns] + ) end - def html_smart_status_for(flag) do case Utils.get_flag_status(flag) do :fully_open -> ~s(Enabled) + :half_open -> ~s(Enabled) + :closed -> ~s(Disabled) end @@ -54,29 +60,27 @@ defmodule FunWithFlags.UI.Templates do end @gate_type_order [ - :boolean, - :actor, - :group, - :percentage_of_actors, - :percentage_of_time, - ] - |> Enum.with_index() - |> Map.new() + :boolean, + :actor, + :group, + :percentage_of_actors, + :percentage_of_time + ] + |> Enum.with_index() + |> Map.new() def html_gate_list(%Flag{gates: gates}) do gates - |> Enum.map(&(&1.type)) + |> Enum.map(& &1.type) |> Enum.uniq() |> Enum.sort_by(&Map.get(@gate_type_order, &1, 99)) |> Enum.join(", ") end - def path(conn, path) do Path.join(conn.assigns[:namespace], path) end - def url_safe(val) do val |> to_string() diff --git a/lib/fun_with_flags/ui/utils.ex b/lib/fun_with_flags/ui/utils.ex index 087596a..986b194 100644 --- a/lib/fun_with_flags/ui/utils.ex +++ b/lib/fun_with_flags/ui/utils.ex @@ -3,11 +3,11 @@ defmodule FunWithFlags.UI.Utils do 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 @@ -21,6 +21,7 @@ defmodule FunWithFlags.UI.Utils do case Enum.find(gates, &Gate.boolean?/1) do %Gate{type: :boolean, enabled: enabled} -> {:ok, enabled} + nil -> :missing end @@ -28,11 +29,10 @@ defmodule FunWithFlags.UI.Utils do defp any_other_gate_open?(gates) do gates - |> Enum.filter(fn(gate) -> !Gate.boolean?(gate) end) - |> Enum.any?(fn(%Gate{enabled: enabled}) -> enabled end) + |> 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 @@ -45,18 +45,21 @@ defmodule FunWithFlags.UI.Utils do a.name < b.name else case sa do - :fully_open -> true + :fully_open -> + true + :half_open -> case sb do :fully_open -> false :closed -> true end - :closed -> false + + :closed -> + false end end end - # Create new flags as disabled. # # Here we are converting a user-provided string to an atom, which is @@ -71,25 +74,24 @@ defmodule FunWithFlags.UI.Utils do |> FunWithFlags.disable() end - def get_flag(name) do if safe_flag_exists?(name) do - FunWithFlags.SimpleStore.lookup(String.to_existing_atom(name)) # {:ok, flag}, or raise + # {:ok, flag}, or raise + FunWithFlags.SimpleStore.lookup(String.to_existing_atom(name)) 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 = string |> String.trim() |> String.length() length == 0 end - def boolean_gate(%Flag{gates: gates}) do Enum.find(gates, &Gate.boolean?/1) end @@ -97,34 +99,34 @@ defmodule FunWithFlags.UI.Utils do def actor_gates(%Flag{gates: gates}) do gates |> Enum.filter(&Gate.actor?/1) - |> Enum.sort_by(&(&1.for)) + |> Enum.sort_by(& &1.for) end def group_gates(%Flag{gates: gates}) do gates |> Enum.filter(&Gate.group?/1) - |> Enum.sort_by(&(&1.for)) + |> Enum.sort_by(& &1.for) end def percentage_gate(%Flag{gates: gates}) do - Enum.find(gates, fn(g) -> + 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."} + + {:fail, + "A flag named '#{name}' already exists."} else :ok end @@ -133,7 +135,6 @@ defmodule FunWithFlags.UI.Utils do 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. @@ -152,7 +153,6 @@ defmodule FunWithFlags.UI.Utils do end end - def sanitize(name) do name |> String.trim() @@ -160,17 +160,19 @@ defmodule FunWithFlags.UI.Utils do 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"} @@ -178,15 +180,16 @@ defmodule FunWithFlags.UI.Utils do 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. @@ -199,7 +202,6 @@ defmodule FunWithFlags.UI.Utils do end end - # Deal with floating point rounding errors without # losing precision. # @@ -225,7 +227,6 @@ defmodule FunWithFlags.UI.Utils do end end - defp _decimal_digits(float) do float |> Float.to_string() diff --git a/mix.exs b/mix.exs index 030557b..5558e02 100644 --- a/mix.exs +++ b/mix.exs @@ -9,17 +9,16 @@ defmodule FunWithFlagsUi.Mixfile do source_url: "https://github.com/tompave/fun_with_flags_ui", version: @version, elixir: "~> 1.16", - elixirc_paths: elixirc_paths(Mix.env), - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, + elixirc_paths: elixirc_paths(Mix.env()), + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, deps: deps(), description: description(), package: package(), - docs: docs(), + docs: docs() ] end - # The most common use case for this library is to embed it in # a host web application and serve it from a sub path: it should # just be plug'ed into a Phoenix or Plug router. @@ -33,28 +32,24 @@ defmodule FunWithFlagsUi.Mixfile do # def application do [ - extra_applications: [:logger], + extra_applications: [:logger] # mod: {FunWithFlags.UI, []}, ] end - defp deps do [ {:plug, "~> 1.12"}, - {:plug_cowboy, ">= 2.0.0", optional: true}, - {:cowboy, ">= 2.0.0", optional: true}, + {:bandit, "~> 1.8", optional: true}, {:fun_with_flags, "~> 1.12"}, {:redix, "~> 1.0", only: [:dev, :test]}, {:ex_doc, ">= 0.0.0", only: :dev}, - {:credo, "~> 1.7", only: :dev, runtime: false}, + {:credo, "~> 1.7", only: :dev, runtime: false} ] end - defp elixirc_paths(:test), do: ["lib", "test/support"] - defp elixirc_paths(_), do: ["lib"] - + defp elixirc_paths(_), do: ["lib"] defp description do """ @@ -71,12 +66,11 @@ defmodule FunWithFlagsUi.Mixfile do "MIT" ], links: %{ - "GitHub" => "https://github.com/tompave/fun_with_flags_ui", + "GitHub" => "https://github.com/tompave/fun_with_flags_ui" } ] end - defp docs do [ extras: ["README.md"], diff --git a/mix.lock b/mix.lock index 101fb41..b87875a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ %{ + "bandit": {:hex, :bandit, "1.8.0", "c2e93d7e3c5c794272fa4623124f827c6f24b643acc822be64c826f9447d92fb", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8458ff4eed20ff2a2ea69d4854883a077c33ea42b51f6811b044ceee0fa15422"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cowboy": {:hex, :cowboy, "2.14.0", "565dcf221ba99b1255b0adcec24d2d8dbe79e46ec79f30f8373cceadc6a41e2a", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "ea99769574550fe8a83225c752e8a62780a586770ef408816b82b6fe6d46476b"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, @@ -9,6 +10,7 @@ "ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "fun_with_flags": {:hex, :fun_with_flags, "1.13.0", "8e8eddd6b723691211387923b160bc779fa593f59485209a058c90ea4bbb1cd5", [:mix], [{:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dbbbff607e6f096d3857c88530d1a4e178d9eeec5dc00118e2cc1d076578710a"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, @@ -22,4 +24,6 @@ "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "redix": {:hex, :redix, "1.5.2", "ab854435a663f01ce7b7847f42f5da067eea7a3a10c0a9d560fa52038fd7ab48", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "78538d184231a5d6912f20567d76a49d1be7d3fca0e1aaaa20f4df8e1142dcb8"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "thousand_island": {:hex, :thousand_island, "1.4.1", "8df065e627407e281f7935da5ad0f3842d10eb721afa92e760b720d71e2e37aa", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "204a8640e5d2818589b87286ae66160978628d7edf6095181cbe0440765fb6c1"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, } diff --git a/test/fun_with_flags/ui/html_escape_test.exs b/test/fun_with_flags/ui/html_escape_test.exs index 962bf34..2b3114c 100644 --- a/test/fun_with_flags/ui/html_escape_test.exs +++ b/test/fun_with_flags/ui/html_escape_test.exs @@ -6,7 +6,10 @@ defmodule FunWithFlags.UI.HTMLEscapeTest do test "it HTML-escapes the input" do assert "<div>" = to_string(html_escape("