guise 0.6.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.codeclimate.yml +36 -0
- data/.rubocop.yml +1063 -0
- data/.travis.yml +25 -5
- data/.yardopts +1 -0
- data/Appraisals +5 -0
- data/README.md +120 -29
- data/gemfiles/3.1.gemfile +1 -1
- data/gemfiles/3.1.gemfile.lock +39 -30
- data/gemfiles/3.2.gemfile +1 -1
- data/gemfiles/3.2.gemfile.lock +47 -38
- data/gemfiles/4.0.gemfile +1 -1
- data/gemfiles/4.0.gemfile.lock +47 -38
- data/gemfiles/4.1.gemfile +1 -1
- data/gemfiles/4.1.gemfile.lock +45 -38
- data/gemfiles/4.2.gemfile +1 -1
- data/gemfiles/4.2.gemfile.lock +44 -37
- data/gemfiles/5.0.gemfile +8 -0
- data/gemfiles/5.0.gemfile.lock +94 -0
- data/guise.gemspec +9 -14
- data/lib/guise.rb +29 -5
- data/lib/guise/builders.rb +129 -0
- data/lib/guise/callbacks.rb +3 -0
- data/lib/guise/errors.rb +32 -0
- data/lib/guise/introspection.rb +23 -11
- data/lib/guise/options.rb +75 -0
- data/lib/guise/registry.rb +19 -0
- data/lib/guise/scopes.rb +65 -0
- data/lib/guise/syntax.rb +158 -83
- data/lib/guise/version.rb +1 -1
- data/spec/guise_spec.rb +112 -46
- data/spec/spec_helper.rb +12 -8
- metadata +50 -29
- data/spec/factories.rb +0 -39
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
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
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(:
|
5
|
-
let!(:supervisor) { create(:
|
6
|
-
let!(:technician) { create(:
|
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
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
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
|
82
|
+
record = User.create!
|
28
83
|
|
29
84
|
expect(record.guises).to eq []
|
30
85
|
|
31
|
-
# NOTE: The user is assigned
|
32
|
-
|
33
|
-
record.guises
|
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
|
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
|
47
|
-
create(:
|
100
|
+
person = Person.create!
|
101
|
+
Permission.create!(person: person, privilege: "Admin")
|
102
|
+
reflection = Person.reflect_on_association(:permissions)
|
48
103
|
|
49
|
-
expect(
|
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 =
|
71
|
-
technician_user = create(
|
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(
|
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 =
|
106
|
-
supervisor_role =
|
107
|
-
user = create(
|
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 =
|
132
|
-
technician_user = create(
|
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 =
|
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
|
-
|
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(:
|
187
|
-
supervisor_role = create(:
|
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
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|