volt 0.9.6 → 0.9.7.pre2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/CHANGELOG.md +26 -0
  4. data/Gemfile +6 -1
  5. data/README.md +2 -0
  6. data/Rakefile +1 -0
  7. data/app/volt/models/active_volt_instance.rb +8 -6
  8. data/app/volt/models/user.rb +11 -2
  9. data/app/volt/models/volt_app_property.rb +8 -0
  10. data/app/volt/tasks/query_tasks.rb +23 -31
  11. data/app/volt/tasks/store_tasks.rb +2 -2
  12. data/app/volt/tasks/volt_admin_tasks.rb +24 -0
  13. data/docs/UPGRADE_GUIDE.md +6 -0
  14. data/lib/volt.rb +19 -12
  15. data/lib/volt/boot.rb +1 -0
  16. data/lib/volt/cli.rb +19 -8
  17. data/lib/volt/cli/console.rb +0 -1
  18. data/lib/volt/cli/generators.rb +14 -3
  19. data/lib/volt/cli/migrate.rb +26 -0
  20. data/lib/volt/config.rb +17 -4
  21. data/lib/volt/controllers/http_controller.rb +12 -0
  22. data/lib/volt/data_stores/base_adaptor_client.rb +2 -2
  23. data/lib/volt/data_stores/base_adaptor_server.rb +2 -0
  24. data/lib/volt/data_stores/data_store.rb +20 -14
  25. data/lib/volt/extra_core/class.rb +28 -14
  26. data/lib/volt/extra_core/hash.rb +5 -0
  27. data/lib/volt/extra_core/string.rb +3 -1
  28. data/lib/volt/helpers/time.rb +9 -43
  29. data/lib/volt/helpers/time/calculations.rb +204 -0
  30. data/lib/volt/helpers/time/distance.rb +63 -0
  31. data/lib/volt/helpers/time/duration.rb +71 -0
  32. data/lib/volt/helpers/time/local_calculations.rb +49 -0
  33. data/lib/volt/helpers/time/local_volt_time.rb +23 -0
  34. data/lib/volt/helpers/time/numeric.rb +59 -0
  35. data/lib/volt/helpers/time/volt_time.rb +170 -0
  36. data/lib/volt/models.rb +5 -0
  37. data/lib/volt/models/array_model.rb +33 -6
  38. data/lib/volt/models/associations.rb +146 -23
  39. data/lib/volt/models/buffer.rb +38 -41
  40. data/lib/volt/models/cursor.rb +15 -0
  41. data/lib/volt/models/errors.rb +11 -0
  42. data/lib/volt/models/field_helpers.rb +108 -68
  43. data/lib/volt/models/helpers/array_model.rb +4 -0
  44. data/lib/volt/models/helpers/base.rb +8 -1
  45. data/lib/volt/models/helpers/change_helpers.rb +31 -12
  46. data/lib/volt/models/helpers/defaults.rb +15 -0
  47. data/lib/volt/models/location.rb +20 -6
  48. data/lib/volt/models/migrations/migration.rb +23 -0
  49. data/lib/volt/models/migrations/migration_runner.rb +146 -0
  50. data/lib/volt/models/model.rb +38 -1
  51. data/lib/volt/models/permissions.rb +8 -1
  52. data/lib/volt/models/persistors/array_store.rb +87 -8
  53. data/lib/volt/models/persistors/base.rb +19 -0
  54. data/lib/volt/models/persistors/model_store.rb +1 -1
  55. data/lib/volt/models/persistors/page.rb +4 -1
  56. data/lib/volt/models/persistors/query/query_identifier.rb +102 -0
  57. data/lib/volt/models/persistors/query/query_listener.rb +57 -12
  58. data/lib/volt/models/root_models/root_models.rb +19 -0
  59. data/lib/volt/models/url.rb +11 -2
  60. data/lib/volt/models/validations/validations.rb +5 -2
  61. data/lib/volt/models/validators/type_validator.rb +11 -0
  62. data/lib/volt/models/validators/unique_validator.rb +2 -2
  63. data/lib/volt/page/bindings/attribute_binding.rb +23 -1
  64. data/lib/volt/page/targets/attribute_section.rb +7 -0
  65. data/lib/volt/page/targets/binding_document/component_node.rb +44 -18
  66. data/lib/volt/page/targets/binding_document/tag_node.rb +41 -0
  67. data/lib/volt/page/tasks.rb +16 -8
  68. data/lib/volt/queries/live_query.rb +109 -0
  69. data/lib/volt/queries/live_query_pool.rb +58 -0
  70. data/lib/volt/queries/live_subquery.rb +0 -0
  71. data/lib/volt/queries/query_association_splitter.rb +31 -0
  72. data/lib/volt/queries/query_diff.rb +100 -0
  73. data/lib/volt/queries/query_runner.rb +110 -0
  74. data/lib/volt/queries/query_subscription.rb +80 -0
  75. data/lib/volt/queries/query_subscription_pool.rb +37 -0
  76. data/lib/volt/reactive/eventable.rb +8 -0
  77. data/lib/volt/reactive/reactive_array.rb +0 -4
  78. data/lib/volt/router/routes.rb +81 -31
  79. data/lib/volt/server/message_bus/base_message_bus.rb +9 -3
  80. data/lib/volt/server/message_bus/peer_to_peer.rb +6 -6
  81. data/lib/volt/server/message_bus/peer_to_peer/server_tracker.rb +1 -1
  82. data/lib/volt/server/middleware/default_middleware_stack.rb +12 -8
  83. data/lib/volt/server/rack/component_paths.rb +31 -4
  84. data/lib/volt/server/rack/http_content_types.rb +62 -0
  85. data/lib/volt/server/rack/http_resource.rb +1 -1
  86. data/lib/volt/server/rack/index_files.rb +8 -1
  87. data/lib/volt/server/rack/opal_files.rb +16 -1
  88. data/lib/volt/server/rack/sprockets_helpers_setup.rb +32 -1
  89. data/lib/volt/server/socket_connection_handler.rb +16 -7
  90. data/lib/volt/server/template_handlers/sprockets_component_handler.rb +5 -3
  91. data/lib/volt/spec/capybara.rb +4 -3
  92. data/lib/volt/spec/setup.rb +5 -0
  93. data/lib/volt/tasks/dispatcher.rb +3 -1
  94. data/lib/volt/utils/data_transformer.rb +4 -4
  95. data/lib/volt/utils/ejson.rb +19 -6
  96. data/lib/volt/utils/promise_extensions.rb +1 -1
  97. data/lib/volt/utils/time_opal_patch.rb +749 -0
  98. data/lib/volt/utils/time_patch.rb +11 -4
  99. data/lib/volt/version.rb +1 -1
  100. data/lib/volt/volt/app.rb +19 -11
  101. data/lib/volt/volt/properties.rb +24 -0
  102. data/lib/volt/volt/server_setup/app.rb +30 -7
  103. data/lib/volt/volt/users.rb +15 -3
  104. data/spec/apps/kitchen_sink/Gemfile +5 -1
  105. data/spec/apps/kitchen_sink/app/main/config/routes.rb +1 -0
  106. data/spec/apps/kitchen_sink/app/main/controllers/save_controller.rb +1 -1
  107. data/spec/apps/kitchen_sink/app/main/controllers/server/simple_http_controller.rb +4 -0
  108. data/spec/apps/kitchen_sink/app/main/controllers/todos_controller.rb +4 -2
  109. data/spec/apps/kitchen_sink/app/main/models/post.rb +0 -1
  110. data/spec/apps/kitchen_sink/app/main/models/todo.rb +4 -0
  111. data/spec/apps/kitchen_sink/app/main/views/mailers/reset_password.html +10 -0
  112. data/spec/apps/kitchen_sink/app/main/views/todos/index.html +2 -0
  113. data/spec/apps/kitchen_sink/config/app.rb +2 -0
  114. data/spec/apps/migrations/config/db/migrations/1445111704_migration1.rb +7 -0
  115. data/spec/apps/migrations/config/db/migrations/1445113517_migration2.rb +7 -0
  116. data/spec/apps/migrations/config/db/migrations/1445115200_migration3.rb +7 -0
  117. data/spec/extra_core/class_spec.rb +10 -0
  118. data/spec/helpers/distance_spec.rb +35 -0
  119. data/spec/helpers/duration_spec.rb +160 -0
  120. data/spec/helpers/volt_time_spec.rb +275 -0
  121. data/spec/integration/callbacks_spec.rb +2 -1
  122. data/spec/integration/http_endpoints_spec.rb +4 -0
  123. data/spec/integration/save_spec.rb +1 -1
  124. data/spec/integration/todos_spec.rb +7 -5
  125. data/spec/models/array_model_spec.rb +17 -3
  126. data/spec/models/associations_spec.rb +48 -1
  127. data/spec/models/field_helpers_spec.rb +7 -3
  128. data/spec/models/migrations/migration_runner_spec.rb +69 -0
  129. data/spec/models/model_spec.rb +42 -8
  130. data/spec/models/permissions_spec.rb +20 -8
  131. data/spec/models/persistors/array_store_spec.rb +18 -0
  132. data/spec/models/persistors/page_spec.rb +15 -10
  133. data/spec/models/persistors/store_spec.rb +13 -3
  134. data/spec/models/url_spec.rb +4 -3
  135. data/spec/models/user_spec.rb +6 -3
  136. data/spec/models/user_validation_spec.rb +3 -3
  137. data/spec/models/validations_spec.rb +4 -0
  138. data/spec/models/validators/block_validations_spec.rb +9 -5
  139. data/spec/models/validators/email_validator_spec.rb +2 -0
  140. data/spec/models/validators/lifecycle_callbacks_spec.rb +86 -0
  141. data/spec/models/validators/unique_validator_spec.rb +1 -0
  142. data/spec/page/path_string_renderer_spec.rb +5 -0
  143. data/spec/queries/live_query_spec.rb +16 -0
  144. data/spec/queries/query_association_splitter_spec.rb +14 -0
  145. data/spec/queries/query_diff_spec.rb +132 -0
  146. data/spec/queries/query_identifier_spec.rb +98 -0
  147. data/spec/queries/query_runner_spec.rb +63 -0
  148. data/spec/queries/query_tracker_spec.rb +141 -0
  149. data/spec/router/routes_spec.rb +52 -21
  150. data/spec/server/middleware/rack_content_types_spec.rb +78 -0
  151. data/spec/server/rack/asset_files_spec.rb +38 -30
  152. data/spec/spec_helper.rb +8 -0
  153. data/spec/utils/ejson_spec.rb +9 -8
  154. data/spec/utils/ejson_volt_time_spec.rb +65 -0
  155. data/templates/migration/migration.rb.tt +9 -0
  156. data/templates/newgem/gitignore.tt +1 -0
  157. data/templates/project/Gemfile.tt +19 -2
  158. data/templates/project/README.md.tt +6 -1
  159. data/templates/project/app/main/config/dependencies.rb +6 -0
  160. data/templates/project/config/app.rb.tt +18 -4
  161. data/volt.gemspec +2 -2
  162. metadata +73 -16
  163. data/app/volt/tasks/live_query/live_query_pool.rb +0 -48
  164. data/app/volt/tasks/live_query/query_tracker.rb +0 -92
  165. data/spec/tasks/live_query_spec.rb +0 -18
  166. data/spec/tasks/query_tasks.rb +0 -7
  167. data/spec/tasks/query_tracker_spec.rb +0 -145
