volt 0.7.1 → 0.7.2

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -2
  3. data/Readme.md +97 -56
  4. data/VERSION +1 -1
  5. data/app/volt/assets/js/sockjs-0.3.4.min.js +27 -0
  6. data/app/volt/assets/js/vertxbus.js +216 -0
  7. data/app/volt/tasks/live_query/live_query.rb +5 -5
  8. data/app/volt/tasks/live_query/live_query_pool.rb +1 -1
  9. data/app/volt/tasks/query_tasks.rb +5 -0
  10. data/app/volt/tasks/store_tasks.rb +44 -18
  11. data/docs/WHY.md +10 -0
  12. data/lib/volt/cli.rb +18 -7
  13. data/lib/volt/controllers/model_controller.rb +30 -8
  14. data/lib/volt/extra_core/inflections.rb +63 -0
  15. data/lib/volt/extra_core/inflector/inflections.rb +203 -0
  16. data/lib/volt/extra_core/inflector/methods.rb +63 -0
  17. data/lib/volt/extra_core/inflector.rb +4 -0
  18. data/lib/volt/extra_core/object.rb +9 -0
  19. data/lib/volt/extra_core/string.rb +10 -14
  20. data/lib/volt/models/array_model.rb +45 -27
  21. data/lib/volt/models/cursor.rb +6 -0
  22. data/lib/volt/models/model.rb +127 -12
  23. data/lib/volt/models/model_hash_behaviour.rb +8 -5
  24. data/lib/volt/models/model_helpers.rb +4 -4
  25. data/lib/volt/models/model_state.rb +22 -0
  26. data/lib/volt/models/persistors/array_store.rb +49 -35
  27. data/lib/volt/models/persistors/base.rb +3 -3
  28. data/lib/volt/models/persistors/model_store.rb +17 -6
  29. data/lib/volt/models/persistors/query/query_listener.rb +0 -2
  30. data/lib/volt/models/persistors/store.rb +0 -4
  31. data/lib/volt/models/persistors/store_state.rb +27 -0
  32. data/lib/volt/models/url.rb +2 -2
  33. data/lib/volt/models/validations/errors.rb +0 -0
  34. data/lib/volt/models/validations/length.rb +13 -0
  35. data/lib/volt/models/validations/validations.rb +82 -0
  36. data/lib/volt/models.rb +1 -1
  37. data/lib/volt/page/bindings/attribute_binding.rb +29 -14
  38. data/lib/volt/page/bindings/base_binding.rb +2 -2
  39. data/lib/volt/page/bindings/component_binding.rb +29 -25
  40. data/lib/volt/page/bindings/content_binding.rb +1 -0
  41. data/lib/volt/page/bindings/each_binding.rb +25 -33
  42. data/lib/volt/page/bindings/event_binding.rb +0 -1
  43. data/lib/volt/page/bindings/if_binding.rb +3 -1
  44. data/lib/volt/page/bindings/template_binding.rb +61 -28
  45. data/lib/volt/page/document_events.rb +3 -1
  46. data/lib/volt/page/draw_cycle.rb +22 -0
  47. data/lib/volt/page/page.rb +10 -1
  48. data/lib/volt/page/reactive_template.rb +23 -16
  49. data/lib/volt/page/sub_context.rb +1 -1
  50. data/lib/volt/page/targets/attribute_section.rb +3 -2
  51. data/lib/volt/page/targets/attribute_target.rb +0 -4
  52. data/lib/volt/page/targets/base_section.rb +25 -0
  53. data/lib/volt/page/targets/binding_document/component_node.rb +13 -14
  54. data/lib/volt/page/targets/binding_document/html_node.rb +4 -0
  55. data/lib/volt/page/targets/dom_section.rb +16 -67
  56. data/lib/volt/page/targets/dom_template.rb +99 -0
  57. data/lib/volt/page/targets/helpers/comment_searchers.rb +29 -0
  58. data/lib/volt/page/template_renderer.rb +2 -14
  59. data/lib/volt/reactive/array_extensions.rb +0 -1
  60. data/lib/volt/reactive/event_chain.rb +9 -2
  61. data/lib/volt/reactive/events.rb +44 -37
  62. data/lib/volt/reactive/object_tracking.rb +1 -1
  63. data/lib/volt/reactive/reactive_array.rb +18 -0
  64. data/lib/volt/reactive/reactive_count.rb +108 -0
  65. data/lib/volt/reactive/reactive_generator.rb +44 -0
  66. data/lib/volt/reactive/reactive_value.rb +73 -73
  67. data/lib/volt/reactive/string_extensions.rb +1 -1
  68. data/lib/volt/router/routes.rb +205 -88
  69. data/lib/volt/server/component_handler.rb +3 -1
  70. data/lib/volt/server/html_parser/view_parser.rb +20 -4
  71. data/lib/volt/server/rack/component_paths.rb +13 -10
  72. data/lib/volt/server/rack/index_files.rb +4 -4
  73. data/lib/volt/server/socket_connection_handler.rb +5 -1
  74. data/lib/volt/server.rb +10 -3
  75. data/spec/apps/kitchen_sink/.gitignore +8 -0
  76. data/spec/apps/kitchen_sink/Gemfile +32 -0
  77. data/spec/apps/kitchen_sink/app/home/views/index/index.html +3 -5
  78. data/spec/apps/kitchen_sink/config.ru +4 -0
  79. data/spec/apps/kitchen_sink/public/index.html +2 -2
  80. data/spec/extra_core/inflector_spec.rb +8 -0
  81. data/spec/models/event_chain_spec.rb +18 -0
  82. data/spec/models/model_buffers_spec.rb +9 -0
  83. data/spec/models/model_spec.rb +22 -9
  84. data/spec/models/reactive_array_spec.rb +26 -1
  85. data/spec/models/reactive_call_times_spec.rb +28 -0
  86. data/spec/models/reactive_value_spec.rb +19 -0
  87. data/spec/models/validations_spec.rb +39 -0
  88. data/spec/page/bindings/content_binding_spec.rb +1 -0
  89. data/spec/{templates → page/bindings}/template_binding_spec.rb +54 -0
  90. data/spec/router/routes_spec.rb +156 -8
  91. data/spec/server/html_parser/sandlebars_parser_spec.rb +55 -47
  92. data/spec/server/html_parser/view_parser_spec.rb +3 -0
  93. data/spec/server/rack/asset_files_spec.rb +1 -1
  94. data/spec/spec_helper.rb +25 -11
  95. data/spec/templates/targets/binding_document/component_node_spec.rb +12 -0
  96. data/templates/project/Gemfile.tt +11 -0
  97. data/templates/project/app/home/config/routes.rb +1 -1
  98. data/templates/project/app/home/controllers/index_controller.rb +5 -5
  99. data/templates/project/app/home/views/index/index.html +6 -6
  100. data/volt.gemspec +5 -6
  101. metadata +34 -76
  102. data/app/volt/assets/js/sockjs-0.2.1.min.js +0 -27
  103. data/lib/volt/reactive/object_tracker.rb +0 -107
