guise 0.2.3 → 0.3.0
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 +7 -0
- data/.travis.yml +14 -0
- data/Appraisals +9 -2
- data/README.md +68 -21
- data/Rakefile +2 -0
- data/gemfiles/3.1.gemfile +2 -1
- data/gemfiles/3.1.gemfile.lock +47 -37
- data/gemfiles/3.2.gemfile +2 -1
- data/gemfiles/3.2.gemfile.lock +46 -36
- data/gemfiles/4.0.gemfile +8 -0
- data/gemfiles/4.0.gemfile.lock +74 -0
- data/guise.gemspec +18 -7
- data/lib/guise/callbacks.rb +28 -0
- data/lib/guise/introspection.rb +15 -16
- data/lib/guise/registry.rb +0 -0
- data/lib/guise/syntax.rb +104 -0
- data/lib/guise/version.rb +1 -1
- data/lib/guise.rb +7 -71
- data/spec/factories.rb +12 -5
- data/spec/guise_spec.rb +107 -43
- data/spec/spec_helper.rb +43 -6
- metadata +116 -52
- data/lib/guise/options.rb +0 -25
- data/spec/support/database.rb +0 -40
data/lib/guise/syntax.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/core_ext/hash/keys'
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
5
|
+
|
6
|
+
module Guise
|
7
|
+
module Syntax
|
8
|
+
def has_guises(*guises)
|
9
|
+
include Introspection
|
10
|
+
|
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
|
+
|
17
|
+
Guise.registry[self.name] = {
|
18
|
+
names: guises,
|
19
|
+
association: association,
|
20
|
+
attribute: attribute
|
21
|
+
}
|
22
|
+
|
23
|
+
guises.each do |guise|
|
24
|
+
method_name = guise.underscore
|
25
|
+
scope method_name.pluralize, -> { joins(association).where(association => { attribute => guise }) }
|
26
|
+
|
27
|
+
define_method "#{method_name}?" do
|
28
|
+
has_guise?(guise)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
has_many association, options.except(:association, :attribute)
|
33
|
+
|
34
|
+
if association != :guises
|
35
|
+
association_singular = association.to_s.singularize
|
36
|
+
|
37
|
+
alias_method :guises, association
|
38
|
+
alias_method "has_#{association_singular}?", :has_guise?
|
39
|
+
alias_method "has_#{association}?", :has_guises?
|
40
|
+
alias_method "has_any_#{association}?", :has_any_guises?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def guise_of(class_name)
|
45
|
+
options = Guise.registry[class_name]
|
46
|
+
|
47
|
+
if options.nil?
|
48
|
+
raise ArgumentError, "no guises defined on #{class_name}"
|
49
|
+
end
|
50
|
+
|
51
|
+
default_scope -> { send(model_name.plural) }
|
52
|
+
|
53
|
+
callback = SourceCallback.new(self.name, options[:attribute])
|
54
|
+
|
55
|
+
after_initialize callback
|
56
|
+
after_create callback
|
57
|
+
end
|
58
|
+
|
59
|
+
def guise_for(class_name, options = {})
|
60
|
+
guise_options = Guise.registry[class_name]
|
61
|
+
|
62
|
+
if guise_options.nil?
|
63
|
+
raise ArgumentError, "no guises defined on #{class_name}"
|
64
|
+
end
|
65
|
+
|
66
|
+
association = class_name.to_s.underscore.to_sym
|
67
|
+
guises = guise_options[:names]
|
68
|
+
attribute = guise_options[:attribute]
|
69
|
+
foreign_key = options[:foreign_key] || "#{class_name.underscore}_id"
|
70
|
+
|
71
|
+
belongs_to association, options.except(:validate)
|
72
|
+
|
73
|
+
guises.each do |guise|
|
74
|
+
scope guise.underscore.pluralize, -> { where(attribute => guise) }
|
75
|
+
end
|
76
|
+
|
77
|
+
if options[:validate] != false
|
78
|
+
validates attribute, uniqueness: { scope: foreign_key }, presence: true, inclusion: { in: guises }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def scoped_guise_for(class_name)
|
83
|
+
guise_options = Guise.registry[class_name]
|
84
|
+
|
85
|
+
if guise_options.nil?
|
86
|
+
raise ArgumentError, "no guises defined on #{class_name}"
|
87
|
+
end
|
88
|
+
|
89
|
+
attribute = guise_options[:attribute]
|
90
|
+
parent_name = table_name.classify
|
91
|
+
|
92
|
+
value = guise_options[:names].detect do |guise|
|
93
|
+
guise == model_name.to_s.chomp(parent_name)
|
94
|
+
end
|
95
|
+
|
96
|
+
default_scope -> { where(attribute => value) }
|
97
|
+
|
98
|
+
callback = AssociationCallback.new(value, attribute)
|
99
|
+
|
100
|
+
after_initialize callback
|
101
|
+
before_create callback
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/guise/version.rb
CHANGED
data/lib/guise.rb
CHANGED
@@ -1,79 +1,15 @@
|
|
1
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
2
|
+
|
1
3
|
require 'guise/version'
|
2
|
-
require 'guise/
|
4
|
+
require 'guise/callbacks'
|
5
|
+
require 'guise/syntax'
|
3
6
|
require 'guise/introspection'
|
4
7
|
|
5
8
|
module Guise
|
6
|
-
|
7
|
-
|
8
|
-
extend Options
|
9
|
-
|
10
|
-
options = names.last.is_a?(Hash) ? names.pop : {}
|
11
|
-
class_names = names.map(&:to_s).map(&:classify)
|
12
|
-
|
13
|
-
guise_options, association_options = extract_guise_options(class_names, options)
|
14
|
-
|
15
|
-
build_guises(class_names, guise_options)
|
16
|
-
introspect_guises(class_names)
|
17
|
-
|
18
|
-
has_many guise_association, association_options
|
19
|
-
|
20
|
-
if guise_association != :guises
|
21
|
-
alias_method :guises, guise_association
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def guise_for(name, options = {})
|
26
|
-
association = Object.const_get(name.to_s.classify)
|
27
|
-
foreign_key = options[:foreign_key] || "#{association.name.underscore}_id"
|
28
|
-
|
29
|
-
belongs_to name, options
|
30
|
-
|
31
|
-
if options[:validate] != false
|
32
|
-
validates association.guise_attribute,
|
33
|
-
:uniqueness => { :scope => foreign_key },
|
34
|
-
:presence => true,
|
35
|
-
:inclusion => { :in => association.guises }
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def build_guises(names, options)
|
42
|
-
names.each do |name|
|
43
|
-
scope_name = name.tableize.to_sym
|
44
|
-
|
45
|
-
# Add a scope for this type of resource
|
46
|
-
scope scope_name, joins(guise_association).where(guise_association => { guise_attribute => name })
|
47
|
-
|
48
|
-
# build the class setting it's default scope to limit to those of itself
|
49
|
-
guise_class = Class.new(self) do
|
50
|
-
default_scope { send(scope_name) }
|
51
|
-
|
52
|
-
after_initialize do
|
53
|
-
self.guises.new(self.guise_attribute => name) unless self.has_role?(name)
|
54
|
-
end
|
55
|
-
|
56
|
-
after_create do
|
57
|
-
self.guises.create(self.guise_attribute => name)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
Object.const_set(name, guise_class)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def introspect_guises(names)
|
66
|
-
include Introspection
|
67
|
-
|
68
|
-
names.each do |name|
|
69
|
-
method_name = "#{name.underscore}?"
|
70
|
-
define_method method_name do
|
71
|
-
has_role?(name)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
9
|
+
mattr_reader :registry
|
10
|
+
@@registry = HashWithIndifferentAccess.new
|
75
11
|
end
|
76
12
|
|
77
13
|
if defined?(ActiveRecord)
|
78
|
-
ActiveRecord::Base.extend
|
14
|
+
ActiveRecord::Base.extend(Guise::Syntax)
|
79
15
|
end
|
data/spec/factories.rb
CHANGED
@@ -5,20 +5,27 @@ FactoryGirl.define do
|
|
5
5
|
email 'mrhalp@mit.edu'
|
6
6
|
|
7
7
|
factory :technician do
|
8
|
-
|
9
|
-
|
8
|
+
after(:create) do |user, proxy|
|
9
|
+
create(:user_role, name: 'Technician', user: user)
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
13
|
factory :supervisor do
|
14
|
-
|
15
|
-
|
14
|
+
after(:create) do |user, proxy|
|
15
|
+
create(:user_role, name: 'Supervisor', user: user)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
20
|
factory :user_role do
|
21
|
-
|
21
|
+
user
|
22
22
|
name 'Technician'
|
23
|
+
|
24
|
+
factory :technician_role do
|
25
|
+
end
|
26
|
+
|
27
|
+
factory :supervisor_role do
|
28
|
+
name 'Supervisor'
|
29
|
+
end
|
23
30
|
end
|
24
31
|
end
|
data/spec/guise_spec.rb
CHANGED
@@ -1,78 +1,126 @@
|
|
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
7
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
+
after do
|
9
|
+
User.delete_all
|
10
|
+
UserRole.delete_all
|
11
|
+
end
|
8
12
|
|
9
13
|
describe ".has_guises" do
|
10
|
-
subject { user }
|
11
|
-
|
12
14
|
it "sets up has_many association" do
|
13
|
-
|
15
|
+
expect(user).to have_many :user_roles
|
14
16
|
end
|
15
17
|
|
16
|
-
it "
|
17
|
-
|
18
|
-
|
18
|
+
it "adds scopes for each type" do
|
19
|
+
technicians = User.technicians
|
20
|
+
|
21
|
+
expect(technicians).to include technician
|
22
|
+
expect(technicians).not_to include user
|
23
|
+
expect(technicians).not_to include supervisor
|
19
24
|
end
|
25
|
+
end
|
20
26
|
|
21
|
-
|
22
|
-
|
23
|
-
|
27
|
+
describe "#has_guise?" do
|
28
|
+
it "checks if record is of the specified type" do
|
29
|
+
expect(user).not_to have_guise :technician
|
24
30
|
|
25
|
-
|
26
|
-
|
31
|
+
expect(technician).to have_guise :technician
|
32
|
+
expect(technician).to have_guise :Technician
|
33
|
+
expect(technician).to have_guise 'Technician'
|
34
|
+
expect(technician).to have_guise 'technician'
|
35
|
+
expect(technician).to have_guise Technician
|
27
36
|
end
|
28
37
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
technician.has_role?(:Technician).should be_true
|
33
|
-
end
|
38
|
+
it "raises an error if type was not specified" do
|
39
|
+
expect { user.has_guise?(:Accountant) }.to raise_error ArgumentError
|
40
|
+
end
|
34
41
|
|
35
|
-
|
36
|
-
|
37
|
-
|
42
|
+
it 'is aliased based on the name of the association' do
|
43
|
+
expect(user).not_to have_user_role :technician
|
44
|
+
expect(technician).to have_user_role :technician
|
38
45
|
end
|
39
46
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
47
|
+
it 'is wrapped for each guise specified' do
|
48
|
+
expect(user).not_to be_technician
|
49
|
+
expect(technician).to be_technician
|
50
|
+
end
|
51
|
+
end
|
44
52
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
53
|
+
describe "#has_guises?" do
|
54
|
+
before do
|
55
|
+
@role = create(:user_role, name: 'Technician', user: supervisor)
|
49
56
|
end
|
50
57
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
58
|
+
after do
|
59
|
+
@role.destroy
|
60
|
+
end
|
61
|
+
|
62
|
+
it "checks if resource is all of the provided types" do
|
63
|
+
expect(technician).not_to have_guises :Supervisor, :Technician
|
64
|
+
expect(supervisor).to have_guises :Supervisor, :Technician
|
56
65
|
end
|
57
66
|
|
58
|
-
it
|
59
|
-
|
60
|
-
|
67
|
+
it 'is aliased based on the association name' do
|
68
|
+
expect(technician).not_to have_user_roles :Supervisor, :Technician
|
69
|
+
expect(supervisor).to have_user_roles :Supervisor, :Technician
|
70
|
+
end
|
71
|
+
end
|
61
72
|
|
62
|
-
|
63
|
-
|
73
|
+
describe "#has_any_roles?" do
|
74
|
+
it "checks if resource is any of the supplied roles" do
|
75
|
+
expect(user).not_to have_any_guises :Supervisor, :Technician
|
76
|
+
expect(technician).to have_any_guises 'supervisor', 'technician'
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'is aliased based on the association name' do
|
80
|
+
expect(user).not_to have_any_user_roles :Supervisor, :Technician
|
81
|
+
expect(technician).to have_any_user_roles 'supervisor', 'Technician'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe '.guise_of' do
|
86
|
+
it "sets default scope to limit to records of the class's type" do
|
87
|
+
technician_ids = Technician.pluck(:id)
|
88
|
+
|
89
|
+
expect(technician_ids).to eq [technician.id]
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'sets up lifecycle callbacks to ensure records are initialized and created with the correct associated records' do
|
93
|
+
new_record = Technician.new
|
94
|
+
expect(new_record).to have_guise :technician
|
95
|
+
|
96
|
+
created_record = Technician.create!
|
97
|
+
expect(created_record).to have_guise :technician
|
64
98
|
end
|
65
99
|
end
|
66
100
|
|
67
101
|
describe ".guise_for" do
|
68
|
-
subject {
|
102
|
+
subject { UserRole.new }
|
69
103
|
|
70
104
|
it "sets up belongs_to" do
|
71
105
|
should belong_to(:user)
|
72
106
|
end
|
73
107
|
|
74
|
-
|
108
|
+
it 'defines scopes for each guise' do
|
109
|
+
technician_role = create(:technician_role)
|
110
|
+
supervisor_role = create(:supervisor_role)
|
111
|
+
|
112
|
+
technician_roles = UserRole.technicians
|
113
|
+
|
114
|
+
expect(technician_roles).to include technician_role
|
115
|
+
expect(technician_roles).not_to include supervisor_role
|
116
|
+
|
117
|
+
supervisor_roles = UserRole.supervisors
|
75
118
|
|
119
|
+
expect(supervisor_roles).to include supervisor_role
|
120
|
+
expect(supervisor_roles).not_to include technician_role
|
121
|
+
end
|
122
|
+
|
123
|
+
describe "adds validations to ensure guise attribute is" do
|
76
124
|
it "present" do
|
77
125
|
should validate_presence_of(:name)
|
78
126
|
end
|
@@ -82,8 +130,24 @@ describe Guise do
|
|
82
130
|
end
|
83
131
|
|
84
132
|
it "is one of the guise names provided" do
|
85
|
-
expect { create(:user_role, :
|
133
|
+
expect { create(:user_role, name: 'Farmer') }.to raise_error ActiveRecord::RecordInvalid
|
86
134
|
end
|
87
135
|
end
|
88
136
|
end
|
137
|
+
|
138
|
+
describe '.scoped_guise_of' do
|
139
|
+
it 'sets default scope' do
|
140
|
+
names = TechnicianUserRole.pluck(:name).uniq
|
141
|
+
|
142
|
+
expect(names).to eq ['Technician']
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'sets up lifecycle callbacks to ensure the object is in the correct state' do
|
146
|
+
new_technician_role = TechnicianUserRole.new
|
147
|
+
created_technician_role = TechnicianUserRole.create!
|
148
|
+
|
149
|
+
expect(new_technician_role.name).to eq 'Technician'
|
150
|
+
expect(created_technician_role.name).to eq 'Technician'
|
151
|
+
end
|
152
|
+
end
|
89
153
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,17 +1,54 @@
|
|
1
|
+
require 'pry'
|
1
2
|
require 'active_record'
|
2
3
|
require 'factory_girl'
|
3
4
|
require 'shoulda-matchers'
|
4
5
|
require 'guise'
|
5
6
|
|
6
|
-
|
7
|
+
I18n.enforce_available_locales = false
|
7
8
|
|
8
|
-
|
9
|
+
ActiveRecord::Base.establish_connection(
|
10
|
+
adapter: 'sqlite3',
|
11
|
+
database: ':memory:'
|
12
|
+
)
|
13
|
+
|
14
|
+
ActiveRecord::Migration.verbose = false
|
9
15
|
|
10
|
-
|
11
|
-
|
12
|
-
|
16
|
+
ActiveRecord::Schema.define do
|
17
|
+
create_table :users, force: true do |t|
|
18
|
+
t.string :name
|
19
|
+
t.string :email
|
13
20
|
end
|
14
21
|
|
15
|
-
|
22
|
+
create_table :user_roles, force: true do |t|
|
23
|
+
t.string :name
|
24
|
+
t.integer :person_id
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class User < ActiveRecord::Base
|
29
|
+
has_guises :Technician, :Supervisor, association: :user_roles, attribute: :name, foreign_key: :person_id
|
30
|
+
end
|
31
|
+
|
32
|
+
class Technician < User
|
33
|
+
guise_of :User
|
34
|
+
end
|
35
|
+
|
36
|
+
class Supervisor < User
|
37
|
+
guise_of :User
|
38
|
+
end
|
39
|
+
|
40
|
+
class UserRole < ActiveRecord::Base
|
41
|
+
guise_for :User,
|
42
|
+
foreign_key: :person_id
|
43
|
+
end
|
44
|
+
|
45
|
+
class TechnicianUserRole < UserRole
|
46
|
+
scoped_guise_for :User
|
16
47
|
end
|
17
48
|
|
49
|
+
|
50
|
+
FactoryGirl.find_definitions
|
51
|
+
RSpec.configure do |config|
|
52
|
+
config.order = 'random'
|
53
|
+
config.include FactoryGirl::Syntax::Methods
|
54
|
+
end
|