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.
- checksums.yaml +4 -4
- data/Gemfile +1 -2
- data/Readme.md +97 -56
- data/VERSION +1 -1
- data/app/volt/assets/js/sockjs-0.3.4.min.js +27 -0
- data/app/volt/assets/js/vertxbus.js +216 -0
- data/app/volt/tasks/live_query/live_query.rb +5 -5
- data/app/volt/tasks/live_query/live_query_pool.rb +1 -1
- data/app/volt/tasks/query_tasks.rb +5 -0
- data/app/volt/tasks/store_tasks.rb +44 -18
- data/docs/WHY.md +10 -0
- data/lib/volt/cli.rb +18 -7
- data/lib/volt/controllers/model_controller.rb +30 -8
- data/lib/volt/extra_core/inflections.rb +63 -0
- data/lib/volt/extra_core/inflector/inflections.rb +203 -0
- data/lib/volt/extra_core/inflector/methods.rb +63 -0
- data/lib/volt/extra_core/inflector.rb +4 -0
- data/lib/volt/extra_core/object.rb +9 -0
- data/lib/volt/extra_core/string.rb +10 -14
- data/lib/volt/models/array_model.rb +45 -27
- data/lib/volt/models/cursor.rb +6 -0
- data/lib/volt/models/model.rb +127 -12
- data/lib/volt/models/model_hash_behaviour.rb +8 -5
- data/lib/volt/models/model_helpers.rb +4 -4
- data/lib/volt/models/model_state.rb +22 -0
- data/lib/volt/models/persistors/array_store.rb +49 -35
- data/lib/volt/models/persistors/base.rb +3 -3
- data/lib/volt/models/persistors/model_store.rb +17 -6
- data/lib/volt/models/persistors/query/query_listener.rb +0 -2
- data/lib/volt/models/persistors/store.rb +0 -4
- data/lib/volt/models/persistors/store_state.rb +27 -0
- data/lib/volt/models/url.rb +2 -2
- data/lib/volt/models/validations/errors.rb +0 -0
- data/lib/volt/models/validations/length.rb +13 -0
- data/lib/volt/models/validations/validations.rb +82 -0
- data/lib/volt/models.rb +1 -1
- data/lib/volt/page/bindings/attribute_binding.rb +29 -14
- data/lib/volt/page/bindings/base_binding.rb +2 -2
- data/lib/volt/page/bindings/component_binding.rb +29 -25
- data/lib/volt/page/bindings/content_binding.rb +1 -0
- data/lib/volt/page/bindings/each_binding.rb +25 -33
- data/lib/volt/page/bindings/event_binding.rb +0 -1
- data/lib/volt/page/bindings/if_binding.rb +3 -1
- data/lib/volt/page/bindings/template_binding.rb +61 -28
- data/lib/volt/page/document_events.rb +3 -1
- data/lib/volt/page/draw_cycle.rb +22 -0
- data/lib/volt/page/page.rb +10 -1
- data/lib/volt/page/reactive_template.rb +23 -16
- data/lib/volt/page/sub_context.rb +1 -1
- data/lib/volt/page/targets/attribute_section.rb +3 -2
- data/lib/volt/page/targets/attribute_target.rb +0 -4
- data/lib/volt/page/targets/base_section.rb +25 -0
- data/lib/volt/page/targets/binding_document/component_node.rb +13 -14
- data/lib/volt/page/targets/binding_document/html_node.rb +4 -0
- data/lib/volt/page/targets/dom_section.rb +16 -67
- data/lib/volt/page/targets/dom_template.rb +99 -0
- data/lib/volt/page/targets/helpers/comment_searchers.rb +29 -0
- data/lib/volt/page/template_renderer.rb +2 -14
- data/lib/volt/reactive/array_extensions.rb +0 -1
- data/lib/volt/reactive/event_chain.rb +9 -2
- data/lib/volt/reactive/events.rb +44 -37
- data/lib/volt/reactive/object_tracking.rb +1 -1
- data/lib/volt/reactive/reactive_array.rb +18 -0
- data/lib/volt/reactive/reactive_count.rb +108 -0
- data/lib/volt/reactive/reactive_generator.rb +44 -0
- data/lib/volt/reactive/reactive_value.rb +73 -73
- data/lib/volt/reactive/string_extensions.rb +1 -1
- data/lib/volt/router/routes.rb +205 -88
- data/lib/volt/server/component_handler.rb +3 -1
- data/lib/volt/server/html_parser/view_parser.rb +20 -4
- data/lib/volt/server/rack/component_paths.rb +13 -10
- data/lib/volt/server/rack/index_files.rb +4 -4
- data/lib/volt/server/socket_connection_handler.rb +5 -1
- data/lib/volt/server.rb +10 -3
- data/spec/apps/kitchen_sink/.gitignore +8 -0
- data/spec/apps/kitchen_sink/Gemfile +32 -0
- data/spec/apps/kitchen_sink/app/home/views/index/index.html +3 -5
- data/spec/apps/kitchen_sink/config.ru +4 -0
- data/spec/apps/kitchen_sink/public/index.html +2 -2
- data/spec/extra_core/inflector_spec.rb +8 -0
- data/spec/models/event_chain_spec.rb +18 -0
- data/spec/models/model_buffers_spec.rb +9 -0
- data/spec/models/model_spec.rb +22 -9
- data/spec/models/reactive_array_spec.rb +26 -1
- data/spec/models/reactive_call_times_spec.rb +28 -0
- data/spec/models/reactive_value_spec.rb +19 -0
- data/spec/models/validations_spec.rb +39 -0
- data/spec/page/bindings/content_binding_spec.rb +1 -0
- data/spec/{templates → page/bindings}/template_binding_spec.rb +54 -0
- data/spec/router/routes_spec.rb +156 -8
- data/spec/server/html_parser/sandlebars_parser_spec.rb +55 -47
- data/spec/server/html_parser/view_parser_spec.rb +3 -0
- data/spec/server/rack/asset_files_spec.rb +1 -1
- data/spec/spec_helper.rb +25 -11
- data/spec/templates/targets/binding_document/component_node_spec.rb +12 -0
- data/templates/project/Gemfile.tt +11 -0
- data/templates/project/app/home/config/routes.rb +1 -1
- data/templates/project/app/home/controllers/index_controller.rb +5 -5
- data/templates/project/app/home/views/index/index.html +6 -6
- data/volt.gemspec +5 -6
- metadata +34 -76
- data/app/volt/assets/js/sockjs-0.2.1.min.js +0 -27
- 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'
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
28
|
+
self.pluralize == self
|
|
33
29
|
end
|
|
34
30
|
|
|
35
31
|
def singular?
|
|
36
32
|
# TODO: Temp implementation
|
|
37
|
-
self
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
46
|
+
return @persistor.find(*args)
|
|
38
47
|
else
|
|
39
|
-
|
|
48
|
+
raise "this model's persistance layer does not support find, try using store"
|
|
40
49
|
end
|
|
41
50
|
end
|
|
42
51
|
|
|
43
|
-
|
|
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
|
|
55
|
+
def fetch(*args, &block)
|
|
52
56
|
if @persistor
|
|
53
|
-
return @persistor.
|
|
57
|
+
return @persistor.fetch(*args, &block)
|
|
54
58
|
else
|
|
55
|
-
raise "this model's persistance layer does not support
|
|
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(
|
|
90
|
-
class_at_path(options[:path]).new(
|
|
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
|
data/lib/volt/models/model.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
146
|
-
|
|
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
|
-
|
|
215
|
+
@attributes = {}
|
|
167
216
|
if @parent
|
|
168
217
|
@parent.expand!
|
|
169
218
|
|
|
170
|
-
@parent.
|
|
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)
|