Boundary

repository·master·Indexed 21 days ago

https://github.com/sasa1977/boundary

An Elixir library and Mix compiler that manages and restricts cross-module dependencies to enforce architectural boundaries. It prevents unauthorized calls between application layers by defining allowed dependencies (deps) and visible modules (exports). Boundary supports restricting external OTP application usage, nested boundaries, and provides Mix tasks to visualize function and module dependencies using Graphviz DOT language.

Tokens
3.4K
Snippets
16
Records
17
Agent score
76%

What's inside boundary

  1. How boundaries and module exports work

    master

    A boundary is a named group of one or more modules. Boundary automatically determines which modules belong to a boundary based on the boundary's name. For example, a boundary named MySystem includes the MySystem module and any module starting with MySystem. (e.g., MySystem.User).

    Each boundary defines:

    1. deps: A list of other boundaries that this boundary is allowed to depend on.
    2. exports: A list of specific modules within this boundary that are visible to other boundaries. If a module is not listed in exports, other boundaries cannot invoke its functions.
    3. top_level?: (Optional) Used in application modules to define them as top-level boundaries.

    During compilation, the boundary compiler reports any cross-module function calls that violate these rules.

    # Example of defining boundaries
    defmodule MySystem do
      use Boundary, deps: [], exports: []
    end
    
    defmodule MySystemWeb do
      # MySystemWeb can depend on MySystem, but only access the Endpoint module
      use Boundary, deps: [MySystem], exports: [Endpoint]
    end
    
    defmodule MySystem.Application do
      # Top level boundary allowed to depend on both
      use Boundary, top_level?: true, deps: [MySystem, MySystemWeb]
    end
  2. Start the MySystem Phoenix server

    master

    To run the MySystem Phoenix application locally, follow these steps:

    1. Install the required Elixir dependencies using Mix.
    2. Start the Phoenix endpoint.
    3. Access the application in your browser at http://localhost:4000.

    For production deployment, refer to the official Phoenix deployment guides.

    mix deps.get
    mix phx.server
  3. Restrict usage of external applications

    master

    By default, Boundary does not check calls to external applications. You can enable this by configuring the boundary key in your mix.exs project definition.

    You can set a default configuration that applies to all boundaries using the check: [apps: [...]] option. This allows you to enforce rules such as preventing Phoenix usage in your context layer or limiting Ecto usage to specific boundaries.

    To allow a boundary to use an external app, include that app in its deps list. Note that if you want to allow a specific sub-module of an app (like Ecto.Changeset) but not the whole app, you must list both or specifically handle the sub-module as a dependency.

    # mix.exs configuration to check external apps
    defmodule MySystem.MixProject do
      def project do
       [
         boundary: [
           default: [
             check: [
               apps: [:phoenix, :ecto, {:mix, :runtime}]
             ]
           ]
         ]
       ]
      end
    end
    
    # In your modules, allow the specific apps
    defmodule MySystemWeb do
      use Boundary, deps: [Phoenix, Ecto.Changeset]
    end
    
    defmodule MySystem do
      use Boundary, deps: [Ecto, Ecto.Changeset]
    end
  4. Install Boundary

    master

    To use Boundary, add it as a dependency in your mix.exs file. It should be set with runtime: false because it functions as a Mix compiler.

    Additionally, you must register :boundary in your project's compilers list within the project/0 function to ensure the boundary rules are validated during the compilation process.

    # mix.exs
    
    defmodule MySystem.MixProject do
      use Mix.Project
    
      defp deps do
        [
          {:boundary, "~> 0.10", runtime: false}
        ]
      end
    
      def project do
        [
          compilers: [:boundary] ++ Mix.compilers(),
          # ...
        ]
      end
    end
  5. Understand Boundary compiler warnings

    master

    When a module attempts to call a function in a module that is not permitted by your defined boundaries, the compiler emits a warning.

    Example warning format:

    warning: forbidden reference to MySystemWeb
        (references from MySystem to MySystemWeb are not allowed)
        lib/my_system/user.ex:3
        See https://hexdocs.pm/boundary/Mix.Tasks.Compile.Boundary.html for details.
  6. How nested boundaries work

    master

    Nested boundaries allow you to control dependencies within a parent boundary.

    • Inheritance: Sub-boundaries inherit deps from ancestors by default. If a sub-boundary is marked type: :strict, it does not inherit deps and must list its own.
    • Exports: A parent boundary can export modules that are themselves exported by sub-boundaries.
    • Root modules: A sub-boundary's root module can be exported by the parent while still defining its own internal constraints.
    • Promotion: You can use top_level?: true to treat a nested module as a top-level boundary, though this is discouraged as it mismatches the namespace hierarchy.
    defmodule BlogEngine.Articles do
      # This is a sub-boundary of BlogEngine
      use Boundary, deps: [BlogEngine.{Accounts, Repo}], exports: [Article]
    end
  7. Handle violations with `dirty_xrefs` and `check` settings

    master

    If you cannot immediately resolve a boundary violation, you can use these mechanisms to relax rules:

    1. dirty_xrefs: A list of modules whose invocations will be ignored by Boundary.
    2. check settings: Control incoming (in) and outgoing (out) call checks. Setting both to false makes a boundary ignored.

    Note: check settings can only be applied to top-level boundaries.

    defmodule MySystem do
      use Boundary,
        dirty_xrefs: [MySystemWeb.Router.Helpers],
        check: [in: true, out: false]
    end
  8. Define a boundary using `use Boundary`

    master

    A boundary is defined by calling use Boundary in a root module. By default, a boundary includes the root module and all modules whose names start with that module's name (e.g., MySystem includes MySystem.*).

    Modules can be manually reclassified using the :classify_to option, which is specifically allowed for Mix tasks and protocol implementations.

    defmodule MySystem do
      use Boundary, deps: [], exports: []
    end
    
    # Manually classifying a Mix task
    defmodule Mix.Tasks.SomeTask do
      use Boundary, classify_to: MySystem.Mix
      use Mix.Task
    end
    
    # Manually classifying a protocol implementation
    defimpl String.Chars, for: MySchema do
      use Boundary, classify_to: MySystem
    end
  9. Add the Boundary compiler to Mix

    master

    To enable boundary enforcement during compilation, add :boundary to your compilers list in mix.exs.

    When developing a library, it is recommended to only include the compiler in :dev and :test environments to avoid overhead in production builds.

    # For an application
    def project do
      [
        compilers: [:boundary] ++ Mix.compilers(),
        # ...
      ]
    end
    
    # For a library (conditional inclusion)
    def project do
      [
        compilers: extra_compilers(Mix.env()) ++ Mix.compilers(),
        # ...
      ]
    end
    
    defp extra_compilers(:prod), do: []
    defp extra_compilers(_env), do: [:boundary]
  10. Install and enable Boundary in Mix

    master

    To enforce boundary rules during compilation, you must add :boundary to your project's compilers list in mix.exs.

    # mix.exs
    defmodule MySystem.MixProject do
      use Mix.Project
    
      def project do
        [
          compilers: [:boundary] ++ Mix.compilers(),
          # ...
        ]
      end
    end
    def project do
          [
            compilers: [:boundary] ++ Mix.compilers(),
            # ...
          ]
        end
  11. Configure boundary exports

    master

    A boundary always exports its root module. You can use the :exports option to specify additional modules that are accessible to other boundaries.

    Supported export patterns:

    • Explicit list: exports: [User]
    • Mass export: exports: :all (exports all modules and sub-boundaries)
    • Mass export with exclusions: exports: {:all, except: [SomeMod]}
    • Namespace mass export: exports: [{Schemas, except: [Base]}] (exports all MySystem.Schemas.* except MySystem.Schemas.Base)
    defmodule MySystem do
      use Boundary, exports: [User]
    end
    
    # Mass export example
    defmodule MySystem do
      use Boundary, exports: [{Schemas, except: [Base]}]
    end