@@ -156,7 +156,7 @@ module Volt
156
156
  if can_delete
157
157
  super(val)
158
158
  else
159
- Promise.new.reject("permissions did not allow delete for #{val.inspect}.")
159
+ Promise.new.reject("permissions did not allow delete for #{val.inspect}.")
160
160
  end
161
161
  end
162
162
  end
@@ -178,12 +178,28 @@ module Volt
178
178
 
179
179
  # Return the first item in the collection, or create one if one does not
180
180
  # exist yet.
181
- def first_or_create
181
+ def first_or_create(attrs={})
182
182
  first.then do |item|
183
183
  if item
184
184
  item
185
185
  else
186
- create
186
+ create(attrs)
187
+ end
188
+ end
189
+ end
190
+
191
+ # Finds a model maching query and calls .update passing in attrs. If the
192
+ # model is not found, it creates the model with attrs merged into query.
193
+ def update_or_create(query, attrs={})
194
+ where(query).first.then do |item|
195
+ if item
196
+ if attrs.size > 0
197
+ item.update(attrs)
198
+ else
199
+ item
200
+ end
201
+ else
202
+ create(query.merge(attrs))
187
203
  end
188
204
  end
189
205
  end
@@ -241,10 +257,13 @@ module Volt
241
257
  def new_model(*args)
