soaspec 0.0.23 → 0.0.24

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 72616e1074159a6f95e6def516fd54d1b9bae9c2
4
- data.tar.gz: c63d9d36c73c36e14dc817ec9f846e01bd0cf3ef
3
+ metadata.gz: 4aaad0d9e089c5a7a5ed3f22e36ad11f74304065
4
+ data.tar.gz: 3a6ace44416ff6c6997012171e7d3c6875971ad4
5
5
  SHA512:
6
- metadata.gz: 6d88345669ab61df3b26d28c3fad0e3ac860693426f348519f17e8e05eb89f1f7343085a81ed41b846251c167ff7a499ae7fbb435e84aaaead3ac4cef867777f
7
- data.tar.gz: 7e63562503b905cceb8597d8d06947f774129df9631d0a9a9423e472dc3de174d52e9662c6b3987b2905fb4b1b285f2efcfb232c41d1b7838a4de7639d2d9574
6
+ metadata.gz: 587fae6e8d6a0d1ba507df5badb2030991302f5958cc0d8db5bfbb732bdb2283641dd5ad1fc59efd1cfba378f134f4d6a1b34a0ebddb7260a5ae0de8db11127d
7
+ data.tar.gz: 0e3410d331d2d2fe0c5dd46eb666910291672a49e380bc76b5b1fc2eba03c124138e1b3465f66170cda7ea43ed38d2258d6e5b703a92f6cfc2ba397d4315b795
data/ChangeLog CHANGED
@@ -1,3 +1,9 @@
1
+ Version 0.0.24 / 2018-2-25
2
+ * Enhancements
3
+ * Added to soaspec-generate more types when creating YAML with data (int, boolean, double and custom enumeration).
4
+ * Binaries use common module (exe-helper) to reduce repeatability
5
+ * TODO: soaspec-generate read xsd's mentioned to import in WSDL schema
6
+
1
7
  Version 0.0.23 / 2018-2-23
2
8
  * Enhancements
3
9
  * Created RestHandler class (Very messy and ugly still) to handle REST requests using style used for SOAP using Rest Client resource
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- soaspec (0.0.23)
4
+ soaspec (0.0.24)
5
5
  rest-client (>= 2.0)
6
6
  rspec (~> 3.0)
7
7
  rspec-its (>= 1.2.0)
data/exe/soaspec-generate CHANGED
@@ -9,16 +9,40 @@ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
9
9
  require 'savon'
10
10
  require 'soaspec'
11
11
 
12
+ include Soaspec::ExeHelpers
13
+
14
+ def try_enum_for(type)
15
+ custom_type = @wsdl_schemas.xpath("//*[@name='#{type}']")
16
+ if custom_type.first
17
+ prefix = custom_type.first.namespace.prefix
18
+ enumerations = custom_type.xpath("//#{prefix}:enumeration")
19
+ return 'Custom Type' if enumerations.empty?
20
+ @enums_values = Array.new
21
+ enumerations.each do |enum_value|
22
+ @enums_values << "'#{enum_value['value']}'"
23
+ end
24
+ "~randomize [#{@enums_values.join(', ')}]"
25
+ else
26
+ 'Custom Type'
27
+ end
28
+ end
29
+
12
30
  def fill_in_field_from_type(type)
13
31
  case type
14
32
  when 'string'
15
33
  'test string'
16
- else
17
- 'test'
34
+ when 'int'
35
+ 2
36
+ when 'boolean'
37
+ true
38
+ when 'double'
39
+ '1.5'
40
+ else
41
+ try_enum_for type
18
42
  end
19
43
  end
20
44
 
21
- def self.ask_wsdl
45
+ def ask_wsdl
22
46
  prompt = <<-EOF
23
47
  Enter WSDL:
24
48
  EOF
@@ -27,7 +51,7 @@ Enter WSDL:
27
51
  puts
28
52
  end
29
53
 
30
- def self.name_of_wsdl
54
+ def name_of_wsdl
31
55
  prompt = <<-EOF
32
56
  Enter what you would like to name WSDL (CamelCase):
