guise 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/guise/syntax.rb CHANGED
@@ -5,98 +5,173 @@ require 'active_support/core_ext/string/inflections'
5
5
 
6
6
  module Guise
7
7
  module Syntax
8
+ # Setup the model's `guises` association. Given the following setup:
9
+ #
10
+ # ```ruby
11
+ # class User < ActiveRecord::Base
12
+ # has_guises :DeskWorker, :MailForwarder, association: :roles, attribute: :value
13
+ # end
14
+ # ```
15
+ #
16
+ # The following is configured:
17
+ #
18
+ # * `has_many` association named according to the `:association` option.
19
+ # * `User.desk_workers` and `User.mail_forwarders` model scopes.
20
+ # * `User#has_guise?` that checks if a user is a particular type.
21
+ # * `User#desk_worker?`, `User#mail_forwarder?` that proxy to `User#has_guise?`.
22
+ # * `User#has_guises?` that checks if a user has records for all the types
23
+ # supplied. This is aliased to `User#has_roles?`.
24
+ # * `User#has_any_guises?` that checks if a user has records for any of the
25
+ # types supplied. This is aliased to `User#has_any_roles?`.
26
+ # * If the association name is not `:guises`:
27
+ # * Aliases the association methods to equivalent methods for `guises`
28
+ # (i.e. `guises=` to `roles=`).
29
+ # * Aliases the introspection methods (i.e. `has_guise?` to `has_role?`
30
+ # and `has_any_guises?` to `has_any_roles?`
31
+ #
32
+ # @overload has_guises(*guises, options)
33
+ # @param [Array<Symbol, String>] *guises names of guises that should be
34
+ # allowed
35
+ # @param [Hash] options options to configure the association
36
+ # @option options [Symbol] :association name of the association to define.
37
+ # This option is required.
38
+ # @option options [Symbol] :attribute name of the association's column
39
+ # where the value of which guise is being represented is stored. This
40
+ # option is required.
8
41
  def has_guises(*guises)
9
42
  include Introspection
10
43
 
11
- options = guises.last.is_a?(Hash) ? guises.pop : {}
12
-
13
- guises = guises.map(&:to_s)
14
- association = options.fetch(:association)
15
- attribute = options.fetch(:attribute)
16
- join_table = options[:table_name] || association
17
-
18
- Guise.registry[self.name] = {
19
- names: guises,
20
- association: association,
21
- attribute: attribute
22
- }
23
-
24
- guises.each do |guise|
25
- method_name = guise.underscore
26
- scope method_name.pluralize, -> { select("#{self.table_name}.*").joins(association).where(join_table => { attribute => guise }) }
27
-
28
- define_method "#{method_name}?" do
29
- has_guise?(guise)
30
- end
31
- end
32
-
33
- has_many association, options.except(:association, :attribute, :table_name)
34
-
35
- if association != :guises
36
- association_singular = association.to_s.singularize
37
-
38
- alias_method :guises, association
39
- alias_method :guises=, "#{association}="
40
- alias_method :guise_ids, "#{association_singular}_ids"
41
- alias_method :guise_ids=, "#{association_singular}_ids="
42
- alias_method "has_#{association_singular}?", :has_guise?
43
- alias_method "has_#{association}?", :has_guises?
44
- alias_method "has_any_#{association}?", :has_any_guises?
45
- end
44
+ Guise.register_source(self, *guises)
46
45
  end
47
46
 