242
258
  Volt::Model.class_at_path(options[:path]).new(*args)
243
259
  end
260
+
244
261
  alias_method :new, :new_model
245
262
 
246
263
  def new_array_model(*args)
247
- Volt::ArrayModel.class_at_path(options[:path]).new(*args)
264
+ klass = Volt::ArrayModel.class_at_path(options[:path])
265
+
266
+ klass.new(*args)
248
267
  end
249
268
 
250
269
  # Convert the model to an array all of the way down
@@ -309,10 +328,15 @@ module Volt
309
328
 
310
329
  alias_method :reactive_count, :count
311
330
  def count(&block)
312
- all.reactive_count(&block)
331
+ if !block && persistor.is_a?(Persistors::ArrayStore)
332
+ @persistor.resolved_value
333
+ else
334
+ all.reactive_count(&block)
335
+ end
313
336
  end
314
337
 
315
338
  private
339
+
316
340
  # called form <<, append, and create. If a hash is passed in, it converts
317
341
  # it to a model. Then it takes the model and inserts it into the ArrayModel
318
342
  # then persists it.
@@ -328,13 +352,15 @@ module Volt
328
352
  model = wrap_values([model]).first
329
353
  end
330
354
 
331
-
332
355
  if model.is_a?(Model)
333
356
  promise = model.can_create?.then do |can_create|
