Skip to content

Instantly share code, notes, and snippets.

@beezee
Last active August 29, 2015 14:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save beezee/48a9edeb29ada7929e45 to your computer and use it in GitHub Desktop.
Save beezee/48a9edeb29ada7929e45 to your computer and use it in GitHub Desktop.
Putting your laundry away without side effects
# An exercise in controlled side effects inspired by
# http://tmd.io/code/2015/01/27/putting-away-your-laundry-a-look-at-service-objects-for-rails.html
# Generic "immutable" wrapper for hash of values and hash of validators
# Only unvalidated or Valid values come out of #to_h
# Validators can return ValidatedParams::Error('message') to come out of #errors
# This is a great candidate for property based testing and has no dependencies beyond hashes and procs
class ValidatedParams
Valid = true
Error = Struct.new(:msg)
def initialize(settings, validations={})
@settings = settings
@validations = validations
end
def key_valid?(key)
@validations[key].kind_of?(Proc) ?
@validations[key].call(@settings) == Valid :
true
end
def to_h
@settings.reject {|k, v| !key_valid?(k)}
end
def errors
e = Hash[@validations.map {|k, p| [k, p.call(@settings)] }]
.select {|k, v| v.kind_of? Error}
Hash[e.map {|k, v| [k, v.msg]}]
end
end
# convenience for building up the prop -> Proc hash
# that will constitute validations for the params passed
# to a user update
class UserParamValidations
def password_valid?(user)
->(params) {
# not trying to update pw, no error but not valid
return if params[:password].nil?
unless params[:password] == params[:password_confirmation]
return ValidatedParams::Error.new("Passwords do not match")
end
unless user.try(:authenticate, params[:current_password])
return ValidatedParams::Error.new("Current password is incorrect")
end
ValidatedParams::Valid
}
end
def self.for_user(user)
i = new
# we can strip off unwanted params without creating errors with noop
noop = ->(params) {}
{password: i.password_valid?(user), current_password: noop,
password_confirmation: noop}
end
end
# At this point the controller does little more than
# call a bunch of Rails methods (we can rely on Rails to test those)
# based on our validations. With solid tests around our validations
# we are fully covered, and those need nothing more than an
# object that implements try to mock the user, an input hash,
# and assertions against return values
class UsersController < ApplicationController
def update
respond_to do |format|
param_validations = UserParamValidations.for_user(current_user)
validated_params = ValidatedParams.new(user_params, param_validations)
if validated_params.errors.empty? &&
current_user.update_attributes(validated_params.to_h)
format.html { redirect_to my_path, notice: 'User successfully updated' }
format.json { render json: current_user.to_json, status: :ok }
else
format.html { redirect_to my_path, notice: validated_params.errors }
format.json { render json: { errors: validated_params.errors }, status: :unprocessable_entity }
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment