spira 0.0.1.pre → 0.0.1
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.
- data/README +151 -29
- data/VERSION +1 -1
- data/lib/spira.rb +86 -1
- data/lib/spira/errors.rb +94 -0
- data/lib/spira/resource.rb +53 -7
- data/lib/spira/resource/class_methods.rb +113 -47
- data/lib/spira/resource/dsl.rb +137 -70
- data/lib/spira/resource/instance_methods.rb +165 -56
- data/lib/spira/resource/validations.rb +32 -8
- data/lib/spira/type.rb +82 -0
- data/lib/spira/types.rb +25 -0
- data/lib/spira/types/any.rb +23 -0
- data/lib/spira/types/boolean.rb +31 -0
- data/lib/spira/types/float.rb +28 -0
- data/lib/spira/types/integer.rb +27 -0
- data/lib/spira/types/string.rb +27 -0
- data/lib/spira/version.rb +23 -0
- metadata +13 -9
data/README
CHANGED
@@ -19,12 +19,12 @@ or to create a new store of RDF data based on simple defaults.
|
|
19
19
|
|
20
20
|
base_uri "http://example.org/example/people"
|
21
21
|
|
22
|
-
property :name, :predicate =>
|
23
|
-
property :age, :predicate =>
|
22
|
+
property :name, :predicate => FOAF.name, :type => String
|
23
|
+
property :age, :predicate => FOAF.age, :type => Integer
|
24
24
|
|
25
25
|
end
|
26
26
|
|
27
|
-
bob =
|
27
|
+
bob = RDF::URI("http://example.org/people/bob").as(Person)
|
28
28
|
bob.age = 15
|
29
29
|
bob.name = "Bob Smith"
|
30
30
|
bob.save!
|
@@ -36,19 +36,21 @@ or to create a new store of RDF data based on simple defaults.
|
|
36
36
|
### Features
|
37
37
|
|
38
38
|
* Extensible validations system
|
39
|
+
* Extensible types system
|
39
40
|
* Easy to use multiple data sources
|
40
41
|
* Easy to adapt models to existing data
|
42
|
+
* Open-world semantics
|
41
43
|
* Objects are still RDF.rb-compatible enumerable objects
|
42
44
|
* No need to put everything about an object into Spira
|
43
45
|
* Easy to use a resource as multiple models
|
44
46
|
|
45
47
|
## Getting Started
|
46
48
|
|
47
|
-
|
49
|
+
The easiest way to work with Spira is to install it via Rubygems:
|
48
50
|
|
49
51
|
$ sudo gem install spira
|
50
52
|
|
51
|
-
|
53
|
+
Downloads will be available on the github project page, as well as on Rubyforge.
|
52
54
|
|
53
55
|
## Defining Model Classes
|
54
56
|
|
@@ -74,9 +76,9 @@ without the `RDF::` prefix. For example:
|
|
74
76
|
|
75
77
|
Then use your model classes, in a way more or less similar to any number of ORMs:
|
76
78
|
|
77
|
-
cd = CD.
|
79
|
+
cd = CD.for("queens-greatest-hits")
|
78
80
|
cd.name = "Queen's greatest hits"
|
79
|
-
artist = Artist.
|
81
|
+
artist = Artist.for("queen")
|
80
82
|
artist.name = "Queen"
|
81
83
|
|
82
84
|
cd.artist = artist
|
@@ -84,21 +86,30 @@ Then use your model classes, in a way more or less similar to any number of ORMs
|
|
84
86
|
artist.cds = [cd]
|
85
87
|
artist.save!
|
86
88
|
|
87
|
-
queen = Arist.
|
88
|
-
hits = CD.
|
89
|
+
queen = Arist.for('queen')
|
90
|
+
hits = CD.for 'queens-greatest-hits'
|
89
91
|
hits.artist == artist == queen
|
90
92
|
|
91
93
|
### Absolute and Relative URIs
|
92
94
|
|
93
95
|
A class with a base URI can reference objects by a short name:
|
94
96
|
|
95
|
-
Artist.
|
97
|
+
Artist.for('queen')
|
96
98
|
|
97
99
|
However, a class is not required to have a base URI, and even if it does, it
|
98
100
|
can always access classes with a full URI:
|
99
101
|
|
100
|
-
nk = Artist.
|
101
|
-
|
102
|
+
nk = Artist.for(RDF::URI.new('http://example.org/my-hidden-cds/new-kids'))
|
103
|
+
|
104
|
+
If you have a URI that you would like to look at as a Spira resource, you can instantiate it from the URI:
|
105
|
+
|
106
|
+
RDF::URI.new('http://example.org/my-hidden-cds/new-kids').as(Artist)
|
107
|
+
# => <Artist @uri=http://example.org/my-hidden-cds/new-kids>
|
108
|
+
|
109
|
+
Any call to 'for' with a valid identifier will always return an object with nil
|
110
|
+
fields. It's a way of looking at a given resource, not a closed-world mapping
|
111
|
+
to one.
|
112
|
+
|
102
113
|
### Class Options
|
103
114
|
|
104
115
|
A number of options are available for Spira classes.
|
@@ -106,11 +117,11 @@ A number of options are available for Spira classes.
|
|
106
117
|
#### base_uri
|
107
118
|
|
108
119
|
A class with a `base_uri` set (either an `RDF::URI` or a `String`) will
|
109
|
-
use that URI as a base URI for non-absolute `
|
120
|
+
use that URI as a base URI for non-absolute `for` calls.
|
110
121
|
|
111
122
|
Example
|
112
|
-
CD.
|
113
|
-
CD.
|
123
|
+
CD.for 'queens-greatest-hits' # is the same as...
|
124
|
+
CD.for RDF::URI.new('http://example.org/cds/queens-greatest-hits')
|
114
125
|
|
115
126
|
#### type
|
116
127
|
|
@@ -118,12 +129,12 @@ A class with a `type` set is assigned an `RDF.type` on creation and saving.
|
|
118
129
|
|
119
130
|
class Album
|
120
131
|
include Spira::Resource
|
121
|
-
type
|
132
|
+
type URI.new('http://example.org/types/album')
|
122
133
|
property :name, :predicate => DC.title
|
123
134
|
end
|
124
135
|
|
125
|
-
rolling_stones = Album.
|
126
|
-
# See RDF.rb for more information about #has_predicate?
|
136
|
+
rolling_stones = Album.for RDF::URI.new('http://example.org/cds/rolling-stones-hits')
|
137
|
+
# See RDF.rb at http://rdf.rubyforge.org/RDF/Enumerable.html for more information about #has_predicate?
|
127
138
|
rolling_stones.has_predicate?(RDF.type) #=> true
|
128
139
|
Album.type #=> RDF::URI('http://example.org/types/album')
|
129
140
|
|
@@ -145,15 +156,16 @@ A class with a `default_vocabulary` set will transparently create predicates for
|
|
145
156
|
|
146
157
|
class Song
|
147
158
|
include Spira::Resource
|
148
|
-
default_vocabulary
|
159
|
+
default_vocabulary URI.new('http://example.org/vocab')
|
149
160
|
base_uri 'http://example.org/songs'
|
150
161
|
property :title
|
151
162
|
property :author, :type => :artist
|
152
163
|
end
|
153
164
|
|
154
|
-
dancing_queen = Song.
|
165
|
+
dancing_queen = Song.for 'dancing-queen'
|
155
166
|
dancing_queen.title = "Dancing Queen"
|
156
167
|
dancing_queen.artist = abba
|
168
|
+
# See RDF::Enumerable for #has_predicate?
|
157
169
|
dancing_queen.has_predicate?(RDF::URI.new('http://example.org/vocab/title')) #=> true
|
158
170
|
dancing_queen.has_predicate?(RDF::URI.new('http://example.org/vocab/artist')) #=> true
|
159
171
|
|
@@ -168,6 +180,11 @@ repository if one is not set.
|
|
168
180
|
|
169
181
|
See 'Defining Repositories' for more information.
|
170
182
|
|
183
|
+
#### validate
|
184
|
+
|
185
|
+
Provides the name of a function which does some sort of validation. See
|
186
|
+
'Validations' for more information.
|
187
|
+
|
171
188
|
### Property Options
|
172
189
|
|
173
190
|
Spira classes can have properties that are either singular or a list. For a
|
@@ -182,11 +199,17 @@ for `property` work for `has_many`.
|
|
182
199
|
Property always takes a symbol name as a name, and a variable list of options. The supported options are:
|
183
200
|
|
184
201
|
* `:type`: The type for this property. This can be a Ruby base class, an
|
185
|
-
RDF::XSD entry, or another Spira model class, referenced as a symbol.
|
202
|
+
RDF::XSD entry, or another Spira model class, referenced as a symbol. See
|
203
|
+
**Types** below. Default: `Any`
|
186
204
|
* `:predicate`: The predicate to use for this type. This can be any RDF URI.
|
187
205
|
This option is required unless the `default_vocabulary` has been used.
|
188
206
|
|
189
|
-
###
|
207
|
+
### Types
|
208
|
+
|
209
|
+
A property's type can be either a class which includes Spira::Type or a
|
210
|
+
reference to another Spira model class, given as a symbol.
|
211
|
+
|
212
|
+
#### Relations
|
190
213
|
|
191
214
|
If the `:type` of a spira class is the name of another Spira class as a symbol,
|
192
215
|
such as `:artist` for `Artist`, Spira will attempt to load the referenced
|
@@ -194,6 +217,71 @@ object when the appropriate property is accessed.
|
|
194
217
|
|
195
218
|
In the RDF store, this will be represented by the URI of the referenced object.
|
196
219
|
|
220
|
+
#### Type Classes
|
221
|
+
|
222
|
+
A type class includes Spira::Type, and can implement serialization and
|
223
|
+
deserialization functions, and register aliases to themselves if their datatype
|
224
|
+
is usually expressed as a URI. Here is the built-in Spira Integer class:
|
225
|
+
|
226
|
+
module Spira::Types
|
227
|
+
class Integer
|
228
|
+
|
229
|
+
include Spira::Type
|
230
|
+
|
231
|
+
def self.unserialize(value)
|
232
|
+
value.object
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.serialize(value)
|
236
|
+
RDF::Literal.new(value)
|
237
|
+
end
|
238
|
+
|
239
|
+
register_alias XSD.integer
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
Classes can now use this particular type like so:
|
244
|
+
|
245
|
+
class Test
|
246
|
+
include Spira::Resource
|
247
|
+
property :test1, :type => Integer
|
248
|
+
property :test2, :type => XSD.integer
|
249
|
+
end
|
250
|
+
|
251
|
+
Spira classes include the Spira::Types namespace, where several default types
|
252
|
+
are implemented:
|
253
|
+
|
254
|
+
* `Integer`
|
255
|
+
* `Float`
|
256
|
+
* `Boolean`
|
257
|
+
* `String`
|
258
|
+
* `Any`
|
259
|
+
|
260
|
+
The default type for a Spira property is `Spira::Types::Any`, which uses
|
261
|
+
`RDF::Literal`'s automatic boxing/unboxing of XSD types as best it can. See
|
262
|
+
`[RDF::Literal](http://rdf.rubyforge.org/RDF/Literal.html)` for more information.
|
263
|
+
|
264
|
+
You can implement your own types as well. Your class' serialize method should
|
265
|
+
turn an RDF::Value into a ruby object, and vice versa.
|
266
|
+
|
267
|
+
module MyModule
|
268
|
+
class MyType
|
269
|
+
include Spira::Type
|
270
|
+
def self.serialize(value)
|
271
|
+
...
|
272
|
+
end
|
273
|
+
|
274
|
+
def self.unserialize(value)
|
275
|
+
...
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
class MyClass
|
281
|
+
include Spira::Resource
|
282
|
+
property :property1, :type => MyModule::MyType
|
283
|
+
end
|
284
|
+
|
197
285
|
## Defining Repositories
|
198
286
|
|
199
287
|
You can define multiple repositories with Spira, and use more than one at a time:
|
@@ -225,24 +313,55 @@ Classes can specify a default repository to use other than `:default` with the
|
|
225
313
|
|
226
314
|
## Validations
|
227
315
|
|
228
|
-
|
229
|
-
|
316
|
+
You may declare any number of validation functions with the `validate` function.
|
317
|
+
Before saving, each referenced validation will be run, and the instance's
|
318
|
+
{Spira::Errors} object will be populated with any errors. You can use the
|
319
|
+
built in `assert` and assert helpers such as `assert_set` and
|
230
320
|
`asssert_numeric`.
|
231
321
|
|
232
322
|
|
233
323
|
class CD
|
234
|
-
|
235
|
-
|
236
|
-
assert(artist.name
|
324
|
+
validate :is_real_music
|
325
|
+
def is_real_music
|
326
|
+
assert(artist.name != "Nickelback", :artist, "cannot be Nickelback")
|
327
|
+
end
|
328
|
+
|
329
|
+
validate :track_count_numeric
|
330
|
+
def track_count_numeric
|
331
|
+
assert_numeric(track_count)
|
237
332
|
end
|
238
333
|
end
|
239
334
|
|
240
|
-
dancing-queen.artist =
|
335
|
+
dancing-queen.artist = nickelback
|
241
336
|
dancing-queen.save! #=> ValidationError
|
337
|
+
dancing-queen.errors.each => ["artist cannot be Nickelback"]
|
242
338
|
|
243
339
|
dancing-queen.artist = abba
|
244
340
|
dancing-queen.save! #=> true
|
245
341
|
|
342
|
+
|
343
|
+
## Inheritance
|
344
|
+
|
345
|
+
You can extend Spira resources without a problem:
|
346
|
+
|
347
|
+
class BoxedSet < CD
|
348
|
+
include Spira::Resource
|
349
|
+
property cd_count, :predicate => CD.count, :type => Integer
|
350
|
+
end
|
351
|
+
|
352
|
+
You can also make Spira modules and include them into other classes:
|
353
|
+
|
354
|
+
module Media
|
355
|
+
include Spira::Resource
|
356
|
+
property :format, :predicate => Media.format
|
357
|
+
end
|
358
|
+
|
359
|
+
class CD
|
360
|
+
include Spira::Resource
|
361
|
+
include Media
|
362
|
+
end
|
363
|
+
|
364
|
+
|
246
365
|
## Using Model Objects as RDF.rb Objects
|
247
366
|
|
248
367
|
All model objects are fully-functional as `RDF::Enumerable`, `RDF::Queryable`,
|
@@ -251,9 +370,10 @@ level. You can also access attributes that are not defined as properties.
|
|
251
370
|
|
252
371
|
## Support
|
253
372
|
|
254
|
-
There are a number of ways to ask for help. In declining order of
|
373
|
+
There are a number of ways to ask for help. In declining order of preference:
|
255
374
|
|
256
375
|
* Fork the project and write a failing test, or a pending test for a feature request
|
376
|
+
* Ask on the [public-rdf-ruby w3c mailing list][]
|
257
377
|
* You can post issues to the Github issue queue
|
258
378
|
* (there might one day be a google group or other such support channel, but not yet)
|
259
379
|
|
@@ -269,3 +389,5 @@ domain. For more information, see the included UNLICENSE file.
|
|
269
389
|
#### Contributing
|
270
390
|
Fork it on Github and go. Please make sure you're kosher with the UNLICENSE
|
271
391
|
file before contributing.
|
392
|
+
|
393
|
+
[public-rdf-ruby w3c mailing list]: http://lists.w3.org/Archives/Public/public-rdf-ruby/
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.1
|
1
|
+
0.0.1
|
data/lib/spira.rb
CHANGED
@@ -1,18 +1,62 @@
|
|
1
1
|
require 'rdf'
|
2
2
|
|
3
|
+
##
|
4
|
+
# Spira is a framework for building projections of RDF data into Ruby classes.
|
5
|
+
# It is built on top of RDF.rb.
|
6
|
+
#
|
7
|
+
# @see http://rdf.rubyforge.org
|
8
|
+
# @see http://github.com/bhuga/spira
|
9
|
+
# @see Spira::Resource
|
3
10
|
module Spira
|
4
11
|
|
5
|
-
|
12
|
+
##
|
13
|
+
# The list of repositories available for Spira resources
|
14
|
+
#
|
15
|
+
# @see http://rdf.rubyforge.org/RDF/Repository.html
|
16
|
+
# @return [Hash{Symbol => RDF::Repository}]
|
17
|
+
# @private
|
6
18
|
def repositories
|
7
19
|
settings[:repositories] ||= {}
|
8
20
|
end
|
9
21
|
module_function :repositories
|
10
22
|
|
23
|
+
##
|
24
|
+
# The list of all property types available for Spira resources
|
25
|
+
#
|
26
|
+
# @see Spira::Types
|
27
|
+
# @return [Hash{Symbol => Spira::Type}]
|
28
|
+
def types
|
29
|
+
settings[:types] ||= {}
|
30
|
+
end
|
31
|
+
module_function :types
|
32
|
+
|
33
|
+
##
|
34
|
+
# A thread-local hash for storing settings. Used by Resource classes.
|
35
|
+
#
|
36
|
+
# @see Spira::Resource
|
37
|
+
# @see Spira.repositories
|
38
|
+
# @see Spira.types
|
11
39
|
def settings
|
12
40
|
Thread.current[:spira] ||= {}
|
13
41
|
end
|
14
42
|
module_function :settings
|
15
43
|
|
44
|
+
##
|
45
|
+
# Add a repository to Spira's list of repositories.
|
46
|
+
#
|
47
|
+
# @overload add_repository(name, repo)
|
48
|
+
# @param [Symbol] name The name of this repository
|
49
|
+
# @param [RDF::Repository] repo An RDF::Repository
|
50
|
+
# @overload add_repository(name, klass, *args)
|
51
|
+
# @param [Symbol] name The name of this repository
|
52
|
+
# @param [RDF::Repository, Class] repo A Class that inherits from RDF::Repository
|
53
|
+
# @param [*Object] The list of arguments to instantiate the class
|
54
|
+
# @example Adding an ntriples file as a repository
|
55
|
+
# Spira.add_repository(:default, RDF::Repository.load('http://datagraph.org/jhacker/foaf.nt'))
|
56
|
+
# @example Adding an empty repository to be instantiated on use
|
57
|
+
# Spira.add_repository(:default, RDF::Repository)
|
58
|
+
# @return [Void]
|
59
|
+
# @see RDF::Repository
|
16
60
|
def add_repository(name, klass, *args)
|
17
61
|
repositories[name] = case klass
|
18
62
|
when RDF::Repository
|
@@ -29,12 +73,53 @@ module Spira
|
|
29
73
|
alias_method :add_repository!, :add_repository
|
30
74
|
module_function :add_repository, :add_repository!
|
31
75
|
|
76
|
+
##
|
77
|
+
# The RDF::Repository for the named repository
|
78
|
+
#
|
79
|
+
# @param [Symbol] name The name of the repository
|
80
|
+
# @return [RDF::Repository]
|
81
|
+
# @see RDF::Repository
|
32
82
|
def repository(name)
|
33
83
|
repositories[name]
|
34
84
|
end
|
35
85
|
module_function :repository
|
36
86
|
|
87
|
+
##
|
88
|
+
# Alias a property type to another. This allows a range of options to be
|
89
|
+
# specified for a property type which all reference one Spira::Type
|
90
|
+
#
|
91
|
+
# @param [Any] new The new symbol or reference
|
92
|
+
# @param [Any] original The type the new symbol should refer to
|
93
|
+
# @return [Void]
|
94
|
+
# @private
|
95
|
+
def type_alias(new, original)
|
96
|
+
types[new] = original
|
97
|
+
end
|
98
|
+
module_function :type_alias
|
99
|
+
|
37
100
|
autoload :Resource, 'spira/resource'
|
101
|
+
autoload :Type, 'spira/type'
|
102
|
+
autoload :Types, 'spira/types'
|
103
|
+
autoload :Errors, 'spira/errors'
|
104
|
+
autoload :VERSION, 'spira/version'
|
38
105
|
|
39
106
|
class ValidationError < StandardError; end
|
40
107
|
end
|
108
|
+
|
109
|
+
module RDF
|
110
|
+
class URI
|
111
|
+
##
|
112
|
+
# Create a projection of this URI as the given Spira::Resource class.
|
113
|
+
# Equivalent to `klass.for(self, *args)`
|
114
|
+
#
|
115
|
+
# @example Instantiating a URI as a Spira Resource
|
116
|
+
# RDF::URI('http://example.org/person/bob').as(Person)
|
117
|
+
# @param [Class] klass
|
118
|
+
# @param [*Any] args Any arguments to pass to klass.for
|
119
|
+
# @return [Klass] An instance of klass
|
120
|
+
def as(klass, *args)
|
121
|
+
raise ArgumentError, "#{klass} is not a Spira resource" unless klass.is_a?(Class) && klass.ancestors.include?(Spira::Resource)
|
122
|
+
klass.for(self, *args)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/spira/errors.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
module Spira
|
2
|
+
|
3
|
+
##
|
4
|
+
# Spira::Errors represents a collection of validation errors for a Spira
|
5
|
+
# resource. It tracks a list of errors for each field.
|
6
|
+
#
|
7
|
+
# This class does not perform validations. It only tracks the results of
|
8
|
+
# them.
|
9
|
+
class Errors
|
10
|
+
|
11
|
+
##
|
12
|
+
# Creates a new Spira::Errors
|
13
|
+
#
|
14
|
+
# @return [Spira::Errors]
|
15
|
+
def initialize
|
16
|
+
@errors = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Returns true if there are no errors, false otherwise
|
21
|
+
#
|
22
|
+
# @return [true, false]
|
23
|
+
def empty?
|
24
|
+
@errors.all? do |field, errors| errors.empty? end
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Returns true if there are errors, false otherwise
|
29
|
+
#
|
30
|
+
# @return [true, false]
|
31
|
+
def any?
|
32
|
+
!empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Returns true if the given property or list has any errors
|
37
|
+
#
|
38
|
+
# @param [Symbol] name The name of the property or list
|
39
|
+
# @return [true, false]
|
40
|
+
def any_for?(property)
|
41
|
+
!(@errors[property].nil?) && !(@errors[property].empty?)
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Add an error to a given property or list
|
46
|
+
#
|
47
|
+
# @example Add an error to a property
|
48
|
+
# errors.add(:username, "cannot be nil")
|
49
|
+
# @param [Symbol] property The property or list to add the error to
|
50
|
+
# @param [String] problem The error
|
51
|
+
# @return [Void]
|
52
|
+
def add(property, problem)
|
53
|
+
@errors[property] ||= []
|
54
|
+
@errors[property].push problem
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# The list of errors for a given property or list
|
59
|
+
#
|
60
|
+
# @example Get the errors for the `:username` field
|
61
|
+
# errors.add(:username, "cannot be nil")
|
62
|
+
# errors.for(:username) #=> ["cannot be nil"]
|
63
|
+
# @param [Symbol] property The property or list to check
|
64
|
+
# @return [Array<String>] The list of errors
|
65
|
+
def for(property)
|
66
|
+
@errors[property]
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Clear all errors
|
71
|
+
#
|
72
|
+
# @return [Void]
|
73
|
+
def clear
|
74
|
+
@errors = {}
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Return all errors as strings
|
79
|
+
#
|
80
|
+
# @example Get all errors
|
81
|
+
# errors.add(:username, "cannot be nil")
|
82
|
+
# errors.each #=> ["username cannot be nil"]
|
83
|
+
# @return [Array<String>]
|
84
|
+
def each
|
85
|
+
@errors.map do |property, problems|
|
86
|
+
problems.map do |problem|
|
87
|
+
property.to_s + " " + problem
|
88
|
+
end
|
89
|
+
end.flatten
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|