334
357
  unless can_create
335
358
  fail "permissions did not allow create for #{model.inspect}"
336
359
  end
337
360
  end.then do
361
+ if (assoc = options[:associate])
362
+ parent.associate(assoc, model)
363
+ end
338
364
 
339
365
  # Add it to the array and trigger any watches or on events.
340
366
  reactive_array_append(model)
@@ -345,6 +371,7 @@ module Volt
345
371
  # Validate and save
346
372
  model.run_changed
347
373
  end.then do
374
+
348
375
  # Mark the model as not new
349
376
  model.instance_variable_set('@new', false)
350
377
 
@@ -2,12 +2,12 @@ module Volt
2
2
  module Associations
3
3
  module ClassMethods
4
4
  def belongs_to(method_name, options = {})
5
- collection ||= options.fetch(:collection, method_name).pluralize
6
- foreign_key ||= options.fetch(:foreign_key, :id)
7
- local_key ||= options.fetch(:local_key, "#{method_name}_id")
5
+ collection, foreign_key, local_key = assoc_parts_and_track(
6
+ method_name, options, :belongs_to
7
+ )
8
8
 
9
9
  # Add a field for the association_id
10
- field(local_key)
10
+ field(local_key, String)
11
11
 
12
12
  # getter
13
13
  define_method(method_name) do
