tamalw-crack 0.1.3

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/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ *.gem
data/History ADDED
@@ -0,0 +1,15 @@
1
+ == 0.1.3 2009-06-22
2
+ * 1 minor patch
3
+ * Calling Crack::XML.parse(xml, :explicit) will include text and attribute combo nodes as arrays (tamalw)
4
+
5
+ == 0.1.2 2009-04-21
6
+ * 2 minor patches
7
+ * Correct unnormalization of attribute values (der-flo)
8
+ * Fix error in parsing YAML in the case where a hash value ends with backslashes, and there are subsequent values in the hash (deadprogrammer)
9
+
10
+ == 0.1.1 2009-03-31
11
+ * 1 minor patch
12
+ * Parsing empty or blank xml now returns empty hash instead of raising error.
13
+
14
+ == 0.1.0 2009-03-28
15
+ * Initial release.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 John Nunemaker
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,26 @@
1
+ = crack
2
+
3
+ Really simple JSON and XML parsing, ripped from Merb and Rails. The XML parser is ripped from Merb and the JSON parser is ripped from Rails. I take no credit, just packaged them for all to enjoy and easily use.
4
+
5
+ = usage
6
+
7
+ gem 'crack'
8
+ require 'crack' # for xml and json
9
+ require 'crack/json' # for just json
10
+ require 'crack/xml' # for just xml
11
+
12
+ = examples
13
+
14
+ Crack::XML.parse("<tag>This is the contents</tag>")
15
+ # => {'tag' => 'This is the contents'}
16
+
17
+ Crack::JSON.parse('{"tag":"This is the contents"}')
18
+ # => {'tag' => 'This is the contents'}
19
+
20
+ == Copyright
21
+
22
+ Copyright (c) 2009 John Nunemaker. See LICENSE for details.
23
+
24
+ == Docs
25
+
26
+ http://rdoc.info/projects/jnunemaker/crack
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "crack"
8
+ gem.summary = %Q{Really simple JSON and XML parsing, ripped from Merb and Rails.}
9
+ gem.email = "nunemaker@gmail.com"
10
+ gem.homepage = "http://github.com/jnunemaker/crack"
11
+ gem.authors = ["John Nunemaker"]
12
+ gem.rubyforge_project = 'crack'
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ require 'rake/rdoctask'
20
+ Rake::RDocTask.new do |rdoc|
21
+ rdoc.rdoc_dir = 'rdoc'
22
+ rdoc.title = 'crack'
23
+ rdoc.options << '--line-numbers' << '--inline-source'
24
+ rdoc.rdoc_files.include('README*')
25
+ rdoc.rdoc_files.include('lib/**/*.rb')
26
+ end
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = false
33
+ end
34
+
35
+ begin
36
+ require 'rcov/rcovtask'
37
+ Rcov::RcovTask.new do |test|
38
+ test.libs << 'test'
39
+ test.pattern = 'test/**/*_test.rb'
40
+ test.verbose = true
41
+ end
42
+ rescue LoadError
43
+ task :rcov do
44
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
45
+ end
46
+ end
47
+
48
+
49
+ task :default => :test
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 3
data/crack.gemspec ADDED
@@ -0,0 +1,57 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{crack}
5
+ s.version = "0.1.3"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["John Nunemaker"]
9
+ s.date = %q{2009-06-23}
10
+ s.email = %q{nunemaker@gmail.com}
11
+ s.extra_rdoc_files = [
12
+ "LICENSE",
13
+ "README.rdoc"
14
+ ]
15
+ s.files = [
16
+ ".gitignore",
17
+ "History",
18
+ "LICENSE",
19
+ "README.rdoc",
20
+ "Rakefile",
21
+ "VERSION.yml",
22
+ "crack.gemspec",
23
+ "lib/crack.rb",
24
+ "lib/crack/core_extensions.rb",
25
+ "lib/crack/json.rb",
26
+ "lib/crack/xml.rb",
27
+ "test/crack_test.rb",
28
+ "test/data/twittersearch-firefox.json",
29
+ "test/data/twittersearch-ie.json",
30
+ "test/json_test.rb",
31
+ "test/test_helper.rb",
32
+ "test/xml_test.rb"
33
+ ]
34
+ s.has_rdoc = true
35
+ s.homepage = %q{http://github.com/jnunemaker/crack}
36
+ s.rdoc_options = ["--charset=UTF-8"]
37
+ s.require_paths = ["lib"]
38
+ s.rubyforge_project = %q{crack}
39
+ s.rubygems_version = %q{1.3.1}
40
+ s.summary = %q{Really simple JSON and XML parsing, ripped from Merb and Rails.}
41
+ s.test_files = [
42
+ "test/crack_test.rb",
43
+ "test/json_test.rb",
44
+ "test/test_helper.rb",
45
+ "test/xml_test.rb"
46
+ ]
47
+
48
+ if s.respond_to? :specification_version then
49
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
50
+ s.specification_version = 2
51
+
52
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
53
+ else
54
+ end
55
+ else
56
+ end
57
+ end
data/lib/crack.rb ADDED
@@ -0,0 +1,7 @@
1
+ module Crack
2
+ class ParseError < StandardError; end
3
+ end
4
+
5
+ require 'crack/core_extensions'
6
+ require 'crack/json'
7
+ require 'crack/xml'
@@ -0,0 +1,126 @@
1
+ class Object #:nodoc:
2
+ # @return <TrueClass, FalseClass>
3
+ #
4
+ # @example [].blank? #=> true
5
+ # @example [1].blank? #=> false
6
+ # @example [nil].blank? #=> false
7
+ #
8
+ # Returns true if the object is nil or empty (if applicable)
9
+ def blank?
10
+ nil? || (respond_to?(:empty?) && empty?)
11
+ end unless method_defined?(:blank?)
12
+ end # class Object
13
+
14
+ class Numeric #:nodoc:
15
+ # @return <TrueClass, FalseClass>
16
+ #
17
+ # Numerics can't be blank
18
+ def blank?
19
+ false
20
+ end unless method_defined?(:blank?)
21
+ end # class Numeric
22
+
23
+ class NilClass #:nodoc:
24
+ # @return <TrueClass, FalseClass>
25
+ #
26
+ # Nils are always blank
27
+ def blank?
28
+ true
29
+ end unless method_defined?(:blank?)
30
+ end # class NilClass
31
+
32
+ class TrueClass #:nodoc:
33
+ # @return <TrueClass, FalseClass>
34
+ #
35
+ # True is not blank.
36
+ def blank?
37
+ false
38
+ end unless method_defined?(:blank?)
39
+ end # class TrueClass
40
+
41
+ class FalseClass #:nodoc:
42
+ # False is always blank.
43
+ def blank?
44
+ true
45
+ end unless method_defined?(:blank?)
46
+ end # class FalseClass
47
+
48
+ class String #:nodoc:
49
+ # @example "".blank? #=> true
50
+ # @example " ".blank? #=> true
51
+ # @example " hey ho ".blank? #=> false
52
+ #
53
+ # @return <TrueClass, FalseClass>
54
+ #
55
+ # Strips out whitespace then tests if the string is empty.
56
+ def blank?
57
+ strip.empty?
58
+ end unless method_defined?(:blank?)
59
+
60
+ def snake_case
61
+ return self.downcase if self =~ /^[A-Z]+$/
62
+ self.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/, '_\&') =~ /_*(.*)/
63
+ return $+.downcase
64
+ end unless method_defined?(:snake_case)
65
+ end # class String
66
+
67
+ class Hash #:nodoc:
68
+ # @return <String> This hash as a query string
69
+ #
70
+ # @example
71
+ # { :name => "Bob",
72
+ # :address => {
73
+ # :street => '111 Ruby Ave.',
74
+ # :city => 'Ruby Central',
75
+ # :phones => ['111-111-1111', '222-222-2222']
76
+ # }
77
+ # }.to_params
78
+ # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
79
+ def to_params
80
+ params = self.map { |k,v| normalize_param(k,v) }.join
81
+ params.chop! # trailing &
82
+ params
83
+ end
84
+
85
+ # @param key<Object> The key for the param.
86
+ # @param value<Object> The value for the param.
87
+ #
88
+ # @return <String> This key value pair as a param
89
+ #
90
+ # @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&"
91
+ def normalize_param(key, value)
92
+ param = ''
93
+ stack = []
94
+
95
+ if value.is_a?(Array)
96
+ param << value.map { |element| normalize_param("#{key}[]", element) }.join
97
+ elsif value.is_a?(Hash)
98
+ stack << [key,value]
99
+ else
100
+ param << "#{key}=#{URI.encode(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}&"
101
+ end
102
+
103
+ stack.each do |parent, hash|
104
+ hash.each do |key, value|
105
+ if value.is_a?(Hash)
106
+ stack << ["#{parent}[#{key}]", value]
107
+ else
108
+ param << normalize_param("#{parent}[#{key}]", value)
109
+ end
110
+ end
111
+ end
112
+
113
+ param
114
+ end
115
+
116
+ # @return <String> The hash as attributes for an XML tag.
117
+ #
118
+ # @example
119
+ # { :one => 1, "two"=>"TWO" }.to_xml_attributes
120
+ # #=> 'one="1" two="TWO"'
121
+ def to_xml_attributes
122
+ map do |k,v|
123
+ %{#{k.to_s.snake_case.sub(/^(.{1,1})/) { |m| m.downcase }}="#{v}"}
124
+ end.join(' ')
125
+ end
126
+ end
data/lib/crack/json.rb ADDED
@@ -0,0 +1,68 @@
1
+ # Copyright (c) 2004-2008 David Heinemeier Hansson
2
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
3
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
4
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
5
+
6
+ require 'yaml'
7
+ require 'strscan'
8
+
9
+ module Crack
10
+ class JSON
11
+ def self.parse(json)
12
+ YAML.load(unescape(convert_json_to_yaml(json)))
13
+ rescue ArgumentError => e
14
+ raise ParseError, "Invalid JSON string"
15
+ end
16
+
17
+ protected
18
+ def self.unescape(str)
19
+ str.gsub(/\\u([0-9a-f]{4})/) { [$1.hex].pack("U") }
20
+ end
21
+
22
+ # matches YAML-formatted dates
23
+ DATE_REGEX = /^\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[ \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?$/
24
+
25
+ # Ensure that ":" and "," are always followed by a space
26
+ def self.convert_json_to_yaml(json) #:nodoc:
27
+ scanner, quoting, marks, pos, times = StringScanner.new(json), false, [], nil, []
28
+ while scanner.scan_until(/(\\['"]|['":,\\]|\\.)/)
29
+ case char = scanner[1]
30
+ when '"', "'"
31
+ if !quoting
32
+ quoting = char
33
+ pos = scanner.pos
34
+ elsif quoting == char
35
+ if json[pos..scanner.pos-2] =~ DATE_REGEX
36
+ # found a date, track the exact positions of the quotes so we can remove them later.
37
+ # oh, and increment them for each current mark, each one is an extra padded space that bumps
38
+ # the position in the final YAML output
39
+ total_marks = marks.size
40
+ times << pos+total_marks << scanner.pos+total_marks
41
+ end
42
+ quoting = false
43
+ end
44
+ when ":",","
45
+ marks << scanner.pos - 1 unless quoting
46
+ when "\\"
47
+ scanner.skip(/\\/)
48
+ end
49
+ end
50
+
51
+ if marks.empty?
52
+ json.gsub(/\\\//, '/')
53
+ else
54
+ left_pos = [-1].push(*marks)
55
+ right_pos = marks << json.length
56
+ output = []
57
+ left_pos.each_with_index do |left, i|
58
+ output << json[left.succ..right_pos[i]]
59
+ end
60
+ output = output * " "
61
+
62
+ times.each { |i| output[i-1] = ' ' }
63
+ output.gsub!(/\\\//, '/')
64
+ output
65
+ end
66
+ end
67
+ end
68
+ end
data/lib/crack/xml.rb ADDED
@@ -0,0 +1,214 @@
1
+ require 'rexml/parsers/streamparser'
2
+ require 'rexml/parsers/baseparser'
3
+ require 'rexml/light/node'
4
+ require 'rexml/text'
5
+ require 'date'
6
+ require 'time'
7
+ require 'yaml'
8
+ require 'bigdecimal'
9
+
10
+ # This is a slighly modified version of the XMLUtilityNode from
11
+ # http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
12
+ # It's mainly just adding vowels, as I ht cd wth n vwls :)
13
+ # This represents the hard part of the work, all I did was change the
14
+ # underlying parser.
15
+ class REXMLUtilityNode #:nodoc:
16
+ attr_accessor :name, :attributes, :children, :type
17
+
18
+ def self.typecasts
19
+ @@typecasts
20
+ end
21
+
22
+ def self.typecasts=(obj)
23
+ @@typecasts = obj
24
+ end
25
+
26
+ def self.available_typecasts
27
+ @@available_typecasts
28
+ end
29
+
30
+ def self.available_typecasts=(obj)
31
+ @@available_typecasts = obj
32
+ end
33
+
34
+ self.typecasts = {}
35
+ self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
36
+ self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
37
+ self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
38
+ self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
39
+ self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
40
+ self.typecasts["decimal"] = lambda{|v| v.nil? ? nil : BigDecimal(v.to_s)}
41
+ self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
42
+ self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
43
+ self.typecasts["symbol"] = lambda{|v| v.nil? ? nil : v.to_sym}
44
+ self.typecasts["string"] = lambda{|v| v.to_s}
45
+ self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
46
+ self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first }
47
+
48
+ self.available_typecasts = self.typecasts.keys
49
+
50
+ def initialize(mode, name, normalized_attributes = {})
51
+
52
+ # unnormalize attribute values
53
+ attributes = Hash[* normalized_attributes.map { |key, value|
54
+ [ key, unnormalize_xml_entities(value) ]
55
+ }.flatten]
56
+
57
+ @name = name.tr("-", "_")
58
+ # leave the type alone if we don't know what it is
59
+ @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
60
+
61
+ @nil_element = attributes.delete("nil") == "true"
62
+ @attributes = undasherize_keys(attributes)
63
+ @children = []
64
+ @text = false
65
+ @mode = mode
66
+ end
67
+
68
+ def add_node(node)
69
+ @text = true if node.is_a? String
70
+ @children << node
71
+ end
72
+
73
+ def to_hash
74
+ if @type == "file"
75
+ f = StringIO.new((@children.first || '').unpack('m').first)
76
+ class << f
77
+ attr_accessor :original_filename, :content_type
78
+ end
79
+ f.original_filename = attributes['name'] || 'untitled'
80
+ f.content_type = attributes['content_type'] || 'application/octet-stream'
81
+ return {name => f}
82
+ end
83
+
84
+ if @text
85
+ t = typecast_value( unnormalize_xml_entities( inner_html ) )
86
+ t = [t, attributes] if !attributes.empty? and @mode == :explicit
87
+ return { name => t }
88
+ else
89
+ #change repeating groups into an array
90
+ groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
91
+
92
+ out = nil
93
+ if @type == "array"
94
+ out = []
95
+ groups.each do |k, v|
96
+ if v.size == 1
97
+ out << v.first.to_hash.entries.first.last
98
+ else
99
+ out << v.map{|e| e.to_hash[k]}
100
+ end
101
+ end
102
+ out = out.flatten
103
+
104
+ else # If Hash
105
+ out = {}
106
+ groups.each do |k,v|
107
+ if v.size == 1
108
+ out.merge!(v.first)
109
+ else
110
+ out.merge!( k => v.map{|e| e.to_hash[k]})
111
+ end
112
+ end
113
+ out.merge! attributes unless attributes.empty?
114
+ out = out.empty? ? nil : out
115
+ end
116
+
117
+ if @type && out.nil?
118
+ { name => typecast_value(out) }
119
+ else
120
+ { name => out }
121
+ end
122
+ end
123
+ end
124
+
125
+ # Typecasts a value based upon its type. For instance, if
126
+ # +node+ has #type == "integer",
127
+ # {{[node.typecast_value("12") #=> 12]}}
128
+ #
129
+ # @param value<String> The value that is being typecast.
130
+ #
131
+ # @details [:type options]
132
+ # "integer"::
133
+ # converts +value+ to an integer with #to_i
134
+ # "boolean"::
135
+ # checks whether +value+, after removing spaces, is the literal
136
+ # "true"
137
+ # "datetime"::
138
+ # Parses +value+ using Time.parse, and returns a UTC Time
139
+ # "date"::
140
+ # Parses +value+ using Date.parse
141
+ #
142
+ # @return <Integer, TrueClass, FalseClass, Time, Date, Object>
143
+ # The result of typecasting +value+.
144
+ #
145
+ # @note
146
+ # If +self+ does not have a "type" key, or if it's not one of the
147
+ # options specified above, the raw +value+ will be returned.
148
+ def typecast_value(value)
149
+ return value unless @type
150
+ proc = self.class.typecasts[@type]
151
+ proc.nil? ? value : proc.call(value)
152
+ end
153
+
154
+ # Take keys of the form foo-bar and convert them to foo_bar
155
+ def undasherize_keys(params)
156
+ params.keys.each do |key, value|
157
+ params[key.tr("-", "_")] = params.delete(key)
158
+ end
159
+ params
160
+ end
161
+
162
+ # Get the inner_html of the REXML node.
163
+ def inner_html
164
+ @children.join
165
+ end
166
+
167
+ # Converts the node into a readable HTML node.
168
+ #
169
+ # @return <String> The HTML node in text form.
170
+ def to_html
171
+ attributes.merge!(:type => @type ) if @type
172
+ "<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
173
+ end
174
+
175
+ # @alias #to_html #to_s
176
+ def to_s
177
+ to_html
178
+ end
179
+
180
+ private
181
+
182
+ def unnormalize_xml_entities value
183
+ REXML::Text.unnormalize(value)
184
+ end
185
+ end
186
+
187
+ module Crack
188
+ class XML
189
+ def self.parse(xml,mode = :safe)
190
+ stack = []
191
+ parser = REXML::Parsers::BaseParser.new(xml)
192
+
193
+ while true
194
+ event = parser.pull
195
+ case event[0]
196
+ when :end_document
197
+ break
198
+ when :end_doctype, :start_doctype
199
+ # do nothing
200
+ when :start_element
201
+ stack.push REXMLUtilityNode.new(mode, event[1], event[2])
202
+ when :end_element
203
+ if stack.size > 1
204
+ temp = stack.pop
205
+ stack.last.add_node(temp)
206
+ end
207
+ when :text, :cdata
208
+ stack.last.add_node(event[1]) unless event[1].strip.length == 0 || stack.empty?
209
+ end
210
+ end
211
+ stack.length > 0 ? stack.pop.to_hash : {}
212
+ end
213
+ end
214
+ end