33
57
  EOF
@@ -36,6 +60,38 @@ Enter what you would like to name WSDL (CamelCase):
36
60
  puts
37
61
  end
38
62
 
63
+ def authentication_type
64
+ prompt = <<-EOF
65
+ Authentication
66
+ (1) Basic
67
+ (2) None
68
+
69
+ Type:
70
+ EOF
71
+ print prompt.chop
72
+ num = $stdin.gets.to_i - 1
73
+ puts
74
+
75
+ @auth = [:basic, :none][num] || :none
76
+
77
+ if @auth == :basic
78
+ prompt = <<-EOF
79
+ User Name:
80
+ EOF
81
+ print prompt.chop
82
+ @auth_name = $stdin.gets.strip
83
+ puts
84
+
85
+ prompt = <<-EOF
86
+ User Password:
87
+ EOF
88
+ print prompt.chop
89
+ @auth_password = $stdin.gets.strip
90
+ puts
91
+ end
92
+
93
+ end
94
+
39
95
  def camel_case(underscore_seperated)
40
96
  underscore_seperated.to_s.split('_').collect(&:capitalize).join
41
97
  end
@@ -48,8 +104,7 @@ end
48
104
  # Add to or override default Savon client options
49
105
  def savon_options
50
106
  {
51
- wsdl: '<%= @wsdl %>',
52
- convert_request_keys_to: :camelcase # Remove this if request is not in camelcase
107
+ wsdl: '<%= @wsdl %>'
53
108
  }
54
109
  end
55
110
 
@@ -62,7 +117,7 @@ require 'spec_helper'
62
117
 
63
118
  Soaspec::Environment.strip_namespaces = true # This allows namespace not to be used. Be careful with this
64
119
 
65
- <%= operation %> = <%= @name %>.new('<%= camel_case(operation) %>')
120
+ <%= operation %> = <%= @name %>.new(<%= @class_params %>)
66
121
  <%= operation %>.operation = :<%= operation %>
67
122
  <%= operation %>.default_hash = data_for '<%= operation %>/default'
68
123
 
@@ -74,78 +129,48 @@ end
74
129
 
75
130
  EOF
76
131
 
77
- rake_content = <<-EOF
78
- # The list of task for a Rake file can be seen with `rake -T`
79
- require 'rspec/core/rake_task' # See See https://relishapp.com/rspec/rspec-core/docs/command-line/rake-task for details
80
-
81
- # This runs `rspec` command with the following options. Type `rake spec` to run this task
82
- RSpec::Core::RakeTask.new(:spec) do |t, task_args|
83
- t.pattern = "spec/*_spec.rb" # Run all specs in 'spec' folder ending in '_spec'
84
- # Next line shows output on the screen, Junit xml report and an HTML report
85
- t.rspec_opts = "--format documentation --format RspecJunitFormatter --out logs/spec.xml --format html --out logs/spec.html"
86
- t.fail_on_error = false
87
- end
88
-
89
- task :default => :spec # This runs the 'spec' task by default when no task is mentioned. E.g., if only `rake` is typed
90
- EOF
91
-
92
-
93
- gem_content = <<-EOF
94
-
95
- source 'https://rubygems.org'
96
-
97
- gem 'data_magic'
98
- gem 'require_all'
99
- gem 'rspec_junit_formatter'
100
- gem 'rake'
101
- gem 'soaspec'
102
-
103
- EOF
104
-
105
- spec_helper_content = <<-EOF
106
-
107
- require 'soaspec'
108
- require 'require_all'
109
- require_all 'lib'
110
- require 'data_magic'
111
-
112
- include DataMagic # Used as example of loading data smartly. Use 'data_for' method to load yml data
113
-
114
- RSpec.configure do |config|
115
- # This will make backtrace much shorter by removing many lines from rspec failure message
116
- config.backtrace_exclusion_patterns = [
117
- /rspec/
118
- ]
119
- end
120
-
121
- EOF
122
-
123
132
  name_of_wsdl
124
133
  ask_wsdl
125
134
 
