volt-repo_cache 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +314 -0
- data/Rakefile +1 -0
- data/lib/volt/repo_cache.rb +6 -0
- data/lib/volt/repo_cache/association.rb +100 -0
- data/lib/volt/repo_cache/cache.rb +116 -0
- data/lib/volt/repo_cache/collection.rb +259 -0
- data/lib/volt/repo_cache/model.rb +671 -0
- data/lib/volt/repo_cache/model_array.rb +169 -0
- data/lib/volt/repo_cache/util.rb +78 -0
- data/lib/volt/repo_cache/version.rb +5 -0
- data/spec/dummy/.gitignore +9 -0
- data/spec/dummy/README.md +4 -0
- data/spec/dummy/app/main/assets/css/app.css.scss +1 -0
- data/spec/dummy/app/main/config/dependencies.rb +11 -0
- data/spec/dummy/app/main/config/initializers/boot.rb +10 -0
- data/spec/dummy/app/main/config/routes.rb +14 -0
- data/spec/dummy/app/main/controllers/main_controller.rb +27 -0
- data/spec/dummy/app/main/models/customer.rb +4 -0
- data/spec/dummy/app/main/models/order.rb +6 -0
- data/spec/dummy/app/main/models/product.rb +5 -0
- data/spec/dummy/app/main/models/user.rb +12 -0
- data/spec/dummy/app/main/views/main/about.html +7 -0
- data/spec/dummy/app/main/views/main/index.html +6 -0
- data/spec/dummy/app/main/views/main/main.html +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/app.rb +147 -0
- data/spec/dummy/config/base/index.html +15 -0
- data/spec/dummy/config/initializers/boot.rb +4 -0
- data/spec/integration/sample_integration_spec.rb +11 -0
- data/spec/sample_spec.rb +7 -0
- data/spec/spec_helper.rb +18 -0
- data/volt-repo_cache.gemspec +38 -0
- metadata +287 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9cb06385cea5ffc9a2c6518ec011d99c65957550
|
4
|
+
data.tar.gz: b88fab46ec514ac59bf7da97a84e2c3461fee23b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ff6af4ea44e4067961a8a3fd289004736538ab165cec33c8a258ae7c5cb770cc120f9d4267adb179a1843ccd6b26df349d0b4683a51bfb324bb22f3a3199b641
|
7
|
+
data.tar.gz: e0616aded5eaff2f4079a7a28e645baf48004e12d5eaeb8119c7086b5266f7cd3ac32d76b4c0d1ea053e0889215be45ed8e73359ad35fb56922aad12aa9ae6b3
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in volt-repo_cache.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
# Optional Gems for testing/dev
|
7
|
+
|
8
|
+
# The implementation of ReadWriteLock in Volt uses concurrent ruby and ext helps performance.
|
9
|
+
gem 'concurrent-ruby-ext', '~> 0.8.0'
|
10
|
+
|
11
|
+
# Gems you use for development should be added to the gemspec file as
|
12
|
+
# development dependencies.
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Colin Gunn
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
# Volt::RepoCache
|
2
|
+
|
3
|
+
- Provides client-side caching of repository (db) collections, models and their associations.
|
4
|
+
- Loads multiple associated collections (or query based subsets) into a cache.
|
5
|
+
- Buffers changes to models, collections and associations until flushed.
|
6
|
+
- Allows for flushes to be performed at model, collection or cache level.
|
7
|
+
- Provides increased associational integrity.
|
8
|
+
- Reduces the burden of promise handling in repository (db) operations.
|
9
|
+
- Is ideal for use where multiple associated models are being displayed and edited.
|
10
|
+
- Preserves standard Volt model and collection interfaces and reactivity.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
gem 'volt-repo_cache'
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install volt-repo_cache
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
Assume we have a sales application with three model classes:
|
29
|
+
|
30
|
+
class Customer < Volt::Model
|
31
|
+
field :name
|
32
|
+
has_many :orders
|
33
|
+
end
|
34
|
+
|
35
|
+
class Product < Volt::Model
|
36
|
+
field :name
|
37
|
+
field :price
|
38
|
+
has_many :orders
|
39
|
+
end
|
40
|
+
|
41
|
+
class Order < Volt:Model
|
42
|
+
belongs_to :customer
|
43
|
+
belongs_to :product
|
44
|
+
field :date
|
45
|
+
field :quantity
|
46
|
+
end
|
47
|
+
|
48
|
+
Let's say we want to cache all customers, products,
|
49
|
+
and orders, the latter between some given dates.
|
50
|
+
|
51
|
+
The following code will create the cache and load it
|
52
|
+
in a controller's `index` method. We'll also add a
|
53
|
+
`before_index_remove` method to clear the cache
|
54
|
+
when leaving the page.
|
55
|
+
|
56
|
+
#### Example 1 - defining and loading a cache
|
57
|
+
|
58
|
+
class OrderController < Volt::ModelController
|
59
|
+
|
60
|
+
def index
|
61
|
+
new_cache.loaded.then do |cache|
|
62
|
+
page._cache = cache
|
63
|
+
end.fail do |errors|
|
64
|
+
flashes << errors.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def new_cache
|
69
|
+
Volt::RepoCache.new(
|
70
|
+
Volt.current_app.store,
|
71
|
+
customer: {
|
72
|
+
has_many: :orders,
|
73
|
+
}
|
74
|
+
product: {
|
75
|
+
has_many: :orders,
|
76
|
+
}
|
77
|
+
order: {
|
78
|
+
belongs_to: [:customer, :product]
|
79
|
+
where: {'$and' => [:date => {'$gte' => start_date}, :date => {'$lte' => end_date}]}
|
80
|
+
}
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def before_index_remove
|
85
|
+
page._cache.clear if _cache
|
86
|
+
end
|
87
|
+
|
88
|
+
...
|
89
|
+
end
|
90
|
+
|
91
|
+
**Under Volt 0.9.7 the specification of associations
|
92
|
+
will be provided by the underlying models' class
|
93
|
+
definitions and will no longer be required in
|
94
|
+
the cache options.**
|
95
|
+
|
96
|
+
In the `index` method we only need to resolve
|
97
|
+
one promise when the cache is `loaded`.
|
98
|
+
|
99
|
+
Collections may be identified in the singular or plural
|
100
|
+
according to preference, e.g. `order:` or `orders:`,
|
101
|
+
with or without an underscore prefix.
|
102
|
+
|
103
|
+
A `where:` or `query:` option may be provided for each collection
|
104
|
+
to specify which models are loaded from the repository.
|
105
|
+
The default behaviour is to load all models in a collection.
|
106
|
+
|
107
|
+
Resolution of associations between cached models will
|
108
|
+
depend on what has been loaded into the cache for
|
109
|
+
each collection.
|
110
|
+
|
111
|
+
After the cache is loaded you can then access
|
112
|
+
collections, models and associations without
|
113
|
+
handling promise resolution or failure.
|
114
|
+
|
115
|
+
Otherwise, the interfaces to cached models and collections
|
116
|
+
largely behave as normal.
|
117
|
+
|
118
|
+
#### Example 2 - query and association resolution
|
119
|
+
|
120
|
+
# find all orders for customer 'ABC and product 'XYZ'
|
121
|
+
cache._customers.where(name: 'ABC').orders.select { |order|
|
122
|
+
order.product.name == 'XYZ'
|
123
|
+
}
|
124
|
+
|
125
|
+
Unlike a standard Volt query and association call
|
126
|
+
(`order.product`) we have no intervening promise(s) to
|
127
|
+
resolve, and also avoid relatively slow database request(s).
|
128
|
+
|
129
|
+
#### Example 3 - query and association resolution
|
130
|
+
|
131
|
+
# total cost of products ordered by customer
|
132
|
+
customer = cache._customers.where(name: 'ABC')
|
133
|
+
total_cost = customer.orders.reduce(0) do |sum, order|
|
134
|
+
sum + (order.quantity * order.product.price)
|
135
|
+
end
|
136
|
+
|
137
|
+
Again, no promises to resolve and faster calculation of
|
138
|
+
total cost than would be the case with uncached database
|
139
|
+
access.
|
140
|
+
|
141
|
+
### Changes and flushing
|
142
|
+
|
143
|
+
Changes to field values in models are buffered until
|
144
|
+
flushed (saved) to the database. Flushes may be requested
|
145
|
+
at the model, collection or cache level. Each flush
|
146
|
+
returns a single promise. Some examples:
|
147
|
+
|
148
|
+
#### Example 4 - change and save a single model
|
149
|
+
|
150
|
+
# change the price of a product and save it
|
151
|
+
product = cache._products.where(name: 'XYZ')
|
152
|
+
product.price = 9.99
|
153
|
+
# flush the product model
|
154
|
+
product.flush!.then do |result|
|
155
|
+
puts "#{result} saved"
|
156
|
+
end.fail do |errors|
|
157
|
+
puts errors
|
158
|
+
end
|
159
|
+
|
160
|
+
#### Example 5 - change and save several models in a collection
|
161
|
+
|
162
|
+
# change the price of multiple products
|
163
|
+
# and save them all together
|
164
|
+
products = cache._products
|
165
|
+
products.where(name: 'X').price = 7.77
|
166
|
+
products.where(name: 'Y').price = 8.88
|
167
|
+
products.where(name: 'Z').price = 9.99
|
168
|
+
# flush the 'products' collection
|
169
|
+
products.flush!.then do |result|
|
170
|
+
puts "all products saved"
|
171
|
+
end.fail do |errors|
|
172
|
+
puts "error saving products: #{errors}"
|
173
|
+
end
|
174
|
+
|
175
|
+
#### Example 6 - change and save models in more than one collection
|
176
|
+
|
177
|
+
# change the price of a product
|
178
|
+
# and the name of a customer
|
179
|
+
# and save them together
|
180
|
+
cache._products.where(name: 'XYZ').price = 7.77
|
181
|
+
cache._customer.where(name: 'ABC').name = 'EFG'
|
182
|
+
# flush the whole cache
|
183
|
+
cache.flush!.then do |result|
|
184
|
+
puts "cached flushed successfully"
|
185
|
+
end.fail do |errors|
|
186
|
+
puts "error flushing cache: #{errors}"
|
187
|
+
end
|
188
|
+
|
189
|
+
### Creating new models with no owners
|
190
|
+
|
191
|
+
There are two ways to create a new instance of a model
|
192
|
+
not belonging to another model:
|
193
|
+
|
194
|
+
#### Example 7 - create a new model (with no owner) via a collection
|
195
|
+
|
196
|
+
# create a new product
|
197
|
+
p = Product.new(name: 'IJK')
|
198
|
+
cache._products << p
|
199
|
+
|
200
|
+
A new model must be added to the appropriate cached collection
|
201
|
+
(using `#<<` or `#append`) before it also is cached. It will
|
202
|
+
not be saved to the database until the model or its containing
|
203
|
+
collection or cache is flushed.
|
204
|
+
|
205
|
+
NB Both `#<<` and `#append` return the collection, not the
|
206
|
+
appended model.
|
207
|
+
|
208
|
+
Another way of creating a new model via a collection using a hash:
|
209
|
+
|
210
|
+
#### Example 8 - create a new model (with no owner) via a collection
|
211
|
+
|
212
|
+
# create a new product
|
213
|
+
cache._products << {name: 'IJK'}
|
214
|
+
p = cache._products.where(name: 'IJK')
|
215
|
+
|
216
|
+
### Creating new models with owners
|
217
|
+
|
218
|
+
When creating a new model which belongs to one or more models
|
219
|
+
you must set the foreign key id(s) to establish the association(s).
|
220
|
+
|
221
|
+
#### Example 9 - create a new model (with two owners) via a collection
|
222
|
+
|
223
|
+
# create a new order which belongs to a customer and a product
|
224
|
+
product = cache._products.where(code: 'XYZ')
|
225
|
+
customer = cache._customers.where(code: 'ABC')
|
226
|
+
order = Order.new(product_id: product.id, customer_id: customer.id, quantity: 1, date: Date.today)
|
227
|
+
cache._orders << order
|
228
|
+
|
229
|
+
An easier way is ask an owner model to create a new owned model:
|
230
|
+
|
231
|
+
#### Example 10 - create a new model (with two owners) via an owner model
|
232
|
+
|
233
|
+
product = cache._products.where(code: 'XYZ')
|
234
|
+
customer = cache._customers.where(code: 'ABC')
|
235
|
+
# ask the customer to create a new order, give it the product id
|
236
|
+
order = customer.new_order(product_id: product.id, quantity: 1, date: Date.today)
|
237
|
+
|
238
|
+
### Destroying models
|
239
|
+
|
240
|
+
Models in the cache can be marked for destruction when the cache is flushed using `#mark_for_destruction!`.
|
241
|
+
Still to do - associational integrity checks when marking for destruction.
|
242
|
+
|
243
|
+
## Warnings
|
244
|
+
|
245
|
+
**Flushes to the underlying repository are not atomic and cannot be rolled back**.
|
246
|
+
If part of the cache/collection/model/association
|
247
|
+
flush fails the transaction(s) may lose integrity.
|
248
|
+
|
249
|
+
The cached models and collections contain circular references
|
250
|
+
(the models refer to the collection which contains them and
|
251
|
+
collections refer to the cache). Not being sure what the
|
252
|
+
implications are for efficient garbage collection (in Ruby
|
253
|
+
on the server and Javascript on the client), a method
|
254
|
+
is provided to clear the cache when it is no longer required,
|
255
|
+
breaking all internal (circular) references.
|
256
|
+
|
257
|
+
## TODO
|
258
|
+
|
259
|
+
1. Use associations_data in Volt::Models when 0.9.7 (sql) version available.
|
260
|
+
2. Handle non-standard collection, foreign_key and local_key Volt model options.
|
261
|
+
3. Association integrity checks on mark_for_destruction!
|
262
|
+
4. Test spec.
|
263
|
+
5. Locking?
|
264
|
+
6. Atomic transactions?
|
265
|
+
7. Removal of circular references?
|
266
|
+
|
267
|
+
## Contributing and use
|
268
|
+
|
269
|
+
This gem was written as part of the development of a production
|
270
|
+
application, primarily to speed up processing requiring many
|
271
|
+
implicit database queries (across associated collections), as well
|
272
|
+
as simplifying association management and reducing the burden of
|
273
|
+
asynchronous promise resolution.
|
274
|
+
|
275
|
+
It works well enough for our current application's needs,
|
276
|
+
but it may not be suitable for all requirements.
|
277
|
+
|
278
|
+
We will look at extending the cache framework to support locking and
|
279
|
+
atomic transactions (with rollback), but in the meantime if you have
|
280
|
+
a need or interest in this area your suggestions and contributions
|
281
|
+
are very welcome.
|
282
|
+
|
283
|
+
To contribute:
|
284
|
+
|
285
|
+
1. Fork it ( http://github.com/[my-github-username]/volt-repo_cache/fork )
|
286
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
287
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
288
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
289
|
+
5. Create new Pull Request
|
290
|
+
|
291
|
+
## License
|
292
|
+
|
293
|
+
Copyright (c) 2015 Colin Gunn
|
294
|
+
|
295
|
+
MIT License
|
296
|
+
|
297
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
298
|
+
a copy of this software and associated documentation files (the
|
299
|
+
"Software"), to deal in the Software without restriction, including
|
300
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
301
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
302
|
+
permit persons to whom the Software is furnished to do so, subject to
|
303
|
+
the following conditions:
|
304
|
+
|
305
|
+
The above copyright notice and this permission notice shall be
|
306
|
+
included in all copies or substantial portions of the Software.
|
307
|
+
|
308
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
309
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
310
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
311
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
312
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
313
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
314
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Volt
|
2
|
+
module RepoCache
|
3
|
+
class Association
|
4
|
+
include Volt::RepoCache::Util
|
5
|
+
|
6
|
+
attr_reader :local_name_singular, :local_name_plural
|
7
|
+
attr_reader :local_collection
|
8
|
+
attr_reader :foreign_name, :foreign_collection_name
|
9
|
+
attr_reader :foreign_model_class_name, :foreign_model_class
|
10
|
+
attr_reader :type, :foreign_id_field, :local_id_field
|
11
|
+
|
12
|
+
def initialize(local_collection, foreign_name, type)
|
13
|
+
_local_name = local_collection.name.to_s.sub(/^_/, '')
|
14
|
+
@local_name_singular = _local_name.singularize.to_sym
|
15
|
+
@local_name_plural = _local_name.pluralize.to_sym
|
16
|
+
@local_collection = local_collection
|
17
|
+
@foreign_name = foreign_name
|
18
|
+
@type = type
|
19
|
+
@foreign_model_class_name = @foreign_name.to_s.singularize.camelize
|
20
|
+
@foreign_model_class = Object.const_get(@foreign_model_class_name)
|
21
|
+
@foreign_collection_name = :"_#{@foreign_name.to_s.pluralize}"
|
22
|
+
@foreign_id_field = has_any? ? :"#{@local_collection.model_class_name.underscore}_id" : :id
|
23
|
+
@local_id_field = belongs_to? ? :"#{@foreign_name.to_s}_id" : :id
|
24
|
+
end
|
25
|
+
|
26
|
+
# Hide circular references to local
|
27
|
+
# and foreign collections for inspection.
|
28
|
+
def inspect
|
29
|
+
__local = @local_collection
|
30
|
+
__foreign = @foreign_collection
|
31
|
+
@local_collection = "{{#{@local_collection ? @local_collection.name : :nil}}"
|
32
|
+
@foreign_collection = "{{#{@foreign_collection ? @foreign_collection.name : :nil}}"
|
33
|
+
result = super
|
34
|
+
@local_collection = __local
|
35
|
+
@foreign_collection = __foreign
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def cache
|
40
|
+
@local_collection.cache
|
41
|
+
end
|
42
|
+
|
43
|
+
# Must be lazy initialization since we
|
44
|
+
# don't know order in which collections
|
45
|
+
# will be loaded to cache.
|
46
|
+
def foreign_collection
|
47
|
+
@foreign_collection ||= cache.collections[@foreign_collection_name]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the reciprocal association
|
51
|
+
# which may be nil if the foreign_collection
|
52
|
+
# is not interested (has not specified)
|
53
|
+
# the reciprocal association.
|
54
|
+
# It may be, for example, that this association
|
55
|
+
# is a belongs_to, but there is no reciprocal
|
56
|
+
# has_one or has_many association in the 'owner'.
|
57
|
+
# Must be lazy initialization since it depends on
|
58
|
+
# foreign_collection being lazily initialized.
|
59
|
+
def reciprocal
|
60
|
+
unless @reciprocal
|
61
|
+
# debug __method__, __LINE__, ""
|
62
|
+
@reciprocal = foreign_collection.associations.values.detect do |a|
|
63
|
+
# debug __method__, __LINE__, "#{a.foreign_collection.name} ?==? #{local_collection.name}"
|
64
|
+
a.foreign_collection.name == local_collection.name
|
65
|
+
end
|
66
|
+
@reciprocal = :nil unless @reciprocal
|
67
|
+
# debug __method__, __LINE__, "reciprocal of #{self.inspect} is #{@reciprocal.inspect}"
|
68
|
+
end
|
69
|
+
@reciprocal == :nil ? nil : @reciprocal
|
70
|
+
end
|
71
|
+
|
72
|
+
def reciprocated?
|
73
|
+
!!reciprocal
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_one?
|
77
|
+
type == :has_one
|
78
|
+
end
|
79
|
+
|
80
|
+
def has_many?
|
81
|
+
type == :has_many
|
82
|
+
end
|
83
|
+
|
84
|
+
def has_any?
|
85
|
+
has_one? || has_many?
|
86
|
+
end
|
87
|
+
|
88
|
+
def belongs_to?
|
89
|
+
type == :belongs_to
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def uncache
|
95
|
+
@local_collection = @foreign_collection = @reciprocal = nil
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|