Mutations with forms in Rails (2.3)
6twenty opened this issue · comments
This isn't an issue or a question. This describes how I have integrated Mutations into form views within a Rails 2.3 app. This could be a discussion on how Mutations could evolve to have wider uses within Rails, or it could just be a reference for others looking to implement their own similar solution. This is a bit of an experiment, but comments & feedback are welcome!
This is what my controller actions look like:
def new
@mutation = CreateMutationClass.new
end
def create
@mutation = CreateMutationClass.new(params[:mutation])
outcome = @mutation.run
if outcome.success?
flash[:notice] = "Success"
redirect_to some_path
else
@errors = outcome.errors.message_list
render :action => :new
end
end
def edit
@mutation = UpdateMutationClass.new(MyModel.find(params[:id]))
end
def update
@mutation = UpdateMutationClass.new(MyModel.find(params[:id]), params[:mutation])
outcome = @mutation.run
if outcome.success?
flash[:notice] = "Success"
redirect_to some_path
else
@errors = outcome.errors.message_list
render :action => :edit
end
end
Note that on the mutation class I initially call new
rather than run
; this allows me to bind the Rails forms to the mutation rather than an ActiveRecord model. I am also passing in an ActiveRecord model as the first parameter on the edit
, and update
actions, which assigns attributes to the mutation straight from the model (more on that below).
In my views I then use form_for @mutation, :url => some_path
(the :url
is required here, although I suspect it would be possible to extend the mutation class to allow Rails to deduce the url).
My mutation classes are mostly unchanged, but with a few additions. Here's an example of the CreateMutationClass
.
class CreateMutationClass < Mutations::Command
include MutationExtensions::Base
use :new_record_mocks
use :model_name
required do
# ...
end
optional do
# ...
end
def execute
# ...
end
end
The required
& optional
blocks, and the execute
method, are all standard as per the mutations docs.
And the UpdateMutationClass
:
class UpdateMutationClass < Mutations::Command
include MutationExtensions::Base
use :existing_record_mocks
use :model_name
use :base_model, :my_model
map(:some_attr) { |model| model.method_to_determine_some_attr }
map(:another_attr) { Date.today }
required do
# ...
end
optional do
# ...
end
def execute
# ...
end
end
use :new_record_mocks
and use :existing_record_mocks
each set up the following methods: id
, to_param
, new_record?
and _destroy
. These allow Rails to render the form properly.
use :model_name
simply changes the input field base name, defaulting to 'mutation'
.
use :base_model
allows linking the mutation to an ActiveRecord model. This redefines the new
method to allow the first parameter to be an ActiveRecord model of the expected class. By default the mutation attributes will attempt to get their value by calling a method of the same name on the model, however using a base model also allows using the map
method to handle non-matching attributes. Passing in multiple parameters still works the same way (subsequent hash parameters will override existing attributes).
And here's the MutationExtensions
module:
module MutationExtensions
module Base
def self.included(base)
base.class_eval do
def self.use(*args)
case args.shift
when :model_name
@model_name = ::ActiveSupport::ModelName.new(args.first || 'mutation')
when :base_model
@base_model = args.first
self.send(:extend, BaseModel)
self.send(:required) do
model(*args)
end
when :new_record_mocks
self.send(:include, Mock::NewRecord)
when :existing_record_mocks
self.send(:include, Mock::ExistingRecord)
end
end
end
end
end
# Allows linking the mutation class to a model, so that
# new mutation instances can be instantiated by passing
# in a model instance as the first parameter
module BaseModel
def base_model
@base_model
end
def mappings
@mappings ||= {}.with_indifferent_access
end
def map(attribute, &block)
mappings[attribute] = block
end
def new(*args)
if args.first.class.model_name.underscore.to_sym == @base_model
record = args.shift
inputs_keys = input_filters.required_inputs.keys | input_filters.optional_inputs.keys
attrs = inputs_keys.inject({}) do |hash, key|
if proc = mappings[key]
hash[key] = proc.call(record)
elsif record.respond_to?(key)
hash[key] = record.send(key)
end
hash
end
attrs[@base_model] = record
args.unshift(attrs)
end
super(*args)
end
def association_reflection(association_name, associated_class)
@association_reflections ||= {}
@association_reflections[association_name.to_sym] = associated_class
end
def reflect_on_association(association_name)
OpenStruct.new(:klass => @association_reflections[association_name.to_sym])
end
end
# Mocks new or existing records, so that `form_for` renders ok
module Mock
module AnyRecord
def _destroy; nil; end
def to_param; id ? id.to_s : id; end
end
module NewRecord
include Mock::AnyRecord
def new_record?; true; end
def id; nil; end
end
module ExistingRecord
include Mock::AnyRecord
def new_record?; false; end
def id
if base_model = self.class.base_model
self.send(base_model).try(:id) || 0
else
0
end
end
end
end
end
@cypriss, I'd be interested to hear your thoughts on moving towards wider Rails support so that mutations can more easily apply to UI apps as well as APIs. I'm aware that there are other similar gems out there which integrate with Rails forms, but I've so far enjoyed working with mutations the most :-)
Hello, I've been looking at this gem to use in a normal Rails app and your post is very helpful @6twenty. Because having mutations work with the Rails form helpers is pretty important.
Could you say how this design has improved your code? I assume your models don't have any validations.
Is testing easier? Is the code cleaner? Etc.
I'm one of those people who has somewhat rejected the "Rails Kool-Aid", but nonetheless I'm practical, and model validations is probably one of the parts of "the Rails way" I'm OK with.
And one can pull out of a lot of business logic into Plain Old Ruby Classes without necessarily using this gem.
@leavengood we implemented this as part of a refactor for a specific area in our app. We had fallen into the very traps described in @cypriss's post introducing Mutations: we had a fat model with several virtual attributes which were being set via form params and handled using ActiveRecord callbacks. This (as the post says) sucked. The callbacks lost almost all context and had to re-determine the current state of the model before handling the changes correctly.
Could you say how this design has improved your code? I assume your models don't have any validations.
The refactored code is wonderfully simple, which is a pleasure since the previous code was quite complex. We ended up with ~10 mutation classes each with a very specific role. This led to some code duplication and probably more lines of code overall, but the code is significantly cleaner and vastly more maintainable.
Our models still have the usual validations -- we deliberately tried to leave the models as un-touched as possible because the refactor only focussed on a specific area of logic. The only changes on the model were to remove the relevant virtual attributes and callbacks.
Is testing easier? Is the code cleaner? Etc.
As you can imagine testing mutation classes is extremely simple. In contrast to testing virtual attributes + callbacks, our test suite is much cleaner (and faster).
And one can pull out of a lot of business logic into Plain Old Ruby Classes without necessarily using this gem.
This entered my mind as well, but to be honest we likely would've ended up with something very similar to this gem anyway. No need to reinvent the wheel.
@6twenty Thanks for the feedback. We already started using mutations in our project and so far so good. We even have a few small fixes and improvements to do as pull requests.
I've been thinking that leaving basic validations on the models is fine and the use mutations for more broad-scoped work. It sounds like that is mostly what you have done, so that is good to know.