@@ -20,43 +20,76 @@ module Volt
20
20
  end
21
21
  end
22
22
 
23
+ # setter
23
24
  define_method(:"#{method_name}=") do |obj|
24
- id = obj.is_a?(Fixnum) ? obj : obj.id
25
+ # Associatie the obj's foreign key
26
+ obj.set(foreign_key, id)
25
27
 
26
- # Assign the current model's something_id to the object's id
27
- set(local_key, id)
28
+ # Associate on the method name
29
+ set(method_name, obj)
28
30
  end
29
31
  end
30
32
 
31
33
  def has_many(method_name, options = {})
32
- collection ||= options.fetch(:collection, method_name).pluralize
34
+ mmethod_name = method_name.to_sym
35
+ if method_name.singular?
36
+ raise NameError, "has_many takes a plural association name"
37
+ end
33
38
 
34
- # Use the underscored current class name as the something_id.
35
- foreign_key ||= options.fetch(:foreign_key, "#{to_s.underscore.singularize}_id")
36
- local_key ||= options.fetch(:local_key, :id)
39
+ collection, foreign_key, local_key = assoc_parts_and_track(
40
+ method_name, options, :has_many
41
+ )
37
42
 
38
43
  define_method(method_name) do
39
- lookup_key = get(local_key)
40
- array_model = root.get(collection).where(foreign_key => lookup_key)
41
-
42
- # Since we are coming off of the root collection, we need to setup
43
- # the right parent and path.
44
- new_path = array_model.options[:path]
45
- # Assign path and parent
46
- array_model.path = self.path + new_path
47
- array_model.parent = self
44
+ # If the association is already in attributes, return
45
+ if attributes[method_name]
46
+ get(method_name)
47
+ else
48
48
 
49
- array_model
49
+ # Get the
50
+ lookup_key = get(local_key)
51
+ array_model = root.get(collection).where(foreign_key => lookup_key)
52
+
53
+ # Since we are coming off of the root collection, we need to setup
54
+ # the right parent and path.
55
+ new_path = array_model.options[:path]
56
+ # Assign path and parent
57
+ array_model.path = self.path + new_path
58
+ array_model.parent = self
59
+
60
+ # Store the associated query, don't track changes, since the
61
+ # association is persisted via _id fields.
62
+ Volt.run_in_mode(:no_change_tracking) do
63
+ set(method_name, array_model)
64
+ end
65
+ array_model
66
+ end
50
67
  end
68
+
69
+ # setter
70
+ # define_method(:"#{method_name}=") do |val|
71
+ # assoc = get(method_name)
72
+
73
+ # # Set the foreign_key on the has_many model to the local_key of the
74
+ # # current model.
75
+ # assoc.append(val).then do |model|
76
+ # model.set(foreign_key, get(local_key))
77
+ # end
78
+ # end
51
79
  end
52
80
 
53
81
  # has_one creates a method on the Volt::Model that returns a promise
54
82
  # to get the associated model.
55
- def has_one(method_name)
83
+ def has_one(method_name, options={})
56
84
  if method_name.plural?
57
85
  raise NameError, "has_one takes a singluar association name"
58
86
  end
59
87
 
88
+ collection, foreign_key, local_key = assoc_parts_and_track(
89
+ method_name, options, :has_one
90
+ )
91
+
92
+
60
93
  define_method(method_name) do
61
94
  association_with_root_model('has_one') do |root|
62
95
  key = self.class.to_s.underscore + '_id'
@@ -64,10 +97,100 @@ module Volt
64
97
  end
65
98
  end
66
99
  end