48
- def guise_of(class_name)
49
- options = Guise.registry[class_name]
50
-
51
- if options.nil?
52
- raise ArgumentError, "no guises defined on #{class_name}"
53
- end
54
-
55
- default_scope -> { send(model_name.plural) }
56
-
57
- after_initialize SourceCallback.new(self.name, options[:attribute])
47
+ # Specifies that the model is a subclass of a model configured with
48
+ # {#has_guises} specified by `class_name`.
49
+ #
50
+ # Configures the caller with the correct `default_scope`. Given the
51
+ # following definition with `has_guises`:
52
+ #
53
+ # ```ruby
54
+ # class User < ActiveRecord::Base
55
+ # has_guises :DeskWorker, :MailForwarder, association: :roles, attribute: :title
56
+ # end
57
+ # ```
58
+ #
59
+ # The following call to `guise_of`:
60
+ #
61
+ # class DeskWorker < User
62
+ # guise_of :User
63
+ # end
64
+ # ```
65
+ #
66
+ # Is equivalent to:
67
+ #
68
+ # ```ruby
69
+ # class DeskWorker < User
70
+ # default_scope -> { desk_workers }
71
+ #
72
+ # after_initialize do
73
+ # self.guises.build(title: 'DeskWorker')
74
+ # end
75
+ #
76
+ # after_create do
77
+ # self.guises.create(title: 'DeskWorker')
78
+ # end
79
+ # end
80
+ # ```
81
+ #
82
+ # @param [String, Symbol] source_class_name name of the superclass
83
+ # configured with {#has_guises}.
84
+ def guise_of(source_class_name)
85
+ options = Guise.registry[source_class_name]
86
+
87
+ default_scope GuiseOfScope.new(name, options)
88
+ after_initialize SourceCallback.new(name, options.attribute)
58
89
  end
59
90
 
60
- def guise_for(class_name, options = {})
61
- guise_options = Guise.registry[class_name]
62
-
63
- if guise_options.nil?
64
- raise ArgumentError, "no guises defined on #{class_name}"
65
- end
66
-
67
- association = class_name.to_s.underscore.to_sym
68
- guises = guise_options[:names]
69
- attribute = guise_options[:attribute]
70
- foreign_key = options[:foreign_key] || "#{class_name.to_s.underscore}_id"
71
-
72
- belongs_to association, options.except(:validate)
73
-
74
- guises.each do |guise|
75
- scope guise.underscore.pluralize, -> { where(attribute => guise) }
76
- end
77
-
78
- if options[:validate] != false
79
- validates attribute, uniqueness: { scope: foreign_key }, presence: true, inclusion: { in: guises }
80
- end
91
+ # Configures the other end of the association defined by {#has_guises}.
92
+ # Defines equivalent scopes defined on the model configured with
93
+ # {#has_guises}.
94
+ #
95
+ # Given the following configuring of `has_guises`:
96
+ #
97
+ # ```ruby
98
+ # class User < ActiveRecord::Base
99
+ # has_guises :DeskWorker, :MailForwarder, association: :roles, attribute: :title
100
+ # end
101
+ # ```
102
+ #
103
+ # The following call to `guise_for`:
104
+ #
105
+ # ```ruby
106
+ # class Role < ActiveRecord::Base
107
+ # guise_for :User
108
+ # end
109
+ # ```
110
+ #
111
+ # Is equivalent to:
112
+ #
113
+ # ```ruby
114
+ # class Role < ActiveRecord::Base
115
+ # belongs_to :user
116
+ #
117
+ # validates :title, presence: true, uniqueness: { scope: :user_id }, inclusion: { in: %w( DeskWorker MailForwarder ) }
118
+ #
119
+ # scope :desk_workers, -> { where(title: "DeskWorker") }
120
+ # scope :mail_forwarder, -> { where(title: "MailForwarder") }
121
+ # end
122
+ # ```
123
+ #
124
+ # @param [Symbol, String] source_class_name name of the class configured
125
+ # with {#has_guises}
126
+ # @param [Hash] options options to configure the `belongs_to` association.
127
+ # @option options [false] :validate specify `false` to skip
128
+ # validations for the `:attribute` specified in {#has_guises}
129
+ # @option options [Symbol] :foreign_key foreign key used to build the
130
+ # association.
131
+ def guise_for(source_class_name, options = {})
132
+ Guise.register_association(self, source_class_name, options)
81
133
  end
82
134
 
