guise 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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