126
- wsdl_doc = Savon.client(wsdl: @wsdl).wsdl
135
+ authentication_type
127
136
 
128
- # Basics. May already be there
129
- Soaspec::FileHelpers.create_file filename: 'Rakefile', content: rake_content, ignore_if_present: true
130
- Soaspec::FileHelpers.create_file filename: 'Gemfile', content: gem_content, ignore_if_present: true
131
- Soaspec::FileHelpers.create_folder 'spec'
132
- Soaspec::FileHelpers.create_file(filename: 'spec/spec_helper.rb', content: spec_helper_content)
137
+ savon_options = { wsdl: @wsdl }
138
+ savon_options[:basic_auth] = [@auth_name, @auth_password] if @auth == :basic
133
139
 
134
- module Soaspec::FileHelpers
135
- create_folder 'logs'
136
- create_folder 'config'
137
- create_folder 'config/data'
138
- create_folder 'lib'
139
- end
140
- Soaspec::FileHelpers.create_file filename: "lib/#{@name}.rb", content: ERB.new(@class_content).result(binding)
140
+ @wsdl_doc = Savon.client(**savon_options).wsdl
141
+ @wsdl_schemas = @wsdl_doc.parser.schemas
142
+
143
+ # Basic files. May already be there
144
+ create_file filename: 'Rakefile', content: rake_content, ignore_if_present: true
145
+ create_file filename: 'Gemfile', content: gem_content, ignore_if_present: true
146
+ create_folder 'spec'
147
+ create_file filename: 'spec/spec_helper.rb', content: spec_helper_content
141
148
 
142
- wsdl_doc.operations.each do |operation, details|
149
+ create_folder 'logs'
150
+ create_folder 'config'
151
+ create_folder 'config/data'
152
+ create_folder 'lib'
153
+ create_file filename: "lib/#{@name}.rb", content: ERB.new(@class_content).result(binding)
154
+ # Files according to WSDL
155
+ @wsdl_doc.operations.each do |operation, details|
143
156
  puts "Creating files for operation: #{operation}"
144
157
  @content = "default:\n"
145
- details[:parameters].each do |element, details|
146
- @content += " #{element.to_s}: #{fill_in_field_from_type(details[:type])} \n"
158
+ @use_camel_case = false
159
+ puts 'Message params: ' + details.to_s
160
+ # From namespace identifier, find namespace, and for that find schemaLocation xsd and use that to build request
161
+ if details[:parameters]
162
+ details[:parameters].each do |element, details|
163
+ @use_camel_case = true if (/[[:upper:]]/.match(element.to_s[0]) != nil)
164
+ @content += " #{underscore_key element}: #{fill_in_field_from_type(details[:type])} # #{details[:type]} \n"
165
+ # TODO: If details is a Hash need to loop again
166
+ end
147
167
  end
148
168
 
149
- Soaspec::FileHelpers.create_file(filename: "config/data/#{operation}.yml", content: @content)
150
- Soaspec::FileHelpers.create_file(filename: "spec/#{operation}_spec.rb", content: ERB.new(@soap_spec_content).result(binding))
169
+ params = []
170
+ params << 'convert_request_keys_to: :camelcase' if @use_camel_case
171
+ params_string = params == [] ? '' : ', ' + params.join(', ')
172
+ @class_params = "'#{camel_case(operation)}'#{params_string}"
173
+
174
+ create_file(filename: "config/data/#{operation}.yml", content: @content)
175
+ create_file(filename: "spec/#{operation}_spec.rb", content: ERB.new(@soap_spec_content).result(binding))
151
176
  end
data/exe/soaspec-init CHANGED
@@ -1,36 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
+
5
+ require 'savon'
3
6
  require 'soaspec'
4
- require 'fileutils'
5
-
6
- def create_file(options)
7
- filename = options[:filename]
8
- raise 'Need to pass filename' unless filename
9
- content = options[:content]
10
- raise 'Need to pass contents to insert into file' unless content
11
- if File.exist? filename
12
- old_content = File.read(filename)
13
- if old_content != content
14
- warn "!! #{filename} already exists and differs from template"
15
- end
16
- else
17
- File.open(filename, 'w') do |f|
18
- f.puts content
19
- end
20
- puts 'Created: ' + filename
21
- end
22
- end
23
7
 