83
- def scoped_guise_for(class_name)
84
- guise_options = Guise.registry[class_name]
85
-
86
- if guise_options.nil?
87
- raise ArgumentError, "no guises defined on #{class_name}"
88
- end
89
-
90
- attribute = guise_options[:attribute]
91
- parent_name = table_name.classify
92
-
93
- value = guise_options[:names].detect do |guise|
94
- guise == model_name.to_s.chomp(parent_name)
95
- end
96
-
97
- default_scope -> { where(attribute => value) }
98
-
99
- after_initialize AssociationCallback.new(value, attribute)
135
+ # Specifies that the model is a subclass of a model configured with
136
+ # {#guise_for} specified by `class_name`. The name of the calling class must
137
+ # be `<value|parent_class_name>`.
138
+ #
139
+ # Given the following configuration with `guise_for`:
140
+ #
141
+ # ```ruby
142
+ # class Role < ActiveRecord::Base
143
+ # guise_for :User
144
+ # end
145
+ # ```
146
+ #
147
+ # The following call to `scoped_guise_for`:
148
+ #
149
+ # ```ruby
150
+ # class DeskWorkerRole < Role
151
+ # scoped_guise_for :Role
152
+ # end
153
+ # ```
154
+ #
155
+ # Is equivalent to:
156
+ #
157
+ # ```ruby
158
+ # class DeskWorkerRole < Role
159
+ # default_scope -> { desk_workers }
160
+ #
161
+ # after_initialize do
162
+ # self.title = "DeskWorker"
163
+ # end
164
+ # end
165
+ # ```
166
+ #
167
+ # @param [Symbol, String] association_class_name name of the superclass
168
+ # configured with {#guise_for}.
169
+ def scoped_guise_for(association_class_name)
170
+ options = Guise.registry[association_class_name]
171
+ value = name.chomp(options.association_class.name)
172
+
173
+ default_scope options.scope(value, :guise_for)
174
+ after_initialize AssociationCallback.new(value, options.attribute)
100
175
  end
101
176
  end
102
177
  end
data/lib/guise/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Guise
2
- VERSION = "0.6.0"
2
+ VERSION = "0.6.1"
3
3
  end
data/spec/guise_spec.rb CHANGED
@@ -1,9 +1,15 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Guise do
4
- let!(:user) { create(:user) }
5
- let!(:supervisor) { create(:supervisor) }
6
- let!(:technician) { create(:technician) }
4
+ let!(:user) { User.create!(email: "bob@bob.bob") }
5
+ let!(:supervisor) { Supervisor.create!(email: "jane@manag.er") }
6
+ let!(:technician) { Technician.create!(email: "sarah@fix.it") }
7
+
8
+ def self.active_record_has_pluck_and_not_version_4_0?
9
+ ActiveRecord::VERSION::STRING >= "4.1" ||
10
+ ActiveRecord::VERSION::STRING <= "3.2" &&
11
+ ActiveRecord::Relation.method_defined?(:pluck)
12
+ end
7
13
 
8
14
  after do
9
15
  User.delete_all
@@ -12,28 +18,76 @@ describe Guise do
12
18
 
13
19
  describe ".has_guises" do
14
20
  it "sets up has_many association" do
15
- expect(user).to have_many :user_roles
21
+ reflection = User.reflect_on_association(:user_roles)
22
+
23
+ expect(reflection).not_to be_nil
16
24
  end
17
25
 
18
26
  it "adds scopes for each type" do
19
- technicians = User.technicians
27
+ technicians = User.technicians.map(&:id)
28
+
29
+ expect(technicians).to include technician.id
30
+ expect(technicians).not_to include user.id
31
+ expect(technicians).not_to include supervisor.id
32
+ end
33
+
34
+ shared_examples(
35
+ "building and saving associated records"
36
+ ) do |source, strategy|
37
+ it "sets up scopes to correctly create associated records" do
38
+ technician = source.public_send(strategy)
39
+
40
+ expect(technician).to be_technician
41
+ expect(technician.user_roles.map(&:name)).to eq ["Technician"]
42
+
43
+ if technician.new_record?
44
+ technician.save!
45
+
46
+ expect(technician.reload).to be_technician
47
+ end
48
+ end
49
+ end
20
50
 
