respect 0.1.0
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/MIT-LICENSE +20 -0
- data/README.md +289 -0
- data/RELATED_WORK.md +40 -0
- data/RELEASE_NOTES.md +23 -0
- data/Rakefile +31 -0
- data/STATUS_MATRIX.html +137 -0
- data/lib/respect.rb +231 -0
- data/lib/respect/any_schema.rb +22 -0
- data/lib/respect/array_def.rb +28 -0
- data/lib/respect/array_schema.rb +203 -0
- data/lib/respect/boolean_schema.rb +32 -0
- data/lib/respect/composite_schema.rb +86 -0
- data/lib/respect/core_statements.rb +206 -0
- data/lib/respect/datetime_schema.rb +27 -0
- data/lib/respect/def_without_name.rb +6 -0
- data/lib/respect/divisible_by_validator.rb +20 -0
- data/lib/respect/doc_helper.rb +24 -0
- data/lib/respect/doc_parser.rb +37 -0
- data/lib/respect/dsl_dumper.rb +181 -0
- data/lib/respect/equal_to_validator.rb +20 -0
- data/lib/respect/fake_name_proxy.rb +116 -0
- data/lib/respect/float_schema.rb +27 -0
- data/lib/respect/format_validator.rb +136 -0
- data/lib/respect/global_def.rb +79 -0
- data/lib/respect/greater_than_or_equal_to_validator.rb +19 -0
- data/lib/respect/greater_than_validator.rb +19 -0
- data/lib/respect/has_constraints.rb +34 -0
- data/lib/respect/hash_def.rb +40 -0
- data/lib/respect/hash_schema.rb +218 -0
- data/lib/respect/in_validator.rb +19 -0
- data/lib/respect/integer_schema.rb +27 -0
- data/lib/respect/ip_addr_schema.rb +23 -0
- data/lib/respect/ipv4_addr_schema.rb +27 -0
- data/lib/respect/ipv6_addr_schema.rb +27 -0
- data/lib/respect/items_def.rb +21 -0
- data/lib/respect/json_schema_html_formatter.rb +143 -0
- data/lib/respect/less_than_or_equal_to_validator.rb +19 -0
- data/lib/respect/less_than_validator.rb +19 -0
- data/lib/respect/match_validator.rb +19 -0
- data/lib/respect/max_length_validator.rb +20 -0
- data/lib/respect/min_length_validator.rb +20 -0
- data/lib/respect/multiple_of_validator.rb +10 -0
- data/lib/respect/null_schema.rb +26 -0
- data/lib/respect/numeric_schema.rb +33 -0
- data/lib/respect/org3_dumper.rb +213 -0
- data/lib/respect/regexp_schema.rb +19 -0
- data/lib/respect/schema.rb +285 -0
- data/lib/respect/schema_def.rb +16 -0
- data/lib/respect/string_schema.rb +21 -0
- data/lib/respect/unit_test_helper.rb +37 -0
- data/lib/respect/uri_schema.rb +23 -0
- data/lib/respect/utc_time_schema.rb +17 -0
- data/lib/respect/validator.rb +51 -0
- data/lib/respect/version.rb +3 -0
- data/test/any_schema_test.rb +79 -0
- data/test/array_def_test.rb +113 -0
- data/test/array_schema_test.rb +487 -0
- data/test/boolean_schema_test.rb +89 -0
- data/test/composite_schema_test.rb +30 -0
- data/test/datetime_schema_test.rb +83 -0
- data/test/doc_helper_test.rb +34 -0
- data/test/doc_parser_test.rb +109 -0
- data/test/dsl_dumper_test.rb +395 -0
- data/test/fake_name_proxy_test.rb +138 -0
- data/test/float_schema_test.rb +146 -0
- data/test/format_validator_test.rb +224 -0
- data/test/hash_def_test.rb +126 -0
- data/test/hash_schema_test.rb +613 -0
- data/test/integer_schema_test.rb +142 -0
- data/test/ip_addr_schema_test.rb +78 -0
- data/test/ipv4_addr_schema_test.rb +71 -0
- data/test/ipv6_addr_schema_test.rb +71 -0
- data/test/json_schema_html_formatter_test.rb +214 -0
- data/test/null_schema_test.rb +46 -0
- data/test/numeric_schema_test.rb +294 -0
- data/test/org3_dumper_test.rb +784 -0
- data/test/regexp_schema_test.rb +54 -0
- data/test/respect_test.rb +108 -0
- data/test/schema_def_test.rb +405 -0
- data/test/schema_test.rb +290 -0
- data/test/string_schema_test.rb +209 -0
- data/test/support/circle.rb +11 -0
- data/test/support/color.rb +24 -0
- data/test/support/point.rb +11 -0
- data/test/support/respect/circle_schema.rb +16 -0
- data/test/support/respect/color_def.rb +19 -0
- data/test/support/respect/color_schema.rb +33 -0
- data/test/support/respect/point_schema.rb +19 -0
- data/test/support/respect/rgba_schema.rb +20 -0
- data/test/support/respect/universal_validator.rb +25 -0
- data/test/support/respect/user_macros.rb +12 -0
- data/test/support/rgba.rb +11 -0
- data/test/test_helper.rb +90 -0
- data/test/uri_schema_test.rb +54 -0
- data/test/utc_time_schema_test.rb +63 -0
- data/test/validator_test.rb +22 -0
- metadata +288 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module Respect
|
2
|
+
# Validate a string containing a regexp but also accept Regexp object.
|
3
|
+
class RegexpSchema < StringSchema
|
4
|
+
|
5
|
+
def validate_type(object)
|
6
|
+
case object
|
7
|
+
when NilClass
|
8
|
+
if allow_nil?
|
9
|
+
nil
|
10
|
+
else
|
11
|
+
raise ValidationError, "object is nil but this #{self.class} does not allow nil"
|
12
|
+
end
|
13
|
+
else
|
14
|
+
FormatValidator.new(:regexp).validate(object)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end # class RegexpSchema
|
19
|
+
end # module Respect
|
@@ -0,0 +1,285 @@
|
|
1
|
+
module Respect
|
2
|
+
# Base class for all object schema.
|
3
|
+
#
|
4
|
+
# A schema defines the expected structure and format for a given object.
|
5
|
+
# It is similar in spirit to {json-schema.org}(http://json-schema.org/)
|
6
|
+
# specification but uses Ruby DSL as definition. Using the DSL is not
|
7
|
+
# mandatory since you can also defines a schema using its own methods.
|
8
|
+
#
|
9
|
+
# Almost all {Schema} sub-classes has an associated statement available in
|
10
|
+
# the DSL for defining it. This statement is named after the class name
|
11
|
+
# (see {Schema.statement_name}). However all sub-classes do not have a statement
|
12
|
+
# associated (see {Respect.schema_for}).
|
13
|
+
#
|
14
|
+
# You can define such a schema using the
|
15
|
+
# {define} method. The {#validate} method allow you to check whether
|
16
|
+
# the given object is valid according to this schema.
|
17
|
+
# Various options can be passed to the schema when initializing it.
|
18
|
+
#
|
19
|
+
# While validating an object the schema build a sanitized
|
20
|
+
# version of this object including all the validated part.
|
21
|
+
# The value presents in this sanitized object have generally a
|
22
|
+
# type specific to the contents they represents. For instance,
|
23
|
+
# a URI would be represented as a string in the original
|
24
|
+
# object but as a URI object in the sanitized object.
|
25
|
+
# There is a she-bang version of the validation method which
|
26
|
+
# update the value of the given object in-place with the value from
|
27
|
+
# the sanitized object if the validation succeeded.
|
28
|
+
#
|
29
|
+
# You can pass several options when creating a Schema:
|
30
|
+
# required:: whether this property associated to this schema is
|
31
|
+
# required in the hash schema (+true+ by default).
|
32
|
+
# default:: the default value to use for the associated property
|
33
|
+
# if it is not present. Setting a default value make
|
34
|
+
# the property private. (+nil+ by default)
|
35
|
+
# doc:: the documentation of this schema (+nil+ by default).
|
36
|
+
# A documentation is composed of a title followed by
|
37
|
+
# an empty line and an optional long description.
|
38
|
+
# If set to false, then this schema is considered
|
39
|
+
# as an implementation details that should not be
|
40
|
+
# publicly documented. Thus, it will not be dumped as
|
41
|
+
# {json-schema.org}[http://json-schema.org/].
|
42
|
+
# allow_nil:: whether the schema accept +nil+ as validation value.
|
43
|
+
# (+false+ by default). This option is not supported
|
44
|
+
# yet by the json-schema.org standard.
|
45
|
+
# These options applies to all schema sub-classes.
|
46
|
+
#
|
47
|
+
# In addition to these options, you can configure any defined
|
48
|
+
# {Validator}. Validators are run during validation process by
|
49
|
+
# certain schema class like {IntegerSchema}, {StringSchema},
|
50
|
+
# etc... They are mostly non-containers schema. In the
|
51
|
+
# following code the {GreaterThanValidator} will be run at
|
52
|
+
# validation time with the value +o+:
|
53
|
+
#
|
54
|
+
# IntegerSchema.define greater_than: 0
|
55
|
+
#
|
56
|
+
# This class is _abstract_. You cannot instantiate it directly.
|
57
|
+
# Use one of its sub-classes instead.
|
58
|
+
class Schema
|
59
|
+
include DocHelper
|
60
|
+
|
61
|
+
class << self
|
62
|
+
# Make this class abstract.
|
63
|
+
private :new
|
64
|
+
|
65
|
+
# If a corresponding _def_ class exists for this class
|
66
|
+
# (see {def_class}) it defines a new schema by evaluating the given
|
67
|
+
# +block+ in the context of this definition class. It behaves as an alias
|
68
|
+
# for new if no block is given.
|
69
|
+
#
|
70
|
+
# If there is no associated _def_ class the block is passed to the constructor.
|
71
|
+
def define(*args, &block)
|
72
|
+
def_class = self.def_class
|
73
|
+
if def_class
|
74
|
+
if block
|
75
|
+
def_class.eval(*args, &block)
|
76
|
+
else
|
77
|
+
self.new(*args)
|
78
|
+
end
|
79
|
+
else
|
80
|
+
self.new(*args, &block)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return the associated _def_ class name for this class.
|
85
|
+
# Example:
|
86
|
+
# ArraySchema.def_class_name #=> "ArrayDef"
|
87
|
+
# HashSchema.def_class_name #=> "HashDef"
|
88
|
+
# Schema.def_class_name #=> "SchemaDef"
|
89
|
+
def def_class_name
|
90
|
+
if self == Schema
|
91
|
+
"Respect::SchemaDef"
|
92
|
+
else
|
93
|
+
self.name.sub(/Schema$/, 'Def')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return the definition class symbol for this schema class or nil
|
98
|
+
# if there is no class (see {def_class_name})
|
99
|
+
def def_class
|
100
|
+
self.def_class_name.safe_constantize
|
101
|
+
end
|
102
|
+
|
103
|
+
# Build a statement name from this class name.
|
104
|
+
#
|
105
|
+
# Example:
|
106
|
+
# Schema.statement_name #=> "schema"
|
107
|
+
# HashSchema.statement_name #=> "hash"
|
108
|
+
def statement_name
|
109
|
+
self.name.underscore.sub(/^.*\//, '').sub(/_schema$/, '')
|
110
|
+
end
|
111
|
+
|
112
|
+
# Return the default options for this schema class.
|
113
|
+
# If you overwrite this method in sub-classes, call super and merge the
|
114
|
+
# result with your default options.
|
115
|
+
def default_options
|
116
|
+
{
|
117
|
+
required: true,
|
118
|
+
default: nil,
|
119
|
+
doc: nil,
|
120
|
+
allow_nil: false,
|
121
|
+
}.freeze
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
# Create a new schema using the given _options_.
|
127
|
+
def initialize(options = {})
|
128
|
+
@sanitized_object = nil
|
129
|
+
@options = self.class.default_options.merge(options)
|
130
|
+
end
|
131
|
+
|
132
|
+
def initialize_copy(other)
|
133
|
+
@options = other.options.dup
|
134
|
+
end
|
135
|
+
|
136
|
+
# Returns the sanitized object. It is +nil+ as long as you have not
|
137
|
+
# validated any object. It is overwritten every times you call
|
138
|
+
# {#validate}. If the validation failed it will be reset to +nil+.
|
139
|
+
attr_reader :sanitized_object
|
140
|
+
|
141
|
+
# Returns the hash of options.
|
142
|
+
attr_reader :options
|
143
|
+
|
144
|
+
# Returns the documentation of this schema.
|
145
|
+
def documentation
|
146
|
+
@options[:doc]
|
147
|
+
end
|
148
|
+
|
149
|
+
alias_method :doc, :documentation
|
150
|
+
|
151
|
+
# Returns whether this schema must be documented (i.e. not ignored
|
152
|
+
# when dumped).
|
153
|
+
def documented?
|
154
|
+
@options[:doc] != false
|
155
|
+
end
|
156
|
+
|
157
|
+
# Whether this schema is required. (opposite of optional?)
|
158
|
+
def required?
|
159
|
+
@options[:required] && !has_default?
|
160
|
+
end
|
161
|
+
|
162
|
+
# Whether this schema is optional.
|
163
|
+
def optional?
|
164
|
+
!required?
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns the default value used when this schema is missing.
|
168
|
+
def default
|
169
|
+
@options[:default]
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns whether this schema has a default value defined.
|
173
|
+
def has_default?
|
174
|
+
@options[:default] != nil
|
175
|
+
end
|
176
|
+
|
177
|
+
# Returns whether this schema accept +nil+ as validation value.
|
178
|
+
def allow_nil?
|
179
|
+
!!@options[:allow_nil]
|
180
|
+
end
|
181
|
+
|
182
|
+
# Return whether the given +object+ validates this schema.
|
183
|
+
# You can get the validation error via {#last_error}.
|
184
|
+
def validate?(object)
|
185
|
+
begin
|
186
|
+
validate(object)
|
187
|
+
true
|
188
|
+
rescue ValidationError => e
|
189
|
+
@last_error = e
|
190
|
+
false
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Return the last validation error that happens during the
|
195
|
+
# validation process. (set by {#validate?}).
|
196
|
+
# Reset each time {#validate?} is called.
|
197
|
+
attr_reader :last_error
|
198
|
+
|
199
|
+
# Raise a {ValidationError} if the given +object+ is not validated by this schema.
|
200
|
+
# Returns true otherwise. A sanitized version of the object is built during
|
201
|
+
# this process and you can access it via {#sanitized_object}.
|
202
|
+
# Rewrite it in sub-classes.
|
203
|
+
def validate(object)
|
204
|
+
raise NoMethodError, "overwrite me in sub-classes"
|
205
|
+
end
|
206
|
+
|
207
|
+
# Return +true+ or +false+ whether this schema validates the given +object+.
|
208
|
+
# If it does +object+ is updated in-place with the sanitized value.
|
209
|
+
# This method does not raise a {ValidationError}. You can access the error
|
210
|
+
# using {#last_error}.
|
211
|
+
def validate!(object)
|
212
|
+
valid = validate?(object)
|
213
|
+
if valid
|
214
|
+
sanitize_object!(object)
|
215
|
+
end
|
216
|
+
valid
|
217
|
+
end
|
218
|
+
|
219
|
+
# Sanitize the given +object+ *in-place* if it validates this schema. The sanitized object
|
220
|
+
# is returned. {ValidationError} is raised on error.
|
221
|
+
def sanitize!(object)
|
222
|
+
validate(object)
|
223
|
+
sanitize_object!(object)
|
224
|
+
end
|
225
|
+
|
226
|
+
# A shortcut for {Respect.sanitize_object!}.
|
227
|
+
def sanitize_object!(object)
|
228
|
+
Respect.sanitize_object!(object, self.sanitized_object)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Returns a string containing a human-readable representation of this schema.
|
232
|
+
def inspect
|
233
|
+
"#<%s:0x%x %s>" % [
|
234
|
+
self.class.name,
|
235
|
+
self.object_id,
|
236
|
+
instance_variables.map{|v| "#{v}=#{instance_variable_get(v).inspect}" }.join(", ")
|
237
|
+
]
|
238
|
+
end
|
239
|
+
|
240
|
+
# Returns a string containing ruby code defining this schema. Theoretically, you can
|
241
|
+
# evaluate it and get the same schema afterward.
|
242
|
+
def to_s
|
243
|
+
DslDumper.new(self).dump
|
244
|
+
end
|
245
|
+
|
246
|
+
# Serialize this schema to a JSON string following the given +format+.
|
247
|
+
def to_json(format = :org3)
|
248
|
+
case format
|
249
|
+
when :org3
|
250
|
+
self.to_h(:org3).to_json
|
251
|
+
else
|
252
|
+
raise ArgumentError, "unknown format '#{format}'"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Return the options with no default value.
|
257
|
+
# (Useful when writing a dumper)
|
258
|
+
def non_default_options
|
259
|
+
@options.select{|opt, value| value != self.class.default_options[opt] }
|
260
|
+
end
|
261
|
+
|
262
|
+
# Convert this schema to a hash representation following the given
|
263
|
+
# +format+.
|
264
|
+
def to_h(format = :org3)
|
265
|
+
case format
|
266
|
+
when :org3
|
267
|
+
Org3Dumper.new(self).dump
|
268
|
+
else
|
269
|
+
raise ArgumentError, "unknown format '#{format}'"
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# Two schema are equal if they have the same type and the set of options.
|
274
|
+
# Sub-class definition may include more attributes.
|
275
|
+
def ==(other)
|
276
|
+
self.class == other.class && @options == other.options
|
277
|
+
end
|
278
|
+
|
279
|
+
private
|
280
|
+
|
281
|
+
# Used by sub-classes to set the formatted object.
|
282
|
+
attr_writer :sanitized_object
|
283
|
+
|
284
|
+
end
|
285
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Respect
|
2
|
+
class StringSchema < Schema
|
3
|
+
include HasConstraints
|
4
|
+
|
5
|
+
public_class_method :new
|
6
|
+
|
7
|
+
def validate_type(object)
|
8
|
+
case object
|
9
|
+
when NilClass
|
10
|
+
if allow_nil?
|
11
|
+
nil
|
12
|
+
else
|
13
|
+
raise ValidationError, "object is nil but this #{self.class} does not allow nil"
|
14
|
+
end
|
15
|
+
else
|
16
|
+
object.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Respect
|
2
|
+
module UnitTestHelper
|
3
|
+
def assert_schema_validation_is(expected, schema, object, msg = nil)
|
4
|
+
valid = schema.validate?(object)
|
5
|
+
if expected
|
6
|
+
if !valid
|
7
|
+
if msg
|
8
|
+
message = msg
|
9
|
+
else
|
10
|
+
message = "Schema:\n#{schema}expected to validate object <#{object.inspect}> but failed with \"#{schema.last_error.context.join(" ")}\"."
|
11
|
+
end
|
12
|
+
assert false, message
|
13
|
+
end
|
14
|
+
else
|
15
|
+
if valid
|
16
|
+
if msg
|
17
|
+
message = msg
|
18
|
+
else
|
19
|
+
message = "Schema:\n#{schema}expected to invalidate object <#{object.inspect}> but succeed."
|
20
|
+
end
|
21
|
+
assert false, message
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def assert_schema_validate(schema, object, msg = nil)
|
27
|
+
assert_schema_validation_is(true, schema, object, msg)
|
28
|
+
end
|
29
|
+
|
30
|
+
def assert_schema_invalidate(schema, object, msg = nil)
|
31
|
+
assert_schema_validation_is(false, schema, object, msg)
|
32
|
+
end
|
33
|
+
|
34
|
+
end # module UnitTestHelper
|
35
|
+
end # module Respect
|
36
|
+
|
37
|
+
Test::Unit::TestCase.send :include, Respect::UnitTestHelper
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Respect
|
4
|
+
# Validate a string containing an URI but also accepts URI object.
|
5
|
+
class URISchema < StringSchema
|
6
|
+
|
7
|
+
def validate_type(object)
|
8
|
+
case object
|
9
|
+
when NilClass
|
10
|
+
if allow_nil?
|
11
|
+
nil
|
12
|
+
else
|
13
|
+
raise ValidationError, "object is nil but this #{self.class} does not allow nil"
|
14
|
+
end
|
15
|
+
when URI
|
16
|
+
object
|
17
|
+
else
|
18
|
+
FormatValidator.new(:uri).validate(object)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end # class URISchema
|
23
|
+
end # module Respect
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Respect
|
2
|
+
# A UTC time. It creates a Time object.
|
3
|
+
class UTCTimeSchema < NumericSchema
|
4
|
+
|
5
|
+
def validate_type(object)
|
6
|
+
value = super
|
7
|
+
unless value.nil?
|
8
|
+
if value < 0
|
9
|
+
raise ValidationError,
|
10
|
+
"UTC time value #{value} cannot be negative"
|
11
|
+
end
|
12
|
+
Time.at(value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end # class UTCTimeSchema
|
17
|
+
end # module Respect
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Respect
|
2
|
+
# A schema validator.
|
3
|
+
#
|
4
|
+
# Validator are an extensible way to validate certain properties of
|
5
|
+
# a schema. There are many validators available in this library (see
|
6
|
+
# all the sub-classes of this class).
|
7
|
+
#
|
8
|
+
# You can attach a validator to any schema through its options
|
9
|
+
# parameters when initializing it. Any schema including the
|
10
|
+
# {HasConstraints} module will execute them when its {Schema#validate}
|
11
|
+
# method is called.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
# # Will call GreaterThanValidator.new(42).validate(-1)
|
15
|
+
# IntegerSchema.define(greater_than: 42).validate?(-1) #=> true
|
16
|
+
#
|
17
|
+
# The validator API is *experimental* so it is not recommended to
|
18
|
+
# write your own.
|
19
|
+
class Validator
|
20
|
+
|
21
|
+
class << self
|
22
|
+
# Turn this validator class name into a constraint name.
|
23
|
+
def constraint_name
|
24
|
+
self.name.sub(/^.*::/, '').sub(/Validator$/, '').underscore
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate(value)
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
# Convert this validator to a Hash using the given +format+.
|
33
|
+
def to_h(format = :org3)
|
34
|
+
case format
|
35
|
+
when :org3
|
36
|
+
to_h_org3
|
37
|
+
else
|
38
|
+
raise ArgumentError, "unknown format '#{format}'"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Called when {#to_h} is called with +:org3+ format.
|
45
|
+
# Sub-classes are supposed to overwrite this methods and to return
|
46
|
+
# their conversion to the JSON schema standard draft v3.
|
47
|
+
def to_h_org3
|
48
|
+
raise NoMethodError, "overwrite me in sub-classes"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|