volt 0.7.1 → 0.7.2

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