21
- expect(technicians).to include technician
22
- expect(technicians).not_to include user
23
- expect(technicians).not_to include supervisor
51
+ include_examples(
52
+ "building and saving associated records",
53
+ User.technicians,
54
+ :new
55
+ )
56
+
57
+ include_examples(
58
+ "building and saving associated records",
59
+ User.technicians,
60
+ :create
61
+ )
62
+
63
+ include_examples(
64
+ "building and saving associated records",
65
+ Technician,
66
+ :new
67
+ )
68
+
69
+ include_examples(
70
+ "building and saving associated records",
71
+ Technician,
72
+ :create
73
+ )
74
+
75
+ if active_record_has_pluck_and_not_version_4_0?
76
+ it "sets up scopes to correctly handle `ActiveRecord::Relation#pluck`" do
77
+ expect(Technician.all.pluck(:id)).to eq [technician.id]
78
+ end
24
79
  end
25
80
 
26
81
  it 'aliases the association methods to `guise=` and `guises=`' do
27
- record = create(:user)
82
+ record = User.create!
28
83
 
29
84
  expect(record.guises).to eq []
30
85
 
31
- # NOTE: The user is assigned in factory_girl to deal with a Rails 3.1
32
- # issue
33
- record.guises = [build(:user_role, user: record)]
34
- record.guises << build(:user_role, name: 'Supervisor', user: record)
86
+ # NOTE: The user is assigned to deal with a Rails 3.1 issue
87
+ record.guises = [UserRole.new(name: "Technician", user: record)]
88
+ record.guises << UserRole.new(name: "Supervisor", user: record)
35
89
 
36
- expect(record.guises(true).length).to eq 2
90
+ expect(record.reload.guises.length).to eq 2
37
91
 
38
92
  expect(record.guise_ids.length).to eq 2
39
93
 
@@ -43,10 +97,11 @@ describe Guise do
43
97
  end
44
98
 
45
99
  it 'handles non-standard table names and foreign key attributes' do
46
- person = create(:person)
47
- create(:permission, person: person)
100
+ person = Person.create!
101
+ Permission.create!(person: person, privilege: "Admin")
102
+ reflection = Person.reflect_on_association(:permissions)
48
103
 
49
- expect(person).to have_many :permissions
104
+ expect(reflection).not_to be_nil
50
105
  expect(Person.admins).to include person
51
106
  end
52
107
  end
@@ -67,8 +122,8 @@ describe Guise do
67
122
  end
68
123
 
69
124
  it 'ignores records marked for destruction' do
70
- technician_role = build(:user_role, name: 'Technician')
71
- technician_user = create(:user, user_roles: [technician_role])
125
+ technician_role = UserRole.new(name: "Technician")
126
+ technician_user = User.create(user_roles: [technician_role])
72
127
 
73
128
  expect(technician_user).to have_guise :technician
74
129
 
@@ -84,12 +139,15 @@ describe Guise do
84
139
  it 'is wrapped for each guise specified' do
85
140
  expect(user).not_to be_technician
86
141
  expect(technician).to be_technician
142
+
143
+ expect(user).not_to be_explorer
144
+ expect(technician).not_to be_explorer
87
145
  end
88
146
  end
89
147
 
90
148
  describe "#has_guises?" do
91
149
  before do
92
- @role = create(:user_role, name: 'Technician', user: supervisor)
150
+ @role = UserRole.create!(name: "Technician", user: supervisor)
93
151
  end
94
152
 
95
153
  after do
@@ -102,9 +160,9 @@ describe Guise do
102
160
  end
103
161
 
104
162
  it 'ignores records marked for destruction' do
