whorm 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,370 @@
1
+ module Whorm
2
+ module Model
3
+
4
+ def self.included(model)
5
+ model.send(:extend, ClassMethods)
6
+ model.send(:include, InstanceMethods)
7
+ ##
8
+ # @config {String} whorm_parent_trail_template This a template used to render mapped field-names.
9
+ # Default is Proc.new{ |field_name| "_#{field_name}" }
10
+ # You could also use the Rails standard
11
+ # Proc.new{ |field_name| "[#{field_name}]" }
12
+ #
13
+ model.cattr_accessor :whorm_parent_trail_template
14
+ model.whorm_parent_trail_template = Proc.new{ |field_name| "_#{field_name}" } if model.whorm_parent_trail_template.nil?
15
+ end
16
+
17
+ ##
18
+ # InstanceMethods
19
+ #
20
+ module InstanceMethods
21
+
22
+ ##
23
+ # Converts a model instance to a record compatible with javascript applications
24
+ #
25
+ # The first parameter should be the fieldset for which the record will be returned.
26
+ # If no parameter is provided, then the default fieldset will be choosen
27
+ # Alternativly the first parameter can be a Hash with a :fields member to directly specify
28
+ # the fields to use for the record.
29
+ #
30
+ # All these are valid calls:
31
+ #
32
+ # user.to_record # returns record for :default fieldset
33
+ # # (fieldset is autmatically defined, if not set)
34
+ #
35
+ # user.to_record :fieldset # returns record for :fieldset fieldset
36
+ # # (fieldset is autmatically defined, if not set)
37
+ #
38
+ # user.to_record :fields => [:id, :password]
39
+ # # returns record for the fields 'id' and 'password'
40
+ #
41
+ # For even more valid options for this method (which all should not be neccessary to use)
42
+ # have a look at Whorm::Model::Util.extract_fieldset_and_options
43
+ def to_record(*params)
44
+ fieldset, options = Util.extract_fieldset_and_options params
45
+
46
+ fields = []
47
+ if options[:fields].empty?
48
+ fields = self.class.whorm_get_fields_for_fieldset(fieldset)
49
+ else
50
+ fields = self.class.process_fields(*options[:fields])
51
+ end
52
+
53
+ assns = self.class.whorm_associations
54
+ pk = self.class.whorm_primary_key
55
+
56
+ # build the initial field data-hash
57
+ data = {pk => self.send(pk)}
58
+
59
+ fields.each do |field|
60
+ next if data.has_key? field[:name] # already processed (e.g. explicit mentioning of :id)
61
+
62
+ value = nil
63
+ if association_reflection = assns[field[:name]] # if field is an association
64
+ association = self.send(field[:name])
65
+
66
+ # skip this association if we already visited it
67
+ # otherwise we could end up in a cyclic reference
68
+ next if options[:visited_classes].include? association.class
69
+
70
+ case association_reflection[:type]
71
+ when :belongs_to, :has_one
72
+ if association.respond_to? :to_record
73
+ assn_fields = field[:fields]
74
+ if assn_fields.nil?
75
+ assn_fields = association.class.whorm_get_fields_for_fieldset(field.fetch(:fieldset, fieldset))
76
+ end
77
+
78
+ value = association.to_record :fields => assn_fields,
79
+ :visited_classes => options[:visited_classes] + [self.class]
80
+ else
81
+ value = {}
82
+ (field[:fields]||[]).each do |sub_field|
83
+ value[sub_field[:name]] = association.send(sub_field[:name]) if association.respond_to? sub_field[:name]
84
+ end
85
+ end
86
+ if association_reflection[:type] == :belongs_to
87
+ # Append associations foreign_key to data
88
+ data[association_reflection[:foreign_key]] = self.send(association_reflection[:foreign_key])
89
+ if association_reflection[:is_polymorphic]
90
+ foreign_type = self.class.whorm_polymorphic_type(association_reflection[:foreign_key])
91
+ data[foreign_type] = self.send(foreign_type)
92
+ end
93
+ end
94
+ when :many
95
+ value = association.collect { |r| r.to_record } # use carefully, can get HUGE
96
+ end
97
+ else # not an association -> get the method's value
98
+ value = self.send(field[:name])
99
+ value = value.to_record if value.respond_to? :to_record
100
+ end
101
+ data[field[:name]] = value
102
+ end
103
+ data
104
+ end
105
+ end
106
+
107
+ ##
108
+ # ClassMethods
109
+ #
110
+ module ClassMethods
111
+ ##
112
+ # render AR columns to Ext.data.Record.create format
113
+ # eg: {name:'foo', type: 'string'}
114
+ #
115
+ # The first parameter should be the fieldset for which the record definition will be returned.
116
+ # If no parameter is provided, then the default fieldset will be choosen
117
+ # Alternativly the first parameter can be a Hash with a :fields member to directly specify
118
+ # the fields to use for the record config.
119
+ #
120
+ # All these are valid calls:
121
+ #
122
+ # User.whorm_schema # returns record config for :default fieldset
123
+ # # (fieldset is autmatically defined, if not set)
124
+ #
125
+ # User.whorm_schema :fieldset # returns record config for :fieldset fieldset
126
+ # # (fieldset is autmatically defined, if not set)
127
+ #
128
+ # User.whorm_schema :fields => [:id, :password]
129
+ # # returns record config for the fields 'id' and 'password'
130
+ #
131
+ # For even more valid options for this method (which all should not be neccessary to use)
132
+ # have a look at Whorm::Model::Util.extract_fieldset_and_options
133
+ def whorm_schema(*params)
134
+ fieldset, options = Util.extract_fieldset_and_options params
135
+
136
+ if options[:fields].empty?
137
+ fields = self.whorm_get_fields_for_fieldset(fieldset)
138
+ else
139
+ fields = self.process_fields(*options[:fields])
140
+ end
141
+
142
+ associations = self.whorm_associations
143
+ columns = self.whorm_columns_hash
144
+ pk = self.whorm_primary_key
145
+ rs = []
146
+
147
+ fields.each do |field|
148
+
149
+ field = Marshal.load(Marshal.dump(field)) # making a deep copy
150
+
151
+ if col = columns[field[:name]] # <-- column on this model
152
+ rs << self.whorm_field(field, col)
153
+ elsif assn = associations[field[:name]]
154
+ # skip this association if we already visited it
155
+ # otherwise we could end up in a cyclic reference
156
+ next if options[:visited_classes].include? assn[:class]
157
+
158
+ assn_fields = field[:fields]
159
+ if assn[:class].respond_to?(:whorm_schema) # <-- exec whorm_schema on assn Model.
160
+ if assn_fields.nil?
161
+ assn_fields = assn[:class].whorm_get_fields_for_fieldset(field.fetch(:fieldset, fieldset))
162
+ end
163
+
164
+ record = assn[:class].whorm_schema(field.fetch(:fieldset, fieldset), { :visited_classes => options[:visited_classes] + [self], :fields => assn_fields})
165
+ rs.concat(record[:fields].collect { |assn_field|
166
+ self.whorm_field(assn_field, :parent_trail => field[:name], :mapping => field[:name], :allowBlank => true) # <-- allowBlank on associated data?
167
+ })
168
+ elsif assn_fields # <-- :parent => [:id, :name, :sub => [:id, :name]]
169
+ field_collector = Proc.new do |parent_trail, mapping, assn_field|
170
+ if assn_field.is_a?(Hash) && assn_field.keys.size == 1 && assn_field.keys[0].is_a?(Symbol) && assn_field.values[0].is_a?(Array)
171
+ field_collector.call(parent_trail.to_s + self.whorm_parent_trail_template.call(assn_field.keys.first), "#{mapping}.#{assn_field.keys.first}", assn_field.values.first)
172
+ else
173
+ self.whorm_field(assn_field, :parent_trail => parent_trail, :mapping => mapping, :allowBlank => true)
174
+ end
175
+ end
176
+ rs.concat(assn_fields.collect { |assn_field| field_collector.call(field[:name], field[:name], assn_field) })
177
+ else
178
+ rs << whorm_field(field)
179
+ end
180
+
181
+ # attach association's foreign_key if not already included.
182
+ if columns.has_key?(assn[:foreign_key]) && !rs.any? { |r| r[:name] == assn[:foreign_key] }
183
+ rs << whorm_field({:name => assn[:foreign_key]}, columns[assn[:foreign_key]])
184
+ end
185
+ # attach association's type if polymorphic association and not alredy included
186
+ if assn[:is_polymorphic]
187
+ foreign_type = self.whorm_polymorphic_type(assn[:foreign_key])
188
+ if columns.has_key?(foreign_type) && !rs.any? { |r| r[:name] == foreign_type }
189
+ rs << whorm_field({:name => foreign_type}, columns[foreign_type])
190
+ end
191
+ end
192
+ else # property is a method?
193
+ rs << whorm_field(field)
194
+ end
195
+ end
196
+
197
+ return {
198
+ :fields => rs,
199
+ :idProperty => pk
200
+ }
201
+ end
202
+
203
+ ##
204
+ # meant to be used within a Model to define the whorm record fields.
205
+ # eg:
206
+ # class User
207
+ # whorm_fieldset :grid, [:first, :last, :email => {"sortDir" => "ASC"}, :company => [:id, :name]]
208
+ # end
209
+ # or
210
+ # class User
211
+ # whorm_fieldset :last, :email => {"sortDir" => "ASC"}, :company => [:id, :name] # => implies fieldset name :default
212
+ # end
213
+ #
214
+ def whorm_fieldset(*params)
215
+ fieldset, options = Util.extract_fieldset_and_options params
216
+ var_name = :"@whorm_fieldsets__#{fieldset}"
217
+ self.instance_variable_set( var_name, self.process_fields(*options[:fields]) )
218
+ end
219
+
220
+ def whorm_get_fields_for_fieldset(fieldset)
221
+ var_name = :"@whorm_fieldsets__#{fieldset}"
222
+ super_value = nil
223
+ unless self.instance_variable_get( var_name )
224
+ if self.superclass.respond_to? :whorm_get_fields_for_fieldset
225
+ super_value = self.superclass.whorm_get_fields_for_fieldset(fieldset)
226
+ end
227
+ self.whorm_fieldset(fieldset, self.whorm_column_names) unless super_value
228
+ end
229
+ super_value || self.instance_variable_get( var_name )
230
+ end
231
+
232
+ ##
233
+ # shortcut to define the default fieldset. For backwards-compatibility.
234
+ #
235
+ def whorm_fields(*params)
236
+ self.whorm_fieldset(:default, {
237
+ :fields => params
238
+ })
239
+ end
240
+
241
+ ##
242
+ # Prepare a field configuration list into a normalized array of Hashes, {:name => "field_name"}
243
+ # @param {Mixed} params
244
+ # @return {Array} of Hashes
245
+ #
246
+ def process_fields(*params)
247
+ fields = []
248
+ if params.size == 1 && params.last.is_a?(Hash) # peek into argument to see if its an option hash
249
+ options = params.last
250
+ if options.has_key?(:additional) && options[:additional].is_a?(Array)
251
+ return self.process_fields(*(self.whorm_column_names + options[:additional].map(&:to_sym)))
252
+ elsif options.has_key?(:exclude) && options[:exclude].is_a?(Array)
253
+ return self.process_fields(*(self.whorm_column_names - options[:exclude].map(&:to_sym)))
254
+ elsif options.has_key?(:only) && options[:only].is_a?(Array)
255
+ return self.process_fields(*options[:only])
256
+ end
257
+ end
258
+
259
+ params = self.whorm_column_names if params.empty?
260
+
261
+ associations = whorm_associations
262
+
263
+ params.each do |f|
264
+ if f.kind_of?(Hash)
265
+ if f.keys.size == 1 && f.keys[0].is_a?(Symbol) && f.values[0].is_a?(Array) # {:association => [:field1, :field2]}
266
+ fields << {
267
+ :name => f.keys[0],
268
+ :fields => process_fields(*f.values[0])
269
+ }
270
+ elsif f.keys.size == 1 && f.keys[0].is_a?(Symbol) && f.values[0].is_a?(Hash) # {:field => {:sortDir => 'ASC'}}
271
+ fields << f.values[0].update(:name => f.keys[0])
272
+ elsif f.has_key?(:name) # already a valid Hash, just copy it over
273
+ fields << f
274
+ else
275
+ raise ArgumentError, "encountered a Hash that I don't know anything to do with `#{f.inspect}:#{f.class}`"
276
+ end
277
+ else # should be a String or Symbol
278
+ raise ArgumentError, "encountered a fields Array that I don't understand: #{params.inspect} -- `#{f.inspect}:#{f.class}` is not a Symbol or String" unless f.is_a?(Symbol) || f.is_a?(String)
279
+ fields << {:name => f.to_sym}
280
+ end
281
+ end
282
+
283
+ fields
284
+ end
285
+
286
+ ##
287
+ # Render a column-config object
288
+ # @param {Hash/Column} field Field-configuration Hash, probably has :name already set and possibly Ext.data.Field options.
289
+ # @param {ORM Column Object from AR, DM or MM}
290
+ #
291
+ def whorm_field(field, config=nil)
292
+ if config.kind_of? Hash
293
+ if config.has_key?(:mapping) && config.has_key?(:parent_trail)
294
+ field.update( # <-- We use a template for rendering mapped field-names.
295
+ :name => config[:parent_trail].to_s + self.whorm_parent_trail_template.call(field[:name]),
296
+ :mapping => "#{config[:mapping]}.#{field[:name]}"
297
+ )
298
+ end
299
+ field.update(config.except(:mapping, :parent_trail))
300
+ elsif !config.nil? # <-- Hopfully an ORM Column object.
301
+ field.update(
302
+ :allowBlank => self.whorm_allow_blank(config),
303
+ :type => self.whorm_type(config),
304
+ :defaultValue => self.whorm_default(config)
305
+ )
306
+ field[:dateFormat] = "c" if field[:type] === "date" && field[:dateFormat].nil? # <-- ugly hack for date
307
+ end
308
+ field.update(:type => "auto") if field[:type].nil?
309
+ # convert Symbol values to String values
310
+ field.keys.each do |k|
311
+ raise ArgumentError, "whorm_field expects a Hash as first parameter with all it's keys Symbols. Found key #{k.inspect}:#{k.class.to_s}" unless k.is_a?(Symbol)
312
+ field[k] = field[k].to_s if field[k].is_a?(Symbol)
313
+ end
314
+ field
315
+ end
316
+
317
+ # ##
318
+ # # Returns an array of symbolized association names that will be referenced by a call to to_record
319
+ # # i.e. [:parent1, :parent2]
320
+ # #
321
+ # def whorm_used_associations
322
+ # if @whorm_used_associations.nil?
323
+ # assoc = []
324
+ # self.whorm_record_fields.each do |f|
325
+ # #This needs to be the first condition because the others will break if f is an Array
326
+ # if whorm_associations[f[:name]]
327
+ # assoc << f[:name]
328
+ # end
329
+ # end
330
+ # @whorm_used_associations = assoc.uniq
331
+ # end
332
+ # @whorm_used_associations
333
+ # end
334
+ end
335
+
336
+ module Util
337
+
338
+ ##
339
+ # returns the fieldset from the arguments and normalizes the options.
340
+ # @return [{Symbol}, {Hash}]
341
+ def self.extract_fieldset_and_options arguments
342
+ orig_args = arguments
343
+ fieldset = :default
344
+ options = { # default options
345
+ :visited_classes => [],
346
+ :fields => []
347
+ }
348
+ if arguments.size > 2 || (arguments.size == 2 && !arguments[0].is_a?(Symbol))
349
+ raise ArgumentError, "Don't know how to handle #{arguments.inspect}"
350
+ elsif arguments.size == 2 && arguments[0].is_a?(Symbol)
351
+ fieldset = arguments.shift
352
+ if arguments[0].is_a?(Array)
353
+ options.update({
354
+ :fields => arguments[0]
355
+ })
356
+ elsif arguments[0].is_a?(Hash)
357
+ options.update(arguments[0])
358
+ end
359
+ elsif arguments.size == 1 && (arguments[0].is_a?(Symbol) || arguments[0].is_a?(String))
360
+ fieldset = arguments.shift.to_sym
361
+ elsif arguments.size == 1 && arguments[0].is_a?(Hash)
362
+ fieldset = arguments[0].delete(:fieldset) || :default
363
+ options.update(arguments[0])
364
+ end
365
+ [fieldset, options]
366
+ end
367
+ end
368
+ end
369
+ end
370
+
File without changes
@@ -0,0 +1,70 @@
1
+ # TODO: Figure out how to iterate each ORM framework AR, DM, MM and test each.
2
+ require 'active_record'
3
+ require 'active_support'
4
+ require 'whorm'
5
+ require 'extlib/inflection'
6
+
7
+ gem 'sqlite3-ruby'
8
+
9
+ class Test::App
10
+
11
+ attr_reader :models
12
+
13
+ def initialize(orm = :active_record)
14
+ @orm = orm
15
+ @config = YAML::load(IO.read("#{ROOT}/config/database.yml"))
16
+
17
+ # Load ORM
18
+ send("boot_#{orm.to_s}")
19
+
20
+ load_models
21
+
22
+ require 'db/schema'
23
+
24
+ end
25
+
26
+ ##
27
+ # Reset a model's @whorm_fieldsets
28
+ #
29
+ def clean_all
30
+ @models.map { |klass| clean klass }
31
+ end
32
+
33
+
34
+ private
35
+
36
+ def boot_active_record
37
+ ActiveRecord::Base.establish_connection(@config['test'])
38
+ end
39
+
40
+ def boot_mongo_mapper
41
+
42
+ end
43
+
44
+ def boot_data_mapper
45
+
46
+ end
47
+
48
+ ##
49
+ # Do a dir on /models and constantize each filename
50
+ #
51
+ def load_models
52
+ @models = []
53
+ # Load Models and Schema for corresponding orm
54
+ re = /^.*\/(.*).rb$/
55
+ Dir["#{ROOT}/models/#{@orm.to_s}/*"].each { |c|
56
+ require c
57
+ match = c.match(re)
58
+ @models << Extlib::Inflection.constantize(Extlib::Inflection.camelize(match[1])) if match
59
+ }
60
+ end
61
+
62
+ def clean klass
63
+ klass.instance_variables.each do |var_name|
64
+ if /\A@whorm_fieldsets__/ =~ var_name.to_s
65
+ klass.instance_variable_set( var_name.to_sym, nil )
66
+ end
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: ":memory:"
@@ -0,0 +1,75 @@
1
+
2
+ ##
3
+ # build simple database
4
+ #
5
+ # people
6
+ #
7
+ ActiveRecord::Base.connection.create_table :users, :force => true do |table|
8
+ table.column :id, :serial
9
+ table.column :person_id, :integer
10
+ table.column :password, :string
11
+ table.column :created_at, :date
12
+ table.column :disabled, :boolean, :default => true
13
+ end
14
+ ##
15
+ # people
16
+ #
17
+ ActiveRecord::Base.connection.create_table :people, :force => true do |table|
18
+ table.column :id, :serial
19
+ table.column :first, :string, :null => false
20
+ table.column :last, :string, :null => false
21
+ table.column :email, :string, :null => false
22
+ end
23
+ ##
24
+ # user_groups, join table
25
+ #
26
+ ActiveRecord::Base.connection.create_table :user_groups, :force => true do |table|
27
+ table.column :user_id, :integer
28
+ table.column :group_id, :integer
29
+ end
30
+
31
+ ##
32
+ # groups
33
+ #
34
+ ActiveRecord::Base.connection.create_table :groups, :force => true do |table|
35
+ table.column :id, :serial
36
+ table.column :title, :string
37
+ end
38
+
39
+ ##
40
+ # locations
41
+ #
42
+ ActiveRecord::Base.connection.create_table :locations, :force => true do |table|
43
+ table.column :id, :serial
44
+ table.column :name, :string
45
+ table.column :street, :string
46
+ table.column :type, :string
47
+ end
48
+
49
+ ##
50
+ # addresses
51
+ #
52
+ ActiveRecord::Base.connection.create_table :addresses, :force => true do |table|
53
+ table.column :id, :serial
54
+ table.column :addressable_type, :string
55
+ table.column :addressable_id, :integer
56
+ table.column :street, :string
57
+ end
58
+
59
+ ##
60
+ # Mock a Model for testing data-types
61
+ #
62
+ ActiveRecord::Base.connection.create_table :data_types, :force => true do |table|
63
+ table.column :id, :serial
64
+ table.column :string_column, :string
65
+ table.column :decimal_column, :decimal
66
+ table.column :float_column, :float
67
+ table.column :date_column, :date
68
+ table.column :datetime_column, :datetime
69
+ table.column :time_column, :time
70
+ table.column :email, :string
71
+ table.column :integer_column, :integer
72
+ table.column :notnull_column, :string, :null => false
73
+ table.column :default_column, :boolean, :default => true
74
+ table.column :boolean_column, :boolean
75
+ end