volt 0.8.8 → 0.8.9

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/Readme.md +2 -1036
  4. data/VERSION +1 -1
  5. data/app/volt/models/user.rb +5 -0
  6. data/app/volt/tasks/query_tasks.rb +3 -3
  7. data/app/volt/tasks/store_tasks.rb +17 -15
  8. data/app/volt/tasks/user_tasks.rb +6 -0
  9. data/lib/volt/cli.rb +9 -0
  10. data/lib/volt/extra_core/string.rb +10 -2
  11. data/lib/volt/models/array_model.rb +0 -1
  12. data/lib/volt/models/model.rb +45 -28
  13. data/lib/volt/models/model_hash_behaviour.rb +16 -4
  14. data/lib/volt/models/model_helpers.rb +4 -2
  15. data/lib/volt/models/model_wrapper.rb +2 -1
  16. data/lib/volt/models/persistors/array_store.rb +6 -6
  17. data/lib/volt/models/persistors/local_store.rb +1 -1
  18. data/lib/volt/models/persistors/model_store.rb +3 -3
  19. data/lib/volt/models/persistors/query/query_listener.rb +6 -4
  20. data/lib/volt/models/persistors/query/query_listener_pool.rb +9 -0
  21. data/lib/volt/models/persistors/store.rb +1 -0
  22. data/lib/volt/models/url.rb +20 -10
  23. data/lib/volt/page/bindings/template_binding.rb +7 -1
  24. data/lib/volt/page/page.rb +3 -3
  25. data/lib/volt/page/tasks.rb +20 -19
  26. data/lib/volt/reactive/reactive_array.rb +44 -0
  27. data/lib/volt/router/routes.rb +5 -0
  28. data/lib/volt/server.rb +3 -1
  29. data/lib/volt/server/component_templates.rb +7 -1
  30. data/lib/volt/server/html_parser/attribute_scope.rb +1 -1
  31. data/lib/volt/server/rack/component_paths.rb +1 -10
  32. data/lib/volt/server/socket_connection_handler.rb +3 -0
  33. data/lib/volt/tasks/dispatcher.rb +3 -7
  34. data/lib/volt/tasks/task_handler.rb +41 -0
  35. data/spec/integration/bindings_spec.rb +1 -1
  36. data/spec/models/model_spec.rb +32 -32
  37. data/spec/models/persistors/params_spec.rb +1 -1
  38. data/spec/router/routes_spec.rb +4 -4
  39. data/templates/model/model.rb.tt +3 -0
  40. metadata +6 -2
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.8
1
+ 0.8.9
@@ -0,0 +1,5 @@
1
+ class User < Model
2
+ def password=(val)
3
+ self._password = '--encoded: ' + val
4
+ end
5
+ end
@@ -1,7 +1,7 @@
1
1
  require_relative 'live_query/data_store'
2
2
  require_relative 'live_query/live_query_pool'
3
3
 
4
- class QueryTasks
4
+ class QueryTasks < TaskHandler
5
5
  @@live_query_pool = LiveQueryPool.new(DataStore.new)
6
6
  @@channel_live_queries = {}
7
7
 
@@ -32,12 +32,12 @@ class QueryTasks
32
32
  error = {:error => exception.message}
33
33
  end
34
34
 
35
- return initial_data, error
35
+ return [initial_data, error]
36
36
  end
37
37
 
38
38
  def initial_data
39
39
  data = live_query.initial_data
40
- data['_id'] = data['_id'].to_s
40
+ data[:_id] = data[:_id].to_s
41
41
 
42
42
  return data
43
43
  end
@@ -1,7 +1,6 @@
1
1
  require 'mongo'
2
- require 'query_tasks'
3
2
 
4
- class StoreTasks
3
+ class StoreTasks < TaskHandler
5
4
  def initialize(channel=nil, dispatcher=nil)
6
5
  @channel = channel
7
6
  @dispatcher = dispatcher
@@ -11,10 +10,11 @@ class StoreTasks
11
10
  @@db ||= Volt::DataStore.fetch
12
11
  end
13
12
 
14
- def model_errors(collection, data)
15
- model_name = collection[1..-1].singularize.camelize
13
+ def model_values_and_errors(collection, data)
14
+ model_name = collection.singularize.camelize
16
15
 
17
16
  # TODO: Security check to make sure we have a valid model
17
+ # and don't load classes we shouldn't
18
18
  begin
19
19
  model_class = Object.send(:const_get, model_name)
20
20
  rescue NameError => e
@@ -22,33 +22,35 @@ class StoreTasks
22
22
  end
23
23
 
24
24
  if model_class
25
- return model_class.new(data).errors
25
+ model = model_class.new(data)
26
+ return model.to_h, model.errors
26
27
  end
27
28
 
