Last active
August 29, 2015 14:14
-
-
Save beezee/48a9edeb29ada7929e45 to your computer and use it in GitHub Desktop.
Putting your laundry away without side effects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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