├── test
├── test_helper.exs
├── elixir_scribe
│ ├── utils
│ │ ├── string
│ │ │ ├── find_acronyms
│ │ │ │ └── find_acronyms_test.exs
│ │ │ ├── camel_case_to_sentence
│ │ │ │ └── camel_case_to_sentence_test.exs
│ │ │ ├── human_capitalize
│ │ │ │ └── human_capitalize_string_test.exs
│ │ │ ├── capitalize
│ │ │ │ └── capitalize_string_test.exs
│ │ │ └── first_word
│ │ │ │ └── first_word_test.exs
│ │ └── string_api_test.exs
│ ├── generator
│ │ ├── domain
│ │ │ └── resource
│ │ │ │ ├── build_api_file_paths
│ │ │ │ └── build_api_file_paths_resource_test.exs
│ │ │ │ ├── generate_schema
│ │ │ │ └── generate_schema_resource_test.exs
│ │ │ │ ├── build_test_action_files_paths
│ │ │ │ └── build_test_action_files_paths_resource_test.exs
│ │ │ │ ├── generate_actions
│ │ │ │ └── generate_actions_resource_test.exs
│ │ │ │ ├── generate_tests
│ │ │ │ └── generate_tests_resource_test.exs
│ │ │ │ ├── build_action_files_paths
│ │ │ │ └── build_action_files_paths_resource_test.exs
│ │ │ │ ├── build_files_to_generate
│ │ │ │ └── build_files_to_generate_resource_test.exs
│ │ │ │ └── generate_test_fixture
│ │ │ │ └── generate_test_fixture_resource_test.exs
│ │ ├── domain_resource_api_sync_test.exs
│ │ ├── schema_resource_api_test.exs
│ │ ├── domain_contract_test.exs
│ │ └── domain_resource_api_async_test.exs
│ ├── template
│ │ ├── module
│ │ │ ├── build_name
│ │ │ │ ├── build_module_action_name_test.exs
│ │ │ │ ├── build_absolute_module_action_name_test.exs
│ │ │ │ ├── build_absolute_module_action_name_aliases_test.exs
│ │ │ │ └── build_absolute_module_name_test.exs
│ │ │ └── build_embed_templates
│ │ │ │ └── build_module_embed_templates_test.exs
│ │ ├── file
│ │ │ ├── build_dir_path_for_html
│ │ │ │ └── build_dir_path_for_html_file_test.exs
│ │ │ ├── build_filename_for_action
│ │ │ │ └── build_filename_for_action_file_test.exs
│ │ │ └── inject
│ │ │ │ ├── inject_content_before_module_end_test.exs
│ │ │ │ └── inject_eex_template_before_module_end_test.exs
│ │ ├── route
│ │ │ └── scope
│ │ │ │ └── scope_action_routes_test.exs
│ │ ├── binding
│ │ │ ├── rebuild
│ │ │ │ └── rebuild_binding_template_test.exs
│ │ │ └── build
│ │ │ │ └── build_binding_template_test.exs
│ │ ├── binding_api_test.exs
│ │ ├── file_api_async_test.exs
│ │ ├── file_api_sync_test.exs
│ │ └── module_api_test.exs
│ ├── mix
│ │ ├── mix_api_test.exs
│ │ └── shell
│ │ │ └── prompt_for_file_conflicts
│ │ │ └── prompt_for_file_conflicts_shell_test.exs
│ └── behaviour_typed_contract_test.exs
├── support
│ ├── base_case.ex
│ └── fixtures
│ │ └── domain_generator_fixtures.ex
├── mix
│ └── tasks
│ │ ├── scribe.gen.html_async_test.exs
│ │ ├── scribe.gen.domain_async_test.exs
│ │ ├── scribe.gen_test.exs
│ │ ├── scribe_test.exs
│ │ └── scribe.gen.domain_sync_test.exs
├── docs
│ └── modules
│ │ └── behaviours
│ │ └── persona_validator_test.exs
├── mix_test_helper.exs
└── elixir_scribe_test.exs
├── .github
└── FUNDING.yml
├── .formatter.exs
├── priv
└── templates
│ ├── scribe.gen.domain
│ ├── actions
│ │ ├── action_module.ex
│ │ ├── action_module_no_schema_access.ex
│ │ ├── no_schema_access
│ │ │ └── any_action.ex
│ │ └── schema_access
│ │ │ ├── new_schema.ex
│ │ │ ├── list_schema.ex
│ │ │ ├── default_schema.ex
│ │ │ ├── read_schema.ex
│ │ │ ├── create_schema.ex
│ │ │ ├── delete_schema.ex
│ │ │ ├── edit_schema.ex
│ │ │ └── update_schema.ex
│ ├── apis
│ │ ├── default_api_function.ex
│ │ ├── api_module.ex
│ │ ├── api_function_no_schema_access.ex
│ │ ├── list_api_function.ex
│ │ ├── new_api_function.ex
│ │ ├── create_api_function.ex
│ │ ├── edit_api_function.ex
│ │ ├── delete_api_function.ex
│ │ ├── read_api_function.ex
│ │ └── update_api_function.ex
│ └── tests
│ │ ├── actions
│ │ ├── action_module_test_no_schema_access.exs
│ │ ├── action_module_test.exs
│ │ ├── schema_access
│ │ │ ├── default_schema_test.exs
│ │ │ ├── new_schema_test.exs
│ │ │ ├── list_schema_test.exs
│ │ │ ├── edit_schema_test.exs
│ │ │ ├── read_schema_test.exs
│ │ │ ├── delete_schema_test.exs
│ │ │ ├── create_schema_test.exs
│ │ │ └── update_schema_test.exs
│ │ └── no_schema_access
│ │ │ └── module_any_action_test.exs
│ │ ├── fixtures_module.ex
│ │ └── fixtures.ex
│ └── scribe.gen.html
│ ├── tests
│ └── controllers
│ │ ├── default_controller_test.exs
│ │ ├── new_controller_test.exs
│ │ ├── list_controller_test.exs
│ │ ├── read_controller_test.exs
│ │ ├── edit_controller_test.exs
│ │ ├── delete_controller_test.exs
│ │ ├── create_controller_test.exs
│ │ └── update_controller_test.exs
│ ├── html
│ └── default
│ │ ├── resource_form.html.heex
│ │ ├── new.html.heex
│ │ ├── edit.html.heex
│ │ ├── html.ex
│ │ ├── read.html.heex
│ │ └── list.html.heex
│ └── controllers
│ ├── default_controller.ex
│ ├── new_controller.ex
│ ├── edit_controller.ex
│ ├── list_controller.ex
│ ├── read_controller.ex
│ ├── delete_controller.ex
│ ├── create_controller.ex
│ └── update_controller.ex
├── .editorconfig
├── lib
├── elixir_scribe
│ ├── template
│ │ ├── route_api.ex
│ │ ├── module
│ │ │ ├── build_embed_templates
│ │ │ │ └── build_module_embed_templates.ex
│ │ │ └── build_name
│ │ │ │ ├── build_module_action_name.ex
│ │ │ │ ├── build_absolute_module_action_name_aliases.ex
│ │ │ │ ├── build_absolute_module_name.ex
│ │ │ │ └── build_absolute_module_action_name.ex
│ │ ├── file
│ │ │ ├── build_dir_path_for_html
│ │ │ │ └── build_dir_path_for_html_file.ex
│ │ │ ├── inject
│ │ │ │ ├── inject_eex_template_before_module.ex
│ │ │ │ └── inject_content_before_module_end.ex
│ │ │ └── build_filename_for_action
│ │ │ │ └── build_filename_for_action_file.ex
│ │ ├── binding_api.ex
│ │ ├── binding_api_contract_build_filename_for_action_file.ex
│ │ ├── binding
│ │ │ ├── rebuild
│ │ │ │ └── rebuild_binding_template.ex
│ │ │ └── build
│ │ │ │ └── build_binding_template.ex
│ │ ├── module_api.ex
│ │ ├── file_api.ex
│ │ └── route
│ │ │ └── scope
│ │ │ └── scope_action_routes.ex
│ ├── utils
│ │ ├── string
│ │ │ ├── first_word
│ │ │ │ └── first_word.ex
│ │ │ ├── human_capitalize
│ │ │ │ └── human_capitalize_string.ex
│ │ │ ├── capitalize
│ │ │ │ └── capitalize_string.ex
│ │ │ ├── find_acronyms
│ │ │ │ └── find_acronyms.ex
│ │ │ └── camel_case_to_sentence
│ │ │ │ └── camel_case_to_sentence.ex
│ │ └── string_api.ex
│ ├── generator
│ │ ├── domain
│ │ │ └── resource
│ │ │ │ ├── build_api_file_paths
│ │ │ │ └── build_api_file_paths_resource.ex
│ │ │ │ ├── generate_schema
│ │ │ │ └── generate_schema_resource.ex
│ │ │ │ ├── generate_new_files
│ │ │ │ └── generate_new_files_resource.ex
│ │ │ │ ├── build_files_to_generate
│ │ │ │ └── build_files_to_generate_resource.ex
│ │ │ │ ├── build_action_files_paths
│ │ │ │ └── build_action_files_paths_resource.ex
│ │ │ │ ├── build_test_action_files_paths
│ │ │ │ └── build_test_action_files_paths_resource.ex
│ │ │ │ ├── generate_actions
│ │ │ │ └── generate_actions_resource.ex
│ │ │ │ ├── generate_tests
│ │ │ │ └── generate_tests_resource.ex
│ │ │ │ ├── generate_api
│ │ │ │ └── generate_api_resource.ex
│ │ │ │ └── generate_test_fixture
│ │ │ │ └── generate_test_fixture_resource.ex
│ │ ├── schema_resource_api.ex
│ │ ├── domain_contract.ex
│ │ ├── schema_contract.ex
│ │ ├── domain_resource_api.ex
│ │ └── schema
│ │ │ └── resource
│ │ │ └── schema_resource_helpers.ex
│ └── mix
│ │ ├── cli_command
│ │ ├── parse_schema
│ │ │ └── parse_schema_cli_command.ex
│ │ └── parse
│ │ │ └── parse_cli_command.ex
│ │ ├── mix_api.ex
│ │ └── shell
│ │ └── prompt_for_file_conflicts
│ │ └── prompt_for_file_conflicts_shell.ex
├── mix
│ └── tasks
│ │ ├── scribe.gen.ex
│ │ └── scribe.ex
└── docs
│ └── modules
│ └── behaviours
│ ├── persona.ex
│ └── persona_validator.ex
├── .gitignore
├── LICENSE.md
├── assets
└── svg
│ └── github-sponsor.svg
├── mix.exs
└── examples
└── scribe.sh
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: ["Exadra37"]
2 |
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/actions/action_module.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %> do
2 | @moduledoc false
3 |
4 | end
5 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/apis/default_api_function.ex:
--------------------------------------------------------------------------------
1 |
2 | def <%= action %>(), do: <%= module_action_name %>.<%= action_first_word %>()
3 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/actions/action_module_no_schema_access.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %> do
2 | @moduledoc false
3 | end
4 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/actions/action_module_test_no_schema_access.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %> do
2 | @moduledoc false
3 | end
4 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/actions/action_module_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>Test do
2 | use <%= inspect contract.base_module %>.DataCase
3 |
4 | end
5 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/actions/no_schema_access/any_action.ex:
--------------------------------------------------------------------------------
1 |
2 | def <%= action_first_word %>() do
3 | raise "TODO: Implement the action `<%= action_first_word %>` for the module `<%= module_action_name %>`"
4 | end
5 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/apis/api_module.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect absolute_module_name %>API do
2 | @moduledoc """
3 | The <%= inspect contract.name %> <%= contract.schema.human_singular %> API.
4 | """
5 | <%= aliases %>
6 | end
7 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/apis/api_function_no_schema_access.ex:
--------------------------------------------------------------------------------
1 |
2 | @doc """
3 | <%= contract.schema.human_singular %>: <%= action_human_capitalized %>.
4 | """
5 | def <%= action %>(), do: <%= module_action_name %>.<%= action_first_word %>()
6 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/fixtures_module.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect contract.schema.module %>Fixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `<%= inspect contract.module %>` contract.
5 | """
6 | end
7 |
--------------------------------------------------------------------------------
/test/elixir_scribe/utils/string/find_acronyms/find_acronyms_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.String.FindAcronymsTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias ElixirScribe.Utils.String.FindAcronyms
5 |
6 | doctest ElixirScribe.Utils.String.FindAcronyms
7 | end
8 |
--------------------------------------------------------------------------------
/test/support/base_case.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.BaseCase do
2 | @moduledoc false
3 |
4 | use ExUnit.CaseTemplate
5 |
6 | using do
7 | quote do
8 | import ElixirScribe.Generator.DomainFixtures
9 | import Assertions
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/actions/schema_access/new_schema.ex:
--------------------------------------------------------------------------------
1 | alias <%= inspect contract.schema.module %>
2 |
3 | def <%= action_first_word %>(attrs \\ %{}) when is_map(attrs) do
4 | <%= inspect contract.schema.alias %>.changeset(%<%= inspect contract.schema.alias %>{}, attrs)
5 | end
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{md, markdown}]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/actions/schema_access/list_schema.ex:
--------------------------------------------------------------------------------
1 | alias <%= inspect contract.schema.repo %><%= contract.schema.repo_alias %>
2 | alias <%= inspect contract.schema.module %>
3 |
4 | def <%= action_first_word %>() do
5 | Repo.all(<%= inspect contract.schema.alias %>)
6 | end
7 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/actions/schema_access/default_schema.ex:
--------------------------------------------------------------------------------
1 | # alias <%= inspect contract.schema.repo %><%= contract.schema.repo_alias %>
2 | # alias <%= inspect contract.schema.module %>
3 |
4 | def <%= action_first_word %>() do
5 | raise "TOOO: Implement business logic for #{__MODULE__}"
6 | end
7 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/actions/schema_access/read_schema.ex:
--------------------------------------------------------------------------------
1 | alias <%= inspect contract.schema.repo %><%= contract.schema.repo_alias %>
2 | alias <%= inspect contract.schema.module %>
3 |
4 | def <%= action_first_word %>!(uuid) when is_binary(uuid), do: Repo.get!(<%= inspect contract.schema.alias %>, uuid)
5 |
--------------------------------------------------------------------------------
/test/elixir_scribe/utils/string/camel_case_to_sentence/camel_case_to_sentence_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.String.CamelCaseToSentenceTest do
2 | use ExUnit.Case, async: true
3 |
4 | alias ElixirScribe.Utils.String.CamelCaseToSentence
5 |
6 | doctest ElixirScribe.Utils.String.CamelCaseToSentence
7 | end
8 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/route_api.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.RouteAPI do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Generator.DomainContract
5 | alias ElixirScribe.Template.Route.Scope.ScopeActionRoutes
6 |
7 | def scope_routes(%DomainContract{} = contract), do: ScopeActionRoutes.scope(contract)
8 | end
9 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/tests/controllers/default_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>ControllerTest do
2 | use <%= inspect contract.web_module %>.ConnCase
3 |
4 | test "<%= action %> <%= contract.schema.singular %>", %{conn: _conn} do
5 | raise "Test not implemented yet..."
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/actions/schema_access/default_schema_test.exs:
--------------------------------------------------------------------------------
1 | # alias <%= module_action_name %>
2 | # alias <%= inspect contract.schema.module %>
3 |
4 | # import <%= inspect contract.schema.module %>Fixtures
5 |
6 | test "<%= action_first_word %>" do
7 | raise "TOOO: Implement test for #{__MODULE__}"
8 | end
9 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/actions/schema_access/new_schema_test.exs:
--------------------------------------------------------------------------------
1 | alias <%= absolute_module_action_name %>
2 |
3 | test "<%= action_first_word %>/0 returns a <%= contract.schema.singular %> changeset" do
4 | assert %Ecto.Changeset{} = <%= action_capitalized %><%= inspect(contract.schema.alias) %>.<%= action_first_word %>()
5 | end
6 |
--------------------------------------------------------------------------------
/test/mix/tasks/scribe.gen.html_async_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Scribe.Gen.HtmlAsyncTest do
2 | use ElixirScribe.BaseCase, async: true
3 |
4 | test "It raises when no arguments are provided" do
5 | assert_raise Mix.Error, ~r/No arguments were provided.*$/s, fn ->
6 | Mix.Tasks.Scribe.Gen.Html.run([])
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/mix/tasks/scribe.gen.domain_async_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Scribe.Gen.DomainAsyncTest do
2 | use ElixirScribe.BaseCase, async: true
3 |
4 | test "It raises when no arguments are provided" do
5 | assert_raise Mix.Error, ~r/No arguments were provided.*$/s, fn ->
6 | Mix.Tasks.Scribe.Gen.Domain.run([])
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/utils/string/first_word/first_word.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.String.FirstWord.FirstWordString do
2 | @moduledoc false
3 |
4 | def first(string, word_separators \\ ["_", "-", " "])
5 | when is_binary(string) and is_list(word_separators) do
6 | string
7 | |> String.split(word_separators)
8 | |> List.first()
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/actions/schema_access/create_schema.ex:
--------------------------------------------------------------------------------
1 | alias <%= inspect contract.schema.repo %><%= contract.schema.repo_alias %>
2 | alias <%= inspect contract.schema.module %>API
3 |
4 | def <%= action_first_word %>(%{} = attrs) when attrs !== %{} do
5 | attrs
6 | |> <%= inspect contract.schema.alias %>API.new()
7 | |> Repo.insert()
8 | end
9 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/utils/string/human_capitalize/human_capitalize_string.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.String.HumanCapitalize.HumanCapitalizeString do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Utils.StringAPI
5 |
6 | def capitalize(string) when is_binary(string) do
7 | joiner = " "
8 |
9 | string
10 | |> StringAPI.capitalize(joiner)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/actions/schema_access/delete_schema.ex:
--------------------------------------------------------------------------------
1 | alias <%= inspect contract.schema.repo %><%= contract.schema.repo_alias %>
2 | alias <%= inspect contract.schema.module %>API
3 |
4 | def <%= action_first_word %>(uuid) when is_binary(uuid) do
5 | uuid
6 | |> <%= inspect(contract.schema.alias) %>API.<%= read_action %>!()
7 | |> Repo.delete()
8 | end
9 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/apis/list_api_function.ex:
--------------------------------------------------------------------------------
1 |
2 | @doc """
3 | Returns the list of <%= contract.schema.plural %>.
4 |
5 | ## Examples
6 |
7 | iex> <%=inspect contract.schema.alias %>API.<%= action %>()
8 | [%<%= inspect contract.schema.alias %>{}, ...]
9 |
10 | """
11 | def <%= action %>(), do: <%= module_action_name %>.<%= action_first_word %>()
12 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/module/build_embed_templates/build_module_embed_templates.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Options.BuildEmbedTemplates.BuildModuleEmbedTemplates do
2 | @moduledoc false
3 |
4 | def build() do
5 | for action <- ElixirScribe.resource_html_actions(), reduce: "" do
6 | embeds -> embeds <> "\n embed_templates \"#{action}/*\""
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/actions/schema_access/edit_schema.ex:
--------------------------------------------------------------------------------
1 | alias <%= inspect contract.schema.module %>
2 | alias <%= inspect contract.schema.module %>API
3 |
4 | def <%= action_first_word %>(uuid, attrs \\ %{}) when is_binary(uuid) and is_map(attrs) do
5 | uuid
6 | |> <%= inspect contract.schema.alias %>API.<%= read_action %>!()
7 | |> <%= inspect contract.schema.alias %>.changeset(attrs)
8 | end
9 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/actions/no_schema_access/module_any_action_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>Test do
2 | use <%= inspect contract.base_module %>.DataCase
3 |
4 | alias <%= module_action_name %>
5 |
6 | test "<%= action_first_word %> works as expected" do
7 | raise "TODO: Implement test for action `<%= action_first_word %>` at `<%= module_action_name %>`"
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/html/default/resource_form.html.heex:
--------------------------------------------------------------------------------
1 | <.simple_form :let={f} for={@changeset} action={@action}>
2 | <.error :if={@changeset.action}>
3 | Oops, something went wrong! Please check the errors below.
4 |
5 | <%= Mix.Tasks.Phx.Gen.Html.indent_inputs(inputs, 2) %>
6 | <:actions>
7 | <.button>Save <%= contract.schema.human_singular %>
8 |
9 |
10 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/utils/string/capitalize/capitalize_string.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.String.Capitalize.CapitalizeString do
2 | @moduledoc false
3 |
4 | def capitalize(string, joiner \\ "") when is_binary(string) and is_binary(joiner) do
5 | string
6 | |> String.split(["_", "-", " "], trim: true)
7 | |> Enum.map(fn part -> part |> String.trim() |> String.capitalize() end)
8 | |> Enum.join(joiner)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/file/build_dir_path_for_html/build_dir_path_for_html_file.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.File.BuildPathForHtml.BuildPathForHtmlFile do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Generator.DomainContract
5 |
6 | def build(%DomainContract{} = contract) do
7 | template = Keyword.get(contract.opts, :html_template, "default")
8 |
9 | ElixirScribe.html_template_path()
10 | |> Path.join(template)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/html/default/new.html.heex:
--------------------------------------------------------------------------------
1 | <.header>
2 | New <%= contract.schema.human_singular %>
3 | <:subtitle>Use this form to manage <%= contract.schema.singular %> records in your database.
4 |
5 |
6 | <.<%= contract.schema.singular %>_form changeset={@changeset} action={~p"<%= contract.schema.route_prefix %>"} />
7 |
8 | <.back navigate={~p"<%= contract.schema.route_prefix %>"}>Back to <%= contract.schema.plural %>
9 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/tests/controllers/new_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>ControllerTest do
2 | use <%= inspect contract.web_module %>.ConnCase
3 |
4 | test "<%= action %> <%= contract.schema.singular %> renders form", %{conn: conn} do
5 | conn = get(conn, ~p"<%= contract.schema.route_prefix %>/<%= action %>")
6 | assert html_response(conn, 200) =~ "<%= action_capitalized %> <%= contract.schema.human_singular %>"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/mix/tasks/scribe.gen_test.exs:
--------------------------------------------------------------------------------
1 | # Get Mix output sent to the current
2 | # process to avoid polluting tests.
3 | Mix.shell(Mix.Shell.Process)
4 |
5 | defmodule Mix.Tasks.Scribe.GenTest do
6 | use ElixirScribe.BaseCase
7 |
8 | test "It list all Elixir Scribe generators" do
9 | Mix.Tasks.Scribe.Gen.run([])
10 | assert_received {:mix_shell, :info, ["mix scribe.gen.html" <> _]}
11 | assert_received {:mix_shell, :info, ["mix scribe.gen.domain" <> _]}
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/actions/schema_access/list_schema_test.exs:
--------------------------------------------------------------------------------
1 | alias <%= absolute_module_action_name %>
2 |
3 | import <%= inspect contract.schema.module %>Fixtures
4 |
5 | test "<%= action_first_word %>/0 returns all <%= contract.schema.plural %>" do
6 | <%= contract.schema.singular %> = <%= contract.schema.singular %>_fixture()
7 | assert <%= action_capitalized <> inspect(contract.schema.alias_plural) <> "." <> action_first_word %>() == [<%= contract.schema.singular %>]
8 | end
9 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/controllers/default_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>Controller do
2 | use <%= inspect contract.web_module %>, :controller
3 |
4 | # alias <%= inspect contract.schema.module %>API
5 |
6 | plug :put_view, html: <%= inspect contract.web_module %>.<%= contract.schema.web_namespace %>.<%= inspect contract.schema.alias %>HTML
7 |
8 | def <%= action %>(_conn, _params) do
9 | raise "TOOO: Implement controller logic for #{__MODULE__}"
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/apis/new_api_function.ex:
--------------------------------------------------------------------------------
1 |
2 | @doc """
3 | Returns an `%Ecto.Changeset{}` with a default `%<%= inspect contract.schema.alias %>{}` for tracking <%= contract.schema.singular %> changes.
4 |
5 | ## Examples
6 |
7 | iex> <%= inspect contract.schema.alias %>API.<%= action %>()
8 | %Ecto.Changeset{data: %<%= inspect contract.schema.alias %>{}}
9 |
10 | """
11 | def <%= action %>(attrs \\ %{}) when is_map(attrs), do: <%= module_action_name %>.<%= action_first_word %>(attrs)
12 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/actions/schema_access/edit_schema_test.exs:
--------------------------------------------------------------------------------
1 | alias <%= absolute_module_action_name %>
2 |
3 | import <%= inspect contract.schema.module %>Fixtures
4 |
5 | test "<%= action_first_word %>/1 returns a <%= contract.schema.singular %> changeset" do
6 | <%= contract.schema.singular %> = <%= contract.schema.singular %>_fixture()
7 | assert %Ecto.Changeset{} = <%= action_capitalized %><%= inspect(contract.schema.alias) %>.<%= action_first_word %>(<%= contract.schema.singular %>.id)
8 | end
9 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/actions/schema_access/update_schema.ex:
--------------------------------------------------------------------------------
1 | alias <%= inspect contract.schema.repo %><%= contract.schema.repo_alias %>
2 | alias <%= inspect contract.schema.module %>
3 | alias <%= inspect contract.schema.module %>API
4 |
5 | def <%= action_first_word %>(uuid, %{} = attrs) when is_binary(uuid) and is_map(attrs) do
6 | uuid
7 | |> <%= inspect(contract.schema.alias) %>API.<%= read_action %>!()
8 | |> <%= inspect(contract.schema.alias) %>.changeset(attrs)
9 | |> Repo.update()
10 | end
11 |
--------------------------------------------------------------------------------
/lib/mix/tasks/scribe.gen.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Scribe.Gen do
2 | # This module was borrowed from the Phoenix Framework module
3 | # Mix.Tasks.Phx.Gen and modified to suite ElixirScribe needs.
4 |
5 | use Mix.Task
6 |
7 | @shortdoc "Lists all available Scribe generators"
8 |
9 | @moduledoc """
10 | Lists all available Scribe generators:
11 |
12 | ```console
13 | mix scribe.gen
14 | ```
15 | """
16 |
17 | @doc false
18 | def run(_args) do
19 | Mix.Task.run("help", ["--search", "scribe.gen."])
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/html/default/edit.html.heex:
--------------------------------------------------------------------------------
1 | <.header>
2 | Edit <%= contract.schema.human_singular %> <%%= @<%= contract.schema.singular %>.id %>
3 | <:subtitle>Use this form to manage <%= contract.schema.singular %> records in your database.
4 |
5 |
6 | <.<%= contract.schema.singular %>_form changeset={@changeset} action={~p"<%= contract.schema.route_prefix %>/#{@<%= contract.schema.singular %>}"} />
7 |
8 | <.back navigate={~p"<%= contract.schema.route_prefix %>"}>Back to <%= contract.schema.plural %>
9 |
--------------------------------------------------------------------------------
/lib/docs/modules/behaviours/persona.ex:
--------------------------------------------------------------------------------
1 | defmodule Persona do
2 | @moduledoc false
3 |
4 | require PersonaValidator
5 |
6 | @fields %{
7 | required: [:name, :email],
8 | optional: [role: nil]
9 | }
10 |
11 | use ElixirScribe.Behaviour.TypedContract, fields: @fields
12 |
13 | @impl true
14 | def type_spec() do
15 | schema(%__MODULE__{
16 | name: is_binary() |> spec(),
17 | email: PersonaValidator.corporate_email?() |> spec(),
18 | role: PersonaValidator.role?() |> spec()
19 | })
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/actions/schema_access/read_schema_test.exs:
--------------------------------------------------------------------------------
1 | alias <%= absolute_module_action_name %>
2 |
3 | import <%= inspect contract.schema.module %>Fixtures
4 |
5 | test "<%= action_first_word %>!/1 returns the <%= contract.schema.singular %> with given id" do
6 | <%= contract.schema.singular %> = <%= contract.schema.singular %>_fixture()
7 | assert <%= action_capitalized %><%= inspect(contract.schema.alias) <> "." <> action_first_word %>!(<%= contract.schema.singular %>.id) == <%= contract.schema.singular %>
8 | end
9 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/binding_api.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.BindingAPI do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Generator.DomainContract
5 | alias ElixirScribe.Template.Binding.Build.BuildBindingTemplate
6 | alias ElixirScribe.Template.Binding.Rebuild.RebuildBindingTemplate
7 |
8 | def build_binding_template(%DomainContract{} = contract),
9 | do: BuildBindingTemplate.build(contract)
10 |
11 | def rebuild_binding_template(binding, action, opts),
12 | do: RebuildBindingTemplate.rebuild(binding, action, opts)
13 | end
14 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/apis/create_api_function.ex:
--------------------------------------------------------------------------------
1 |
2 | @doc """
3 | Creates a <%= contract.schema.singular %> from the given `attrs`.
4 |
5 | ## Examples
6 |
7 | iex> <%= inspect contract.schema.alias %>API.<%= action %>(%{field: value})
8 | {:ok, %<%= inspect contract.schema.alias %>{}}
9 |
10 | iex> <%= inspect contract.schema.alias %>API.<%= action %>(%{field: bad_value})
11 | {:error, %Ecto.Changeset{}}
12 |
13 | """
14 | def <%= action %>(attrs) when is_map(attrs), do: <%= module_action_name %>.<%= action_first_word %>(attrs)
15 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/html/default/html.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= inspect contract.web_module %>.<%= inspect Module.concat(contract.schema.web_namespace, contract.schema.alias) %>HTML do
2 | use <%= inspect contract.web_module %>, :html
3 | <%= embeded_templates %>
4 | embed_templates "<%= contract.schema.singular %>_form.html"
5 |
6 | @doc """
7 | Renders a <%= contract.schema.singular %> form.
8 | """
9 | attr :changeset, Ecto.Changeset, required: true
10 | attr :action, :string, required: true
11 |
12 | def <%= contract.schema.singular %>_form(assigns)
13 | end
14 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain/resource/build_api_file_paths/build_api_file_paths_resource.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.BuildAPIFilePaths.BuildAPIFilePathsResource do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Generator.DomainContract
5 |
6 | def build(%DomainContract{} = contract), do: build_api_file(contract)
7 |
8 | defp build_api_file(contract) do
9 | source_path = ElixirScribe.domain_api_template_path() |> Path.join("api_module.ex")
10 | target_path = contract.api_file
11 |
12 | {:eex, :api, source_path, target_path}
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/file/inject/inject_eex_template_before_module.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.File.Inject.InjectEExTemplateBeforeModuleEnd do
2 | @moduledoc false
3 | alias ElixirScribe.Template.FileAPI
4 |
5 | def inject(base_template_paths, source_path, target_path, binding)
6 | when is_list(base_template_paths) and is_binary(source_path) and is_binary(target_path) and
7 | is_list(binding) do
8 | base_template_paths
9 | |> Mix.Phoenix.eval_from(source_path, binding)
10 | |> FileAPI.inject_content_before_module_end(target_path)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/apis/edit_api_function.ex:
--------------------------------------------------------------------------------
1 |
2 | @doc """
3 | Returns an `%Ecto.Changeset{}` with a `%<%= inspect contract.schema.alias %>{}` for tracking changes for the given <%= contract.schema.singular %> `uuid`.
4 |
5 | ## Examples
6 |
7 | iex> <%= inspect contract.schema.alias %>API.<%= action %>("38cd0012-79dc-4838-acc0-94d4143c4f2c")
8 | %Ecto.Changeset{data: %<%= inspect contract.schema.alias %>{id: uuid}}
9 |
10 | """
11 | def <%= action %>(uuid, attrs \\ %{}) when is_binary(uuid) and is_map(attrs), do: <%= module_action_name %>.<%= action_first_word %>(uuid, attrs)
12 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/controllers/new_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>Controller do
2 | use <%= inspect contract.web_module %>, :controller
3 |
4 | alias <%= inspect contract.schema.module %>API
5 |
6 | plug :put_view, html: <%= inspect contract.web_module %>.<%= contract.schema.web_namespace %>.<%= inspect contract.schema.alias %>HTML
7 |
8 | def <%= action %>(conn, _params) do
9 | changeset = <%= inspect(contract.schema.alias) %>API.<%= action %>()
10 | render(conn, "<%= action %>_<%= contract.schema.singular %>.html", changeset: changeset)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/apis/delete_api_function.ex:
--------------------------------------------------------------------------------
1 |
2 | @doc """
3 | Deletes the <%= contract.schema.singular %> for the given `uuid`.
4 |
5 | ## Examples
6 |
7 | iex> <%= inspect contract.schema.alias %>API.<%= action %>("38cd0012-79dc-4838-acc0-94d4143c4f2c")
8 | {:ok, %<%= inspect contract.schema.alias %>{}}
9 |
10 | iex> <%= inspect contract.schema.alias %>API.<%= action %>("815af9955-19ab-2567-bdd1-49e4143c4g3d")
11 | {:error, %Ecto.Changeset{}}
12 |
13 | """
14 | def <%= action %>(uuid) when is_binary(uuid), do: <%= module_action_name %>.<%= action_first_word %>(uuid)
15 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/module/build_name/build_module_action_name.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Module.BuildName.BuildModuleActionName do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Generator.DomainContract
5 | alias ElixirScribe.Utils.StringAPI
6 |
7 | def build(%DomainContract{} = context, action) when is_binary(action) do
8 | schema =
9 | (action in ElixirScribe.resource_plural_actions() && context.schema.alias_plural) ||
10 | context.schema.alias
11 |
12 | action_capitalized = action |> StringAPI.capitalize()
13 |
14 | "#{action_capitalized}#{inspect(schema)}"
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/binding_api_contract_build_filename_for_action_file.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.BuildFilenameForActionFileContract do
2 | @moduledoc false
3 |
4 | @fields %{
5 | required: [:action, :action_suffix, :file_type, :file_extension],
6 | optional: []
7 | }
8 |
9 | use ElixirScribe.Behaviour.TypedContract, fields: @fields
10 |
11 | @impl true
12 | def type_spec() do
13 | schema(%__MODULE__{
14 | action: is_binary() |> spec(),
15 | action_suffix: is_binary() |> spec(),
16 | file_type: is_binary() |> spec(),
17 | file_extension: is_binary() |> spec()
18 | })
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/support/fixtures/domain_generator_fixtures.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.DomainFixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `ElixirScribe.Generator.Domain` context.
5 | """
6 |
7 | alias ElixirScribe.MixAPI
8 | alias ElixirScribe.Generator.DomainResourceAPI
9 |
10 | @default_args ["Site.Blog", "Post", "posts", "name:string", "desc:string"]
11 | def domain_contract_fixture(args \\ @default_args) do
12 | {valid_args, opts, _invalid_args} = args |> MixAPI.parse_cli_command()
13 |
14 | DomainResourceAPI.build_domain_resource_contract!(valid_args, opts)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/domain/resource/build_api_file_paths/build_api_file_paths_resource_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.BuildAPIFilePaths.BuildAPIFilePathsResourceTest do
2 | alias ElixirScribe.Generator.DomainResourceAPI
3 | use ElixirScribe.BaseCase, async: true
4 |
5 | test "returns the API file paths" do
6 | expected_files =
7 | {:eex, :api, "priv/templates/scribe.gen.domain/apis/api_module.ex",
8 | "lib/elixir_scribe/domain/site/blog/post_api.ex"}
9 |
10 | contract = domain_contract_fixture()
11 |
12 | assert DomainResourceAPI.build_api_file_paths(contract) === expected_files
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/controllers/edit_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>Controller do
2 | use <%= inspect contract.web_module %>, :controller
3 |
4 | alias <%= inspect contract.schema.module %>API
5 |
6 | plug :put_view, html: <%= inspect contract.web_module %>.<%= contract.schema.web_namespace %>.<%= inspect contract.schema.alias %>HTML
7 |
8 | def <%= action %>(conn, %{"id" => id}) do
9 | changeset = <%= inspect(contract.schema.alias) %>API.<%= action %>(id)
10 | render(conn, "<%= action %>_<%= contract.schema.singular %>.html", <%= contract.schema.singular %>: changeset.data, changeset: changeset)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/controllers/list_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>Controller do
2 | use <%= inspect contract.web_module %>, :controller
3 |
4 | alias <%= inspect contract.schema.module %>API
5 |
6 | plug :put_view, html: <%= inspect contract.web_module %>.<%= contract.schema.web_namespace %>.<%= inspect contract.schema.alias %>HTML
7 |
8 | def <%= action %>(conn, _params) do
9 | <%= contract.schema.plural %> = <%= inspect(contract.schema.alias) %>API.<%= action %>()
10 | render(conn, "<%= action %>_<%= contract.schema.plural %>.html", <%= contract.schema.collection %>: <%= contract.schema.plural %>)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/file/build_filename_for_action/build_filename_for_action_file.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.File.BuildFilenameForAction.BuildFilenameForActionFile do
2 | @moduledoc false
3 | alias ElixirScribe.Template.BuildFilenameForActionFileContract
4 |
5 | def build(%BuildFilenameForActionFileContract{} = contract) do
6 | case contract.action in ElixirScribe.resource_actions() do
7 | true ->
8 | "#{contract.action}#{contract.action_suffix}#{contract.file_type}#{contract.file_extension}"
9 |
10 | false ->
11 | "default#{contract.action_suffix}#{contract.file_type}#{contract.file_extension}"
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/controllers/read_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>Controller do
2 | use <%= inspect contract.web_module %>, :controller
3 |
4 | alias <%= inspect contract.schema.module %>API
5 |
6 | plug :put_view, html: <%= inspect contract.web_module %>.<%= contract.schema.web_namespace %>.<%= inspect contract.schema.alias %>HTML
7 |
8 | def <%= action %>(conn, %{"id" => id}) do
9 | <%= contract.schema.singular %> = <%= inspect(contract.schema.alias) %>API.<%= action %>!(id)
10 | render(conn, "<%= action %>_<%= contract.schema.singular %>.html", <%= contract.schema.singular %>: <%= contract.schema.singular %>)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/tests/controllers/list_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>ControllerTest do
2 | use <%= inspect contract.web_module %>.ConnCase
3 |
4 | import <%= inspect contract.schema.module %>Fixtures
5 |
6 | setup do
7 | <%= contract.schema.singular %> = <%= contract.schema.singular %>_fixture()
8 | %{<%= contract.schema.singular %>: <%= contract.schema.singular %>}
9 | end
10 |
11 | test "lists all <%= contract.schema.plural %>", %{conn: conn} do
12 | conn = get(conn, ~p"<%= contract.schema.route_prefix %>")
13 | assert html_response(conn, 200) =~ "Listing <%= contract.schema.human_plural %>"
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/module/build_name/build_module_action_name_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Module.BuildName.BuildModuleActionNameTest do
2 | alias ElixirScribe.Template.ModuleAPI
3 | use ElixirScribe.BaseCase, async: true
4 |
5 | test "it builds the module action name for a singular action" do
6 | contract = domain_contract_fixture()
7 |
8 | assert ModuleAPI.build_module_action_name(contract, "read") === "ReadPost"
9 | end
10 |
11 | test "it builds the module action name for a plural action" do
12 | contract = domain_contract_fixture()
13 |
14 | assert ModuleAPI.build_module_action_name(contract, "list") === "ListPosts"
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/mix/tasks/scribe_test.exs:
--------------------------------------------------------------------------------
1 | # Get Mix output sent to the current
2 | # process to avoid polluting tests.
3 | Mix.shell(Mix.Shell.Process)
4 |
5 | defmodule Mix.Tasks.ScribeTest do
6 | use ElixirScribe.BaseCase
7 |
8 | test "It list all Elixir Scribe tasks" do
9 | Mix.Tasks.Scribe.run([])
10 | assert_received {:mix_shell, :info, ["mix scribe.gen" <> _]}
11 | assert_received {:mix_shell, :info, ["mix scribe.gen.html" <> _]}
12 | assert_received {:mix_shell, :info, ["mix scribe.gen.domain" <> _]}
13 | end
14 |
15 | test "expects no arguments" do
16 | assert_raise Mix.Error, fn ->
17 | Mix.Tasks.Scribe.run(["invalid"])
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/module/build_name/build_absolute_module_action_name_aliases.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Module.BuildName.BuildAbsoluteModuleActionNameAliases do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.ModuleAPI
5 | alias ElixirScribe.Generator.DomainContract
6 |
7 | def build(%DomainContract{} = context, opts) when is_list(opts) do
8 | resource_actions = context.opts |> Keyword.get(:resource_actions)
9 |
10 | for action <- resource_actions, reduce: "" do
11 | aliases ->
12 | aliases <>
13 | "\n alias " <>
14 | ModuleAPI.build_absolute_module_action_name(context, action, opts)
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/module/build_embed_templates/build_module_embed_templates_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Options.BuildEmbedTemplates.BuildModuleEmbedTemplatesTest do
2 | alias ElixirScribe.Template.ModuleAPI
3 | use ElixirScribe.BaseCase, async: true
4 |
5 | test "it builds an build_embeded_templates statement for each HTML resource action" do
6 | expected_embeds =
7 | """
8 |
9 | embed_templates "read/*"
10 | embed_templates "new/*"
11 | embed_templates "edit/*"
12 | embed_templates "list/*"
13 | """
14 | |> String.trim_trailing()
15 |
16 | assert ModuleAPI.build_embeded_templates() == expected_embeds
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/apis/read_api_function.ex:
--------------------------------------------------------------------------------
1 |
2 | @doc """
3 | Gets the `<%= inspect contract.schema.alias %>` for the given `uuid`.
4 |
5 | Raises `Ecto.NoResultsError` if the `<%= inspect contract.schema.alias %>` does not exist.
6 |
7 | ## Examples
8 |
9 | iex> <%= inspect contract.schema.alias %>API.<%= action %>!("38cd0012-79dc-4838-acc0-94d4143c4f2c")
10 | %<%= inspect contract.schema.alias %>{}
11 |
12 | iex> <%= inspect contract.schema.alias %>API.<%= action %>!("815af9955-19ab-2567-bdd1-49e4143c4g3d")
13 | ** (Ecto.NoResultsError)
14 |
15 | """
16 | def <%= action %>!(uuid) when is_binary(uuid), do: <%= module_action_name %>.<%= action_first_word %>!(uuid)
17 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/apis/update_api_function.ex:
--------------------------------------------------------------------------------
1 |
2 | @doc """
3 | Updates the `<%= inspect contract.schema.alias %>` for the given `uuid` with the provided `attrs`.
4 |
5 | ## Examples
6 |
7 | iex> <%= inspect contract.schema.alias %>API.<%= action %>("38cd0012-79dc-4838-acc0-94d4143c4f2c", %{field: new_value})
8 | {:ok, %<%= inspect contract.schema.alias %>{}}
9 |
10 | iex> <%= inspect contract.schema.alias %>API.<%= action %>("38cd0012-79dc-4838-acc0-94d4143c4f2c", %{field: bad_value})
11 | {:error, %Ecto.Changeset{}}
12 |
13 | """
14 | def <%= action %>(uuid, attrs) when is_binary(uuid) and is_map(attrs), do: <%= module_action_name %>.<%= action_first_word %>(uuid, attrs)
15 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/controllers/delete_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>Controller do
2 | use <%= inspect contract.web_module %>, :controller
3 |
4 | alias <%= inspect contract.schema.module %>API
5 |
6 | plug :put_view, html: <%= inspect contract.web_module %>.<%= contract.schema.web_namespace %>.<%= inspect contract.schema.alias %>HTML
7 |
8 | def <%= action %>(conn, %{"id" => id}) do
9 | {:ok, _<%= contract.schema.singular %>} = <%= inspect(contract.schema.alias) %>API.<%= action %>(id)
10 |
11 | conn
12 | |> put_flash(:info, "<%= contract.schema.human_singular %> deleted successfully.")
13 | |> redirect(to: ~p"<%= contract.schema.route_prefix %>")
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | elixir_scribe-*.tar
24 |
25 | # Temporary files, for example, from tests.
26 | /tmp/
27 |
--------------------------------------------------------------------------------
/test/elixir_scribe/utils/string/human_capitalize/human_capitalize_string_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.String.HumanCapitalize.HumanCapitalizeStringTest do
2 | use ExUnit.Case
3 |
4 | alias ElixirScribe.Utils.StringAPI
5 |
6 | describe "human_capitalize/1" do
7 | test "capitalizes a string to be human readable" do
8 | assert " string " |> StringAPI.human_capitalize() === "String"
9 | end
10 |
11 | test "capitalizes a snake_case string to be human readable" do
12 | assert "some_string" |> StringAPI.human_capitalize() === "Some String"
13 | end
14 |
15 | test "capitalizes a kebab-case string to be human readable" do
16 | assert "some-string" |> StringAPI.human_capitalize() === "Some String"
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/module/build_name/build_absolute_module_action_name_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Module.BuildName.BuildAbsoluteModuleActionNameTest do
2 | alias ElixirScribe.Template.ModuleAPI
3 | use ElixirScribe.BaseCase, async: true
4 |
5 | test "it builds the absolute module name for the given action" do
6 | contract = domain_contract_fixture()
7 |
8 | assert ModuleAPI.build_absolute_module_action_name(contract, "read", file_type: :lib_core) ===
9 | "ElixirScribe.Site.Blog.Post.Read.ReadPost"
10 | end
11 |
12 | test "it returns nil when the file type is HTML" do
13 | contract = domain_contract_fixture()
14 |
15 | assert ModuleAPI.build_absolute_module_action_name(contract, "read", file_type: :html) === nil
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/module/build_name/build_absolute_module_name.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Module.BuildName.BuildAbsoluteModuleName do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Generator.DomainContract
5 |
6 | def build(%DomainContract{} = _contract, file_type: :html), do: nil
7 |
8 | def build(%DomainContract{} = contract, opts) when is_list(opts),
9 | do: module_name(contract, opts)
10 |
11 | @core_files [:resource, :resource_test, :lib_core, :test_core]
12 | defp module_name(contract, file_type: type) when type in @core_files,
13 | do: contract.resource_module
14 |
15 | @web_files [:lib_web, :controller, :controller_test, :test_web]
16 | defp module_name(contract, file_type: type) when type in @web_files,
17 | do: contract.web_resource_module
18 | end
19 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/tests/controllers/read_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>ControllerTest do
2 | use <%= inspect contract.web_module %>.ConnCase
3 |
4 | import <%= inspect contract.schema.module %>Fixtures
5 |
6 | setup do
7 | <%= contract.schema.singular %> = <%= contract.schema.singular %>_fixture()
8 | %{<%= contract.schema.singular %>: <%= contract.schema.singular %>}
9 | end
10 |
11 | test "<%= action %> <%= contract.schema.singular %> renders form", %{conn: conn, <%= contract.schema.singular %>: <%= contract.schema.singular %>} do
12 | conn = get(conn, ~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>.id}")
13 | assert html_response(conn, 200) =~ "<%= contract.schema.human_singular %>"
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/html/default/read.html.heex:
--------------------------------------------------------------------------------
1 | <.header>
2 | <%= contract.schema.human_singular %> <%%= @<%= contract.schema.singular %>.id %>
3 | <:subtitle>This is a <%= contract.schema.singular %> record from your database.
4 | <:actions>
5 | <.link href={~p"<%= contract.schema.route_prefix %>/#{@<%= contract.schema.singular %>}/edit"}>
6 | <.button>Edit <%= contract.schema.singular %>
7 |
8 |
9 |
10 |
11 | <.list><%= for {k, _} <- contract.schema.attrs do %>
12 | <:item title="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"><%%= @<%= contract.schema.singular %>.<%= k %> %><% end %>
13 |
14 |
15 | <.back navigate={~p"<%= contract.schema.route_prefix %>"}>Back to <%= contract.schema.plural %>
16 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/module/build_name/build_absolute_module_action_name.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Module.BuildName.BuildAbsoluteModuleActionName do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.ModuleAPI
5 | alias ElixirScribe.Generator.DomainContract
6 | alias ElixirScribe.Utils.StringAPI
7 |
8 | def build(%DomainContract{} = _context, _action, file_type: :html), do: nil
9 |
10 | def build(%DomainContract{} = context, action, opts) when is_list(opts) do
11 | base_module = ModuleAPI.build_absolute_module_name(context, opts) |> inspect()
12 | module_action_name = ModuleAPI.build_module_action_name(context, action)
13 | action_capitalized = action |> StringAPI.capitalize()
14 |
15 | "#{base_module}.#{action_capitalized}.#{module_action_name}"
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/fixtures.ex:
--------------------------------------------------------------------------------
1 | alias <%= inspect contract.schema.module %>API
2 |
3 | <%= for {attr, {_function_name, function_def, _needs_impl?}} <- contract.schema.fixture_unique_functions do %> @doc """
4 | Generate a unique <%= contract.schema.singular %> <%= attr %>.
5 | """
6 | <%= function_def %>
7 | <% end %> @doc """
8 | Generate a <%= contract.schema.singular %>.
9 | """
10 | def <%= contract.schema.singular %>_fixture(attrs \\ %{}) do
11 | {:ok, <%= contract.schema.singular %>} =
12 | attrs
13 | |> Enum.into(%{
14 | <%= contract.schema.fixture_params |> Enum.map(fn {key, code} -> " #{key}: #{code}" end) |> Enum.join(",\n") %>
15 | })
16 | |> <%= inspect(contract.schema.alias) <> "API." <> create_action %>()
17 |
18 | <%= contract.schema.singular %>
19 | end
20 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/tests/controllers/edit_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>ControllerTest do
2 | use <%= inspect contract.web_module %>.ConnCase
3 |
4 | import <%= inspect contract.schema.module %>Fixtures
5 |
6 | setup do
7 | <%= contract.schema.singular %> = <%= contract.schema.singular %>_fixture()
8 | %{<%= contract.schema.singular %>: <%= contract.schema.singular %>}
9 | end
10 |
11 | test "<%= action %> <%= contract.schema.singular %> renders form for editing chosen <%= contract.schema.singular %>", %{conn: conn, <%= contract.schema.singular %>: <%= contract.schema.singular %>} do
12 | conn = get(conn, ~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}/<%= action %>")
13 | assert html_response(conn, 200) =~ "<%= edit_action_capitalized %> <%= contract.schema.human_singular %>"
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test/elixir_scribe/utils/string/capitalize/capitalize_string_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.String.Capitalize.CapitalizeStringTest do
2 | use ExUnit.Case
3 |
4 | alias ElixirScribe.Utils.StringAPI
5 |
6 | describe "capitalize/1" do
7 | test "capitalizes a string" do
8 | assert " string " |> StringAPI.capitalize() === "String"
9 | end
10 |
11 | test "capitalizes a snake_case string" do
12 | assert "some_string" |> StringAPI.capitalize() === "SomeString"
13 | end
14 |
15 | test "capitalizes a kebab-case string" do
16 | assert "some-string" |> StringAPI.capitalize() === "SomeString"
17 | end
18 | end
19 |
20 | describe "capitalize/2" do
21 | test "capitalizes a string with the given joiner" do
22 | joiner = " "
23 |
24 | assert "some-string" |> StringAPI.capitalize(joiner) === "Some String"
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/docs/modules/behaviours/persona_validator.ex:
--------------------------------------------------------------------------------
1 | defmodule PersonaValidator do
2 | @moduledoc false
3 |
4 | # A very simplistic set o validations used by the docs usage examples.
5 |
6 | @email_providers ["gmail.com", "yahoo.com", "hotmail.com"]
7 | def corporate_email?(email) when is_binary(email) do
8 | case String.split(email, "@", trim: true) do
9 | [_one_part] ->
10 | false
11 |
12 | [_, email_provider] when email_provider not in @email_providers ->
13 | true
14 |
15 | _ ->
16 | false
17 | end
18 | end
19 |
20 | def corporate_email?(_email), do: false
21 |
22 | # The role is optional, thus we return true
23 | def role?(role) when is_nil(role), do: true
24 |
25 | def role?(role) when is_binary(role) do
26 | role = role |> String.trim()
27 | String.length(role) >= 3
28 | end
29 |
30 | def role?(_role), do: false
31 | end
32 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/binding/rebuild/rebuild_binding_template.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Binding.Rebuild.RebuildBindingTemplate do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.ModuleAPI
5 | alias ElixirScribe.Utils.StringAPI
6 |
7 | def rebuild(bindings, action, opts)
8 | when is_list(bindings) and is_binary(action) and is_list(opts) do
9 | contract = Keyword.get(bindings, :contract)
10 |
11 | Keyword.merge(bindings,
12 | action: action,
13 | action_first_word: StringAPI.first_word(action),
14 | action_capitalized: StringAPI.capitalize(action),
15 | action_human_capitalized: StringAPI.human_capitalize(action),
16 | module_action_name: ModuleAPI.build_module_action_name(contract, action),
17 | absolute_module_action_name:
18 | ModuleAPI.build_absolute_module_action_name(contract, action, opts)
19 | )
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/file/inject/inject_content_before_module_end.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.File.Inject.InjectContentBeforeModuleEnd do
2 | @moduledoc false
3 |
4 | def inject(content_to_inject, file_path)
5 | when is_binary(content_to_inject) and is_binary(file_path) do
6 | file_content = File.read!(file_path)
7 |
8 | if String.contains?(file_content, content_to_inject) do
9 | {:noop, :content_to_inject_already_exists}
10 | else
11 | Mix.shell().info([:green, "* injecting ", :reset, Path.relative_to_cwd(file_path)])
12 |
13 | file_content
14 | |> String.trim_trailing()
15 | |> String.trim_trailing("end")
16 | |> Kernel.<>(content_to_inject)
17 | |> Kernel.<>("end\n")
18 | |> write_file(file_path)
19 | end
20 | end
21 |
22 | defp write_file(content, file_path) do
23 | File.write!(file_path, content)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain/resource/generate_schema/generate_schema_resource.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.GenerateSchema.GenerateSchemaResource do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.BindingAPI
5 | alias ElixirScribe.Generator.DomainContract
6 |
7 | def generate(%DomainContract{schema: %{generate?: false}} = contract), do: contract
8 |
9 | def generate(%DomainContract{schema: %{generate?: true}} = contract) do
10 | schema = ElixirScribe.to_phoenix_schema(contract.schema)
11 | paths = ElixirScribe.base_template_paths()
12 | bindings = build_bindings(contract)
13 |
14 | Mix.Tasks.Phx.Gen.Schema.copy_new_files(schema, paths, bindings)
15 |
16 | contract
17 | end
18 |
19 | defp build_bindings(contract) do
20 | contract
21 | |> BindingAPI.build_binding_template()
22 | |> Keyword.merge(schema: contract.schema)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/actions/schema_access/delete_schema_test.exs:
--------------------------------------------------------------------------------
1 | alias <%= absolute_module_action_name %>
2 | alias <%= inspect contract.schema.module %>
3 | alias <%= inspect contract.schema.module %><%= "." <> read_action_capitalized <> "." <> read_action_capitalized <> inspect(contract.schema.alias) %>
4 |
5 | import <%= inspect contract.schema.module %>Fixtures
6 |
7 | test "<%= action_first_word %>/1 deletes the <%= contract.schema.singular %>" do
8 | <%= contract.schema.singular %> = <%= contract.schema.singular %>_fixture()
9 | assert {:ok, %<%= inspect contract.schema.alias %>{}} = <%= action_capitalized %><%= inspect(contract.schema.alias) %>.<%= action_first_word %>(<%= contract.schema.singular %>.id)
10 | assert_raise Ecto.NoResultsError, fn -> <%= read_action_capitalized %><%= inspect(contract.schema.alias) %>.<%= read_action %>!(<%= contract.schema.singular %>.id) end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/mix/cli_command/parse_schema/parse_schema_cli_command.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Mix.CLICommand.ParseSchema.ParseSchemaCLICommand do
2 | @moduledoc false
3 |
4 | @supported_options [
5 | migration: :boolean,
6 | table: :string,
7 | web: :string,
8 | prefix: :string,
9 | repo: :string,
10 | migration_dir: :string
11 | ]
12 |
13 | @default_opts [
14 | migration: true
15 | ]
16 |
17 | def parse(args) when is_list(args) do
18 | {opts, parsed_args, invalid_opts} =
19 | args
20 | |> extract_args_and_opts()
21 |
22 | all_opts = opts |> parse_options()
23 |
24 | {parsed_args, all_opts, invalid_opts}
25 | end
26 |
27 | defp extract_args_and_opts(args) do
28 | OptionParser.parse(args, strict: @supported_options)
29 | end
30 |
31 | defp parse_options(opts) when is_list(opts) do
32 | @default_opts
33 | |> Keyword.merge(opts)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/tests/controllers/delete_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>ControllerTest do
2 | use <%= inspect contract.web_module %>.ConnCase
3 |
4 | import <%= inspect contract.schema.module %>Fixtures
5 |
6 | setup do
7 | <%= contract.schema.singular %> = <%= contract.schema.singular %>_fixture()
8 | %{<%= contract.schema.singular %>: <%= contract.schema.singular %>}
9 | end
10 |
11 | test "deletes chosen <%= contract.schema.singular %>", %{conn: conn, <%= contract.schema.singular %>: <%= contract.schema.singular %>} do
12 | conn = delete(conn, ~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}")
13 | assert redirected_to(conn) == ~p"<%= contract.schema.route_prefix %>"
14 |
15 | assert_error_sent 404, fn ->
16 | get(conn, ~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}")
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/elixir_scribe/utils/string/first_word/first_word_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.String.FirstWord.FirstWordStringTest do
2 | use ExUnit.Case
3 |
4 | alias ElixirScribe.Utils.StringAPI
5 |
6 | describe "first_word/2" do
7 | test "returns first word in a string with more then one word" do
8 | assert "This is a string" |> StringAPI.first_word() === "This"
9 | end
10 |
11 | test "returns the same string when it only has one word" do
12 | assert "string" |> StringAPI.first_word() === "string"
13 | end
14 |
15 | test "returns an empty string when an empty string is given" do
16 | assert "" |> StringAPI.first_word() === ""
17 | end
18 |
19 | test "returns an empty string when a string with white spaces is given" do
20 | assert " " |> StringAPI.first_word() === ""
21 | end
22 |
23 | test "returns the first word in a string when word separators are given as the second argument" do
24 | assert "This|is a string" |> StringAPI.first_word(["|"]) === "This"
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/mix/tasks/scribe.gen.domain_sync_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("test/mix_test_helper.exs")
2 |
3 | defmodule Mix.Tasks.Scribe.Gen.DomainSyncTest do
4 | use ElixirScribe.BaseCase
5 |
6 | import MixTestHelper
7 |
8 | setup do
9 | Mix.Task.clear()
10 | :ok
11 | end
12 |
13 | # test "it raises when inside in umbrella project", config do
14 | # in_tmp_umbrella_project(config.test, fn ->
15 | # assert_raise Mix.Error, ~r/No arguments were provided.*$/s, fn ->
16 | # Mix.Tasks.Scribe.Gen.Domain.run(~w(blog Post posts title:string))
17 | # end
18 | # end)
19 | # end
20 |
21 | test "it generates the domain resource", config do
22 | in_tmp_project(config.test, fn ->
23 | assert :ok = Mix.Tasks.Scribe.Gen.Domain.run(~w(Blog Post posts title:string))
24 |
25 | assert_received {:mix_shell, :info,
26 | [
27 | "\nRemember to update your repository by running migrations:\n\n $ mix ecto.migrate\n"
28 | ]}
29 | end)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/controllers/create_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>Controller do
2 | use <%= inspect contract.web_module %>, :controller
3 |
4 | alias <%= inspect contract.schema.module %>API
5 |
6 | plug :put_view, html: <%= inspect contract.web_module %>.<%= contract.schema.web_namespace %>.<%= inspect contract.schema.alias %>HTML
7 |
8 | def <%= action %>(conn, %{<%= inspect contract.schema.singular %> => <%= contract.schema.singular %>_params}) do
9 | case <%= inspect(contract.schema.alias) %>API.<%= action %>(<%= contract.schema.singular %>_params) do
10 | {:ok, <%= contract.schema.singular %>} ->
11 | conn
12 | |> put_flash(:info, "<%= contract.schema.human_singular %> created successfully.")
13 | |> redirect(to: ~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}")
14 |
15 | {:error, %Ecto.Changeset{} = changeset} ->
16 | render(conn, "<%= new_action %>_<%= contract.schema.singular %>.html", changeset: changeset)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/elixir_scribe/mix/mix_api_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.MixAPITest do
2 | alias ElixirScribe.MixAPI
3 | use ElixirScribe.BaseCase, async: true
4 |
5 | # @INFO: Tests in the API module only care about testing the function can be invoked and that the API contract is respected for guards, pattern matching and expected return types. The unit tests for the functionality are done in their respective modules.
6 |
7 | describe "parse_cli_command/1" do
8 | test "can be invoked with the correct argument type (list) and returns the expected type ({list, list, list})" do
9 | assert {parsed_args, opts, invalid_args} = MixAPI.parse_cli_command([])
10 | assert is_list(parsed_args)
11 | assert is_list(opts)
12 | assert is_list(invalid_args)
13 | end
14 |
15 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (list)" do
16 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
17 | MixAPI.parse_cli_command(%{})
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain/resource/generate_new_files/generate_new_files_resource.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.GenerateNewFiles.GenerateNewFilesResource do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.MixAPI
5 | alias ElixirScribe.Generator.DomainContract
6 | alias ElixirScribe.Generator.DomainResourceAPI
7 |
8 | def generate(%DomainContract{} = contract, opts) when is_list(opts) do
9 | prompt_for_conflicts? = Keyword.get(opts, :prompt_for_conflicts?, true)
10 |
11 | if prompt_for_conflicts? do
12 | prompt_for_conflicts(contract)
13 | end
14 |
15 | contract
16 | |> DomainResourceAPI.generate_api()
17 | |> DomainResourceAPI.generate_schema()
18 | |> DomainResourceAPI.generate_actions()
19 | |> DomainResourceAPI.generate_tests()
20 | |> DomainResourceAPI.generate_test_fixture()
21 |
22 | contract
23 | end
24 |
25 | defp prompt_for_conflicts(contract) do
26 | contract
27 | |> DomainResourceAPI.build_files_to_generate()
28 | |> MixAPI.prompt_for_file_conflicts()
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/file/build_dir_path_for_html/build_dir_path_for_html_file_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.File.BuildPathForHtml.BuildPathForHtmlFileTest do
2 | alias ElixirScribe.Template.FileAPI
3 |
4 | use ElixirScribe.BaseCase, async: true
5 |
6 | test "it builds the directory path for the default HTML template" do
7 | args = ["Site.Blog", "Post", "posts", "name:string", "desc:string"]
8 | contract = domain_contract_fixture(args)
9 |
10 | assert FileAPI.build_dir_path_for_html_file(contract) ===
11 | "priv/templates/scribe.gen.html/html/default"
12 | end
13 |
14 | test "it builds the directory path for a custom HTML template" do
15 | args = [
16 | "Site.Blog",
17 | "Post",
18 | "posts",
19 | "name:string",
20 | "desc:string",
21 | "--html-template",
22 | "online_shop"
23 | ]
24 |
25 | contract = domain_contract_fixture(args)
26 |
27 | assert FileAPI.build_dir_path_for_html_file(contract) ===
28 | "priv/templates/scribe.gen.html/html/online_shop"
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/module/build_name/build_absolute_module_action_name_aliases_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Module.BuildName.BuildAbsoluteModuleActionNameAliasesTest do
2 | alias ElixirScribe.Template.ModuleAPI
3 |
4 | use ElixirScribe.BaseCase, async: true
5 |
6 | test "it builds the module alias for each resource action" do
7 | contract = domain_contract_fixture()
8 |
9 | expected_aliases =
10 | """
11 |
12 | alias ElixirScribe.Site.Blog.Post.List.ListPosts
13 | alias ElixirScribe.Site.Blog.Post.New.NewPost
14 | alias ElixirScribe.Site.Blog.Post.Read.ReadPost
15 | alias ElixirScribe.Site.Blog.Post.Edit.EditPost
16 | alias ElixirScribe.Site.Blog.Post.Create.CreatePost
17 | alias ElixirScribe.Site.Blog.Post.Update.UpdatePost
18 | alias ElixirScribe.Site.Blog.Post.Delete.DeletePost
19 | """
20 | |> String.trim_trailing()
21 |
22 | assert ModuleAPI.build_absolute_module_action_name_aliases(contract,
23 | file_type: :lib_core
24 | ) === expected_aliases
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/route/scope/scope_action_routes_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Route.Scope.ScopeActionRoutesTest do
2 | alias ElixirScribe.Template.RouteAPI
3 | use ElixirScribe.BaseCase, async: true
4 |
5 | test "it builds the routes scope for all resource actions" do
6 | contract = domain_contract_fixture()
7 |
8 | expected_scope =
9 | """
10 |
11 | scope "/site/blog/posts", ElixirScribeWeb.Site.Blog.Post, as: :site_blog_posts do
12 | pipe_through :browser
13 |
14 | get "/", List.ListPostsController, :list
15 | get "/new", New.NewPostController, :new
16 | get "/:id", Read.ReadPostController, :read
17 | get "/:id/edit", Edit.EditPostController, :edit
18 | post "/", Create.CreatePostController, :create
19 | patch "/:id", Update.UpdatePostController, :update
20 | put "/:id", Update.UpdatePostController, :update
21 | delete "/:id", Delete.DeletePostController, :delete
22 | end
23 | """
24 |
25 | assert RouteAPI.scope_routes(contract) == expected_scope
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/controllers/update_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>Controller do
2 | use <%= inspect contract.web_module %>, :controller
3 |
4 | alias <%= inspect contract.schema.module %>API
5 |
6 | plug :put_view, html: <%= inspect contract.web_module %>.<%= contract.schema.web_namespace %>.<%= inspect contract.schema.alias %>HTML
7 |
8 | def <%= action %>(conn, %{"id" => id, <%= inspect contract.schema.singular %> => <%= contract.schema.singular %>_params}) do
9 | case <%= inspect(contract.schema.alias) %>API.<%= action %>(id, <%= contract.schema.singular %>_params) do
10 | {:ok, <%= contract.schema.singular %>} ->
11 | conn
12 | |> put_flash(:info, "<%= contract.schema.human_singular %> updated successfully.")
13 | |> redirect(to: ~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}")
14 |
15 | {:error, %Ecto.Changeset{} = changeset} ->
16 | render(conn, "<%= edit_action %>_<%= contract.schema.singular %>.html", <%= contract.schema.singular %>: changeset.data, changeset: changeset)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/actions/schema_access/create_schema_test.exs:
--------------------------------------------------------------------------------
1 | alias <%= absolute_module_action_name %>
2 | alias <%= inspect contract.schema.module %>
3 |
4 | @invalid_attrs <%= Mix.Phoenix.to_text for {key, _} <- contract.schema.params.create, into: %{}, do: {key, nil} %>
5 |
6 | test "<%= action_first_word %>/1 with valid data creates a <%= contract.schema.singular %>" do
7 | valid_attrs = <%= Mix.Phoenix.to_text contract.schema.params.create %>
8 |
9 | assert {:ok, %<%= inspect contract.schema.alias %>{} = <%= contract.schema.singular %>} = <%= action_capitalized %><%= inspect(contract.schema.alias) %>.<%= action_first_word %>(valid_attrs)
10 | <%= for {field, value} <- contract.schema.params.create do %>
11 | assert <%= contract.schema.singular %>.<%= field %> == <%= Mix.Phoenix.Schema.value(contract.schema, field, value) %><% end %>
12 | end
13 |
14 | test "<%= action_first_word %>/1 with invalid data returns error changeset" do
15 | assert {:error, %Ecto.Changeset{}} = <%= action_capitalized %><%= inspect(contract.schema.alias) %>.<%= action_first_word %>(@invalid_attrs)
16 | end
17 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2024] [Paulo Renato]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/utils/string/find_acronyms/find_acronyms.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.String.FindAcronyms do
2 | @moduledoc false
3 |
4 | @doc """
5 | Finds acronyms in a string. An acronym is defined as two or more consecutive uppercase letters.
6 |
7 | ## Examples
8 |
9 | iex> FindAcronyms.find("HTTPRequestHandler is a BooksAPI example")
10 | ["HTTP", "API"]
11 |
12 | iex> FindAcronyms.find("The ParseISBNHandler is an example")
13 | ["ISBN"]
14 |
15 | iex> FindAcronyms.find("JSONParser from file_OCAID")
16 | ["JSON", "OCAID"]
17 |
18 | iex> FindAcronyms.find("Find all TAR files from SQL_dumps dir")
19 | ["TAR", "SQL"]
20 |
21 | iex> FindAcronyms.find("Find all TXT files from dir some/JSON/folder")
22 | ["TXT", "JSON"]
23 |
24 | iex> FindAcronyms.find("This is a test with no acronyms")
25 | []
26 | """
27 | def find(string) when is_binary(string) and byte_size(string) > 0 do
28 | # Regex to match two or more uppercase letters not followed by a lowercase letter
29 | ~r/[A-Z]{2,}(?=[^a-z]|$)/
30 | |> Regex.scan(string)
31 | |> List.flatten()
32 | end
33 |
34 | def find(_string), do: []
35 | end
36 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/file/build_filename_for_action/build_filename_for_action_file_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.File.BuildFilenameForAction.BuildFilenameForActionFileTest do
2 | alias ElixirScribe.Template.FileAPI
3 | alias ElixirScribe.Template.BuildFilenameForActionFileContract
4 |
5 | use ElixirScribe.BaseCase, async: true
6 |
7 | test "it builds the template filename for an action in the default resource actions" do
8 | attrs = %{
9 | action: "read",
10 | action_suffix: "_",
11 | file_type: "schema",
12 | file_extension: ".ex"
13 | }
14 |
15 | contract = BuildFilenameForActionFileContract.new!(attrs)
16 |
17 | assert FileAPI.build_template_action_filename(contract) === "read_schema.ex"
18 | end
19 |
20 | test "it builds the default template filename for an action not in the the default resource actions" do
21 | attrs = %{
22 | action: "report",
23 | action_suffix: "_",
24 | file_type: "schema",
25 | file_extension: ".ex"
26 | }
27 |
28 | contract = BuildFilenameForActionFileContract.new!(attrs)
29 |
30 | assert FileAPI.build_template_action_filename(contract) === "default_schema.ex"
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain/resource/build_files_to_generate/build_files_to_generate_resource.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.BuildFilesToGenerate.BuildFilesToGenerateResource do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Generator.DomainResourceAPI
5 | alias ElixirScribe.Generator.DomainContract
6 |
7 | def build(%DomainContract{} = contract) do
8 | api_file = [DomainResourceAPI.build_api_file_paths(contract)]
9 | resource_files = DomainResourceAPI.build_action_files_paths(contract)
10 | resource_test_files = DomainResourceAPI.build_test_action_files_paths(contract)
11 | schema = ElixirScribe.to_phoenix_schema(contract.schema)
12 | schema_files = Mix.Tasks.Phx.Gen.Schema.files_to_be_generated(schema)
13 | test_fixtures_file = build_test_fixture_file(contract)
14 |
15 | api_file
16 | |> Kernel.++(resource_files)
17 | |> Kernel.++(resource_test_files)
18 | |> Kernel.++(schema_files)
19 | |> Kernel.++(test_fixtures_file)
20 | end
21 |
22 | defp build_test_fixture_file(%DomainContract{test_fixtures_file: test_fixtures_file}) do
23 | source_path = ElixirScribe.domain_tests_template_path() |> Path.join("fixtures_module.ex")
24 |
25 | [{:eex, source_path, test_fixtures_file}]
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/binding/rebuild/rebuild_binding_template_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Binding.Rebuild.RebuildBindingTemplateTest do
2 | alias ElixirScribe.Template.BindingAPI
3 |
4 | use ElixirScribe.BaseCase, async: true
5 |
6 | test "it rebuilds the bindings for the template" do
7 | contract = domain_contract_fixture()
8 |
9 | bindings = [
10 | contract: contract,
11 | action: "build_report",
12 | action_first_word: "build",
13 | action_capitalized: "BuildReport",
14 | action_human_capitalized: "Build Report",
15 | module_action_name: "BuildReportPost",
16 | absolute_module_action_name: "ElixirScribe.Site.Blog.Post.BatchUpdate.BatchUpdatePost"
17 | ]
18 |
19 | expected_bindings = [
20 | contract: contract,
21 | action: "batch_update",
22 | action_first_word: "batch",
23 | action_capitalized: "BatchUpdate",
24 | action_human_capitalized: "Batch Update",
25 | module_action_name: "BatchUpdatePost",
26 | absolute_module_action_name: "ElixirScribe.Site.Blog.Post.BatchUpdate.BatchUpdatePost"
27 | ]
28 |
29 | assert BindingAPI.rebuild_binding_template(bindings, "batch_update", file_type: :resource) ===
30 | expected_bindings
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/mix/tasks/scribe.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Scribe do
2 | use Mix.Task
3 |
4 | @shortdoc "Prints Elixir Scribe help information"
5 |
6 | @moduledoc """
7 | Prints Elixir Scribe tasks and their information.
8 |
9 | $ mix scribe
10 |
11 | To print the Phoenix version, pass `-v` or `--version`, for example:
12 |
13 | $ mix scribe --version
14 |
15 | """
16 |
17 | @version Mix.Project.config()[:version]
18 |
19 | @impl true
20 | @doc false
21 | def run([version]) when version in ~w(-v --version) do
22 | Mix.shell().info("Elixir Scribe v#{@version}")
23 | end
24 |
25 | def run(args) do
26 | case args do
27 | [] -> help()
28 | _ -> Mix.raise("Invalid arguments, expected: mix scribe")
29 | end
30 | end
31 |
32 | defp help() do
33 | Application.ensure_all_started(:phoenix)
34 | Mix.shell().info("Elixir Scribe v#{Application.spec(:phoenix, :vsn)}")
35 |
36 | Mix.shell().info(
37 | "\nEnables craftsmanship through Clean Code in a Clean Software Architecture\n"
38 | )
39 |
40 | Mix.shell().info("\n## Options\n")
41 | Mix.shell().info("-v, --version # Prints Elixir Scribe version\n")
42 | Mix.shell().info("\n## Tasks\n")
43 | Mix.Tasks.Help.run(["--search", "scribe."])
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/tests/controllers/create_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>ControllerTest do
2 | use <%= inspect contract.web_module %>.ConnCase
3 |
4 | @create_attrs <%= Mix.Phoenix.to_text contract.schema.params.create %>
5 | @invalid_attrs <%= Mix.Phoenix.to_text (for {key, _} <- contract.schema.params.create, into: %{}, do: {key, nil}) %>
6 |
7 | test "<%= action %> <%= contract.schema.singular %> redirects to show when data is valid", %{conn: conn} do
8 | conn = post(conn, ~p"<%= contract.schema.route_prefix %>", <%= contract.schema.singular %>: @create_attrs)
9 |
10 | assert %{id: id} = redirected_params(conn)
11 | assert redirected_to(conn) == ~p"<%= contract.schema.route_prefix %>/#{id}"
12 |
13 | conn = get(conn, ~p"<%= contract.schema.route_prefix %>/#{id}")
14 | assert html_response(conn, 200) =~ "<%= contract.schema.human_singular %> #{id}"
15 | end
16 |
17 | test "<%= action %> <%= contract.schema.singular %> renders errors when data is invalid", %{conn: conn} do
18 | conn = post(conn, ~p"<%= contract.schema.route_prefix %>", <%= contract.schema.singular %>: @invalid_attrs)
19 | assert html_response(conn, 200) =~ "<%= new_action_capitalized %> <%= contract.schema.human_singular %>"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/mix/mix_api.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.MixAPI do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Mix.Shell.PromptForFileConflicts.PromptForFileConflictsShell
5 | alias ElixirScribe.Mix.CLICommand.Parse.ParseCLICommand
6 | alias ElixirScribe.Mix.CLICommand.ParseSchema.ParseSchemaCLICommand
7 |
8 | @doc """
9 | Parses the provided list, that represents the CLI command, into one list of arguments and one list of options. The returned options will include all the options with their defaults.
10 |
11 | It returns a tuple in this format: `{parsed_args, all_opts, invalid}`.
12 | """
13 | def parse_cli_command(args) when is_list(args), do: ParseCLICommand.parse(args)
14 |
15 | @doc """
16 | Parses the provided list, that represents the Schema CLI command, into one list of arguments and one list of options. The returned options will include all the options with their defaults.
17 |
18 | It returns a tuple in this format: `{parsed_args, all_opts, invalid}`.
19 | """
20 | def parse_schema_cli_command(args) when is_list(args), do: ParseSchemaCLICommand.parse(args)
21 |
22 | @doc """
23 | Prompts to continue if any files exist.
24 | """
25 | def prompt_for_file_conflicts(files) when is_list(files),
26 | do: PromptForFileConflictsShell.prompt(files)
27 | end
28 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/html/default/list.html.heex:
--------------------------------------------------------------------------------
1 | <.header>
2 | Listing <%= contract.schema.human_plural %>
3 | <:actions>
4 | <.link href={~p"<%= contract.schema.route_prefix %>/new"}>
5 | <.button>New <%= contract.schema.human_singular %>
6 |
7 |
8 |
9 |
10 | <.table id="<%= contract.schema.plural %>" rows={@<%= contract.schema.collection %>} row_click={&JS.navigate(~p"<%= contract.schema.route_prefix %>/#{&1}")}><%= for {k, _} <- contract.schema.attrs do %>
11 | <:col :let={<%= contract.schema.singular %>} label="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"><%%= <%= contract.schema.singular %>.<%= k %> %><% end %>
12 | <:action :let={<%= contract.schema.singular %>}>
13 |
14 | <.link navigate={~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}"}>Show
15 |
16 | <.link navigate={~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}/edit"}>Edit
17 |
18 | <:action :let={<%= contract.schema.singular %>}>
19 | <.link href={~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}"} method="delete" data-confirm="Are you sure?">
20 | Delete
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/schema_resource_api.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.SchemaResourceAPI do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Generator.SchemaContract
5 | alias ElixirScribe.Generator.Schema.Resource.SchemaResourceHelpers
6 | alias ElixirScribe.Generator.Schema.Resource.BuildSchemaResourceContract
7 |
8 | def build_schema_resource_contract(args, opts) when is_list(args) and is_list(opts),
9 | do: BuildSchemaResourceContract.build(args, opts)
10 |
11 | def build_schema_resource_contract!(args, opts) when is_list(args) and is_list(opts),
12 | do: BuildSchemaResourceContract.build!(args, opts)
13 |
14 | def default_param_value(%SchemaContract{} = contract, action) when is_atom(action),
15 | do: SchemaResourceHelpers.default_param(contract, action)
16 |
17 | def live_form_value(%Date{} = date), do: SchemaResourceHelpers.live_form_value(date)
18 | def live_form_value(%Time{} = time), do: SchemaResourceHelpers.live_form_value(time)
19 | def live_form_value(%DateTime{} = datetime), do: SchemaResourceHelpers.live_form_value(datetime)
20 |
21 | def live_form_value(%NaiveDateTime{} = naive_datetime),
22 | do: SchemaResourceHelpers.live_form_value(naive_datetime)
23 |
24 | def live_form_value(value), do: SchemaResourceHelpers.live_form_value(value)
25 | end
26 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/mix/shell/prompt_for_file_conflicts/prompt_for_file_conflicts_shell.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Mix.Shell.PromptForFileConflicts.PromptForFileConflictsShell do
2 | @moduledoc false
3 |
4 | # The function `prompt/1` was copied from the Phoenix Framework module at `/lib/mix/phoenix.ex`.
5 | # It was modified to use the `action` from the provided `generator_files`,
6 | # which is now a three elements tuple.
7 |
8 | @doc false
9 | def prompt(generator_files) when is_list(generator_files) do
10 | file_paths =
11 | Enum.flat_map(generator_files, fn
12 | {:new_eex, _, _path} -> []
13 | {_kind, _source_path, target_path} -> [target_path]
14 | {_kind, _file_type, _source_path, target_path} -> [target_path]
15 | {_kind, _file_type, _source_path, target_path, _action} -> [target_path]
16 | end)
17 |
18 | case Enum.filter(file_paths, &File.exists?(&1)) do
19 | [] ->
20 | :ok
21 |
22 | conflicts ->
23 | Mix.shell().info("""
24 | The following files conflict with new files to be generated:
25 |
26 | #{Enum.map_join(conflicts, "\n", &" * #{&1}")}
27 |
28 | """)
29 |
30 | unless Mix.shell().yes?("Proceed with interactive overwrite?") do
31 | System.halt()
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/domain/resource/generate_schema/generate_schema_resource_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("test//mix_test_helper.exs")
2 |
3 | defmodule ElixirScribe.Generator.Domain.Resource.GenerateSchema.GenerateSchemaResourceTest do
4 | alias ElixirScribe.Generator.DomainResourceAPI
5 |
6 | use ElixirScribe.BaseCase
7 | import MixTestHelper
8 |
9 | setup do
10 | Mix.Task.clear()
11 | :ok
12 | end
13 |
14 | test "generates the Resource Schema file", config do
15 | in_tmp_project(config.test, fn ->
16 | contract = domain_contract_fixture()
17 | DomainResourceAPI.generate_schema(contract)
18 |
19 | assert_file("lib/elixir_scribe/domain/site/blog/post/post_schema.ex", fn file ->
20 | assert file =~ "field :name, :string"
21 | assert file =~ "field :desc, :string"
22 | end)
23 | end)
24 | end
25 |
26 | test "doesn't generate the Resource Schema file when the flag --no-schema is used", config do
27 | in_tmp_project(config.test, fn ->
28 | args = ["Blog", "Post", "posts", "title:string", "desc:string", "--no-schema"]
29 |
30 | contract = domain_contract_fixture(args)
31 |
32 | DomainResourceAPI.generate_schema(contract)
33 |
34 | refute File.exists?("lib/elixir_scribe/domain/site/blog/post/post_schema.ex")
35 | end)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/module_api.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.ModuleAPI do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Generator.DomainContract
5 | alias ElixirScribe.Template.Module.BuildName.BuildAbsoluteModuleName
6 | alias ElixirScribe.Template.Module.BuildName.BuildModuleActionName
7 | alias ElixirScribe.Template.Module.BuildName.BuildAbsoluteModuleActionName
8 | alias ElixirScribe.Template.Module.BuildName.BuildAbsoluteModuleActionNameAliases
9 | alias ElixirScribe.Template.Options.BuildEmbedTemplates.BuildModuleEmbedTemplates
10 |
11 | def build_embeded_templates(), do: BuildModuleEmbedTemplates.build()
12 |
13 | def build_absolute_module_action_name(%DomainContract{} = contract, action, opts)
14 | when is_list(opts),
15 | do: BuildAbsoluteModuleActionName.build(contract, action, opts)
16 |
17 | def build_absolute_module_action_name_aliases(%DomainContract{} = contract, opts)
18 | when is_list(opts),
19 | do: BuildAbsoluteModuleActionNameAliases.build(contract, opts)
20 |
21 | def build_absolute_module_name(%DomainContract{} = contract, opts) when is_list(opts),
22 | do: BuildAbsoluteModuleName.build(contract, opts)
23 |
24 | def build_module_action_name(%DomainContract{} = contract, action) when is_binary(action),
25 | do: BuildModuleActionName.build(contract, action)
26 | end
27 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/binding/build/build_binding_template_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Binding.Build.BuildBindingTemplateTest do
2 | alias ElixirScribe.Template.BindingAPI
3 |
4 | use ElixirScribe.BaseCase, async: true
5 |
6 | test "it builds the template bindings" do
7 | contract = domain_contract_fixture()
8 |
9 | expected_embeds =
10 | """
11 |
12 | embed_templates "read/*"
13 | embed_templates "new/*"
14 | embed_templates "edit/*"
15 | embed_templates "list/*"
16 | """
17 | |> String.trim_trailing()
18 |
19 | expected_bindings = [
20 | embeded_templates: expected_embeds,
21 | contract: contract,
22 | list_action: "list",
23 | new_action: "new",
24 | read_action: "read",
25 | edit_action: "edit",
26 | create_action: "create",
27 | update_action: "update",
28 | delete_action: "delete",
29 | list_action_capitalized: "List",
30 | new_action_capitalized: "New",
31 | read_action_capitalized: "Read",
32 | edit_action_capitalized: "Edit",
33 | create_action_capitalized: "Create",
34 | update_action_capitalized: "Update",
35 | delete_action_capitalized: "Delete"
36 | ]
37 |
38 | assert BindingAPI.build_binding_template(contract) === expected_bindings
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/utils/string_api.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.StringAPI do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Utils.String.Capitalize.CapitalizeString
5 | alias ElixirScribe.Utils.String.HumanCapitalize.HumanCapitalizeString
6 | alias ElixirScribe.Utils.String.FirstWord.FirstWordString
7 | alias ElixirScribe.Utils.String.CamelCaseToSentence
8 | alias ElixirScribe.Utils.String.FindAcronyms
9 |
10 | def capitalize(string) when is_binary(string), do: CapitalizeString.capitalize(string)
11 |
12 | def capitalize(string, joiner) when is_binary(string) and is_binary(joiner),
13 | do: CapitalizeString.capitalize(string, joiner)
14 |
15 | def human_capitalize(string) when is_binary(string),
16 | do: HumanCapitalizeString.capitalize(string)
17 |
18 | def first_word(string) when is_binary(string),
19 | do: FirstWordString.first(string)
20 |
21 | def first_word(string, word_separators) when is_binary(string) when is_list(word_separators),
22 | do: FirstWordString.first(string, word_separators)
23 |
24 | def camel_case_to_sentence(camel_case_word)
25 | when is_binary(camel_case_word) and byte_size(camel_case_word) > 0,
26 | do: CamelCaseToSentence.convert(camel_case_word)
27 |
28 | def camel_case_to_sentence(camel_case_word, modifier)
29 | when is_binary(camel_case_word) and byte_size(camel_case_word) > 0 and is_atom(modifier),
30 | do: CamelCaseToSentence.convert(camel_case_word)
31 |
32 | def find_acronyms(string) when is_binary(string), do: FindAcronyms.find(string)
33 | end
34 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/file_api.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.FileAPI do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.BuildFilenameForActionFileContract
5 | alias ElixirScribe.Generator.DomainContract
6 | alias ElixirScribe.Template.File.Inject.InjectContentBeforeModuleEnd
7 | alias ElixirScribe.Template.File.Inject.InjectEExTemplateBeforeModuleEnd
8 | alias ElixirScribe.Template.File.BuildFilenameForAction.BuildFilenameForActionFile
9 | alias ElixirScribe.Template.File.BuildPathForHtml.BuildPathForHtmlFile
10 |
11 | def build_dir_path_for_html_file(%DomainContract{} = contract),
12 | do: BuildPathForHtmlFile.build(contract)
13 |
14 | def build_template_action_filename(%BuildFilenameForActionFileContract{} = contract),
15 | do: BuildFilenameForActionFile.build(contract)
16 |
17 | def inject_content_before_module_end(content_to_inject, file_path)
18 | when is_binary(content_to_inject) and is_binary(file_path),
19 | do: InjectContentBeforeModuleEnd.inject(content_to_inject, file_path)
20 |
21 | def inject_eex_template_before_module_end(
22 | base_template_paths,
23 | source_path,
24 | target_path,
25 | binding
26 | )
27 | when is_list(base_template_paths) and is_binary(source_path) and is_binary(target_path) and
28 | is_list(binding),
29 | do:
30 | InjectEExTemplateBeforeModuleEnd.inject(
31 | base_template_paths,
32 | source_path,
33 | target_path,
34 | binding
35 | )
36 | end
37 |
--------------------------------------------------------------------------------
/test/docs/modules/behaviours/persona_validator_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PersonaValidatorTest do
2 | use ElixirScribe.BaseCase, async: true
3 |
4 | # This validator is used as part of the Persona module in the docs for the
5 | # typed contract, therefore not a strict validator that needs to be exhaustively tested.
6 |
7 | describe "corporate_email?/1" do
8 | test "returns true with valid corporate email" do
9 | assert PersonaValidator.corporate_email?("me@company.com")
10 | end
11 |
12 | test "returns false with valid personal email" do
13 | refute PersonaValidator.corporate_email?("me@gmail.com")
14 | end
15 |
16 | test "returns false with invalid email format" do
17 | refute PersonaValidator.corporate_email?("me@")
18 | refute PersonaValidator.corporate_email?("megmail.com")
19 | end
20 |
21 | test "returns false when the email isn't a string" do
22 | refute PersonaValidator.corporate_email?(["me@company.com"])
23 | end
24 | end
25 |
26 | describe "role?/1" do
27 | test "it returns true when the optional role attribute it's nil" do
28 | assert PersonaValidator.role?(nil)
29 | end
30 |
31 | test "it returns true when the role is a string with at least three characters" do
32 | assert PersonaValidator.role?("dev")
33 | end
34 |
35 | test "it returns false when the role is a string with less then three characters" do
36 | refute PersonaValidator.role?("dv")
37 | end
38 |
39 | test "it returns false when the role isn't a string" do
40 | refute PersonaValidator.role?(:dev)
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/binding/build/build_binding_template.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Binding.Build.BuildBindingTemplate do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.ModuleAPI
5 | alias ElixirScribe.Generator.DomainContract
6 | alias ElixirScribe.Utils.StringAPI
7 |
8 | def build(%DomainContract{} = contract) do
9 | resource_actions = contract.resource_actions
10 |
11 | [contract: contract]
12 | |> add_resource_action_aliases(resource_actions)
13 | |> add_resource_action_aliases_capitalized(resource_actions)
14 | |> add_embeded_templates()
15 | end
16 |
17 | defp add_resource_action_aliases(binding, resource_actions) do
18 | new_bindings =
19 | resource_actions
20 | |> Keyword.new(fn action ->
21 | action_alias = ElixirScribe.resource_action_alias(action)
22 | action_key = String.to_atom("#{action}_action")
23 | {action_key, action_alias}
24 | end)
25 |
26 | Keyword.merge(binding, new_bindings)
27 | end
28 |
29 | defp add_resource_action_aliases_capitalized(binding, resource_actions) do
30 | new_bindings =
31 | resource_actions
32 | |> Keyword.new(&new_action_binding/1)
33 |
34 | Keyword.merge(binding, new_bindings)
35 | end
36 |
37 | defp new_action_binding(action) do
38 | action_alias_capitalized =
39 | action
40 | |> ElixirScribe.resource_action_alias()
41 | |> StringAPI.capitalize()
42 |
43 | action_key = String.to_atom("#{action}_action_capitalized")
44 |
45 | {action_key, action_alias_capitalized}
46 | end
47 |
48 | defp add_embeded_templates(binding) do
49 | Keyword.put(binding, :embeded_templates, ModuleAPI.build_embeded_templates())
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.domain/tests/actions/schema_access/update_schema_test.exs:
--------------------------------------------------------------------------------
1 | alias <%= absolute_module_action_name %>
2 | alias <%= inspect contract.schema.module %>
3 | alias <%= inspect contract.schema.module %><%= "." <> read_action_capitalized <> "." <> read_action_capitalized <> inspect(contract.schema.alias) %>
4 |
5 | import <%= inspect contract.schema.module %>Fixtures
6 |
7 | @invalid_attrs <%= Mix.Phoenix.to_text for {key, _} <- contract.schema.params.create, into: %{}, do: {key, nil} %>
8 |
9 | test "<%= action_first_word %>/2 with valid data updates the <%= contract.schema.singular %>" do
10 | <%= contract.schema.singular %> = <%= contract.schema.singular %>_fixture()
11 | update_attrs = <%= Mix.Phoenix.to_text contract.schema.params.update%>
12 |
13 | assert {:ok, %<%= inspect contract.schema.alias %>{} = <%= contract.schema.singular %>} = <%= action_capitalized %><%= inspect(contract.schema.alias) %>.<%= action_first_word %>(<%= contract.schema.singular %>.id, update_attrs)
14 | <%= for {field, value} <- contract.schema.params.update do %>
15 | assert <%= contract.schema.singular %>.<%= field %> == <%= Mix.Phoenix.Schema.value(contract.schema, field, value) %><% end %>
16 | end
17 |
18 | test "<%= action_first_word %>/2 with invalid data returns error changeset" do
19 | <%= contract.schema.singular %> = <%= contract.schema.singular %>_fixture()
20 | assert {:error, %Ecto.Changeset{}} = <%= action_capitalized %><%= inspect(contract.schema.alias) <> "." <> action_first_word %>(<%= contract.schema.singular %>.id, @invalid_attrs)
21 | assert <%= contract.schema.singular %> == <%= read_action_capitalized %><%= inspect(contract.schema.alias) <> "." <> read_action %>!(<%= contract.schema.singular %>.id)
22 | end
23 |
--------------------------------------------------------------------------------
/test/elixir_scribe/behaviour_typed_contract_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Behaviour.TypedContractTest do
2 | use ExUnit.Case, async: true
3 | doctest ElixirScribe.Behaviour.TypedContract
4 |
5 | defmodule CarContract do
6 | @moduledoc false
7 |
8 | @fields %{
9 | required: [:brand, :model],
10 | optional: [year: nil]
11 | }
12 |
13 | use ElixirScribe.Behaviour.TypedContract, fields: @fields
14 |
15 | @impl true
16 | def type_spec() do
17 | schema(%__MODULE__{
18 | brand: is_binary() |> spec(),
19 | model: is_binary() |> spec(),
20 | year: spec(is_integer() or is_nil())
21 | })
22 | end
23 | end
24 |
25 | test "it can create a typed contract with :required and :optional fields" do
26 | attrs = %{
27 | brand: "Toyota",
28 | model: "Supra",
29 | year: 2000
30 | }
31 |
32 | assert {:ok, %CarContract{} = contract} = CarContract.new(attrs)
33 | assert ^contract = CarContract.new!(attrs)
34 | end
35 |
36 | defmodule ProductContract do
37 | @moduledoc false
38 |
39 | @fields %{
40 | required: [:title, :description]
41 | }
42 |
43 | use ElixirScribe.Behaviour.TypedContract, fields: @fields
44 |
45 | @impl true
46 | def type_spec() do
47 | schema(%__MODULE__{
48 | title: is_binary() |> spec(),
49 | description: is_binary() |> spec()
50 | })
51 | end
52 | end
53 |
54 | test "it can create a typed contract without the :optional key" do
55 | attrs = %{
56 | title: "Penguin Laptop",
57 | description: "Awesome Linux machine"
58 | }
59 |
60 | assert {:ok, %ProductContract{} = contract} = ProductContract.new(attrs)
61 | assert ^contract = ProductContract.new!(attrs)
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/priv/templates/scribe.gen.html/tests/controllers/update_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule <%= absolute_module_action_name %>ControllerTest do
2 | use <%= inspect contract.web_module %>.ConnCase
3 |
4 | import <%= inspect contract.schema.module %>Fixtures
5 |
6 | @update_attrs <%= Mix.Phoenix.to_text contract.schema.params.update %>
7 | @invalid_attrs <%= Mix.Phoenix.to_text (for {key, _} <- contract.schema.params.create, into: %{}, do: {key, nil}) %>
8 |
9 | setup do
10 | <%= contract.schema.singular %> = <%= contract.schema.singular %>_fixture()
11 | %{<%= contract.schema.singular %>: <%= contract.schema.singular %>}
12 | end
13 |
14 | test "<%= action %> <%= contract.schema.singular %> redirects when data is valid", %{conn: conn, <%= contract.schema.singular %>: <%= contract.schema.singular %>} do
15 | conn = put(conn, ~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}", <%= contract.schema.singular %>: @update_attrs)
16 | assert redirected_to(conn) == ~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}"
17 |
18 | conn = get(conn, ~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}")<%= if contract.schema.string_attr do %>
19 | assert html_response(conn, 200) =~ <%= inspect ElixirScribe.Generator.SchemaResourceAPI.default_param_value(contract.schema, :update) %><% else %>
20 | assert html_response(conn, 200)<% end %>
21 | end
22 |
23 | test "renders errors when data is invalid", %{conn: conn, <%= contract.schema.singular %>: <%= contract.schema.singular %>} do
24 | conn = put(conn, ~p"<%= contract.schema.route_prefix %>/#{<%= contract.schema.singular %>}", <%= contract.schema.singular %>: @invalid_attrs)
25 | assert html_response(conn, 200) =~ "<%= edit_action_capitalized %> <%= contract.schema.human_singular %>"
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain/resource/build_action_files_paths/build_action_files_paths_resource.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.BuildActionFilesPaths.BuildActionFilesPathsResource do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.BuildFilenameForActionFileContract
5 | alias ElixirScribe.Generator.DomainContract
6 | alias ElixirScribe.Template.FileAPI
7 |
8 | def build(%DomainContract{} = contract) do
9 | resource_actions = contract.opts |> Keyword.get(:resource_actions)
10 |
11 | for action <- resource_actions do
12 | source_path = build_source_path(contract.schema, action)
13 | target_path = build_target_path(contract, action)
14 |
15 | {:eex, :resource, source_path, target_path, action}
16 | end
17 | end
18 |
19 | defp build_target_path(contract, action) do
20 | plural_actions = ElixirScribe.resource_plural_actions()
21 |
22 | resource_name =
23 | (action in plural_actions && contract.resource_path_name_plural) ||
24 | contract.resource_path_name_singular
25 |
26 | filename = "#{action}_" <> resource_name <> ".ex"
27 |
28 | Path.join([contract.lib_resource_dir, action, filename])
29 | end
30 |
31 | defp build_source_path(schema, action) do
32 | resource_action_path = ElixirScribe.resource_actions_template_path()
33 | schema_folder = ElixirScribe.schema_template_folder_name(schema)
34 | action_template_filename = build_template_action_filename(action, schema.generate?)
35 |
36 | Path.join([resource_action_path, schema_folder, action_template_filename])
37 | end
38 |
39 | defp build_template_action_filename(action, true) do
40 | attrs = %{action: action, action_suffix: "_", file_type: "schema", file_extension: ".ex"}
41 |
42 | contract = BuildFilenameForActionFileContract.new!(attrs)
43 |
44 | FileAPI.build_template_action_filename(contract)
45 | end
46 |
47 | defp build_template_action_filename(_action, false), do: "any_action.ex"
48 | end
49 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/mix/cli_command/parse/parse_cli_command.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Mix.CLICommand.Parse.ParseCLICommand do
2 | @moduledoc false
3 |
4 | @supported_options [
5 | table: :string,
6 | web: :string,
7 | schema: :boolean,
8 | context_app: :string,
9 | merge_with_existing_context: :boolean,
10 | prefix: :string,
11 | live: :boolean,
12 | actions: :string,
13 | no_default_actions: :boolean,
14 | html_template: :string
15 | ]
16 |
17 | @default_opts [
18 | schema: true,
19 | no_default_actions: false,
20 | actions: nil,
21 | html_template: "default"
22 | ]
23 |
24 | def parse(args) when is_list(args) do
25 | {opts, parsed_args, invalid_opts} =
26 | args
27 | |> extract_args_and_opts()
28 |
29 | all_opts = opts |> parse_options()
30 |
31 | {parsed_args, all_opts, invalid_opts}
32 | end
33 |
34 | defp extract_args_and_opts(args) do
35 | OptionParser.parse(args, strict: @supported_options)
36 | end
37 |
38 | defp parse_options(opts) when is_list(opts) do
39 | @default_opts
40 | |> Keyword.merge(opts)
41 | |> put_context_app(opts[:context_app])
42 | |> put_resource_actions()
43 | end
44 |
45 | defp put_context_app(opts, nil), do: opts
46 |
47 | defp put_context_app(opts, string) do
48 | Keyword.put(opts, :context_app, String.to_atom(string))
49 | end
50 |
51 | defp put_resource_actions(opts) do
52 | resource_actions = build_actions_from_options(opts)
53 | Keyword.put(opts, :resource_actions, resource_actions)
54 | end
55 |
56 | defp build_actions_from_options(opts) when is_list(opts) do
57 | no_default_actions = (opts[:no_default_actions] && true) || false
58 |
59 | if no_default_actions do
60 | actions(opts[:actions])
61 | else
62 | (actions(opts[:actions]) ++ ElixirScribe.resource_actions())
63 | |> Enum.uniq()
64 | end
65 | end
66 |
67 | defp actions(actions) when is_binary(actions) and byte_size(actions) > 0,
68 | do: actions |> String.split(",")
69 |
70 | defp actions(_actions), do: []
71 | end
72 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain/resource/build_test_action_files_paths/build_test_action_files_paths_resource.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.BuildTestActionFilesPaths.BuildTestActionFilesPathsResource do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.BuildFilenameForActionFileContract
5 | alias ElixirScribe.Generator.DomainContract
6 | alias ElixirScribe.Template.FileAPI
7 |
8 | def build(%DomainContract{} = contract) do
9 | resource_actions = contract.opts |> Keyword.get(:resource_actions)
10 |
11 | for action <- resource_actions do
12 | source_path = build_source_path(contract.schema, action)
13 | target_path = build_target_path(contract, action)
14 |
15 | {:eex, :resource_test, source_path, target_path, action}
16 | end
17 | end
18 |
19 | defp build_target_path(contract, action) do
20 | plural_actions = ElixirScribe.resource_plural_actions()
21 |
22 | resource_name =
23 | (action in plural_actions && contract.resource_path_name_plural) ||
24 | contract.resource_path_name_singular
25 |
26 | filename = "#{action}_" <> resource_name <> "_test.exs"
27 |
28 | Path.join([contract.test_resource_dir, action, filename])
29 | end
30 |
31 | defp build_source_path(schema, action) do
32 | resource_action_path = ElixirScribe.resource_test_actions_template_path()
33 | schema_folder = ElixirScribe.schema_template_folder_name(schema)
34 | action_template_filename = build_template_action_filename(action, schema.generate?)
35 |
36 | Path.join([resource_action_path, schema_folder, action_template_filename])
37 | end
38 |
39 | defp build_template_action_filename(action, true) do
40 | attrs = %{
41 | action: action,
42 | action_suffix: "_",
43 | file_type: "schema",
44 | file_extension: "_test.exs"
45 | }
46 |
47 | contract = BuildFilenameForActionFileContract.new!(attrs)
48 |
49 | FileAPI.build_template_action_filename(contract)
50 | end
51 |
52 | defp build_template_action_filename(_action, false), do: "module_any_action_test.exs"
53 | end
54 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/domain/resource/build_test_action_files_paths/build_test_action_files_paths_resource_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.BuildTestActionFilesPaths.BuildTestActionFilesPathsResourceTest do
2 | alias ElixirScribe.Generator.DomainResourceAPI
3 |
4 | use ElixirScribe.BaseCase, async: true
5 |
6 | test "returns a list of all resource action test files paths" do
7 | expected_files = [
8 | {:eex, :resource_test,
9 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/list_schema_test.exs",
10 | "test/elixir_scribe/domain/site/blog/post/list/list_posts_test.exs", "list"},
11 | {:eex, :resource_test,
12 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/new_schema_test.exs",
13 | "test/elixir_scribe/domain/site/blog/post/new/new_post_test.exs", "new"},
14 | {:eex, :resource_test,
15 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/read_schema_test.exs",
16 | "test/elixir_scribe/domain/site/blog/post/read/read_post_test.exs", "read"},
17 | {:eex, :resource_test,
18 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/edit_schema_test.exs",
19 | "test/elixir_scribe/domain/site/blog/post/edit/edit_post_test.exs", "edit"},
20 | {:eex, :resource_test,
21 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/create_schema_test.exs",
22 | "test/elixir_scribe/domain/site/blog/post/create/create_post_test.exs", "create"},
23 | {:eex, :resource_test,
24 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/update_schema_test.exs",
25 | "test/elixir_scribe/domain/site/blog/post/update/update_post_test.exs", "update"},
26 | {:eex, :resource_test,
27 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/delete_schema_test.exs",
28 | "test/elixir_scribe/domain/site/blog/post/delete/delete_post_test.exs", "delete"}
29 | ]
30 |
31 | contract = domain_contract_fixture()
32 |
33 | assert DomainResourceAPI.build_test_action_files_paths(contract) === expected_files
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/file/inject/inject_content_before_module_end_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("test/mix_test_helper.exs")
2 |
3 | defmodule ElixirScribe.Template.File.Inject.InjectContentBeforeModuleEndTest do
4 | alias ElixirScribe.Template.FileAPI
5 | use ElixirScribe.BaseCase
6 |
7 | import MixTestHelper
8 |
9 | setup do
10 | Mix.Task.clear()
11 | :ok
12 | end
13 |
14 | test "it injects content in the module before the final end tag", config do
15 | in_tmp(config.test, fn ->
16 | api_content = """
17 | defmodule API do
18 | def whatever(), do: :whatever
19 | end
20 | """
21 |
22 | File.write!("api.ex", api_content)
23 |
24 | content_to_inject = """
25 |
26 | def something(), do: :something
27 | """
28 |
29 | expected_api_content = """
30 | defmodule API do
31 | def whatever(), do: :whatever
32 |
33 | def something(), do: :something
34 | end
35 | """
36 |
37 | assert :ok = FileAPI.inject_content_before_module_end(content_to_inject, "api.ex")
38 |
39 | assert_received {:mix_shell, :info, ["* injecting api.ex"]}
40 |
41 | assert_file("api.ex", fn file ->
42 | assert file === expected_api_content
43 | end)
44 | end)
45 | end
46 |
47 | test "doesn't inject the content when already present in the module", config do
48 | in_tmp(config.test, fn ->
49 | content_to_inject = """
50 |
51 | def something(), do: :something
52 | """
53 |
54 | expected_api_content = """
55 | defmodule API do
56 | def whatever(), do: :whatever
57 |
58 | def something(), do: :something
59 | end
60 | """
61 |
62 | File.write!("api.ex", expected_api_content)
63 |
64 | assert {:noop, :content_to_inject_already_exists} =
65 | FileAPI.inject_content_before_module_end(content_to_inject, "api.ex")
66 |
67 | refute_received {:mix_shell, :info, ["* injecting api.ex"]}
68 |
69 | assert_file("api.ex", fn file ->
70 | assert file === expected_api_content
71 | end)
72 | end)
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/assets/svg/github-sponsor.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain/resource/generate_actions/generate_actions_resource.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.GenerateActions.GenerateActionsResource do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.BindingAPI
5 | alias ElixirScribe.Template.FileAPI
6 | alias ElixirScribe.Generator.DomainResourceAPI
7 | alias ElixirScribe.Generator.DomainContract
8 |
9 | def generate(%DomainContract{} = contract) do
10 | base_template_paths = ElixirScribe.base_template_paths()
11 |
12 | binding = BindingAPI.build_binding_template(contract)
13 |
14 | for {:eex, :resource, source_path, target_path, action} <-
15 | DomainResourceAPI.build_action_files_paths(contract) do
16 | binding = BindingAPI.rebuild_binding_template(binding, action, file_type: :lib_core)
17 |
18 | target_path = Regex.replace(~r/^default/, target_path, action)
19 |
20 | # When the file already exists we are asked if we want to overwrite it.
21 | created_or_overwritten? =
22 | create_action_module_file(
23 | base_template_paths,
24 | target_path,
25 | binding,
26 | contract.schema.generate?
27 | )
28 |
29 | if created_or_overwritten? do
30 | inject_action_function_into_module(base_template_paths, source_path, target_path, binding)
31 | end
32 | end
33 |
34 | contract
35 | end
36 |
37 | defp create_action_module_file(base_template_paths, target_path, binding, schema_generate?) do
38 | module_template_path = build_module_template_path(schema_generate?)
39 | content = Mix.Phoenix.eval_from(base_template_paths, module_template_path, binding)
40 |
41 | Mix.Generator.create_file(target_path, content)
42 | end
43 |
44 | defp build_module_template_path(true) do
45 | ElixirScribe.resource_actions_template_path()
46 | |> Path.join("action_module.ex")
47 | end
48 |
49 | defp build_module_template_path(false) do
50 | ElixirScribe.resource_actions_template_path()
51 | |> Path.join("action_module_no_schema_access.ex")
52 | end
53 |
54 | defp inject_action_function_into_module(base_template_paths, source_path, target_path, binding) do
55 | FileAPI.inject_eex_template_before_module_end(
56 | base_template_paths,
57 | source_path,
58 | target_path,
59 | binding
60 | )
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain/resource/generate_tests/generate_tests_resource.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.GenerateTests.GenerateTestsResource do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.BindingAPI
5 | alias ElixirScribe.Generator.DomainResourceAPI
6 | alias ElixirScribe.Generator.DomainContract
7 | alias ElixirScribe.Template.FileAPI
8 |
9 | def generate(%DomainContract{} = contract) do
10 | base_template_paths = ElixirScribe.base_template_paths()
11 | binding = BindingAPI.build_binding_template(contract)
12 |
13 | for {:eex, :resource_test, source_path, target_path, action} <-
14 | DomainResourceAPI.build_test_action_files_paths(contract) do
15 | binding =
16 | BindingAPI.rebuild_binding_template(binding, action, file_type: :lib_core)
17 |
18 | # When the file already exists we are asked if we want to overwrite it.
19 | created_or_overwritten? =
20 | create_test_action_module_file(
21 | base_template_paths,
22 | target_path,
23 | binding,
24 | contract.schema.generate?
25 | )
26 |
27 | if created_or_overwritten? do
28 | inject_action_function_into_module(
29 | base_template_paths,
30 | source_path,
31 | target_path,
32 | binding
33 | )
34 | end
35 | end
36 |
37 | contract
38 | end
39 |
40 | defp create_test_action_module_file(base_template_paths, target_path, binding, schema_generate?) do
41 | module_template_path = build_module_template_path(schema_generate?)
42 | content = Mix.Phoenix.eval_from(base_template_paths, module_template_path, binding)
43 |
44 | Mix.Generator.create_file(target_path, content)
45 | end
46 |
47 | defp build_module_template_path(true) do
48 | ElixirScribe.resource_test_actions_template_path()
49 | |> Path.join("action_module_test.exs")
50 | end
51 |
52 | defp build_module_template_path(false) do
53 | ElixirScribe.resource_test_actions_template_path()
54 | |> Path.join("action_module_test_no_schema_access.exs")
55 | end
56 |
57 | defp inject_action_function_into_module(base_template_paths, source_path, target_path, binding) do
58 | FileAPI.inject_eex_template_before_module_end(
59 | base_template_paths,
60 | source_path,
61 | target_path,
62 | binding
63 | )
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/binding_api_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.BindingAPITest do
2 | alias ElixirScribe.Template.BindingAPI
3 | use ElixirScribe.BaseCase, async: true
4 |
5 | # Tests in the API module only care about testing the function can be invoked and that the API contract is respected for guards, pattern matching and expected return types. The unit tests for the functionality are done in their respective modules.
6 |
7 | describe "build_binding_template/1" do
8 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (list)" do
9 | contract = domain_contract_fixture()
10 |
11 | assert BindingAPI.build_binding_template(contract) |> is_list()
12 | end
13 |
14 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (%DomainContract{})" do
15 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
16 | BindingAPI.build_binding_template(%{})
17 | end
18 | end
19 | end
20 |
21 | describe "rebuild_binding_template/1" do
22 | test "can be invoked with the correct arguments type (list, string, list) and returns the expected type (list)" do
23 | contract = domain_contract_fixture()
24 |
25 | binding = BindingAPI.build_binding_template(contract)
26 |
27 | assert BindingAPI.rebuild_binding_template(
28 | binding,
29 | "whatever_action",
30 | file_type: :lib_core
31 | )
32 | |> is_list()
33 | end
34 |
35 | test "raises a FunctionClauseError when isn't invoked with the correct first argument type (list)" do
36 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
37 | BindingAPI.rebuild_binding_template(%{}, "", [])
38 | end
39 | end
40 |
41 | test "raises a FunctionClauseError when isn't invoked with the correct second argument type (list)" do
42 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
43 | BindingAPI.rebuild_binding_template([], 1, [])
44 | end
45 | end
46 |
47 | test "raises a FunctionClauseError when isn't invoked with the correct third argument type (list)" do
48 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
49 | BindingAPI.rebuild_binding_template([], "", %{})
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.MixProject do
2 | use Mix.Project
3 |
4 | # The Elixir version here needs to be equal or greater then the one
5 | # used by the Phoenix Framework and Phoenix Installer.
6 | @elixir_requirement "~> 1.14"
7 | @scm_url "https://github.com/exadra37/elixir-scribe"
8 |
9 | @description """
10 | Elixir Scribe - A Mix Task to Generate Elixir and Phoenix Projects
11 |
12 | The Elixir Scribe tool aims to help developers to more easily write clean code in a clean software architecture for enhanced developer experience and productivity.
13 | """
14 |
15 | def project do
16 | [
17 | app: :elixir_scribe,
18 | name: "Elixir Scribe",
19 | version: "0.3.0",
20 | elixir: @elixir_requirement,
21 | elixirc_paths: elixirc_paths(Mix.env()),
22 | start_permanent: Mix.env() == :prod,
23 | homepage_url: @scm_url,
24 | source_url: @scm_url,
25 | description: @description,
26 | package: package(),
27 | docs: docs(),
28 | aliases: aliases(),
29 | deps: deps()
30 | ]
31 | end
32 |
33 | # Run "mix help compile.app" to learn about applications.
34 | def application do
35 | [
36 | extra_applications: [:logger]
37 | ]
38 | end
39 |
40 | # Specifies which paths to compile per environment.
41 | defp elixirc_paths(:test), do: ["lib", "test/support"]
42 | defp elixirc_paths(_), do: ["lib"]
43 |
44 | defp package do
45 | [
46 | maintainers: ["Paulo Renato (Exadra37)"],
47 | licenses: ["MIT"],
48 | links: %{"GitHub" => @scm_url},
49 | files: ~w(lib priv LICENSE.md mix.exs README.md .formatter.exs),
50 | exclude_patterns: [".local"]
51 | ]
52 | end
53 |
54 | defp docs() do
55 | [
56 | main: "readme",
57 | extras: ["README.md"]
58 | # groups_for_extras: [
59 | # Examples: ~r"test/examples"
60 | # ],
61 | ]
62 | end
63 |
64 | defp aliases do
65 | [docs: ["docs", ©_images/1]]
66 | end
67 |
68 | defp copy_images(_) do
69 | File.cp_r!("assets", "doc/assets")
70 | end
71 |
72 | # Run "mix help deps" to learn about dependencies.
73 | defp deps do
74 | [
75 | # {:dep_from_hexpm, "~> 0.3.0"},
76 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
77 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
78 | {:phoenix, ">= 1.7.0"},
79 | {:phoenix_ecto, "~> 4.4"},
80 | {:norm, "~> 0.13"},
81 | {:assertions, "0.19.0", only: [:test]}
82 | ]
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/test/elixir_scribe/mix/shell/prompt_for_file_conflicts/prompt_for_file_conflicts_shell_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("test/mix_test_helper.exs")
2 |
3 | defmodule ElixirScribe.Mix.Shell.PromptForFileConflicts.PromptForFileConflictsShellTest do
4 | alias ElixirScribe.MixAPI
5 | use ElixirScribe.BaseCase
6 |
7 | import MixTestHelper
8 |
9 | setup do
10 | Mix.Task.clear()
11 | :ok
12 | end
13 |
14 | test "it prompts for file conflicts when the file already exists", config do
15 | in_tmp_project(config.test, fn ->
16 | files = [
17 | {:eex, "from/schema_template", "schema.ex"},
18 | {:eex, :api, "from/api_template_file", "api_file.ex"},
19 | {:eex, :resource, "from/resource_template_file", "create_resource.ex", "create"}
20 | ]
21 |
22 | File.touch("schema.ex")
23 | File.touch("api_file.ex")
24 | File.touch("create_resource.ex")
25 |
26 | expected_info = """
27 | The following files conflict with new files to be generated:
28 |
29 | * schema.ex
30 | * api_file.ex
31 | * create_resource.ex
32 |
33 | """
34 |
35 | send(self(), {:mix_shell_input, :yes?, true})
36 | MixAPI.prompt_for_file_conflicts(files)
37 |
38 | assert_received {:mix_shell, :info, [^expected_info]}
39 |
40 | assert_received {:mix_shell, :yes?, ["Proceed with interactive overwrite?" <> _]}
41 | end)
42 | end
43 |
44 | test "doesn't prompt for file conflicts when the file is :new_eex", config do
45 | in_tmp_project(config.test, fn ->
46 | files = [
47 | {:new_eex, "from/some_template", "new_eex_ignored.ex"}
48 | ]
49 |
50 | File.touch("new_eex_ignored.ex")
51 |
52 | expected_info = """
53 | The following files conflict with new files to be generated:
54 |
55 | * new_eex_ignored.ex
56 |
57 | """
58 |
59 | send(self(), {:mix_shell_input, :yes?, true})
60 | MixAPI.prompt_for_file_conflicts(files)
61 |
62 | refute_received {:mix_shell, :info, [^expected_info]}
63 |
64 | refute_received {:mix_shell, :yes?, ["Proceed with interactive overwrite?" <> _]}
65 | end)
66 | end
67 |
68 | test "doesn't prompt for file conflicts when the file doesn't exist", config do
69 | in_tmp_project(config.test, fn ->
70 | file = "new.ex"
71 |
72 | MixAPI.prompt_for_file_conflicts([{:ex, :any, file}])
73 |
74 | refute_received {:mix_shell, :info,
75 | ["The following files conflict with new files to be generated:" <> _]}
76 |
77 | refute_received {:mix_shell_input, :yes?, true}
78 | end)
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/examples/scribe.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -eu
4 |
5 | Maybe_Create_App() {
6 | local _app_dir="${TARGET_DIR}/${APP_DIRNAME}"
7 |
8 | if [ -d "${_app_dir}" ]; then
9 | cd "${_app_dir}"
10 | git stash -u
11 | else
12 | mkdir -p "${TARGET_DIR}"
13 | cd "${TARGET_DIR}"
14 | mix phx.new "${APP_DIRNAME}" --database sqlite3
15 |
16 | echo "-> Add the Elixir Scribe dependency."
17 | echo "-> Run mix deps.get."
18 | echo "-> Run mix compile."
19 | echo "-> Run mix ecto.setup."
20 | echo "-> Run git init and commit it all."
21 | echo "-> You can now use ./examples/.scribe.sh example-here"
22 |
23 | exit 0
24 | fi
25 | }
26 |
27 | Todo_App() {
28 | # sleep .5 is required to ensure each migration has a different datetime
29 |
30 | mix scribe.gen.html Todo Task tasks title:string done:boolean --no-schema
31 | sleep .5
32 |
33 | mix scribe.gen.html Todo Tag tags title:string desc:string
34 | sleep .5
35 |
36 | mix scribe.gen.html Acounts User users name:string email:string
37 | }
38 |
39 | Shop_App() {
40 | # sleep .5 is required to ensure each migration has a different datetime
41 |
42 | # Sales Catalog
43 | mix scribe.gen.html Sales.Catalog Category categories name:string desc:string
44 | sleep .5
45 |
46 | mix scribe.gen.html Sales.Catalog Product products sku:string name:string desc:string price:integer vat:integer --actions import,export
47 | sleep .5
48 |
49 | mix scribe.gen.html Sales.Catalog Cart carts total_amount:integer total_quantity:integer products_skus:array:string --actions report
50 | sleep .5
51 |
52 | # Sales Checkout
53 | mix scribe.gen.domain Sales.Checkout CheckoutProduct checkout_products sku:string name:string desc:string --no-default-actions --actions build
54 | sleep .5
55 |
56 | mix scribe.gen.html Sales.Checkout Order orders total_amount:integer total_quantity:integer products_skus:array:string cart_uuid:string shipping_uuid:string --actions report
57 | sleep .5
58 |
59 | # Warehouse Fulfillment
60 | mix scribe.gen.html Warehouse.Fulfillment FulfillmentProduct fulfillment_products sku:string label:string total_quantity:integer location:string --no-default-actions --actions build
61 | sleep .5
62 |
63 | mix scribe.gen.html Warehouse.Shipment Parcel parcels pickup_datetime:datetime label:string carrier_uuid:string
64 | }
65 |
66 | Main() {
67 | export MIX_ENV=dev
68 |
69 | local APP_DIRNAME="my_app"
70 | local TARGET_DIR=".local"
71 |
72 | Maybe_Create_App
73 |
74 | for input in "${@}"; do
75 | case "${input}" in
76 | --dir )
77 | shift 1
78 | TARGET_DIR="${1}"
79 | ;;
80 |
81 | todo )
82 | Todo_App
83 | exit $?
84 | ;;
85 |
86 | shop )
87 | Shop_App
88 | exit $?
89 | ;;
90 | esac
91 | done
92 | }
93 |
94 | Main "${@}"
95 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/template/route/scope/scope_action_routes.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Route.Scope.ScopeActionRoutes do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.ModuleAPI
5 | alias ElixirScribe.Generator.DomainContract
6 |
7 | def scope(%DomainContract{schema: schema} = contract) do
8 | resource = schema.plural
9 | controller = inspect(contract.web_resource_module)
10 | scope_alias = String.replace(schema.web_path, "/", "_") <> "_" <> resource
11 |
12 | """
13 |
14 | scope "/#{schema.web_path}/#{resource}", #{controller}, as: :#{scope_alias} do
15 | pipe_through :browser
16 |
17 | #{build_action_routes(contract)}
18 | end
19 | """
20 | end
21 |
22 | defp build_action_routes(%DomainContract{} = contract) do
23 | resource_actions = contract.opts |> Keyword.get(:resource_actions)
24 |
25 | for action <- resource_actions, reduce: "" do
26 | routes ->
27 | routes <> build_route_action(action, contract)
28 | end
29 | |> String.trim_leading()
30 | end
31 |
32 | defp build_route_action("create", contract) do
33 | assemble_route_action("post", "create", contract)
34 | end
35 |
36 | defp build_route_action("update", contract) do
37 | assemble_route_action("patch", "update", contract) <>
38 | assemble_route_action("put", "update", contract)
39 | end
40 |
41 | defp build_route_action("delete", contract) do
42 | assemble_route_action("delete", "delete", contract)
43 | end
44 |
45 | defp build_route_action(action, contract) do
46 | assemble_route_action("get", action, contract)
47 | end
48 |
49 | defp assemble_route_action(method, action, contract) do
50 | action_alias = ElixirScribe.resource_action_alias(action)
51 | endpoint = build_endpoint(action, action_alias)
52 | controller = build_controller(contract, action, action_alias)
53 |
54 | "\n #{method} \"#{endpoint}\", #{controller}, :#{action_alias}"
55 | end
56 |
57 | @http_get_actions ["read", "new", "edit", "list"]
58 | @resource_id_actions ["read", "update", "delete"]
59 |
60 | defp build_endpoint("create", _action_alias), do: "/"
61 | defp build_endpoint("new", action_alias), do: "/#{action_alias}"
62 | defp build_endpoint("edit", action_alias), do: "/:id/#{action_alias}"
63 |
64 | defp build_endpoint(action, _action_alias) when action in @resource_id_actions,
65 | do: "/:id"
66 |
67 | defp build_endpoint(action, _action_alias) when action in @http_get_actions,
68 | do: "/"
69 |
70 | defp build_endpoint(_action, action_alias), do: "/#{action_alias}"
71 |
72 | defp build_controller(contract, action, action_alias) do
73 | action_capitalized = String.capitalize(action_alias)
74 | module_action_name = ModuleAPI.build_module_action_name(contract, action)
75 |
76 | "#{action_capitalized}.#{module_action_name}Controller"
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/module/build_name/build_absolute_module_name_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.Module.BuildName.BuildAbsoluteModuleNameTest do
2 | alias ElixirScribe.Template.ModuleAPI
3 | use ElixirScribe.BaseCase, async: true
4 |
5 | describe "absolute module name for core file" do
6 | test "builds correctly for `file_type: :resource`" do
7 | contract = domain_contract_fixture()
8 |
9 | assert ModuleAPI.build_absolute_module_name(contract, file_type: :resource) ===
10 | ElixirScribe.Site.Blog.Post
11 | end
12 |
13 | test "builds correctly for `file_type: :resource_test`" do
14 | contract = domain_contract_fixture()
15 |
16 | assert ModuleAPI.build_absolute_module_name(contract, file_type: :resource_test) ===
17 | ElixirScribe.Site.Blog.Post
18 | end
19 |
20 | test "builds correctly for `file_type: :lib_core`" do
21 | contract = domain_contract_fixture()
22 |
23 | assert ModuleAPI.build_absolute_module_name(contract, file_type: :lib_core) ===
24 | ElixirScribe.Site.Blog.Post
25 | end
26 |
27 | test "builds correctly for `file_type: :test_core`" do
28 | contract = domain_contract_fixture()
29 |
30 | assert ModuleAPI.build_absolute_module_name(contract, file_type: :test_core) ===
31 | ElixirScribe.Site.Blog.Post
32 | end
33 |
34 | test "it returns nil for when the file type is HTML" do
35 | contract = domain_contract_fixture()
36 |
37 | assert ModuleAPI.build_absolute_module_name(contract, file_type: :html) === nil
38 | end
39 | end
40 |
41 | describe "absolute module name for web file" do
42 | test "builds correctly for `file_type: :lib_web`" do
43 | contract = domain_contract_fixture()
44 |
45 | assert ModuleAPI.build_absolute_module_name(contract, file_type: :lib_web) ===
46 | ElixirScribeWeb.Site.Blog.Post
47 | end
48 |
49 | test "builds correctly for `file_type: :controller`" do
50 | contract = domain_contract_fixture()
51 |
52 | assert ModuleAPI.build_absolute_module_name(contract, file_type: :controller) ===
53 | ElixirScribeWeb.Site.Blog.Post
54 | end
55 |
56 | test "builds correctly for `file_type: :controller_test`" do
57 | contract = domain_contract_fixture()
58 |
59 | assert ModuleAPI.build_absolute_module_name(contract, file_type: :controller_test) ===
60 | ElixirScribeWeb.Site.Blog.Post
61 | end
62 |
63 | test "builds correctly for `file_type: :test_web`" do
64 | contract = domain_contract_fixture()
65 |
66 | assert ModuleAPI.build_absolute_module_name(contract, file_type: :test_web) ===
67 | ElixirScribeWeb.Site.Blog.Post
68 | end
69 | end
70 |
71 | test "it returns nil for `file_type: :test_web`" do
72 | contract = domain_contract_fixture()
73 |
74 | assert ModuleAPI.build_absolute_module_name(contract, file_type: :html) === nil
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/utils/string/camel_case_to_sentence/camel_case_to_sentence.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.String.CamelCaseToSentence do
2 | @moduledoc false
3 |
4 | # Regex Components:
5 | # * (?<=[A-Z])(?=[A-Z][a-z]) - Matches a position after an uppercase letter
6 | # and before a sequence of an uppercase letter followed by a lowercase
7 | # letter. Example: In "ISBNFiles", this matches between "ISBN" and "Files".
8 | # * (?<=[a-z])(?=[A-Z]) - Matches a position after a lowercase letter and
9 | # before an uppercase letter. Example: In "MySuperVariable", this matches
10 | # between "My" and "Super".
11 |
12 | @doc """
13 | Converts a camel case word to a capitalized sentence and preserves acronyms.
14 |
15 | ## Examples
16 | iex> CamelCaseToSentence.convert("ISBNFiles", :capitalized)
17 | "ISBN Files"
18 |
19 | iex> CamelCaseToSentence.convert("CamelCaseToSentence", :capitalized)
20 | "Camel Case To Sentence"
21 |
22 | iex> CamelCaseToSentence.convert("ABCD", :capitalized)
23 | "ABCD"
24 | """
25 | def convert(camel_case_word, :capitalized)
26 | when is_binary(camel_case_word) and byte_size(camel_case_word) > 0 do
27 | camel_case_word
28 | |> String.replace(~r/(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-z])(?=[A-Z])/, " ")
29 | end
30 |
31 | @doc """
32 | Translates a camel case word into a sentence, preserving acronyms, with only
33 | the first word capitalized.
34 |
35 | ## Examples
36 |
37 | iex> CamelCaseToSentence.convert("HTTPRequestURLParser")
38 | "HTTP request URL parser"
39 |
40 | iex> CamelCaseToSentence.convert("XMLToJSON")
41 | "XML to JSON"
42 |
43 | iex> CamelCaseToSentence.convert("JSONAPIHandler")
44 | "JSONAPI handler"
45 |
46 | iex> CamelCaseToSentence.convert("SimpleExample")
47 | "Simple example"
48 | """
49 | def convert(camel_case_word)
50 | when is_binary(camel_case_word) and byte_size(camel_case_word) > 0 do
51 | camel_case_word
52 | |> split_camel_case_into_words()
53 | |> format_words()
54 | |> Enum.join(" ")
55 | end
56 |
57 | defp split_camel_case_into_words(word) do
58 | # Regular expression for splitting camel case while preserving acronyms
59 | regex = ~r/(?<=[a-z])(?=[A-Z])|(?<=[A-Z]{2})(?=[A-Z][a-z])/
60 |
61 | String.split(word, regex)
62 | end
63 |
64 | defp format_words(words) do
65 | words
66 | |> Enum.with_index()
67 | |> Enum.map(fn
68 | {first_word, 0} -> capitalize_sentence_first_word(first_word)
69 | {remaining_words, _} -> downcase_rest_of_sentence(remaining_words)
70 | end)
71 | end
72 |
73 | defp capitalize_sentence_first_word(word) do
74 | if acronym?(word), do: word, else: String.capitalize(word)
75 | end
76 |
77 | defp downcase_rest_of_sentence(word) do
78 | if acronym?(word), do: word, else: String.downcase(word)
79 | end
80 |
81 | defp acronym?(word) do
82 | String.upcase(word) == word
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain/resource/generate_api/generate_api_resource.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.GenerateApi.GenerateApiResource do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.ModuleAPI
5 | alias ElixirScribe.Template.BindingAPI
6 | alias ElixirScribe.Template.BuildFilenameForActionFileContract
7 | alias ElixirScribe.Generator.DomainContract
8 | alias ElixirScribe.Template.FileAPI
9 | alias ElixirScribe.Generator.DomainResourceAPI
10 |
11 | def generate(%DomainContract{} = contract) do
12 | base_template_paths = ElixirScribe.base_template_paths()
13 | binding = BindingAPI.build_binding_template(contract)
14 |
15 | contract
16 | |> ensure_api_file_exists(base_template_paths, binding)
17 | |> inject_api_functions(base_template_paths, binding)
18 |
19 | contract
20 | end
21 |
22 | defp build_api_binding(contract, binding) do
23 | Keyword.merge(binding,
24 | absolute_module_name: ModuleAPI.build_absolute_module_name(contract, file_type: :lib_core),
25 | aliases:
26 | ModuleAPI.build_absolute_module_action_name_aliases(contract,
27 | file_type: :lib_core
28 | )
29 | )
30 | end
31 |
32 | defp ensure_api_file_exists(contract, base_template_paths, binding) do
33 | binding = build_api_binding(contract, binding)
34 |
35 | unless File.exists?(contract.api_file) do
36 | {:eex, :api, source_path, target_path} = DomainResourceAPI.build_api_file_paths(contract)
37 |
38 | Mix.Generator.create_file(
39 | target_path,
40 | Mix.Phoenix.eval_from(base_template_paths, source_path, binding)
41 | )
42 | end
43 |
44 | contract
45 | end
46 |
47 | defp inject_api_functions(contract, base_template_paths, binding) do
48 | resource_actions = contract.opts |> Keyword.get(:resource_actions)
49 |
50 | for action <- resource_actions do
51 | binding =
52 | build_api_binding(contract, binding)
53 | |> BindingAPI.rebuild_binding_template(action, file_type: :lib_core)
54 |
55 | api_action_template_path = build_api_action_template_path(action, contract.schema.generate?)
56 |
57 | FileAPI.inject_eex_template_before_module_end(
58 | base_template_paths,
59 | api_action_template_path,
60 | contract.api_file,
61 | binding
62 | )
63 | end
64 | end
65 |
66 | defp build_api_action_template_path(action, true) do
67 | attrs = %{
68 | action: action,
69 | action_suffix: "_",
70 | file_type: "api_function",
71 | file_extension: ".ex"
72 | }
73 |
74 | contract = BuildFilenameForActionFileContract.new!(attrs)
75 |
76 | action_template_filename = FileAPI.build_template_action_filename(contract)
77 |
78 | ElixirScribe.domain_api_template_path() |> Path.join(action_template_filename)
79 | end
80 |
81 | defp build_api_action_template_path(_action, false) do
82 | ElixirScribe.domain_api_template_path()
83 | |> Path.join("api_function_no_schema_access.ex")
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain_contract.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.DomainContract do
2 | @moduledoc false
3 |
4 | # This contract mirrors the Mix.Phoenix.Context module from the Phoenix
5 | # Framework with some added fields to suite ElixirScribe needs.
6 |
7 | @required [
8 | :name,
9 | :module,
10 | :resource_module,
11 | :resource_module_plural,
12 | :schema,
13 | :alias,
14 | :base_module,
15 | :web_module,
16 | :web_domain_module,
17 | :web_resource_module,
18 | :web_resource_module_plural,
19 | :basename,
20 | :api_file,
21 | :test_file,
22 | :test_fixtures_file,
23 | :lib_web_domain_dir,
24 | :lib_domain_dir,
25 | :lib_web_resource_dir,
26 | :lib_web_resource_dir_plural,
27 | :lib_resource_dir,
28 | :lib_resource_dir_plural,
29 | :test_web_domain_dir,
30 | :test_domain_dir,
31 | :test_web_resource_dir,
32 | :test_web_resource_dir_plural,
33 | :test_resource_dir,
34 | :test_resource_dir_plural,
35 | :context_app,
36 | :resource_path_name_singular,
37 | :resource_path_name_plural,
38 | :resource_actions
39 | ]
40 |
41 | @optional [
42 | generate?: true,
43 | opts: []
44 | ]
45 |
46 | use ElixirScribe.Behaviour.TypedContract, fields: %{required: @required, optional: @optional}
47 |
48 | @impl true
49 | def type_spec() do
50 | schema(%__MODULE__{
51 | name: is_binary() |> spec(),
52 | module: is_atom() |> spec(),
53 | resource_module: is_atom() |> spec(),
54 | resource_module_plural: is_atom() |> spec(),
55 | schema: spec(is_struct() or is_nil()),
56 | alias: is_atom() |> spec(),
57 | base_module: is_atom() |> spec(),
58 | web_module: is_atom() |> spec(),
59 | web_domain_module: is_atom() |> spec(),
60 | web_resource_module: is_atom() |> spec(),
61 | web_resource_module_plural: is_atom() |> spec(),
62 | basename: is_binary() |> spec(),
63 | api_file: is_binary() |> spec(),
64 | test_file: is_binary() |> spec(),
65 | test_fixtures_file: is_binary() |> spec(),
66 | lib_web_domain_dir: is_binary() |> spec(),
67 | lib_domain_dir: is_binary() |> spec(),
68 | lib_web_resource_dir: is_binary() |> spec(),
69 | lib_web_resource_dir_plural: is_binary() |> spec(),
70 | lib_resource_dir: is_binary() |> spec(),
71 | lib_resource_dir_plural: is_binary() |> spec(),
72 | test_web_domain_dir: is_binary() |> spec(),
73 | test_domain_dir: is_binary() |> spec(),
74 | test_web_resource_dir: is_binary() |> spec(),
75 | test_web_resource_dir_plural: is_binary() |> spec(),
76 | test_resource_dir: is_binary() |> spec(),
77 | test_resource_dir_plural: is_binary() |> spec(),
78 | generate?: is_boolean() |> spec(),
79 | context_app: is_atom() |> spec(),
80 | resource_actions: is_list() |> spec(),
81 | resource_path_name_singular: is_binary() |> spec(),
82 | resource_path_name_plural: is_binary() |> spec(),
83 | opts: is_list() |> spec()
84 | })
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/test/elixir_scribe/utils/string_api_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Utils.StringAPITest do
2 | alias ElixirScribe.Utils.StringAPI
3 | use ElixirScribe.BaseCase, async: true
4 |
5 | # INFO: Tests in the API module only care about testing the function can be invoked and that the API contract is respected (for now only guards and pattern matching). The unit tests for the functionality are done in their respective modules
6 |
7 | describe "capitalize/1" do
8 | test "can be invoked and returns a string" do
9 | assert StringAPI.capitalize("whatever") |> is_binary()
10 | end
11 |
12 | test "raises a FunctionClauseError when the argument isn't a string" do
13 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
14 | StringAPI.capitalize(123)
15 | end
16 | end
17 | end
18 |
19 | describe "capitalize/2" do
20 | test "can be invoked and returns a string" do
21 | assert StringAPI.capitalize("whatever word", "") |> is_binary()
22 | end
23 |
24 | test "raises a FunctionClauseError when the first arguments isn't a string" do
25 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
26 | StringAPI.capitalize(:whatever, "")
27 | end
28 | end
29 |
30 | test "raises a FunctionClauseError when the joiner (2nd arg) isn't a string" do
31 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
32 | StringAPI.capitalize("whatever word", :bad_joiner)
33 | end
34 | end
35 | end
36 |
37 | describe "human_capitalize/1" do
38 | test "can be invoked and returns a string" do
39 | assert StringAPI.capitalize("whatever word") |> is_binary()
40 | end
41 |
42 | test "raises a FunctionClauseError when the argument isn't a string" do
43 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
44 | StringAPI.capitalize(:whatever)
45 | end
46 | end
47 | end
48 |
49 | describe "first_word/1" do
50 | test "can be invoked and returns a string" do
51 | assert StringAPI.first_word("whatever word") |> is_binary()
52 | end
53 |
54 | test "raises a FunctionClauseError when the argument isn't a string" do
55 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
56 | StringAPI.first_word(:whatever)
57 | end
58 | end
59 | end
60 |
61 | describe "first_word/2" do
62 | test "can be invoked and returns a string" do
63 | assert StringAPI.first_word("whatever_word") |> is_binary()
64 | end
65 |
66 | test "raises a FunctionClauseError when the first argument isn't a string" do
67 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
68 | StringAPI.first_word(:whatever_word)
69 | end
70 | end
71 |
72 | test "raises a FunctionClauseError when the second argument (word_separators) isn't a list" do
73 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
74 | StringAPI.first_word(:whatever_word, "")
75 | end
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/schema_contract.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.SchemaContract do
2 | @moduledoc false
3 |
4 | # This module was borrowed from the Phoenix Framework module
5 | # Mix.Phoenix.Schema and modified to suite ElixirScribe needs.
6 |
7 | @optional [
8 | module: nil,
9 | repo: nil,
10 | repo_alias: nil,
11 | table: nil,
12 | collection: nil,
13 | embedded?: false,
14 | generate?: true,
15 | opts: [],
16 | alias: nil,
17 | alias_plural: nil,
18 | file: nil,
19 | attrs: [],
20 | string_attr: nil,
21 | plural: nil,
22 | singular: nil,
23 | uniques: [],
24 | redacts: [],
25 | assocs: [],
26 | types: [],
27 | indexes: [],
28 | defaults: [],
29 | human_singular: nil,
30 | human_plural: nil,
31 | binary_id: false,
32 | migration_defaults: nil,
33 | migration?: false,
34 | migration_dir: nil,
35 | params: %{},
36 | optionals: [],
37 | sample_id: nil,
38 | web_path: nil,
39 | web_namespace: nil,
40 | context_app: nil,
41 | route_helper: nil,
42 | route_prefix: nil,
43 | api_route_prefix: nil,
44 | migration_module: nil,
45 | fixture_unique_functions: [],
46 | fixture_params: [],
47 | prefix: nil,
48 | timestamp_type: :naive_datetime
49 | ]
50 |
51 | use ElixirScribe.Behaviour.TypedContract, fields: %{required: [], optional: @optional}
52 |
53 | @impl true
54 | def type_spec() do
55 | schema(%__MODULE__{
56 | module: is_atom() |> spec(),
57 | repo: is_atom() |> spec(),
58 | repo_alias: is_binary() |> spec(),
59 | table: is_binary() |> spec(),
60 | collection: is_binary() |> spec(),
61 | embedded?: is_boolean() |> spec(),
62 | generate?: is_boolean() |> spec(),
63 | opts: [],
64 | alias: is_atom() |> spec(),
65 | alias_plural: is_atom() |> spec(),
66 | file: is_binary() |> spec(),
67 | attrs: [],
68 | string_attr: is_atom() |> spec(),
69 | plural: is_binary() |> spec(),
70 | singular: is_binary() |> spec(),
71 | uniques: [],
72 | redacts: [],
73 | assocs: [],
74 | types: [],
75 | indexes: [],
76 | defaults: [],
77 | human_singular: is_binary() |> spec(),
78 | human_plural: is_binary() |> spec(),
79 | binary_id: is_boolean() |> spec(),
80 | migration_defaults: is_map() |> spec(),
81 | migration?: is_boolean() |> spec(),
82 | migration_dir: is_binary() |> spec(),
83 | params: %{},
84 | optionals: [],
85 | sample_id: is_binary() |> spec(),
86 | web_path: is_binary() |> spec(),
87 | web_namespace: is_binary() |> spec(),
88 | context_app: is_atom() |> spec(),
89 | route_helper: is_binary() |> spec(),
90 | route_prefix: is_binary() |> spec(),
91 | api_route_prefix: is_binary() |> spec(),
92 | migration_module: is_atom() |> spec(),
93 | fixture_unique_functions: [],
94 | fixture_params: [],
95 | prefix: spec(is_binary() or is_nil()),
96 | timestamp_type: is_atom() |> spec()
97 | })
98 | end
99 | end
100 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain/resource/generate_test_fixture/generate_test_fixture_resource.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.GenerateTestFixture.GenerateTestFixtureResource do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Template.BindingAPI
5 | alias ElixirScribe.Generator.DomainContract
6 | alias ElixirScribe.Template.FileAPI
7 |
8 | def generate(%DomainContract{} = contract) do
9 | base_template_paths = ElixirScribe.base_template_paths()
10 | binding = BindingAPI.build_binding_template(contract)
11 |
12 | contract
13 | |> ensure_test_fixtures_file_exists(base_template_paths, binding)
14 | |> inject_test_fixture(base_template_paths, binding)
15 |
16 | contract
17 | end
18 |
19 | defp ensure_test_fixtures_file_exists(
20 | %DomainContract{test_fixtures_file: test_fixtures_file} = contract,
21 | base_template_paths,
22 | binding
23 | ) do
24 | unless File.exists?(test_fixtures_file) do
25 | fixtures_module_template_path =
26 | ElixirScribe.domain_tests_template_path() |> Path.join("fixtures_module.ex")
27 |
28 | Mix.Generator.create_file(
29 | test_fixtures_file,
30 | Mix.Phoenix.eval_from(base_template_paths, fixtures_module_template_path, binding)
31 | )
32 | end
33 |
34 | contract
35 | end
36 |
37 | defp inject_test_fixture(
38 | %DomainContract{test_fixtures_file: test_fixtures_file, opts: opts} = contract,
39 | base_template_paths,
40 | binding
41 | ) do
42 | no_default_actions = Keyword.get(opts, :no_default_actions, false)
43 |
44 | unless no_default_actions do
45 | fixtures_file_template_path =
46 | ElixirScribe.domain_tests_template_path() |> Path.join("fixtures.ex")
47 |
48 | FileAPI.inject_eex_template_before_module_end(
49 | base_template_paths,
50 | fixtures_file_template_path,
51 | test_fixtures_file,
52 | binding
53 | )
54 |
55 | maybe_print_unimplemented_fixture_functions(contract)
56 | end
57 | end
58 |
59 | defp maybe_print_unimplemented_fixture_functions(%DomainContract{} = contract) do
60 | fixture_functions_needing_implementations =
61 | Enum.flat_map(
62 | contract.schema.fixture_unique_functions,
63 | fn
64 | {_field, {_function_name, function_def, true}} -> [function_def]
65 | {_field, {_function_name, _function_def, false}} -> []
66 | end
67 | )
68 |
69 | if Enum.any?(fixture_functions_needing_implementations) do
70 | Mix.shell().info("""
71 |
72 | Some of the generated database columns are unique. Please provide
73 | unique implementations for the following fixture function(s) in
74 | #{contract.test_fixtures_file}:
75 |
76 | #{fixture_functions_needing_implementations |> Enum.map_join(&indent(&1, 2)) |> String.trim_trailing()}
77 | """)
78 | end
79 |
80 | contract
81 | end
82 |
83 | defp indent(string, spaces) do
84 | indent_string = String.duplicate(" ", spaces)
85 |
86 | string
87 | |> String.split("\n")
88 | |> Enum.map_join(fn line ->
89 | if String.trim(line) == "" do
90 | "\n"
91 | else
92 | indent_string <> line <> "\n"
93 | end
94 | end)
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/file/inject/inject_eex_template_before_module_end_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("test/mix_test_helper.exs")
2 |
3 | defmodule ElixirScribe.Template.File.Inject.InjectEExTemplateBeforeModuleEndTest do
4 | alias ElixirScribe.Template.BindingAPI
5 | alias ElixirScribe.Template.FileAPI
6 | use ElixirScribe.BaseCase
7 |
8 | import MixTestHelper
9 |
10 | setup do
11 | Mix.Task.clear()
12 | :ok
13 | end
14 |
15 | test "it injects an EEX template before the module end tag", config do
16 | in_tmp_project(config.test, fn ->
17 | api_content = """
18 | defmodule API do
19 | end
20 | """
21 |
22 | File.write!("api_existing_module.ex", api_content)
23 |
24 | function_template = """
25 | def <%= action %>(), do: :<%= action %>
26 | """
27 |
28 | File.write!("api_function_template.ex", function_template)
29 |
30 | expected_api_content = """
31 | defmodule API do
32 | def create(), do: :create
33 | end
34 | """
35 |
36 | base_template_paths = ElixirScribe.base_template_paths()
37 |
38 | binding =
39 | domain_contract_fixture()
40 | |> BindingAPI.build_binding_template()
41 | |> BindingAPI.rebuild_binding_template("create",
42 | file_type: :lib_core
43 | )
44 |
45 | assert :ok =
46 | FileAPI.inject_eex_template_before_module_end(
47 | base_template_paths,
48 | "api_function_template.ex",
49 | "api_existing_module.ex",
50 | binding
51 | )
52 |
53 | assert_received {:mix_shell, :info, ["* injecting api_existing_module.ex"]}
54 |
55 | assert_file("api_existing_module.ex", fn file ->
56 | assert file === expected_api_content
57 | end)
58 | end)
59 | end
60 |
61 | test "doesn't inject an EEX template when already exists in the module", config do
62 | in_tmp_project(config.test, fn ->
63 | function_template = """
64 | def <%= action %>(), do: :<%= action %>
65 | """
66 |
67 | File.write!("api_function_template.ex", function_template)
68 |
69 | expected_api_content = """
70 | defmodule API do
71 | def create(), do: :create
72 | end
73 | """
74 |
75 | File.write!("api_existing_module.ex", expected_api_content)
76 |
77 | base_template_paths = ElixirScribe.base_template_paths()
78 |
79 | binding =
80 | domain_contract_fixture()
81 | |> BindingAPI.build_binding_template()
82 | |> BindingAPI.rebuild_binding_template("create",
83 | file_type: :lib_core
84 | )
85 |
86 | assert {:noop, :content_to_inject_already_exists} =
87 | FileAPI.inject_eex_template_before_module_end(
88 | base_template_paths,
89 | "api_function_template.ex",
90 | "api_existing_module.ex",
91 | binding
92 | )
93 |
94 | refute_received {:mix_shell, :info, ["* injecting api_existing_module.ex"]}
95 |
96 | assert_file("api_existing_module.ex", fn file ->
97 | assert file === expected_api_content
98 | end)
99 | end)
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/domain_resource_api.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.DomainResourceAPI do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Generator.Domain.Resource.GenerateSchema.GenerateSchemaResource
5 | alias ElixirScribe.Generator.DomainContract
6 | alias ElixirScribe.Generator.Domain.Resource.BuildContract.BuildDomainResourceContract
7 | alias ElixirScribe.Generator.Domain.Resource.BuildFilesToGenerate.BuildFilesToGenerateResource
8 | alias ElixirScribe.Generator.Domain.Resource.BuildAPIFilePaths.BuildAPIFilePathsResource
9 | alias ElixirScribe.Generator.Domain.Resource.GenerateActions.GenerateActionsResource
10 | alias ElixirScribe.Generator.Domain.Resource.GenerateTests.GenerateTestsResource
11 | alias ElixirScribe.Generator.Domain.Resource.BuildActionFilesPaths.BuildActionFilesPathsResource
12 |
13 | alias ElixirScribe.Generator.Domain.Resource.BuildTestActionFilesPaths.BuildTestActionFilesPathsResource
14 |
15 | alias ElixirScribe.Generator.Domain.Resource.GenerateApi.GenerateApiResource
16 | alias ElixirScribe.Generator.Domain.Resource.GenerateTestFixture.GenerateTestFixtureResource
17 | alias ElixirScribe.Generator.Domain.Resource.GenerateNewFiles.GenerateNewFilesResource
18 |
19 | @doc """
20 | Resource: Build DomainContract.
21 | """
22 | def build_domain_resource_contract(args, opts) when is_list(args) and is_list(opts),
23 | do: BuildDomainResourceContract.build(args, opts)
24 |
25 | def build_domain_resource_contract!(args, opts) when is_list(args) and is_list(opts),
26 | do: BuildDomainResourceContract.build!(args, opts)
27 |
28 | @doc """
29 | Resource: Files To Generate.
30 | """
31 | def build_files_to_generate(%DomainContract{} = contract),
32 | do: BuildFilesToGenerateResource.build(contract)
33 |
34 | @doc """
35 | Resource: Build Action Files To Generate
36 | """
37 | def build_action_files_paths(%DomainContract{} = contract),
38 | do: BuildActionFilesPathsResource.build(contract)
39 |
40 | @doc """
41 | Resource: Build Test Action Files To Generate
42 | """
43 | def build_test_action_files_paths(%DomainContract{} = contract),
44 | do: BuildTestActionFilesPathsResource.build(contract)
45 |
46 | def build_api_file_paths(%DomainContract{} = contract),
47 | do: BuildAPIFilePathsResource.build(contract)
48 |
49 | @doc """
50 | Resource: Generate Schema.
51 | """
52 | def generate_schema(%DomainContract{} = contract),
53 | do: GenerateSchemaResource.generate(contract)
54 |
55 | @doc """
56 | Resource: Generate Actions.
57 | """
58 | def generate_actions(%DomainContract{} = contract),
59 | do: GenerateActionsResource.generate(contract)
60 |
61 | @doc """
62 | Resource: Generate Tests.
63 | """
64 | def generate_tests(%DomainContract{} = contract),
65 | do: GenerateTestsResource.generate(contract)
66 |
67 | @doc """
68 | Resource: Generate Api.
69 | """
70 | def generate_api(%DomainContract{} = contract), do: GenerateApiResource.generate(contract)
71 |
72 | @doc """
73 | Resource: Generate Test Fixture.
74 | """
75 | def generate_test_fixture(%DomainContract{} = contract),
76 | do: GenerateTestFixtureResource.generate(contract)
77 |
78 | @doc """
79 | Resource: Generate New Files.
80 | """
81 | def generate_new_files(%DomainContract{} = contract, opts \\ []) when is_list(opts),
82 | do: GenerateNewFilesResource.generate(contract, opts)
83 | end
84 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/domain/resource/generate_actions/generate_actions_resource_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("test/mix_test_helper.exs")
2 |
3 | defmodule ElixirScribe.Generator.Domain.Resource.GenerateActions.GenerateActionsResourceTest do
4 | alias ElixirScribe.Generator.DomainResourceAPI
5 | use ElixirScribe.BaseCase
6 |
7 | import MixTestHelper
8 |
9 | setup do
10 | Mix.Task.clear()
11 | :ok
12 | end
13 |
14 | test "with flag --no-schema the resource action file is generated without the logic to access the schema",
15 | config do
16 | in_tmp_project(config.test, fn ->
17 | args = [
18 | "Blog",
19 | "Post",
20 | "posts",
21 | "slug:unique",
22 | "title:string",
23 | "--no-schema"
24 | ]
25 |
26 | domain_contract = domain_contract_fixture(args)
27 | DomainResourceAPI.generate_actions(domain_contract)
28 |
29 | assert_file("lib/elixir_scribe/domain/blog/post/list/list_posts.ex", fn file ->
30 | assert file =~ "defmodule ElixirScribe.Blog.Post.List.ListPosts do"
31 | assert file =~ "def list()"
32 | assert file =~ "raise \"TODO: Implement the action `list` for the module `ListPosts`"
33 | end)
34 | end)
35 | end
36 |
37 | test "generates a file for each Resource Action", config do
38 | in_tmp_project(config.test, fn ->
39 | args = [
40 | "Blog",
41 | "Post",
42 | "posts",
43 | "slug:unique",
44 | "secret:redact",
45 | "title:string",
46 | "--actions",
47 | "export,import"
48 | ]
49 |
50 | contract = domain_contract_fixture(args)
51 |
52 | DomainResourceAPI.generate_actions(contract)
53 |
54 | assert_file("lib/elixir_scribe/domain/blog/post/list/list_posts.ex", fn file ->
55 | assert file =~ "defmodule ElixirScribe.Blog.Post.List.ListPosts do"
56 | assert file =~ "def list()"
57 | end)
58 |
59 | assert_file("lib/elixir_scribe/domain/blog/post/new/new_post.ex", fn file ->
60 | assert file =~ "defmodule ElixirScribe.Blog.Post.New.NewPost do"
61 | assert file =~ "def new(attrs \\\\ %{})"
62 | end)
63 |
64 | assert_file("lib/elixir_scribe/domain/blog/post/read/read_post.ex", fn file ->
65 | assert file =~ "defmodule ElixirScribe.Blog.Post.Read.ReadPost do"
66 | assert file =~ "def read!(uuid)"
67 | end)
68 |
69 | assert_file("lib/elixir_scribe/domain/blog/post/edit/edit_post.ex", fn file ->
70 | assert file =~ "defmodule ElixirScribe.Blog.Post.Edit.EditPost do"
71 | assert file =~ "def edit(uuid, attrs \\\\ %{})"
72 | end)
73 |
74 | assert_file("lib/elixir_scribe/domain/blog/post/create/create_post.ex", fn file ->
75 | assert file =~ "defmodule ElixirScribe.Blog.Post.Create.CreatePost do"
76 | assert file =~ "def create(%{} = attrs)"
77 | end)
78 |
79 | assert_file("lib/elixir_scribe/domain/blog/post/update/update_post.ex", fn file ->
80 | assert file =~ "defmodule ElixirScribe.Blog.Post.Update.UpdatePost do"
81 | assert file =~ "def update(uuid, %{} = attrs)"
82 | end)
83 |
84 | assert_file("lib/elixir_scribe/domain/blog/post/delete/delete_post.ex", fn file ->
85 | assert file =~ "defmodule ElixirScribe.Blog.Post.Delete.DeletePost do"
86 | assert file =~ "def delete(uuid)"
87 | end)
88 | end)
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/domain/resource/generate_tests/generate_tests_resource_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("test/mix_test_helper.exs")
2 |
3 | defmodule ElixirScribe.Generator.Domain.Resource.GenerateTests.GenerateTestsResourceTest do
4 | alias ElixirScribe.Generator.DomainResourceAPI
5 | use ElixirScribe.BaseCase
6 |
7 | import MixTestHelper
8 |
9 | setup do
10 | Mix.Task.clear()
11 | :ok
12 | end
13 |
14 | test "with flag --no-schema the test file is generated without tests being implemented ",
15 | config do
16 | in_tmp_project(config.test, fn ->
17 | args = [
18 | "Blog",
19 | "Post",
20 | "posts",
21 | "slug:unique",
22 | "title:string",
23 | "--no-schema"
24 | ]
25 |
26 | domain_contract = domain_contract_fixture(args)
27 | DomainResourceAPI.generate_tests(domain_contract)
28 |
29 | assert_file("test/elixir_scribe/domain/blog/post/list/list_posts_test.exs", fn file ->
30 | assert file =~ "defmodule ElixirScribe.Blog.Post.List.ListPostsTest do"
31 | assert file =~ "test \"list"
32 | assert file =~ "raise \"TODO: Implement test for action `list` at `ListPosts`"
33 | end)
34 | end)
35 | end
36 |
37 | test "generates a test file for each Resource Action", config do
38 | in_tmp_project(config.test, fn ->
39 | args = [
40 | "Blog",
41 | "Post",
42 | "posts",
43 | "slug:unique",
44 | "secret:redact",
45 | "title:string",
46 | "--actions",
47 | "export,import"
48 | ]
49 |
50 | contract = domain_contract_fixture(args)
51 |
52 | DomainResourceAPI.generate_tests(contract)
53 |
54 | assert_file("test/elixir_scribe/domain/blog/post/list/list_posts_test.exs", fn file ->
55 | assert file =~ "defmodule ElixirScribe.Blog.Post.List.ListPostsTest do"
56 | assert file =~ "test \"list/0"
57 | end)
58 |
59 | assert_file("test/elixir_scribe/domain/blog/post/new/new_post_test.exs", fn file ->
60 | assert file =~ "defmodule ElixirScribe.Blog.Post.New.NewPostTest do"
61 | assert file =~ "test \"new/0"
62 | end)
63 |
64 | assert_file("test/elixir_scribe/domain/blog/post/read/read_post_test.exs", fn file ->
65 | assert file =~ "defmodule ElixirScribe.Blog.Post.Read.ReadPostTest do"
66 | assert file =~ "test \"read!/1"
67 | end)
68 |
69 | assert_file("test/elixir_scribe/domain/blog/post/edit/edit_post_test.exs", fn file ->
70 | assert file =~ "defmodule ElixirScribe.Blog.Post.Edit.EditPostTest do"
71 | assert file =~ "test \"edit/1"
72 | end)
73 |
74 | assert_file("test/elixir_scribe/domain/blog/post/create/create_post_test.exs", fn file ->
75 | assert file =~ "defmodule ElixirScribe.Blog.Post.Create.CreatePostTest do"
76 | assert file =~ "test \"create/1"
77 | end)
78 |
79 | assert_file("test/elixir_scribe/domain/blog/post/update/update_post_test.exs", fn file ->
80 | assert file =~ "defmodule ElixirScribe.Blog.Post.Update.UpdatePostTest do"
81 | assert file =~ "test \"update/2"
82 | end)
83 |
84 | assert_file("test/elixir_scribe/domain/blog/post/delete/delete_post_test.exs", fn file ->
85 | assert file =~ "defmodule ElixirScribe.Blog.Post.Delete.DeletePostTest do"
86 | assert file =~ "test \"delete/1"
87 | end)
88 | end)
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/domain_resource_api_sync_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("../../mix_test_helper.exs", __DIR__)
2 |
3 | defmodule ElixirScribe.Generator.DomainResourceAPISyncTest do
4 | alias ElixirScribe.Generator.DomainContract
5 | alias ElixirScribe.Generator.DomainResourceAPI
6 | use ElixirScribe.BaseCase
7 |
8 | import MixTestHelper
9 |
10 | # Tests in the API module only care about testing the function can be invoked and that the API contract is respected for guards, pattern matching and expected return types. The unit tests for the functionality are done in their respective modules.
11 |
12 | setup do
13 | Mix.Task.clear()
14 | :ok
15 | end
16 |
17 | # Check the ASYNC tests for `generate_actions/1` at test/elixir_scribe/generator/domain_resource_api_async_test.exs
18 | describe "generate_actions/1" do
19 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (%DomainContract{})",
20 | config do
21 | in_tmp_project(config.test, fn ->
22 | domain_contract = domain_contract_fixture()
23 |
24 | assert %DomainContract{} = DomainResourceAPI.generate_actions(domain_contract)
25 | end)
26 | end
27 | end
28 |
29 | # Check the ASYNC tests for `generate_tests/1` at test/elixir_scribe/generator/domain_resource_api_async_test.exs
30 | describe "generate_tests/1" do
31 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (%DomainContract{})",
32 | config do
33 | in_tmp_project(config.test, fn ->
34 | domain_contract = domain_contract_fixture()
35 |
36 | assert %DomainContract{} = DomainResourceAPI.generate_tests(domain_contract)
37 | end)
38 | end
39 | end
40 |
41 | # Check the ASYNC tests for `generate_api/1` at test/elixir_scribe/generator/domain_resource_api_async_test.exs
42 | describe "generate_api/1" do
43 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (%DomainContract{})",
44 | config do
45 | in_tmp_project(config.test, fn ->
46 | domain_contract = domain_contract_fixture()
47 |
48 | assert %DomainContract{} = DomainResourceAPI.generate_api(domain_contract)
49 | end)
50 | end
51 | end
52 |
53 | # Check the ASYNC tests for `generate_test_fixture/1` at test/elixir_scribe/generator/domain_resource_api_async_test.exs
54 | describe "generate_test_fixture/1" do
55 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (%DomainContract{})",
56 | config do
57 | in_tmp_project(config.test, fn ->
58 | domain_contract = domain_contract_fixture()
59 |
60 | assert %DomainContract{} = DomainResourceAPI.generate_test_fixture(domain_contract)
61 | end)
62 | end
63 | end
64 |
65 | # Check the ASYNC tests for `generate_new_files/1` at test/elixir_scribe/generator/domain_resource_api_async_test.exs
66 | describe "generate_new_files/1" do
67 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (%DomainContract{})",
68 | config do
69 | in_tmp_project(config.test, fn ->
70 | domain_contract = domain_contract_fixture()
71 |
72 | assert %DomainContract{} = DomainResourceAPI.generate_new_files(domain_contract)
73 | end)
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/domain/resource/build_action_files_paths/build_action_files_paths_resource_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.BuildActionFilesPaths.BuildActionFilesPathsResourceTest do
2 | use ElixirScribe.BaseCase, async: true
3 |
4 | alias ElixirScribe.Generator.DomainResourceAPI
5 |
6 | test "returns a list of all resource action files paths" do
7 | expected_files = [
8 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/list_schema.ex",
9 | "lib/elixir_scribe/domain/site/blog/post/list/list_posts.ex", "list"},
10 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/new_schema.ex",
11 | "lib/elixir_scribe/domain/site/blog/post/new/new_post.ex", "new"},
12 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/read_schema.ex",
13 | "lib/elixir_scribe/domain/site/blog/post/read/read_post.ex", "read"},
14 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/edit_schema.ex",
15 | "lib/elixir_scribe/domain/site/blog/post/edit/edit_post.ex", "edit"},
16 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/create_schema.ex",
17 | "lib/elixir_scribe/domain/site/blog/post/create/create_post.ex", "create"},
18 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/update_schema.ex",
19 | "lib/elixir_scribe/domain/site/blog/post/update/update_post.ex", "update"},
20 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/delete_schema.ex",
21 | "lib/elixir_scribe/domain/site/blog/post/delete/delete_post.ex", "delete"}
22 | ]
23 |
24 | contract = domain_contract_fixture()
25 |
26 | assert DomainResourceAPI.build_action_files_paths(contract) === expected_files
27 | end
28 |
29 | test "it returns the expected source template path when the flag --no-schema is used" do
30 | expected_files = [
31 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/no_schema_access/any_action.ex",
32 | "lib/elixir_scribe/domain/blog/post/list/list_posts.ex", "list"},
33 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/no_schema_access/any_action.ex",
34 | "lib/elixir_scribe/domain/blog/post/new/new_post.ex", "new"},
35 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/no_schema_access/any_action.ex",
36 | "lib/elixir_scribe/domain/blog/post/read/read_post.ex", "read"},
37 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/no_schema_access/any_action.ex",
38 | "lib/elixir_scribe/domain/blog/post/edit/edit_post.ex", "edit"},
39 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/no_schema_access/any_action.ex",
40 | "lib/elixir_scribe/domain/blog/post/create/create_post.ex", "create"},
41 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/no_schema_access/any_action.ex",
42 | "lib/elixir_scribe/domain/blog/post/update/update_post.ex", "update"},
43 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/no_schema_access/any_action.ex",
44 | "lib/elixir_scribe/domain/blog/post/delete/delete_post.ex", "delete"}
45 | ]
46 |
47 | args = ["Blog", "Post", "posts", "title:string", "desc:string", "--no-schema"]
48 | contract = domain_contract_fixture(args)
49 |
50 | assert DomainResourceAPI.build_action_files_paths(contract) === expected_files
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/domain/resource/build_files_to_generate/build_files_to_generate_resource_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Domain.Resource.BuildFilesToGenerate.BuildFilesToGenerateResourceTest do
2 | use ElixirScribe.BaseCase, async: true
3 |
4 | alias ElixirScribe.Generator.DomainResourceAPI
5 |
6 | test "files_to_generate/1 works as expected" do
7 | expected_files = [
8 | {:eex, :api, "priv/templates/scribe.gen.domain/apis/api_module.ex",
9 | "lib/elixir_scribe/domain/site/blog/post_api.ex"},
10 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/list_schema.ex",
11 | "lib/elixir_scribe/domain/site/blog/post/list/list_posts.ex", "list"},
12 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/new_schema.ex",
13 | "lib/elixir_scribe/domain/site/blog/post/new/new_post.ex", "new"},
14 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/read_schema.ex",
15 | "lib/elixir_scribe/domain/site/blog/post/read/read_post.ex", "read"},
16 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/edit_schema.ex",
17 | "lib/elixir_scribe/domain/site/blog/post/edit/edit_post.ex", "edit"},
18 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/create_schema.ex",
19 | "lib/elixir_scribe/domain/site/blog/post/create/create_post.ex", "create"},
20 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/update_schema.ex",
21 | "lib/elixir_scribe/domain/site/blog/post/update/update_post.ex", "update"},
22 | {:eex, :resource, "priv/templates/scribe.gen.domain/actions/schema_access/delete_schema.ex",
23 | "lib/elixir_scribe/domain/site/blog/post/delete/delete_post.ex", "delete"},
24 | {:eex, :resource_test,
25 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/list_schema_test.exs",
26 | "test/elixir_scribe/domain/site/blog/post/list/list_posts_test.exs", "list"},
27 | {:eex, :resource_test,
28 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/new_schema_test.exs",
29 | "test/elixir_scribe/domain/site/blog/post/new/new_post_test.exs", "new"},
30 | {:eex, :resource_test,
31 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/read_schema_test.exs",
32 | "test/elixir_scribe/domain/site/blog/post/read/read_post_test.exs", "read"},
33 | {:eex, :resource_test,
34 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/edit_schema_test.exs",
35 | "test/elixir_scribe/domain/site/blog/post/edit/edit_post_test.exs", "edit"},
36 | {:eex, :resource_test,
37 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/create_schema_test.exs",
38 | "test/elixir_scribe/domain/site/blog/post/create/create_post_test.exs", "create"},
39 | {:eex, :resource_test,
40 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/update_schema_test.exs",
41 | "test/elixir_scribe/domain/site/blog/post/update/update_post_test.exs", "update"},
42 | {:eex, :resource_test,
43 | "priv/templates/scribe.gen.domain/tests/actions/schema_access/delete_schema_test.exs",
44 | "test/elixir_scribe/domain/site/blog/post/delete/delete_post_test.exs", "delete"},
45 | {:eex, "schema.ex", "lib/elixir_scribe/domain/site/blog/post/post_schema.ex"},
46 | {:eex, "priv/templates/scribe.gen.domain/tests/fixtures_module.ex",
47 | "test/support/fixtures/domain/site/blog/post_fixtures.ex"}
48 | ]
49 |
50 | domain_contract = domain_contract_fixture()
51 |
52 | assert DomainResourceAPI.build_files_to_generate(domain_contract) === expected_files
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/file_api_async_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.TemplateFileAPIAsyncTest do
2 | alias ElixirScribe.Template.BuildFilenameForActionFileContract
3 | alias ElixirScribe.Template.FileAPI
4 | use ElixirScribe.BaseCase, async: true
5 |
6 | # Tests in the API module only care about testing the function can be invoked and that the API contract is respected for guards, pattern matching and expected return types. The unit tests for the functionality are done in their respective modules.
7 |
8 | describe "build_dir_path_for_html_file/1" do
9 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (string)" do
10 | contract = domain_contract_fixture()
11 |
12 | assert FileAPI.build_dir_path_for_html_file(contract) |> is_binary()
13 | end
14 |
15 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (%DomainContract{})" do
16 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
17 | FileAPI.build_dir_path_for_html_file(%{})
18 | end
19 | end
20 | end
21 |
22 | describe "build_template_action_filename/1" do
23 | test "can be invoked with the correct argument type (%BuildFilenameForActionFileContract{}) and returns the expected type (string)" do
24 | attrs = %{
25 | action: "read",
26 | action_suffix: "_",
27 | file_type: "schema",
28 | file_extension: ".ex"
29 | }
30 |
31 | contract = BuildFilenameForActionFileContract.new!(attrs)
32 |
33 | assert FileAPI.build_template_action_filename(contract) |> is_binary()
34 | end
35 |
36 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (%BuildFilenameForActionFileContract{})" do
37 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
38 | FileAPI.build_template_action_filename(%{})
39 | end
40 | end
41 | end
42 |
43 | # Check the SYNC tests at test/elixir_scribe/template/template_binding_api_sync_test.exs
44 | describe "inject_content_before_module_end/2" do
45 | test "raises a FunctionClauseError when the first argument isn't a string" do
46 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
47 | FileAPI.inject_content_before_module_end(["whatever"], "api.ex")
48 | end
49 | end
50 |
51 | test "raises a FunctionClauseError when the second argument isn't a string" do
52 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
53 | FileAPI.inject_content_before_module_end("whatever", ["api.ex"])
54 | end
55 | end
56 | end
57 |
58 | describe "inject_eex_template_before_module_end/4" do
59 | test "raises a FunctionClauseError when the first argument isn't a list" do
60 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
61 | FileAPI.inject_eex_template_before_module_end(%{}, "source.ex", "target.ex", [])
62 | end
63 | end
64 |
65 | test "raises a FunctionClauseError when the second argument isn't a string" do
66 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
67 | FileAPI.inject_eex_template_before_module_end([], ["source.ex"], "target.ex", [])
68 | end
69 | end
70 |
71 | test "raises a FunctionClauseError when the third argument isn't a string" do
72 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
73 | FileAPI.inject_eex_template_before_module_end([], "source.ex", ["target.ex"], [])
74 | end
75 | end
76 |
77 | test "raises a FunctionClauseError when the fourth argument isn't a list" do
78 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
79 | FileAPI.inject_eex_template_before_module_end([], "source.ex", "target.ex", "")
80 | end
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/lib/elixir_scribe/generator/schema/resource/schema_resource_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.Schema.Resource.SchemaResourceHelpers do
2 | @moduledoc false
3 |
4 | alias ElixirScribe.Generator.SchemaContract
5 |
6 | @valid_types [
7 | :integer,
8 | :float,
9 | :decimal,
10 | :boolean,
11 | :map,
12 | :string,
13 | :array,
14 | :references,
15 | :text,
16 | :date,
17 | :time,
18 | :time_usec,
19 | :naive_datetime,
20 | :naive_datetime_usec,
21 | :utc_datetime,
22 | :utc_datetime_usec,
23 | :uuid,
24 | :binary,
25 | :enum
26 | ]
27 |
28 | def valid_types, do: @valid_types
29 |
30 | @doc """
31 | Returns the string value of the default schema param.
32 | """
33 | def default_param(%SchemaContract{} = schema, action) when is_atom(action) do
34 | schema.params
35 | |> Map.fetch!(action)
36 | |> Map.fetch!(schema.params.default_key)
37 | |> to_string()
38 | end
39 |
40 | @doc """
41 | Converts the given value to map format when it is a date, time, datetime or naive_datetime.
42 |
43 | Since `form_component.html.heex` generated by the live generator uses selects for dates and/or
44 | times, fixtures must use map format for those fields in order to submit the live form.
45 | """
46 | def live_form_value(%Date{} = date), do: Calendar.strftime(date, "%Y-%m-%d")
47 |
48 | def live_form_value(%Time{} = time), do: Calendar.strftime(time, "%H:%M")
49 |
50 | def live_form_value(%NaiveDateTime{} = naive) do
51 | NaiveDateTime.to_iso8601(naive)
52 | end
53 |
54 | def live_form_value(%DateTime{} = naive) do
55 | DateTime.to_iso8601(naive)
56 | end
57 |
58 | def live_form_value(value), do: value
59 |
60 | @doc """
61 | Build an invalid value for `@invalid_attrs` which is nil by default.
62 |
63 | * In case the value is a list, this will return an empty array.
64 | * In case the value is date, datetime, naive_datetime or time, this will return an invalid date.
65 | * In case it is a boolean, we keep it as false
66 | """
67 | def invalid_form_value(value) when is_list(value), do: []
68 |
69 | def invalid_form_value(%{day: _day, month: _month, year: _year} = _date),
70 | do: "2022-00"
71 |
72 | def invalid_form_value(%{hour: _hour, minute: _minute}), do: %{hour: 14, minute: 00}
73 | def invalid_form_value(true), do: false
74 | def invalid_form_value(_value), do: nil
75 |
76 | @doc """
77 | Generates an invalid error message according to the params present in the schema.
78 | """
79 | def failed_render_change_message(_schema) do
80 | "can't be blank"
81 | end
82 |
83 | def type_for_migration({:enum, _}), do: :string
84 | def type_for_migration(other), do: other
85 |
86 | def format_fields_for_schema(schema) do
87 | Enum.map_join(schema.types, "\n", fn {k, v} ->
88 | " field #{inspect(k)}, #{type_and_opts_for_schema(v)}#{schema.defaults[k]}#{maybe_redact_field(k in schema.redacts)}"
89 | end)
90 | end
91 |
92 | @doc """
93 | Return the required fields in the schema. Anything not in the `optionals` list
94 | is considered required.
95 | """
96 | def required_fields(schema) do
97 | Enum.reject(schema.attrs, fn {key, _} -> key in schema.optionals end)
98 | end
99 |
100 | def type_and_opts_for_schema({:enum, opts}),
101 | do: ~s|Ecto.Enum, values: #{inspect(Keyword.get(opts, :values))}|
102 |
103 | def type_and_opts_for_schema(other), do: inspect(other)
104 |
105 | def maybe_redact_field(true), do: ", redact: true"
106 | def maybe_redact_field(false), do: ""
107 |
108 | @doc """
109 | Returns the string value for use in EEx templates.
110 | """
111 | def value(schema, field, value) do
112 | schema.types
113 | |> Map.fetch!(field)
114 | |> inspect_value(value)
115 | end
116 |
117 | defp inspect_value(:decimal, value), do: "Decimal.new(\"#{value}\")"
118 | defp inspect_value(_type, value), do: inspect(value)
119 | end
120 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/schema_resource_api_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.SchemaResourceAPITest do
2 | alias ElixirScribe.MixAPI
3 | alias ElixirScribe.Generator.SchemaResourceAPI
4 | alias ElixirScribe.Generator.SchemaContract
5 | use ElixirScribe.BaseCase, async: true
6 |
7 | # Tests in the API module only care about testing the function can be invoked and that the API contract is respected for guards, pattern matching and expected return types. The unit tests for the functionality are done in their respective modules.
8 |
9 | describe "build_schema_resource_contract/2" do
10 | test "can be invoked with the correct arguments types (list, list) and returns the expected tuple ({:ok, %SchemaContract{}})" do
11 | args = ["Blog", "Post", "posts", "title:string", "desc:string"]
12 |
13 | {valid_args, opts, _invalid_args} = args |> MixAPI.parse_cli_command()
14 |
15 | assert {:ok, %SchemaContract{} = contract} =
16 | SchemaResourceAPI.build_schema_resource_contract(valid_args, opts)
17 |
18 | assert ^contract = SchemaResourceAPI.build_schema_resource_contract!(valid_args, opts)
19 | end
20 |
21 | test "raises a FunctionClauseError when isn't invoked with the correct first argument type (list)" do
22 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
23 | SchemaResourceAPI.build_schema_resource_contract(%{}, [])
24 | end
25 | end
26 |
27 | test "raises a FunctionClauseError when isn't invoked with the correct second argument type (list)" do
28 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
29 | SchemaResourceAPI.build_schema_resource_contract([], %{})
30 | end
31 | end
32 | end
33 |
34 | describe "default_param_value/2" do
35 | test "can be invoked with the correct arguments types (%SchemaContract{}, atom) and returns the expected string" do
36 | contract = domain_contract_fixture()
37 |
38 | assert SchemaResourceAPI.default_param_value(contract.schema, :create) === "some name"
39 | end
40 |
41 | test "raises a FunctionClauseError when isn't invoked with the correct first argument type (%SchemaContract{})" do
42 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
43 | SchemaResourceAPI.default_param_value(%{}, [])
44 | end
45 | end
46 |
47 | test "raises a FunctionClauseError when isn't invoked with the correct second argument type (atom)" do
48 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
49 | contract = domain_contract_fixture()
50 | SchemaResourceAPI.default_param_value(contract, [])
51 | end
52 | end
53 | end
54 |
55 | describe "live_form_value/2" do
56 | test "can be invoked with the correct first argument type (%Date{}) and returns the expected string format %Y-%m-%d" do
57 | date = Date.new!(2024, 9, 6)
58 |
59 | assert SchemaResourceAPI.live_form_value(date) === "2024-09-06"
60 | end
61 |
62 | test "can be invoked with the correct first argument type (%Time{}) and returns the expected string format %H:%M" do
63 | time = Time.new!(9, 12, 25)
64 |
65 | assert SchemaResourceAPI.live_form_value(time) === "09:12"
66 | end
67 |
68 | test "can be invoked with the correct first argument type (%DateTime{}) and returns the expected string format %Y-%m-%d %H:%MZ" do
69 | date = Date.new!(2024, 9, 6)
70 | time = Time.new!(9, 12, 25)
71 | datetime = DateTime.new!(date, time)
72 |
73 | assert SchemaResourceAPI.live_form_value(datetime) === "2024-09-06T09:12:25Z"
74 | end
75 |
76 | test "can be invoked with the correct first argument type (%NaiveDateTime{}) and returns the expected string format %Y-%m-%dT%H:%M" do
77 | naive_datetime = NaiveDateTime.new!(2024, 9, 6, 9, 12, 25)
78 |
79 | assert SchemaResourceAPI.live_form_value(naive_datetime) === "2024-09-06T09:12:25"
80 | end
81 |
82 | test "when isn't invoked with the correct first argument type (%Date{}, %Time{}, %DateTime{} or %NaiveDateTime{}) it returns the same unchanged value" do
83 | assert SchemaResourceAPI.live_form_value("whatever") === "whatever"
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/domain/resource/generate_test_fixture/generate_test_fixture_resource_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("test/mix_test_helper.exs")
2 |
3 | defmodule ElixirScribe.Generator.Domain.Resource.GenerateTestFixture.GenerateTestFixtureResourceTest do
4 | use ElixirScribe.BaseCase
5 |
6 | alias ElixirScribe.Generator.DomainContract
7 | alias ElixirScribe.Generator.DomainResourceAPI
8 |
9 | import MixTestHelper
10 |
11 | setup do
12 | Mix.Task.clear()
13 | :ok
14 | end
15 |
16 | test "generates the file for the resource test fixture", config do
17 | in_tmp_project(config.test, fn ->
18 | args = [
19 | "Blog",
20 | "Post",
21 | "posts",
22 | "slug:unique",
23 | "secret:redact",
24 | "title:string",
25 | "--actions",
26 | "export,import"
27 | ]
28 |
29 | contract = domain_contract_fixture(args)
30 |
31 | assert %DomainContract{} = DomainResourceAPI.generate_test_fixture(contract)
32 |
33 | assert_file("test/support/fixtures/domain/blog/post_fixtures.ex", fn file ->
34 | assert file =~ "defmodule ElixirScribe.Blog.PostFixtures do"
35 | assert file =~ "def post_fixture(attrs \\\\ %{})"
36 | assert file =~ "title: \"some title\""
37 | end)
38 | end)
39 | end
40 |
41 | test "generates the file for the resource test fixture with unique schema fields", config do
42 | in_tmp_project(config.test, fn ->
43 | args = ~w(Blog Post posts
44 | slug:string:unique
45 | subject:unique
46 | body:text:unique
47 | order:integer:unique
48 | price:decimal:unique
49 | published_at:utc_datetime:unique
50 | author:references:users:unique
51 | published?:boolean
52 | )
53 |
54 | contract = domain_contract_fixture(args)
55 |
56 | assert %DomainContract{} = DomainResourceAPI.generate_test_fixture(contract)
57 |
58 | assert_received {:mix_shell, :info,
59 | [
60 | """
61 |
62 | Some of the generated database columns are unique. Please provide
63 | unique implementations for the following fixture function(s) in
64 | test/support/fixtures/domain/blog/post_fixtures.ex:
65 |
66 | def unique_post_price do
67 | raise "implement the logic to generate a unique post price"
68 | end
69 |
70 | def unique_post_published_at do
71 | raise "implement the logic to generate a unique post published_at"
72 | end
73 | """
74 | ]}
75 |
76 | assert_file("test/support/fixtures/domain/blog/post_fixtures.ex", fn file ->
77 | assert file =~ "defmodule ElixirScribe.Blog.PostFixtures do"
78 | assert file =~ ~S|def unique_post_order, do: System.unique_integer([:positive])|
79 |
80 | assert file =~
81 | ~S|def unique_post_slug, do: "some slug#{System.unique_integer([:positive])}"|
82 |
83 | assert file =~
84 | ~S|def unique_post_body, do: "some body#{System.unique_integer([:positive])}"|
85 |
86 | assert file =~
87 | ~S|def unique_post_subject, do: "some subject#{System.unique_integer([:positive])}"|
88 |
89 | refute file =~ ~S|def unique_post_author|
90 |
91 | assert file =~ """
92 | def unique_post_price do
93 | raise "implement the logic to generate a unique post price"
94 | end
95 | """
96 |
97 | assert file =~ """
98 | body: unique_post_body(),
99 | order: unique_post_order(),
100 | price: unique_post_price(),
101 | published?: true,
102 | published_at: unique_post_published_at(),
103 | slug: unique_post_slug(),
104 | subject: unique_post_subject()
105 | """
106 | end)
107 | end)
108 | end
109 | end
110 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/file_api_sync_test.exs:
--------------------------------------------------------------------------------
1 | Code.require_file("test/mix_test_helper.exs")
2 |
3 | defmodule ElixirScribe.TemplateFileAPISyncTest do
4 | alias ElixirScribe.Template.BindingAPI
5 | alias ElixirScribe.Template.FileAPI
6 | use ElixirScribe.BaseCase
7 |
8 | import MixTestHelper
9 |
10 | setup do
11 | Mix.Task.clear()
12 | :ok
13 | end
14 |
15 | # Tests in the API module only care about testing the function can be invoked and that the API contract is respected for guards, pattern matching and expected return types. The unit tests for the functionality are done in their respective modules.
16 |
17 | # Check the ASYNC tests at test/elixir_scribe/template/template_binding_api_async_test.exs
18 | describe "inject_content_before_module_end/2" do
19 | test "can be invoked with the correct arguments types (string, string) and returns the expected atom (:ok) for a successful operation",
20 | config do
21 | in_tmp_project(config.test, fn ->
22 | module_content = """
23 | defmodule API do
24 | end
25 | """
26 |
27 | File.write!("api.ex", module_content)
28 |
29 | content_to_inject = """
30 | def whatever, do: :whatever
31 | """
32 |
33 | assert :ok = FileAPI.inject_content_before_module_end(content_to_inject, "api.ex")
34 | end)
35 | end
36 |
37 | test "returns the tuple {:noop, reason} for an unsuccessful operation", config do
38 | in_tmp_project(config.test, fn ->
39 | module_content = """
40 | defmodule API do
41 | def whatever, do: :whatever
42 | end
43 | """
44 |
45 | File.write!("api.ex", module_content)
46 |
47 | content_to_inject = """
48 | def whatever, do: :whatever
49 | """
50 |
51 | assert {:noop, :content_to_inject_already_exists} =
52 | FileAPI.inject_content_before_module_end(content_to_inject, "api.ex")
53 | end)
54 | end
55 | end
56 |
57 | describe "inject_eex_template_before_module_end/4" do
58 | test "can be invoked with the correct arguments types (list, string, string, list) and returns the expected atom (:ok) for a successful operation",
59 | config do
60 | in_tmp_project(config.test, fn ->
61 | api_content = """
62 | defmodule API do
63 | end
64 | """
65 |
66 | File.write!("api_existing_module.ex", api_content)
67 |
68 | function_template = """
69 | def <%= action %>(), do: :<%= action %>
70 | """
71 |
72 | File.write!("api_function_template.ex", function_template)
73 |
74 | base_template_paths = ElixirScribe.base_template_paths()
75 |
76 | binding =
77 | domain_contract_fixture()
78 | |> BindingAPI.build_binding_template()
79 | |> BindingAPI.rebuild_binding_template(
80 | "read",
81 | file_type: :lib_core
82 | )
83 |
84 | assert :ok =
85 | FileAPI.inject_eex_template_before_module_end(
86 | base_template_paths,
87 | "api_function_template.ex",
88 | "api_existing_module.ex",
89 | binding
90 | )
91 | end)
92 | end
93 |
94 | test "returns the tuple {:noop, reason} for an unsuccessful operation", config do
95 | in_tmp_project(config.test, fn ->
96 | function_template = """
97 | def <%= action %>(), do: :<%= action %>
98 | """
99 |
100 | File.write!("api_function_template.ex", function_template)
101 |
102 | expected_api_content = """
103 | defmodule API do
104 | def create(), do: :create
105 | end
106 | """
107 |
108 | File.write!("api_existing_module.ex", expected_api_content)
109 |
110 | base_template_paths = ElixirScribe.base_template_paths()
111 |
112 | binding =
113 | domain_contract_fixture()
114 | |> BindingAPI.build_binding_template()
115 | |> BindingAPI.rebuild_binding_template("create",
116 | file_type: :lib_core
117 | )
118 |
119 | assert {:noop, :content_to_inject_already_exists} =
120 | FileAPI.inject_eex_template_before_module_end(
121 | base_template_paths,
122 | "api_function_template.ex",
123 | "api_existing_module.ex",
124 | binding
125 | )
126 | end)
127 | end
128 | end
129 | end
130 |
--------------------------------------------------------------------------------
/test/elixir_scribe/template/module_api_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Template.ModuleAPITest do
2 | alias ElixirScribe.Template.ModuleAPI
3 | use ElixirScribe.BaseCase, async: true
4 |
5 | describe "build_embeded_templates/0" do
6 | test "can be invoked and returns the expected type (string)" do
7 | assert ModuleAPI.build_embeded_templates() |> is_binary()
8 | end
9 | end
10 |
11 | describe "build_absolute_module_action_name/3" do
12 | test "can be invoked with the correct arguments type (%DomainContract{}, string, list) and returns the expected type (string)" do
13 | contract = domain_contract_fixture()
14 |
15 | assert ModuleAPI.build_absolute_module_action_name(contract, "read", file_type: :lib_core)
16 | |> is_binary()
17 | end
18 |
19 | test "raises a FunctionClauseError when isn't invoked with the correct first argument type (%DomainContract{})" do
20 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
21 | ModuleAPI.build_absolute_module_action_name(%{}, "action", [])
22 | end
23 | end
24 |
25 | test "raises a FunctionClauseError when isn't invoked with the correct second argument type (%DomainContract{})" do
26 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
27 | ModuleAPI.build_absolute_module_action_name(%{}, ["action"], [])
28 | end
29 | end
30 |
31 | test "raises a FunctionClauseError when isn't invoked with the correct third argument type (%DomainContract{})" do
32 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
33 | ModuleAPI.build_absolute_module_action_name(%{}, "action", "")
34 | end
35 | end
36 | end
37 |
38 | describe "build_absolute_module_action_name_aliases/2" do
39 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (string)" do
40 | contract = domain_contract_fixture()
41 |
42 | assert ModuleAPI.build_absolute_module_action_name_aliases(contract, file_type: :lib_core)
43 | |> is_binary()
44 | end
45 |
46 | test "raises a FunctionClauseError when isn't invoked with the correct first argument type (%DomainContract{})" do
47 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
48 | ModuleAPI.build_absolute_module_action_name_aliases(%{}, [])
49 | end
50 | end
51 |
52 | test "raises a FunctionClauseError when isn't invoked with the correct second argument type (list)" do
53 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
54 | domain_contract_fixture()
55 | |> ModuleAPI.build_absolute_module_action_name_aliases(%{})
56 | end
57 | end
58 | end
59 |
60 | describe "build_absolute_module_name/2" do
61 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (atom)" do
62 | contract = domain_contract_fixture()
63 |
64 | assert ModuleAPI.build_absolute_module_name(contract, file_type: :lib_core) |> is_atom()
65 | end
66 |
67 | test "raises a FunctionClauseError when isn't invoked with the correct first argument type (%DomainContract{})" do
68 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
69 | ModuleAPI.build_absolute_module_name(%{}, [])
70 | end
71 | end
72 |
73 | test "raises a FunctionClauseError when isn't invoked with the correct second argument type (list)" do
74 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
75 | domain_contract_fixture()
76 | |> ModuleAPI.build_absolute_module_name("")
77 | end
78 | end
79 | end
80 |
81 | describe "build_module_action_name/2" do
82 | test "can be invoked with the correct arguments type (%DomainContract{}, string) and returns the expected type (string)" do
83 | contract = domain_contract_fixture()
84 |
85 | assert ModuleAPI.build_module_action_name(contract, "action") |> is_binary()
86 | end
87 |
88 | test "raises a FunctionClauseError when isn't invoked with the correct first argument type (%DomainContract{})" do
89 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
90 | ModuleAPI.build_module_action_name(%{}, "action")
91 | end
92 | end
93 |
94 | test "raises a FunctionClauseError when isn't invoked with the correct second argument type (list)" do
95 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
96 | domain_contract_fixture()
97 | |> ModuleAPI.build_module_action_name(:whatever)
98 | end
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/test/mix_test_helper.exs:
--------------------------------------------------------------------------------
1 | # Get Mix output sent to the current
2 | # process to avoid polluting tests.
3 | Mix.shell(Mix.Shell.Process)
4 |
5 | defmodule MixTestHelper do
6 | import ExUnit.Assertions
7 | import ExUnit.CaptureIO
8 |
9 | def tmp_path do
10 | Path.expand("../tmp", __DIR__)
11 | end
12 |
13 | def random_tmp_path(which) do
14 | test_name = to_string(which) |> String.trim() |> String.replace(" ", "_")
15 | Path.join([tmp_path(), random_string(10) <> "_" <> test_name])
16 | end
17 |
18 | defp random_string(len) do
19 | len |> :crypto.strong_rand_bytes() |> Base.encode64() |> binary_part(0, len)
20 | end
21 |
22 | def in_tmp(which, function) do
23 | path = random_tmp_path(which)
24 |
25 | try do
26 | File.rm_rf!(path)
27 | File.mkdir_p!(path)
28 | File.cd!(path, function)
29 | after
30 | File.rm_rf!(path)
31 | end
32 | end
33 |
34 | def in_tmp_project(which, function) do
35 | conf_before = Application.get_env(:phoenix, :generators) || []
36 | path = random_tmp_path(which)
37 |
38 | try do
39 | File.rm_rf!(path)
40 | File.mkdir_p!(path)
41 |
42 | File.cd!(path, fn ->
43 | File.touch!("mix.exs")
44 |
45 | File.mkdir_p!("lib/elixir_scribe_web/")
46 |
47 | File.write!("lib/elixir_scribe_web/router.ex", """
48 | defmodule ElixirScribeAppWeb.Router do
49 | use ElixirScribeAppWeb, :router
50 |
51 | end
52 | """)
53 |
54 | File.write!(".formatter.exs", """
55 | [
56 | import_deps: [:phoenix, :ecto, :ecto_sql],
57 | inputs: ["*.exs"]
58 | ]
59 | """)
60 |
61 | function.()
62 | end)
63 | after
64 | File.rm_rf!(path)
65 | Application.put_env(:phoenix, :generators, conf_before)
66 | end
67 | end
68 |
69 | def in_tmp_umbrella_project(which, function) do
70 | conf_before = Application.get_env(:phoenix, :generators) || []
71 | path = random_tmp_path(which)
72 |
73 | try do
74 | apps_path = Path.join(path, "apps")
75 | config_path = Path.join(path, "config")
76 | File.rm_rf!(path)
77 | File.mkdir_p!(path)
78 | File.mkdir_p!(apps_path)
79 | File.mkdir_p!(config_path)
80 | File.touch!(Path.join(path, "mix.exs"))
81 |
82 | for file <- ~w(config.exs dev.exs test.exs prod.exs) do
83 | File.write!(Path.join(config_path, file), "import Config\n")
84 | end
85 |
86 | File.cd!(apps_path, function)
87 | after
88 | Application.put_env(:phoenix, :generators, conf_before)
89 | File.rm_rf!(path)
90 | end
91 | end
92 |
93 | def in_project(app, path, fun) do
94 | %{name: name, file: file} = Mix.Project.pop()
95 |
96 | try do
97 | capture_io(:stderr, fn ->
98 | Mix.Project.in_project(app, path, [prune_code_paths: false], fun)
99 | end)
100 | after
101 | Mix.Project.push(name, file)
102 | end
103 | end
104 |
105 | def assert_file(file) do
106 | assert File.regular?(file), "Expected #{file} to exist, but does not"
107 | end
108 |
109 | def refute_file(file) do
110 | refute File.regular?(file), "Expected #{file} to not exist, but it does"
111 | end
112 |
113 | def assert_file(file, match) do
114 | cond do
115 | is_list(match) ->
116 | assert_file(file, &Enum.each(match, fn m -> assert &1 =~ m end))
117 |
118 | is_binary(match) or is_struct(match, Regex) ->
119 | assert_file(file, &assert(&1 =~ match))
120 |
121 | is_function(match, 1) ->
122 | assert_file(file)
123 | match.(File.read!(file))
124 |
125 | true ->
126 | raise inspect({file, match})
127 | end
128 | end
129 |
130 | def modify_file(path, function) when is_binary(path) and is_function(function, 1) do
131 | path
132 | |> File.read!()
133 | |> function.()
134 | |> write_file!(path)
135 | end
136 |
137 | defp write_file!(content, path) do
138 | File.write!(path, content)
139 | end
140 |
141 | def with_generator_env(app_name \\ :phoenix, new_env, fun) do
142 | config_before = Application.fetch_env(app_name, :generators)
143 | Application.put_env(app_name, :generators, new_env)
144 |
145 | try do
146 | fun.()
147 | after
148 | case config_before do
149 | {:ok, config} -> Application.put_env(app_name, :generators, config)
150 | :error -> Application.delete_env(app_name, :generators)
151 | end
152 | end
153 | end
154 |
155 | def umbrella_mixfile_contents do
156 | """
157 | defmodule Umbrella.MixProject do
158 | use Mix.Project
159 |
160 | def project do
161 | [
162 | apps_path: "apps",
163 | deps: deps()
164 | ]
165 | end
166 |
167 | defp deps do
168 | []
169 | end
170 | end
171 | """
172 | end
173 |
174 | def flush do
175 | receive do
176 | _ -> flush()
177 | after
178 | 0 -> :ok
179 | end
180 | end
181 | end
182 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/domain_contract_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.DomainContractTest do
2 | use ElixirScribe.BaseCase, async: true
3 |
4 | alias ElixirScribe.Generator.DomainContract
5 |
6 | setup do
7 | contract_attrs = %{
8 | alias: Blog,
9 | api_file: "lib/elixir_scribe/domain/blog/post_api.ex",
10 | base_module: ElixirScribe,
11 | basename: "blog",
12 | context_app: :elixir_scribe,
13 | generate?: true,
14 | lib_domain_dir: "lib/elixir_scribe/domain/blog",
15 | lib_resource_dir: "lib/elixir_scribe/domain/blog/post",
16 | lib_resource_dir_plural: "lib/elixir_scribe/domain/blog/posts",
17 | lib_web_domain_dir: "lib/elixir_scribe_web/domain/blog",
18 | lib_web_resource_dir: "lib/elixir_scribe_web/domain/blog/post",
19 | lib_web_resource_dir_plural: "lib/elixir_scribe_web/domain/blog/posts",
20 | module: ElixirScribe.Blog,
21 | name: "Blog",
22 | opts: [
23 | {:web, "Blog"},
24 | {:resource_actions, ["list", "new", "read", "edit", "create", "update", "delete"]},
25 | {:schema, true},
26 | {:context, true},
27 | {:no_default_actions, false},
28 | {:actions, nil},
29 | {:binary_id, true}
30 | ],
31 | resource_actions: ["list", "new", "read", "edit", "create", "update", "delete"],
32 | resource_module: ElixirScribe.Blog.Post,
33 | resource_module_plural: ElixirScribe.Blog.Posts,
34 | resource_path_name_plural: "posts",
35 | resource_path_name_singular: "post",
36 | schema: %ElixirScribe.Generator.SchemaContract{
37 | alias: Post,
38 | api_route_prefix: "/api/blog/posts",
39 | assocs: [],
40 | attrs: [{:title, :string}, {:desc, :string}],
41 | binary_id: true,
42 | collection: "posts",
43 | context_app: :elixir_scribe,
44 | defaults: %{title: "", desc: ""},
45 | embedded?: false,
46 | file: "lib/elixir_scribe/domain/blog/post/post_schema.ex",
47 | fixture_params: [{:desc, "\"some desc\""}, {:title, "\"some title\""}],
48 | fixture_unique_functions: [],
49 | generate?: true,
50 | human_plural: "Posts",
51 | human_singular: "Post",
52 | indexes: [],
53 | migration?: true,
54 | migration_defaults: %{title: "", desc: ""},
55 | migration_module: Ecto.Migration,
56 | module: ElixirScribe.Blog.Post,
57 | optionals: [],
58 | opts: [
59 | {:web, "Blog"},
60 | {:resource_actions, ["list", "new", "read", "edit", "create", "update", "delete"]},
61 | {:schema, true},
62 | {:context, true},
63 | {:no_default_actions, false},
64 | {:actions, nil},
65 | {:binary_id, true}
66 | ],
67 | params: %{
68 | create: %{title: "some title", desc: "some desc"},
69 | default_key: :title,
70 | update: %{title: "some updated title", desc: "some updated desc"}
71 | },
72 | plural: "posts",
73 | prefix: nil,
74 | redacts: [],
75 | repo: ElixirScribe.Repo,
76 | repo_alias: "",
77 | route_helper: "blog_post",
78 | route_prefix: "/blog/posts",
79 | sample_id: "11111111-1111-1111-1111-111111111111",
80 | self: ElixirScribe.Generator.SchemaContract,
81 | singular: "post",
82 | string_attr: :title,
83 | table: "posts",
84 | timestamp_type: :naive_datetime,
85 | types: %{title: :string, desc: :string},
86 | uniques: [],
87 | web_namespace: "Blog",
88 | web_path: "blog"
89 | },
90 | self: ElixirScribe.Generator.DomainContract,
91 | test_domain_dir: "test/elixir_scribe/domain/blog",
92 | test_file: "test/elixir_scribe/domain/blog_test.exs",
93 | test_fixtures_file: "test/support/fixtures/domain/blog/post_fixtures.ex",
94 | test_resource_dir: "test/elixir_scribe/domain/blog/post",
95 | test_resource_dir_plural: "test/elixir_scribe/domain/blog/posts",
96 | test_web_domain_dir: "test/elixir_scribe_web/domain/blog",
97 | test_web_resource_dir: "test/elixir_scribe_web/domain/blog/post",
98 | test_web_resource_dir_plural: "test/elixir_scribe_web/domain/blog/posts",
99 | web_domain_module: ElixirScribeWeb.Blog,
100 | web_module: ElixirScribeWeb,
101 | web_resource_module: ElixirScribeWeb.Blog.Post,
102 | web_resource_module_plural: ElixirScribeWeb.Blog.Posts
103 | }
104 |
105 | %{contract_attrs: contract_attrs}
106 | end
107 |
108 | describe "Builds the contract successfully with" do
109 | test "new/1", %{contract_attrs: contract_attrs} do
110 | assert {:ok, domain_contract = %DomainContract{}} = DomainContract.new(contract_attrs)
111 |
112 | expected_fields = Map.keys(contract_attrs)
113 |
114 | assert_maps_equal(contract_attrs, domain_contract, expected_fields)
115 | end
116 |
117 | test "new!/1", %{contract_attrs: contract_attrs} do
118 | assert domain_contract = %DomainContract{} = DomainContract.new!(contract_attrs)
119 |
120 | expected_fields = Map.keys(contract_attrs)
121 |
122 | assert_maps_equal(contract_attrs, domain_contract, expected_fields)
123 | end
124 | end
125 |
126 | describe "When it fails to build the contract" do
127 | test "new/1 returns an :error tuple" do
128 | assert {:error, _reason} = DomainContract.new(%{})
129 | end
130 |
131 | test "new!/1 raises" do
132 | assert_raise Norm.MismatchError,
133 | ~r/^Could not conform input:.*$/s,
134 | fn -> DomainContract.new!(%{}) end
135 | end
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/test/elixir_scribe_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribeTest do
2 | use ElixirScribe.BaseCase, async: true
3 |
4 | doctest ElixirScribe
5 |
6 | describe "base_template_paths/0" do
7 | test "returns a list of base paths in the expected order" do
8 | assert ElixirScribe.base_template_paths() === [".", :elixir_scribe, :phoenix]
9 | end
10 | end
11 |
12 | describe "app_name/0" do
13 | test "return the app name as a string" do
14 | assert ElixirScribe.app_name() === "elixir_scribe"
15 | end
16 | end
17 |
18 | describe "app_path/1" do
19 | test "returns app path for :lib_core" do
20 | assert ElixirScribe.app_path(:lib_core) === "lib/elixir_scribe"
21 | end
22 |
23 | test "returns app path for :test_core" do
24 | assert ElixirScribe.app_path(:test_core) === "test/elixir_scribe"
25 | end
26 |
27 | test "returns app path for :lib_web" do
28 | assert ElixirScribe.app_path(:lib_web) === "lib/elixir_scribe_web"
29 | end
30 |
31 | test "returns app path for :test_web" do
32 | assert ElixirScribe.app_path(:test_web) === "test/elixir_scribe_web"
33 | end
34 | end
35 |
36 | describe "web_template_path/0" do
37 | test "returns the web template path" do
38 | assert ElixirScribe.web_template_path() === "priv/templates/scribe.gen.html"
39 | end
40 | end
41 |
42 | describe "html_template_path/0" do
43 | test "returns the html template path" do
44 | assert ElixirScribe.html_template_path() === "priv/templates/scribe.gen.html/html"
45 | end
46 | end
47 |
48 | describe "controller_template_path/0" do
49 | test "returns the controller template path" do
50 | assert ElixirScribe.controller_template_path() ===
51 | "priv/templates/scribe.gen.html/controllers"
52 | end
53 | end
54 |
55 | describe "controller_test_template_path/0" do
56 | test "returns the controller test template path" do
57 | assert ElixirScribe.controller_test_template_path() ===
58 | "priv/templates/scribe.gen.html/tests/controllers"
59 | end
60 | end
61 |
62 | describe "domain_template_path/0" do
63 | test "returns the domain template path" do
64 | assert ElixirScribe.domain_template_path() === "priv/templates/scribe.gen.domain"
65 | end
66 | end
67 |
68 | describe "domain_tests_template_path/0" do
69 | test "returns the default domain tests template path" do
70 | assert ElixirScribe.domain_tests_template_path() ===
71 | "priv/templates/scribe.gen.domain/tests"
72 | end
73 | end
74 |
75 | describe "domain_api_template_path/0" do
76 | test "returns the default domain api template path" do
77 | assert ElixirScribe.domain_api_template_path() === "priv/templates/scribe.gen.domain/apis"
78 | end
79 | end
80 |
81 | describe "resource_actions_template_path/0" do
82 | test "returns the default domain actions template path" do
83 | assert ElixirScribe.resource_actions_template_path() ===
84 | "priv/templates/scribe.gen.domain/actions"
85 | end
86 | end
87 |
88 | describe "resource_actions/0" do
89 | test "returns the resource actions in the expected order" do
90 | assert ElixirScribe.resource_actions() === [
91 | "list",
92 | "new",
93 | "read",
94 | "edit",
95 | "create",
96 | "update",
97 | "delete"
98 | ]
99 | end
100 | end
101 |
102 | describe "resource_actions_aliases/0" do
103 | test "returns an empty map when no aliases are configured in the app config" do
104 | assert ElixirScribe.resource_actions_aliases() === %{}
105 | end
106 | end
107 |
108 | describe "resource_action_alias/1" do
109 | test "returns the same action when no alias is configured for action in the app config" do
110 | assert ElixirScribe.resource_action_alias("create") === "create"
111 | end
112 | end
113 |
114 | describe "resource_html_actions/0" do
115 | test "returns the resource html actions in the expected order" do
116 | assert ElixirScribe.resource_html_actions() === ["read", "new", "edit", "list"]
117 | end
118 | end
119 |
120 | describe "resource_plural_actions/0" do
121 | test "returns the resource plural actions" do
122 | assert ElixirScribe.resource_plural_actions() === ["index", "list"]
123 | end
124 | end
125 |
126 | describe "schema_template_folder_name/1" do
127 | test "returns `schema_access` as the folder template name when schema.generate? is true" do
128 | domain_contract = domain_contract_fixture()
129 |
130 | assert ElixirScribe.schema_template_folder_name(domain_contract.schema) === "schema_access"
131 | end
132 |
133 | test "returns `no_schema_access` as the folder template name when schema.generate? is false" do
134 | domain_contract = domain_contract_fixture()
135 | schema = Map.put(domain_contract.schema, :generate?, false)
136 |
137 | assert ElixirScribe.schema_template_folder_name(schema) === "no_schema_access"
138 | end
139 | end
140 |
141 | describe "app_file_extensions/0" do
142 | test "it returns a list with all app file extensions" do
143 | assert ElixirScribe.app_file_extensions() === [".ex", ".exs", "html.heex"]
144 | end
145 | end
146 |
147 | describe "app_file_types/0" do
148 | test "it returns a list with all app file types" do
149 | assert ElixirScribe.app_file_types() === ["", "controller", "controller_test", "test"]
150 | end
151 | end
152 |
153 | describe "app_path_types/0" do
154 | test "it returns a list with all file path types" do
155 | assert ElixirScribe.app_path_types() === [:lib_core, :lib_web, :test_core, :test_web]
156 | end
157 | end
158 | end
159 |
--------------------------------------------------------------------------------
/test/elixir_scribe/generator/domain_resource_api_async_test.exs:
--------------------------------------------------------------------------------
1 | defmodule ElixirScribe.Generator.DomainResourceAPIAsyncTest do
2 | alias ElixirScribe.MixAPI
3 | alias ElixirScribe.Generator.DomainContract
4 | alias ElixirScribe.Generator.DomainResourceAPI
5 | use ElixirScribe.BaseCase, async: true
6 |
7 | # Tests in the API module only care about testing the function can be invoked and that the API contract is respected for guards, pattern matching and expected return types. The unit tests for the functionality are done in their respective modules.
8 |
9 | describe "build_domain_resource_contract/2" do
10 | test "can be invoked with the correct arguments types (list, list) and returns the expected tuple ({:ok, %DomainContract{}})" do
11 | args = ["Blog", "Post", "posts", "title:string", "desc:string"]
12 | {valid_args, opts, _invalid_args} = args |> MixAPI.parse_cli_command()
13 |
14 | assert {:ok, %DomainContract{}} =
15 | DomainResourceAPI.build_domain_resource_contract(valid_args, opts)
16 | end
17 |
18 | test "raises a FunctionClauseError when isn't invoked with the correct first argument type (list)" do
19 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
20 | DomainResourceAPI.build_domain_resource_contract(%{}, [])
21 | end
22 | end
23 |
24 | test "raises a FunctionClauseError when isn't invoked with the correct second argument type (list)" do
25 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
26 | DomainResourceAPI.build_domain_resource_contract([], %{})
27 | end
28 | end
29 | end
30 |
31 | describe "build_files_to_generate/1" do
32 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (list)" do
33 | domain_contract = domain_contract_fixture()
34 |
35 | assert DomainResourceAPI.build_files_to_generate(domain_contract) |> is_list()
36 | end
37 |
38 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (%DomainContract{})" do
39 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
40 | DomainResourceAPI.build_files_to_generate(%{})
41 | end
42 | end
43 | end
44 |
45 | describe "build_action_files_paths/1" do
46 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (list)" do
47 | domain_contract = domain_contract_fixture()
48 |
49 | assert DomainResourceAPI.build_action_files_paths(domain_contract) |> is_list()
50 | end
51 |
52 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (%DomainContract{})" do
53 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
54 | DomainResourceAPI.build_action_files_paths(%{})
55 | end
56 | end
57 | end
58 |
59 | describe "build_test_action_files_paths/1" do
60 | test "can be invoked with the correct argument type (%DomainContract{}) and returns the expected type (list)" do
61 | domain_contract = domain_contract_fixture()
62 |
63 | assert DomainResourceAPI.build_test_action_files_paths(domain_contract) |> is_list()
64 | end
65 |
66 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (%DomainContract{})" do
67 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
68 | DomainResourceAPI.build_test_action_files_paths(%{})
69 | end
70 | end
71 | end
72 |
73 | # The SYNC tests for `generate_actions/1` at test/elixir_scribe/generator/domain_resource_api_sync_test.exs
74 | describe "generate_actions/1" do
75 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (%DomainContract{})" do
76 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
77 | DomainResourceAPI.generate_actions(%{})
78 | end
79 | end
80 | end
81 |
82 | # The SYNC tests for `generate_tests/1` at test/elixir_scribe/generator/domain_resource_api_sync_test.exs
83 | describe "generate_tests/1" do
84 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (%DomainContract{})" do
85 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
86 | DomainResourceAPI.generate_tests(%{})
87 | end
88 | end
89 | end
90 |
91 | # The SYNC tests for `generate_api/1` at test/elixir_scribe/generator/domain_resource_api_sync_test.exs
92 | describe "generate_api/1" do
93 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (%DomainContract{})" do
94 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
95 | DomainResourceAPI.generate_api(%{})
96 | end
97 | end
98 | end
99 |
100 | # The SYNC tests for `generate_test_fixture/1` at test/elixir_scribe/generator/domain_resource_api_sync_test.exs
101 | describe "generate_test_fixture/1" do
102 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (%DomainContract{})" do
103 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
104 | DomainResourceAPI.generate_test_fixture(%{})
105 | end
106 | end
107 | end
108 |
109 | # The SYNC tests for `generate_new_files/1` at test/elixir_scribe/generator/domain_resource_api_sync_test.exs
110 | describe "generate_new_files/1" do
111 | test "raises a FunctionClauseError when isn't invoked with the correct argument type (%DomainContract{})" do
112 | assert_raise FunctionClauseError, ~r/^no function clause matching in.*$/s, fn ->
113 | DomainResourceAPI.generate_new_files(%{})
114 | end
115 | end
116 | end
117 | end
118 |
--------------------------------------------------------------------------------