28
- return {}
29
+ return data, {}
29
30
  end
30
31
 
31
32
  def save(collection, data)
33
+ puts "SAVE: #{data.inspect}"
32
34
  data = data.symbolize_keys
33
-
34
- errors = model_errors(collection, data)
35
+ values, errors = model_values_and_errors(collection, data)
35
36
 
36
37
  if errors.size == 0
37
- # id = BSON::ObjectId(data[:_id])
38
- id = data[:_id]
38
+ # id = BSON::ObjectId(values[:_id])
39
+ id = values[:_id]
39
40
 
40
41
  # Try to create
41
42
  # TODO: Seems mongo is dumb and doesn't let you upsert with custom id's
42
43
  begin
43
- # data['_id'] = BSON::ObjectId('_id') if data['_id']
44
- db[collection].insert(data)
44
+ # values['_id'] = BSON::ObjectId('_id') if values['_id']
45
+ puts "VALUES: #{values.inspect}"
46
+ db[collection].insert(values)
45
47
  rescue Mongo::OperationFailure => error
46
48
  # Really mongo client?
47
49
  if error.message[/^11000[:]/]
48
50
  # Update because the id already exists
49
- update_data = data.dup
50
- update_data.delete(:_id)
51
- db[collection].update({:_id => id}, update_data)
51
+ update_values = values.dup
52
+ update_values.delete(:_id)
53
+ db[collection].update({:_id => id}, update_values)
52
54
  else
53
55
  return {:error => error.message}
54
56
  end
@@ -0,0 +1,6 @@
1
+ class UserTasks < TaskHandler
2
+ # Login a user, takes a username and password
3
+ def create_user(username, password)
4
+ $page.store._users << {email: username, password: password}
5
+ end
6
+ end
data/lib/volt/cli.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'bundler/setup'
2
2
  require 'thor'
3
+ require 'volt/extra_core/extra_core'
3
4
 
4
5
  class CLI < Thor
5
6
  include Thor::Actions
@@ -77,6 +78,14 @@ class CLI < Thor
77
78
  NewGem.new(self, name, options)
78
79
  end
79
80
 
81
+ desc "model NAME COMPONENT", "Creates a model named NAME in the component named COMPONENT"
82
+ method_option :name, :type => :string, :banner => "The name of the model."
83
+ method_option :component, :type => :string, :default => 'main', :banner => "The component the model should be created in.", :required => false
84
+ def model(name, component='main')
85
+ output_file = Dir.pwd + "/app/#{component.underscore}/models/#{name.underscore.singularize}.rb"
86
+ template("model/model.rb.tt", output_file, {model_name: name.camelize.singularize})
87
+ end
88
+
80
89
  def self.source_root
81
90
  File.expand_path(File.join(File.dirname(__FILE__), '../../templates'))
82
91
  end
@@ -3,12 +3,20 @@ require 'volt/extra_core/inflector'
3
3
  class String
4
4
  # TODO: replace with better implementations
5
5
  # NOTE: strings are currently immutable in Opal, so no ! methods
6
+
7
+ # Turns a string into the camel case version. If it is already camel case, it should
8
+ # return the same string.
6
9
  def camelize
7
- self.split("_").map {|s| s.capitalize }.join("")
10
+ new_str = self.gsub(/_[a-z]/) { |a| a[1].upcase }
11
+ new_str = new_str[0].capitalize + new_str[1..-1]
12
+
13
+ return new_str
8
14
  end
9
15
 
16
+ # Returns the underscore version of a string. If it is already underscore, it should
17
+ # return the same string.
10
18
  def underscore
11
- self.scan(/[A-Z][a-z]*/).join("_").downcase
19
+ self.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
12
20
  end
13
21
 
14
22
  def dasherize
@@ -10,7 +10,6 @@ class ArrayModel < ReactiveArray
10
10
 
11
11
  attr_reader :parent, :path, :persistor, :options, :array
12
12
 
13
-
14
13
  # For many methods, we want to call load data as soon as the model is interacted
15
14
  # with, so we proxy the method, then call super.
16
15
  def self.proxy_with_load_data(*method_names)
@@ -26,8 +26,6 @@ class Model
26
26
 
27
27
  self.send(:attributes=, attributes, true)
28
28
 
29
- @cache = {}
30
-
31
29
  # Models start in a loaded state since they are normally setup from an
32
30
  # ArrayModel, which will have the data when they get added.
33
31
  @state = :loaded
@@ -35,6 +33,15 @@ class Model
35
33
  @persistor.loaded(initial_state) if @persistor
36
34
  end
37
35
 
36
+ # the id is stored in a field named _id, so we setup _id to proxy to this
37
+ def _id
38
+ @attributes && @attributes[:_id]
39
+ end
40
+
41
+ def _id=(val)
42
+ self.__id = val
43
+ end
44
+
38
45
  # Update the options
