Skip to content

Commit

Permalink
add actionmodel concern
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkazar committed Feb 11, 2014
1 parent c0010fc commit 52834e2
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 55 deletions.
10 changes: 10 additions & 0 deletions Guardfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# A sample Guardfile
# More info at https://github.com/guard/guard#readme

guard :rspec do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/actionmodel/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch('lib/actionmodel.rb') { 'spec' }
watch('spec/spec_helper.rb') { 'spec' }
end

4 changes: 4 additions & 0 deletions actionmodel.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ['lib']

spec.add_dependency 'activesupport'

spec.add_development_dependency 'bundler', '~> 1.5'
spec.add_development_dependency 'rake'
spec.add_development_dependency 'rspec'
spec.add_development_dependency 'guard'
spec.add_development_dependency 'guard-rspec'
spec.add_development_dependency 'coveralls'
end
1 change: 1 addition & 0 deletions lib/actionmodel.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'actionmodel/version'
require 'actionmodel/concern'

module ActionModel
end
143 changes: 93 additions & 50 deletions lib/actionmodel/concern.rb
Original file line number Diff line number Diff line change
@@ -1,57 +1,100 @@
module Concern

# Add action to model.
#
# @example Add viewable action
# class Post
# act_as :viewable
# ...
# end
#
# @example Add sortable action
# class User
# act_as_sortable :email, :name
# ...
# end
def act_as(*args)
args.each do |action|
action_class = action_module(action)
unless action_class
raise ActionModel::NameError, "Action #{action} is not found in [#{self.name.demodulize}::#{action.to_s.camelize}, Actions::#{action.to_s.camelize}]."
end
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/object/blank'
require 'active_support/concern'
require 'actionmodel/context'

actions[action.to_sym] ||= ActionModel::Context.new
module ActionModel
module Concern
extend ActiveSupport::Concern

include action_class
# Return actions context hash for model instance.
def actions
self.class.actions
end
end

# Action module for action name
#
# @param action [Symbol/String] action name
def action_module(action)
"#{self.name.demodulize}::#{action.to_s.camelize}".safe_constantize ||
"Actions::#{action.to_s.camelize}".safe_constantize
end
module ClassMethods

# Add actions to model.
#
# Add single action to model
#
# class Post
# act_as :searchable
# ...
# end
#
# Add actions to model
#
# class Post
# act_as :searchable, :viewable
# ...
# end
#
# Actions can be added with options. Options applied for each action
#
# class Post
# act_as :searchable, ignorecase: true
# ...
# end
def act_as(*args)
options = args.extract_options!

args.each { |action_name| include_action action_name, options }
end

# Return actions context hash for model class.
def actions
@actions ||= {}
end

# Add action to model.
#
#
# @example Add historyable action
# class Rank
# act_as_historyable :value, :boost, autofill: true
# ...
# end
def method_missing(method, *args, &block)
action = method[/^act_as_(.+)/, 1]
if action.nil?
super method, *args, &block
else
include_action *args.unshift(action)
end
end

private

# Add action to model.
#
#
# @example Add historyable action
# class Rank
# act_as_historyable :value, :boost, autofill: true
# ...
# end
def method_missing(method, *args, &block)
action = method[/^act_as_(.+)/, 1]
if action.nil?
super(method, *args, &block)
else
options = args.extract_options!
action = action.to_sym
actions[action] = OpenStruct.new(args: args, options: options)

act_as action
# Include action into model
def include_action(*args)
return if args.empty?

action_name = args.shift.to_sym

action = action_module action_name
unless action
raise 'Action %1$s is not found in [%2$s::%3$s, Actions::%3$s].' %
[ action_name, self.name.demodulize, action_name.to_s.camelize ]
end

find_or_create_action(action_name).update *args

include action unless include? action
end

# Find or create action by action name
def find_or_create_action(action_name)
actions[action_name] ||= ActionModel::Context.new
end

# Return action module by action name
def action_module(action)
"#{self.name.demodulize}::#{action.to_s.camelize}".safe_constantize ||
"Actions::#{action.to_s.camelize}".safe_constantize
end
end
end
end
end

19 changes: 19 additions & 0 deletions lib/actionmodel/context.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
module ActionModel
class Context
def options
@options ||= {}
end

def fields
@fields ||= {}
end

def update(*args)
options = args.extract_options!

if args.empty?
self.options.merge! options
else
args.each do |field|
fields[field.to_sym] ||= {}
fields[field.to_sym].merge! options
end
end
end
end
end
4 changes: 0 additions & 4 deletions lib/actionmodel/name_error.rb

This file was deleted.

104 changes: 104 additions & 0 deletions spec/concern_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
require 'spec_helper'

describe ActionModel::Concern do
let(:model) { Class.new { include ActionModel::Concern } }

before do
allow(model).to receive(:name).and_return 'Post'
model.public_class_method :include_action, :find_or_create_action, :action_module
end

describe '.act_as' do
it 'include action' do
expect(model).to receive(:include_action).with :searchable, {}
model.act_as :searchable
end

it 'include action with options' do
expect(model).to receive(:include_action).with :searchable, { ignorecase: true }
model.act_as :searchable, ignorecase: true
end
end

describe '.include_action' do
context 'action module exists' do
let(:action) { double :action }

before do
allow(model).to receive(:action_module).and_return action
allow(model).to receive(:include?).and_return false
end

it 'update action context' do
allow(model).to receive(:include)
expect_any_instance_of(ActionModel::Context).to receive(:update)
model.include_action :searchable
end

it 'update action context with options' do
allow(model).to receive(:include)
expect_any_instance_of(ActionModel::Context).to receive(:update).with(ignorecase: true)
model.include_action :searchable, ignorecase: true
end

it 'include action module' do
expect(model).to receive(:include).with action
model.include_action :searchable
end
end

context 'action module does not exists' do
before { allow(model).to receive(:action_module).and_return nil }

it 'raise exception' do
expect { model.include_action :unknownable }.to raise_error RuntimeError, 'Action unknownable is not found in [Post::Unknownable, Actions::Unknownable].'
end
end
end

describe '.action_module' do
let(:action) { "#{model.name.demodulize}::Knownable" }

context 'when a model action exists' do
before { allow(ActiveSupport::Inflector).to receive(:safe_constantize).with(action).and_return action }

subject { model.action_module :knownable }

it { should eq action }
end

context 'when a global action exists' do
let(:action) { 'Actions::Knownable' }

before { allow(ActiveSupport::Inflector).to receive(:safe_constantize).and_return nil, action }

subject { model.action_module :knownable }

it { should eq action }
end

context 'when the action model is not exist' do
before { allow(ActiveSupport::Inflector).to receive(:safe_constantize).and_return nil }

subject { model.action_module(:unknownable) }

it { should be_nil }
end
end

describe '.method_missing' do
context 'without action method' do
it 'call class method' do
expect(model).to receive(:class_method)
model.class_method
end
end

context 'with action method' do
it 'config action' do
expect(model).to receive(:include_action).with 'searchable', :name, ignorecase: true
model.act_as_searchable :name, ignorecase: true
end
end
end
end
5 changes: 4 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
require 'actionmodel'

require 'coveralls'
Coveralls.wear!

RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
config.run_all_when_everything_filtered = true
config.filter_run :focus

Expand All @@ -11,4 +12,6 @@
# the seed, which is printed after each run.
# --seed 1234
config.order = 'random'

config.expect_with(:rspec) { |c| c.syntax = :expect }
end

0 comments on commit 52834e2

Please sign in to comment.