mustermann-contrib 1.0.0.beta2

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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +1239 -0
  3. data/examples/highlighting.rb +35 -0
  4. data/highlighting.png +0 -0
  5. data/irb.png +0 -0
  6. data/lib/mustermann/cake.rb +18 -0
  7. data/lib/mustermann/express.rb +37 -0
  8. data/lib/mustermann/file_utils.rb +217 -0
  9. data/lib/mustermann/file_utils/glob_pattern.rb +39 -0
  10. data/lib/mustermann/fileutils.rb +1 -0
  11. data/lib/mustermann/flask.rb +198 -0
  12. data/lib/mustermann/grape.rb +35 -0
  13. data/lib/mustermann/pyramid.rb +28 -0
  14. data/lib/mustermann/rails.rb +46 -0
  15. data/lib/mustermann/shell.rb +56 -0
  16. data/lib/mustermann/simple.rb +50 -0
  17. data/lib/mustermann/string_scanner.rb +313 -0
  18. data/lib/mustermann/strscan.rb +1 -0
  19. data/lib/mustermann/template.rb +62 -0
  20. data/lib/mustermann/uri_template.rb +1 -0
  21. data/lib/mustermann/versions.rb +46 -0
  22. data/lib/mustermann/visualizer.rb +38 -0
  23. data/lib/mustermann/visualizer/highlight.rb +137 -0
  24. data/lib/mustermann/visualizer/highlighter.rb +37 -0
  25. data/lib/mustermann/visualizer/highlighter/ad_hoc.rb +94 -0
  26. data/lib/mustermann/visualizer/highlighter/ast.rb +102 -0
  27. data/lib/mustermann/visualizer/highlighter/composite.rb +45 -0
  28. data/lib/mustermann/visualizer/highlighter/dummy.rb +18 -0
  29. data/lib/mustermann/visualizer/highlighter/regular.rb +104 -0
  30. data/lib/mustermann/visualizer/pattern_extension.rb +68 -0
  31. data/lib/mustermann/visualizer/renderer/ansi.rb +23 -0
  32. data/lib/mustermann/visualizer/renderer/generic.rb +46 -0
  33. data/lib/mustermann/visualizer/renderer/hansi_template.rb +34 -0
  34. data/lib/mustermann/visualizer/renderer/html.rb +50 -0
  35. data/lib/mustermann/visualizer/renderer/sexp.rb +37 -0
  36. data/lib/mustermann/visualizer/tree.rb +63 -0
  37. data/lib/mustermann/visualizer/tree_renderer.rb +78 -0
  38. data/mustermann-contrib.gemspec +19 -0
  39. data/spec/cake_spec.rb +90 -0
  40. data/spec/express_spec.rb +209 -0
  41. data/spec/file_utils_spec.rb +119 -0
  42. data/spec/flask_spec.rb +361 -0
  43. data/spec/flask_subclass_spec.rb +368 -0
  44. data/spec/grape_spec.rb +747 -0
  45. data/spec/pattern_extension_spec.rb +49 -0
  46. data/spec/pyramid_spec.rb +101 -0
  47. data/spec/rails_spec.rb +647 -0
  48. data/spec/shell_spec.rb +147 -0
  49. data/spec/simple_spec.rb +268 -0
  50. data/spec/string_scanner_spec.rb +271 -0
  51. data/spec/template_spec.rb +841 -0
  52. data/spec/visualizer_spec.rb +199 -0
  53. data/theme.png +0 -0
  54. data/tree.png +0 -0
  55. metadata +126 -0
