From 7a27d2d711c7ae8b6474f47c18585c93174aa51e Mon Sep 17 00:00:00 2001 From: Britton Broderick Date: Sat, 17 Jan 2026 15:37:07 -0700 Subject: [PATCH] feat: Adds igniter installer --- lib/mix/tasks/fun_with_flags.install.ex | 292 ++++++++++++++++++++++++ mix.exs | 1 + mix.lock | 13 ++ 3 files changed, 306 insertions(+) create mode 100644 lib/mix/tasks/fun_with_flags.install.ex diff --git a/lib/mix/tasks/fun_with_flags.install.ex b/lib/mix/tasks/fun_with_flags.install.ex new file mode 100644 index 0000000..4c529d5 --- /dev/null +++ b/lib/mix/tasks/fun_with_flags.install.ex @@ -0,0 +1,292 @@ +if Code.ensure_loaded?(Igniter) do + defmodule Mix.Tasks.FunWithFlags.Install do + @shortdoc "Installs and configures FunWithFlags" + + @moduledoc """ + Installs and configures FunWithFlags for your application. + + ## Usage + + mix fun_with_flags.install [options] + + ## Options + + * `--persistence` - The persistence adapter to use. Options: `ecto`, `redis`. Defaults to `ecto`. + * `--pubsub` - The pubsub adapter to use. Options: `phoenix`, `redis`. Defaults to `phoenix`. + * `--repo` - The Ecto repo to use (required when persistence is `ecto`). + * `--table-name` - The Ecto table name. Defaults to `fun_with_flags_toggles`. + + ## Examples + + # Install with Ecto persistence and Phoenix PubSub + mix fun_with_flags.install --persistence ecto --repo MyApp.Repo + + # Install with Redis persistence and Redis PubSub + mix fun_with_flags.install --persistence redis --pubsub redis + + """ + + use Igniter.Mix.Task + + @impl Igniter.Mix.Task + def info(_argv, _composing_task) do + %Igniter.Mix.Task.Info{ + group: :fun_with_flags, + adds_deps: [], + installs: [], + example: "mix fun_with_flags.install --persistence ecto --repo MyApp.Repo", + only: nil, + positional: [], + schema: [ + persistence: :string, + pubsub: :string, + repo: :string, + table_name: :string + ], + defaults: [ + persistence: "ecto", + pubsub: "phoenix", + table_name: "fun_with_flags_toggles" + ], + aliases: [], + required: [] + } + end + + @impl Igniter.Mix.Task + def igniter(igniter) do + opts = igniter.args.options + + persistence = opts[:persistence] + pubsub = opts[:pubsub] + repo = opts[:repo] + table_name = opts[:table_name] + + validate_and_configure(igniter, persistence, pubsub, repo, table_name) + end + + defp validate_and_configure(igniter, persistence, pubsub, repo, table_name) do + case {persistence, repo} do + {"ecto", nil} -> + select_repo_and_configure(igniter, pubsub, table_name) + + {"ecto", repo_string} -> + repo_module = Module.concat([repo_string]) + configure_with_ecto(igniter, repo_module, pubsub, table_name) + + {"redis", _} -> + configure_with_redis(igniter, pubsub) + + {invalid, _} -> + Igniter.add_issue( + igniter, + "Invalid persistence adapter: #{inspect(invalid)}. Must be 'ecto' or 'redis'." + ) + end + end + + defp select_repo_and_configure(igniter, pubsub, table_name) do + case Igniter.Libs.Ecto.select_repo(igniter, label: "Which Ecto repo should FunWithFlags use?") do + {igniter, nil} -> + Igniter.add_issue( + igniter, + "No Ecto repo found. Please specify a repo with --repo or ensure your project has an Ecto repo configured." + ) + + {igniter, repo} -> + configure_with_ecto(igniter, repo, pubsub, table_name) + end + end + + defp configure_with_ecto(igniter, repo, pubsub, table_name) do + persistence_config = + {:code, + Sourceror.parse_string!(""" + [adapter: FunWithFlags.Store.Persistent.Ecto, repo: #{inspect(repo)}] + """)} + + igniter + |> configure_persistence(persistence_config) + |> configure_cache() + |> configure_pubsub(pubsub) + |> generate_ecto_migration(repo, table_name) + |> add_ecto_notices(repo, table_name) + end + + defp configure_with_redis(igniter, pubsub) do + persistence_config = + {:code, + Sourceror.parse_string!(""" + [adapter: FunWithFlags.Store.Persistent.Redis] + """)} + + igniter + |> configure_persistence(persistence_config) + |> configure_redis() + |> configure_cache() + |> configure_pubsub(pubsub) + |> add_redis_notices() + end + + defp configure_persistence(igniter, persistence_config) do + Igniter.Project.Config.configure( + igniter, + "config.exs", + :fun_with_flags, + [:persistence], + persistence_config + ) + end + + defp configure_redis(igniter) do + redis_config = + {:code, + Sourceror.parse_string!(""" + [host: "localhost", port: 6379, database: 0] + """)} + + Igniter.Project.Config.configure_new( + igniter, + "config.exs", + :fun_with_flags, + [:redis], + redis_config + ) + end + + defp configure_cache(igniter) do + cache_config = + {:code, + Sourceror.parse_string!(""" + [enabled: true, ttl: 900] + """)} + + Igniter.Project.Config.configure_new( + igniter, + "config.exs", + :fun_with_flags, + [:cache], + cache_config + ) + end + + defp configure_pubsub(igniter, "phoenix") do + pubsub_config = + {:code, + Sourceror.parse_string!(""" + [adapter: FunWithFlags.Notifications.PhoenixPubSub, client: nil] + """)} + + igniter + |> Igniter.Project.Config.configure( + "config.exs", + :fun_with_flags, + [:cache_bust_notifications], + pubsub_config + ) + |> Igniter.add_notice(""" + FunWithFlags is configured to use Phoenix.PubSub for cache notifications. + + You need to set the :client option in your config to your PubSub module: + + config :fun_with_flags, :cache_bust_notifications, + adapter: FunWithFlags.Notifications.PhoenixPubSub, + client: MyApp.PubSub + """) + end + + defp configure_pubsub(igniter, "redis") do + pubsub_config = + {:code, + Sourceror.parse_string!(""" + [adapter: FunWithFlags.Notifications.Redis] + """)} + + Igniter.Project.Config.configure( + igniter, + "config.exs", + :fun_with_flags, + [:cache_bust_notifications], + pubsub_config + ) + end + + defp configure_pubsub(igniter, invalid) do + Igniter.add_issue( + igniter, + "Invalid pubsub adapter: #{inspect(invalid)}. Must be 'phoenix' or 'redis'." + ) + end + + defp generate_ecto_migration(igniter, repo, table_name) do + migration_body = """ + def change do + create table(:#{table_name}) do + add :flag_name, :string, null: false + add :gate_type, :string, null: false + add :target, :string, null: false + add :enabled, :boolean, null: false + end + + create index(:#{table_name}, [:flag_name]) + create unique_index(:#{table_name}, [:flag_name, :gate_type, :target], name: "fwf_flag_name_gate_target_idx") + end + """ + + Igniter.Libs.Ecto.gen_migration( + igniter, + repo, + "create_fun_with_flags_table", + body: migration_body, + on_exists: :skip + ) + end + + defp add_ecto_notices(igniter, repo, table_name) do + Igniter.add_notice(igniter, """ + FunWithFlags has been configured with Ecto persistence. + + Summary: + - Persistence: Ecto (#{inspect(repo)}) + - Table name: #{table_name} + + Next steps: + 1. Run `mix deps.get` to fetch dependencies + 2. Run `mix ecto.migrate` to create the flags table + 3. Start using FunWithFlags in your application! + + Basic usage: + FunWithFlags.enabled?(:my_feature) + FunWithFlags.enable(:my_feature) + FunWithFlags.disable(:my_feature) + """) + end + + defp add_redis_notices(igniter) do + Igniter.add_notice(igniter, """ + FunWithFlags has been configured with Redis persistence. + + Summary: + - Persistence: Redis + - Default Redis config: localhost:6379, database 0 + + Next steps: + 1. Run `mix deps.get` to fetch dependencies + 2. Ensure you have `{:redix, "~> 1.0"}` in your dependencies + 3. Configure your Redis connection if needed: + + config :fun_with_flags, :redis, + host: "localhost", + port: 6379, + database: 0 + + 4. Start using FunWithFlags in your application! + + Basic usage: + FunWithFlags.enabled?(:my_feature) + FunWithFlags.enable(:my_feature) + FunWithFlags.disable(:my_feature) + """) + end + end +end diff --git a/mix.exs b/mix.exs index dec9430..3064488 100644 --- a/mix.exs +++ b/mix.exs @@ -75,6 +75,7 @@ defmodule FunWithFlags.Mixfile do {:myxql, "~> 0.2", optional: true, only: [:dev, :test]}, {:phoenix_pubsub, "~> 2.0", optional: true}, {:telemetry, "~> 1.3"}, + {:igniter, "~> 0.6", optional: true}, {:mock, "~> 0.3", only: :test}, diff --git a/mix.lock b/mix.lock index 39cf08b..0b42257 100644 --- a/mix.lock +++ b/mix.lock @@ -20,18 +20,31 @@ "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"}, "exqlite": {:hex, :exqlite, "0.33.0", "2cc96c4227fbb2d0864716def736dff18afb9949b1eaa74630822a0865b4b342", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8a7c2792e567bbebb4dafe96f6397f1c527edd7039d74f508a603817fbad2844"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, + "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, + "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "igniter": {:hex, :igniter, "0.7.0", "6848714fa5afa14258c82924a57af9364745316241a409435cf39cbe11e3ae80", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "1e7254780dbf4b44c9eccd6d86d47aa961efc298d7f520c24acb0258c8e90ba9"}, "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"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, "myxql": {:hex, :myxql, "0.8.0", "60c60e87c7320d2f5759416aa1758c8e7534efbae07b192861977f8455e35acd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4 or ~> 4.0", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "1ec0ceb26fb3cd0f8756519cf4f0e4f9348177a020705223bdf4742a2c44d774"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, "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"}, + "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, + "rewrite": {:hex, :rewrite, "1.2.0", "80220eb14010e175b67c939397e1a8cdaa2c32db6e2e0a9d5e23e45c0414ce21", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "a1cd702bbb9d51613ab21091f04a386d750fc6f4516b81900df082d78b2d8c50"}, + "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, + "spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"}, "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, }