24
- def create_folder(folder)
25
- if File.exists? folder
26
- unless File.directory? folder
27
- $stderr.puts "!! #{folder} already exists and is not a directory"
28
- end
29
- else
30
- FileUtils.mkdir folder
31
- puts "Created folder: #{folder}/"
32
- end
33
- end
8
+ include Soaspec::ExeHelpers
34
9
 
35
10
  puts 'Creating files for soaspec'
36
11
 
@@ -46,40 +21,6 @@ gem 'soaspec'
46
21
 
47
22
  EOF
48
23
 
49
- rake_content = <<-EOF
50
- # The list of task for a Rake file can be seen with `rake -T`
51
- require 'rspec/core/rake_task' # See See https://relishapp.com/rspec/rspec-core/docs/command-line/rake-task for details
52
-
53
- # This runs `rspec` command with the following options. Type `rake spec` to run this task
54
- RSpec::Core::RakeTask.new(:spec) do |t, task_args|
55
- t.pattern = "spec/*_spec.rb" # Run all specs in 'spec' folder ending in '_spec'
56
- # Next line shows output on the screen, Junit xml report and an HTML report
57
- t.rspec_opts = "--format documentation --format RspecJunitFormatter --out logs/spec.xml --format html --out logs/spec.html"
58
- t.fail_on_error = false
59
- end
60
-
61
- task :default => :spec # This runs the 'spec' task by default when no task is mentioned. E.g., if only `rake` is typed
62
- EOF
63
-
64
-
65
- spec_helper_content = <<-EOF
66
-
67
- require 'soaspec'
68
- require 'require_all'
69
- require_all 'lib'
70
- require 'data_magic'
71
-
72
- include DataMagic # Used as example of loading data smartly. Use 'data_for' method to load yml data
73
-
74
- RSpec.configure do |config|
75
- # This will make backtrace much shorter by removing many lines from rspec failure message
76
- config.backtrace_exclusion_patterns = [
77
- /rspec/
78
- ]
79
- end
80
-
81
- EOF
82
-
83
24
  weather_web_service = <<-EOF
84
25
 
85
26
  require 'soaspec'
data/exe/xml_to_yaml_file CHANGED
@@ -4,6 +4,12 @@ require 'xmlsimple'
4
4
  require 'yaml'
5
5
  require 'fileutils'
6
6
 
7
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
8
+
9
+ require 'soaspec'
10
+
11
+ include Soaspec::ExeHelpers
12
+
7
13
  default_output_file = 'output.yml'
8
14
 
9
15
  # Create file if not present. If present but different give warning
@@ -25,25 +31,15 @@ def create_file(options)
25
31
  end
26
32
  end
27
33
 
28
- # Convert key in camelcase to underscore separated (snakecase)
29
- def underscore_key(key)
30
- key.to_s.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
31
- .gsub(/([a-z\d])([A-Z])/,'\1_\2')
32
- .tr('-', '_')
33
- .gsub(/\s/, '_')
34
- .gsub(/__+/, '_')
35
- .downcase
36
- end
37
-
38
34
  # For all keys in a Hash, convert Camelcase to underscore separated
39
35
  def convert_hash_keys(value)
40
36
  case value
41
- when Array
42
- value.map { |v| convert_hash_keys(v) }
43
- when Hash
44
- Hash[value.map { |k, v| [underscore_key(k), convert_hash_keys(v)] }]
45
- else
46
- value
37
+ when Array
38
+ value.map { |v| convert_hash_keys(v) }
39
+ when Hash
40
+ Hash[value.map { |k, v| [underscore_key(k), convert_hash_keys(v)] }]
41
+ else
42
+ value
47
43
  end
48
44
  end
49
45
 
data/lib/soaspec.rb CHANGED
@@ -15,9 +15,10 @@ require 'soaspec/matchers'
15
15
  require 'soaspec/soaspec_shared_examples'
