sequel_dm 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ vendor/
2
+ tags
3
+ .bundle
4
+ .DS_Store
5
+ pkg
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ rvm:
2
+ - 2.0.0
3
+ script: "bundle exec rspec spec/"
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in aggregate_builder.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rspec'
8
+ gem 'debugger'
9
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,51 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sequel_dm (0.0.2)
5
+ activesupport
6
+ sequel (~> 4.6)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (4.0.2)
12
+ i18n (~> 0.6, >= 0.6.4)
13
+ minitest (~> 4.2)
14
+ multi_json (~> 1.3)
15
+ thread_safe (~> 0.1)
16
+ tzinfo (~> 0.3.37)
17
+ atomic (1.1.14)
18
+ columnize (0.3.6)
19
+ debugger (1.6.5)
20
+ columnize (>= 0.3.1)
21
+ debugger-linecache (~> 1.2.0)
22
+ debugger-ruby_core_source (~> 1.3.1)
23
+ debugger-linecache (1.2.0)
24
+ debugger-ruby_core_source (1.3.1)
25
+ diff-lcs (1.2.5)
26
+ i18n (0.6.9)
27
+ minitest (4.7.5)
28
+ multi_json (1.8.4)
29
+ rake (10.1.1)
30
+ rspec (2.14.1)
31
+ rspec-core (~> 2.14.0)
32
+ rspec-expectations (~> 2.14.0)
33
+ rspec-mocks (~> 2.14.0)
34
+ rspec-core (2.14.7)
35
+ rspec-expectations (2.14.4)
36
+ diff-lcs (>= 1.1.3, < 2.0)
37
+ rspec-mocks (2.14.4)
38
+ sequel (4.6.0)
39
+ thread_safe (0.1.3)
40
+ atomic
41
+ tzinfo (0.3.38)
42
+
43
+ PLATFORMS
44
+ ruby
45
+
46
+ DEPENDENCIES
47
+ bundler (~> 1.3)
48
+ debugger
49
+ rake
50
+ rspec
51
+ sequel_dm!
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Albert Gazizov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # SequelDM [![Build Status](https://travis-ci.org/AlbertGazizov/sequel_dm.png)](https://travis-ci.org/AlbertGazizov/sequel_dm) [![Code Climate](https://codeclimate.com/github/AlbertGazizov/sequel_dm.png)](https://codeclimate.com/github/AlbertGazizov/sequel_dm)
2
+
3
+
4
+
5
+ SequelDM is a Sequel based Data Mapper pattern implementation
6
+
7
+ NOTE: This gem is not production ready, it needs a lot of work!
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'sequel_dm', github: 'AlbertGazizov/sequel_dm'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ ## Contributing
20
+
21
+ 1. Fork it
22
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
23
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
24
+ 4. Push to the branch (`git push origin my-new-feature`)
25
+ 5. Create new Pull Request
26
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,76 @@
1
+ # Helper class for arguments validation
2
+ module SequelDM::ArgsValidator
3
+ class << self
4
+
5
+ # Checks that specifid +obj+ is a symbol
6
+ # @param obj some object
7
+ # @param obj_name object's name, used to clarify error causer in exception
8
+ def is_symbol!(obj, obj_name)
9
+ unless obj.is_a?(Symbol)
10
+ raise ArgumentError, "#{obj_name} should be a Symbol"
11
+ end
12
+ end
13
+
14
+ # Checks that specifid +obj+ is an Array
15
+ # @param obj some object
16
+ # @param obj_name object's name, used to clarify error causer in exception
17
+ def is_array!(obj, obj_name)
18
+ unless obj.is_a?(Array)
19
+ raise ArgumentError, "#{obj_name} should be an Array"
20
+ end
21
+ end
22
+
23
+ # Checks that specifid +obj+ is a Hash
24
+ # @param obj some object
25
+ # @param obj_name object's name, used to clarify error causer in exception
26
+ def is_hash!(obj, obj_name)
27
+ unless obj.is_a?(Hash)
28
+ raise ArgumentError, "#{obj_name} should be a Hash"
29
+ end
30
+ end
31
+
32
+ # Checks that specifid +obj+ is a Class
33
+ # @param obj some object
34
+ # @param obj_name object's name, used to clarify error causer in exception
35
+ def is_class!(obj, obj_name)
36
+ unless obj.is_a?(Class)
37
+ raise ArgumentError, "#{obj_name} should be a Class"
38
+ end
39
+ end
40
+
41
+ # Checks that specifid +obj+ is a Proc
42
+ # @param obj some object
43
+ # @param obj_name object's name, used to clarify error causer in exception
44
+ def is_proc!(obj, obj_name)
45
+ unless obj.is_a?(Proc)
46
+ raise ArgumentError, "#{obj_name} should be a Proc"
47
+ end
48
+ end
49
+
50
+ # Checks that specifid +obj+ is a symbol or Class
51
+ # @param obj some object
52
+ # @param obj_name object's name, used to clarify error causer in exception
53
+ def is_symbol_or_class!(obj, obj_name)
54
+ if !obj.is_a?(Symbol) && !obj.is_a?(Class)
55
+ raise ArgumentError, "#{obj_name} should be a Symbol or Class"
56
+ end
57
+ end
58
+
59
+ # Checks that specifid +hash+ has a specified +key+
60
+ # @param hash some hash
61
+ # @param key hash's key
62
+ def has_key!(hash, key)
63
+ unless hash.has_key?(key)
64
+ raise ArgumentError, "#{hash} should has #{key} key"
65
+ end
66
+ end
67
+
68
+ # Checks that specified +block+ is given
69
+ # @param block some block
70
+ def block_given!(block)
71
+ unless block
72
+ raise ArgumentError, "Block should be given"
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,353 @@
1
+ require 'sequel_dm/extensions/select_fields'
2
+
3
+ SequelDM::DAO = Class.new(Sequel::Model)
4
+ module SequelDM
5
+ def self.DAO(source)
6
+ Class.new(SequelDM::DAO).set_dataset(source)
7
+ end
8
+
9
+ class DAO
10
+ class_attribute :mapper
11
+
12
+ dataset_module do
13
+ include SequelDM::Extensions::SelectFields
14
+ end
15
+
16
+ class << self
17
+ def def_one_to_many(opts)
18
+ one_to_one = opts[:type] == :one_to_one
19
+ name = opts[:name]
20
+ model = self
21
+ key = (opts[:key] ||= opts.default_key)
22
+ km = opts[:key_method] ||= opts[:key]
23
+ cks = opts[:keys] = Array(key)
24
+ opts[:key_methods] = Array(opts[:key_method])
25
+ primary_key = (opts[:primary_key] ||= self.primary_key)
26
+ opts[:eager_loader_key] = primary_key unless opts.has_key?(:eager_loader_key)
27
+ cpks = opts[:primary_keys] = Array(primary_key)
28
+ pkc = opts[:primary_key_column] ||= primary_key
29
+ pkcs = opts[:primary_key_columns] ||= Array(pkc)
30
+ raise(Error, "mismatched number of keys: #{cks.inspect} vs #{cpks.inspect}") unless cks.length == cpks.length
31
+ uses_cks = opts[:uses_composite_keys] = cks.length > 1
32
+ slice_range = opts.slice_range
33
+ opts[:dataset] ||= proc do
34
+ opts.associated_dataset.where(opts.predicate_keys.zip(cpks.map{|k| send(k)}))
35
+ end
36
+ opts[:eager_loader] = proc do |eo|
37
+ h = eo[:id_map]
38
+ rows = eo[:rows]
39
+ reciprocal = opts.reciprocal
40
+ klass = opts.associated_class
41
+ filter_keys = opts.predicate_key
42
+ ds = model.eager_loading_dataset(opts, klass.where(filter_keys=>h.keys), nil, eo[:associations], eo)
43
+ assign_singular = true if one_to_one
44
+ case opts.eager_limit_strategy
45
+ when :distinct_on
46
+ ds = ds.distinct(*filter_keys).order_prepend(*filter_keys)
47
+ when :window_function
48
+ delete_rn = true
49
+ rn = ds.row_number_column
50
+ ds = apply_window_function_eager_limit_strategy(ds, opts)
51
+ when :ruby
52
+ assign_singular = false if one_to_one && slice_range
53
+ end
54
+ ds.all do |assoc_record|
55
+ assoc_record.values.delete(rn) if delete_rn
56
+ hash_key = uses_cks ? km.map{|k| assoc_record.send(k)} : assoc_record.send(km)
57
+ next unless objects = h[hash_key]
58
+ if assign_singular
59
+ objects.each do |object|
60
+ unless object.send(name)
61
+ # TODO: add persistance_associations update here
62
+ object.send("#{name}=", assoc_record)
63
+ assoc_record.send("#{reciprocal}=", object) if reciprocal
64
+ end
65
+ end
66
+ else
67
+ objects.each do |object|
68
+ add_to_associations_state(object, name, assoc_record)
69
+ object.send(name).push(assoc_record)
70
+ assoc_record.send("#{reciprocal}=", object) if reciprocal
71
+ end
72
+ end
73
+ end
74
+ if opts.eager_limit_strategy == :ruby
75
+ if one_to_one
76
+ if slice_range
77
+ rows.each{|o| o.associations[name] = o.associations[name][slice_range.begin]}
78
+ end
79
+ else
80
+ rows.each{|o| o.associations[name] = o.associations[name][slice_range] || []}
81
+ end
82
+ end
83
+ end
84
+ super
85
+ end
86
+
87
+ def set_dataset_row_proc(ds)
88
+ ds.row_proc = Proc.new do |raw|
89
+ raise StandardError, "Mapper should be specified" if !self.mapper
90
+ entity = self.mapper.to_entity(raw)
91
+ save_state(entity, raw)
92
+ entity
93
+ end
94
+ end
95
+
96
+ def set_mapper(mapper)
97
+ SequelDM::ArgsValidator.is_class!(mapper, :mapper)
98
+ self.mapper = mapper
99
+ end
100
+
101
+ # Database methods
102
+
103
+ def insert(entity, root = nil)
104
+ raw = mapper.to_hash(entity, root)
105
+ key = dataset.insert(raw)
106
+ set_entity_primary_key(entity, raw, key)
107
+ save_state(entity, raw)
108
+ insert_associations(entity)
109
+ entity
110
+ end
111
+
112
+ def insert_all(entities, root = nil)
113
+ entities.each do |entity|
114
+ insert(entity, root)
115
+ end
116
+ end
117
+
118
+ def update(entity, root = nil)
119
+ raw = mapper.to_hash(entity, root)
120
+ raw = select_only_changed_values(entity, raw)
121
+
122
+ unless raw.empty?
123
+ update_state(entity, raw)
124
+
125
+ key_condition = prepare_key_condition_from_entity(entity)
126
+ dataset.where(key_condition).update(raw)
127
+ end
128
+
129
+ insert_or_update_associations(entity)
130
+ entity
131
+ end
132
+
133
+ def update_all(entities, root = nil)
134
+ entities.each do |entity|
135
+ update(entity, root)
136
+ end
137
+ end
138
+
139
+ def save(entity, root = nil)
140
+ if has_persistance_state?(entity)
141
+ update(entity, root)
142
+ else
143
+ insert(entity, root)
144
+ end
145
+ end
146
+
147
+ def save_all(entities, root = nil)
148
+ entities.each do |entity|
149
+ save(entity, root)
150
+ end
151
+ end
152
+
153
+ def delete(entity)
154
+ key_condition = prepare_key_condition_from_entity(entity)
155
+ dataset.where(key_condition).delete
156
+ delete_associations(entity)
157
+ end
158
+
159
+ # TODO: refactor
160
+ def delete_all(entities)
161
+ entity_ids = entities.map(&:id)
162
+ dataset.where(id: entity_ids).delete
163
+ unless association_reflections.empty?
164
+ association_reflections.each do |association, options|
165
+ association_dao = options[:class]
166
+ conditions = (options[:conditions] || {}).merge(options[:key] => entity_ids)
167
+ association_dao.where(conditions).delete
168
+ end
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ def select_only_changed_values(entity, hash)
175
+ changes = {}
176
+ return hash unless entity.instance_variable_defined?(:@persistance_state)
177
+
178
+ persistance_state = entity.instance_variable_get(:@persistance_state)
179
+ hash.each do |column, value|
180
+ previous_column_value = persistance_state[column]
181
+ if persistance_state.has_key?(column) && column_value_changed?(previous_column_value, value)
182
+ changes[column] = value
183
+ end
184
+ end
185
+ changes
186
+ end
187
+
188
+ def column_value_changed?(previous_value, new_value)
189
+ previous_value != new_value
190
+ end
191
+
192
+ def save_state(entity, raw)
193
+ if !entity.is_a?(Integer) && !entity.is_a?(Symbol)
194
+ entity.instance_variable_set(:@persistance_state, raw)
195
+ end
196
+ end
197
+
198
+ def update_state(entity, raw)
199
+ persistance_state = entity.instance_variable_get(:@persistance_state)
200
+ if persistance_state
201
+ persistance_state.merge!(raw)
202
+ end
203
+ end
204
+
205
+ def has_persistance_state?(entity)
206
+ !!entity.instance_variable_get(:@persistance_state)
207
+ end
208
+
209
+ def set_associations_state(entity, association_name, associations)
210
+ persistance_associations = entity.instance_variable_get(:@persistance_associations) || {}
211
+ persistance_associations[association_name] ||= []
212
+ persistance_associations[association_name] |= associations
213
+ entity.instance_variable_set(:@persistance_associations, persistance_associations)
214
+ end
215
+
216
+ def add_to_associations_state(entity, association_name, association)
217
+ persistance_associations = entity.instance_variable_get(:@persistance_associations) || {}
218
+ persistance_associations[association_name] ||= []
219
+ persistance_associations[association_name] << association
220
+ entity.instance_variable_set(:@persistance_associations, persistance_associations)
221
+ end
222
+
223
+ def prepare_key_condition_from_entity(entity)
224
+ key_condition = {}
225
+ if primary_key.is_a?(Array)
226
+ primary_key.each do |key_part|
227
+ key_part_value = entity.send(key_part)
228
+ raise ArgumentError, "entity's primary key can't be nil, got nil for #{key_part}" unless key_part_value
229
+ key_condition[key_part] = key_part_value
230
+ end
231
+ elsif primary_key.is_a?(Symbol)
232
+ key_value = entity.send(primary_key)
233
+ raise ArgumentError, "entity's primary key can't be nil, got nil for #{primary_key}" unless key_value
234
+ key_condition[primary_key] = key_value
235
+ else
236
+ raise StandardError, "primary key should be array or symbol"
237
+ end
238
+ key_condition
239
+ end
240
+
241
+ def set_entity_primary_key(entity, raw, key)
242
+ if key && !primary_key.is_a?(Array)
243
+ entity.instance_variable_set("@#{primary_key}", key)
244
+ raw[primary_key] = key
245
+ end
246
+ end
247
+
248
+ def insert_associations(entity)
249
+ unless association_reflections.empty?
250
+ association_reflections.each do |association_name, options|
251
+ association_dao = options[:class]
252
+ if entity.respond_to?(association_name)
253
+ children = association_dao.insert_all(entity.send(association_name), entity)
254
+ set_associations_state(entity, association_name, children)
255
+ end
256
+ end
257
+ end
258
+ end
259
+
260
+ def insert_or_update_associations(entity)
261
+ unless association_reflections.empty?
262
+ association_reflections.each do |association_name, options|
263
+ association_dao = options[:class]
264
+ raise ArgumentError, "class option should be specified for #{association_name}" unless association_dao
265
+
266
+ delete_dissapeared_children(entity, association_name, options)
267
+
268
+ children = entity.send(association_name)
269
+ association_dao.save_all(children, entity)
270
+ set_associations_state(entity, association_name, children)
271
+ end
272
+ end
273
+ end
274
+
275
+ def delete_associations(entity)
276
+ unless association_reflections.empty?
277
+ association_reflections.each do |association, options|
278
+ if options[:delete]
279
+ association_dao = options[:class]
280
+ conditions = (options[:conditions] || {}).merge(options[:key] => entity.send(primary_key))
281
+ association_dao.where(conditions).delete
282
+ end
283
+ end
284
+ end
285
+ end
286
+
287
+ def delete_dissapeared_children(entity, association, options)
288
+ association_dao = options[:class]
289
+ unless options[:key]
290
+ raise ArgumentError, "key option should be specified for #{association}"
291
+ end
292
+ if options[:key].is_a?(Symbol)
293
+ conditions = (options[:conditions] || {}).merge(options[:key] => entity.send(primary_key))
294
+ elsif options[:key].is_a?(Array)
295
+ conditions = options[:key].inject(options[:conditions] || {}) { |result, key| result[key] = entity.send(key); result }
296
+ else
297
+ raise ArgumentError, "key should be symbol or array"
298
+ end
299
+
300
+ # get ids of removed children
301
+ association_objects = get_association_objects(entity, association)
302
+ dissapeared_objects = association_objects - entity.send(association)
303
+
304
+ scope_key = options[:scope_key] || association_dao.primary_key
305
+ if scope_key.is_a?(Symbol)
306
+ child_keys = { scope_key => [] }
307
+ dissapeared_objects.each do |child_object|
308
+ key = child_object.send(scope_key)
309
+ child_keys[scope_key] << key
310
+ end
311
+
312
+ if !child_keys[scope_key].empty?
313
+ association_dao.where(conditions).where(child_keys).delete
314
+ end
315
+ elsif scope_key.is_a?(Array)
316
+ child_keys = []
317
+ dissapeared_objects.each do |child_object|
318
+ child_keys << scope_key.inject({}) do |condition, key|
319
+ condition[key] = child_object.send(key)
320
+ condition
321
+ end
322
+ end
323
+ if !child_keys.empty?
324
+ child_keys.each { |keys| keys.merge!(conditions) }
325
+ association_dao.where(Sequel.|(*child_keys)).delete
326
+ end
327
+ elsif scope_key.is_a?(Proc)
328
+ child_keys = []
329
+ dissapeared_objects.each do |child_object|
330
+ child_keys << scope_key.call(child_object)
331
+ end
332
+ if !child_keys.empty?
333
+ child_keys.each { |keys| keys.merge!(conditions) }
334
+ association_dao.where(Sequel.|(*child_keys)).delete
335
+ end
336
+ else
337
+ raise StandardError, "scope key should be array or symbol"
338
+ end
339
+ end
340
+
341
+ def get_association_objects(entity, association)
342
+ persistance_associations = entity.instance_variable_get(:@persistance_associations)
343
+ if persistance_associations
344
+ persistance_associations[association] || []
345
+ else
346
+ []
347
+ end
348
+ end
349
+
350
+ end
351
+ end
352
+
353
+ end
@@ -0,0 +1,47 @@
1
+ module SequelDM
2
+ module Extensions
3
+ module SelectFields
4
+ def select_fields(fields)
5
+ if fields.empty?
6
+ if !model.association_reflections.empty?
7
+ eager(model.association_reflections.keys)
8
+ else
9
+ self
10
+ end
11
+ else
12
+ eager_associations = {}
13
+ fields.each do |association, columns|
14
+ next if association == :fields
15
+ if columns && !columns.is_a?(Array)
16
+ columns = get_columns_from_mapper(association)
17
+ end
18
+ if columns
19
+ table_name = model.association_reflections[association][:class].table_name
20
+ columns = columns.map { |column| :"#{table_name}__#{column}___#{column}" }
21
+ eager_associations[association] = proc{|ds| ds.select(*columns) }
22
+ end
23
+ end
24
+
25
+ if fields[:fields].is_a?(Array)
26
+ columns = fields[:fields]
27
+ else
28
+ columns = model.mapper.mappings.keys
29
+ end
30
+ columns = columns.map { |column| :"#{model.table_name}__#{column}___#{column}" }
31
+ eager(eager_associations).select(*columns)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def get_columns_from_mapper(association)
38
+ reflection = model.association_reflections[association]
39
+ raise ArgumentError, "association with name #{association} is not defined in dao" unless reflection
40
+ association_dao = reflection[:class]
41
+ raise ArgumentError, "association #{association} should have class option" unless association_dao
42
+ association_dao.mapper.mappings.keys
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,54 @@
1
+ require 'sequel_dm/args_validator'
2
+ require 'sequel_dm/mappings_dsl'
3
+
4
+ module SequelDM::Mapper
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :entity_class, :mappings
9
+ end
10
+
11
+ module ClassMethods
12
+ def map(entity_class, &mappings_proc)
13
+ SequelDM::ArgsValidator.is_class!(entity_class, :entity_class)
14
+ self.entity_class = entity_class
15
+ self.mappings = SequelDM::MappingsDSL.new(&mappings_proc).mappings
16
+ end
17
+
18
+ def to_entity(hash)
19
+ attributes = {}
20
+ entity = self.entity_class.new
21
+ hash.each do |key, value|
22
+ if mapping = self.mappings[key]
23
+ entity.instance_variable_set(:"@#{mapping.entity_field}", to_attribute(hash, value, mapping)) if mapping.set_field?
24
+ end
25
+ end
26
+ entity
27
+ end
28
+
29
+ def to_hash(entity, *args)
30
+ hash = {}
31
+
32
+ entity_mappings = self.mappings
33
+ entity_mappings.each do |column, mapping|
34
+ value = to_column(entity, mapping, *args)
35
+ hash[column] = value if mapping.set_column?
36
+ end
37
+
38
+ hash
39
+ end
40
+
41
+ private
42
+
43
+ def to_attribute(hash, value, mapping)
44
+ mapping.load? ? mapping.load(hash) : value
45
+ end
46
+
47
+ def to_column(entity, mapping, *args)
48
+ mapping.dump? ? mapping.dump(entity, *args) : entity.send(mapping.entity_field)
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+
@@ -0,0 +1,69 @@
1
+ class SequelDM::MappingsDSL
2
+ attr_reader :mappings
3
+ def initialize(&dsl_block)
4
+ @mappings = {}
5
+ instance_exec(&dsl_block)
6
+ end
7
+
8
+ def column(column_name, options = {})
9
+ SequelDM::ArgsValidator.is_symbol!(column_name, :column_name)
10
+ SequelDM::ArgsValidator.is_hash!(options, :column_options)
11
+ SequelDM::ArgsValidator.is_symbol!(options[:to], :to) if options[:to]
12
+ SequelDM::ArgsValidator.is_proc!(options[:load], :load) if options[:load]
13
+ SequelDM::ArgsValidator.is_proc!(options[:dump], :dump) if options[:dump]
14
+
15
+ set_field = options[:set_field] == false ? false : true
16
+ set_column = options[:set_column] == false ? false : true
17
+ mappings[column_name] = Mapping.new(
18
+ column_name,
19
+ options[:to] || column_name,
20
+ options[:load],
21
+ options[:dump],
22
+ set_field,
23
+ set_column,
24
+ )
25
+ end
26
+
27
+ def columns(*column_names)
28
+ SequelDM::ArgsValidator.is_array!(column_names, :column_names)
29
+ column_names.each { |column_name| column(column_name) }
30
+ end
31
+
32
+ class Mapping
33
+ attr_accessor :column_name, :entity_field, :load, :dump
34
+
35
+ def initialize(column_name, entity_field, load = nil, dump = nil, set_field = true, set_column = true)
36
+ @column_name = column_name
37
+ @entity_field = entity_field
38
+ @load = load
39
+ @dump = dump
40
+ @set_field = set_field
41
+ @set_column = set_column
42
+ end
43
+
44
+ def set_field?
45
+ @set_field
46
+ end
47
+
48
+ def set_column?
49
+ @set_column
50
+ end
51
+
52
+ def load?
53
+ !!@load
54
+ end
55
+
56
+ def dump?
57
+ !!@dump
58
+ end
59
+
60
+ def load(value)
61
+ @load.call(value)
62
+ end
63
+
64
+ def dump(value, *args)
65
+ @dump.call(value, *args)
66
+ end
67
+ end
68
+ end
69
+
@@ -0,0 +1,3 @@
1
+ module SequelDM
2
+ VERSION = "0.0.2"
3
+ end
data/lib/sequel_dm.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/class/attribute'
3
+ require 'sequel'
4
+ require 'sequel_dm/version'
5
+ require 'sequel_dm/mapper'
6
+ require 'sequel_dm/dao'
data/sequel_dm.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sequel_dm/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sequel_dm"
8
+ spec.version = SequelDM::VERSION
9
+ spec.authors = ["Albert Gazizov", "Ruslan Gatiyatov"]
10
+ spec.email = ["deeper4k@gmail.com", "ruslan.gatiyatov@gmail.com"]
11
+ spec.description = %q{Sequel based Data Mapper implementation}
12
+ spec.summary = %q{Sequel based Data Mapper implementation}
13
+ spec.homepage = "http://github.com/deeper4k/sequel_dm"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(spec)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_dependency "sequel", "~> 4.6"
25
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+ require 'sequel_dm'
3
+
4
+ describe "SequelDM::DAO" do
5
+ class Entity
6
+ end
7
+
8
+ class Mapper
9
+ def self.to_entity(hash)
10
+ Entity.new
11
+ end
12
+ end
13
+
14
+ describe ".dataset.row_proc" do
15
+ it "should return entity" do
16
+ dao = Class.new(SequelDM::DAO(:items))
17
+ dao.mapper = Mapper
18
+ dao.dataset.row_proc.call({}).should be_instance_of(Entity)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ describe SequelDM::Mapper do
4
+ module MapperTest
5
+
6
+ class Event
7
+ attr_accessor :name, :description, :when, :settings
8
+
9
+ def initialize(attrs = {})
10
+ @name = attrs[:name]
11
+ @description = attrs[:description]
12
+ @when = attrs[:when]
13
+ @settings = attrs[:settings ]
14
+ end
15
+ end
16
+
17
+ class EventMapper
18
+ include SequelDM::Mapper
19
+
20
+ map MapperTest::Event do
21
+ column :subject, to: :name
22
+ column :description
23
+ column :settings, load: ->(hash) { YAML.load(hash[:settings]) }, dump: ->(event) { YAML.dump(event.settings) }
24
+ end
25
+ end
26
+ end
27
+
28
+ describe ".to_entity" do
29
+ it "should build Event instance from hash using mappings" do
30
+ tomorrow = Time.now + 24*60*60
31
+ event = MapperTest::EventMapper.to_entity({
32
+ subject: "Meet parents",
33
+ description: "I need to meet them",
34
+ when: tomorrow,
35
+ settings: "---\n:important: true\n",
36
+ occurrences_number: 2,
37
+ })
38
+ event.should be_instance_of(MapperTest::Event)
39
+
40
+ event.name.should == "Meet parents"
41
+ event.description.should == "I need to meet them"
42
+ event.when.should == nil # unlisted column
43
+ event.settings.should == { important: true }
44
+ end
45
+ end
46
+
47
+ describe ".to_hash" do
48
+ it "should convert entity to hash" do
49
+ event = MapperTest::Event.new({
50
+ name: "Event",
51
+ description: "Description",
52
+ when: Time.now,
53
+ settings: { important: true },
54
+ })
55
+ MapperTest::EventMapper.to_hash(event).should == {
56
+ description: "Description",
57
+ settings: "---\n:important: true\n",
58
+ subject: "Event"
59
+ }
60
+ end
61
+ end
62
+ end
63
+
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'debugger'
4
+ require 'sequel'
5
+ require 'sequel_dm'
6
+
7
+ RSpec.configure do |config|
8
+ config.color_enabled = true
9
+
10
+ db = Sequel.mock(:fetch=>{:id => 1, :x => 1}, :numrows=>1, :autoid=>proc{|sql| 10})
11
+ def db.schema(*) [[:id, {:primary_key=>true}]] end
12
+ def db.reset() sqls end
13
+ def db.supports_schema_parsing?() true end
14
+ Sequel::Model.db = DB = db
15
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sequel_dm
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.2
6
+ platform: ruby
7
+ authors:
8
+ - Albert Gazizov
9
+ - Ruslan Gatiyatov
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2014-04-08 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ prerelease: false
17
+ name: activesupport
18
+ type: :runtime
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ! '>='
22
+ - !ruby/object:Gem::Version
23
+ version: '0'
24
+ none: false
25
+ requirement: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ none: false
31
+ - !ruby/object:Gem::Dependency
32
+ prerelease: false
33
+ name: bundler
34
+ type: :development
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ~>
38
+ - !ruby/object:Gem::Version
39
+ version: '1.3'
40
+ none: false
41
+ requirement: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.3'
46
+ none: false
47
+ - !ruby/object:Gem::Dependency
48
+ prerelease: false
49
+ name: rake
50
+ type: :development
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ none: false
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ none: false
63
+ - !ruby/object:Gem::Dependency
64
+ prerelease: false
65
+ name: sequel
66
+ type: :runtime
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ~>
70
+ - !ruby/object:Gem::Version
71
+ version: '4.6'
72
+ none: false
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '4.6'
78
+ none: false
79
+ description: Sequel based Data Mapper implementation
80
+ email:
81
+ - deeper4k@gmail.com
82
+ - ruslan.gatiyatov@gmail.com
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - .gitignore
88
+ - .travis.yml
89
+ - Gemfile
90
+ - Gemfile.lock
91
+ - LICENSE.txt
92
+ - README.md
93
+ - Rakefile
94
+ - lib/sequel_dm.rb
95
+ - lib/sequel_dm/args_validator.rb
96
+ - lib/sequel_dm/dao.rb
97
+ - lib/sequel_dm/extensions/select_fields.rb
98
+ - lib/sequel_dm/mapper.rb
99
+ - lib/sequel_dm/mappings_dsl.rb
100
+ - lib/sequel_dm/version.rb
101
+ - sequel_dm.gemspec
102
+ - spec/sequel_dm/dao_spec.rb
103
+ - spec/sequel_dm/mapper_spec.rb
104
+ - spec/spec_helper.rb
105
+ homepage: http://github.com/deeper4k/sequel_dm
106
+ licenses:
107
+ - MIT
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ! '>='
115
+ - !ruby/object:Gem::Version
116
+ segments:
117
+ - 0
118
+ hash: -3775447022657045354
119
+ version: '0'
120
+ none: false
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ segments:
126
+ - 0
127
+ hash: -3775447022657045354
128
+ version: '0'
129
+ none: false
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 1.8.23
133
+ signing_key:
134
+ specification_version: 3
135
+ summary: Sequel based Data Mapper implementation
136
+ test_files:
137
+ - spec/sequel_dm/dao_spec.rb
138
+ - spec/sequel_dm/mapper_spec.rb
139
+ - spec/spec_helper.rb