├── .editorconfig ├── .formatter.exs ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── assets └── svg │ └── github-sponsor.svg ├── examples └── scribe.sh ├── lib ├── docs │ └── modules │ │ └── behaviours │ │ ├── persona.ex │ │ └── persona_validator.ex ├── elixir_scribe.ex ├── elixir_scribe │ ├── behaviour_typed_contract.ex │ ├── generator │ │ ├── domain │ │ │ └── resource │ │ │ │ ├── build_action_files_paths │ │ │ │ └── build_action_files_paths_resource.ex │ │ │ │ ├── build_api_file_paths │ │ │ │ └── build_api_file_paths_resource.ex │ │ │ │ ├── build_domain_contract │ │ │ │ └── build_domain_resource_contract.ex │ │ │ │ ├── build_files_to_generate │ │ │ │ └── build_files_to_generate_resource.ex │ │ │ │ ├── build_test_action_files_paths │ │ │ │ └── build_test_action_files_paths_resource.ex │ │ │ │ ├── generate_actions │ │ │ │ └── generate_actions_resource.ex │ │ │ │ ├── generate_api │ │ │ │ └── generate_api_resource.ex │ │ │ │ ├── generate_new_files │ │ │ │ └── generate_new_files_resource.ex │ │ │ │ ├── generate_schema │ │ │ │ └── generate_schema_resource.ex │ │ │ │ ├── generate_test_fixture │ │ │ │ └── generate_test_fixture_resource.ex │ │ │ │ └── generate_tests │ │ │ │ └── generate_tests_resource.ex │ │ ├── domain_contract.ex │ │ ├── domain_resource_api.ex │ │ ├── schema │ │ │ └── resource │ │ │ │ ├── build_schema_contract │ │ │ │ └── build_schema_contract.ex │ │ │ │ └── schema_resource_helpers.ex │ │ ├── schema_contract.ex │ │ └── schema_resource_api.ex │ ├── mix │ │ ├── cli_command │ │ │ ├── parse │ │ │ │ └── parse_cli_command.ex │ │ │ └── parse_schema │ │ │ │ └── parse_schema_cli_command.ex │ │ ├── mix_api.ex │ │ └── shell │ │ │ └── prompt_for_file_conflicts │ │ │ └── prompt_for_file_conflicts_shell.ex │ ├── template │ │ ├── binding │ │ │ ├── build │ │ │ │ └── build_binding_template.ex │ │ │ └── rebuild │ │ │ │ └── rebuild_binding_template.ex │ │ ├── binding_api.ex │ │ ├── binding_api_contract_build_filename_for_action_file.ex │ │ ├── file │ │ │ ├── build_dir_path_for_html │ │ │ │ └── build_dir_path_for_html_file.ex │ │ │ ├── build_filename_for_action │ │ │ │ └── build_filename_for_action_file.ex │ │ │ └── inject │ │ │ │ ├── inject_content_before_module_end.ex │ │ │ │ └── inject_eex_template_before_module.ex │ │ ├── file_api.ex │ │ ├── module │ │ │ ├── build_embed_templates │ │ │ │ └── build_module_embed_templates.ex │ │ │ └── build_name │ │ │ │ ├── build_absolute_module_action_name.ex │ │ │ │ ├── build_absolute_module_action_name_aliases.ex │ │ │ │ ├── build_absolute_module_name.ex │ │ │ │ └── build_module_action_name.ex │ │ ├── module_api.ex │ │ ├── route │ │ │ └── scope │ │ │ │ └── scope_action_routes.ex │ │ └── route_api.ex │ └── utils │ │ ├── string │ │ ├── camel_case_to_sentence │ │ │ └── camel_case_to_sentence.ex │ │ ├── capitalize │ │ │ └── capitalize_string.ex │ │ ├── find_acronyms │ │ │ └── find_acronyms.ex │ │ ├── first_word │ │ │ └── first_word.ex │ │ └── human_capitalize │ │ │ └── human_capitalize_string.ex │ │ └── string_api.ex └── mix │ └── tasks │ ├── scribe.ex │ ├── scribe.gen.domain.ex │ ├── scribe.gen.ex │ └── scribe.gen.html.ex ├── mix.exs ├── mix.lock ├── priv └── templates │ ├── scribe.gen.domain │ ├── actions │ │ ├── action_module.ex │ │ ├── action_module_no_schema_access.ex │ │ ├── no_schema_access │ │ │ └── any_action.ex │ │ └── schema_access │ │ │ ├── create_schema.ex │ │ │ ├── default_schema.ex │ │ │ ├── delete_schema.ex │ │ │ ├── edit_schema.ex │ │ │ ├── list_schema.ex │ │ │ ├── new_schema.ex │ │ │ ├── read_schema.ex │ │ │ └── update_schema.ex │ ├── apis │ │ ├── api_function_no_schema_access.ex │ │ ├── api_module.ex │ │ ├── create_api_function.ex │ │ ├── default_api_function.ex │ │ ├── delete_api_function.ex │ │ ├── edit_api_function.ex │ │ ├── list_api_function.ex │ │ ├── new_api_function.ex │ │ ├── read_api_function.ex │ │ └── update_api_function.ex │ └── tests │ │ ├── actions │ │ ├── action_module_test.exs │ │ ├── action_module_test_no_schema_access.exs │ │ ├── no_schema_access │ │ │ └── module_any_action_test.exs │ │ └── schema_access │ │ │ ├── create_schema_test.exs │ │ │ ├── default_schema_test.exs │ │ │ ├── delete_schema_test.exs │ │ │ ├── edit_schema_test.exs │ │ │ ├── list_schema_test.exs │ │ │ ├── new_schema_test.exs │ │ │ ├── read_schema_test.exs │ │ │ └── update_schema_test.exs │ │ ├── fixtures.ex │ │ └── fixtures_module.ex │ └── scribe.gen.html │ ├── controllers │ ├── create_controller.ex │ ├── default_controller.ex │ ├── delete_controller.ex │ ├── edit_controller.ex │ ├── list_controller.ex │ ├── new_controller.ex │ ├── read_controller.ex │ └── update_controller.ex │ ├── html │ └── default │ │ ├── edit.html.heex │ │ ├── html.ex │ │ ├── list.html.heex │ │ ├── new.html.heex │ │ ├── read.html.heex │ │ └── resource_form.html.heex │ └── tests │ └── controllers │ ├── create_controller_test.exs │ ├── default_controller_test.exs │ ├── delete_controller_test.exs │ ├── edit_controller_test.exs │ ├── list_controller_test.exs │ ├── new_controller_test.exs │ ├── read_controller_test.exs │ └── update_controller_test.exs └── test ├── docs └── modules │ └── behaviours │ └── persona_validator_test.exs ├── elixir_scribe ├── behaviour_typed_contract_test.exs ├── generator │ ├── domain │ │ └── resource │ │ │ ├── build_action_files_paths │ │ │ └── build_action_files_paths_resource_test.exs │ │ │ ├── build_api_file_paths │ │ │ └── build_api_file_paths_resource_test.exs │ │ │ ├── build_domain_contract │ │ │ └── build_domain_resource_contract_test.exs │ │ │ ├── build_files_to_generate │ │ │ └── build_files_to_generate_resource_test.exs │ │ │ ├── build_test_action_files_paths │ │ │ └── build_test_action_files_paths_resource_test.exs │ │ │ ├── generate_actions │ │ │ └── generate_actions_resource_test.exs │ │ │ ├── generate_api │ │ │ └── generate_api_resource_test.exs │ │ │ ├── generate_new_files │ │ │ └── generate_new_files_resource_test.exs │ │ │ ├── generate_schema │ │ │ └── generate_schema_resource_test.exs │ │ │ ├── generate_test_fixture │ │ │ └── generate_test_fixture_resource_test.exs │ │ │ └── generate_tests │ │ │ └── generate_tests_resource_test.exs │ ├── domain_contract_test.exs │ ├── domain_resource_api_async_test.exs │ ├── domain_resource_api_sync_test.exs │ ├── schema │ │ └── resource │ │ │ └── build_schema_contract │ │ │ └── build_schema_contract_test.exs │ ├── schema_contract_test.exs │ └── schema_resource_api_test.exs ├── mix │ ├── cli_command │ │ └── parse │ │ │ └── parse_cli_command_test.exs │ ├── mix_api_test.exs │ └── shell │ │ └── prompt_for_file_conflicts │ │ └── prompt_for_file_conflicts_shell_test.exs ├── template │ ├── binding │ │ ├── build │ │ │ └── build_binding_template_test.exs │ │ └── rebuild │ │ │ └── rebuild_binding_template_test.exs │ ├── binding_api_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 │ ├── file_api_async_test.exs │ ├── file_api_sync_test.exs │ ├── module │ │ ├── build_embed_templates │ │ │ └── build_module_embed_templates_test.exs │ │ └── build_name │ │ │ ├── build_absolute_module_action_name_aliases_test.exs │ │ │ ├── build_absolute_module_action_name_test.exs │ │ │ ├── build_absolute_module_name_test.exs │ │ │ └── build_module_action_name_test.exs │ ├── module_api_test.exs │ └── route │ │ └── scope │ │ └── scope_action_routes_test.exs └── utils │ ├── string │ ├── camel_case_to_sentence │ │ └── camel_case_to_sentence_test.exs │ ├── capitalize │ │ └── capitalize_string_test.exs │ ├── find_acronyms │ │ └── find_acronyms_test.exs │ ├── first_word │ │ └── first_word_test.exs │ └── human_capitalize │ │ └── human_capitalize_string_test.exs │ └── string_api_test.exs ├── elixir_scribe_test.exs ├── mix └── tasks │ ├── scribe.gen.domain_async_test.exs │ ├── scribe.gen.domain_sync_test.exs │ ├── scribe.gen.html_async_test.exs │ ├── scribe.gen.html_sync_test.exs │ ├── scribe.gen_test.exs │ └── scribe_test.exs ├── mix_test_helper.exs ├── support ├── base_case.ex └── fixtures │ └── domain_generator_fixtures.ex └── test_helper.exs /.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 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ["Exadra37"] 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/svg/github-sponsor.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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.domain/apis/default_api_function.ex: -------------------------------------------------------------------------------- 1 | 2 | def <%= action %>(), do: <%= module_action_name %>.<%= action_first_word %>() 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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/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.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/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/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.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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/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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |