motion-loco 0.1.3 → 0.2.0

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