volt 0.9.6 → 0.9.7.pre2

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.
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