39
46
  def options=(options)
40
47
  @options = options
@@ -46,7 +53,28 @@ class Model
46
53
 
47
54
  # Assign multiple attributes as a hash, directly.
48
55
  def attributes=(attrs, initial_setup=false)
49
- @attributes = wrap_values(attrs)
56
+ @attributes = {}
57
+
58
+ attrs = wrap_values(attrs)
59
+
60
+ if attrs
61
+ # Assign id first
62
+ id = attrs.delete(:_id)
63
+ self._id = id if id
64
+
65
+ # Assign each attribute using setters
66
+ attrs.each_pair do |key, value|
67
+ if self.respond_to?(key)
68
+ # If a method without an underscore is defined, call that.
69
+ self.send(:"#{key}=", value)
70
+ else
71
+ # Otherwise, use the _ version
72
+ self.send(:"_#{key}=", value)
73
+ end
74
+ end
75
+ else
76
+ @attributes = attrs
77
+ end
50
78
 
51
79
  # Trigger and change all
52
80
  @deps.changed_all!
@@ -90,24 +118,21 @@ class Model
90
118
  read_attribute(method_name)
91
119
  end
92
120
  else
93
- # Call method directly on attributes. (since they are
94
- # not using _ )
95
- attributes.send(method_name, *args, &block)
121
+ # Call on parent
122
+ super
96
123
  end
97
124
  end
98
125
 
99
126
  # Do the assignment to a model and trigger a changed event
100
127
  def assign_attribute(method_name, *args, &block)
101
- # Free any cached value
102
- free(method_name)
103
-
104
128
  self.expand!
105
129
  # Assign, without the =
106
- attribute_name = method_name[0..-2].to_sym
130
+ attribute_name = method_name[1..-2].to_sym
107
131
 
108
132
  value = args[0]
109
133
 
110
- attributes[attribute_name] = wrap_value(value, [attribute_name])
134
+ @attributes[attribute_name] = wrap_value(value, [attribute_name])
135
+
111
136
  @deps.changed!(attribute_name)
112
137
 
113
138
  # Let the persistor know something changed
@@ -122,29 +147,26 @@ class Model
122
147
  # Reading an attribute, we may get back a nil model.
123
148
  method_name = method_name.to_sym
124
149
 
125
- if method_name[0] != '_' && attributes == nil
150
+ if method_name[0] != '_' && @attributes == nil
126
151
  # The method we are calling is on a nil model, return a wrapped
127
152
  # exception.
128
153
  return return_undefined_method(method_name)
129
154
  else
155
+ attr_name = method_name[1..-1].to_sym
130
156
  # See if the value is in attributes
131
- value = (attributes && attributes[method_name])
132
-
133
- # Also check @cache
134
- value ||= (@cache && @cache[method_name])
157
+ value = (@attributes && @attributes[attr_name])
135
158
 
136
159
  # Track dependency
137
- @deps.depend(method_name)
160
+ @deps.depend(attr_name)
138
161
 
139
162
  if value
140
163
  # key was in attributes or cache
141
164
  return value
142
165
  else
143
- # Cache the value, will be removed when expanded or something
144
- # is assigned over it.
145
166
  # TODO: implement a timed out cache flusing
146
- new_model = read_new_model(method_name)
147
- @cache[method_name] = new_model
167
+ new_model = read_new_model(attr_name)
168
+ @attributes ||= {}
169
+ @attributes[attr_name] = new_model
148
170
 
149
171
  return new_model
150
172
  end
@@ -204,11 +226,6 @@ class Model
204
226
  end
205
227
  end
206
228
 
207
- # Removes an item from the cache
208
- def free(name)
209
- @cache.delete(name)
210
- end
211
-
212
229
  # If this model is nil, it makes it into a hash model, then
213
230
  # sets it up to track from the parent.
214
231
  def expand!
@@ -217,7 +234,7 @@ class Model
217
234
  if @parent
218
235
  @parent.expand!
219
236
 
220
- @parent.send(:"#{@path.last}=", self)
237
+ @parent.send(:"_#{@path.last}=", self)
221
238
  end
222
239
  end
223
240
  end
@@ -264,7 +281,7 @@ class Model
264
281
  if save_to
265
282
  if save_to.is_a?(ArrayModel)
266
283
  # Add to the collection
267
- new_model = save_to.append(self.attributes)
284
+ new_model = save_to << self.attributes
268
285
 
269
286
  # Set the buffer's id to track the main model's id
270
287
  self.attributes[:_id] = new_model._id
@@ -18,6 +18,10 @@ module ModelHashBehaviour
18
18
  attributes.nil?
19
19
  end
20
20
 
