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.
- 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
|