hydra_attribute 0.4.2 → 0.5.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -1
- data/.travis.yml +6 -5
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -1
- data/README.md +3 -3
- data/Rakefile +2 -7
- data/gemfiles/activerecord-3.2.gemfile +5 -0
- data/hydra_attribute.gemspec +6 -7
- data/lib/hydra_attribute.rb +17 -18
- data/lib/hydra_attribute/active_record.rb +34 -13
- data/lib/hydra_attribute/active_record/association_preloader.rb +47 -28
- data/lib/hydra_attribute/active_record/attribute_methods.rb +29 -140
- data/lib/hydra_attribute/active_record/mass_assignment_security.rb +39 -0
- data/lib/hydra_attribute/active_record/migration.rb +4 -4
- data/lib/hydra_attribute/active_record/relation.rb +6 -7
- data/lib/hydra_attribute/active_record/relation/query_methods.rb +28 -18
- data/lib/hydra_attribute/hydra_attribute.rb +12 -49
- data/lib/hydra_attribute/hydra_attribute_set.rb +67 -0
- data/lib/hydra_attribute/hydra_entity.rb +110 -0
- data/lib/hydra_attribute/hydra_entity_attribute_association.rb +155 -0
- data/lib/hydra_attribute/hydra_set.rb +24 -26
- data/lib/hydra_attribute/hydra_value.rb +210 -0
- data/lib/hydra_attribute/identity_map.rb +18 -0
- data/lib/hydra_attribute/middleware/identity_map.rb +15 -0
- data/lib/hydra_attribute/migrator.rb +24 -21
- data/lib/hydra_attribute/model.rb +47 -0
- data/lib/hydra_attribute/model/cacheable.rb +207 -0
- data/lib/hydra_attribute/model/dirty.rb +39 -0
- data/lib/hydra_attribute/model/has_many_through.rb +168 -0
- data/lib/hydra_attribute/model/identity_map.rb +59 -0
- data/lib/hydra_attribute/model/mediator.rb +89 -0
- data/lib/hydra_attribute/model/notifiable.rb +23 -0
- data/lib/hydra_attribute/model/persistence.rb +424 -0
- data/lib/hydra_attribute/model/validations.rb +40 -0
- data/lib/hydra_attribute/version.rb +1 -1
- data/spec/environments/mysql.rb +23 -0
- data/spec/environments/postgresql.rb +23 -0
- data/spec/environments/sqlite.rb +12 -0
- data/spec/fixtures/category.rb +8 -0
- data/spec/fixtures/product.rb +8 -0
- data/spec/fixtures/product_black_list.rb +13 -0
- data/spec/fixtures/product_white_list.rb +13 -0
- data/spec/hydra_attribute/active_record/attribute_methods_spec.rb +23 -28
- data/spec/hydra_attribute/active_record/mass_assignment_security_spec.rb +41 -0
- data/spec/hydra_attribute/active_record_spec.rb +577 -0
- data/spec/hydra_attribute/hydra_attribute_set_spec.rb +651 -0
- data/spec/hydra_attribute/hydra_attribute_spec.rb +208 -10
- data/spec/hydra_attribute/hydra_entity_attribute_association_spec.rb +216 -0
- data/spec/hydra_attribute/hydra_entity_spec.rb +71 -0
- data/spec/hydra_attribute/hydra_set_spec.rb +51 -10
- data/spec/hydra_attribute/hydra_value_spec.rb +286 -0
- data/spec/hydra_attribute/identity_map_spec.rb +47 -0
- data/spec/hydra_attribute/migrator_spec.rb +411 -0
- data/spec/hydra_attribute/model/cacheable_spec.rb +106 -0
- data/spec/hydra_attribute/model/has_many_through_spec.rb +132 -0
- data/spec/hydra_attribute/model/identity_map_spec.rb +39 -0
- data/spec/hydra_attribute/model/mediator_spec.rb +62 -0
- data/spec/hydra_attribute/model/persistence_spec.rb +550 -0
- data/spec/hydra_attribute/model_spec.rb +39 -0
- data/spec/hydra_attribute_spec.rb +36 -0
- data/spec/spec_helper.rb +10 -42
- metadata +76 -100
- data/Appraisals +0 -7
- data/cucumber.yml +0 -1
- data/features/entity/create.feature +0 -145
- data/features/entity/destroy.feature +0 -111
- data/features/entity/new.feature +0 -121
- data/features/entity/update.feature +0 -147
- data/features/hydra_attribute/create.feature +0 -30
- data/features/hydra_attribute/destroy.feature +0 -26
- data/features/hydra_attribute/update.feature +0 -36
- data/features/hydra_set/destroy.feature +0 -31
- data/features/migrations/create_and_drop.feature +0 -165
- data/features/migrations/migrate_and_rollback.feature +0 -211
- data/features/relation/query_methods/group.feature +0 -42
- data/features/relation/query_methods/order.feature +0 -67
- data/features/relation/query_methods/reorder.feature +0 -29
- data/features/relation/query_methods/reverse_order.feature +0 -29
- data/features/relation/query_methods/select.feature +0 -50
- data/features/relation/query_methods/where.feature +0 -115
- data/features/step_definitions/connections.rb +0 -65
- data/features/step_definitions/model_steps.rb +0 -136
- data/features/step_definitions/query_methods.rb +0 -48
- data/features/step_definitions/record_steps.rb +0 -93
- data/features/support/env.rb +0 -38
- data/features/support/world.rb +0 -61
- data/lib/hydra_attribute/active_record/association.rb +0 -113
- data/lib/hydra_attribute/active_record/reflection.rb +0 -16
- data/lib/hydra_attribute/association_builder.rb +0 -69
- data/lib/hydra_attribute/builder.rb +0 -37
- data/lib/hydra_attribute/entity_callbacks.rb +0 -26
- data/lib/hydra_attribute/hydra_attribute_methods.rb +0 -226
- data/lib/hydra_attribute/hydra_methods.rb +0 -528
- data/lib/hydra_attribute/hydra_set_methods.rb +0 -95
- data/lib/hydra_attribute/hydra_value_methods.rb +0 -21
- data/lib/hydra_attribute/memoizable.rb +0 -37
- data/spec/hydra_attribute/active_record/relation/query_methods_spec.rb +0 -31
- data/spec/hydra_attribute/hydra_attribute_methods_spec.rb +0 -458
- data/spec/hydra_attribute/hydra_methods_spec.rb +0 -456
- data/spec/hydra_attribute/hydra_set_methods_spec.rb +0 -203
- data/spec/hydra_attribute/memoizable_spec.rb +0 -95
@@ -0,0 +1,39 @@
|
|
1
|
+
module HydraAttribute
|
2
|
+
module Model
|
3
|
+
module Dirty
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
# Defines dirty method
|
8
|
+
#
|
9
|
+
# @param [String] column_name
|
10
|
+
# @return [NilClass]
|
11
|
+
def define_attribute_method(column_name)
|
12
|
+
super(column_name)
|
13
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
14
|
+
def #{column_name}_was
|
15
|
+
@attributes_were[:#{column_name}]
|
16
|
+
end
|
17
|
+
|
18
|
+
def #{column_name}_changed?
|
19
|
+
@attributes_were[:#{column_name}] != @attributes[:#{column_name}]
|
20
|
+
end
|
21
|
+
EOS
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Redefine initializer for catching previous attributes
|
26
|
+
def initialize(attributes = {})
|
27
|
+
super(attributes)
|
28
|
+
@attributes_were = @attributes.clone
|
29
|
+
end
|
30
|
+
|
31
|
+
# Update previous attributes after saving
|
32
|
+
def save
|
33
|
+
result = super
|
34
|
+
@attributes_were = @attributes.clone
|
35
|
+
result
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
module HydraAttribute
|
2
|
+
module Model
|
3
|
+
module HasManyThrough
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class Relation
|
7
|
+
def initialize(object, options = {})
|
8
|
+
@object = object
|
9
|
+
|
10
|
+
@relation_class = options[:relation_class]
|
11
|
+
@through_class = options[:through_class]
|
12
|
+
@through_method = options[:through_method]
|
13
|
+
@object_method_id = options[:object_method_id]
|
14
|
+
@relation_method_id = options[:relation_method_id]
|
15
|
+
@copy_attribute = options[:copy_attribute]
|
16
|
+
end
|
17
|
+
|
18
|
+
# API method
|
19
|
+
# build relation object
|
20
|
+
#
|
21
|
+
# @param [Hash] attributes
|
22
|
+
# @return [HydraAttribute::Model]
|
23
|
+
def build(attributes = {})
|
24
|
+
relation_object = @relation_class.new(prepare_relation_attributes(attributes))
|
25
|
+
unsaved_relation_objects << relation_object
|
26
|
+
relation_object
|
27
|
+
end
|
28
|
+
|
29
|
+
# API method
|
30
|
+
# create relation object
|
31
|
+
#
|
32
|
+
# @param [Hash] attributes
|
33
|
+
# @return [HydraAttribute::Model]
|
34
|
+
def create(attributes = {})
|
35
|
+
relation_object = @relation_class.create(prepare_relation_attributes(attributes))
|
36
|
+
self << relation_object
|
37
|
+
relation_object
|
38
|
+
end
|
39
|
+
|
40
|
+
# API method
|
41
|
+
# add relation object
|
42
|
+
#
|
43
|
+
# @param [HydraAttribute::Model] relation_object
|
44
|
+
# @return [HydraAttribute::Model::HasManyThrough::Relation]
|
45
|
+
def <<(relation_object)
|
46
|
+
if @object.persisted? && relation_object.persisted?
|
47
|
+
@through_class.create(@object_method_id => @object.id, @relation_method_id => relation_object.id)
|
48
|
+
else
|
49
|
+
unsaved_relation_objects << relation_object
|
50
|
+
end
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
# API method
|
55
|
+
# delete relation object
|
56
|
+
#
|
57
|
+
# @param [HydraAttribute::Model] relation_object
|
58
|
+
# @return [HydraAttribute::Model::HasManyThrough::Relation]
|
59
|
+
def destroy(relation_object)
|
60
|
+
if @object.persisted? and relation_object.persisted?
|
61
|
+
through_object = through_object_by_relation_object(relation_object)
|
62
|
+
through_object.destroy if through_object
|
63
|
+
else
|
64
|
+
unsaved_relation_objects.delete(relation_object)
|
65
|
+
end
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def save_unsaved_associations #:nodoc:
|
70
|
+
unsaved_relation_objects.each do |relation_object|
|
71
|
+
self << relation_object if relation_object.save
|
72
|
+
end
|
73
|
+
unsaved_relation_objects.clear
|
74
|
+
end
|
75
|
+
|
76
|
+
def delete_unsaved_associations #:nodoc:
|
77
|
+
unsaved_relation_objects.clear
|
78
|
+
end
|
79
|
+
|
80
|
+
def inspect
|
81
|
+
relation_objects.inspect
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
def respond_to_missing?(method, include_private)
|
86
|
+
relation_objects.respond_to?(method, include_private)
|
87
|
+
end
|
88
|
+
|
89
|
+
def method_missing(method, *args, &block)
|
90
|
+
relation_objects.public_send(method, *args, &block)
|
91
|
+
end
|
92
|
+
|
93
|
+
def relation_objects
|
94
|
+
persisted_relation_objects + unsaved_relation_objects
|
95
|
+
end
|
96
|
+
|
97
|
+
def persisted_relation_objects
|
98
|
+
@through_class.send(@through_method, @object.id)
|
99
|
+
end
|
100
|
+
|
101
|
+
def unsaved_relation_objects
|
102
|
+
@unsaved_relation_objects ||= []
|
103
|
+
end
|
104
|
+
|
105
|
+
def through_object_by_relation_object(relation_object)
|
106
|
+
@through_class.all.find do |through_object|
|
107
|
+
through_object.send(@object_method_id) == @object.id && through_object.send(@relation_method_id) == relation_object.id
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def prepare_relation_attributes(attributes)
|
112
|
+
if @copy_attribute
|
113
|
+
attributes.reverse_merge(@copy_attribute => @object.send(@copy_attribute))
|
114
|
+
else
|
115
|
+
attributes
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
module ClassMethods
|
121
|
+
def has_many(collection, options = {})
|
122
|
+
relation_class_name = "::HydraAttribute::#{collection.to_s.singularize.camelize}" # HydraAttribute::HydraAttribute
|
123
|
+
through_class_name = "::HydraAttribute::#{options[:through].to_s.camelize}" # HydraAttribute::HydraAttributeSet
|
124
|
+
object_method_id = "#{name.demodulize.underscore}_id" # 'hydra_set_id'
|
125
|
+
relation_method_id = "#{collection.to_s.singularize}_id" # 'hydra_attribute_id'
|
126
|
+
through_method_name = "#{collection}_by_#{object_method_id}" # 'hydra_attributes_by_hydra_set_id'
|
127
|
+
copy_attribute = options[:copy_attribute] ? ":#{options[:copy_attribute]}" : 'nil' # :entity_type
|
128
|
+
|
129
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
130
|
+
def #{collection}
|
131
|
+
@#{collection} ||= Relation.new(self,
|
132
|
+
:relation_class => #{relation_class_name},
|
133
|
+
:through_class => #{through_class_name},
|
134
|
+
:through_method => :#{through_method_name},
|
135
|
+
:object_method_id => :#{object_method_id},
|
136
|
+
:relation_method_id => :#{relation_method_id},
|
137
|
+
:copy_attribute => #{copy_attribute})
|
138
|
+
end
|
139
|
+
|
140
|
+
def create
|
141
|
+
result = super
|
142
|
+
if result
|
143
|
+
#{collection}.save_unsaved_associations
|
144
|
+
end
|
145
|
+
result
|
146
|
+
end
|
147
|
+
|
148
|
+
def update
|
149
|
+
result = super
|
150
|
+
if result
|
151
|
+
#{collection}.save_unsaved_associations
|
152
|
+
end
|
153
|
+
result
|
154
|
+
end
|
155
|
+
|
156
|
+
def delete
|
157
|
+
result = super
|
158
|
+
if result
|
159
|
+
#{collection}.delete_unsaved_associations
|
160
|
+
end
|
161
|
+
result
|
162
|
+
end
|
163
|
+
EOS
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module HydraAttribute
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# @see HydraAttribute::Model::IdentityMap::ClassMethods ClassMethods for documentation
|
5
|
+
module IdentityMap
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
# Identity map key
|
10
|
+
#
|
11
|
+
# @return [Symbol]
|
12
|
+
def identity_map_cache_key
|
13
|
+
@identity_map_cache_key ||= name.underscore.to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
# Identity map
|
17
|
+
#
|
18
|
+
# @return [HydraAttribute::IdentityMap]
|
19
|
+
def identity_map
|
20
|
+
::HydraAttribute.cache(identity_map_cache_key) { ::HydraAttribute::IdentityMap.new }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns identity map object which is inserted into the default one
|
24
|
+
#
|
25
|
+
# @param [Symbol] cache_key
|
26
|
+
# @return [HydraAttribute::IdentityMap]
|
27
|
+
def nested_identity_map(cache_key)
|
28
|
+
identity_map.cache(cache_key) { ::HydraAttribute::IdentityMap.new }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Proxy method to +identity_map+
|
32
|
+
#
|
33
|
+
# @param [String, Symbol] key
|
34
|
+
# @param [NilClass, Object] value
|
35
|
+
# @yield
|
36
|
+
# @return [Object]
|
37
|
+
def cache(key, value = nil, &block)
|
38
|
+
identity_map.cache(key, value, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Registers nested cache
|
42
|
+
#
|
43
|
+
# @param [Array<Symbol>] cache_keys
|
44
|
+
# @return [NilClass]
|
45
|
+
def register_nested_cache(*cache_keys)
|
46
|
+
cache_keys.each do |cache_key|
|
47
|
+
nested_cache_keys << cache_key
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
# Store all nested cache keys
|
53
|
+
def nested_cache_keys
|
54
|
+
@nested_cache_keys ||= []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module HydraAttribute
|
2
|
+
module Model
|
3
|
+
module Mediator
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class << self
|
7
|
+
# Holds all subscriptions
|
8
|
+
#
|
9
|
+
# @return [Hash]
|
10
|
+
def subscriptions
|
11
|
+
@subscriptions ||= Hash.new do |reporters, reporter|
|
12
|
+
reporters[reporter] = Hash.new do |events, event|
|
13
|
+
events[event] = []
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Subscribes listeners for reporter event
|
19
|
+
#
|
20
|
+
# @param [String] listener the name of the class which listens the event
|
21
|
+
# @param [String] reporter
|
22
|
+
# @param [Hash] events
|
23
|
+
# @return [NilClass]
|
24
|
+
def subscribe(listener, reporter, events)
|
25
|
+
events.each do |event, callback|
|
26
|
+
subscriptions[reporter][event] << [listener, callback]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Notifies listeners
|
31
|
+
#
|
32
|
+
# @param [String] reporter
|
33
|
+
# @param [Symbol] event
|
34
|
+
# @param [HydraAttribute::Model::Mediator] object
|
35
|
+
# @return [NilClass]
|
36
|
+
def notify(reporter, event, object)
|
37
|
+
subscriptions[reporter][event].each do |listener, callback|
|
38
|
+
listener.constantize.send(callback, object)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Clears all subscriptions
|
43
|
+
#
|
44
|
+
# @return [NilClass]
|
45
|
+
def clear
|
46
|
+
@subscriptions = nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
module ClassMethods
|
51
|
+
# Defines which class and its methods this object should observe
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# class ModelOne
|
55
|
+
# include HydraAttribute::Model::Mediator
|
56
|
+
# observe 'ModelTwo', create: :model_two_created, destroy: :model_two_destroyed
|
57
|
+
#
|
58
|
+
# def self.model_two_created(model_two)
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# def self.model_two_destroyed(model_two)
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# @param [String] class_name
|
66
|
+
# @param [Hash] events
|
67
|
+
# @return [NilClass]
|
68
|
+
def observe(class_name, events)
|
69
|
+
Mediator.subscribe(name, class_name, events)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Notifies all listeners about event
|
74
|
+
#
|
75
|
+
# @param [Symbol] event
|
76
|
+
# @return [NilClass]
|
77
|
+
def notify(event)
|
78
|
+
if block_given?
|
79
|
+
Mediator.notify(self.class.name, "before_#{event}".to_sym, self)
|
80
|
+
result = yield
|
81
|
+
Mediator.notify(self.class.name, "after_#{event}".to_sym, self)
|
82
|
+
result
|
83
|
+
else
|
84
|
+
Mediator.notify(self.class.name, event, self)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module HydraAttribute
|
2
|
+
module Model
|
3
|
+
module Notifiable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
private
|
7
|
+
# Overwrite for notifying subscribed objects
|
8
|
+
def create
|
9
|
+
notify(:create) { super }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Overwrite for notifying subscribed objects
|
13
|
+
def update
|
14
|
+
notify(:update) { super }
|
15
|
+
end
|
16
|
+
|
17
|
+
# Overwrite for notifying subscribed objects
|
18
|
+
def delete
|
19
|
+
notify(:destroy) { super }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,424 @@
|
|
1
|
+
module HydraAttribute
|
2
|
+
class RecordNotFound < ::ActiveRecord::RecordNotFound
|
3
|
+
end
|
4
|
+
|
5
|
+
module Model
|
6
|
+
# @see HydraAttribute::Model::Persistence::ClassMethods ClassMethods for documentation
|
7
|
+
module Persistence
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Creates +Mutex+ object
|
12
|
+
#
|
13
|
+
# @return [Mutex]
|
14
|
+
def attribute_methods_mutex
|
15
|
+
@attribute_methods_mutex ||= Mutex.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# Holds generated attribute methods status
|
19
|
+
#
|
20
|
+
# @return [TrueClass, FalseClass]
|
21
|
+
def generated_attribute_methods?
|
22
|
+
@generated_attribute_methods ||= false
|
23
|
+
end
|
24
|
+
|
25
|
+
# Define attribute methods based on column names
|
26
|
+
#
|
27
|
+
# @return [NilClass]
|
28
|
+
def define_attribute_methods
|
29
|
+
attribute_methods_mutex.synchronize do
|
30
|
+
return if generated_attribute_methods?
|
31
|
+
column_names.each do |column_name|
|
32
|
+
define_attribute_method(column_name)
|
33
|
+
end
|
34
|
+
@generated_attribute_methods = true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Defines attribute getter and setter
|
39
|
+
#
|
40
|
+
# @param [String] column_name
|
41
|
+
# @return [NilClass]
|
42
|
+
def define_attribute_method(column_name)
|
43
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
44
|
+
def #{column_name} # def name
|
45
|
+
attributes[:#{column_name}] # attributes[:name]
|
46
|
+
end # end
|
47
|
+
|
48
|
+
def #{column_name}=(value) # def name=(value)
|
49
|
+
attributes[:#{column_name}] = type_cast_value(:#{column_name}, value) # attributes[:name] = type_cast_value(:name, value)
|
50
|
+
end # end
|
51
|
+
|
52
|
+
def #{column_name}? # name?
|
53
|
+
attributes[:#{column_name}].present? # attributes[:name].present?
|
54
|
+
end # end
|
55
|
+
EOS
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns database adapter
|
59
|
+
#
|
60
|
+
# @return [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
61
|
+
def connection
|
62
|
+
@connection ||= ::ActiveRecord::Base.connection
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns table name
|
66
|
+
#
|
67
|
+
# @return [String]
|
68
|
+
def table_name
|
69
|
+
@table_name ||= name.demodulize.tableize
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns table columns
|
73
|
+
#
|
74
|
+
# @return [Array<ActiveRecord::ConnectionAdapters::Column>]
|
75
|
+
def columns
|
76
|
+
@columns ||= connection.schema_cache.columns[table_name]
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns hash of column objects with their names as a keys
|
80
|
+
# Keys are string
|
81
|
+
#
|
82
|
+
# @return [Hash]
|
83
|
+
def columns_hash
|
84
|
+
@columns_hash ||= connection.schema_cache.columns_hash[table_name]
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns hash of column objects with their names as a keys
|
88
|
+
# Keys are symbols
|
89
|
+
#
|
90
|
+
# @return [Hash]
|
91
|
+
def symbolized_columns_hash
|
92
|
+
@symbolized_columns_hash ||= columns_hash.symbolize_keys
|
93
|
+
end
|
94
|
+
|
95
|
+
# Return column object
|
96
|
+
#
|
97
|
+
# @param [String]
|
98
|
+
# @return [ActiveRecord::ConnectionAdapters::Column]
|
99
|
+
def column(name)
|
100
|
+
columns_hash[name]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns column names
|
104
|
+
#
|
105
|
+
# @return [Array<String>]
|
106
|
+
def column_names
|
107
|
+
@column_names ||= columns.map(&:name)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Holds attributes with default values
|
111
|
+
#
|
112
|
+
# @return [Hash]
|
113
|
+
def attributes
|
114
|
+
@attributes ||= columns.each_with_object({}) do |column, attributes|
|
115
|
+
attributes[column.name.to_sym] = column.default
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns arel table
|
120
|
+
#
|
121
|
+
# @return [Arel::Table]
|
122
|
+
def arel_table
|
123
|
+
@arel_table ||= Arel::Table.new(table_name, self)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Finds all records
|
127
|
+
#
|
128
|
+
# @return [Array<HydraAttribute::Model>]
|
129
|
+
def all
|
130
|
+
where
|
131
|
+
end
|
132
|
+
|
133
|
+
# Finds one record by ID
|
134
|
+
#
|
135
|
+
# @param [Integer] id
|
136
|
+
# @return [HydraAttribute::Model]
|
137
|
+
def find(id)
|
138
|
+
result = connection.select_one(compile_select({id: id}, Arel.star, 1))
|
139
|
+
raise RecordNotFound, "Couldn't find #{name} with id=#{id}" unless result
|
140
|
+
new(result)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Finds records with where filter
|
144
|
+
#
|
145
|
+
# @param [Hash] attributes
|
146
|
+
# @param [Array] fields
|
147
|
+
# @param [NilClass, Integer] limit
|
148
|
+
# @param [NilClass, Integer] offset
|
149
|
+
# @return [Array<HydraAttribute::Model>]
|
150
|
+
def where(attributes = {}, fields = Arel.star, limit = nil, offset = nil)
|
151
|
+
connection.select_all(compile_select(attributes, fields, limit, offset)).map do |values|
|
152
|
+
new(values)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Finds records with negative where filter
|
157
|
+
#
|
158
|
+
# @param [Hash] attributes
|
159
|
+
# @param [Array] fields
|
160
|
+
# @param [NilClass, Integer] limit
|
161
|
+
# @param [NilClass, Integer] offset
|
162
|
+
# @return [Array<HydraAttribute::Model>]
|
163
|
+
def where_not(attributes = {}, fields = Arel.star, limit = nil, offset = nil)
|
164
|
+
select = compile_select({}, fields, limit, offset)
|
165
|
+
select.where(compile_where_not(attributes)) unless attributes.empty?
|
166
|
+
connection.select_all(select).map do |values|
|
167
|
+
new(values)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Creates new record
|
172
|
+
#
|
173
|
+
# @param [Hash] attributes
|
174
|
+
# @return [HydraAttribute::Model]
|
175
|
+
def create(attributes = {})
|
176
|
+
model = new(attributes.except(:id, 'id'))
|
177
|
+
model.save
|
178
|
+
model
|
179
|
+
end
|
180
|
+
|
181
|
+
# Updates record by ID
|
182
|
+
#
|
183
|
+
# @param [Integer] id
|
184
|
+
# @param [Hash] attributes
|
185
|
+
# @return [HydraAttribute::Model]
|
186
|
+
def update(id, attributes = {})
|
187
|
+
model = find(id)
|
188
|
+
model.assign_attributes(attributes.except(:id, 'id'))
|
189
|
+
model.save
|
190
|
+
model
|
191
|
+
end
|
192
|
+
|
193
|
+
# Destroys model by its ID
|
194
|
+
#
|
195
|
+
# @param [Integer] id
|
196
|
+
# @return [TrueClass, FalseClass]
|
197
|
+
def destroy(id)
|
198
|
+
find(id).destroy
|
199
|
+
end
|
200
|
+
|
201
|
+
# Destroys all models
|
202
|
+
#
|
203
|
+
# @return [Hash] result for each deleted object
|
204
|
+
def destroy_all
|
205
|
+
all.map(&:id).each_with_object({}) do |model_id, result|
|
206
|
+
result[model_id] = destroy(model_id)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Compiles attributes for performing +SELECT+ query
|
211
|
+
#
|
212
|
+
# @param [Hash] attributes
|
213
|
+
# @param [Array] fields attributes which should be selected
|
214
|
+
# @param [NilClass, Integer] limit
|
215
|
+
# @param [NilClass, Integer] offset
|
216
|
+
# @return [Arel::SelectManager]
|
217
|
+
def compile_select(attributes = {}, fields = Arel.star, limit = nil, offset = nil)
|
218
|
+
columns = Array(fields).map { |field| arel_table[field] }
|
219
|
+
arel = select_manager.project(columns).take(limit).skip(offset)
|
220
|
+
arel.where(compile_where(attributes)) unless attributes.blank?
|
221
|
+
arel
|
222
|
+
end
|
223
|
+
|
224
|
+
# Compiles attributes for performing +INSERT+ query
|
225
|
+
#
|
226
|
+
# @param [Hash] attributes
|
227
|
+
# @return [Arel::InsertManager]
|
228
|
+
def compile_insert(attributes = {})
|
229
|
+
fields = attributes_to_columns(attributes)
|
230
|
+
arel_table.compile_insert(fields)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Compiles attributes for performing +UPDATE+ query
|
234
|
+
#
|
235
|
+
# @param [String] id
|
236
|
+
# @param [Hash] attributes
|
237
|
+
# @return [Arel::UpdateManager]
|
238
|
+
def compile_update(id, attributes = {})
|
239
|
+
fields = attributes_to_columns(attributes)
|
240
|
+
compile_select(id: id).compile_update(fields)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Compiles attributes for performing +DELETE+ query
|
244
|
+
#
|
245
|
+
# @param [Hash] attributes
|
246
|
+
# @return [Arel::DeleteManager]
|
247
|
+
def compile_delete(attributes = {})
|
248
|
+
compile_select(attributes).compile_delete
|
249
|
+
end
|
250
|
+
|
251
|
+
# Builds +arel+ object for select query
|
252
|
+
#
|
253
|
+
# @return [Arel::SelectManager]
|
254
|
+
def select_manager
|
255
|
+
arel_table.from(arel_table)
|
256
|
+
end
|
257
|
+
|
258
|
+
private
|
259
|
+
# Compiles data for +WHERE+ part
|
260
|
+
#
|
261
|
+
# @param [Hash] attributes
|
262
|
+
# @return [Arel::Nodes::And, Arel::Nodes::Equality]
|
263
|
+
def compile_where(attributes = {})
|
264
|
+
attributes.map do |name, value|
|
265
|
+
method = value.is_a?(Array) ? :in : :eq
|
266
|
+
arel_table[name].send(method, value)
|
267
|
+
end.inject(:and)
|
268
|
+
end
|
269
|
+
|
270
|
+
# Compiles negative data for +WHERE+ part
|
271
|
+
#
|
272
|
+
# @param [Hash] attributes
|
273
|
+
# @return [Arel::Nodes::And, Arel::Nodes::Equality]
|
274
|
+
def compile_where_not(attributes = {})
|
275
|
+
attributes.map do |name, value|
|
276
|
+
method = value.is_a?(Array) ? :not_in : :not_eq
|
277
|
+
arel_table[name].send(method, value)
|
278
|
+
end.inject(:and)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Replaces attributes' keys to +arel+ columns
|
282
|
+
#
|
283
|
+
# @param [Hash] attributes
|
284
|
+
# @return [Hash]
|
285
|
+
def attributes_to_columns(attributes = {})
|
286
|
+
attributes.each_with_object({}) do |(name, value), fields|
|
287
|
+
fields[arel_table[name]] = value
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Model initializer
|
293
|
+
#
|
294
|
+
# @param [Hash] attributes
|
295
|
+
def initialize(attributes = {})
|
296
|
+
@destroyed = false
|
297
|
+
@attributes = self.class.attributes.clone
|
298
|
+
|
299
|
+
assign_attributes(attributes)
|
300
|
+
end
|
301
|
+
|
302
|
+
# Assigns attributes
|
303
|
+
#
|
304
|
+
# @return [Hash] current attributes
|
305
|
+
def assign_attributes(new_attributes = {})
|
306
|
+
new_attributes.symbolize_keys.each do |name, value|
|
307
|
+
@attributes[name] = type_cast_value(name, value)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Return all attributes
|
312
|
+
#
|
313
|
+
# @return [Hash]
|
314
|
+
def attributes
|
315
|
+
@attributes
|
316
|
+
end
|
317
|
+
|
318
|
+
# Checks if model is saved in database
|
319
|
+
#
|
320
|
+
# @return [TrueClass, FalseClass]
|
321
|
+
def persisted?
|
322
|
+
id.present? and not destroyed?
|
323
|
+
end
|
324
|
+
|
325
|
+
# Checks if model is destroyed
|
326
|
+
#
|
327
|
+
# @return [TrueClass, FalseClass]
|
328
|
+
def destroyed?
|
329
|
+
@destroyed
|
330
|
+
end
|
331
|
+
|
332
|
+
# Saves model
|
333
|
+
# If model is persisted, update it otherwise create it.
|
334
|
+
#
|
335
|
+
# @return [TrueClass]
|
336
|
+
def save
|
337
|
+
return true if destroyed?
|
338
|
+
return false unless valid?
|
339
|
+
|
340
|
+
self.class.connection.transaction do
|
341
|
+
persisted? ? update : create
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# Destroys record from database
|
346
|
+
# This method runs callbacks
|
347
|
+
#
|
348
|
+
# @return [TrueClass]
|
349
|
+
def destroy
|
350
|
+
self.class.connection.transaction do
|
351
|
+
delete
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# Redefines base method because attribute methods define dynamically
|
356
|
+
#
|
357
|
+
# @param [Symbol] method
|
358
|
+
# @param [FalseClass, TrueClass] include_private
|
359
|
+
# @return [FalseClass, TrueClass]
|
360
|
+
def respond_to?(method, include_private = false)
|
361
|
+
self.class.define_attribute_methods unless self.class.generated_attribute_methods?
|
362
|
+
super
|
363
|
+
end
|
364
|
+
|
365
|
+
private
|
366
|
+
# Performs +INSERT+ query
|
367
|
+
#
|
368
|
+
# @return [Integer] primary key
|
369
|
+
def create
|
370
|
+
return id if persisted? or destroyed?
|
371
|
+
columns = attributes.except(:id, :created_at, :updated_at).merge(created_at: Time.now, updated_at: Time.now)
|
372
|
+
self.id = self.class.connection.insert(self.class.compile_insert(columns), 'SQL').to_i
|
373
|
+
end
|
374
|
+
|
375
|
+
# Performs +UPDATE+ query
|
376
|
+
#
|
377
|
+
# @return [TrueClass]
|
378
|
+
def update
|
379
|
+
return true unless persisted?
|
380
|
+
columns = attributes.except(:id, :created_at, :updated_at).merge(updated_at: Time.now)
|
381
|
+
self.class.connection.update(self.class.compile_update(id, columns), 'SQL')
|
382
|
+
true
|
383
|
+
end
|
384
|
+
|
385
|
+
# Deletes record from database
|
386
|
+
#
|
387
|
+
# @return [TrueClass]
|
388
|
+
def delete
|
389
|
+
return true unless persisted?
|
390
|
+
self.class.connection.delete(self.class.compile_delete(id: id), 'SQL')
|
391
|
+
@destroyed = true
|
392
|
+
end
|
393
|
+
|
394
|
+
# Type casts value based on its database type
|
395
|
+
#
|
396
|
+
# @param [Symbol] name
|
397
|
+
# @param [Object] value
|
398
|
+
# @return [Object] type casted value
|
399
|
+
def type_cast_value(name, value)
|
400
|
+
self.class.symbolized_columns_hash[name].type_cast(value)
|
401
|
+
end
|
402
|
+
|
403
|
+
# Redefine method for auto generation attribute methods
|
404
|
+
#
|
405
|
+
# @param [Symbol] symbol
|
406
|
+
# @params[Array] args
|
407
|
+
# @yield
|
408
|
+
# @return [Object]
|
409
|
+
def method_missing(symbol, *args, &block)
|
410
|
+
if self.class.generated_attribute_methods?
|
411
|
+
super
|
412
|
+
else
|
413
|
+
self.class.define_attribute_methods
|
414
|
+
if respond_to?(symbol)
|
415
|
+
send(symbol, *args, &block)
|
416
|
+
else
|
417
|
+
super
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|