volt 0.8.8 → 0.8.9

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