defmodule Mix.Tasks.Saaskit.Gen.Resource do @shortdoc "Generates a resource with functions around an Ecto schema" use Mix.Task import SaasKit.MixUtils @name "saaskit.gen.resource" @doc false # ["Ninjas", "Ninja", "ninjas", "name", "skill:integer", # "user_id:references:users", "--web", "Admin", "--i"] def run(args) do refute_umbrella(@name) Application.ensure_all_started(:hackney) args |> account_scoped_question() |> build_resource_from_args(__MODULE__) |> additional_questions() |> load_templates(@name) |> follow_instructions() end def raise_with_help(msg) do Mix.raise """ #{msg} mix saaskit.gen.resource expect a context module name, followed by singular and plural names of the generated resource, ending with any number of attributes. For example: mix saaskit.gen.resouerce Accounts User users name:string The context serves as the API boundary for the given resource. Multiple resources may belong to a context and a resource may be split over distinct contexts (such as Accounts.User and Payments.User). """ end # %{ # account_scoped: false, # app_name: "Racer", # app_name_lower: "racer", # associations: [ # %{ # foreign_key: :ninja_level_id, # schema_module: "Racer.Ninjas.NinjaLevel", # singular: :ninja_level, # table_name: :ninja_levels # } # ], # binary_id: true, # context: true, # context_name: "Ninjas", # embedded: false, # fields: "name:string skill:integer", # indexes: ["create index(:ninjas, [:ninja_level_id])"], # migration: true, # route_helper: "ninja", # schema: true, # schema_name: "Ninja", # table_name: "ninjas", # types: %{name: :string, skill: :integer}, # web_namespace: nil # } def additional_questions(argument_map) do argument_map |> ask_about_associations() |> ask_about_live(true) |> ask_about_admin(Application.get_env(:saas_kit, :admin) == true) end def ask_about_live(argument_map, false), do: argument_map def ask_about_live(argument_map, true) do Mix.shell().info """ =============================================================== === Live CRUD Interface ======================================= =============================================================== Do you want to add an LiveView interface with the CRUD actions """ if Mix.shell().yes?( """ #{IO.ANSI.green}Do you want to add an LiveView interface?#{IO.ANSI.reset} """ |> String.trim_trailing()) do argument_map |> Map.put(:live_view, true) else argument_map |> Map.put(:live_view, false) end end def ask_about_admin(argument_map, false), do: argument_map def ask_about_admin(%{web_namespace: "Admin"} = argument_map, true), do: Map.put(argument_map, :admin, true) def ask_about_admin(argument_map, true) do Mix.shell().info """ =============================================================== === Admin Interface =========================================== =============================================================== Do you want to add an admin interface with the CRUD actions """ if Mix.shell().yes?( """ #{IO.ANSI.green}Do you want to add an admin interface?#{IO.ANSI.reset} """ |> String.trim_trailing()) do argument_map |> Map.put(:admin, true) |> Map.put_new(:web_namespace, "Admin") else argument_map end end def ask_about_associations(%{associations: []} = argument_map), do: argument_map def ask_about_associations(%{associations: [_|_] = associations} = argument_map) do associations = Enum.map(associations, &ask_about_association(&1)) Map.put(argument_map, :associations, associations) end defp ask_about_association(%{schema_module: schema_module, table_name: table_name} = association) do Mix.shell().info """ =============================================================== === Associations ============================================== =============================================================== The resource seems to belong to another schema. """ if Mix.shell().yes?( """ Is this the correct context and schema? #{IO.ANSI.green}#{schema_module}#{IO.ANSI.reset} """ |> String.trim_trailing()) do association else [app_name, _, schema_name] = schema_module |> String.split(".") context_name = Macro.camelize "#{table_name}" maybe_schema_module = Enum.join([app_name, context_name, schema_name], ".") if Mix.shell().yes?( """ Ok, is this then the correct context and schema? #{IO.ANSI.green}#{maybe_schema_module}#{IO.ANSI.reset} """ |> String.trim_trailing()) do Map.put(association, :schema_module, maybe_schema_module) else new_schema_module = question_with_fallback( """ Add the new schema name (ex: MyApp.Todos.Todo): """, :schema_module) Map.put(association, :schema_module, new_schema_module) end end end def question_with_fallback(question, rule, valid \\ true) do if valid == false do Mix.shell().error """ You entered an invalid value, try to enter again. """ end response = Mix.shell().prompt(question) response = String.trim("#{response}") if response |> String.split(".") |> length() == 3 do String.trim(response) else question_with_fallback(question, rule, false) end end end