motion-loco 0.1.3 → 0.2.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.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NDUzNjAyZjM3YWVhYzM3ZjgyZTJlYjkzZmE2NjY2ODI0NTc3ZjU5YQ==
4
+ NzdjMjhmYjA5ZTgzNjEzMTg2ZTQ1ZjcxZjYyOGEwMWRjM2I3NWI2MA==
5
5
  data.tar.gz: !binary |-
6
- ZTI3YTRjMDEwNzQ5M2FjZjQyYjU2MWU2YWIyY2FlOTI5MTRhOGZiZQ==
6
+ NTVkY2JlMmE0MzE1NTI0OTk2YmI5YjJiZmJiNmRlZGZlYmEyZDI3Mg==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- ZTA2ODIxODZkY2M0NzgyMTMxZGM2YmM0NDAzYTQ1MGJlNzA5ZmNjNzEzOTJj
10
- ZjM2NWU3NmZkM2Y0ZTQ4ZDUwY2ZlZWZlMTg2ZTI5YjRjYzNhNzJiY2Q2ODA5
11
- Njk3MThkZGY4MzNiNTUzMjYwMmQ0N2E3ODEzMzZlNDg1ZTZiMmU=
9
+ MzQ5YTcyNjAxODRlYTU5NGVlMmE1YWU2OTc5MTcxYzVjZmVhNzkxNTk2NmQ3
10
+ ZDQxMzQ3ZTJlNzczYjYxYWI2OTQ2NTlmY2JlZGUzNzI4ODNiNTI2YjMxNTll
11
+ ZGZhNDI5MGFhZGM2MzM3ZTYwNWU4Y2NlMDYyODlmYTk2NGQxYTg=
12
12
  data.tar.gz: !binary |-
13
- MWFiNzQ4MmZlYzA2ZGFhYTcwZmM0ODA3MjdjOGFlZGUwY2FlNWEwNmNjMGI2
14
- NzM1ODU4NmY5ZGRlNTUyYWJiMDAyOGIzYTBhN2I3ZDYzNWFlMzEzZjVkM2E1
15
- Mzk1MDg0NzVjZjY0ODQxZmQzOWQwNDhmZTQ2Zjg3MzZjN2JiOGI=
13
+ MzlhNDI5ODUzODczZDZlNzgzMDIxOGNkYjkyMGU5YTZiY2M3YWM2NDg3YTNm
14
+ YWZiZjE2NzE3NWNiZTExNTZkYTAwOGYzY2NmZTlmYTY3MmUxOWVkZjU4NTVk
15
+ NTVlYzlkMWRkOTRmNWFhNWQxMTU4ZDZjMzQwY2JkODY0NmZhNTQ=
data/README.md CHANGED
@@ -2,20 +2,19 @@
2
2
 