@@ -0,0 +1,203 @@
1
+ module Inflector
2
+ # A singleton instance of this class is yielded by Inflector.inflections,
3
+ # which can then be used to specify additional inflection rules. If passed
4
+ # an optional locale, rules for other languages can be specified. The
5
+ # default locale is <tt>:en</tt>. Only rules for English are provided.
6
+ #
7
+ # ActiveSupport::Inflector.inflections(:en) do |inflect|
8
+ # inflect.plural /^(ox)$/i, '\1\2en'
9
+ # inflect.singular /^(ox)en/i, '\1'
10
+ #
11
+ # inflect.irregular 'octopus', 'octopi'
12
+ #
13
+ # inflect.uncountable 'equipment'
14
+ # end
15
+ #
16
+ # New rules are added at the top. So in the example above, the irregular
17
+ # rule for octopus will now be the first of the pluralization and
18
+ # singularization rules that is runs. This guarantees that your rules run
19
+ # before any of the rules that may already have been loaded.
20
+ class Inflections
21
+ @__instance__ = {}
22
+
23
+ def self.instance(locale = :en)
24
+ @__instance__[locale] ||= new
25
+ end
26
+
27
+ attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex
28
+
29
+ def initialize
30
+ @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], [], [], {}, /(?=a)b/
31
+ end
32
+
33
+ # Private, for the test suite.
34
+ def initialize_dup(orig) # :nodoc:
35
+ %w(plurals singulars uncountables humans acronyms acronym_regex).each do |scope|
36
+ instance_variable_set("@#{scope}", orig.send(scope).dup)
37
+ end
38
+ end
39
+
40
+ # Specifies a new acronym. An acronym must be specified as it will appear
41
+ # in a camelized string. An underscore string that contains the acronym
42
+ # will retain the acronym when passed to +camelize+, +humanize+, or
43
+ # +titleize+. A camelized string that contains the acronym will maintain
44
+ # the acronym when titleized or humanized, and will convert the acronym
45
+ # into a non-delimited single lowercase word when passed to +underscore+.
46
+ #
47
+ # acronym 'HTML'
48
+ # titleize 'html' #=> 'HTML'
49
+ # camelize 'html' #=> 'HTML'
50
+ # underscore 'MyHTML' #=> 'my_html'
51
+ #
52
+ # The acronym, however, must occur as a delimited unit and not be part of
53
+ # another word for conversions to recognize it:
54
+ #
55
+ # acronym 'HTTP'
56
+ # camelize 'my_http_delimited' #=> 'MyHTTPDelimited'
57
+ # camelize 'https' #=> 'Https', not 'HTTPs'
58
+ # underscore 'HTTPS' #=> 'http_s', not 'https'
59
+ #
60
+ # acronym 'HTTPS'
61
+ # camelize 'https' #=> 'HTTPS'
62
+ # underscore 'HTTPS' #=> 'https'
63
+ #
64
+ # Note: Acronyms that are passed to +pluralize+ will no longer be
65
+ # recognized, since the acronym will not occur as a delimited unit in the
66
+ # pluralized result. To work around this, you must specify the pluralized
67
+ # form as an acronym as well:
68
+ #
69
+ # acronym 'API'
70
+ # camelize(pluralize('api')) #=> 'Apis'
71
+ #
72
+ # acronym 'APIs'
73
+ # camelize(pluralize('api')) #=> 'APIs'
74
+ #
75
+ # +acronym+ may be used to specify any word that contains an acronym or
76
+ # otherwise needs to maintain a non-standard capitalization. The only
77
+ # restriction is that the word must begin with a capital letter.
78
+ #
79
+ # acronym 'RESTful'
80
+ # underscore 'RESTful' #=> 'restful'
81
+ # underscore 'RESTfulController' #=> 'restful_controller'
82
+ # titleize 'RESTfulController' #=> 'RESTful Controller'
83
+ # camelize 'restful' #=> 'RESTful'
84
+ # camelize 'restful_controller' #=> 'RESTfulController'
85
+ #
86
+ # acronym 'McDonald'
87
+ # underscore 'McDonald' #=> 'mcdonald'
88
+ # camelize 'mcdonald' #=> 'McDonald'
89
+ def acronym(word)
90
+ @acronyms[word.downcase] = word
91
+ @acronym_regex = /#{@acronyms.values.join("|")}/
92
+ end
93
+
94
+ # Specifies a new pluralization rule and its replacement. The rule can
95
+ # either be a string or a regular expression. The replacement should
96
+ # always be a string that may include references to the matched data from
97
+ # the rule.
98
+ def plural(rule, replacement)
99
+ @uncountables.delete(rule) if rule.is_a?(String)
100
+ @uncountables.delete(replacement)
101
+ @plurals.insert(0, [rule, replacement])
102
+ end
103
+
104
+ # Specifies a new singularization rule and its replacement. The rule can
105
+ # either be a string or a regular expression. The replacement should
106
+ # always be a string that may include references to the matched data from
107
+ # the rule.
108
+ def singular(rule, replacement)
109
+ @uncountables.delete(rule) if rule.is_a?(String)
110
+ @uncountables.delete(replacement)
111
+ @singulars.insert(0, [rule, replacement])
112
+ end
113
+
114
+ # Specifies a new irregular that applies to both pluralization and
115
+ # singularization at the same time. This can only be used for strings, not
116
+ # regular expressions. You simply pass the irregular in singular and
117
+ # plural form.
118
+ #
119
+ # irregular 'octopus', 'octopi'
120
+ # irregular 'person', 'people'
121
+ def irregular(singular, plural)
122
+ @uncountables.delete(singular)
123
+ @uncountables.delete(plural)
124
+
125
+ s0 = singular[0]
126
+ srest = singular[1..-1]
127
+
128
+ p0 = plural[0]
129
+ prest = plural[1..-1]
130
+
131
+ if s0.upcase == p0.upcase
132
+ plural(/(#{s0})#{srest}$/i, '\1' + prest)
133
+ plural(/(#{p0})#{prest}$/i, '\1' + prest)
134
+
135
+ singular(/(#{s0})#{srest}$/i, '\1' + srest)
136
+ singular(/(#{p0})#{prest}$/i, '\1' + srest)
137
+ else
138
+ plural(/#{s0.upcase}(?i)#{srest}$/, p0.upcase + prest)
139
+ plural(/#{s0.downcase}(?i)#{srest}$/, p0.downcase + prest)
140
+ plural(/#{p0.upcase}(?i)#{prest}$/, p0.upcase + prest)
141
+ plural(/#{p0.downcase}(?i)#{prest}$/, p0.downcase + prest)
142
+
143
+ singular(/#{s0.upcase}(?i)#{srest}$/, s0.upcase + srest)
144
+ singular(/#{s0.downcase}(?i)#{srest}$/, s0.downcase + srest)
145
+ singular(/#{p0.upcase}(?i)#{prest}$/, s0.upcase + srest)
146
+ singular(/#{p0.downcase}(?i)#{prest}$/, s0.downcase + srest)
147
+ end
148
+ end
149
+
150
+ # Add uncountable words that shouldn't be attempted inflected.
151
+ #
152
+ # uncountable 'money'
153
+ # uncountable 'money', 'information'
154
+ # uncountable %w( money information rice )
155
+ def uncountable(*words)
156
+ (@uncountables << words).flatten!
157
+ end
158
+
159
+ # Specifies a humanized form of a string by a regular expression rule or
160
+ # by a string mapping. When using a regular expression based replacement,
161
+ # the normal humanize formatting is called after the replacement. When a
162
+ # string is used, the human form should be specified as desired (example:
163
+ # 'The name', not 'the_name').
164
+ #
165
+ # human /_cnt$/i, '\1_count'
166
+ # human 'legacy_col_person_name', 'Name'
167
+ def human(rule, replacement)
168
+ @humans.insert(0, [rule, replacement])
169
+ end
170
+
171
+ # Clears the loaded inflections within a given scope (default is
172
+ # <tt>:all</tt>). Give the scope as a symbol of the inflection type, the
173
+ # options are: <tt>:plurals</tt>, <tt>:singulars</tt>, <tt>:uncountables</tt>,
174
+ # <tt>:humans</tt>.
175
+ #
176
+ # clear :all
177
+ # clear :plurals
178
+ def clear(scope = :all)
179
+ case scope
180
+ when :all
181
+ @plurals, @singulars, @uncountables, @humans = [], [], [], []
182
+ else
183
+ instance_variable_set "@#{scope}", []
184
+ end
185
+ end
186
+ end
187
+
188
+ # Yields a singleton instance of Inflector::Inflections so you can specify
189
+ # additional inflector rules. If passed an optional locale, rules for other
190
+ # languages can be specified. If not specified, defaults to <tt>:en</tt>.
191
+ # Only rules for English are provided.
192
+ #
193
+ # ActiveSupport::Inflector.inflections(:en) do |inflect|
194
+ # inflect.uncountable 'rails'
195
+ # end
196
+ def self.inflections(locale = :en)
197
+ if block_given?
198
+ yield Inflections.instance(locale)
199
+ else
200
+ Inflections.instance(locale)
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,63 @@
1
+ # encoding: utf-8
2
+
3
+ # The Inflector transforms words from singular to plural, class names to table
4
+ # names, modularized class names to ones without, and class names to foreign
5
+ # keys. The default inflections for pluralization, singularization, and
6
+ # uncountable words are kept in inflections.rb.
7
+ module Inflector
8
+ # Returns the plural form of the word in the string.
9
+ #
10
+ # If passed an optional +locale+ parameter, the word will be
11
+ # pluralized using rules defined for that language. By default,
12
+ # this parameter is set to <tt>:en</tt>.
13
+ #
14
+ # 'post'.pluralize # => "posts"
15
+ # 'octopus'.pluralize # => "octopi"
16
+ # 'sheep'.pluralize # => "sheep"
17
+ # 'words'.pluralize # => "words"
18
+ # 'CamelOctopus'.pluralize # => "CamelOctopi"
19
+ # 'ley'.pluralize(:es) # => "leyes"
20
+ def self.pluralize(word, locale = :en)
21
+ apply_inflections(word, inflections(locale).plurals)
22
+ end
23
+
24
+ # The reverse of +pluralize+, returns the singular form of a word in a
25
+ # string.
26
+ #
27
+ # If passed an optional +locale+ parameter, the word will be
28
+ # pluralized using rules defined for that language. By default,
29
+ # this parameter is set to <tt>:en</tt>.
30
+ #
31
+ # 'posts'.singularize # => "post"
32
+ # 'octopi'.singularize # => "octopus"
33
+ # 'sheep'.singularize # => "sheep"
34
+ # 'word'.singularize # => "word"
35
+ # 'CamelOctopi'.singularize # => "CamelOctopus"
36
+ # 'leyes'.singularize(:es) # => "ley"
37
+ def self.singularize(word, locale = :en)
38
+ apply_inflections(word, inflections(locale).singulars)
39
+ end
40
+
41
+
42
+ private
43
+
44
+ # Applies inflection rules for +singularize+ and +pluralize+.
45
+ #
46
+ # apply_inflections('post', inflections.plurals) # => "posts"
47
+ # apply_inflections('posts', inflections.singulars) # => "post"
48
+ def self.apply_inflections(word, rules)
49
+ result = word.to_s.dup
50
+
51
+ if word.empty? || inflections.uncountables.include?(result.downcase[/\b\w+\Z/])
52
+ result
53
+ else
54
+ rules.each do |(rule, replacement)|
55
+ if result.match(rule)
56
+ result = result.sub(rule, replacement)
57
+ break
58
+ end
59
+ end
60
+ result
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,4 @@
1
+ # Copied from rails ActiveSupport: https://github.com/rails/rails/blob/feaa6e2048fe86bcf07e967d6e47b865e42e055b/activesupport/lib/active_support/inflector/inflections.rb
2
+ require 'volt/extra_core/inflector/inflections'
3
+ require 'volt/extra_core/inflector/methods'
4
+ require 'volt/extra_core/inflections'
@@ -37,4 +37,13 @@ class Object
37
37
  def deep_cur
38
38
  self.cur
39
39
  end
40
+
41
+ def html_inspect
42
+ inspect.gsub('<', '&lt;').gsub('>', '&gt;')
43
+ end
44
+
45
+ # TODO: Need a real implementation of this
46
+ def deep_clone
47
+ Marshal.load(Marshal.dump(self))
48
+ end
40
49
  end
@@ -1,3 +1,5 @@
1
+ require 'volt/extra_core/inflector'
2
+
1
3
  class String
2
4
  # TODO: replace with better implementations
3
5
  # NOTE: strings are currently immutable in Opal, so no ! methods
@@ -9,31 +11,25 @@ class String
9
11
  self.scan(/[A-Z][a-z]*/).join("_").downcase
10
12
  end
11
13
 
14
+ def dasherize
15
+ self.gsub('_', '-')
16
+ end
17
+
12
18
  def pluralize
13
- # TODO: Temp implementation
14
- if self[-1] != 's'
15
- return self + 's'
16
- else
17
- return self
18
- end
19
+ Inflector.pluralize(self)
19
20
  end
20
21
 
21
22
  def singularize
22
- # TODO: Temp implementation
23
- if self[-1] == 's'
24
- return self[0..-2]
25
- else
26
- return self
27
- end
23
+ Inflector.singularize(self)
28
24
  end
29
25
 
30
26
  def plural?
31
27
  # TODO: Temp implementation
32
- self[-1] == 's'
28
+ self.pluralize == self
33
29
  end
34
30
 
35
31
  def singular?
36
32
  # TODO: Temp implementation
37
- self[-1] != 's'
33
+ self.singularize == self
38
34
  end
39
35
  end
@@ -1,11 +1,28 @@
1
1
  require 'volt/models/model_wrapper'
2
2
  require 'volt/models/model_helpers'
3
+ require 'volt/models/model_state'
3
4
 
4
5
  class ArrayModel < ReactiveArray
5
6
  include ModelWrapper
6
7
  include ModelHelpers
8
+ include ModelState
7
9
 
8
- attr_reader :parent, :path, :persistor, :options
10
+ attr_reader :parent, :path, :persistor, :options, :array
11
+
12
+
13
+ # For many methods, we want to call load data as soon as the model is interacted
14
+ # with, so we proxy the method, then call super.
15
+ def self.proxy_with_load_data(*method_names)
16
+ method_names.each do |method_name|
17
+ define_method(method_name) do |*args|
18
+ # puts "CALL #{method_name} - Load Data on #{self.inspect}"
19
+ load_data
20
+ super(*args)
21
+ end
22
+ end
23
+ end
24
+
25
+ proxy_with_load_data :[], :size, :first, :last
9
26
 
10
27
  def initialize(array=[], options={})
11
28
  @options = options
@@ -20,39 +37,26 @@ class ArrayModel < ReactiveArray
20
37
  @persistor.loaded if @persistor
21
38
  end
22
39
 
23
- # For stored items, tell the collection to load the data when it
24
- # is requested.
25
- def [](index)
26
- load_data
27
- super
28
- end
29
-
30
- def size
31
- load_data
32
- super
40
+ tag_method(:find) do
41
+ destructive!
42
+ pass_reactive!
33
43
  end
34
-
35
- def state
44
+ def find(*args)
36
45
  if @persistor
37
- @persistor.state
46
+ return @persistor.find(*args)
38
47
  else
39
- :loaded
48
+ raise "this model's persistance layer does not support find, try using store"
40
49
  end
41
50
  end
42
51
 
43
- def loaded?
44
- state == :loaded
45
- end
46
-
47
- tag_method(:find) do
52
+ tag_method(:fetch) do
48
53
  destructive!
49
- pass_reactive!
50
54
  end
51
- def find(*args)
55
+ def fetch(*args, &block)
52
56
  if @persistor
53
- return @persistor.find(*args)
57
+ return @persistor.fetch(*args, &block)
54
58
  else
55
- raise "this model's persistance layer does not support find, try using store"
59
+ raise "this model's persistance layer does not support fetch, try using store"
56
60
  end
57
61
  end
58
62
 
@@ -72,7 +76,10 @@ class ArrayModel < ReactiveArray
72
76
  super(model)
73
77
 
74
78
  @persistor.added(model, @array.size-1) if @persistor
79
+
80
+ return model
75
81
  end
82
+ alias_method :append, :<<
76
83
 
77
84
  # Make sure it gets wrapped
78
85
  def inject(*args)
@@ -86,8 +93,8 @@ class ArrayModel < ReactiveArray
86
93
  super(*args)
87
94
  end
88
95
 
89
- def new_model(attributes, options)
90
- class_at_path(options[:path]).new(attributes, options)
96
+ def new_model(*args)
97
+ class_at_path(options[:path]).new(*args)
91
98
  end
92
99
 
93
100
  def new_array_model(*args)
@@ -114,6 +121,18 @@ class ArrayModel < ReactiveArray
114
121
  super
115
122
  end
116
123
 
124
+ tag_method(:buffer) do
125
+ destructive!
126
+ end
127
+ def buffer
128
+ model_path = options[:path] + [:[]]
129
+ model_klass = class_at_path(model_path)
130
+
131
+ new_options = options.merge(path: model_path, save_to: self).reject {|k,_| k.to_sym == :persistor }
132
+ model = model_klass.new({}, new_options)
133
+
134
+ return ReactiveValue.new(model)
135
+ end
117
136
 
118
137
  private
119
138
  # Takes the persistor if there is one and
@@ -130,5 +149,4 @@ class ArrayModel < ReactiveArray
130
149
  end
131
150
  end
132
151
 
133
-
134
152
  end
@@ -0,0 +1,6 @@
1
+ require 'volt/models/array_model'
2
+
3
+ class Cursor < ArrayModel
4
+
5
+
6
+ end
@@ -3,6 +3,9 @@ require 'volt/models/array_model'
3
3
  require 'volt/models/model_helpers'
4
4
  require 'volt/reactive/object_tracking'
5
5
  require 'volt/models/model_hash_behaviour'
6
+ require 'volt/models/validations/validations'
7
+ require 'volt/models/model_state'
8
+
6
9
 
7
10
  class NilMethodCall < NoMethodError
8
11
  def true?
@@ -20,16 +23,24 @@ class Model
20
23
  include ObjectTracking
21
24
  include ModelHelpers
22
25
  include ModelHashBehaviour
26
+ include Validations
27
+ include ModelState
23
28
 
24
29
  attr_accessor :attributes
25
30
  attr_reader :parent, :path, :persistor, :options
26
31
 
27
- def initialize(attributes={}, options={})
32
+ def initialize(attributes={}, options={}, initial_state=nil)
28
33
  self.options = options
29
34
 
30
- self.attributes = wrap_values(attributes)
35
+ self.send(:attributes=, attributes, true)
36
+
37
+ @cache = {}
38
+
39
+ # Models stat in a loaded state since they are normally setup from an
40
+ # ArrayModel, which will have the data when they get added.
41
+ @state = :loaded
31
42
 
32
- @persistor.loaded if @persistor
43
+ @persistor.loaded(initial_state) if @persistor
33
44
  end
34
45
 
35
46
  # Update the options
@@ -41,6 +52,18 @@ class Model
41
52
  @persistor = setup_persistor(options[:persistor])
42
53
  end
43
54
 
55
+ # Assign multiple attributes as a hash, directly.
56
+ def attributes=(attrs, initial_setup=false)
57
+ @attributes = wrap_values(attrs)
58
+
59
+ unless initial_setup
60
+ trigger!('changed')
61
+
62
+ # Let the persistor know something changed
63
+ @persistor.changed if @persistor
64
+ end
65
+ end
66
+ alias_method :assign_attributes, :attributes=
44
67
 
45
68
  # Pass the comparison through
46
69
  def ==(val)
@@ -81,6 +104,9 @@ class Model
81
104
 
82
105
  # Do the assignment to a model and trigger a changed event
83
106
  def assign_attribute(method_name, *args, &block)
107
+ # Free any cached value
108
+ free(method_name)
109
+
84
110
  self.expand!
85
111
  # Assign, without the =
86
112
  attribute_name = method_name[0..-2].to_sym
@@ -107,11 +133,25 @@ class Model
107
133
  # The method we are calling is on a nil model, return a wrapped
108
134
  # exception.
109
135
  return return_undefined_method(method_name)
110
- elsif attributes && attributes.has_key?(method_name)
111
- # Method has the key, look it up directly
112
- return attributes[method_name]
113
136
  else
114
- return read_new_model(method_name)
137
+ # See if the value is in attributes
138
+ value = (attributes && attributes[method_name])
139
+
140
+ # Also check @cache
141
+ value ||= (@cache && @cache[method_name])
142
+
143
+ if value
144
+ # key was in attributes or cache
145
+ return value
146
+ else
147
+ # Cache the value, will be removed when expanded or something
148
+ # is assigned over it.
149
+ # TODO: implement a timed out cache flusing
150
+ new_model = read_new_model(method_name)
151
+ @cache[method_name] = new_model
152
+
153
+ return new_model
154
+ end
115
155
  end
116
156
  end
117
157
 
@@ -142,8 +182,12 @@ class Model
142
182
  class_at_path(options[:path]).new(attributes, options)
143
183
  end
144
184
 
145
- def new_array_model(*args)
146
- ArrayModel.new(*args)
185
+ def new_array_model(attributes, options)
186
+ # Start with an empty query
187
+ options = options.dup
188
+ options[:query] = {}
189
+
190
+ ArrayModel.new(attributes, options)
147
191
  end
148
192
 
149
193
  def trigger_by_attribute!(event_name, attribute, *passed_args)
@@ -159,15 +203,20 @@ class Model
159
203
  end
160
204
  end
161
205
 
206
+ # Removes an item from the cache
207
+ def free(name)
208
+ @cache.delete(name)
209
+ end
210
+
162
211
  # If this model is nil, it makes it into a hash model, then
163
212
  # sets it up to track from the parent.
164
213
  def expand!
165
214
  if attributes.nil?
166
- self.attributes = {}
215
+ @attributes = {}
167
216
  if @parent
168
217
  @parent.expand!
169
218
 
170
- @parent.attributes[@path.last] = self
219
+ @parent.send(:"#{@path.last}=", self)
171
220
  end
172
221
  end
173
222
  end
@@ -196,18 +245,84 @@ class Model
196
245
  # Add the new item
197
246
  result << value
198
247
 
248
+ trigger!('added', nil, 0)
249
+ trigger!('changed')
250
+
199
251
  return nil
200
252
  end
201
253
 
202
254
  def inspect
203
- "<#{self.class.to_s} #{attributes.inspect}>"
255
+ "<#{self.class.to_s}:#{object_id} #{attributes.inspect}>"
204
256
  end
205
257
 
206
258
  def deep_cur
207
259
  attributes
208
260
  end
209
261
 
262
+
263
+ def save!
264
+ if errors.size == 0
265
+ puts "SAVING: #{self.errors.inspect} - #{self.inspect} - #{options[:save_to].inspect} on #{self.inspect}"
266
+ save_to = options[:save_to]
267
+ if save_to
268
+ if save_to.is_a?(ArrayModel)
269
+ # Add to the collection
270
+ new_model = save_to.append(self.attributes)
271
+
272
+ options[:save_to] = new_model
273
+ else
274
+ puts "SAVE BUFFERED"
275
+ # We have a saved model
276
+ return save_to.assign_attributes(self.attributes)
277
+ end
278
+ else
279
+ raise "Model is not a buffer, can not be saved, modifications should be persisted as they are made."
280
+ end
281
+
282
+ return true
283
+ else
284
+ # Some errors, mark all fields
285
+ self.class.validations.keys.each do |key|
286
+ mark_field!(key.to_sym)
287
+ end
288
+ trigger_for_methods!('changed', :errors, :marked_errors)
289
+
290
+ return false
291
+ end
292
+ end
293
+
294
+
295
+ # Returns a buffered version of the model
296
+ tag_method(:buffer) do
297
+ destructive!
298
+ end
299
+ def buffer
300
+ model_path = options[:path]
301
+ model_klass = class_at_path(model_path)
302
+
303
+ new_options = options.merge(path: model_path, save_to: self).reject {|k,_| k.to_sym == :persistor }
304
+ model = model_klass.new({}, new_options, :loading)
305
+
306
+ if state == :loaded
307
+ setup_buffer(model)
308
+ else
309
+ self.parent.fetch do
310
+ setup_buffer(model)
311
+ end
312
+ end
313
+
314
+ # puts "SAVE TO:: #{model.options[:save_to].inspect} for #{model.inspect}"
315
+
316
+ return ReactiveValue.new(model)
317
+ end
318
+
319
+
210
320
  private
321
+ def setup_buffer(model)
322
+ model.attributes = self.attributes
323
+ model.change_state_to(:loaded)
324
+ end
325
+
211
326
  # Clear the previous value and assign a new one
212
327
  def __assign_element(key, value)
213
328
  __clear_element(key)