motion_model 0.4.2 → 0.4.4

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.
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: objective-c
2
+ before_install: ruby --version
3
+ rvm:
4
+ - "1.9.3"
5
+ script: bundle exec rake spec
data/README.md CHANGED
@@ -1,15 +1,16 @@
1
- [![Code Climate](https://codeclimate.com/github/sxross/MotionModel.png)](https://codeclimate.com/github/sxross/MotionModel)
1
+ [![Code Climate](https://codeclimate.com/github/sxross/MotionModel.png)](https://codeclimate.com/github/sxross/MotionModel)[![Build Status](https://travis-ci.org/sxross/MotionModel.png)](https://travis-ci.org/sxross]/MotionModel)
2
2
 
3
- MotionModel -- Simple Model, Validation, and Input Mixins for RubyMotion
3
+ MotionModel: Models, Relations, and Validation for RubyMotion
4
4
  ================
5
5
 
6
6
  MotionModel is a DSL for cases where Core Data is too heavy to lift but you are
7
- still intending to work with your data, its types, and its relations.
7
+ still intending to work with your data, its types, and its relations. It also provides for
8
+ data validation and actually quite a bit more.
8
9
 
9
10
  File | Module | Description
10
11
  ---------------------|---------------------------|------------------------------------
11
12
  **ext.rb** | N/A | Core Extensions that provide a few Rails-like niceties. Nothing new here, moving on...
12
- **model.rb** | MotionModel::Model | You can read about it in "What Model Can Do" but it's a mixin that provides you accessible attributes, row indexing, serialization for persistence, and some other niceties like row counting.
13
+ **model.rb** | MotionModel::Model | You should read about it in "[What Model Can Do](#what-model-can-do)". Model is the raison d'etre and the centerpiece of MotionModel.
13
14
  **validatable.rb** | MotionModel::Validatable | Provides a basic validation framework for any arbitrary class. You can also create custom validations to suit your app's unique needs.
14
15
  **input_helpers** | MotionModel::InputHelpers | Helps hook a collection up to a data form, populate the form, and retrieve the data afterwards. Note: *MotionModel supports Formotion for input handling as well as these input helpers*.
15
16
  **formotion.rb** | MotionModel::Formotion | Provides an interface between MotionModel and Formotion
@@ -31,7 +32,7 @@ you like with it. See the LICENSE file in this project.
31
32
  * [Problems/Comments](#problemscomments)
32
33
  * [Submissions/Patches](#submissionspatches)
33
34
 
34
- Most Recent Important Change
35
+ Changes for Existing Users to Be Aware Of
35
36
  =================
36
37
 
37
38
  Please see the CHANGELOG for update on changes.
@@ -61,6 +62,8 @@ or if you are using bundler:
61
62
  gem motion_model, "0.3.8"
62
63
  ```
63
64
 
65
+ Version 0.3.8 was the last that did not separate the model and persistence concerns.
66
+
64
67
  Getting Going
65
68
  ================
66
69
 
@@ -88,8 +91,15 @@ then put this in your Rakefile after requiring `motion/project`:
88
91
  require 'motion_model'
89
92
  ```
90
93
 
94
+ If you want to use Bundler from `master`, put this in your Gemfile:
95
+
96
+ ```
97
+ gem 'motion_model', :git => 'git@github.com:sxross/MotionModel.git'
98
+ ```
99
+
100
+ Note that in the above construct, Ruby 1.8.x hash keys are used. That's because Apple's System Ruby is 1.8.7 and won't recognize keen new 1.9.x hash syntax.
91
101
 
92
- What Model Can Do
102
+ What MotionModel Can Do
93
103
  ================
94
104
 
95
105
  You can define your models and their schemas in Ruby. For example:
@@ -100,19 +110,21 @@ class Task
100
110
  include MotionModel::ArrayModelAdapter
101
111
 
102
112
  columns :name => :string,
103
- :description => :string,
113
+ :long_name => :string,
104
114
  :due_date => :date
105
115
  end
106
116
 
107
117
  class MyCoolController
108
118
  def some_method
109
119
  @task = Task.create :name => 'walk the dog',
110
- :description => 'get plenty of exercise. pick up the poop',
111
- :due_date => '2012-09-15'
120
+ :long_name => 'get plenty of exercise. pick up the poop',
121
+ :due_date => '2012-09-15'
112
122
  end
113
123
  end
114
124
  ```
115
125
 
126
+ Side note: The original documentation on this used `description` for the column that is now `long_name`. It turns out Apple reserves `description` so MotionModel saves you the trouble of finding that particular bug by not allowing you to use it for a column name.
127
+
116
128
  Models support default values, so if you specify your model like this, you get defaults:
117
129
 
118
130
  ```ruby
@@ -134,16 +146,16 @@ class Task
134
146
  include MotionModel::Validatable
135
147
 
136
148
  columns :name => :string,
137
- :description => :string,
149
+ :long_name => :string,
138
150
  :due_date => :date
139
151
  validates :name => :presence => true
140
152
  end
141
153
 
142
154
  class MyCoolController
143
155
  def some_method
144
- @task = Task.new :name => 'walk the dog',
145
- :description => 'get plenty of exercise. pick up the poop',
146
- :due_date => '2012-09-15'
156
+ @task = Task.new :name => 'walk the dog',
157
+ :long_name => 'get plenty of exercise. pick up the poop',
158
+ :due_date => '2012-09-15'
147
159
 
148
160
  show_scary_warning unless @task.valid?
149
161
  end
@@ -125,17 +125,12 @@ module MotionModel
125
125
  @dirty = @new_record = false
126
126
  end
127
127
 
128
-
129
-
130
128
  # Count of objects in the current collection
131
129
  def length
132
130
  collection.length
133
131
  end
134
132
  alias_method :count, :length
135
133
 
136
- def rebuild_relation_for(name, instance_or_collection) # nodoc
137
- end
138
-
139
134
  private
140
135
 
141
136
  def _next_id
@@ -147,18 +142,21 @@ module MotionModel
147
142
  increment_next_id(options[:id])
148
143
  end
149
144
 
150
- def relation_for(col) # nodoc
151
- col = column_named(col)
152
- related_klass = col.classify
145
+ def belongs_to_relation(col) # nodoc
146
+ col.classify.find(_get_attr(col.foreign_key))
147
+ end
153
148
 
154
- case col.type
155
- when :belongs_to
156
- related_klass.find(@data[:id])
157
- when :has_many
158
- related_klass.find(generate_belongs_to_id(self.class)).belongs_to(self, related_klass).eq(@data[:id])
159
- else
160
- nil
161
- end
149
+ def has_many_relation(col) # nodoc
150
+ _has_many_has_one_relation(col)
151
+ end
152
+
153
+ def has_one_relation(col) # nodoc
154
+ _has_many_has_one_relation(col)
155
+ end
156
+
157
+ def _has_many_has_one_relation(col) # nodoc
158
+ related_klass = col.classify
159
+ related_klass.find(col.inverse_column.foreign_key).belongs_to(self, related_klass).eq(_get_attr(:id))
162
160
  end
163
161
 
164
162
  def do_insert(options = {})
@@ -128,7 +128,7 @@ module MotionModel
128
128
  def encodeWithCoder(coder)
129
129
  columns.each do |attr|
130
130
  # Serialize attributes except the proxy has_many and belongs_to ones.
131
- unless [:belongs_to, :has_many].include? column_named(attr).type
131
+ unless [:belongs_to, :has_many].include? column(attr).type
132
132
  value = self.send(attr)
133
133
  unless value.nil?
134
134
  coder.encodeObject(value, forKey: attr.to_s)
@@ -1,31 +1,51 @@
1
1
  module MotionModel
2
2
  module Model
3
3
  class Column
4
- attr_accessor :name
5
- attr_accessor :type
6
- attr_accessor :default
7
- attr_accessor :dependent
4
+ attr_reader :name
5
+ attr_reader :owner
6
+ attr_reader :type
7
+ attr_reader :options
8
8
 
9
- def initialize(name = nil, type = nil, options = {})
9
+ OPTION_ATTRS = [:as, :conditions, :default, :dependent, :foreign_key, :inverse_of, :joined_class_name,
10
+ :polymorphic, :symbolize, :through]
11
+
12
+ OPTION_ATTRS.each do |key|
13
+ define_method(key) { @options[key] }
14
+ end
15
+
16
+ def initialize(owner, name = nil, type = nil, options = {})
17
+ raise RuntimeError.new "columns need a type declared." if type.nil?
18
+ @owner = owner
10
19
  @name = name
11
20
  @type = type
12
- raise RuntimeError.new "columns need a type declared." if type.nil?
13
- @default = options.delete :default
14
- @dependent = options.delete :dependent
21
+ @klass = options.delete(:class)
15
22
  @options = options
16
23
  end
17
24
 
18
- def options
19
- @options
25
+ def class_name
26
+ joined_class_name || name
20
27
  end
21
28
 
22
- def class_name
23
- @options[:joined_class_name] || @name
29
+ def primary_key
30
+ :id
31
+ end
32
+
33
+ def foreign_name
34
+ as || name
35
+ end
36
+
37
+ def foreign_polymorphic_type
38
+ "#{foreign_name}_type".to_sym
39
+ end
40
+
41
+ def foreign_key
42
+ @options[:foreign_key] || "#{foreign_name.to_s.singularize}_id".to_sym
24
43
  end
25
44
 
26
45
  def classify
27
- if @options[:class]
28
- @options[:class]
46
+ fail "Column#classify indeterminate for polymorphic associations" if type == :belongs_to && polymorphic
47
+ if @klass
48
+ @klass
29
49
  else
30
50
  case @type
31
51
  when :belongs_to
@@ -43,8 +63,33 @@ module MotionModel
43
63
  end
44
64
 
45
65
  def through_class
46
- Kernel::const_get(@options[:through].to_s.classify)
66
+ Kernel::const_get(through.to_s.classify)
67
+ end
68
+
69
+ def inverse_foreign_key
70
+ inverse_column.foreign_key
47
71
  end
72
+
73
+ def inverse_name
74
+ if as
75
+ as
76
+ elsif inverse_of
77
+ inverse_of
78
+ elsif type == :belongs_to
79
+ # Check for a singular and a plural relationship
80
+ name = owner.name.singularize.underscore
81
+ col = classify.column(name)
82
+ col ||= classify.column(name.pluralize)
83
+ col.name
84
+ else
85
+ owner.name.singularize.underscore.to_sym
86
+ end
87
+ end
88
+
89
+ def inverse_column
90
+ classify.column(inverse_name)
91
+ end
92
+
48
93
  end
49
94
  end
50
95
  end
@@ -44,7 +44,7 @@ module MotionModel
44
44
  end
45
45
 
46
46
  def combine_options(column, hash) #nodoc
47
- options = column_named(column).options[:formotion]
47
+ options = column(column).options[:formotion]
48
48
  options ? hash.merge(options) : hash
49
49
  end
50
50
 
@@ -47,13 +47,19 @@ module MotionModel
47
47
  def self.included(base)
48
48
  base.extend(PrivateClassMethods)
49
49
  base.extend(PublicClassMethods)
50
+
51
+ base.instance_eval do
52
+ unless self.respond_to?(:id)
53
+ add_field(:id, :integer)
54
+ end
55
+ end
50
56
  end
51
57
 
52
58
  module PublicClassMethods
53
59
 
54
60
  def new(options = {})
55
61
  object_class = options[:inheritance_type] ? Kernel.const_get(options[:inheritance_type]) : self
56
- object_class.alloc.instance_eval do
62
+ object_class.allocate.instance_eval do
57
63
  initialize(options)
58
64
  self
59
65
  end
@@ -130,10 +136,6 @@ module MotionModel
130
136
  add_field relation, :has_one, options # Relation must be plural
131
137
  end
132
138
 
133
- def generate_belongs_to_id(relation)
134
- (relation.to_s.singularize.underscore + '_id').to_sym
135
- end
136
-
137
139
  # Use at class level, as follows
138
140
  #
139
141
  # class Assignee
@@ -150,13 +152,17 @@ module MotionModel
150
152
  end
151
153
 
152
154
  # Returns true if a column exists on this model, otherwise false.
153
- def column?(column)
154
- !column_named(column).nil?
155
+ def column?(col)
156
+ !column(col).nil?
155
157
  end
156
158
 
157
159
  # Returns type of this column.
158
- def column_type(column)
159
- column_named(column).type || nil
160
+ def column_type(col)
161
+ column(col).type || nil
162
+ end
163
+
164
+ def column(col)
165
+ col.is_a?(Column) ? col : _column_hashes[col.to_sym]
160
166
  end
161
167
 
162
168
  def has_many_columns
@@ -176,9 +182,9 @@ module MotionModel
176
182
  end
177
183
 
178
184
  # returns default value for this column or nil.
179
- def default(column)
180
- col = column_named(column)
181
- col.nil? ? nil : col.default
185
+ def default(col)
186
+ _col = column(col)
187
+ _col.nil? ? nil : _col.default
182
188
  end
183
189
 
184
190
  # Build an instance that represents a saved object from the persistence layer.
@@ -249,8 +255,10 @@ module MotionModel
249
255
  @_column_hashes ||= {}
250
256
  end
251
257
 
258
+ # BUGBUG: This appears not to be executed, therefore @_issue_notifications is always nil to begin with.
252
259
  @_issue_notifications = true
253
260
  def _issue_notifications
261
+ @_issue_notifications = true if @_issue_notifications.nil?
254
262
  @_issue_notifications
255
263
  end
256
264
 
@@ -302,135 +310,42 @@ module MotionModel
302
310
  end
303
311
 
304
312
  def define_accessor_methods(name, type, options = {}) #nodoc
305
- unless alloc.respond_to?(name.to_sym)
306
- define_method(name.to_sym) {
307
- return nil if @data[name].nil?
308
- if options[:symbolize]
309
- @data[name].to_sym
310
- else
311
- @data[name]
312
- end
313
- }
314
- end
315
- define_method("#{name}=".to_sym) { |value|
316
- old_value = @data[name]
317
- new_value = cast_to_type(name, value)
318
- if new_value != old_value
319
- @data[name] = new_value
320
- @dirty = true
321
- end
322
- }
313
+ define_method(name.to_sym) { _get_attr(name) } unless allocate.respond_to?(name)
314
+ define_method("#{name}=".to_sym) { |v| _set_attr(name, v) }
323
315
  end
324
316
 
325
317
  def define_belongs_to_methods(name) #nodoc
326
- col = column_named(name)
327
-
328
- define_method(name) {
329
- return @data[name] if @data[name]
330
- if col.options[:polymorphic]
331
- if (owner_class_name = send("#{name}_type"))
332
- owner_class = Kernel::deep_const_get(owner_class_name.classify)
333
- parent_id = send("#{name}_id")
334
- end
335
- else
336
- owner_class = col.classify
337
- parent_id = send(self.class.generate_belongs_to_id(col.name))
338
- end
339
- parent_id.nil? ? nil : owner_class.find_by_id(parent_id)
340
- }
341
-
342
- define_method("#{name}_relation") {
343
- relation_for(name)
344
- }
345
-
346
- # Associate the parent and delegate the inverse assignment
347
- define_method("#{name}=") { |parent|
348
- rebuild_relation_for(name, parent)
349
- send("set_#{name}", parent)
350
- if col.options[:polymorphic]
351
- foreign_column_name = parent.column_as_name(col.options[:as] || col.name)
352
- else
353
- foreign_column_name = self.class.name.underscore.to_sym
354
- end
355
- parent.rebuild_relation_for(foreign_column_name, self) if parent
356
- }
357
-
358
- # Associate the parent but without delegating the inverse assignment
359
- define_method("set_#{name}") { |parent|
360
- @data[name] = parent
361
- if col.options[:polymorphic]
362
- send("#{name}_type=", parent.class.name)
363
- send("#{name}_id=", parent.id)
364
- else
365
- parent_id_name = self.class.generate_belongs_to_id(col.name)
366
- send("#{parent_id_name}=", parent ? parent.id : nil)
367
- end
368
- }
318
+ col = column(name)
319
+ define_method(name) { get_belongs_to_attr(col) }
320
+ define_method("#{name}=") { |owner| set_belongs_to_attr(col, owner) }
369
321
 
370
322
  # TODO also define #{name}+id= methods....
371
323
 
372
- if col.options[:polymorphic]
373
- add_field "#{name}_type", :belongs_to_type
374
- add_field "#{name}_id", :belongs_to_id
324
+ if col.polymorphic
325
+ add_field col.foreign_polymorphic_type, :belongs_to_type
326
+ add_field col.foreign_key, :belongs_to_id
375
327
  else
376
- add_field generate_belongs_to_id(name), :belongs_to_id # a relation is singular.
328
+ add_field col.foreign_key, :belongs_to_id # a relation is singular.
377
329
  end
378
330
  end
379
331
 
380
332
  def define_has_many_methods(name) #nodoc
381
- col = column_named(name)
382
-
383
- define_method("#{name}_relation") {
384
- relation_for(name)
385
- }
386
-
387
- define_method(name) {
388
- send("#{name}_relation").to_a
389
- }
390
-
391
- define_method("#{name}=") do |collection|
392
- rebuild_relation_for(name, collection)
393
- collection.each do |instance|
394
- if col.options[:polymorphic]
395
- foreign_column_name = col.options[:as] || col.name
396
- else
397
- foreign_column_name = self.class.name.underscore.to_sym
398
- end
399
- instance.send("set_#{foreign_column_name}", self)
400
- instance.rebuild_relation_for(foreign_column_name, self)
401
- end
402
- end
403
-
333
+ col = column(name)
334
+ define_method(name) { get_has_many_attr(col) }
335
+ define_method("#{name}=") { |collection| set_has_many_attr(col, *collection) }
404
336
  end
405
337
 
406
338
  def define_has_one_methods(name) #nodoc
407
- col = column_named(name)
408
-
409
- define_method("#{name}_relation") {
410
- relation_for(name)
411
- }
412
-
413
- define_method(name) {
414
- send("#{name}_relation").instance
415
- }
416
-
417
- define_method("#{name}=") do |instance|
418
- relation_for(name).instance = instance
419
- if instance
420
- if col.options[:polymorphic]
421
- foreign_column_name = col.options[:as] || col.name
422
- else
423
- foreign_column_name = self.class.name.underscore.to_sym
424
- end
425
- instance.rebuild_relation_for(foreign_column_name, self)
426
- end
427
- end
339
+ col = column(name)
340
+ define_method(name) { get_has_one_attr(col) }
341
+ define_method("#{name}=") { |instance| set_has_one_attr(col, instance) }
428
342
  end
429
343
 
430
344
  def add_field(name, type, options = {:default => nil}) #nodoc
431
- col = Column.new(name, type, options)
345
+ name = name.to_sym
346
+ col = Column.new(self, name, type, options)
432
347
 
433
- _column_hashes[col.name.to_sym] = col
348
+ _column_hashes[col.name] = col
434
349
 
435
350
  case type
436
351
  when :has_many then define_has_many_methods(name)
@@ -440,36 +355,28 @@ module MotionModel
440
355
  end
441
356
  end
442
357
 
443
- # Returns a column denoted by +name+
444
- def column_named(name) #nodoc
445
- _column_hashes[name.to_sym]
446
- end
447
-
448
358
  # Returns the column that has the name as its :as option
449
- def column_as(name) #nodoc
450
- _column_hashes.values.find{ |c| c.options[:as] == name }
359
+ def column_as(col) #nodoc
360
+ _col = column(col)
361
+ _column_hashes.values.find{ |c| c.as == _col.name }
451
362
  end
452
363
 
453
364
  # All relation columns, including type and id columns for polymorphic associations
454
- def relation_column?(column) #nodoc
455
- [:belongs_to, :belongs_to_id, :belongs_to_type, :has_many, :has_one].include? column_named(column).type
365
+ def relation_column?(col) #nodoc
366
+ _col = column(col)
367
+ [:belongs_to, :belongs_to_id, :belongs_to_type, :has_many, :has_one].include?(_col.type)
456
368
  end
457
369
 
458
370
  # Polymorphic association columns that are not stored in DB
459
- def virtual_polymorphic_relation_column?(column) #nodoc
460
- [:belongs_to, :has_many, :has_one].include? column_named(column).type
371
+ def virtual_polymorphic_relation_column?(col) #nodoc
372
+ _col = column(col)
373
+ [:belongs_to, :has_many, :has_one].include?(_col.type)
461
374
  end
462
375
 
463
376
  def has_relation?(col) #nodoc
464
377
  return false if col.nil?
465
-
466
- col = case col
467
- when MotionModel::Model::Column
468
- column_named(col.name)
469
- else
470
- column_named(col)
471
- end
472
- [:has_many, :has_one, :belongs_to].include?(col.type)
378
+ _col = column(col)
379
+ [:has_many, :has_one, :belongs_to].include?(_col.type)
473
380
  end
474
381
 
475
382
  end
@@ -523,7 +430,7 @@ module MotionModel
523
430
  def ==(comparison_object)
524
431
  super ||
525
432
  comparison_object.instance_of?(self.class) &&
526
- id.present? &&
433
+ !id.nil? &&
527
434
  comparison_object.id == id
528
435
  end
529
436
  alias :eql? :==
@@ -533,7 +440,7 @@ module MotionModel
533
440
  end
534
441
 
535
442
  def attributes=(attrs)
536
- attrs.each { |k, v| send("#{k}=", v) }
443
+ attrs.each { |k, v| set_attr(k, v) }
537
444
  end
538
445
 
539
446
  def update_attributes(attrs)
@@ -552,10 +459,14 @@ module MotionModel
552
459
  @data[:id].to_i
553
460
  end
554
461
 
555
- # Default to_s implementation returns a list of columns and values
556
- # separated by newlines.
462
+ # Default inspect implementation returns identifier and ID
463
+ # Need to keep this short, i.e. for running specs as the output could be very large
464
+ def inspect
465
+ object_identifier
466
+ end
467
+
557
468
  def to_s
558
- columns.each{|c| "#{c}: #{self.send(c)}\n"}
469
+ columns.each{|c| "#{c}: #{get_attr(c)}\n"}
559
470
  end
560
471
 
561
472
  def save!(options = {})
@@ -604,6 +515,8 @@ module MotionModel
604
515
  def after_save(sender); end
605
516
  def before_delete(sender); end
606
517
  def after_delete(sender); end
518
+ def before_destroy(sender); end
519
+ def after_destroy(sender); end
607
520
 
608
521
  def call_hook(hook_name, postfix)
609
522
  hook = "#{hook_name}_#{postfix}"
@@ -644,7 +557,7 @@ module MotionModel
644
557
  options[:omit_model_identifiers] ||= {}
645
558
  options[:omit_model_identifiers][model_identifier] = self
646
559
  self.class.association_columns.each do |name, col|
647
- delete_candidates = self.send(name)
560
+ delete_candidates = get_attr(name)
648
561
  Array(delete_candidates).each do |candidate|
649
562
  next if options[:omit_model_identifiers][candidate.model_identifier]
650
563
  if col.dependent == :destroy
@@ -660,8 +573,12 @@ module MotionModel
660
573
  end
661
574
 
662
575
  # True if the column exists, otherwise false
663
- def column?(column_name)
664
- self.class.column?(column_name.to_sym)
576
+ def column?(col)
577
+ self.class.column?(col)
578
+ end
579
+
580
+ def column(col)
581
+ self.class.column(col)
665
582
  end
666
583
 
667
584
  # Returns list of column names as an array
@@ -670,8 +587,8 @@ module MotionModel
670
587
  end
671
588
 
672
589
  # Type of a given column
673
- def column_type(column_name)
674
- self.class.column_type(column_name)
590
+ def column_type(col)
591
+ self.class.column_type(col)
675
592
  end
676
593
 
677
594
  # Options hash for column, excluding the core
@@ -682,8 +599,8 @@ module MotionModel
682
599
  # example:
683
600
  #
684
601
  # columns :date => {:type => :date, :formotion => {:picker_type => :date_time}}
685
- def options(column_name)
686
- column_named(column_name).options
602
+ def options(col)
603
+ column(col).options
687
604
  end
688
605
 
689
606
  def dirty?
@@ -694,8 +611,141 @@ module MotionModel
694
611
  @dirty = true
695
612
  end
696
613
 
697
- def column_as_name(name) #nodoc
698
- self.class.send(:column_as, name.to_sym).try(:name)
614
+ def get_attr(name)
615
+ send(name)
616
+ end
617
+
618
+ def _attr_present?(name)
619
+ @data.has_key?(name)
620
+ end
621
+
622
+ def _get_attr(col)
623
+ _col = column(col)
624
+ return nil if @data[_col.name].nil?
625
+ if _col.symbolize
626
+ @data[_col.name].to_sym
627
+ else
628
+ @data[_col.name]
629
+ end
630
+ end
631
+
632
+ def set_attr(name, value)
633
+ send("#{name}=", value)
634
+ end
635
+
636
+ def _set_attr(name, value)
637
+ name = name.to_sym
638
+ old_value = @data[name]
639
+ new_value = relation_column?(name) ? value : cast_to_type(name, value)
640
+ if new_value != old_value
641
+ @data[name] = new_value
642
+ @dirty = true
643
+ end
644
+ end
645
+
646
+ def get_belongs_to_attr(col)
647
+ belongs_to_relation(col)
648
+ end
649
+
650
+ def get_has_many_attr(col)
651
+ _has_many_has_one_relation(col)
652
+ end
653
+
654
+ def get_has_one_attr(col)
655
+ _has_many_has_one_relation(col)
656
+ end
657
+
658
+ # Associate the owner but without rebuilding the inverse assignment
659
+ def set_belongs_to_attr(col, owner, options = {})
660
+ _col = column(col)
661
+ unless belongs_to_synced?(_col, owner)
662
+ _set_attr(_col.name, owner)
663
+ rebuild_relation(_col, owner, set_inverse: options[:set_inverse])
664
+ if _col.polymorphic
665
+ set_polymorphic_attr(_col.name, owner)
666
+ else
667
+ _set_attr(_col.foreign_key, owner ? owner.id : nil)
668
+ end
669
+ end
670
+
671
+ owner
672
+ end
673
+
674
+ # Determine if the :belongs_to relationship is synchronized. Checks the instance and the DB column attributes.
675
+ def belongs_to_synced?(col, owner)
676
+ # The :belongs_to that points to the instance has changed
677
+ return false if get_belongs_to_attr(col) != owner
678
+
679
+ # The polymorphic reference (_type, _id) columns do not match, maybe it was just saved
680
+ return false if col.polymorphic && !polymorphic_attr_matches?(col, owner)
681
+
682
+ # The key reference (_id) column does not match, maybe it was just saved
683
+ return false if _get_attr(col.foreign_key) != owner.try(:id)
684
+
685
+ true
686
+ end
687
+
688
+ def push_has_many_attr(col, *instances)
689
+ _col = column(col)
690
+ collection = get_has_many_attr(_col)
691
+ _collection = []
692
+ instances.each do |instance|
693
+ next if collection.include?(instance)
694
+ _collection << instance
695
+ end
696
+ push_relation(_col, *_collection)
697
+ instances
698
+ end
699
+
700
+ # TODO clean up existing reference, check rails
701
+ def set_has_many_attr(col, *instances)
702
+ _col = column(col)
703
+ unload_relation(_col)
704
+ push_has_many_attr(_col, *instances)
705
+ instances
706
+ end
707
+
708
+ def set_has_one_attr(col, instance)
709
+ _col = column(col)
710
+ if get_has_one_attr(_col) != instance
711
+ rebuild_relation(_col, instance)
712
+ end
713
+ instance
714
+ end
715
+
716
+ def get_polymorphic_attr(col)
717
+ _col = column(col)
718
+ owner_class = nil
719
+ id = _get_attr(_col.foreign_key)
720
+ unless id.nil?
721
+ owner_class_name = _get_attr(_col.foreign_polymorphic_type)
722
+ owner_class_name = String(owner_class_name) # RubyMotion issue, String#classify might fail otherwise
723
+ owner_class = Kernel::deep_const_get(owner_class_name.classify)
724
+ end
725
+ [owner_class, id]
726
+ end
727
+
728
+
729
+ def polymorphic_attr_matches?(col, instance)
730
+ klass, id = get_polymorphic_attr(col)
731
+ klass == instance.class && id == instance.id
732
+ end
733
+
734
+ def set_polymorphic_attr(col, instance)
735
+ _col = column(col)
736
+ _set_attr(_col.foreign_polymorphic_type, instance.class.name)
737
+ _set_attr(_col.foreign_key, instance.id)
738
+ instance
739
+ end
740
+
741
+ def foreign_column_name(col)
742
+ if col.polymorphic
743
+ col.as || col.name
744
+ elsif col.foreign_key
745
+ col.foreign_key
746
+ else
747
+ self.class.name.underscore.to_sym
748
+ end
699
749
  end
700
750
 
701
751
  private
@@ -716,20 +766,15 @@ module MotionModel
716
766
  self.class.send(:has_relation?, col)
717
767
  end
718
768
 
719
- def initialize_data_columns(column, value) #nodoc
720
- self.attributes = {column => value || self.class.default(column)}
721
- end
722
-
723
- def column_named(name) #nodoc
724
- self.class.send(:column_named, name.to_sym)
769
+ def rebuild_relation(column_name, instance_or_collection, options = {}) # nodoc
725
770
  end
726
771
 
727
- def column_as(name) #nodoc
728
- self.class.send(:column_as, name.to_sym)
772
+ def initialize_data_columns(column, value) #nodoc
773
+ self.attributes = {column => value || self.class.default(column)}
729
774
  end
730
775
 
731
- def generate_belongs_to_id(class_or_column) # nodoc
732
- self.class.generate_belongs_to_id(self.class)
776
+ def column_as(col) #nodoc
777
+ self.class.send(:column_as, col)
733
778
  end
734
779
 
735
780
  def issue_notification(info) #nodoc
@@ -737,19 +782,8 @@ module MotionModel
737
782
  end
738
783
 
739
784
  def method_missing(sym, *args, &block)
740
- if sym.to_s[-1] == '='
741
- @data["#{sym.to_s.chop}".to_sym] = args.first
742
- return args.first
743
- else
744
- return @data[sym] if @data && @data.has_key?(sym)
745
- end
746
- begin
747
- r = super
748
- rescue NoMethodError => exc
749
- unless exc.to_s =~ /undefined method `(?:before|after)_/
750
- raise
751
- end
752
- end
785
+ return @data[sym] if sym.to_s[-1] != '=' && @data && @data.has_key?(sym)
786
+ super
753
787
  end
754
788
 
755
789
  end
@@ -60,6 +60,9 @@ module MotionModel
60
60
  # * First, it triggers validations.
61
61
  #
62
62
  # * Second, it returns the result of performing the validations.
63
+ def before_validation(sender); end
64
+ def after_validation(sender); end
65
+
63
66
  def valid?
64
67
  call_hooks 'validation' do
65
68
  @messages = []
@@ -181,5 +184,10 @@ module MotionModel
181
184
  def add_message(field, message)
182
185
  @messages.push({field.to_sym => message})
183
186
  end
187
+
188
+ # Stub methods for hook protocols
189
+ def before_validation(sender); end
190
+ def after_validation(sender); end
191
+
184
192
  end
185
193
  end
@@ -4,5 +4,5 @@
4
4
  # or forward port their code to take advantage
5
5
  # of adapters.
6
6
  module MotionModel
7
- VERSION = "0.4.2"
7
+ VERSION = "0.4.4"
8
8
  end
data/motion_model.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |gem|
12
12
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
13
  gem.name = "motion_model"
14
14
  gem.require_paths = ["lib"]
15
- gem.add_dependency 'bubble-wrap', '~> 1.1.4'
15
+ gem.add_dependency 'bubble-wrap', '1.3.0.osx'
16
16
  gem.add_dependency 'motion-support', '>=0.1.0'
17
17
  gem.version = MotionModel::VERSION
18
18
  end
@@ -205,8 +205,8 @@ end
205
205
  describe "serialization of relations" do
206
206
  before do
207
207
  parent = Parent.create(:name => 'BoB')
208
- parent.children_relation.create :name => 'Fergie'
209
- parent.children_relation.create :name => 'Will I Am'
208
+ parent.children.create :name => 'Fergie'
209
+ parent.children.create :name => 'Will I Am'
210
210
  end
211
211
 
212
212
  it "is wired up right" do
@@ -35,7 +35,7 @@ class Employee
35
35
  include MotionModel::Model
36
36
  include MotionModel::ArrayModelAdapter
37
37
  columns :name
38
- belongs_to :CascadedAssignee
38
+ belongs_to :cascaded_assignee
39
39
  end
40
40
 
41
41
  describe "cascading deletes" do
@@ -58,8 +58,8 @@ describe "cascading deletes" do
58
58
 
59
59
  it "deletes assignees that belong to a destroyed task" do
60
60
  task = CascadingTask.create(:name => 'cascading')
61
- task.cascaded_assignees_relation.create(:assignee_name => 'joe')
62
- task.cascaded_assignees_relation.create(:assignee_name => 'bill')
61
+ task.cascaded_assignees.create(:assignee_name => 'joe')
62
+ task.cascaded_assignees.create(:assignee_name => 'bill')
63
63
 
64
64
  CascadingTask.count.should == 1
65
65
  CascadedAssignee.count.should == 2
@@ -74,7 +74,7 @@ describe "cascading deletes" do
74
74
  1.upto(3) do |item|
75
75
  task = CascadingTask.create :name => "Task #{item}"
76
76
  1.upto(3) do |assignee|
77
- task.cascaded_assignees_relation.create :assignee_name => "assignee #{assignee} for task #{task}"
77
+ task.cascaded_assignees.create :assignee_name => "assignee #{assignee} for task #{task}"
78
78
  end
79
79
  end
80
80
  CascadingTask.count.should == 3
@@ -88,8 +88,8 @@ describe "cascading deletes" do
88
88
 
89
89
  it "deletes only one level when a task is destroyed but dependent is delete" do
90
90
  task = CascadingTask.create :name => 'dependent => :delete'
91
- assignee = task.cascaded_assignees_relation.create :assignee_name => 'deletable assignee'
92
- assignee.employees_relation.create :name => 'person who sticks around'
91
+ assignee = task.cascaded_assignees.create :assignee_name => 'deletable assignee'
92
+ assignee.employees.create :name => 'person who sticks around'
93
93
 
94
94
  CascadingTask.count.should == 1
95
95
  CascadedAssignee.count.should == 1
data/spec/model_spec.rb CHANGED
@@ -205,6 +205,11 @@ describe "Creating a model" do
205
205
  lambda{task.bar}.should.raise(NoMethodError)
206
206
  end
207
207
 
208
+ it 'raises a NoMethodError exception when an unknown attribute receives an assignment' do
209
+ task = Task.new
210
+ lambda{task.bar = 'foo'}.should.raise(NoMethodError)
211
+ end
212
+
208
213
  it 'successfully retrieves by attribute' do
209
214
  task = Task.create(:name => 'my task')
210
215
  task.name.should == 'my task'
@@ -39,7 +39,7 @@ describe 'related objects' do
39
39
 
40
40
  it "camelcased style" do
41
41
  t = User.create(:name => "Arkan")
42
- t.email_accounts_relation.create(:name => "Gmail")
42
+ t.email_accounts.create(:name => "Gmail")
43
43
  EmailAccount.first.user.name.should == "Arkan"
44
44
  User.last.email_accounts.last.name.should == "Gmail"
45
45
  end
@@ -58,12 +58,12 @@ describe 'related objects' do
58
58
 
59
59
  it 'relation objects are empty on initialization' do
60
60
  a_task = Task.create
61
- a_task.assignees_relation.all.should.be.empty
61
+ a_task.assignees.all.should.be.empty
62
62
  end
63
63
 
64
64
  it "supports creating related objects directly on parents" do
65
65
  a_task = Task.create(:name => 'Walk the Dog')
66
- a_task.assignees_relation.create(:assignee_name => 'bob')
66
+ a_task.assignees.create(:assignee_name => 'bob')
67
67
  a_task.assignees.count.should == 1
68
68
  a_task.assignees.first.assignee_name.should == 'bob'
69
69
  Assignee.count.should == 1
@@ -81,7 +81,7 @@ describe 'related objects' do
81
81
  assignee_index = 1
82
82
  @tasks << t
83
83
  1.upto(task * 2) do |assignee|
84
- @assignees << t.assignees_relation.create(:assignee_name => "employee #{assignee_index}_assignee_for_task_#{t.id}")
84
+ @assignees << t.assignees.create(:assignee_name => "employee #{assignee_index}_assignee_for_task_#{t.id}")
85
85
  assignee_index += 1
86
86
  end
87
87
  end
@@ -105,7 +105,7 @@ describe 'related objects' do
105
105
  assignee = Assignee.new(:assignee_name => 'Zoe')
106
106
  Task.count.should == 3
107
107
  assignee_count = Task.find(3).assignees.count
108
- Task.find(3).assignees_relation.push(assignee)
108
+ Task.find(3).assignees.push(assignee)
109
109
  Task.find(3).assignees.count.should == assignee_count + 1
110
110
  end
111
111
 
@@ -113,7 +113,7 @@ describe 'related objects' do
113
113
 
114
114
  it "supports creating blank (empty) scratchpad associated objects" do
115
115
  task = Task.create :name => 'watch a movie'
116
- assignee = task.assignees_relation.new # TODO per Rails convention, this should really be #build, not #new
116
+ assignee = task.assignees.new # TODO per Rails convention, this should really be #build, not #new
117
117
  assignee.assignee_name = 'Chloe'
118
118
  assignee.save
119
119
  task.assignees.count.should == 1
@@ -129,7 +129,7 @@ describe 'related objects' do
129
129
 
130
130
  it "allows a child to back-reference its parent" do
131
131
  t = Task.create(:name => "Walk the Dog")
132
- t.assignees_relation.create(:assignee_name => "Rihanna")
132
+ t.assignees.create(:assignee_name => "Rihanna")
133
133
  Assignee.first.task.name.should == "Walk the Dog"
134
134
  end
135
135
 
@@ -143,7 +143,7 @@ describe 'related objects' do
143
143
 
144
144
  describe "basic wiring" do
145
145
  before do
146
- @t1.assignees_relation << @a1
146
+ @t1.assignees << @a1
147
147
  end
148
148
 
149
149
  it "pushing a created assignee gives a task count of 1" do
@@ -161,7 +161,7 @@ describe 'related objects' do
161
161
 
162
162
  describe "when pushing assignees onto two different tasks" do
163
163
  before do
164
- @t2.assignees_relation << @a1
164
+ @t2.assignees << @a1
165
165
  end
166
166
 
167
167
  it "pushing assignees to two different tasks lets the last task have the assignee (count)" do
metadata CHANGED
@@ -1,61 +1,57 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: motion_model
3
- version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 0
7
- - 4
8
- - 2
9
- version: 0.4.2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.4
5
+ prerelease:
10
6
  platform: ruby
11
- authors:
7
+ authors:
12
8
  - Steve Ross
13
9
  autorequire:
14
10
  bindir: bin
15
11
  cert_chain: []
16
-
17
- date: 2013-05-01 00:00:00 -07:00
18
- default_executable:
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
12
+ date: 2013-05-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
21
15
  name: bubble-wrap
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
24
- requirements:
25
- - - ~>
26
- - !ruby/object:Gem::Version
27
- segments:
28
- - 1
29
- - 1
30
- - 4
31
- version: 1.1.4
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.3.0.osx
32
22
  type: :runtime
33
- version_requirements: *id001
34
- - !ruby/object:Gem::Dependency
35
- name: motion-support
36
23
  prerelease: false
37
- requirement: &id002 !ruby/object:Gem::Requirement
38
- requirements:
39
- - - ">="
40
- - !ruby/object:Gem::Version
41
- segments:
42
- - 0
43
- - 1
44
- - 0
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.3.0.osx
30
+ - !ruby/object:Gem::Dependency
31
+ name: motion-support
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
45
37
  version: 0.1.0
46
38
  type: :runtime
47
- version_requirements: *id002
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 0.1.0
48
46
  description: Simple model and validation mixins for RubyMotion
49
- email:
47
+ email:
50
48
  - sxross@gmail.com
51
49
  executables: []
52
-
53
50
  extensions: []
54
-
55
51
  extra_rdoc_files: []
56
-
57
- files:
52
+ files:
58
53
  - .gitignore
54
+ - .travis.yml
59
55
  - CHANGELOG
60
56
  - Gemfile
61
57
  - LICENSE
@@ -91,37 +87,31 @@ files:
91
87
  - spec/relation_spec.rb
92
88
  - spec/transaction_spec.rb
93
89
  - spec/validation_spec.rb
94
- has_rdoc: true
95
90
  homepage: https://github.com/sxross/MotionModel
96
91
  licenses: []
97
-
98
92
  post_install_message:
99
93
  rdoc_options: []
100
-
101
- require_paths:
94
+ require_paths:
102
95
  - lib
103
- required_ruby_version: !ruby/object:Gem::Requirement
104
- requirements:
105
- - - ">="
106
- - !ruby/object:Gem::Version
107
- segments:
108
- - 0
109
- version: "0"
110
- required_rubygems_version: !ruby/object:Gem::Requirement
111
- requirements:
112
- - - ">="
113
- - !ruby/object:Gem::Version
114
- segments:
115
- - 0
116
- version: "0"
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ! '>='
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
117
108
  requirements: []
118
-
119
109
  rubyforge_project:
120
- rubygems_version: 1.3.6
110
+ rubygems_version: 1.8.24
121
111
  signing_key:
122
112
  specification_version: 3
123
113
  summary: Simple model and validation mixins for RubyMotion
124
- test_files:
114
+ test_files:
125
115
  - spec/adapter_spec.rb
126
116
  - spec/array_model_persistence_spec.rb
127
117
  - spec/cascading_delete_spec.rb