datashift 0.15.0 → 0.16.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 +7 -0
- data/README.markdown +91 -55
- data/VERSION +1 -1
- data/datashift.gemspec +8 -23
- data/lib/applications/jexcel_file.rb +1 -2
- data/lib/datashift.rb +34 -15
- data/lib/datashift/column_packer.rb +98 -34
- data/lib/datashift/data_transforms.rb +83 -0
- data/lib/datashift/delimiters.rb +58 -10
- data/lib/datashift/excel_base.rb +123 -0
- data/lib/datashift/exceptions.rb +45 -7
- data/lib/datashift/load_object.rb +25 -0
- data/lib/datashift/mapping_service.rb +91 -0
- data/lib/datashift/method_detail.rb +40 -62
- data/lib/datashift/method_details_manager.rb +18 -2
- data/lib/datashift/method_dictionary.rb +27 -10
- data/lib/datashift/method_mapper.rb +49 -41
- data/lib/datashift/model_mapper.rb +42 -22
- data/lib/datashift/populator.rb +258 -143
- data/lib/datashift/thor_base.rb +38 -0
- data/lib/exporters/csv_exporter.rb +57 -145
- data/lib/exporters/excel_exporter.rb +73 -60
- data/lib/generators/csv_generator.rb +65 -5
- data/lib/generators/generator_base.rb +69 -3
- data/lib/generators/mapping_generator.rb +112 -0
- data/lib/helpers/core_ext/csv_file.rb +33 -0
- data/lib/loaders/csv_loader.rb +41 -39
- data/lib/loaders/excel_loader.rb +130 -116
- data/lib/loaders/loader_base.rb +190 -146
- data/lib/loaders/paperclip/attachment_loader.rb +4 -4
- data/lib/loaders/paperclip/datashift_paperclip.rb +5 -3
- data/lib/loaders/paperclip/image_loading.rb +9 -7
- data/lib/loaders/reporter.rb +17 -8
- data/lib/thor/export.thor +12 -13
- data/lib/thor/generate.thor +1 -9
- data/lib/thor/import.thor +13 -24
- data/lib/thor/mapping.thor +65 -0
- data/spec/Gemfile +13 -11
- data/spec/Gemfile.lock +98 -93
- data/spec/csv_exporter_spec.rb +104 -99
- data/spec/csv_generator_spec.rb +159 -0
- data/spec/csv_loader_spec.rb +197 -16
- data/spec/datashift_spec.rb +9 -0
- data/spec/excel_exporter_spec.rb +149 -58
- data/spec/excel_generator_spec.rb +35 -44
- data/spec/excel_loader_spec.rb +196 -178
- data/spec/excel_spec.rb +8 -5
- data/spec/loader_base_spec.rb +47 -7
- data/spec/mapping_spec.rb +117 -0
- data/spec/method_dictionary_spec.rb +24 -11
- data/spec/method_mapper_spec.rb +5 -7
- data/spec/model_mapper_spec.rb +41 -0
- data/spec/paperclip_loader_spec.rb +3 -6
- data/spec/populator_spec.rb +48 -14
- data/spec/spec_helper.rb +85 -73
- data/spec/thor_spec.rb +40 -5
- metadata +93 -86
- data/lib/applications/excel_base.rb +0 -63
@@ -3,16 +3,13 @@
|
|
3
3
|
# Date :: Aug 2010
|
4
4
|
# License:: MIT
|
5
5
|
#
|
6
|
-
# Details:: This class provides info and access to the individual population methods
|
6
|
+
# Details:: This class provides info and access to the details of individual population methods
|
7
7
|
# on an AR model. Populated by, and coupled with MethodMapper,
|
8
8
|
# which does the model interrogation work and stores sets of MethodDetails.
|
9
9
|
#
|
10
10
|
# Enables 'loaders' to iterate over the MethodMapper results set,
|
11
11
|
# and assign values to AR object, without knowing anything about that receiving object.
|
12
12
|
#
|
13
|
-
require 'to_b'
|
14
|
-
require 'logging'
|
15
|
-
require 'populator'
|
16
13
|
require 'set'
|
17
14
|
|
18
15
|
module DataShift
|
@@ -32,21 +29,35 @@ module DataShift
|
|
32
29
|
end
|
33
30
|
|
34
31
|
|
35
|
-
|
32
|
+
def self.is_association_type? ( type )
|
33
|
+
association_types_enum.member?( type )
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
# Klass is the class of the 'parent' object i.e with the associations,
|
38
|
+
# For example Product which may have operator orders
|
39
|
+
attr_accessor :klass
|
40
|
+
|
41
|
+
# Name is the raw, client supplied name e.g Orders
|
36
42
|
attr_accessor :name
|
37
43
|
attr_accessor :column_index
|
38
44
|
|
39
45
|
# The rel col type from the DB
|
40
|
-
attr_reader :col_type
|
46
|
+
attr_reader :col_type
|
41
47
|
|
48
|
+
# The :operator that can be called to assign e.g orders or Products.new.orders << Order.new
|
49
|
+
#
|
50
|
+
# The type of operator e.g :assignment, :belongs_to, :has_one, :has_many etc
|
42
51
|
attr_reader :operator, :operator_type
|
43
52
|
|
44
53
|
# TODO make it a list/primary keys
|
54
|
+
# Additional helpers for where clauses
|
45
55
|
attr_accessor :find_by_operator, :find_by_value
|
46
56
|
|
47
57
|
# Store the raw (client supplied) name against the active record klass(model).
|
48
58
|
# Operator is the associated method call on klass,
|
49
|
-
#
|
59
|
+
# i.e client supplies name 'Price' in a spreadsheet,
|
60
|
+
# but true operator to call on klass is price
|
50
61
|
#
|
51
62
|
# col_types can typically be derived from klass.columns - set of ActiveRecord::ConnectionAdapters::Column
|
52
63
|
|
@@ -88,11 +99,10 @@ module DataShift
|
|
88
99
|
|
89
100
|
# Return the operator's expected class name, if can be derived, else nil
|
90
101
|
def operator_class_name()
|
91
|
-
@operator_class_name ||=
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
rescue; ""; end
|
102
|
+
@operator_class_name ||=
|
103
|
+
if(operator_for(:has_many) || operator_for(:belongs_to) || operator_for(:has_one))
|
104
|
+
|
105
|
+
get_operator_class.name
|
96
106
|
|
97
107
|
elsif(@col_type)
|
98
108
|
@col_type.type.to_s.classify
|
@@ -115,63 +125,31 @@ module DataShift
|
|
115
125
|
|
116
126
|
private
|
117
127
|
|
118
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
else
|
128
|
+
# Return the operator's expected class, if can be derived, else nil
|
129
|
+
# TODO rspec- can reflect_on_association ever actually fail & do we ever need to try ourselves (badly)
|
130
|
+
def get_operator_class()
|
131
|
+
|
132
|
+
if(operator_for(:has_many) || operator_for(:belongs_to) || operator_for(:has_one))
|
124
133
|
|
125
|
-
|
126
|
-
begin
|
127
|
-
next unless operator_class.respond_to?( "find_by_#{x}" )
|
128
|
-
item = operator_class.send( "find_by_#{x}", value)
|
129
|
-
if(item)
|
130
|
-
record.send(operator + '=', item)
|
131
|
-
break
|
132
|
-
end
|
133
|
-
rescue => e
|
134
|
-
logger.error "Failed to match belongs_to association #{value}"
|
135
|
-
puts "ERROR: #{e.inspect}"
|
136
|
-
if(x == Populator::insistent_method_list.last)
|
137
|
-
raise "I'm sorry I have failed to assign [#{value}] to #{@assignment}" unless value.nil?
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
134
|
+
result = klass.reflect_on_association(operator)
|
143
135
|
|
144
|
-
|
145
|
-
def insistent_has_many( record, value )
|
136
|
+
return result.klass if(result)
|
146
137
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
@@insistent_find_by_list.each do |x|
|
138
|
+
result = ModelMapper::class_from_string(operator.classify)
|
139
|
+
|
140
|
+
if(result.nil?)
|
151
141
|
begin
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
142
|
+
|
143
|
+
first = klass.to_s.split('::').first
|
144
|
+
logger.debug "Trying to find operator class with Parent Namespace #{first}"
|
145
|
+
|
146
|
+
result = ModelMapper::const_get_from_string("#{first}::#{operator.classify}")
|
157
147
|
rescue => e
|
158
|
-
|
159
|
-
if(x == Populator::insistent_method_list.last)
|
160
|
-
raise "I'm sorry I have failed to assign [#{value}] to #{operator}" unless value.nil?
|
161
|
-
end
|
148
|
+
logger.error("Failed to derive Class for #{operator} (#{@operator_type} - #{e.inspect}")
|
162
149
|
end
|
163
150
|
end
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
private
|
168
|
-
|
169
|
-
# Return the operator's expected class, if can be derived, else nil
|
170
|
-
def get_operator_class()
|
171
|
-
if(operator_for(:has_many) || operator_for(:belongs_to) || operator_for(:has_one))
|
172
|
-
begin
|
173
|
-
Kernel.const_get(operator.classify)
|
174
|
-
rescue; nil; end
|
151
|
+
|
152
|
+
result
|
175
153
|
|
176
154
|
elsif(@col_type)
|
177
155
|
begin
|
@@ -6,6 +6,11 @@
|
|
6
6
|
# Details:: This class provides info and access to groups of accessor methods,
|
7
7
|
# grouped by AR model.
|
8
8
|
#
|
9
|
+
# Stores complete collection of MethodDetail instances per mapped class.
|
10
|
+
# Provides high level find facilites to find a MethodDetail and to list out
|
11
|
+
# operators per type (has_one, has_many, belongs_to, instance_method etc)
|
12
|
+
# and all possible operators,
|
13
|
+
|
9
14
|
require 'to_b'
|
10
15
|
|
11
16
|
module DataShift
|
@@ -68,11 +73,22 @@ module DataShift
|
|
68
73
|
get_list(type).collect { |md| md.operator }
|
69
74
|
end
|
70
75
|
|
71
|
-
alias_method(:get_list_of_operators, :
|
76
|
+
alias_method(:get_list_of_operators, :get_operators)
|
72
77
|
|
73
|
-
def
|
78
|
+
def available_operators
|
74
79
|
method_details_list.values.flatten.collect(&:operator)
|
75
80
|
end
|
81
|
+
|
82
|
+
# A reverse map showing all operators with their associated 'type'
|
83
|
+
def available_operators_with_type
|
84
|
+
h = {}
|
85
|
+
method_details_list.each { |t, mds| mds.each do |v| h[v.operator] = t end }
|
86
|
+
|
87
|
+
# this is meant to be more efficient that Hash[h.sort]
|
88
|
+
sh = {}
|
89
|
+
h.keys.sort.each do |k| sh[k] = h[k] end
|
90
|
+
sh
|
91
|
+
end
|
76
92
|
|
77
93
|
end
|
78
94
|
|
@@ -10,7 +10,8 @@ module DataShift
|
|
10
10
|
class MethodDictionary
|
11
11
|
|
12
12
|
include DataShift::Logging
|
13
|
-
|
13
|
+
extend DataShift::Logging
|
14
|
+
|
14
15
|
# Return true if dictionary has been populated for klass
|
15
16
|
def self.for?(klass)
|
16
17
|
any = has_many[klass] || belongs_to[klass] || has_one[klass] || assignments[klass]
|
@@ -27,6 +28,8 @@ module DataShift
|
|
27
28
|
|
28
29
|
raise "Cannot find operators supplied klass nil #{klass}" if(klass.nil?)
|
29
30
|
|
31
|
+
logger.debug("MethodDictionary - building operators information for #{klass}")
|
32
|
+
|
30
33
|
# Find the has_many associations which can be populated via <<
|
31
34
|
if( options[:reload] || has_many[klass].nil? )
|
32
35
|
has_many[klass] = klass.reflect_on_all_associations(:has_many).map { |i| i.name.to_s }
|
@@ -63,8 +66,6 @@ module DataShift
|
|
63
66
|
|
64
67
|
assignments[klass].uniq!
|
65
68
|
|
66
|
-
#puts "\nDEBUG: DICT Setters\n#{assignments[klass]}\n"
|
67
|
-
|
68
69
|
assignments[klass].each do |assign|
|
69
70
|
column_types[klass] ||= {}
|
70
71
|
column_def = klass.columns.find{ |col| col.name == assign }
|
@@ -95,10 +96,17 @@ module DataShift
|
|
95
96
|
|
96
97
|
# Build a thorough and usable picture of the operators by building dictionary of our MethodDetail
|
97
98
|
# objects which can be used to import/export data to objects of type 'klass'
|
98
|
-
#
|
99
|
-
|
99
|
+
# Subsequent calls with same class will return existign mapping
|
100
|
+
# To over ride this behaviour, supply :force => true to force regeneration
|
101
|
+
|
102
|
+
def self.build_method_details( klass, options = {} )
|
103
|
+
|
104
|
+
return method_details_mgrs[klass] if(method_details_mgrs[klass] && !options[:force])
|
105
|
+
|
100
106
|
method_details_mgr = MethodDetailsManager.new( klass )
|
101
|
-
|
107
|
+
|
108
|
+
method_details_mgrs[klass] = method_details_mgr
|
109
|
+
|
102
110
|
assignments_for(klass).each do |n|
|
103
111
|
method_details_mgr << MethodDetail.new(n, klass, n, :assignment, column_types[klass])
|
104
112
|
end
|
@@ -115,7 +123,7 @@ module DataShift
|
|
115
123
|
method_details_mgr << MethodDetail.new(n, klass, n, :belongs_to)
|
116
124
|
end
|
117
125
|
|
118
|
-
|
126
|
+
method_details_mgr
|
119
127
|
|
120
128
|
end
|
121
129
|
|
@@ -135,9 +143,18 @@ module DataShift
|
|
135
143
|
name.gsub(' ', '_').underscore
|
136
144
|
]
|
137
145
|
end
|
138
|
-
|
139
|
-
|
140
|
-
#
|
146
|
+
|
147
|
+
|
148
|
+
# Dump out all available operators
|
149
|
+
#
|
150
|
+
def self.dump( klass )
|
151
|
+
method_details_mgr = get_method_details_mgr( klass )
|
152
|
+
#TODO
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
# For a client supplied name/header - find the operator i.e appropriate call + column type
|
157
|
+
#
|
141
158
|
# e.g Given users entry in spread sheet check for pluralization, missing underscores etc
|
142
159
|
#
|
143
160
|
# If not nil, returned method can be used directly in for example klass.new.send( call, .... )
|
@@ -23,22 +23,6 @@ module DataShift
|
|
23
23
|
|
24
24
|
attr_accessor :method_details, :missing_methods
|
25
25
|
|
26
|
-
|
27
|
-
# As well as just the column name, support embedding find operators for that column
|
28
|
-
# in the heading .. i.e Column header => 'BlogPosts:user_id'
|
29
|
-
# ... association has many BlogPosts selected via find_by_user_id
|
30
|
-
#
|
31
|
-
# in the heading .. i.e Column header => 'BlogPosts:user_name:John Smith'
|
32
|
-
# ... association has many BlogPosts selected via find_by_user_name("John Smith")
|
33
|
-
#
|
34
|
-
def self.column_delim
|
35
|
-
@column_delim ||= ':'
|
36
|
-
@column_delim
|
37
|
-
end
|
38
|
-
|
39
|
-
def self.set_column_delim(x) @column_delim = x; end
|
40
|
-
|
41
|
-
|
42
26
|
def initialize
|
43
27
|
@method_details = []
|
44
28
|
end
|
@@ -47,10 +31,16 @@ module DataShift
|
|
47
31
|
# Handles method names as defined by a user, from spreadsheets or file headers where the names
|
48
32
|
# specified may not be exactly as required e.g handles capitalisation, white space, _ etc
|
49
33
|
#
|
50
|
-
# The header can also contain the fields to use in lookups, separated with
|
51
|
-
#
|
52
|
-
# product:name
|
53
|
-
#
|
34
|
+
# The header can also contain the fields to use in lookups, separated with Delimiters ::column_delim
|
35
|
+
# For example specify that lookups on has_one association called 'product', be performed using name'
|
36
|
+
# product:name
|
37
|
+
#
|
38
|
+
# The header can also contain a default value for the lookup field, again separated with Delimiters ::column_delim
|
39
|
+
#
|
40
|
+
# For example specify lookups on assoc called 'user', be performed using 'email' == 'test@blah.com'
|
41
|
+
#
|
42
|
+
# user:email:test@blah.com
|
43
|
+
#
|
54
44
|
# Returns: Array of matching method_details, including nils for non matched items
|
55
45
|
#
|
56
46
|
# N.B Columns that could not be mapped are left in the array as NIL
|
@@ -59,15 +49,14 @@ module DataShift
|
|
59
49
|
#
|
60
50
|
# Other callers can simply call compact on the results if the index not important.
|
61
51
|
#
|
62
|
-
# The
|
63
|
-
#
|
52
|
+
# The MethodDetails instance will contain a pointer to the column index from which it was mapped.
|
64
53
|
#
|
65
54
|
# Options:
|
66
55
|
#
|
67
|
-
# [:force_inclusion] : List of columns that do not map to any operator but should be
|
56
|
+
# [:force_inclusion] : List of columns that do not map to any operator but should be included in processing.
|
68
57
|
#
|
69
58
|
# This provides the opportunity for loaders to provide specific methods to handle these fields
|
70
|
-
# when no direct operator is available on the
|
59
|
+
# when no direct operator is available on the model or it's associations
|
71
60
|
#
|
72
61
|
# [:include_all] : Include all headers in processing - takes precedence of :force_inclusion
|
73
62
|
#
|
@@ -88,7 +77,7 @@ module DataShift
|
|
88
77
|
@method_details, @missing_methods = [], []
|
89
78
|
|
90
79
|
columns.each_with_index do |col_data, col_index|
|
91
|
-
|
80
|
+
|
92
81
|
raw_col_data = col_data.to_s
|
93
82
|
|
94
83
|
if(raw_col_data.nil? or raw_col_data.empty?)
|
@@ -97,36 +86,55 @@ module DataShift
|
|
97
86
|
next
|
98
87
|
end
|
99
88
|
|
100
|
-
raw_col_name,
|
89
|
+
raw_col_name, where_field, where_value, *data = raw_col_data.split(Delimiters::column_delim)
|
101
90
|
|
102
91
|
md = MethodDictionary::find_method_detail(klass, raw_col_name)
|
103
|
-
|
104
|
-
if(md.nil?)
|
105
|
-
#puts "DEBUG: Check Forced\n #{forced}.include?(#{raw_col_name}) #{forced.include?(raw_col_name.downcase)}"
|
106
|
-
|
92
|
+
|
93
|
+
if(md.nil?)
|
107
94
|
if(options[:include_all] || forced.include?(raw_col_name.downcase))
|
95
|
+
logger.debug("Operator #{raw_col_name} not found but forced inclusion operative")
|
108
96
|
md = MethodDictionary::add(klass, raw_col_name)
|
109
97
|
end
|
110
98
|
end
|
111
99
|
|
112
|
-
if(md)
|
100
|
+
if(md)
|
101
|
+
|
113
102
|
md.name = raw_col_name
|
114
103
|
md.column_index = col_index
|
115
|
-
|
116
|
-
#
|
117
|
-
|
118
|
-
|
119
|
-
if(
|
120
|
-
|
121
|
-
|
122
|
-
md.
|
123
|
-
|
104
|
+
|
105
|
+
# put data back as string for now - leave it to clients to decide what to do with it later
|
106
|
+
Populator::set_header_default_data(md.operator, data.join(Delimiters::column_delim))
|
107
|
+
|
108
|
+
if(where_field)
|
109
|
+
logger.info("Lookup query field [#{where_field}] - specified for association #{md.operator}")
|
110
|
+
|
111
|
+
md.find_by_value = where_value
|
112
|
+
|
113
|
+
# Example :
|
114
|
+
# Project:name:My Best Project
|
115
|
+
# User (klass) has_one project (operator) lookup by name (find_by_operator) == 'My Best Project' (find_by_value)
|
116
|
+
# User.project.where( :name => 'My Best Project')
|
117
|
+
|
118
|
+
# check the finder method name is a valid field on the actual association class
|
119
|
+
|
120
|
+
if(klass.reflect_on_association(md.operator) &&
|
121
|
+
klass.reflect_on_association(md.operator).klass.new.respond_to?(where_field))
|
122
|
+
md.find_by_operator = where_field
|
123
|
+
logger.info("Complex Lookup specified for [#{md.operator}] : on field [#{md.find_by_operator}] (optional value [#{md.find_by_value}])")
|
124
|
+
else
|
125
|
+
logger.warn("Find by operator [#{where_field}] Not Found on association [#{md.operator}] on Class #{klass.name} (#{md.inspect})")
|
126
|
+
logger.warn("Check column (#{md.column_index}) heading - e.g association field names are case sensitive")
|
127
|
+
# TODO - maybe derived loaders etc want this data for another purpose - should we stash elsewhere ?
|
128
|
+
end
|
124
129
|
end
|
125
130
|
else
|
126
131
|
# TODO populate unmapped with a real MethodDetail that is 'null' and create is_nil
|
132
|
+
logger.warn("No operator or association found for Header #{raw_col_name}")
|
127
133
|
@missing_methods << raw_col_name
|
128
134
|
end
|
129
|
-
|
135
|
+
|
136
|
+
logger.debug("Column [#{col_data}] (#{col_index}) - mapped to :\n#{md.inspect}")
|
137
|
+
|
130
138
|
@method_details << md
|
131
139
|
|
132
140
|
end
|
@@ -1,27 +1,47 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
1
|
+
module DataShift
|
2
|
+
|
3
|
+
class ModelMapper
|
4
|
+
|
5
|
+
|
6
|
+
def self.class_from_string_or_raise( klass )
|
7
|
+
|
8
|
+
ruby_klass = begin
|
9
|
+
# support modules e.g "Spree::Property")
|
10
|
+
ModelMapper::class_from_string(klass) #Kernel.const_get(model)
|
11
|
+
rescue NameError => e
|
12
|
+
puts e
|
13
|
+
raise Thor::Error.new("ERROR: No such Class [#{klass}] found - check valid model supplied")
|
14
|
+
end
|
15
|
+
|
16
|
+
raise NoSuchClassError.new("ERROR: No such Model [#{klass}] found - check valid model supplied") unless(ruby_klass)
|
17
|
+
|
18
|
+
ruby_klass
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# Helper to deal with string versions of modules/namespaced classes
|
23
|
+
# Find and return the base class from a string.
|
24
|
+
#
|
25
|
+
# e.g "Spree::Property" returns the Spree::Property class
|
26
|
+
# Raises exception if no such class found
|
27
|
+
def self.const_get_from_string(str)
|
28
|
+
str.to_s.split('::').inject(Object) do |mod, class_name|
|
29
|
+
mod.const_get(class_name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# Similar to const_get_from_string except this version
|
35
|
+
# returns nil if no such class found
|
36
|
+
# Support modules e.g "Spree::Property"
|
37
|
+
#
|
38
|
+
def self.class_from_string( str )
|
20
39
|
begin
|
21
40
|
ModelMapper::const_get_from_string(str.to_s) #Kernel.const_get(model)
|
22
|
-
rescue
|
23
|
-
|
41
|
+
rescue
|
42
|
+
return nil
|
24
43
|
end
|
44
|
+
end
|
45
|
+
|
25
46
|
end
|
26
|
-
|
27
47
|
end
|