In our last post we set up some code hygiene tools and for the most part got everything configured and working the way we want. Today we’ll take a closer look at Boundary.
If you followed along with part 1 you can continue with the code from there. Otherwise you can grab today’s starting off point by:
Terminal
Terminal
You should now see the following git history.
Terminal
Now you can install the existing dependencies, and set up the database.
Terminal
Terminal
Note: I’ve found I occasionally get some weird errors when attempting a git commit
after downloading a repo that has GitHooks
configured. Removing the hooks (rm -rf ./git/hooks
), and then reinstalling them (mix git_hooks.install
) seems to clear things up… so I’d try the same if you get some errors when attempting a commit.
Now let’s take a closer look at Boundary
.
What does Boundary actually do?
Before we get started with some code, let’s figure out what Boundary
actually does. Based on the documentation some examples of what you can do with Boundary
:
- Prevent invocations from the context layer to the web layer.
- Prevent invocations from the web layer to internal context modules.
- Prevent usage of Phoenix and Plug in the context layer.
- Limit usage of Ecto in the web layer to only Ecto.Changeset.
- Allow :mix modules to be used only at compile time.
So there are a number of things you can do with Boundary
, and a lot of it has to do with ensuring we have some structure and rules around how our modules interact with each other.
For our application, we’ll enforce that the Web layer can only call into the backend via the main CodeHygiene
module. I like this convention as it makes it very easy to see what your application’s API is; you don’t need to look thru a bunch of context modules. The drawback to this approach is you can get a pretty big main context module. I find this can be somewhat offset by delegating to sub modules.
Another convention we’ll adapt is using a separate top-level schema
namespace. This is based on the excellent series of posts by the author of Boundary
Saša Jurić. This means instead of the somewhat awkward convention of MyApp.Widgets.Widget
, we’ll instead have MyAppSchema.Widget
.
So to see this in action, let’s create some code!
Add some code
I typically add things manually versus using scaffolding, but I think in this case scaffolding will be pretty handy. Using scaffolding will also provide an example of what is different between the approach we are taking and the default scaffolding code. So let’s scaffold out a simple Product entity.
Initial scaffold code
Terminal
As per the output, we’ll add the necessary routes…
/lib/code_hygiene_web/router.ex
… and then run migrations.
Terminal
We’re already seeing a bunch of Boundary
errors popping up, we’ll ignore them for now and make sure our new Products
page is working.
Terminal
If we navigate to http://localhost:4000/products it looks like we’re all good.
So we’re now ready to fix up those pesky warnings.
Fix the warnings
The reason we’re seeing some warnings is we already set up some Boundary
rules in our first post and the scaffolding code is violating those rules. Specifically the controller code is calling directly into the Products
context module where-as we’ve specified everything needs to go thru the main API module (i.e. code_hygiene.ex
).
So the first step is to delegate those calls thru the main API module.
Delegating all front-end calls thru the API
First we’ll add defdelegates
to the API module.
/lib/code_hygiene.ex
So now we just need to replace the web calls that are currently running thru CodeHygiene.Product.something()
to the API function, i.e. CodeHygiene.something()
.
There is not much to say about these changes so I’m just going to vomit out the code…
🤮
/lib/code_hygiene_web/live/product_live/form_component.ex
/lib/code_hygiene_web/live/product_live/index.ex
/lib/code_hygiene_web/live/product_live/show.ex
The above will take care of almost all the Boundary warnings, we still got the Product
schema popping a warning however.
Terminal
To get rid of this we need to set up the schemas as a top level namespace.
Moving the schemas
The final step in getting rid of our warnings is to move the schemas into top level modules.
So we’ll create a top level lib
directory for our schemas and move over the existing Product
schema file.
Terminal
The only change we need to make to product.ex
is the namespace.
/lib/code_hygiene_schema/product.ex
Now we need a single top level schema module similar to how we have code_hygiene.ex
and code_hygiene_web.ex
that act as top level modules for the backend and web layers. The top level schema module will hold the Boundary
rules for the schemas.
Terminal
/lib/code_hygiene_schema.ex
Here we’re saying the schema module doesn’t depend on anything… and exports the Product
schema.
The final bit of code to close out this change is to update any references to the Product
schema module to now point to the new namespace, i.e. CodeHygieneSchema.Product
instead of CodeHygiene.Products.Product
.
/lib/code_hygiene/products.ex
/lib/code_hygiene_web/live/product_live/index.ex
/test/code_hygiene/products_test.exs
With these changes lets see where we are at.
Terminal
Oh no, we’ve gone backwards, even more errors than before!
This is easy enough to fix however, the piece we are missing is to indicate that our API and Web layers have a dependency on the schema layer. We can do this by updating the Boundary
statements in code_hygiene.ex
and code_hygiene_web.ex
.
/lib/code_hygiene.ex
/lib/code_hygiene_web.ex
Now running mix compile --warnings-as-errors
will run clean… sweet!
We’ve successfully enforced some boundaries around our code and for any new sub-context modules we simply need to defdelegate
the sub-context calls thru the main API
. For any new schemas we add we need to update the exports
list in code_hygiene_schema.ex
.
I usually add Typespecs for the functions in the API module… so before moving on, let’s do so.
Add Typespecs
First we need to add a type for the Product
schema.
/lib/code_hygiene_schema/product.ex
And now we can add specs to code_hygiene.ex
.
/lib/code_hygiene.ex
Nice!
What about generated code we want to keep as is
As mentionned previously I typically don’t use the scaffold generators so unlike in the last section I would just add new functionality / code that conformed to the Boundary
rules right off the bat; but what about a situation where we want to use some code generators and not mess with the generated code? A good example of this would be the mix phx.gen.auth
command which adds authentication to a project.
Although we could update the generated code to conform to our Boundary
rules like we we did with Products
; we might prefer to keep the authentication code as is; doing so will make it easier to apply any updates that might be applied to the Phx.Gen.Auth
. Let’s see how we would do this.
We’ll start by running the generator:
Terminal
And do as we are told:
Terminal
Terminal
Just like when we generated the Product
scaffolding we immediately see some Boundary
warnings.
It is super simple to get rid of these warnings however, we just need to export the main phx.gen.auth
context module (accounts.ex
) and the schema (user.ex
) in our API module:
/lib/code_hygiene.ex
Terminal
And we’re all good!
However what happens if we run our tests?
Terminal
We see we’re getting a warning, this is because the test fixture files are under the core CodeHygiene
namespace and thus fall under our Boundary
rules for our main context module.
To alleviate this, I like to move the fixtures into their own namespace (CodeHygieneFixtures
) and in the fixture modules disable the Boundary
checks with use Boundary, check: [in: false, out: false]
.
Let’s apply these changes to the accounts_fixtures
module.
/test/support/fixtures/accounts_fixtures.ex
The products fixture didn’t pop any Boundary
warnings but for continuity we’ll apply the same changes to the products fixture (and any future fixture modules).
/test/support/fixtures/products_fixtures.ex
We now have to update all our tests to refer to the new fixture namespace. I’ll show one example:
/test/code_hygiene_web/controllers/user_auth_test.exs
The following files need the same treatment, changing any instances of CodeHygiene.AccountsFixtures
to CodeHygieneFixtures.AccountsFixtures
and any instances of import CodeHygiene.ProductsFixtures
to CodeHygieneFixtures.ProductsFixtures
:
test/code_hygiene/accounts_test.exs
test/code_hygiene/products_test.exs
test/code_hygiene_web/controllers/user_confirmation_controller_test.exs
test/code_hygiene_web/controllers/user_registration_controller_test.exs
test/code_hygiene_web/controllers/user_reset_password_controller_test.exs
test/code_hygiene_web/controllers/user_session_controller_test.exs
test/code_hygiene_web/controllers/user_settings_controller_test.exs
test/code_hygiene_web/live/product_live_test.exs
We also need to change the register_and_log_in_user
helper in conn_case.ex
.
/test/support/conn_case.ex
Now if we run our tests, we won’t get any warnings!
Summary
So that’s it for today. We saw how we can use Boundary
to enforce some rules around the way our code interacts with itself.
Next time out we’ll look at the final piece of the puzzle which is ExDoc
. We already have ExDoc
included as part of our project… but with some customizations we can improve the utility of the documents being generated.
Today’s code
If you want to retrieve the GitHub commit that corresponds to today’s code:
Terminal
Terminal
You should now see the following git history.
Terminal
Thanks for reading and I hope you enjoyed the post!