105
- technician_role = build(:user_role, name: 'Technician')
106
- supervisor_role = build(:user_role, name: 'Supervisor')
107
- user = create(:user, user_roles: [technician_role, supervisor_role])
163
+ technician_role = UserRole.new(name: "Technician")
164
+ supervisor_role = UserRole.new(name: "Supervisor")
165
+ user = User.create!(user_roles: [technician_role, supervisor_role])
108
166
 
109
167
  expect(user).to have_guises :technician, :supervisor
110
168
 
@@ -128,8 +186,8 @@ describe Guise do
128
186
  end
129
187
 
130
188
  it 'ignores records marked for destruction' do
131
- technician_role = build(:user_role, name: 'Technician')
132
- technician_user = create(:user, user_roles: [technician_role])
189
+ technician_role = UserRole.new(name: "Technician")
190
+ technician_user = User.create!(user_roles: [technician_role])
133
191
 
134
192
  expect(technician_user).to have_any_guises :technician, :supervisor
135
193
 
@@ -145,7 +203,7 @@ describe Guise do
145
203
 
146
204
  describe '.guise_of' do
147
205
  it "sets default scope to limit to records of the class's type" do
148
- technician_ids = User.technicians.map(&:id)
206
+ technician_ids = Technician.all.map(&:id)
149
207
 
150
208
  expect(technician_ids).to eq [technician.id]
151
209
  end
@@ -171,20 +229,20 @@ describe Guise do
171
229
  Class.new(ActiveRecord::Base) do
172
230
  guise_of :Model
173
231
  end
174
- end
232
+ end.to raise_error Guise::DefinitionNotFound
175
233
  end
176
234
  end
177
235
 
178
236
  describe ".guise_for" do
179
- subject { UserRole.new }
180
-
181
237
  it "sets up belongs_to" do
182
- should belong_to(:user)
238
+ reflection = UserRole.reflect_on_association(:user)
239
+
240
+ expect(reflection).not_to be_nil
183
241
  end
184
242
 
185
243
  it 'defines scopes for each guise' do
186
- technician_role = create(:technician_role)
187
- supervisor_role = create(:supervisor_role)
244
+ technician_role = UserRole.create(name: "Technician")
245
+ supervisor_role = UserRole.create(name: "Supervisor")
188
246
 
189
247
  technician_roles = UserRole.technicians
190
248
 
@@ -202,21 +260,29 @@ describe Guise do
202
260
  Class.new(ActiveRecord::Base) do
203
261
  guise_for :Model
204
262
  end
205
- end
263
+ end.to raise_error Guise::DefinitionNotFound
206
264
  end
207
265
 
208
- describe "adds validations to ensure guise attribute is" do
209
- it "present" do
210
- should validate_presence_of(:name)
211
- end
212
-
213
- it "unique per resource" do
214
- should validate_uniqueness_of(:name).scoped_to(:user_id)
215
- end
216
-
217
- it "is one of the guise names provided" do
218
- expect { create(:user_role, name: 'Farmer') }.to raise_error ActiveRecord::RecordInvalid
219
- end
266
+ it "adds validations to ensure presence, inclusion and uniqueness " \
267
+ "of the guise attribute" do
268
+ user_role = UserRole.new
269
+ user_role.valid?
270
+
271
+ expect(user_role.errors[:name]).to match_array [
272
+ I18n.t("errors.messages.blank"),
273
+ I18n.t("errors.messages.inclusion")
274
+ ]
275
+
276
+ user_role.user = technician
277
+ user_role.name = "Technician"
278
+ user_role.valid?
279
+
280
+ expect(user_role.errors[:name]).to eq [
281
+ I18n.t(
282
+ "activerecord.errors.messages.taken",
283
+ default: :"errors.messages.taken"
284
+ )
285
+ ]
220
286
  end
221
287
  end
222
288
 
@@ -240,7 +306,7 @@ describe Guise do
240
306
  Class.new(ActiveRecord::Base) do
241
307
  scoped_guise_for :Model
242
308
  end
243
- end.to raise_error
309
+ end.to raise_error Guise::DefinitionNotFound
244
310
  end
245
311
  end
246
312
  end