From 2bdde8873e241eb57d72847730c31e14f2be5715 Mon Sep 17 00:00:00 2001 From: Szymon Soppa Date: Wed, 19 Feb 2025 07:07:31 +0100 Subject: [PATCH 1/3] Adds better initial page for hexdocs as well as structure of the rest of docs --- README.md | 2 +- lib/contexted.ex | 209 ++++++++++++++++++++++++++++++++++++++++++++++- mix.exs | 34 +++++++- 3 files changed, 240 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8bb3202..60917d2 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Add the following to your `mix.exs` file: ```elixir defp deps do [ - {:contexted, "~> 0.3.3"} + {:contexted, "~> 0.3.4"} ] end ``` diff --git a/lib/contexted.ex b/lib/contexted.ex index 710f2d7..12d4b29 100644 --- a/lib/contexted.ex +++ b/lib/contexted.ex @@ -1,3 +1,210 @@ defmodule Contexted do - @moduledoc false + @moduledoc ~S""" + + Contexted helps you structure Phoenix contexts by enforcing separation, allowing modularization, and generating common CRUD operations. + + ## Features + + - `Contexted.Tracer` - trace and enforce definite separation between specific context modules. + - `Contexted.Delegator` - divide the big context module into smaller parts and use delegations to build the final context. + - `Contexted.CRUD` - auto-generate the most common CRUD operations whenever needed. + +
+ + ## Installation + + Add the following to your `mix.exs` file: + + ```elixir + defp deps do + [ + {:contexted, "~> 0.3.4"} + ] + end + ``` + + Then run `mix deps.get`. + +
+ + ## Step by step overview + + To describe a sample usage of this library, let's assume that your project has three contexts: + + - `Account` + - `Subscription` + - `Blog` + + Our goal, as the project grows, is to: + + 1. Keep contexts separate and not create any cross-references. For this to work, we'll raise errors during compilation whenever such a cross-reference happens. + 2. Divide each context into smaller parts so that it is easier to maintain. In this case, we'll refer to each of these parts as **Subcontext**. It's not a new term added to the Phoenix framework but rather a term proposed to emphasize that it's a subset of Context. For this to work, we'll use delegates. + 3. Not repeat ourselves with common business logic operations. For this to work, we'll be using CRUD functions generator, since these are the most common. + +
+ + ### Keep contexts separate + + It's very easy to monitor cross-references between context modules with the `contexted` library. + + First, add `contexted` as one of the compilers in _mix.exs_: + + ```elixir + def project do + [ + ... + compilers: [:contexted] ++ Mix.compilers(), + ... + ] + end + ``` + + Next, define a list of contexts available in the app inside config file: + + ```elixir + config :contexted, contexts: [ + # list of context modules goes here, for instance: + # [App.Account, App.Subscription, App.Blog] + ] + ``` + + And that's it. From now on, whenever you will cross-reference one context with another, you will see an error raised during compilation. Here is an example of such an error: + + ``` + == Compilation error in file lib/app/accounts.ex == + ** (RuntimeError) You can't reference App.Blog context within App.Accounts context. + ``` + + See more in [Contexted.Tracer](https://hexdocs.pm/contexted/Contexted.Tracer.html). + +
+ + #### Exclude files and folders from cross-references context check + + In special cases, you may need to exclude certain folders or files from cross-reference checks due to project structure or naming conventions. To do this, add a list of exclusions in config `exclude_paths` option: + + ```elixir + config :contexted, + exclude_paths: ["app/test"] + ``` + +
+ + ### Dividing each context into smaller parts + + To divide big Context into smaller Subcontexts, we can use `delegate_all/1` macro from `Contexted.Delegator` module. + + Let's assume that the `Account` context has `User`, `UserToken` and `Admin` resources. Here is how we can split the context module: + + ```elixir + # Users subcontext + + defmodule App.Account.Users do + def get_user(id) do + ... + end + end + + # UserTokens subcontext + + defmodule App.Account.UserTokens do + def get_user_token(id) do + ... + end + end + + # Admins subcontext + + defmodule App.Account.Admins do + def get_admin(id) do + ... + end + end + + # Account context + + defmodule App.Account do + import Contexted.Delegator + + delegate_all App.Account.Users + delegate_all App.Account.UserTokens + delegate_all App.Account.Admins + end + ``` + + From now on, you can treat the `Account` context module as the API for the "outside" world. + + Instead of calling: + + ```elixir + App.Account.Users.find_user(1) + ``` + + You will simply do: + + ```elixir + App.Account.find_user(1) + ``` + +
+ + #### Being able to access docs and specs in auto-delegated functions + + Both docs and specs are attached as metadata of module once it's compiled and saved as `.beam`. In reference to the example of `App.Account` context, it's possible that `App.Account.Users` will not be saved in `.beam` file before the `delegate_all` macro is executed. Therefore, first, all of the modules have to be compiled, and saved to `.beam` and only then we can create `@doc` and `@spec` of each delegated function. + + As a workaround, in `Contexted.Tracer.after_compiler/1` all of the contexts `.beam` files are first deleted and then recompiled. This is an opt-in functionality, as it extends compilation time. If you want to enable it, set the following config values: + + ```elixir + config :contexted, + app: :your_app_name, # replace 'your_app_name' with your real app name + enable_recompilation: true + ``` + + You may also want to enable it only for certain environments, like `dev`. + + *Please also note that when this functionality is enabled, during the recompilation process, warnings are temporarily silenced to avoid logging conflict warnings. It will still log warnings as intended, during the first compilation, therefore it won't have any affect on normal compilation flow.* + + See more in [Contexted.Delegator](https://hexdocs.pm/contexted/Contexted.Delegator.html). + +
+ + ### Don't repeat yourself with CRUD operations + + In most web apps CRUD operations are very common. Most of these, have the same pattern. Why not autogenerate them? + + Here is how you can generate common CRUD operations for `App.Account.Users`: + + ```elixir + defmodule App.Account.Users do + use Contexted.CRUD, + repo: App.Repo, + schema: App.Accounts.User + end + ``` + + This will generate the following functions: + + ```elixir + iex> App.Accounts.Users.__info__(:functions) + [ + change_user: 1, + change_user: 2, + create_user: 0, + create_user: 1, + create_user!: 0, + create_user!: 1, + delete_user: 1, + delete_user!: 1, + get_user: 1, + get_user!: 1, + list_users: 0, + update_user: 1, + update_user: 2, + update_user!: 1, + update_user!: 2 + ] + ``` + + See more in [Contexted.CRUD](https://hexdocs.pm/contexted/Contexted.CRUD.html). + """ end diff --git a/mix.exs b/mix.exs index 69355b0..69dbd85 100644 --- a/mix.exs +++ b/mix.exs @@ -1,16 +1,20 @@ defmodule Contexted.MixProject do use Mix.Project + @version "0.3.4" + @github_url "https://github.com/curiosum-dev/contexted" + def project do [ app: :contexted, description: "Contexted is an Elixir library designed to streamline the management of complex Phoenix contexts in your projects, offering tools for module separation, subcontext creation, and auto-generating CRUD operations for improved code maintainability.", - version: "0.3.3", + version: @version, elixir: "~> 1.14", start_permanent: Mix.env() == :prod, deps: deps(), - package: package() + package: package(), + docs: docs() ] end @@ -34,8 +38,32 @@ defmodule Contexted.MixProject do [ licenses: ["MIT"], maintainers: ["Curiosum"], - links: %{"GitHub" => "https://github.com/curiosum-dev/contexted"}, + links: %{"GitHub" => @github_url}, files: ~w(lib mix.exs README.md LICENSE) ] end + + defp docs do + [ + main: "Contexted", + source_ref: "v#{@version}", + source_url: @github_url, + groups_for_modules: [ + Setup: [ + Contexted + ], + Features: [ + Contexted.Tracer, + Contexted.Delegator, + Contexted.CRUD + ], + Helpers: [ + Contexted.ModuleAnalyzer + ], + Utils: [ + Contexted.Utils + ] + ] + ] + end end From 605a595c96032656ed64714d5153fac278c0c10b Mon Sep 17 00:00:00 2001 From: Szymon Soppa Date: Wed, 19 Feb 2025 07:21:03 +0100 Subject: [PATCH 2/3] Attem to fix code execution in Contexted module docs --- lib/contexted.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/contexted.ex b/lib/contexted.ex index 12d4b29..6632974 100644 --- a/lib/contexted.ex +++ b/lib/contexted.ex @@ -184,6 +184,7 @@ defmodule Contexted do This will generate the following functions: + ``` ```elixir iex> App.Accounts.Users.__info__(:functions) [ @@ -204,6 +205,7 @@ defmodule Contexted do update_user!: 2 ] ``` + ``` See more in [Contexted.CRUD](https://hexdocs.pm/contexted/Contexted.CRUD.html). """ From e85e17f1fed5ccac78661ab56f756730ccd3a178 Mon Sep 17 00:00:00 2001 From: Szymon Soppa Date: Wed, 19 Feb 2025 07:25:48 +0100 Subject: [PATCH 3/3] Drops examples of running command in terminal in Contexted module doc --- lib/contexted.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/contexted.ex b/lib/contexted.ex index 6632974..ff6a77e 100644 --- a/lib/contexted.ex +++ b/lib/contexted.ex @@ -185,8 +185,6 @@ defmodule Contexted do This will generate the following functions: ``` - ```elixir - iex> App.Accounts.Users.__info__(:functions) [ change_user: 1, change_user: 2, @@ -205,7 +203,6 @@ defmodule Contexted do update_user!: 2 ] ``` - ``` See more in [Contexted.CRUD](https://hexdocs.pm/contexted/Contexted.CRUD.html). """