media_types 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/debian.yml +43 -43
  3. data/.github/workflows/publish-bookworm.yml +33 -0
  4. data/.github/workflows/publish-sid.yml +33 -0
  5. data/.github/workflows/ruby.yml +22 -22
  6. data/.gitignore +20 -10
  7. data/.rubocop.yml +29 -29
  8. data/CHANGELOG.md +175 -164
  9. data/Gemfile +6 -6
  10. data/LICENSE +21 -21
  11. data/README.md +666 -664
  12. data/Rakefile +12 -12
  13. data/bin/console +15 -15
  14. data/bin/setup +8 -8
  15. data/lib/media_types/constructable.rb +161 -160
  16. data/lib/media_types/dsl/errors.rb +18 -18
  17. data/lib/media_types/dsl.rb +172 -172
  18. data/lib/media_types/errors.rb +25 -19
  19. data/lib/media_types/formatter.rb +56 -56
  20. data/lib/media_types/hash.rb +21 -21
  21. data/lib/media_types/object.rb +35 -35
  22. data/lib/media_types/scheme/allow_nil.rb +30 -30
  23. data/lib/media_types/scheme/any_of.rb +41 -41
  24. data/lib/media_types/scheme/attribute.rb +46 -46
  25. data/lib/media_types/scheme/enumeration_context.rb +18 -18
  26. data/lib/media_types/scheme/enumeration_of_type.rb +80 -80
  27. data/lib/media_types/scheme/errors.rb +87 -87
  28. data/lib/media_types/scheme/links.rb +54 -54
  29. data/lib/media_types/scheme/missing_validation.rb +41 -41
  30. data/lib/media_types/scheme/not_strict.rb +15 -15
  31. data/lib/media_types/scheme/output_empty_guard.rb +45 -45
  32. data/lib/media_types/scheme/output_iterator_with_predicate.rb +66 -66
  33. data/lib/media_types/scheme/output_type_guard.rb +39 -39
  34. data/lib/media_types/scheme/rules.rb +186 -173
  35. data/lib/media_types/scheme/rules_exhausted_guard.rb +73 -73
  36. data/lib/media_types/scheme/validation_options.rb +44 -43
  37. data/lib/media_types/scheme.rb +535 -513
  38. data/lib/media_types/testing/assertions.rb +20 -20
  39. data/lib/media_types/validations.rb +118 -105
  40. data/lib/media_types/version.rb +5 -5
  41. data/lib/media_types/views.rb +12 -12
  42. data/lib/media_types.rb +73 -73
  43. data/media_types.gemspec +33 -33
  44. metadata +8 -6
