motion_model 0.4.2 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
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