100
+
101
+
102
+ def assoc_parts(method_name, options, type)
103
+ collection = options.fetch(:collection, method_name).pluralize
104
+
105
+ default_foreign_key = case type
106
+ when :has_many, :has_one
107
+ :"#{to_s.underscore.singularize}_id"
108
+ else
109
+ :id
110
+ end
111
+ foreign_key = options.fetch(:foreign_key, default_foreign_key)
112
+
113
+ default_local_key = case type
114
+ when :belongs_to
115
+ :"#{method_name}_id"
116
+ else
117
+ :id
118
+ end
119
+
120
+ local_key = options.fetch(:local_key, default_local_key)
121
+
122
+ return collection, foreign_key, local_key
123
+ end
124
+
125
+ private
126
+ def check_name_in_use(name)
127
+ if self.fields[name]
128
+ type = 'A field'
129
+ elsif self.associations[name]
130
+ type = 'An association'
131
+ else
132
+ type = nil
133
+ end
134
+
135
+ if type
136
+ raise "#{type} is already defined for `#{name}` on `#{to_s}`"
137
+ end
138
+ end
139
+
140
+ # Checks to make sure the association isn't in use, then generates the
141
+ # collection, foreign_key, and local_key's based on the type and options.
142
+ # Then tracks the association data on the model class.
143
+ def assoc_parts_and_track(method_name, options, type)
144
+ method_name = method_name.to_sym
145
+ check_name_in_use(method_name)
146
+
147
+ collection, foreign_key, local_key = assoc_parts(method_name, options,
148
+ type)
149
+
150
+ # Track the association
151
+ self.associations[method_name] = {
152
+ type: type,
153
+ to_many: type == :has_many,
154
+ collection: collection,
155
+ foreign_key: foreign_key,
156
+ local_key: local_key
157
+ }
158
+
159
+ return collection, foreign_key, local_key
160
+ end
67
161
  end
68
162
 
69
163
  def self.included(base)
70
164
  base.send :extend, ClassMethods
165
+ base.class_attribute :associations
166
+ base.associations = {}
167
+ end
168
+
169
+ # Associate takes an association name, and a model and changes the
170
+ # association field (the foreign_key for has_one, has_many, or the local_key
171
+ # for belongs_to). This works for both explicit (has_many, has_one,
172
+ # belongs_to) associations, and implicit (._items)
173
+ def associate(method_name, model)
174
+ assoc_data = self.class.associations[method_name]
175
+
176
+ if assoc_data
177
+ # Extract from association data
178
+ collection, foreign_key, local_key, type =
179
+ assoc_data.mfetch(:collection, :foreign_key, :local_key, :type)
180
+ else
181
+ # Association is implicit, generate instead
182
+ type = :has_many
183
+ collection, foreign_key, local_key =
184
+ self.class.assoc_parts(method_name, {}, type)
185
+ end
186
+
187
+ case type
188
+ when :has_many, :has_one
189
+ model.set(foreign_key, get(local_key))
190
+ else
191
+ # belongs_to
192
+ set(local_key, get(foreign_key))
193
+ end
71
194
  end
72
195
 
73
196
  private
@@ -79,7 +202,7 @@ module Volt
79
202
 
80
203
  # Check if we are on the store collection
81
204
  if persistor.is_a?(Volt::Persistors::ModelStore) ||
82
- persistor.is_a?(Volt::Persistors::Page)
205
+ persistor.is_a?(Volt::Persistors::Page)
83
206
  # Get the root node
84
207
  root = persistor.try(:root_model)
85
208
 
@@ -10,60 +10,57 @@ module Volt
10
10
  # TODO: this shouldn't need to be run, but if no attributes are assigned, then
11
11
  # if needs to be run. Maybe there's a better way to handle it.
12
12
  validate!.then do
13
- # Get errors from validate
14
- errors = self.errors.to_h
15
-
16
13
  result = nil
17
14
 