@@ -1,41 +1,41 @@
1
- # frozen_string_literal: true
2
-
3
- require 'delegate'
4
-
5
- module MediaTypes
6
- class Scheme
7
- class CaseEqualityWithList < SimpleDelegator
8
-
9
- # True for Enumerable#any? {Object#===}
10
- def ===(other)
11
- any? { |it| it === other } # rubocop:disable Style/CaseEquality
12
- end
13
-
14
- def inspect
15
- "[Scheme::AnyOf(#{__getobj__})]"
16
- end
17
- end
18
-
19
- class << self
20
- # noinspection RubyClassMethodNamingConvention
21
- ##
22
- # Allows +it+ to be any of the wrapped +klazzes+
23
- #
24
- # @param [Array<Class>] klazzes the classes that are valid for +it+
25
- # @return [CaseEqualityWithList]
26
- def AnyOf(*klazzes) # rubocop:disable Naming/MethodName
27
- CaseEqualityWithList.new(klazzes)
28
- end
29
- end
30
-
31
- # noinspection RubyInstanceMethodNamingConvention
32
- ##
33
- # Allows +it+ to be any of the wrapped +klazzes+
34
- #
35
- # @param [Array<Class>] klazzes the classes that are valid for +it+
36
- # @return [CaseEqualityWithList]
37
- def AnyOf(*klazzes) # rubocop:disable Naming/MethodName
38
- self.class.AnyOf(*klazzes)
39
- end
40
- end
41
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ module MediaTypes
6
+ class Scheme
7
+ class CaseEqualityWithList < SimpleDelegator
8
+
9
+ # True for Enumerable#any? {Object#===}
10
+ def ===(other)
11
+ any? { |it| it === other } # rubocop:disable Style/CaseEquality
12
+ end
13
+
14
+ def inspect
15
+ "[Scheme::AnyOf(#{__getobj__})]"
16
+ end
17
+ end
18
+
19
+ class << self
20
+ # noinspection RubyClassMethodNamingConvention
21
+ ##
22
+ # Allows +it+ to be any of the wrapped +klazzes+
23
+ #
24
+ # @param [Array<Class>] klazzes the classes that are valid for +it+
25
+ # @return [CaseEqualityWithList]
26
+ def AnyOf(*klazzes) # rubocop:disable Naming/MethodName
27
+ CaseEqualityWithList.new(klazzes)
28
+ end
29
+ end
30
+
31
+ # noinspection RubyInstanceMethodNamingConvention
32
+ ##
33
+ # Allows +it+ to be any of the wrapped +klazzes+
34
+ #
35
+ # @param [Array<Class>] klazzes the classes that are valid for +it+
36
+ # @return [CaseEqualityWithList]
37
+ def AnyOf(*klazzes) # rubocop:disable Naming/MethodName
38
+ self.class.AnyOf(*klazzes)
39
+ end
40
+ end
41
+ end
@@ -1,46 +1,46 @@
1
- # frozen_string_literal: true
2
-
3
- require 'media_types/scheme/errors'
4
- require 'media_types/scheme/allow_nil'
5
-
6
- module MediaTypes
7
- class Scheme
8
- class Attribute
9
-
10
- ##
11
- # An attribute that expects a value of type +type+
12
- #
13
- # @see AllowNil
14
- # @see AnyOf
15
- #
16
- # @param [Class] type the class +it+ must be
17
- # @param [TrueClass, FalseClass] allow_nil if true, nil? is allowed
18
- #
19
- def initialize(type, allow_nil: false)
20
- self.type = allow_nil ? Scheme.AllowNil(type) : type
21
-
22
- freeze
23
- end
24
-
25
- def validate!(output, options, **_opts)
26
- return true if type === output # rubocop:disable Style/CaseEquality
27
- raise ValidationError,
28
- format(
29
- 'Expected %<type>s, got %<actual>s at [%<backtrace>s]',
30
- type: type,
31
- actual: output.inspect,
32
- backtrace: options.backtrace.join('->')
33
- )
34
- end
35
-
36
- def inspect
37
- "[Scheme::Attribute of #{type.inspect}]"
38
- end
39
-
40
- private
41
-
42
- attr_accessor :allow_nil, :type
43
-
44
- end
45
- end
46
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'media_types/scheme/errors'
4
+ require 'media_types/scheme/allow_nil'
5
+
6
+ module MediaTypes
7
+ class Scheme
8
+ class Attribute
9
+
10
+ ##
11
+ # An attribute that expects a value of type +type+
12
+ #
13
+ # @see AllowNil
14
+ # @see AnyOf
15
+ #
16
+ # @param [Class] type the class +it+ must be
17
+ # @param [TrueClass, FalseClass] allow_nil if true, nil? is allowed
18
+ #
19
+ def initialize(type, allow_nil: false)
20
+ self.type = allow_nil ? Scheme.AllowNil(type) : type
21
+
22
+ freeze
23
+ end
24
+
25
+ def validate!(output, options, **_opts)
26
+ return true if type === output # rubocop:disable Style/CaseEquality
27
+ raise ValidationError,
28
+ format(
29
+ 'Expected %<type>s, got %<actual>s at [%<backtrace>s]',
30
+ type: type,
31
+ actual: output.inspect,
32
+ backtrace: options.backtrace.join('->')
33
+ )
34
+ end
35
+
36
+ def inspect
37
+ "[Scheme::Attribute of #{type.inspect}]"
38
+ end
39
+
40
+ private
41
+
42
+ attr_accessor :allow_nil, :type
43
+
44
+ end
45
+ end
46
+ end
@@ -1,18 +1,18 @@
1
- # frozen_string_literal: true
2
-
3
- module MediaTypes
4
- class Scheme
5
- class EnumerationContext
6
- def initialize(rules:)
7
- self.rules = rules
8
- end
9
-
10
- def enumerate(val)
11
- self.key = val
12
- self
13
- end
14
-
15
- attr_accessor :rules, :key
16
- end
17
- end
18
- end
1
+ # frozen_string_literal: true
2
+
3
+ module MediaTypes
4
+ class Scheme
5
+ class EnumerationContext
6
+ def initialize(rules:)
7
+ self.rules = rules
8
+ end
9
+
10
+ def enumerate(val)
11
+ self.key = val
12
+ self
13
+ end
14
+
15
+ attr_accessor :rules, :key
16
+ end
17
+ end
18
+ end
@@ -1,80 +1,80 @@
1
- # frozen_string_literal: true
2
-
3
- require 'media_types/scheme/errors'
4
-
5
- module MediaTypes
6
- class Scheme
7
- class EnumerationOfType
8
-
9
- ##
10
- # An attribute that expects a value of type +enumeration_type+ and each item of type +item_type+
11
- #
12
- # @param [Class] item_type the type of each item
13
- # @param [Class] enumeration_type the type of the enumeration as a whole
14
- # @param [TrueClass, FalseClass] allow_empty if true, an empty instance of +enumeration_type+ is valid
15
- #
16
- def initialize(item_type, enumeration_type: Array, allow_empty: false)
17
- self.item_type = item_type
18
- self.enumeration_type = enumeration_type
19
- self.allow_empty = allow_empty
20
-
21
- freeze
22
- end
23
-
24
- def validate!(output, options, **_opts)
25
- validate_enumeration!(output, options) &&
26
- validate_not_empty!(output, options) &&
27
- validate_items!(output, options)
28
- end
29
-
30
- def inspect
31
- "[Scheme::EnumerationOfType #{item_type} collection=#{enumeration_type} empty=#{allow_empty}]"
32
- end
33
-
34
- private
35
-
36
- attr_accessor :allow_empty, :enumeration_type, :item_type
37
-
38
- def validate_enumeration!(output, options)
39
- return true if enumeration_type === output # rubocop:disable Style/CaseEquality
40
- raise ValidationError,
41
- format(
42
- 'Expected collection as %<type>s, got %<actual>s at [%<backtrace>s]',
43
- type: enumeration_type,
44
- actual: output.inspect,
45
- backtrace: options.backtrace.join('->')
46
- )
47
- end
48
-
49
- def validate_not_empty!(output, options)
50
- return true if allow_empty
51
- return true if output.respond_to?(:length) && output.length.positive?
52
- return true if output.respond_to?(:empty?) && !output.empty?
53
-
54
- raise EmptyOutputError,
55
- format(
56
- 'Expected collection to be not empty, got empty at [%<backtrace>s]',
57
- backtrace: options.backtrace.join('->')
58
- )
59
- end
60
-
61
- def validate_items!(output, options)
62
- output.each_with_index.all? do |item, index|
63
- next true if item_type === item # rubocop:disable Style/CaseEquality
64
- if item_type.is_a?(Scheme)
65
- item_type.validate(item, options.trace("[#{index}]"))
66
- next true
67
- end
68
- raise ValidationError,
69
- format(
70
- 'Expected collection item as %<type>s, got %<actual>s at [%<backtrace>s]',
71
- type: item_type.inspect,
72
- actual: item.inspect,
73
- backtrace: options.backtrace.join('->')
74
- )
75
- end
76
- end
77
-
78
- end
79
- end
80
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'media_types/scheme/errors'
4
+
5
+ module MediaTypes
6
+ class Scheme
7
+ class EnumerationOfType
8
+
9
+ ##
10
+ # An attribute that expects a value of type +enumeration_type+ and each item of type +item_type+
11
+ #
12
+ # @param [Class] item_type the type of each item
13
+ # @param [Class] enumeration_type the type of the enumeration as a whole
14
+ # @param [TrueClass, FalseClass] allow_empty if true, an empty instance of +enumeration_type+ is valid
15
+ #
16
+ def initialize(item_type, enumeration_type: Array, allow_empty: false)
17
+ self.item_type = item_type
18
+ self.enumeration_type = enumeration_type
19
+ self.allow_empty = allow_empty
20
+
21
+ freeze
22
+ end
23
+
24
+ def validate!(output, options, **_opts)
25
+ validate_enumeration!(output, options) &&
26
+ validate_not_empty!(output, options) &&
27
+ validate_items!(output, options)
28
+ end
29
+
30
+ def inspect
31
+ "[Scheme::EnumerationOfType #{item_type} collection=#{enumeration_type} empty=#{allow_empty}]"
32
+ end
33
+
34
+ private
35
+
36
+ attr_accessor :allow_empty, :enumeration_type, :item_type
37
+
38
+ def validate_enumeration!(output, options)
39
+ return true if enumeration_type === output # rubocop:disable Style/CaseEquality
40
+ raise ValidationError,
41
+ format(
42
+ 'Expected collection as %<type>s, got %<actual>s at [%<backtrace>s]',
43
+ type: enumeration_type,
44
+ actual: output.inspect,
45
+ backtrace: options.backtrace.join('->')
46
+ )
47
+ end
48
+
49
+ def validate_not_empty!(output, options)
50
+ return true if allow_empty
51
+ return true if output.respond_to?(:length) && output.length.positive?
52
+ return true if output.respond_to?(:empty?) && !output.empty?
53
+
54
+ raise EmptyOutputError,
55
+ format(
56
+ 'Expected collection to be not empty, got empty at [%<backtrace>s]',
57
+ backtrace: options.backtrace.join('->')
58
+ )
59
+ end
60
+
61
+ def validate_items!(output, options)
62
+ output.each_with_index.all? do |item, index|
63
+ next true if item_type === item # rubocop:disable Style/CaseEquality
64
+ if item_type.is_a?(Scheme)
65
+ item_type.validate(item, options.trace("[#{index}]"))
66
+ next true
67
+ end
68
+ raise ValidationError,
69
+ format(
70
+ 'Expected collection item as %<type>s, got %<actual>s at [%<backtrace>s]',
71
+ type: item_type.inspect,
72
+ actual: item.inspect,
73
+ backtrace: options.backtrace.join('->')
74
+ )
75
+ end
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -1,87 +1,87 @@
1
- # frozen_string_literal: true
2
-
3
- module MediaTypes
4
- class Scheme
5
- class ConflictingTypeDefinitionError < ArgumentError; end
6
-
7
- # Base class for all validations errors
8
- class ValidationError < ArgumentError; end
9
-
10
- # Raised when trying to register an attribute with a non-string key
11
- class KeyTypeError < ArgumentError; end
12
-
13
- # Raised when trying to register a key twice
14
- class DuplicateKeyError < ArgumentError; end
15
-
16
- class DuplicateSymbolKeyError < DuplicateKeyError
17
- MESSAGE_TEMPLATE = '%<rule_type>s rule with key :%<key>s has already been defined. Please remove one of the two.'
18
-
19
- def initialize(rule_type, key)
20
- super(format(MESSAGE_TEMPLATE, rule_type: rule_type, key: key))
21
- end
22
- end
23
-
24
- class DuplicateStringKeyError < DuplicateKeyError
25
- MESSAGE_TEMPLATE = '%<rule_type>s rule with key %<key>s has already been defined. Please remove one of the two.'
26
-
27
- def initialize(rule_type, key)
28
- super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
29
- end
30
- end
31
-
32
- class StringOverwritingSymbolError < DuplicateKeyError
33
- MESSAGE_TEMPLATE = 'Trying to add %<rule_type>s rule String key %<key>s while a Symbol with the same name already exists. Please remove one of the two.'
34
-
35
- def initialize(rule_type, key)
36
- super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
37
- end
38
- end
39
-
40
- class SymbolOverwritingStringError < DuplicateKeyError
41
- MESSAGE_TEMPLATE = 'Trying to add %<rule_type>s rule with Symbol key :%<key>s while a String key with the same name already exists. Please remove one of the two.'
42
-
43
- def initialize(rule_type, key)
44
- super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
45
- end
46
- end
47
-
48
- # Raised when it did not expect more data, but there was more left
49
- class StrictValidationError < ValidationError; end
50
-
51
- # Raised when it expected not to be empty, but it was
52
- class EmptyOutputError < ValidationError; end
53
-
54
- # Raised when a value did not have the expected type
55
- class OutputTypeMismatch < ValidationError; end
56
-
57
- # Raised when it expected more data but there wasn't any left
58
- class ExhaustedOutputError < ValidationError; end
59
-
60
- # Raised when trying to override a non default rule scheme in the Rules Hash's default object method
61
- class OverwritingRuleError < ArgumentError; end
62
-
63
- class DuplicateAnyRuleError < OverwritingRuleError
64
- def message
65
- "An 'any' rule has already been defined. Please remove one of the two."
66
- end
67
- end
68
-
69
- class DuplicateNotStrictRuleError < OverwritingRuleError
70
- def message
71
- "The 'not_strict' rule has already been defined. Please remove one of the two."
72
- end
73
- end
74
-
75
- class NotStrictOverwritingAnyError < OverwritingRuleError
76
- def message
77
- "An 'any' rule has already been defined. Setting 'not_strict' will override that rule. Please remove one of the two."
78
- end
79
- end
80
-
81
- class AnyOverwritingNotStrictError < OverwritingRuleError
82
- def message
83
- "The 'not_strict' rule has already been defined. Setting 'any' will override that rule. Please remove one of the two."
84
- end
85
- end
86
- end
87
- end
1
+ # frozen_string_literal: true
2
+
3
+ module MediaTypes
4
+ class Scheme
5
+ class ConflictingTypeDefinitionError < ArgumentError; end
6
+
7
+ # Base class for all validations errors
8
+ class ValidationError < ArgumentError; end
9
+
10
+ # Raised when trying to register an attribute with a non-string key
11
+ class KeyTypeError < ArgumentError; end
12
+
13
+ # Raised when trying to register a key twice
14
+ class DuplicateKeyError < ArgumentError; end
15
+
16
+ class DuplicateSymbolKeyError < DuplicateKeyError
17
+ MESSAGE_TEMPLATE = '%<rule_type>s rule with key :%<key>s has already been defined. Please remove one of the two.'
18
+
19
+ def initialize(rule_type, key)
20
+ super(format(MESSAGE_TEMPLATE, rule_type: rule_type, key: key))
21
+ end
22
+ end
23
+
24
+ class DuplicateStringKeyError < DuplicateKeyError
25
+ MESSAGE_TEMPLATE = '%<rule_type>s rule with key %<key>s has already been defined. Please remove one of the two.'
26
+
27
+ def initialize(rule_type, key)
28
+ super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
29
+ end
30
+ end
31
+
32
+ class StringOverwritingSymbolError < DuplicateKeyError
33
+ MESSAGE_TEMPLATE = 'Trying to add %<rule_type>s rule String key %<key>s while a Symbol with the same name already exists. Please remove one of the two.'
34
+
35
+ def initialize(rule_type, key)
36
+ super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
37
+ end
38
+ end
39
+
40
+ class SymbolOverwritingStringError < DuplicateKeyError
41
+ MESSAGE_TEMPLATE = 'Trying to add %<rule_type>s rule with Symbol key :%<key>s while a String key with the same name already exists. Please remove one of the two.'
42
+
43
+ def initialize(rule_type, key)
44
+ super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
45
+ end
46
+ end
47
+
48
+ # Raised when it did not expect more data, but there was more left
49
+ class StrictValidationError < ValidationError; end
50
+
51
+ # Raised when it expected not to be empty, but it was
52
+ class EmptyOutputError < ValidationError; end
53
+
54
+ # Raised when a value did not have the expected type
55
+ class OutputTypeMismatch < ValidationError; end
56
+
57
+ # Raised when it expected more data but there wasn't any left
58
+ class ExhaustedOutputError < ValidationError; end
59
+
60
+ # Raised when trying to override a non default rule scheme in the Rules Hash's default object method
61
+ class OverwritingRuleError < ArgumentError; end
62
+
63
+ class DuplicateAnyRuleError < OverwritingRuleError
64
+ def message
65
+ "An 'any' rule has already been defined. Please remove one of the two."
66
+ end
67
+ end
68
+
69
+ class DuplicateNotStrictRuleError < OverwritingRuleError
70
+ def message
71
+ "The 'not_strict' rule has already been defined. Please remove one of the two."
72
+ end
73
+ end
74
+
75
+ class NotStrictOverwritingAnyError < OverwritingRuleError
76
+ def message
77
+ "An 'any' rule has already been defined. Setting 'not_strict' will override that rule. Please remove one of the two."
78
+ end
79
+ end
80
+
81
+ class AnyOverwritingNotStrictError < OverwritingRuleError
82
+ def message
83
+ "The 'not_strict' rule has already been defined. Setting 'any' will override that rule. Please remove one of the two."
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,54 +1,54 @@
1
- # frozen_string_literal: true
2
-
3
- require 'media_types/scheme/rules'
4
- require 'media_types/scheme/rules_exhausted_guard'
5
-
6
- module MediaTypes
7
- class Scheme
8
- class Links
9
- def initialize
10
- self.links = Rules.new(allow_empty: false, expected_type: ::Hash)
11
- end
12
-
13
- def link(key, allow_nil: false, optional: false, &block)
14
- links.add(
15
- key,
16
- Scheme.new do
17
- attribute :href, String, allow_nil: allow_nil
18
- instance_exec(&block) if block_given?
19
- end,
20
- optional: optional
21
- )
22
-
23
- self
24
- end
25
-
26
- def validate!(output, options, **_opts)
27
- RulesExhaustedGuard.call(output, options, rules: links)
28
- end
29
-
30
- def inspect
31
- "[Scheme::Links #{links.keys}]"
32
- end
33
-
34
- def run_fixture_validations(expect_symbol_keys, backtrace = [])
35
- fixture_errors = @links.flat_map {|key, rule|
36
- if rule.is_a?(Scheme)
37
- begin
38
- rule.run_fixture_validations(expect_symbol_keys, backtrace.dup.append(key))
39
- nil
40
- rescue AssertionError => e
41
- e.fixture_errors
42
- end
43
- end
44
- }.compact
45
-
46
- raise AssertionError.new(fixture_errors) unless fixture_errors.empty?
47
- end
48
-
49
- private
50
-
51
- attr_accessor :links
52
- end
53
- end
54
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'media_types/scheme/rules'
4
+ require 'media_types/scheme/rules_exhausted_guard'
5
+
6
+ module MediaTypes
7
+ class Scheme
8
+ class Links
9
+ def initialize
10
+ self.links = Rules.new(allow_empty: false, expected_type: ::Hash)
11
+ end
12
+
13
+ def link(key, allow_nil: false, optional: false, &block)
14
+ links.add(
15
+ key,
16
+ Scheme.new do
17
+ attribute :href, String, allow_nil: allow_nil
18
+ instance_exec(&block) if block_given?
19
+ end,
20
+ optional: optional
21
+ )
22
+
23
+ self
24
+ end
25
+
26
+ def validate!(output, options, **_opts)
27
+ RulesExhaustedGuard.call(output, options, rules: links)
28
+ end
29
+
30
+ def inspect
31
+ "[Scheme::Links #{links.keys}]"
32
+ end
33
+
34
+ def run_fixture_validations(expect_symbol_keys, backtrace = [])
35
+ fixture_errors = @links.flat_map {|key, rule|
36
+ if rule.is_a?(Scheme)
37
+ begin
38
+ rule.run_fixture_validations(expect_symbol_keys, backtrace.dup.append(key))
39
+ nil
40
+ rescue AssertionError => e
41
+ e.fixture_errors
42
+ end
43
+ end
44
+ }.compact
45
+
46
+ raise AssertionError.new(fixture_errors) unless fixture_errors.empty?
47
+ end
48
+
49
+ private
50
+
51
+ attr_accessor :links
52
+ end
53
+ end
54
+ end