16
16
  require 'soaspec/hash_methods'
17
17
  require 'soaspec/spec_logger'
18
- require 'soaspec/file_helpers'
18
+ require 'soaspec/exe_helpers'
19
19
  require 'soaspec/rest_handler'
20
20
 
21
+
21
22
  # Gem for handling SOAP and REST api tests
22
23
  module Soaspec
23
24
 
@@ -0,0 +1,97 @@
1
+
2
+ module Soaspec
3
+ # Help with tasks common to soaspec executables
4
+ module ExeHelpers
5
+ require 'fileutils'
6
+
7
+ # @param [String] filename Name of the file to create
8
+ # @param [String] content Content to place inside file
9
+ def create_file(filename: nil, content: nil, ignore_if_present: false)
10
+ #filename = options[:filename]
11
+ raise 'Need to pass filename' unless filename
12
+ #content = options[:content]
13
+ raise 'Need to pass contents to insert into file' unless content
14
+ if File.exist? filename
15
+ old_content = File.read(filename)
16
+ if old_content != content && !ignore_if_present
17
+ warn "!! #{filename} already exists and differs from template"
18
+ end
19
+ else
20
+ File.open(filename, 'w') { |f| f.puts content }
21
+ puts 'Created: ' + filename
22
+ end
23
+ end
24
+
25
+ def create_folder(folder)
26
+ if File.exist? folder
27
+ unless File.directory? folder
28
+ warn "!! #{folder} already exists and is not a directory"
29
+ end
30
+ else
31
+ FileUtils.mkdir folder
32
+ puts "Created folder: #{folder}/"
33
+ end
34
+ end
35
+
36
+ def rake_content
37
+ <<-EOF
38
+ # The list of task for a Rake file can be seen with `rake -T`
39
+ require 'rspec/core/rake_task' # See See https://relishapp.com/rspec/rspec-core/docs/command-line/rake-task for details
40
+
41
+ # This runs `rspec` command with the following options. Type `rake spec` to run this task
42
+ RSpec::Core::RakeTask.new(:spec) do |t|
43
+ t.pattern = "spec/*_spec.rb" # Run all specs in 'spec' folder ending in '_spec'
44
+ # Next line shows output on the screen, Junit xml report and an HTML report
45
+ t.rspec_opts = "--format documentation --format RspecJunitFormatter --out logs/spec.xml --format html --out logs/spec.html"
46
+ t.fail_on_error = false
47
+ end
48
+
49
+ task :default => :spec # This runs the 'spec' task by default when no task is mentioned. E.g., if only `rake` is typed
50
+ EOF
51
+ end
52
+
53
+ def gem_content
54
+ <<-EOF
55
+ source 'https://rubygems.org'
56
+
57
+ gem 'data_magic'
58
+ gem 'require_all'
59
+ gem 'rspec_junit_formatter'
60
+ gem 'rake'
61
+ gem 'soaspec'
62
+
63
+ EOF
64
+ end
65
+
66
+ def spec_helper_content
67
+ <<-EOF
68
+
69
+ require 'soaspec'
70
+ require 'require_all'
71
+ require_all 'lib'
72
+ require 'data_magic'
73
+
74
+ include DataMagic # Used as example of loading data smartly. Use 'data_for' method to load yml data
75
+
76
+ RSpec.configure do |config|
77
+ # This will make backtrace much shorter by removing many lines from rspec failure message
78
+ config.backtrace_exclusion_patterns = [
79
+ /rspec/
80
+ ]
81
+ end
82
+
83
+ EOF
84
+ end
85
+
86
+ # Convert key in camelcase to underscore separated (snakecase)
87
+ def underscore_key(key)
88
+ key.to_s.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
89
+ .gsub(/([a-z\d])([A-Z])/,'\1_\2')
90
+ .tr('-', '_')
91
+ .gsub(/\s/, '_')
92
+ .gsub(/__+/, '_')
93
+ .downcase
94
+ end
95
+
96
+ end
97
+ end
@@ -1,3 +1,3 @@
1
1
  module Soaspec