21
+ def empty?
22
+ !attributes || attributes.size == 0
23
+ end
24
+
21
25
  def false?
22
26
  attributes.false?
23
27
  end
@@ -36,15 +40,23 @@ module ModelHashBehaviour
36
40
  @persistor.removed(nil) if @persistor
37
41
  end
38
42
 
43
+ def each_with_object(*args, &block)
44
+ return (@attributes || {}).each_with_object(*args, &block)
45
+ end
46
+
39
47
 
40
48
  # Convert the model to a hash all of the way down.
41
49
  def to_h
42
50
  hash = {}
43
- attributes.each_pair do |key, value|
44
- hash[key] = deep_unwrap(value)
51
+ if empty?
52
+ return nil
53
+ else
54
+ attributes.each_pair do |key, value|
55
+ hash[key] = deep_unwrap(value)
56
+ end
57
+
58
+ return hash
45
59
  end
46
-
47
- return hash
48
60
  end
49
61
 
50
62
  end
@@ -26,11 +26,13 @@ module ModelHelpers
26
26
  begin
27
27
  # remove the _ and then singularize
28
28
  if path.last == :[]
29
- klass_name = path[-2][1..-1].singularize.camelize
29
+ index = -2
30
30
  else
31
- klass_name = path[-1][1..-1].singularize.camelize
31
+ index = -1
32
32
  end
33
33
 
34
+ klass_name = path[index].singularize.camelize
35
+
34
36
  klass = $page.model_classes[klass_name] || Model
35
37
  rescue NameError => e
36
38
  # Ignore exception, just means the model isn't defined
@@ -17,7 +17,8 @@ module ModelWrapper
17
17
  values = values.map {|v| wrap_value(v,lookup + [:[]]) }
18
18
  elsif values.is_a?(Hash)
19
19
  pairs = values.map do |k,v|
20
- path = lookup + [k]
20
+ # TODO: We should be able to move wrapping into the method_missing on model
21
+ path = lookup + [k.to_sym]
21
22
 
22
23
  [k, wrap_value(v,path)]
23
24
  end
@@ -83,7 +83,6 @@ module Persistors
83
83
  def run_query(model, query={})
84
84
  @model.clear
85
85
 
86
- # puts "Run Query: #{query.inspect}"
87
86
  collection = model.path.last
88
87
  # Scope to the parent
89
88
  if model.path.size > 1
@@ -143,12 +142,13 @@ module Persistors
143
142
  def add(index, data)
144
143
  $loading_models = true
145
144
 
146
- new_options = @model.options.merge(path: @model.path + [:[]], parent: @model)
147
-
148
145
  # Don't add if the model is already in the ArrayModel
149
- if !@model.array.find {|v| v['_id'] == data['_id'] }
146
+ if !@model.array.find {|v| v._id == data[:_id] }
150
147
  # Find the existing model, or create one
151
- new_model = @@identity_map.find(data['_id']) { @model.new_model(data.symbolize_keys, new_options, :loaded) }
148
+ new_model = @@identity_map.find(data[:_id]) do
149
+ new_options = @model.options.merge(path: @model.path + [:[]], parent: @model)
150
+ @model.new_model(data, new_options, :loaded)
151
+ end
152
152
 
153
153
  @model.insert(index, new_model)
154
154
  end
@@ -193,7 +193,7 @@ module Persistors
193
193
  if defined?($loading_models) && $loading_models
194
194
  return
195
195
  else
196
- @tasks.call('StoreTasks', 'delete', channel_name, model.attributes[:_id])
196
+ StoreTasks.delete(channel_name, model.attributes[:_id])
197
197
  end
198
198
  end
199
199
 
@@ -41,7 +41,7 @@ module Persistors
41
41
 
42
42
  @loading_data = true
43
43
  root_attributes.each_pair do |key, value|
44
- @model.send(:"#{key}=", value)
44
+ @model.send(:"_#{key}=", value)
45
45
  end
46
46
  @loading_data = nil
47
47
  end
@@ -69,9 +69,9 @@ module Persistors
69
69
  puts "Attempting to save model directly on store."
70
70
  raise "Attempting to save model directly on store."
71
71
  else
72
- @tasks.call('StoreTasks', 'save', collection, self_attributes) do |errors|
72
+ StoreTasks.save(collection, self_attributes).then do |errors|
73
73
  if errors.size == 0
74
- promise.resolve
74
+ promise.resolve(nil)
75
75
  else
76
76
  promise.reject(errors)
77
77
  end
@@ -95,7 +95,7 @@ module Persistors
95
95
 
96
96
  if model
97
97
  data.each_pair do |key, value|
98
- if key != '_id'
98
+ if key != :_id
99
99
  model.send(:"#{key}=", value)
100
100
  end
101
101
  end