@@ -0,0 +1,78 @@
1
+ require 'mustermann/visualizer/tree'
2
+ require 'mustermann/ast/translator'
3
+ require 'hansi'
4
+
5
+ module Mustermann
6
+ module Visualizer
7
+ # Turns an AST into a Tree
8
+ # @!visibility private
9
+ class TreeRenderer < AST::Translator
10
+ TEMPLATE = '"<base01>%s</base01><underline><green>%s</green></underline><base01>%s</base01>" '
11
+ THEME = Hansi::Theme[:solarized]
12
+ PREFIX_COLOR = THEME[:violet]
13
+ FakeNode = Struct.new(:type, :start, :stop, :length)
14
+ private_constant(:TEMPLATE, :THEME, :PREFIX_COLOR, :FakeNode)
15
+
16
+ # Takes a pattern (or pattern string and option) and turns it into a tree.
17
+ # Runs translation if pattern implements to_ast, otherwise returns single
18
+ # node tree.
19
+ #
20
+ # @!visibility private
21
+ def self.render(pattern, **options)
22
+ pattern &&= Mustermann.new(pattern, **options)
23
+ renderer = new(pattern.to_s)
24
+ if pattern.respond_to? :to_ast
25
+ renderer.translate(pattern.to_ast)
26
+ else
27
+ length = renderer.string.length
28
+ node = FakeNode.new("pattern (not AST based)", 0, length, length)
29
+ renderer.tree(node)
30
+ end
31
+ end
32
+
33
+ # @!visibility private
34
+ attr_reader :string
35
+
36
+ # @!visibility private
37
+ def initialize(string)
38
+ @string = string
39
+ end
40
+
41
+ # access a substring of the pattern, in inspect mode
42
+ # @!visibility private
43
+ def sub(*args)
44
+ string[*args].inspect[1..-2]
45
+ end
46
+
47
+ # creates a tree node
48
+ # @!visibility private
49
+ def tree(node, *children, **typed_children)
50
+ children += children_for(typed_children)
51
+ children = children.flatten.grep(Tree)
52
+ infos = sub(0, node.start), sub(node.start, node.length), sub(node.stop..-1)
53
+ description = Hansi.render(THEME[:green], node.type.to_s.tr("_", " "))
54
+ after = Hansi.render(TEMPLATE, *infos, theme: THEME, tags: true)
55
+ Tree.new(description, *children, after: after, prefix_color: PREFIX_COLOR)
56
+ end
57
+
58
+ # Take a hash with trees as values and turn the keys into trees, too.
59
+ # Read again if that didn't make sense.
60
+ # @!visibility private
61
+ def children_for(list)
62
+ list.map do |key, value|
63
+ value = Array(value).flatten
64
+ if value.any?
65
+ after = " " * string.inspect.length + " "
66
+ description = Hansi.render(THEME[:orange], key.to_s)
67
+ Tree.new(description, *value, after: after, prefix_color: PREFIX_COLOR)
68
+ end
69
+ end
70
+ end
71
+
72
+ translate(:node) { t.tree(node, payload: t(payload)) }
73
+ translate(:with_look_ahead) { t.tree(node, head: t(head), payload: t(payload)) }
74
+ translate(Array) { map { |e| t(e) }}
75
+ translate(Object) { }
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,19 @@
1
+ $:.unshift File.expand_path("../../mustermann/lib", __FILE__)
2
+ require "mustermann/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "mustermann-contrib"
6
+ s.version = Mustermann::VERSION
7
+ s.authors = ["Konstantin Haase", "Zachary Scott"]
8
+ s.email = "mail@zzak.io"
9
+ s.homepage = "https://github.com/sinatra/mustermann"
10
+ s.summary = %q{Collection of extensions for Mustermann}
11
+ s.description = %q{Adds many plugins to Mustermman}
12
+ s.license = 'MIT'
13
+ s.required_ruby_version = '>= 2.2.0'
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.add_dependency 'mustermann', Mustermann::VERSION
18
+ s.add_dependency 'hansi', '~> 0.2.0'
19
+ end
data/spec/cake_spec.rb ADDED
@@ -0,0 +1,90 @@
1
+ require 'support'
2
+ require 'mustermann/cake'
3
+
4
+ describe Mustermann::Cake do
5
+ extend Support::Pattern
6
+
7
+ pattern '' do
8
+ it { should match('') }
9
+ it { should_not match('/') }
10
+
11
+ it { should expand.to('') }
12
+ it { should_not expand(a: 1) }
13
+
14
+ it { should generate_template('') }
15
+
16
+ it { should respond_to(:expand) }
17
+ it { should respond_to(:to_templates) }
18
+ end
19
+
20
+ pattern '/' do
21
+ it { should match('/') }
22
+ it { should_not match('/foo') }
23
+
24
+ it { should expand.to('/') }
25
+ it { should_not expand(a: 1) }
26
+ end
27
+
28
+ pattern '/foo' do
29
+ it { should match('/foo') }
30
+ it { should_not match('/bar') }
31
+ it { should_not match('/foo.bar') }
32
+
33
+ it { should expand.to('/foo') }
34
+ it { should_not expand(a: 1) }
35
+ end
36
+
37
+ pattern '/foo/bar' do
38
+ it { should match('/foo/bar') }
39
+ it { should_not match('/foo%2Fbar') }
40
+ it { should_not match('/foo%2fbar') }
41
+
42
+ it { should expand.to('/foo/bar') }
43
+ it { should_not expand(a: 1) }
44
+ end
45
+
46
+ pattern '/:foo' do
47
+ it { should match('/foo') .capturing foo: 'foo' }
48
+ it { should match('/bar') .capturing foo: 'bar' }
49
+ it { should match('/foo.bar') .capturing foo: 'foo.bar' }
50
+ it { should match('/%0Afoo') .capturing foo: '%0Afoo' }
51
+ it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' }
52
+
53
+ it { should_not match('/foo?') }
54
+ it { should_not match('/foo/bar') }
55
+ it { should_not match('/') }
56
+ it { should_not match('/foo/') }
57
+
58
+ example { pattern.params('/foo') .should be == {"foo" => "foo"} }
59
+ example { pattern.params('/f%20o') .should be == {"foo" => "f o"} }
60
+ example { pattern.params('').should be_nil }
61
+
62
+ it { should expand(foo: 'bar') .to('/bar') }
63
+ it { should expand(foo: 'b r') .to('/b%20r') }
64
+ it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') }
65
+
66
+ it { should_not expand(foo: 'foo', bar: 'bar') }
67
+ it { should_not expand(bar: 'bar') }
68
+ it { should_not expand }
69
+
70
+ it { should generate_template('/{foo}') }
71
+ end
72
+
73
+ pattern '/*' do
74
+ it { should match('/') }
75
+ it { should match('/foo') }
76
+ it { should match('/foo/bar') }
77
+
78
+ example { pattern.params('/foo/bar') .should be == {"splat" => ["foo", "bar"]}}
79
+ it { should generate_template('/{+splat}') }
80
+ end
81
+
82
+ pattern '/**' do
83
+ it { should match('/') .capturing splat: '' }
84
+ it { should match('/foo') .capturing splat: 'foo' }
85
+ it { should match('/foo/bar') .capturing splat: 'foo/bar' }
86
+
87
+ example { pattern.params('/foo/bar') .should be == {"splat" => ["foo/bar"]} }
88
+ it { should generate_template('/{+splat}') }
89
+ end
90
+ end
@@ -0,0 +1,209 @@
1
+ require 'support'
2
+ require 'mustermann/express'
3
+
4
+ describe Mustermann::Express do
5
+ extend Support::Pattern
6
+
7
+ pattern '' do
8
+ it { should match('') }
9
+ it { should_not match('/') }
10
+
11
+ it { should expand.to('') }
12
+ it { should_not expand(a: 1) }
13
+
14
+ it { should generate_template('') }
15
+
16
+ it { should respond_to(:expand) }
17
+ it { should respond_to(:to_templates) }
18
+ end
19
+
20
+ pattern '/' do
21
+ it { should match('/') }
22
+ it { should_not match('/foo') }
23
+
24
+ it { should expand.to('/') }
25
+ it { should_not expand(a: 1) }
26
+ end
27
+
28
+ pattern '/foo' do
29
+ it { should match('/foo') }
30
+ it { should_not match('/bar') }
31
+ it { should_not match('/foo.bar') }
32
+
33
+ it { should expand.to('/foo') }
34
+ it { should_not expand(a: 1) }
35
+ end
36
+
37
+ pattern '/foo/bar' do
38
+ it { should match('/foo/bar') }
39
+ it { should_not match('/foo%2Fbar') }
40
+ it { should_not match('/foo%2fbar') }
41
+
42
+ it { should expand.to('/foo/bar') }
43
+ it { should_not expand(a: 1) }
44
+ end
45
+
46
+ pattern '/:foo' do
47
+ it { should match('/foo') .capturing foo: 'foo' }
48
+ it { should match('/bar') .capturing foo: 'bar' }
49
+ it { should match('/foo.bar') .capturing foo: 'foo.bar' }
50
+ it { should match('/%0Afoo') .capturing foo: '%0Afoo' }
51
+ it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' }
52
+
53
+ it { should_not match('/foo?') }
54
+ it { should_not match('/foo/bar') }
55
+ it { should_not match('/') }
56
+ it { should_not match('/foo/') }
57
+
58
+ example { pattern.params('/foo') .should be == {"foo" => "foo"} }
59
+ example { pattern.params('/f%20o') .should be == {"foo" => "f o"} }
60
+ example { pattern.params('').should be_nil }
61
+
62
+ it { should expand(foo: 'bar') .to('/bar') }
63
+ it { should expand(foo: 'b r') .to('/b%20r') }
64
+ it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') }
65
+
66
+ it { should_not expand(foo: 'foo', bar: 'bar') }
67
+ it { should_not expand(bar: 'bar') }
68
+ it { should_not expand }
69
+
70
+ it { should generate_template('/{foo}') }
71
+ end
72
+
73
+ pattern '/:foo+' do
74
+ it { should_not match('/') }
75
+ it { should match('/foo') .capturing foo: 'foo' }
76
+ it { should match('/foo/bar') .capturing foo: 'foo/bar' }
77
+
78
+ it { should expand .to('/') }
79
+ it { should expand(foo: nil) .to('/') }
80
+ it { should expand(foo: '') .to('/') }
81
+ it { should expand(foo: 'foo') .to('/foo') }
82
+ it { should expand(foo: 'foo/bar') .to('/foo/bar') }
83
+ it { should expand(foo: 'foo.bar') .to('/foo.bar') }
84
+
85
+ it { should generate_template('/{+foo}') }
86
+ end
87
+
88
+ pattern '/:foo?' do
89
+ it { should match('/foo') .capturing foo: 'foo' }
90
+ it { should match('/bar') .capturing foo: 'bar' }
91
+ it { should match('/foo.bar') .capturing foo: 'foo.bar' }
92
+ it { should match('/%0Afoo') .capturing foo: '%0Afoo' }
93
+ it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' }
94
+ it { should match('/') }
95
+
96
+ it { should_not match('/foo?') }
97
+ it { should_not match('/foo/bar') }
98
+ it { should_not match('/foo/') }
99
+
100
+ example { pattern.params('/foo') .should be == {"foo" => "foo"} }
101
+ example { pattern.params('/f%20o') .should be == {"foo" => "f o"} }
102
+ example { pattern.params('/') .should be == {"foo" => nil } }
103
+
104
+ it { should expand(foo: 'bar') .to('/bar') }
105
+ it { should expand(foo: 'b r') .to('/b%20r') }
106
+ it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') }
107
+ it { should expand .to('/') }
108
+
109
+ it { should_not expand(foo: 'foo', bar: 'bar') }
110
+ it { should_not expand(bar: 'bar') }
111
+
112
+ it { should generate_template('/{foo}') }
113
+ it { should generate_template('/') }
114
+ end
115
+
116
+ pattern '/:foo*' do
117
+ it { should match('/') .capturing foo: '' }
118
+ it { should match('/foo') .capturing foo: 'foo' }
119
+ it { should match('/foo/bar') .capturing foo: 'foo/bar' }
120
+
121
+ it { should expand .to('/') }
122
+ it { should expand(foo: nil) .to('/') }
123
+ it { should expand(foo: '') .to('/') }
124
+ it { should expand(foo: 'foo') .to('/foo') }
125
+ it { should expand(foo: 'foo/bar') .to('/foo/bar') }
126
+ it { should expand(foo: 'foo.bar') .to('/foo.bar') }
127
+
128
+ it { should generate_template('/{+foo}') }
129
+ end
130
+
131
+ pattern '/:foo(.*)' do
132
+ it { should match('/') .capturing foo: '' }
133
+ it { should match('/foo') .capturing foo: 'foo' }
134
+ it { should match('/foo/bar') .capturing foo: 'foo/bar' }
135
+
136
+ it { should expand(foo: '') .to('/') }
137
+ it { should expand(foo: 'foo') .to('/foo') }
138
+ it { should expand(foo: 'foo/bar') .to('/foo/bar') }
139
+ it { should expand(foo: 'foo.bar') .to('/foo.bar') }
140
+
141
+ it { should generate_template('/{foo}') }
142
+ end
143
+
144
+ pattern '/:foo(\d+)' do
145
+ it { should_not match('/') }
146
+ it { should_not match('/foo') }
147
+ it { should match('/15') .capturing foo: '15' }
148
+ it { should generate_template('/{foo}') }
149
+ end
150
+
151
+ pattern '/:foo(\d+|bar)' do
152
+ it { should_not match('/') }
153
+ it { should_not match('/foo') }
154
+ it { should match('/15') .capturing foo: '15' }
155
+ it { should match('/bar') .capturing foo: 'bar' }
156
+ it { should generate_template('/{foo}') }
157
+ end
158
+
159
+ pattern '/:foo(\))' do
160
+ it { should_not match('/') }
161
+ it { should_not match('/foo') }
162
+ it { should match('/)').capturing foo: ')' }
163
+ it { should generate_template('/{foo}') }
164
+ end
165
+
166
+ pattern '/:foo(prefix(\d+|bar))' do
167
+ it { should_not match('/prefix') }
168
+ it { should_not match('/prefixfoo') }
169
+ it { should match('/prefix15') .capturing foo: 'prefix15' }
170
+ it { should match('/prefixbar') .capturing foo: 'prefixbar' }
171
+ it { should generate_template('/{foo}') }
172
+ end
173
+
174
+ pattern '/(.+)' do
175
+ it { should_not match('/') }
176
+ it { should match('/foo') .capturing splat: 'foo' }
177
+ it { should match('/foo/bar') .capturing splat: 'foo/bar' }
178
+ it { should generate_template('/{+splat}') }
179
+ end
180
+
181
+ pattern '/(foo(a|b))' do
182
+ it { should_not match('/') }
183
+ it { should match('/fooa') .capturing splat: 'fooa' }
184
+ it { should match('/foob') .capturing splat: 'foob' }
185
+ it { should generate_template('/{+splat}') }
186
+ end
187
+
188
+ context 'invalid syntax' do
189
+ example 'unexpected closing parenthesis' do
190
+ expect { Mustermann::Express.new('foo)bar') }.
191
+ to raise_error(Mustermann::ParseError, 'unexpected ) while parsing "foo)bar"')
192
+ end
193
+
194
+ example 'missing closing parenthesis' do
195
+ expect { Mustermann::Express.new('foo(bar') }.
196
+ to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo(bar"')
197
+ end
198
+
199
+ example 'unexpected ?' do
200
+ expect { Mustermann::Express.new('foo?bar') }.
201
+ to raise_error(Mustermann::ParseError, 'unexpected ? while parsing "foo?bar"')
202
+ end
203
+
204
+ example 'unexpected *' do
205
+ expect { Mustermann::Express.new('foo*bar') }.
206
+ to raise_error(Mustermann::ParseError, 'unexpected * while parsing "foo*bar"')
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,119 @@
1
+ require 'support'
2
+ require 'mustermann/file_utils'
3
+
4
+ describe Mustermann::FileUtils do
5
+ subject(:utils) { Mustermann::FileUtils }
6
+ include FileUtils
7
+
8
+ before do
9
+ @pwd = Dir.pwd
10
+ @tmp_dir = File.join(__dir__, 'tmp')
11
+
12
+ mkdir_p(@tmp_dir)
13
+ chdir(@tmp_dir)
14
+
15
+ touch("foo.txt")
16
+ touch("foo.rb")
17
+ touch("bar.txt")
18
+ touch("bar.rb")
19
+ end
20
+
21
+ after do
22
+ chdir(@pwd) if @pwd
23
+ rm_rf(@tmp_dir) if @tmp_dir
24
+ end
25
+
26
+ describe :glob_pattern do
27
+ example { utils.glob_pattern('/:foo') .should be == '/*' }
28
+ example { utils.glob_pattern('/*foo') .should be == '/**/*' }
29
+ example { utils.glob_pattern('/(ab|c)?/:foo/d/*bar') .should be == '/{{ab,c},}/*/d/**/*' }
30
+ example { utils.glob_pattern('/a', '/b') .should be == '{/a,/b}' }
31
+ example { utils.glob_pattern('**/*', type: :shell) .should be == '**/*' }
32
+ example { utils.glob_pattern(/can't parse this/) .should be == '**/*' }
33
+ example { utils.glob_pattern('/foo', type: :identity) .should be == '/foo' }
34
+ example { utils.glob_pattern('/fo*', type: :identity) .should be == '/fo\\*' }
35
+ end
36
+
37
+ describe :glob do
38
+ example do
39
+ utils.glob(":name.txt").sort.should be == ['bar.txt', 'foo.txt']
40
+ end
41
+
42
+ example do
43
+ extensions = []
44
+ utils.glob("foo.:ext") { |file, params| extensions << params['ext'] }
45
+ extensions.sort.should be == ['rb', 'txt']
46
+ end
47
+
48
+ example do
49
+ utils.glob(":name.:ext", capture: { ext: 'rb', name: 'foo' }).should be == ['foo.rb']
50
+ end
51
+ end
52
+
53
+ describe :glob_map do
54
+ example do
55
+ utils.glob_map(':name.rb' => ':name/init.rb').should be == {
56
+ "bar.rb" => "bar/init.rb",
57
+ "foo.rb" => "foo/init.rb"
58
+ }
59
+ end
60
+
61
+ example do
62
+ result = {}
63
+ returned = utils.glob_map(':name.rb' => ':name/init.rb') { |k, v| result[v] = k.upcase }
64
+ returned.sort .should be == ["BAR.RB", "FOO.RB"]
65
+ result["bar/init.rb"] .should be == "BAR.RB"
66
+ end
67
+ end
68
+
69
+ describe :cp do
70
+ example do
71
+ utils.cp(':name.rb' => ':name.ruby', ':name.txt' => ':name.md')
72
+ File.should be_exist('foo.ruby')
73
+ File.should be_exist('bar.md')
74
+ File.should be_exist('bar.txt')
75
+ end
76
+ end
77
+
78
+ describe :cp_r do
79
+ example do
80
+ mkdir_p "foo/bar"
81
+ utils.cp_r('foo/:name' => :name)
82
+ File.should be_directory('bar')
83
+ end
84
+ end
85
+
86
+ describe :mv do
87
+ example do
88
+ utils.mv(':name.rb' => ':name.ruby', ':name.txt' => ':name.md')
89
+ File.should be_exist('foo.ruby')
90
+ File.should be_exist('bar.md')
91
+ File.should_not be_exist('bar.txt')
92
+ end
93
+ end
94
+
95
+ describe :ln do
96
+ example do
97
+ utils.ln(':name.rb' => ':name.ruby', ':name.txt' => ':name.md')
98
+ File.should be_exist('foo.ruby')
99
+ File.should be_exist('bar.md')
100
+ File.should be_exist('bar.txt')
101
+ end
102
+ end
103
+
104
+ describe :ln_s do
105
+ example do
106
+ utils.ln_s(':name.rb' => ':name.ruby', ':name.txt' => ':name.md')
107
+ File.should be_symlink('foo.ruby')
108
+ File.should be_symlink('bar.md')
109
+ File.should be_exist('bar.txt')
110
+ end
111
+ end
112
+
113
+ describe :ln_sf do
114
+ example do
115
+ utils.ln_sf(':name.rb' => ':name.txt')
116
+ File.should be_symlink('foo.txt')
117
+ end
118
+ end
119
+ end