3
3
  Motion-Loco is a library for [RubyMotion](http://rubymotion.com)
4
4
  that includes [Ember.js](http://emberjs.com) inspired bindings,
5
- computed properties, and observers.
5
+ computed properties, and observers. **And Ember-Data inspired [data](#locofixtureadapter) [adapters](#locorestadapter)!**
6
6
 
7
- Also included is a set of views that are easier to position and size.
7
+ ## What's New!
8
8
 
9
- **I'm not using this in production yet. It might be ready,
10
- but I feel like it needs some more features to really be useful.**
9
+ ### June 23th, 2013
11
10
 
12
- ## What's New!
11
+ #### SQLiteAdapter and Relationships!
13
12
 
14
- ### June 7th, 2013
13
+ I need to write up some better documentation, but be sure to check out the [Loco::SQLiteAdapter](locosqliteadapter) and the new `has_many` and `belongs_to` relationships in `Loco::Model`.
15
14
 
16
- #### Data Adapters
15
+ The relationship stuff needs some major code clean up, so don't copy my code if you're doing anything similar for loading/saving relationship data. :)
17
16
 
18
- These are still a bit of a work in progress, but [worth](#locofixtureadapter) [checking out](#locorestadapter)!
17
+ The [tests](https://github.com/brianpattison/motion-loco/tree/master/spec) all pass though... so that's something, right?
19
18
 
20
19
  ## Installation
21
20
 
@@ -40,8 +39,8 @@ They beat out using a method because they can be observed like any other propert
40
39
 
41
40
  ```ruby
42
41
  class Person < Loco::Model
43
- property :first_name
44
- property :last_name
42
+ property :first_name, :string
43
+ property :last_name, :string
45
44
 
46
45
  # Computed property for full name that watches for changes
47
46
  # in the object's first_name and last_name properties.
@@ -64,8 +63,8 @@ Bindings are used to link the property of an object to a property of another obj
64
63
 
65
64
  ```ruby
66
65
  class Person < Loco::Model
67
- property :first_name
68
- property :last_name
66
+ property :first_name, :string
67
+ property :last_name, :string
69
68
  property :full_name, lambda{|object|
70
69
  "#{object.first_name} #{object.last_name}"
71
70
  }.property(:first_name, :last_name)
@@ -76,7 +75,7 @@ end
76
75
  last_name: 'Pattison'
77
76
  )
78
77
 
79
- @label = Loco::Label.alloc.initWithFrame(
78
+ @label = Loco::UI::Label.alloc.initWithFrame(
80
79
  textBinding: [@person, 'full_name'],
81
80
  height: 30,
82
81
  top: 20,
@@ -96,7 +95,7 @@ class PersonController < Loco::Controller
96
95
  property :content
97
96
  end
98
97
 
99
- @label = Loco::Label.alloc.initWithFrame(
98
+ @label = Loco::UI::Label.alloc.initWithFrame(
100
99
  textBinding: 'PersonController.content.full_name',
101
100
  height: 30,
102
101
  top: 20,
@@ -113,18 +112,18 @@ PersonController.content = @person
113
112
  @label.text # "Brian Pattison"
114
113
  ```
115
114
 
116
- ### Loco::TableView
115
+ ### Loco::UI::TableView
117
116
 
118
- A `Loco::TableView` is used for to easily bind a collection of objects
117
+ A `Loco::UI::TableView` is used for to easily bind a collection of objects
119
118
  to a `UITableView` and each item in the collection to a reusable `UITableViewCell`.
120
119
 
121
120
  ```ruby
122
- class MyTableViewCell < Loco::TableViewCell
121
+ class MyTableViewCell < Loco::UI::TableViewCell
123
122
  # The `view_setup` method is called the first time the cell is created.
124
123
  # Bindings can be made to the item assigned to the cell
125
124
  # by binding to `parentView.content`.
126
125
  def view_setup
127
- @label = Loco::Label.alloc.initWithFrame(
126
+ @label = Loco::UI::Label.alloc.initWithFrame(
128
127
  textBinding: 'parentView.content.first_name',
129
128
  height: 30,
130
129
  left: 60,
@@ -135,7 +134,7 @@ class MyTableViewCell < Loco::TableViewCell
135
134
  end
136
135
  end
137
136
 
138
- class MyTableView < Loco::TableView
137
+ class MyTableView < Loco::UI::TableView
139
138
  item_view_class MyTableViewCell
140
139
  end
141
140
 
@@ -153,7 +152,7 @@ end
153
152
  ```ruby
154
153
  class Show < Loco::Model
155
154
  adapter 'Loco::FixtureAdapter'
156
- property :title
155
+ property :title, :string
157
156
  end
158
157
 
159
158
  @show = Show.find(2) # Loads from `resources/fixtures/plural_class_name.json`
@@ -165,8 +164,8 @@ end
165
164
  ```ruby
166
165
  class Post < Loco::Model
167
166
  adapter 'Loco::RESTAdapter', 'http://localhost:3000'
168
- property :title
169
- property :body
167
+ property :title, :string
168
+ property :body, :string
170
169
  end
171
170
 
172
171
  # GET http://localhost:3000/posts/1.json
@@ -211,13 +210,42 @@ end
211
210
  end
212
211
  ```
213
212
 
214
- ## Todo
213
+ ### Loco::SQLiteAdapter
214
+ ```ruby
215
+ class Player < Loco::Model
216
+ adapter 'Loco::SQLiteAdapter'
217
+ property :name, :string
218
+ has_many :scores
219
+ end
220
+
221
+ class Score < Loco::Model
222
+ adapter 'Loco::SQLiteAdapter'
223
+ property :rank, :integer
224
+ property :value, :integer
225
+ belongs_to :player
226
+ end
227
+
228
+ @player = Player.new(name: 'Kirsten Pattison')
229
+ @player.save
230
+
231
+ @score = Score.new(rank: 1, value: 50000, player: @player)
232
+ @score.save
233
+
234
+ @player.scores.length # 1
235
+ @score.player.name # Kirsten Pattison
236
+ ```
237
+
238
+ ## TODO
215
239
 
240
+ - RESTAdapter
241
+ - Sideload belongs_to/has_many
242
+ - SQLiteAdapter
243
+ - Data migrations?
216
244
  - State Manager
217
- - Relationships
218
- - More Adapters
219
- - Core Data (Are SQLite and iCloud also part of this adapter??)
220
- - Lots of stuff
245
+ - Pretty big challenge, so it might be a while
246
+ - Limit transitions between states
247
+ - Rollback dirty/unsaved records
248
+ - Improve everything! :)
221
249
 
222
250
  ## Contributing
223
251
 
@@ -8,4 +8,8 @@ require 'bubble-wrap/http'
8
8
  require 'motion-require'
9
9
  require 'motion-support/inflector'
10
10
 
11
- Motion::Require.all(Dir.glob(File.expand_path('../motion-loco/**/*.rb', __FILE__)))
11
+ Motion::Require.all(Dir.glob(File.expand_path('../motion-loco/**/*.rb', __FILE__)))
12
+
13
+ Motion::Project::App.setup do |app|
14
+ app.frameworks += ['CoreData']
15
+ end
@@ -1,3 +1,5 @@
1
+ motion_require 'convenience_methods'
2
+
1
3
  module Loco
2
4
 
3
5
  class Adapter
@@ -6,6 +8,10 @@ module Loco
6
8
  raise NoMethodError, "Loco::Adapter subclasses must implement #create_record(record, &block)."
7
9
  end
8
10
 
11
+ def delete_record(record, &block)
12
+ raise NoMethodError, "Loco::Adapter subclasses must implement #delete_record(record, &block)."
13
+ end
14
+
9
15
  def find(record, id, &block)
10
16
  raise NoMethodError, "Loco::Adapter subclasses must implement #find(record, id, &block)."
11
17
  end
@@ -22,14 +28,172 @@ module Loco
22
28
  raise NoMethodError, "Loco::Adapter subclasses must implement #find_query(type, records, params, &block)."
23
29
  end
24
30
 
25
- def save_record(record, &block)
26
- raise NoMethodError, "Loco::Adapter subclasses must implement #save_record(record, &block)."
31
+ def update_record(record, &block)
32
+ raise NoMethodError, "Loco::Adapter subclasses must implement #update_record(record, &block)."
27
33
  end
28
34
 
29
- def delete_record(record, &block)
30
- raise NoMethodError, "Loco::Adapter subclasses must implement #delete_record(record, &block)."
35
+ def serialize(record, options={}, json={})
36
+ properties = record.class.get_class_properties.select{|prop|
37
+ if prop[:type]
38
+ if prop[:name] == :id
39
+ options[:include_id] || options[:includeId]
40
+ else
41
+ true
42
+ end
43
+ end
44
+ }
45
+
46
+ transforms = self.class.get_transforms
47
+
48
+ properties.each do |property|
49
+ key = property[:name].to_sym
50
+ transform = transforms[property[:type]]
51
+ if transform
52
+ json[key] = transform[:serialize].call(record.valueForKey(key))
53
+ else
54
+ json[key] = record.valueForKey(key)
55
+ end
56
+ end
57
+
58
+ if options[:root] != false
59
+ if options[:root].nil? || options[:root] == true
60
+ root = record.class.to_s.underscore.to_sym
61
+ else
62
+ root = options[:root].to_sym
63
+ end
64
+ temp = {}
65
+ temp[root] = json
66
+ json = temp
67
+ end
68
+ json
69
+ end
70
+
71
+ class << self
72
+
73
+ def get_transforms
74
+ if @transforms.nil?
75
+ @transforms = {}
76
+ if self.superclass.respond_to? :get_transforms
77
+ @transforms.merge!(self.superclass.get_transforms)
78
+ end
79
+ end
80
+ @transforms
81
+ end
82
+
83
+ def register_transform(name, transforms={})
84
+ @transforms = get_transforms
85
+ @transforms[name.to_sym] = transforms
86
+ end
87
+ alias_method :registerTransform, :register_transform
88
+
31
89
  end
32
90
 
91
+ private
92
+
93
+ def load(type, records, data)
94
+ if records.is_a? Array
95
+ if data.is_a? Hash
96
+ data = data[type.to_s.underscore.pluralize]
97
+ end
98
+ records.load(type, transform_data(type, data))
99
+ else
100
+ if data.is_a?(Hash) && data.has_key?(type.to_s.underscore)
101
+ data = data[type.to_s.underscore]
102
+ end
103
+ records.load(data.valueForKey(:id), transform_data(type, data))
104
+ end
105
+ end
106
+
107
+ def transform_data_item(type, data)
108
+ json = {}
109
+ transforms = self.class.get_transforms
110
+
111
+ type.get_class_properties.each do |property|
112
+ key = property[:name].to_sym
113
+ transform = transforms[property[:type]]
114
+ if transform
115
+ json[key] = transform[:deserialize].call(data.valueForKey(key))
116
+ else
117
+ json[key] = data.valueForKey(key)
118
+ end
119
+ end
120
+
121
+ type.get_class_relationships.each do |relationship|
122
+ if relationship[:belongs_to]
123
+ key = "#{relationship[:belongs_to]}_id".to_sym
124
+ elsif relationship[:has_many]
125
+ key = "#{relationship[:has_many].to_s.singularize}_ids".to_sym
126
+ end
127
+ json[key] = data.valueForKey(key)
128
+ end
129
+
130
+ json
131
+ end
132
+
133
+ def transform_data(type, data)
134
+ if data.is_a? Array
135
+ json = []
136
+ data.each do |data_item|
137
+ json << transform_data_item(type, data_item)
138
+ end
139
+ json
140
+ else
141
+ transform_data_item(type, data)
142
+ end
143
+ end
33
144
  end
34
145
 
146
+ Adapter.register_transform(:date, {
147
+ serialize: lambda{|value|
148
+ dateFormatter = NSDateFormatter.alloc.init
149
+ dateFormatter.setDateFormat('yyyy-MM-dd')
150
+ value = dateFormatter.stringFromDate(value)
151
+ },
152
+ deserialize: lambda{|value|
153
+ if value.is_a? NSDate
154
+ value
155
+ else
156
+ dateFormatter = NSDateFormatter.alloc.init
157
+ dateFormatter.setDateFormat('yyyy-MM-dd')
158
+ dateFormatter.dateFromString(value.to_s)
159
+ end
160
+ }
161
+ })
162
+
163
+ Adapter.register_transform(:array, {
164
+ serialize: lambda{|value|
165
+ value.to_a
166
+ },
167
+ deserialize: lambda{|value|
168
+ value.to_a
169
+ }
170
+ })
171
+
172
+ Adapter.register_transform(:integer, {
173
+ serialize: lambda{|value|
174
+ value.to_i
175
+ },
176
+ deserialize: lambda{|value|
177
+ value.to_i
178
+ }
179
+ })
180
+
181
+ Adapter.register_transform(:float, {
182
+ serialize: lambda{|value|
183
+ value.to_f
184
+ },
185
+ deserialize: lambda{|value|
186
+ value.to_f
187
+ }
188
+ })
189
+
190
+ Adapter.register_transform(:string, {
191
+ serialize: lambda{|value|
192
+ value.to_s
193
+ },
194
+ deserialize: lambda{|value|
195
+ value.to_s
196
+ }
197
+ })
198
+
35
199
  end
@@ -11,15 +11,16 @@ module Loco
11
11
  end
12
12
 
13
13
  def find(record, id, &block)
14
+ type = record.class
14
15
  error = Pointer.new(:id)
15
- file = File.read(File.join(NSBundle.mainBundle.resourcePath, "fixtures", "#{record.class.to_s.underscore.pluralize}.json"))
16
+ file = File.read(File.join(NSBundle.mainBundle.resourcePath, "fixtures", "#{type.to_s.underscore.pluralize}.json"))
16
17
  data = NSJSONSerialization.JSONObjectWithData(file.to_data, options:JSON_OPTIONS, error:error).find{|obj| obj[:id] == id }
17
18
  if data
18
- record.load(id, data)
19
+ load(type, record, data)
19
20
  block.call(record) if block.is_a? Proc
20
21
  record
21
22
  else
22
- raise Loco::FixtureAdapter::RecordNotFound, "#{record.class} with the id `#{id}' could not be loaded."
23
+ raise Loco::FixtureAdapter::RecordNotFound, "#{type} with the id `#{id}' could not be loaded."
23
24
  end
24
25
  end
25
26
 
@@ -27,7 +28,7 @@ module Loco
27
28
  error = Pointer.new(:id)
28
29
  file = File.read(File.join(NSBundle.mainBundle.resourcePath, "fixtures", "#{type.to_s.underscore.pluralize}.json"))
29
30
  data = NSJSONSerialization.JSONObjectWithData(file.to_data, options:JSON_OPTIONS, error:error)
30
- records.load(type, data)
31
+ load(type, records, data)
31
32
  block.call(records) if block.is_a? Proc
32
33
  records
33
34
  end
@@ -38,7 +39,7 @@ module Loco
38
39
  data = NSJSONSerialization.JSONObjectWithData(file.to_data, options:JSON_OPTIONS, error:error).select{|obj|
39
40
  ids.map(&:to_s).include?(obj[:id].to_s)
40
41
  }
41
- records.load(type, data)
42
+ load(type, records, data)
42
43
  block.call(records) if block.is_a? Proc
43
44
  records
44
45
  end
@@ -53,13 +54,13 @@ module Loco
53
54
  end
54
55
  match
55
56
  }
56
- records.load(type, data)
57
+ load(type, records, data)
57
58
  block.call(records) if block.is_a? Proc
58
59
  records
59
60
  end
60
61
 
61
- def save_record(record, &block)
62
- raise NoMethodError, "Loco::FixtureAdapter cannot save records."
62
+ def update_record(record, &block)
63
+ raise NoMethodError, "Loco::FixtureAdapter cannot update records."
63
64
  end
64
65
 
65
66
  def delete_record(record, &block)
@@ -1,12 +1,181 @@
1
1
  motion_require 'observable'
2
- motion_require 'savable'
2
+ motion_require 'record_array'
3
3
 
4
4
  module Loco
5
5
 
6
6
  class Model
7
7
  include Observable
8
- include Savable
9
- property :id
8
+ property :id, :integer
9
+
10
+ def destroy(&block)
11
+ adapter = self.class.get_class_adapter
12
+ unless self.new?
13
+ adapter.delete_record(self) do |record|
14
+ block.call(record) if block.is_a? Proc
15
+ end
16
+ end
17
+ end
18
+
19
+ def load(id, data)
20
+ data.merge!({ id: id })
21
+ self.set_properties(data)
22
+ self
23
+ end
24
+
25
+ def new?
26
+ self.id.nil?
27
+ end
28
+
29
+ def save(&block)
30
+ adapter = self.class.get_class_adapter
31
+ if self.new?
32
+ adapter.create_record(self) do |record|
33
+ block.call(record) if block.is_a? Proc
34
+ end
35
+ else
36
+ adapter.update_record(self) do |record|
37
+ block.call(record) if block.is_a? Proc
38
+ end
39
+ end
40
+ end
41
+
42
+ def serialize(options={})
43
+ self.class.get_class_adapter.serialize(self, options)
44
+ end
45
+
46
+ class << self
47
+
48
+ def adapter(adapter_class, *args)
49
+ if adapter_class.is_a? String
50
+ @adapter = adapter_class.constantize.new(*args)
51
+ else
52
+ @adapter = adapter_class.new(*args)
53
+ end
54
+ end
55
+
56
+ def belongs_to(model)
57
+ attr_accessor model
58
+ attr_accessor "#{model}_id"
59
+
60
+ belongs_to_class_name = model.to_s.classify
61
+
62
+ define_method "#{model}" do |&block|
63
+ record = instance_variable_get("@#{model}")
64
+ if record
65
+ block.call(record) if block.is_a? Proc
66
+ record
67
+ else
68
+ belongs_to_id = self.send("#{model}_id")
69
+ if belongs_to_id
70
+ belongs_to_class_name.constantize.find(belongs_to_id) do |record|
71
+ instance_variable_set("@#{model}", record)
72
+ block.call(record) if block.is_a? Proc
73
+ end
74
+ else
75
+ block.call(record) if block.is_a? Proc
76
+ record
77
+ end
78
+ end
79
+ end
80
+
81
+ define_method "#{model}=" do |record|
82
+ raise TypeError, "Expecting a #{belongs_to_class_name} as defined by #belongs_to :#{model}" unless record.is_a? belongs_to_class_name.constantize
83
+ self.send("#{model}_id=", (record.nil? ? nil : record.id))
84
+ instance_variable_set("@#{model}", record)
85
+ record
86
+ end
87
+
88
+ relationships = get_class_relationships
89
+ relationships << { belongs_to: model }
90
+ end
91
+
92
+ def has_many(model)
93
+ attr_accessor model
94
+ attr_accessor "#{model.to_s.singularize}_ids"
95
+
96
+ has_many_class_name = model.to_s.singularize.classify
97
+
98
+ define_method "#{model}" do |&block|
99
+ has_many_class = has_many_class_name.constantize
100
+ records = instance_variable_get("@#{model}")
101
+ if records
102
+ block.call(records) if block.is_a? Proc
103
+ records
104
+ else
105
+ has_many_ids = self.send("#{model.to_s.singularize}_ids")
106
+ if has_many_ids
107
+ has_many_class.find(has_many_ids) do |records|
108
+ block.call(records) if block.is_a? Proc
109
+ end
110
+ else
111
+ query = {}
112
+ query["#{self.class.to_s.underscore.singularize}_id"] = self.id
113
+ has_many_class.find(query) do |records|
114
+ block.call(records) if block.is_a? Proc
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ define_method "#{model}=" do |records|
121
+ has_many_class = has_many_class_name.constantize
122
+ if (records.is_a?(RecordArray) || records.is_a?(Array)) && (records.length == 0 || (records.length > 0 && records.first.class == has_many_class))
123
+ unless records.is_a?(RecordArray)
124
+ record_array = RecordArray.new
125
+ record_array.addObjectsFromArray(records)
126
+ records = record_array
127
+ end
128
+ else
129
+ raise TypeError, "Expecting a Loco::RecordArray of #{has_many_class_name} objects as defined by #has_many :#{model}"
130
+ end
131
+
132
+ self.send("#{model.to_s.singularize}_ids=", records.map(&:id))
133
+ instance_variable_set("@#{model}", records)
134
+ end
135
+
136
+ relationships = get_class_relationships
137
+ relationships << { has_many: model }
138
+ end
139
+
140
+ def find(id=nil, &block)
141
+ adapter = self.get_class_adapter
142
+ if id.nil?
143
+ # Return all records
144
+ records = RecordArray.new
145
+ adapter.find_all(self, records) do |records|
146
+ block.call(records) if block.is_a? Proc
147
+ end
148
+ elsif id.is_a? Array
149
+ # Return records with given ids
150
+ records = RecordArray.new
151
+ id.each do |i|
152
+ records << self.new(id: i)
153
+ end
154
+ adapter.find_many(self, records, id) do |records|
155
+ block.call(records) if block.is_a? Proc
156
+ end
157
+ elsif id.is_a? Hash
158
+ # Return records matching query
159
+ records = RecordArray.new
160
+ adapter.find_query(self, records, id) do |records|
161
+ block.call(records) if block.is_a? Proc
162
+ end
163
+ else
164
+ record = self.new(id: id)
165
+ adapter.find(record, id) do |record|
166
+ block.call(record) if block.is_a? Proc
167
+ end
168
+ end
169
+ end
170
+ alias_method :all, :find
171
+ alias_method :where, :find
172
+
173
+ def get_class_adapter
174
+ @adapter ||= Adapter.new
175
+ end
176
+
177
+ end
178
+
10
179
  end
11
180
 
12
181
  end