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.
Files changed (101) hide show
  1. data/.gitignore +2 -1
  2. data/.travis.yml +6 -5
  3. data/CHANGELOG.md +6 -0
  4. data/Gemfile +1 -1
  5. data/README.md +3 -3
  6. data/Rakefile +2 -7
  7. data/gemfiles/activerecord-3.2.gemfile +5 -0
  8. data/hydra_attribute.gemspec +6 -7
  9. data/lib/hydra_attribute.rb +17 -18
  10. data/lib/hydra_attribute/active_record.rb +34 -13
  11. data/lib/hydra_attribute/active_record/association_preloader.rb +47 -28
  12. data/lib/hydra_attribute/active_record/attribute_methods.rb +29 -140
  13. data/lib/hydra_attribute/active_record/mass_assignment_security.rb +39 -0
  14. data/lib/hydra_attribute/active_record/migration.rb +4 -4
  15. data/lib/hydra_attribute/active_record/relation.rb +6 -7
  16. data/lib/hydra_attribute/active_record/relation/query_methods.rb +28 -18
  17. data/lib/hydra_attribute/hydra_attribute.rb +12 -49
  18. data/lib/hydra_attribute/hydra_attribute_set.rb +67 -0
  19. data/lib/hydra_attribute/hydra_entity.rb +110 -0
  20. data/lib/hydra_attribute/hydra_entity_attribute_association.rb +155 -0
  21. data/lib/hydra_attribute/hydra_set.rb +24 -26
  22. data/lib/hydra_attribute/hydra_value.rb +210 -0
  23. data/lib/hydra_attribute/identity_map.rb +18 -0
  24. data/lib/hydra_attribute/middleware/identity_map.rb +15 -0
  25. data/lib/hydra_attribute/migrator.rb +24 -21
  26. data/lib/hydra_attribute/model.rb +47 -0
  27. data/lib/hydra_attribute/model/cacheable.rb +207 -0
  28. data/lib/hydra_attribute/model/dirty.rb +39 -0
  29. data/lib/hydra_attribute/model/has_many_through.rb +168 -0
  30. data/lib/hydra_attribute/model/identity_map.rb +59 -0
  31. data/lib/hydra_attribute/model/mediator.rb +89 -0
  32. data/lib/hydra_attribute/model/notifiable.rb +23 -0
  33. data/lib/hydra_attribute/model/persistence.rb +424 -0
  34. data/lib/hydra_attribute/model/validations.rb +40 -0
  35. data/lib/hydra_attribute/version.rb +1 -1
  36. data/spec/environments/mysql.rb +23 -0
  37. data/spec/environments/postgresql.rb +23 -0
  38. data/spec/environments/sqlite.rb +12 -0
  39. data/spec/fixtures/category.rb +8 -0
  40. data/spec/fixtures/product.rb +8 -0
  41. data/spec/fixtures/product_black_list.rb +13 -0
  42. data/spec/fixtures/product_white_list.rb +13 -0
  43. data/spec/hydra_attribute/active_record/attribute_methods_spec.rb +23 -28
  44. data/spec/hydra_attribute/active_record/mass_assignment_security_spec.rb +41 -0
  45. data/spec/hydra_attribute/active_record_spec.rb +577 -0
  46. data/spec/hydra_attribute/hydra_attribute_set_spec.rb +651 -0
  47. data/spec/hydra_attribute/hydra_attribute_spec.rb +208 -10
  48. data/spec/hydra_attribute/hydra_entity_attribute_association_spec.rb +216 -0
  49. data/spec/hydra_attribute/hydra_entity_spec.rb +71 -0
  50. data/spec/hydra_attribute/hydra_set_spec.rb +51 -10
  51. data/spec/hydra_attribute/hydra_value_spec.rb +286 -0
  52. data/spec/hydra_attribute/identity_map_spec.rb +47 -0
  53. data/spec/hydra_attribute/migrator_spec.rb +411 -0
  54. data/spec/hydra_attribute/model/cacheable_spec.rb +106 -0
  55. data/spec/hydra_attribute/model/has_many_through_spec.rb +132 -0
  56. data/spec/hydra_attribute/model/identity_map_spec.rb +39 -0
  57. data/spec/hydra_attribute/model/mediator_spec.rb +62 -0
  58. data/spec/hydra_attribute/model/persistence_spec.rb +550 -0
  59. data/spec/hydra_attribute/model_spec.rb +39 -0
  60. data/spec/hydra_attribute_spec.rb +36 -0
  61. data/spec/spec_helper.rb +10 -42
  62. metadata +76 -100
  63. data/Appraisals +0 -7
  64. data/cucumber.yml +0 -1
  65. data/features/entity/create.feature +0 -145
  66. data/features/entity/destroy.feature +0 -111
  67. data/features/entity/new.feature +0 -121
  68. data/features/entity/update.feature +0 -147
  69. data/features/hydra_attribute/create.feature +0 -30
  70. data/features/hydra_attribute/destroy.feature +0 -26
  71. data/features/hydra_attribute/update.feature +0 -36
  72. data/features/hydra_set/destroy.feature +0 -31
  73. data/features/migrations/create_and_drop.feature +0 -165
  74. data/features/migrations/migrate_and_rollback.feature +0 -211
  75. data/features/relation/query_methods/group.feature +0 -42
  76. data/features/relation/query_methods/order.feature +0 -67
  77. data/features/relation/query_methods/reorder.feature +0 -29
  78. data/features/relation/query_methods/reverse_order.feature +0 -29
  79. data/features/relation/query_methods/select.feature +0 -50
  80. data/features/relation/query_methods/where.feature +0 -115
  81. data/features/step_definitions/connections.rb +0 -65
  82. data/features/step_definitions/model_steps.rb +0 -136
  83. data/features/step_definitions/query_methods.rb +0 -48
  84. data/features/step_definitions/record_steps.rb +0 -93
  85. data/features/support/env.rb +0 -38
  86. data/features/support/world.rb +0 -61
  87. data/lib/hydra_attribute/active_record/association.rb +0 -113
  88. data/lib/hydra_attribute/active_record/reflection.rb +0 -16
  89. data/lib/hydra_attribute/association_builder.rb +0 -69
  90. data/lib/hydra_attribute/builder.rb +0 -37
  91. data/lib/hydra_attribute/entity_callbacks.rb +0 -26
  92. data/lib/hydra_attribute/hydra_attribute_methods.rb +0 -226
  93. data/lib/hydra_attribute/hydra_methods.rb +0 -528
  94. data/lib/hydra_attribute/hydra_set_methods.rb +0 -95
  95. data/lib/hydra_attribute/hydra_value_methods.rb +0 -21
  96. data/lib/hydra_attribute/memoizable.rb +0 -37
  97. data/spec/hydra_attribute/active_record/relation/query_methods_spec.rb +0 -31
  98. data/spec/hydra_attribute/hydra_attribute_methods_spec.rb +0 -458
  99. data/spec/hydra_attribute/hydra_methods_spec.rb +0 -456
  100. data/spec/hydra_attribute/hydra_set_methods_spec.rb +0 -203
  101. 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