I’ve gone through various iterations of experiments to more quickly prototype and enforce conventions in my contexts. Generally, my app has a lot of CRUD and repetition in the contexts. I don’t mind so much in the areas with complexity, churn, etc, but some of the areas rarely get touched. However, sometimes the conventions for all of these CRUD functions might take on slight changes in naming, arguments, specs, etc and it’s not a great time updating throughout the app. While the functions are not difficult to write at all, I’m questioning some of the maintenance burden and the ability to enforce consistency.
I know that there are “admin panels” and such that can generate everything from database updates to templates. However, in most cases I’m not willing to cede that much control. I’d like to be able to add custom functions alongside those CRUD functions generated within the context and easily transition the CRUD functions to custom when needed without friction.
I’ve experimented with the API below and I’m curious if others are able to fairly easily understand it.
In short, these Crud.*
macros generate functions (commented above each) that follow the naming conventions and arguments of my app-specific context functions. They are purposefully limited to require all arguments with no custom options. The concept is that if I need the generated function to be named or operate in ANY different way, that it should just be written out. I’ve tried out more customizable macros, but that seems to result in spending more time trying to remember what is/isn’t supported than just writing the function out in the first place. This would hopefully allow easily seeing those functions that follow the conventions 100% and those that necessarily do not. Any updates to the conventions could easily be made in the macros.
defmodule MyApp.Inventory.Unit do
# In practice, require/alias would be pulled in via a shared context `use`
require MyApp.Crud
use MyApp.Crud
alias MyApp.Inventory.Unit
# Configure the resource schema and repo.
@resource Crud.config(Unit, Repo)
#######################################################################
# READ QUERIES
#
# This is very specific to my app, but all of these generated functions take an optional
# `queries` argument. This allows passing public preload, join, order, and filter functions.
#
# Example:
#
# list_units(fn query ->
# query
# |> preload_unit_property()
# |> filter_by_one(property)
# |> order_units_by_name()
# end)
#######################################################################
# list_units(queries \\ & &1)
Crud.list(@resource)
# paginate_units(page \\ 1, page_size \\ 25, queries \\ & &1)
Crud.paginate(@resource)
# get_unit!(id, queries \\ & &1)
Crud.get!(@resource)
# get_unit(id, queries \\ & &1)
Crud.get(@resource)
# The "get for" functions convention in my app is specifically for cases where there is a
# one-to-one relationship between two resources. This example would assume that `unit`
# `belongs_to` a `network.
#
# get_unit_for_network!(network, queries \\ & &1)
Crud.get_for!(@resource, :network)
# get_unit_for_network(network, queries \\ & &1)
Crud.get_for(@resource, :network)
# get_unit_by_name!(name, queries \\ & &1)
Crud.get_by_attr!(@resource, :name)
# get_unit_by_name(name, queries \\ & &1)
Crud.get_by_attr(@resource, :name)
# preload_unit_property(query)
Crud.preload(@resource, :property)
# join_unit_property(query)
Crud.join(@resource, :property)
# Only ONE of these three macros would be called depending upon whether one,
# many, or one or many are allowed. The generated function name is the same
# for all.
#
# filter_units_by_property(query, property)
Crud.filter_by_one(@resource, :property)
Crud.filter_by_many(@resource, :property)
Crud.filter_by_one_or_many(@resource, :property)
# order_units_by_name(query)
Crud.order_by(@resource, :name, :desc)
#######################################################################
# CREATE, UPDATE, DELETE
#
# create, change, and upate are passed a changeset function
#######################################################################
# new_unit()
Crud.new(@resource)
# create_unit(attrs)
Crud.create(@resource, &Unit.changeset/2)
# change_unit(unit, attrs \\ %{})
Crud.change(@resource, &Unit.changeset/2)
# update_unit(unit, attrs)
Crud.update(@resource, &Unit.changeset/2)
# delete_unit(unit)
Crud.delete(@resource)
end
UPDATE: The underlying macros can be viewed in this gist. I’m more focused on the desired API at this point and these macros could also use some work (enforcing struct args, maybe specs, documentation, etc).
In practice, a lot of context resource modules would end up as simple as something like the following:
defmodule MyApp.Inventory.Unit do
use MyApp.Contexts
alias MyApp.Inventory.Unit
@resource Crud.config(Unit, Repo)
# Common CRUD functions
Crud.list(@resource)
Crud.get!(@resource)
Crud.get(@resource)
Crud.new(@resource)
Crud.create(@resource, &Unit.changeset/2)
Crud.change(@resource, &Unit.changeset/2)
Crud.update(@resource, &Unit.changeset/2)
Crud.delete(@resource)
# CRUD helpers
Crud.paginate(@resource)
Crud.get_for!(@resource, :network)
Crud.get_for(@resource, :network)
Crud.get_by_attr!(@resource, :name)
Crud.get_by_attr(@resource, :name)
Crud.preload(@resource, :property)
Crud.join(@resource, :property)
Crud.filter_by_one(@resource, :property)
Crud.filter_by_one_or_many(@resource, :unit_type)
Crud.order_by(@resource, :name, :desc)
end