attributor 5.0.2 → 5.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +30 -0
- data/.travis.yml +6 -4
- data/CHANGELOG.md +6 -1
- data/Gemfile +1 -1
- data/Guardfile +14 -8
- data/Rakefile +4 -5
- data/attributor.gemspec +34 -29
- data/lib/attributor.rb +23 -29
- data/lib/attributor/attribute.rb +108 -127
- data/lib/attributor/attribute_resolver.rb +12 -26
- data/lib/attributor/dsl_compiler.rb +17 -21
- data/lib/attributor/dumpable.rb +1 -2
- data/lib/attributor/example_mixin.rb +5 -8
- data/lib/attributor/exceptions.rb +5 -6
- data/lib/attributor/extensions/randexp.rb +3 -5
- data/lib/attributor/extras/field_selector.rb +4 -4
- data/lib/attributor/extras/field_selector/transformer.rb +6 -7
- data/lib/attributor/families/numeric.rb +0 -2
- data/lib/attributor/families/temporal.rb +1 -4
- data/lib/attributor/hash_dsl_compiler.rb +22 -25
- data/lib/attributor/type.rb +24 -32
- data/lib/attributor/types/bigdecimal.rb +7 -14
- data/lib/attributor/types/boolean.rb +5 -8
- data/lib/attributor/types/class.rb +9 -10
- data/lib/attributor/types/collection.rb +34 -44
- data/lib/attributor/types/container.rb +9 -15
- data/lib/attributor/types/csv.rb +7 -10
- data/lib/attributor/types/date.rb +20 -25
- data/lib/attributor/types/date_time.rb +7 -14
- data/lib/attributor/types/float.rb +4 -6
- data/lib/attributor/types/hash.rb +171 -196
- data/lib/attributor/types/ids.rb +2 -6
- data/lib/attributor/types/integer.rb +12 -17
- data/lib/attributor/types/model.rb +39 -48
- data/lib/attributor/types/object.rb +2 -4
- data/lib/attributor/types/polymorphic.rb +118 -0
- data/lib/attributor/types/regexp.rb +4 -5
- data/lib/attributor/types/string.rb +6 -7
- data/lib/attributor/types/struct.rb +8 -15
- data/lib/attributor/types/symbol.rb +3 -6
- data/lib/attributor/types/tempfile.rb +5 -6
- data/lib/attributor/types/time.rb +11 -11
- data/lib/attributor/types/uri.rb +9 -10
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_resolver_spec.rb +57 -78
- data/spec/attribute_spec.rb +174 -216
- data/spec/attributor_spec.rb +11 -15
- data/spec/dsl_compiler_spec.rb +19 -33
- data/spec/dumpable_spec.rb +6 -7
- data/spec/extras/field_selector/field_selector_spec.rb +1 -1
- data/spec/families_spec.rb +1 -3
- data/spec/hash_dsl_compiler_spec.rb +65 -74
- data/spec/spec_helper.rb +9 -3
- data/spec/support/hashes.rb +2 -3
- data/spec/support/models.rb +30 -36
- data/spec/support/polymorphics.rb +10 -0
- data/spec/type_spec.rb +38 -61
- data/spec/types/bigdecimal_spec.rb +11 -15
- data/spec/types/boolean_spec.rb +12 -39
- data/spec/types/class_spec.rb +10 -11
- data/spec/types/collection_spec.rb +72 -81
- data/spec/types/container_spec.rb +22 -26
- data/spec/types/csv_spec.rb +15 -16
- data/spec/types/date_spec.rb +16 -33
- data/spec/types/date_time_spec.rb +16 -33
- data/spec/types/file_upload_spec.rb +1 -2
- data/spec/types/float_spec.rb +7 -14
- data/spec/types/hash_spec.rb +285 -289
- data/spec/types/ids_spec.rb +5 -7
- data/spec/types/integer_spec.rb +37 -46
- data/spec/types/model_spec.rb +111 -128
- data/spec/types/polymorphic_spec.rb +134 -0
- data/spec/types/regexp_spec.rb +4 -7
- data/spec/types/string_spec.rb +17 -21
- data/spec/types/struct_spec.rb +40 -47
- data/spec/types/tempfile_spec.rb +1 -2
- data/spec/types/temporal_spec.rb +9 -0
- data/spec/types/time_spec.rb +16 -32
- data/spec/types/type_spec.rb +15 -0
- data/spec/types/uri_spec.rb +6 -7
- metadata +77 -25
data/lib/attributor/type.rb
CHANGED
@@ -1,38 +1,34 @@
|
|
1
1
|
module Attributor
|
2
|
-
|
3
2
|
# It is the abstract base class to hold an attribute, both a leaf and a container (hash/Array...)
|
4
3
|
# TODO: should this be a mixin since it is an abstract class?
|
5
4
|
module Type
|
6
|
-
|
7
5
|
def self.included(klass)
|
8
6
|
klass.extend(ClassMethods)
|
9
7
|
end
|
10
8
|
|
11
9
|
module ClassMethods
|
12
|
-
|
13
10
|
# Does this type support the generation of subtypes?
|
14
11
|
def constructable?
|
15
12
|
false
|
16
13
|
end
|
17
14
|
|
18
15
|
# Allow a type to be marked as if it was anonymous (i.e. not referenceable by name)
|
19
|
-
def anonymous_type(val=true)
|
16
|
+
def anonymous_type(val = true)
|
20
17
|
@_anonymous = val
|
21
18
|
end
|
22
19
|
|
23
20
|
def anonymous?
|
24
|
-
if @_anonymous
|
25
|
-
|
21
|
+
if @_anonymous.nil?
|
22
|
+
name.nil? # if nothing is set, consider it anonymous if the class does not have a name
|
26
23
|
else
|
27
24
|
@_anonymous
|
28
25
|
end
|
29
26
|
end
|
30
27
|
|
31
|
-
|
32
28
|
# Generic decoding and coercion of the attribute.
|
33
|
-
def load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **
|
29
|
+
def load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
34
30
|
return nil if value.nil?
|
35
|
-
unless value.is_a?(
|
31
|
+
unless value.is_a?(native_type)
|
36
32
|
raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
|
37
33
|
end
|
38
34
|
|
@@ -40,13 +36,13 @@ module Attributor
|
|
40
36
|
end
|
41
37
|
|
42
38
|
# Generic encoding of the attribute
|
43
|
-
def dump(value
|
39
|
+
def dump(value, **_opts)
|
44
40
|
value
|
45
41
|
end
|
46
42
|
|
47
43
|
# TODO: refactor this to take just the options instead of the full attribute?
|
48
44
|
# TODO: delegate to subclass
|
49
|
-
def validate(value,context=Attributor::DEFAULT_ROOT_CONTEXT,attribute)
|
45
|
+
def validate(value, context = Attributor::DEFAULT_ROOT_CONTEXT, attribute) # rubocop:disable Style/OptionalArguments
|
50
46
|
errors = []
|
51
47
|
attribute.options.each do |option, opt_definition|
|
52
48
|
case option
|
@@ -55,7 +51,7 @@ module Attributor
|
|
55
51
|
when :min
|
56
52
|
errors << "#{Attributor.humanize_context(context)} value (#{value}) is smaller than the allowed min (#{opt_definition.inspect})" unless value >= opt_definition
|
57
53
|
when :regexp
|
58
|
-
errors << "#{Attributor.humanize_context(context)} value (#{value}) does not match regexp (#{opt_definition.inspect})"
|
54
|
+
errors << "#{Attributor.humanize_context(context)} value (#{value}) does not match regexp (#{opt_definition.inspect})" unless value =~ opt_definition
|
59
55
|
end
|
60
56
|
end
|
61
57
|
errors
|
@@ -65,35 +61,32 @@ module Attributor
|
|
65
61
|
def valid_type?(value)
|
66
62
|
return value.is_a?(native_type) if respond_to?(:native_type)
|
67
63
|
|
68
|
-
raise AttributorException
|
64
|
+
raise AttributorException, "#{self} must implement #valid_type? or #native_type"
|
69
65
|
end
|
70
66
|
|
71
67
|
# Default, overridable example function
|
72
|
-
def example(
|
73
|
-
raise AttributorException
|
68
|
+
def example(_context = nil, options: {})
|
69
|
+
raise AttributorException, "#{self} must implement #example"
|
74
70
|
end
|
75
71
|
|
76
|
-
|
77
72
|
# HELPER FUNCTIONS
|
78
73
|
|
79
|
-
|
80
74
|
def check_option!(name, definition)
|
81
75
|
case name
|
82
76
|
when :min
|
83
|
-
raise AttributorException
|
77
|
+
raise AttributorException, "Value for option :min does not implement '<='. Got: (#{definition.inspect})" unless definition.respond_to?(:<=)
|
84
78
|
when :max
|
85
|
-
raise AttributorException
|
79
|
+
raise AttributorException, "Value for option :max does not implement '>='. Got(#{definition.inspect})" unless definition.respond_to?(:>=)
|
86
80
|
when :regexp
|
87
81
|
# could go for a respoind_to? :=~ here, but that seems overly... cute... and not useful.
|
88
|
-
raise AttributorException
|
82
|
+
raise AttributorException, "Value for option :regexp is not a Regexp object. Got (#{definition.inspect})" unless definition.is_a? ::Regexp
|
89
83
|
else
|
90
84
|
return :unknown
|
91
85
|
end
|
92
86
|
|
93
|
-
|
87
|
+
:ok
|
94
88
|
end
|
95
89
|
|
96
|
-
|
97
90
|
def generate_subcontext(context, subname)
|
98
91
|
context + [subname]
|
99
92
|
end
|
@@ -103,20 +96,20 @@ module Attributor
|
|
103
96
|
end
|
104
97
|
|
105
98
|
# By default, non complex types will not have a DSL subdefinition this handles such case
|
106
|
-
def compile_dsl(
|
107
|
-
raise AttributorException
|
99
|
+
def compile_dsl(options, block)
|
100
|
+
raise AttributorException, 'Basic structures cannot take extra block definitions' if block
|
108
101
|
# Simply create a DSL compiler to store the options, and not to parse any DSL
|
109
|
-
sub_definition=dsl_compiler.new(
|
110
|
-
|
102
|
+
sub_definition = dsl_compiler.new(options)
|
103
|
+
sub_definition
|
111
104
|
end
|
112
105
|
|
113
106
|
# Default describe for simple types...only their name (stripping the base attributor module)
|
114
|
-
def describe(
|
107
|
+
def describe(_root = false, example: nil)
|
115
108
|
type_name = Attributor.type_name(self)
|
116
109
|
hash = {
|
117
110
|
name: type_name.gsub(Attributor::MODULE_PREFIX_REGEX, ''),
|
118
|
-
family:
|
119
|
-
id:
|
111
|
+
family: family,
|
112
|
+
id: id
|
120
113
|
}
|
121
114
|
hash[:anonymous] = @_anonymous unless @_anonymous.nil?
|
122
115
|
hash[:example] = example if example
|
@@ -124,14 +117,13 @@ module Attributor
|
|
124
117
|
end
|
125
118
|
|
126
119
|
def id
|
127
|
-
return nil if
|
128
|
-
|
120
|
+
return nil if name.nil?
|
121
|
+
name.gsub('::'.freeze, '-'.freeze)
|
129
122
|
end
|
130
123
|
|
131
124
|
def family
|
132
125
|
'any'
|
133
126
|
end
|
134
|
-
|
135
127
|
end
|
136
128
|
end
|
137
129
|
end
|
@@ -1,27 +1,20 @@
|
|
1
1
|
require 'bigdecimal'
|
2
2
|
|
3
3
|
module Attributor
|
4
|
-
|
5
4
|
class BigDecimal < Numeric
|
6
|
-
|
7
5
|
def self.native_type
|
8
|
-
|
6
|
+
::BigDecimal
|
9
7
|
end
|
10
8
|
|
11
|
-
def self.example(
|
12
|
-
|
9
|
+
def self.example(_context = nil, options: {})
|
10
|
+
::BigDecimal.new("#{/\d{3}/.gen}.#{/\d{3}/.gen}")
|
13
11
|
end
|
14
12
|
|
15
|
-
def self.load(value,
|
13
|
+
def self.load(value, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
16
14
|
return nil if value.nil?
|
17
|
-
return value if value.is_a?(
|
18
|
-
if value.
|
19
|
-
|
20
|
-
end
|
21
|
-
return BigDecimal(value)
|
15
|
+
return value if value.is_a?(native_type)
|
16
|
+
return BigDecimal(value, 10) if value.is_a?(::Float)
|
17
|
+
BigDecimal(value)
|
22
18
|
end
|
23
|
-
|
24
19
|
end
|
25
|
-
|
26
20
|
end
|
27
|
-
|
@@ -3,7 +3,6 @@
|
|
3
3
|
require_relative '../exceptions'
|
4
4
|
|
5
5
|
module Attributor
|
6
|
-
|
7
6
|
class Boolean
|
8
7
|
include Type
|
9
8
|
|
@@ -11,23 +10,21 @@ module Attributor
|
|
11
10
|
value == true || value == false
|
12
11
|
end
|
13
12
|
|
14
|
-
def self.example(
|
13
|
+
def self.example(_context = nil, options: {})
|
15
14
|
[true, false].sample
|
16
15
|
end
|
17
16
|
|
18
|
-
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **
|
17
|
+
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
19
18
|
return nil if value.nil?
|
20
19
|
|
21
|
-
raise CoercionError, context: context, from: value.class, to: self, value: value
|
22
|
-
return false if [
|
23
|
-
return true if [
|
20
|
+
raise CoercionError, context: context, from: value.class, to: self, value: value if value.is_a?(::Float)
|
21
|
+
return false if [false, 'false', 'FALSE', '0', 0, 'f', 'F'].include?(value)
|
22
|
+
return true if [true, 'true', 'TRUE', '1', 1, 't', 'T'].include?(value)
|
24
23
|
raise CoercionError, context: context, from: value.class, to: self
|
25
24
|
end
|
26
25
|
|
27
26
|
def self.family
|
28
27
|
'boolean'
|
29
28
|
end
|
30
|
-
|
31
29
|
end
|
32
30
|
end
|
33
|
-
|
@@ -2,38 +2,37 @@ require 'active_support'
|
|
2
2
|
|
3
3
|
require_relative '../exceptions'
|
4
4
|
|
5
|
-
|
6
5
|
module Attributor
|
7
6
|
class Class
|
8
7
|
include Type
|
9
8
|
|
10
9
|
def self.native_type
|
11
|
-
|
10
|
+
::Class
|
12
11
|
end
|
13
12
|
|
14
|
-
def self.load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **
|
15
|
-
return value if value.is_a?(
|
13
|
+
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
14
|
+
return value if value.is_a?(native_type)
|
16
15
|
return @klass || nil if value.nil?
|
17
16
|
|
18
17
|
# Must be given a String object or nil
|
19
|
-
unless value.
|
18
|
+
unless value.is_a?(::String) || value.nil?
|
20
19
|
raise IncompatibleTypeError, context: context, value_type: value.class, type: self
|
21
20
|
end
|
22
21
|
|
23
|
-
value =
|
22
|
+
value = '::' + value if value[0..1] != '::'
|
24
23
|
result = value.constantize
|
25
24
|
|
26
25
|
# Class given must match class specified when type created using .of() method
|
27
26
|
unless @klass.nil? || result == @klass
|
28
|
-
raise LoadError, "Error loading class #{value} for attribute with "
|
29
|
-
|
27
|
+
raise LoadError, "Error loading class #{value} for attribute with " \
|
28
|
+
"defined class #{@klass} while loading #{Attributor.humanize_context(context)}."
|
30
29
|
end
|
31
30
|
|
32
31
|
result
|
33
32
|
end
|
34
33
|
|
35
|
-
def self.example(
|
36
|
-
@klass.nil? ?
|
34
|
+
def self.example(_context = nil, options: {})
|
35
|
+
@klass.nil? ? 'MyClass' : @klass.name
|
37
36
|
end
|
38
37
|
|
39
38
|
# Create a Class attribute type of a specific Class.
|
@@ -2,7 +2,6 @@
|
|
2
2
|
#
|
3
3
|
|
4
4
|
module Attributor
|
5
|
-
|
6
5
|
class Collection < Array
|
7
6
|
include Container
|
8
7
|
include Dumpable
|
@@ -15,7 +14,7 @@ module Attributor
|
|
15
14
|
def self.of(type)
|
16
15
|
resolved_type = Attributor.resolve_type(type)
|
17
16
|
unless resolved_type.ancestors.include?(Attributor::Type)
|
18
|
-
raise Attributor::AttributorException
|
17
|
+
raise Attributor::AttributorException, 'Collections can only have members that are Attributor::Types'
|
19
18
|
end
|
20
19
|
::Class.new(self) do
|
21
20
|
@member_type = resolved_type
|
@@ -30,17 +29,16 @@ module Attributor
|
|
30
29
|
end
|
31
30
|
end
|
32
31
|
|
33
|
-
|
34
|
-
|
32
|
+
class << self
|
33
|
+
attr_reader :options
|
35
34
|
end
|
36
35
|
|
37
|
-
|
38
36
|
def self.native_type
|
39
37
|
self
|
40
38
|
end
|
41
39
|
|
42
40
|
def self.valid_type?(type)
|
43
|
-
type.
|
41
|
+
type.is_a?(self) || type.is_a?(::Enumerable)
|
44
42
|
end
|
45
43
|
|
46
44
|
def self.family
|
@@ -53,16 +51,15 @@ module Attributor
|
|
53
51
|
|
54
52
|
def self.member_attribute
|
55
53
|
@member_attribute ||= begin
|
56
|
-
|
54
|
+
construct(nil, {})
|
57
55
|
|
58
56
|
@member_attribute
|
59
57
|
end
|
60
58
|
end
|
61
59
|
|
62
|
-
|
63
60
|
# generates an example Collection
|
64
61
|
# @return An Array of native type objects conforming to the specified member_type
|
65
|
-
def self.example(context=nil, options: {})
|
62
|
+
def self.example(context = nil, options: {})
|
66
63
|
result = []
|
67
64
|
size = options[:size] || (rand(3) + 1)
|
68
65
|
size = [*size].sample if size.is_a?(Range)
|
@@ -76,74 +73,70 @@ module Attributor
|
|
76
73
|
|
77
74
|
size.times do |i|
|
78
75
|
subcontext = context + ["at(#{i})"]
|
79
|
-
result <<
|
76
|
+
result << member_attribute.example(subcontext)
|
80
77
|
end
|
81
78
|
|
82
|
-
|
79
|
+
new(result)
|
83
80
|
end
|
84
81
|
|
85
|
-
|
86
82
|
# The incoming value should be array-like here, so the only decoding that we need to do
|
87
83
|
# is from the members (if there's an :member_type defined option).
|
88
|
-
def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **
|
89
|
-
if value.nil?
|
90
|
-
|
91
|
-
elsif value.is_a?(Enumerable)
|
84
|
+
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
85
|
+
return nil if value.nil?
|
86
|
+
if value.is_a?(Enumerable)
|
92
87
|
loaded_value = value
|
93
88
|
elsif value.is_a?(::String)
|
94
|
-
loaded_value = decode_string(value,context)
|
89
|
+
loaded_value = decode_string(value, context)
|
95
90
|
elsif value.respond_to?(:to_a)
|
96
91
|
loaded_value = value.to_a
|
97
92
|
else
|
98
93
|
raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
|
99
94
|
end
|
100
95
|
|
101
|
-
|
96
|
+
new(loaded_value.collect { |member| member_attribute.load(member, context) })
|
102
97
|
end
|
103
98
|
|
104
|
-
|
105
|
-
|
106
|
-
decode_json(value,context)
|
99
|
+
def self.decode_string(value, context)
|
100
|
+
decode_json(value, context)
|
107
101
|
end
|
108
102
|
|
109
|
-
|
110
103
|
def self.dump(values, **opts)
|
111
104
|
return nil if values.nil?
|
112
|
-
values.collect { |value| member_attribute.dump(value,opts) }
|
105
|
+
values.collect { |value| member_attribute.dump(value, opts) }
|
113
106
|
end
|
114
107
|
|
115
|
-
def self.describe(shallow=false, example: nil)
|
108
|
+
def self.describe(shallow = false, example: nil)
|
116
109
|
hash = super(shallow)
|
117
110
|
hash[:options] = {} unless hash[:options]
|
118
|
-
|
119
|
-
|
111
|
+
if example
|
112
|
+
hash[:example] = example
|
113
|
+
member_example = example.first
|
114
|
+
end
|
115
|
+
hash[:member_attribute] = member_attribute.describe(true, example: member_example)
|
120
116
|
hash
|
121
117
|
end
|
122
118
|
|
123
|
-
|
124
119
|
def self.constructable?
|
125
120
|
true
|
126
121
|
end
|
127
122
|
|
128
|
-
|
129
123
|
def self.construct(constructor_block, options)
|
130
|
-
member_options =
|
131
|
-
if options.
|
124
|
+
member_options = (options[:member_options] || {}).clone
|
125
|
+
if options.key?(:reference) && !member_options.key?(:reference)
|
132
126
|
member_options[:reference] = options[:reference]
|
133
127
|
end
|
134
128
|
|
135
129
|
# create the member_attribute, passing in our member_type and whatever constructor_block is.
|
136
130
|
# that in turn will call construct on the type if applicable.
|
137
|
-
@member_attribute = Attributor::Attribute.new
|
131
|
+
@member_attribute = Attributor::Attribute.new member_type, member_options, &constructor_block
|
138
132
|
|
139
133
|
# overwrite our type with whatever type comes out of the attribute
|
140
134
|
@member_type = @member_attribute.type
|
141
135
|
|
142
|
-
|
136
|
+
self
|
143
137
|
end
|
144
138
|
|
145
|
-
|
146
|
-
def self.check_option!(name, definition)
|
139
|
+
def self.check_option!(name, _definition)
|
147
140
|
# TODO: support more options like :max_size
|
148
141
|
case name
|
149
142
|
when :reference
|
@@ -156,33 +149,30 @@ module Attributor
|
|
156
149
|
end
|
157
150
|
|
158
151
|
# @param object [Collection] Collection instance to validate.
|
159
|
-
def self.validate(object, context=Attributor::DEFAULT_ROOT_CONTEXT,
|
152
|
+
def self.validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute = nil)
|
160
153
|
context = [context] if context.is_a? ::String
|
161
154
|
|
162
|
-
unless object.
|
163
|
-
raise ArgumentError, "#{
|
155
|
+
unless object.is_a?(self)
|
156
|
+
raise ArgumentError, "#{name} can not validate object of type #{object.class.name} for #{Attributor.humanize_context(context)}."
|
164
157
|
end
|
165
158
|
|
166
159
|
object.validate(context)
|
167
160
|
end
|
168
161
|
|
169
|
-
def self.validate_options(
|
162
|
+
def self.validate_options(_value, _context, _attribute)
|
170
163
|
errors = []
|
171
164
|
errors
|
172
165
|
end
|
173
166
|
|
174
|
-
|
175
|
-
|
176
|
-
self.each_with_index.collect do |value, i|
|
167
|
+
def validate(context = Attributor::DEFAULT_ROOT_CONTEXT)
|
168
|
+
each_with_index.collect do |value, i|
|
177
169
|
subcontext = context + ["at(#{i})"]
|
178
170
|
self.class.member_attribute.validate(value, subcontext)
|
179
171
|
end.flatten.compact
|
180
172
|
end
|
181
173
|
|
182
|
-
|
183
174
|
def dump(**opts)
|
184
|
-
|
175
|
+
collect { |value| self.class.member_attribute.dump(value, opts) }
|
185
176
|
end
|
186
|
-
|
187
177
|
end
|
188
178
|
end
|