whorm 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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