18
- if errors.size == 0
19
- # cache save_to in a local
20
- save_to = self.save_to
21
- if save_to
22
- if save_to.is_a?(ArrayModel)
23
- # Add to the collection
24
- promise = save_to.create(attributes)
25
- else
26
- # We have a saved model
27
- promise = save_to.assign_attributes(attributes)
28
- end
29
-
30
- result = promise.then do |new_model|
31
- # The main model saved, so mark the buffer as not new
32
- @new = false
15
+ # cache save_to in a local
16
+ save_to = self.save_to
17
+ if save_to
18
+ if save_to.is_a?(ArrayModel)
19
+ # Add to the collection
20
+ promise = save_to.create(attributes)
21
+ else
22
+ # We have a saved model
23
+ promise = save_to.assign_attributes(attributes, false, true)
24
+ end
33
25
 
34
- if new_model
35
- # Mark the model as loaded
36
- new_model.change_state_to(:loaded_state, :loaded)
26
+ result = promise.then do |new_model|
27
+ # The main model saved, so mark the buffer as not new
28
+ @new = false
37
29
 
38
- # Set the buffer's id to track the main model's id
39
- self.save_to = new_model
40
- end
30
+ if new_model
31
+ # Mark the model as loaded
32
+ new_model.change_state_to(:loaded_state, :loaded)
41
33
 
42
- # Copy attributes back from save_to model
43
- @attributes = new_model.attributes
34
+ # Set the buffer's id to track the main model's id
35
+ self.save_to = new_model
36
+ end
44
37
 
45
- # Remove tracked changes
46
- clear_tracked_changes!
38
+ # Copy attributes back from save_to model
39
+ @attributes = new_model.attributes
47
40
 
48
- new_model
49
- end.fail do |errors|
50
- if errors.is_a?(Hash)
51
- server_errors.replace(errors)
41
+ # Remove tracked changes
42
+ clear_tracked_changes!
52
43
 
53
- # Merge the server errors into the main errors
54
- self.errors.merge!(server_errors.to_h)
55
- end
44
+ new_model
45
+ end.fail do |errors|
46
+ if errors.is_a?(Hash)
47
+ server_errors.replace(errors)
56
48
 
57
- promise_for_errors(errors)
49
+ # Merge the server errors into the main errors
50
+ self.errors.merge!(server_errors.to_h)
58
51
  end
59
- else
60
- fail 'Model is not a buffer, can not be saved, modifications should be persisted as they are made.'
52
+
53
+ promise_for_errors(errors)
61
54
  end
55
+
56
+ result
62
57
  else
63
- # Some errors, mark all fields
64
- result = promise_for_errors(errors)
58
+ fail 'Model is not a buffer, can not be saved, modifications should be persisted as they are made.'
65
59
  end
66
-
60
+ end.fail do |errors|
61
+ # Some errors, mark all fields
62
+ promise_for_errors(errors)
63
+ end.then do |result|
67
64
  # If passed a block, call then on it with the block.
68
65
  result = result.then(&block) if block
69
66
 
@@ -2,5 +2,20 @@ require 'volt/models/array_model'
2
2
 
3
3
  module Volt
4
4
  class Cursor < ArrayModel
5
+ # Some cursors return a value instead of an ArrayModel, in this case, we
6
+ # store the array in the ArrayModel (so we can reuse ArrayModel's path)
7
+ # TODO: should abstract this into a base class.
8
+ def value=(val)
9
+ @array = val
10
+ @has_value = true
11
+ end
12
+
13
+ def value
14
+ @array
15
+ end
16
+
17
+ def has_value?
18
+ @has_value
19
+ end
5
20
  end
6
21
  end
@@ -28,5 +28,16 @@ module Volt
28
28
 
29
29
  str.join(', ')
30
30
  end
31
+
32
+ # message returns a human readable string for the errors
33
+ def message
34
+ str = []
35
+
36
+ each_pair do |field, error|
37
+ str << "#{field} #{error}"
38
+ end
39
+
40
+ str.to_sentence
41
+ end
31
42
  end
32
43
  end