diff --git a/README.md b/README.md index e5b3f0c..f5690d9 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,14 @@ Supervisor.init( ) ``` +**using [`igniter`](https://hex.pm/packages/igniter)** + +```bash +mix oidcc.gen.provider_configuration_worker \ + --name MyApp.OidccConfigProvider \ + --issuer https://accounts.google.com +``` + ## Usage ### Companion libraries @@ -213,8 +221,6 @@ Supervisor.init( - [`oidcc_plug`](https://hex.pm/packages/oidcc_plug) - Integrations for [`plug`](https://hex.pm/packages/plug) and [`phoenix`](https://hex.pm/packages/phoenix) -- [`phx_gen_oidcc`](https://hex.pm/packages/phx_gen_oidcc) - Setup Generator for - [`phoenix`](https://hex.pm/packages/phoenix) - [`ueberauth_oidcc`](https://hex.pm/packages/ueberauth_oidcc) - Integration for [`ueberauth`](https://hex.pm/packages/ueberauth) diff --git a/lib/mix/tasks/oidcc.gen.provider_configuration_worker.ex b/lib/mix/tasks/oidcc.gen.provider_configuration_worker.ex new file mode 100644 index 0000000..a16a102 --- /dev/null +++ b/lib/mix/tasks/oidcc.gen.provider_configuration_worker.ex @@ -0,0 +1,141 @@ +defmodule Mix.Tasks.Oidcc.Gen.ProviderConfigurationWorker do + @example "mix oidcc.gen.provider_configuration_worker --name MyApp.OpenIDProvider" + + @shortdoc "Generate an OpenID Connect provider configuration worker" + if !Code.ensure_loaded?(Igniter) do + @shortdoc "#{@shortdoc} | Install `igniter` to use" + end + + @moduledoc """ + #{@shortdoc} + + Adds an `Oidcc.ProviderConfiguration.Worker` to your application and + configures it via the `runtime.exs` configuration file. + + ## Example + + ```bash + #{@example} + ``` + + ## Options + + * `--name` or `-n` - The name of the provider configuration worker + * `--issuer` or `-i` - The issuer of the provider + """ + + if Code.ensure_loaded?(Igniter) do + use Igniter.Mix.Task + + alias Igniter.Code.Module + alias Igniter.Project.Application + alias Igniter.Project.Config + + @impl Igniter.Mix.Task + def info(_argv, _composing_task) do + %Igniter.Mix.Task.Info{ + # dependencies to add + adds_deps: [], + # dependencies to add and call their associated installers, if they exist + installs: [], + # An example invocation + example: @example, + # Accept additional arguments that are not in your schema + # Does not guarantee that, when composed, the only options you get are the ones you define + extra_args?: false, + # A list of environments that this should be installed in, only relevant if this is an installer. + only: nil, + # a list of positional arguments, i.e `[:file]` + positional: [], + # Other tasks your task composes using `Igniter.compose_task`, passing in the CLI argv + # This ensures your option schema includes options from nested tasks + composes: [], + # `OptionParser` schema + schema: [name: :string, issuer: :string], + # CLI aliases + aliases: [n: :name, i: :issuer] + } + end + + @impl Igniter.Mix.Task + def igniter(igniter, argv) do + # extract positional arguments according to `positional` above + {_arguments, argv} = positional_args!(argv) + # extract options according to `schema` and `aliases` above + options = setup_options(argv, igniter) + + igniter + |> configure_issuer(options) + |> add_application_worker(options) + end + + defp setup_options(argv, igniter) do + argv + |> options!() + |> Keyword.update( + :name, + Module.module_name(igniter, "OpenIDProvider"), + &Module.parse/1 + ) + |> Keyword.put(:app_name, Igniter.Project.Application.app_name(igniter)) + end + + defp configure_issuer(igniter, options) do + env_prefix = + options[:name] |> Macro.underscore() |> String.upcase() |> String.replace("/", "_") + + config = + case Keyword.fetch(options, :issuer) do + {:ok, issuer} -> + quote do + [issuer: System.get_env(unquote("#{env_prefix}_ISSUER"), unquote(issuer))] + end + + :error -> + quote do + [issuer: System.fetch_env!(unquote("#{env_prefix}_ISSUER"))] + end + end + + Config.configure_new( + igniter, + "runtime.exs", + options[:app_name], + [options[:name]], + {:code, config} + ) + end + + defp add_application_worker(igniter, options) do + Application.add_new_child( + igniter, + {Oidcc.ProviderConfiguration.Worker, + {:code, + quote do + %{ + name: unquote(options[:name]), + issuer: + Application.fetch_env!(unquote(options[:app_name]), unquote(options[:name]))[ + :issuer + ] + } + end}} + ) + end + else + use Mix.Task + + @impl Mix.Task + def run(_argv) do + Mix.shell().error(""" + The task 'oidcc.gen.provider_configuration_worker' requires igniter to be run. + + Please install igniter and try again. + + For more information, see: https://hexdocs.pm/igniter + """) + + exit({:shutdown, 1}) + end + end +end diff --git a/mix.exs b/mix.exs index 59224f3..9d0e507 100644 --- a/mix.exs +++ b/mix.exs @@ -40,7 +40,8 @@ defmodule Oidcc.Mixfile do {:mock, "~> 0.3.8", only: :test}, {:ex_doc, "~> 0.29", only: :dev, runtime: false}, {:credo, "~> 1.7", only: :dev, runtime: false}, - {:dialyxir, "~> 1.4", only: :dev, runtime: false} + {:dialyxir, "~> 1.4", only: :dev, runtime: false}, + {:igniter, "~> 0.3.34", optional: true} ] end diff --git a/test/mix/tasks/oidcc.gen.provider_configuration_worker_test.exs b/test/mix/tasks/oidcc.gen.provider_configuration_worker_test.exs new file mode 100644 index 0000000..0dac8a7 --- /dev/null +++ b/test/mix/tasks/oidcc.gen.provider_configuration_worker_test.exs @@ -0,0 +1,94 @@ +defmodule Mix.Tasks.Oidcc.Gen.ProviderConfigurationWorkerTest do + use ExUnit.Case, async: true + import Igniter.Test + + test "adds configuration if the file doesn't exist yet" do + test_project() + |> Igniter.compose_task("oidcc.gen.provider_configuration_worker", [ + "--name", + "Test.Provider", + "--issuer", + "https://accounts.google.com" + ]) + |> assert_creates("config/runtime.exs", """ + import Config + + config :test, Test.Provider, + issuer: System.get_env("TEST_PROVIDER_ISSUER", "https://accounts.google.com") + """) + end + + test "patches configuration if the file exists" do + test_project( + files: %{ + "config/runtime.exs" => """ + import Config + + config :logger, level: :info + """ + } + ) + |> Igniter.compose_task("oidcc.gen.provider_configuration_worker", []) + |> assert_has_patch("config/runtime.exs", """ + 1 1 |import Config + 2 2 | + 3 + |config :test, Test.OpenIDProvider, issuer: System.fetch_env!("TEST_OPEN_ID_PROVIDER_ISSUER") + 3 4 |config :logger, level: :info + 4 5 | + """) + end + + test "adds worker to application supervision tree" do + test_project() + |> Igniter.compose_task("oidcc.gen.provider_configuration_worker", ["--name", "Test.Provider"]) + |> assert_creates("lib/test/application.ex", """ + defmodule Test.Application do + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + {Oidcc.ProviderConfiguration.Worker, + %{name: Test.Provider, issuer: Application.fetch_env!(:test, Test.Provider)[:issuer]}} + ] + + opts = [strategy: :one_for_one, name: Test.Supervisor] + Supervisor.start_link(children, opts) + end + end + """) + end + + test "keeps existing worker in application supervision tree" do + test_project( + files: %{ + "config/runtime.exs" => """ + import Config + config :test, Test.Provider, issuer: System.fetch_env!("TEST_PROVIDER_ISSUER") + """, + "lib/test/application.ex" => """ + defmodule Test.Application do + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + {Oidcc.ProviderConfiguration.Worker, + %{name: Test.Provider, issuer: Application.fetch_env!(:test, Test.Provider)[:issuer]}} + ] + + opts = [strategy: :one_for_one, name: Test.Supervisor] + Supervisor.start_link(children, opts) + end + end + """ + } + ) + |> Igniter.compose_task("oidcc.gen.provider_configuration_worker", ["--name", "Test.Provider"]) + |> assert_unchanged(["config/runtime.exs", "lib/test/application.ex"]) + end +end