2
- VERSION = '0.0.23'
2
+ VERSION = '0.0.24'
3
3
  end
data/test_wsdl.rb CHANGED
@@ -3,22 +3,42 @@ require 'wasabi'
3
3
  require 'savon'
4
4
 
5
5
 
6
- document = Savon.client(wsdl: 'test.wsdl').wsdl
6
+ #document = Savon.client(wsdl: 'test.wsdl').wsdl
7
7
  #document = Savon.client(wsdl: 'http://www.webservicex.com/globalweather.asmx?wsdl').wsdl
8
8
  #document = Wasabi.document File.read('test.wsdl')
9
+ document = Wasabi.document 'http://www.webservicex.net/ConvertTemperature.asmx?WSDL'
9
10
  parser = document.parser
10
11
 
11
12
 
12
-
13
- parser.schemas.each do |schema|
14
- puts 'SCHEMA * '
15
- puts schema
16
-
17
- xsd = Nokogiri::XML::Schema(schema.to_s)
18
-
19
- doc = Nokogiri::XML(File.read('test.xml'))
20
-
21
- xsd.validate(doc).each do |error|
22
- puts error.message
13
+ # TEST SCHEMA
14
+ #
15
+ # parser.schemas.each do |schema|
16
+ # puts 'SCHEMA * '
17
+ # puts schema
18
+ #
19
+ # xsd = Nokogiri::XML::Schema(schema.to_s)
20
+ #
21
+ # doc = Nokogiri::XML(File.read('test.xml'))
22
+ #
23
+ # xsd.validate(doc).each do |error|
24
+ # puts error.message
25
+ # end
26
+ # end
27
+ #
28
+
29
+ schemes = parser.schemas
30
+ puts schemes
31
+ custom_type = schemes.xpath("//*[@name='TemperatureUnit']")
32
+ if custom_type.first
33
+ puts 'CUST' + custom_type.to_s
34
+ prefix = custom_type.first.namespace.prefix
35
+ puts prefix
36
+
37
+ enumerations = custom_type.xpath("//#{prefix}:enumeration")
38
+
39
+ puts 'ENUM' + enumerations.to_s
40
+
41
+ enumerations.each do |enum_value|
42
+ puts enum_value['value']
23
43
  end
24
44
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: soaspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.23
4
+ version: 0.0.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - SamuelGarrattIQA
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-02-23 00:00:00.000000000 Z
11
+ date: 2018-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -153,7 +153,7 @@ files:
153
153
  - lib/soaspec.rb
154
154
  - lib/soaspec/basic_soap_handler.rb
155
155
  - lib/soaspec/exchange.rb
156
- - lib/soaspec/file_helpers.rb
156
+ - lib/soaspec/exe_helpers.rb
157
157
  - lib/soaspec/hash_methods.rb
158
158
  - lib/soaspec/matchers.rb
159
159
  - lib/soaspec/rest_handler.rb
@@ -1,35 +0,0 @@
1
-
2
- module Soaspec
3
- module FileHelpers
4
- require 'fileutils'
5
-
6
- def self.create_file(options)
7
- filename = options[:filename]
8
- raise 'Need to pass filename' unless filename
9
- content = options[:content]
10
- raise 'Need to pass contents to insert into file' unless content
11
- if File.exist? filename
12
- old_content = File.read(filename)
13
- if old_content != content && !options[:ignore_if_present]
14
- warn "!! #{filename} already exists and differs from template"
15
- end
16
- else
17
- File.open(filename, 'w') do |f|
18
- f.puts content
19
- end
20
- puts 'Created: ' + filename
21
- end
22
- end
23
-
24
- def self.create_folder(folder)
25
- if File.exists? folder
26
- unless File.directory? folder
27
- $stderr.puts "!! #{folder} already exists and is not a directory"
28
- end
29
- else
30
- FileUtils.mkdir folder
31
- puts "Created folder: #{folder}/"
32
- end
33
- end
34
- end
35
- end