http_router 0.10.2 → 0.11.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.
Files changed (42) hide show
  1. data/.gitignore +2 -1
  2. data/Rakefile +7 -5
  3. data/benchmarks/gen2.rb +1 -1
  4. data/benchmarks/rack_mount.rb +8 -14
  5. data/examples/rack_mapper.ru +12 -13
  6. data/examples/variable_with_regex.ru +1 -1
  7. data/http_router.gemspec +1 -1
  8. data/lib/http_router.rb +159 -62
  9. data/lib/http_router/generation_helper.rb +29 -0
  10. data/lib/http_router/generator.rb +150 -0
  11. data/lib/http_router/node.rb +27 -17
  12. data/lib/http_router/node/abstract_request_node.rb +31 -0
  13. data/lib/http_router/node/host.rb +9 -0
  14. data/lib/http_router/node/lookup.rb +8 -10
  15. data/lib/http_router/node/path.rb +23 -38
  16. data/lib/http_router/node/request_method.rb +16 -0
  17. data/lib/http_router/node/root.rb +104 -10
  18. data/lib/http_router/node/scheme.rb +9 -0
  19. data/lib/http_router/node/user_agent.rb +9 -0
  20. data/lib/http_router/regex_route_generation.rb +10 -0
  21. data/lib/http_router/request.rb +7 -17
  22. data/lib/http_router/response.rb +4 -0
  23. data/lib/http_router/route.rb +16 -277
  24. data/lib/http_router/route_helper.rb +126 -0
  25. data/lib/http_router/util.rb +1 -37
  26. data/lib/http_router/version.rb +1 -1
  27. data/test/common/generate.txt +1 -1
  28. data/test/generation.rb +15 -11
  29. data/test/generic.rb +2 -3
  30. data/test/helper.rb +15 -10
  31. data/test/rack/test_route.rb +0 -5
  32. data/test/test_misc.rb +50 -40
  33. data/test/test_mounting.rb +27 -26
  34. data/test/test_recognition.rb +1 -76
  35. metadata +104 -161
  36. data/.rspec +0 -1
  37. data/lib/http_router/node/arbitrary.rb +0 -30
  38. data/lib/http_router/node/request.rb +0 -52
  39. data/lib/http_router/rack.rb +0 -19
  40. data/lib/http_router/rack/builder.rb +0 -61
  41. data/lib/http_router/rack/url_map.rb +0 -16
  42. data/lib/http_router/regex_route.rb +0 -39
@@ -0,0 +1,29 @@
1
+ class HttpRouter
2
+ module GenerationHelper
3
+ def max_param_count
4
+ @generator.max_param_count
5
+ end
6
+
7
+ def url(*args)
8
+ @generator.url(*args)
9
+ rescue InvalidRouteException
10
+ nil
11
+ end
12
+
13
+ def url_ns(*args)
14
+ @generator.url_ns(*args)
15
+ rescue InvalidRouteException
16
+ nil
17
+ end
18
+
19
+ def path(*args)
20
+ @generator.path(*args)
21
+ rescue InvalidRouteException
22
+ nil
23
+ end
24
+
25
+ def param_names
26
+ @generator.param_names
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,150 @@
1
+ class HttpRouter
2
+ class Generator
3
+ SCHEME_PORTS = {'http' => 80, 'https' => 443}
4
+
5
+ class PathGenerator
6
+ attr_reader :path
7
+ attr_accessor :param_names
8
+ def initialize(route, path, validation_regex = nil)
9
+ @route = route
10
+ @path = path.dup
11
+ @param_names = []
12
+ if path.is_a?(String)
13
+ path[0, 0] = '/' unless path[0] == ?/
14
+ regex_parts = path.split(/([:\*][a-zA-Z0-9_]+)/)
15
+ regex, code = '', ''
16
+ dynamic = false
17
+ regex_parts.each_with_index do |part, index|
18
+ case part[0]
19
+ when ?:, ?*
20
+ if index != 0 && regex_parts[index - 1][-1] == ?\\
21
+ regex << Regexp.quote(part) unless validation_regex
22
+ code << part
23
+ dynamic = true
24
+ else
25
+ regex << (@route.matches_with(part[1, part.size].to_sym) || '.*?').to_s unless validation_regex
26
+ code << "\#{args.shift || (options && options.delete(:#{part[1, part.size]})) || return}"
27
+ dynamic = true
28
+ end
29
+ else
30
+ regex << Regexp.quote(part) unless validation_regex
31
+ code << part
32
+ end
33
+ end
34
+ validation_regex ||= Regexp.new("^#{regex}$") if dynamic
35
+ if validation_regex
36
+ instance_eval <<-EOT, __FILE__, __LINE__ + 1
37
+ def generate(args, options)
38
+ generated_path = \"#{code}\"
39
+ #{validation_regex.inspect}.match(generated_path) ? URI.escape(generated_path) : nil
40
+ end
41
+ EOT
42
+ else
43
+ instance_eval <<-EOT, __FILE__, __LINE__ + 1
44
+ def generate(args, options)
45
+ URI.escape(\"#{code}\")
46
+ end
47
+ EOT
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def initialize(route, paths)
54
+ @route, @paths = route, paths
55
+ @router = @route.router
56
+ @route.generator = self
57
+ @path_generators = @paths.map do |p|
58
+ generator = PathGenerator.new(route, p.is_a?(String) ? p : route.path_for_generation, p.is_a?(Regexp) ? p : nil)
59
+ end
60
+ end
61
+
62
+ def param_names
63
+ @param_names ||= @path_generators.map{|path| path.param_names}.flatten.uniq
64
+ end
65
+
66
+ def max_param_count
67
+ @max_param_count ||= @path_generators.map{|p| p.param_names.size}.max
68
+ end
69
+
70
+ def each_path
71
+ @path_generators.each {|p| yield p }
72
+ @path_generators.sort! do |p1, p2|
73
+ p2.param_names.size <=> p1.param_names.size
74
+ end
75
+ end
76
+
77
+ def url(*args)
78
+ "#{scheme_port.first}#{url_ns(*args)}"
79
+ end
80
+
81
+ def url_ns(*args)
82
+ "://#{@route.host || @router.default_host}#{scheme_port.last}#{path(*args)}"
83
+ end
84
+
85
+ def path(*args)
86
+ result, extra_params = path_with_params(*args)
87
+ append_querystring(result, extra_params)
88
+ end
89
+
90
+ private
91
+ def scheme_port
92
+ @scheme_port ||= begin
93
+ scheme = @route.scheme || @router.default_scheme
94
+ port = @router.default_port
95
+ port_part = SCHEME_PORTS.key?(scheme) && SCHEME_PORTS[scheme] == port ? '' : ":#{port}"
96
+ [scheme, port_part]
97
+ end
98
+ end
99
+
100
+ def path_with_params(*a)
101
+ path_args_processing(a) do |args, options|
102
+ path = args.empty? ? matching_path(options) : matching_path(args, options)
103
+ path &&= path.generate(args, options)
104
+ raise TooManyParametersException unless args.empty?
105
+ raise InvalidRouteException.new("Error generating #{@route.path_for_generation}") unless path
106
+ path ? [path, options] : nil
107
+ end
108
+ end
109
+
110
+ def path_args_processing(args)
111
+ options = args.last.is_a?(Hash) ? args.pop : nil
112
+ options = options.nil? ? @route.default_values.dup : @route.default_values.merge(options) if @route.default_values
113
+ options.delete_if{ |k,v| v.nil? } if options
114
+ result, params = yield args, options
115
+ mount_point = @router.url_mount && (options ? @router.url_mount.url(options) : @router.url_mount.url)
116
+ mount_point ? [File.join(mount_point, result), params] : [result, params]
117
+ end
118
+
119
+ def matching_path(params, other_hash = nil)
120
+ return @path_generators.first if @path_generators.size == 1
121
+ case params
122
+ when Array, nil
123
+ @path_generators.find do |path|
124
+ significant_key_count = params ? params.size : 0
125
+ significant_key_count += (path.param_names & other_hash.keys).size if other_hash
126
+ significant_key_count >= path.param_names.size
127
+ end
128
+ when Hash
129
+ @path_generators.find { |path| (params && !params.empty? && (path.param_names & params.keys).size == path.param_names.size) || path.param_names.empty? }
130
+ end
131
+ end
132
+
133
+ def append_querystring_value(uri, key, value)
134
+ case value
135
+ when Array then value.each{ |v| append_querystring_value(uri, "#{key}[]", v) }
136
+ when Hash then value.each{ |k, v| append_querystring_value(uri, "#{key}[#{k}]", v) }
137
+ else uri << "&#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
138
+ end
139
+ end
140
+
141
+ def append_querystring(uri, params)
142
+ if params && !params.empty?
143
+ uri_size = uri.size
144
+ params.each{ |k,v| append_querystring_value(uri, k, v) }
145
+ uri[uri_size] = ??
146
+ end
147
+ uri
148
+ end
149
+ end
150
+ end
@@ -1,17 +1,20 @@
1
1
  class HttpRouter
2
2
  class Node
3
- autoload :Root, 'http_router/node/root'
4
- autoload :Glob, 'http_router/node/glob'
5
- autoload :GlobRegex, 'http_router/node/glob_regex'
6
- autoload :Variable, 'http_router/node/variable'
7
- autoload :Regex, 'http_router/node/regex'
8
- autoload :SpanningRegex, 'http_router/node/spanning_regex'
9
- autoload :GlobRegex, 'http_router/node/glob_regex'
10
- autoload :FreeRegex, 'http_router/node/free_regex'
11
- autoload :Arbitrary, 'http_router/node/arbitrary'
12
- autoload :Request, 'http_router/node/request'
13
- autoload :Lookup, 'http_router/node/lookup'
14
- autoload :Path, 'http_router/node/path'
3
+ autoload :Root, 'http_router/node/root'
4
+ autoload :Glob, 'http_router/node/glob'
5
+ autoload :GlobRegex, 'http_router/node/glob_regex'
6
+ autoload :Variable, 'http_router/node/variable'
7
+ autoload :Regex, 'http_router/node/regex'
8
+ autoload :SpanningRegex, 'http_router/node/spanning_regex'
9
+ autoload :GlobRegex, 'http_router/node/glob_regex'
10
+ autoload :FreeRegex, 'http_router/node/free_regex'
11
+ autoload :AbstractRequestNode, 'http_router/node/abstract_request_node'
12
+ autoload :Host, 'http_router/node/host'
13
+ autoload :UserAgent, 'http_router/node/user_agent'
14
+ autoload :RequestMethod, 'http_router/node/request_method'
15
+ autoload :Scheme, 'http_router/node/scheme'
16
+ autoload :Lookup, 'http_router/node/lookup'
17
+ autoload :Path, 'http_router/node/path'
15
18
 
16
19
  attr_reader :router
17
20
 
@@ -31,13 +34,20 @@ class HttpRouter
31
34
  add(GlobRegex.new(@router, self, matcher))
32
35
  end
33
36
 
34
- def add_request(opts)
35
- raise unless opts
36
- add(Request.new(@router, self, opts))
37
+ def add_host(hosts)
38
+ add(Host.new(@router, self, hosts))
37
39
  end
38
40
 
39
- def add_arbitrary(blk, allow_partial, param_names)
40
- add(Arbitrary.new(@router, self, allow_partial, blk, param_names))
41
+ def add_user_agent(uas)
42
+ add(UserAgent.new(@router, self, uas))
43
+ end
44
+
45
+ def add_request_method(rm)
46
+ add(RequestMethod.new(@router, self, rm))
47
+ end
48
+
49
+ def add_scheme(scheme)
50
+ add(Scheme.new(@router, self, scheme))
41
51
  end
42
52
 
43
53
  def add_match(regexp, matching_indicies = [0], splitting_indicies = nil)
@@ -0,0 +1,31 @@
1
+ class HttpRouter
2
+ class Node
3
+ class AbstractRequestNode < Node
4
+ attr_reader :request_method, :tests
5
+
6
+ def initialize(route, parent, tests, request_method)
7
+ @request_method = request_method
8
+ @tests = case tests
9
+ when Array then tests
10
+ when Set then tests.to_a
11
+ else [tests]
12
+ end
13
+ super(route, parent)
14
+ end
15
+
16
+ def usable?(other)
17
+ other.class == self.class && other.tests == tests && other.request_method == request_method
18
+ end
19
+
20
+ def to_code
21
+ "if #{@tests.map { |test| "#{test.inspect} === request.rack_request.#{request_method}" } * ' or '}
22
+ #{super}
23
+ end"
24
+ end
25
+
26
+ def inspect_label
27
+ "#{self.class.name.split("::").last} #{tests.inspect} (#{@matchers.size} matchers)"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ class HttpRouter
2
+ class Node
3
+ class Host < AbstractRequestNode
4
+ def initialize(router, parent, hosts)
5
+ super(router, parent, hosts, :host)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -25,16 +25,14 @@ class HttpRouter
25
25
  end
26
26
 
27
27
  def to_code
28
- lookup_ivar = inject_root_ivar(@map)
29
- method_prefix = "lookup_#{root.next_counter} "
30
- inject_root_methods @map.keys.map {|k|
31
- method = :"#{method_prefix}#{k}"
32
- "define_method(#{method.inspect}) do |request|
33
- part = request.path.shift
34
- #{@map[k].map{|n| n.to_code} * "\n"}
35
- request.path.unshift part
36
- end"}.join("\n")
37
- "send(\"#{method_prefix}\#{request.path.first}\", request) if !request.path_finished? && #{lookup_ivar}.key?(request.path.first)"
28
+ part_name = "part#{root.next_counter}"
29
+ "unless request.path_finished?
30
+ #{part_name} = request.path.shift
31
+ case #{part_name}
32
+ #{@map.map{|k, v| "when #{k.inspect}; #{v.map(&:to_code) * "\n"};"} * "\n"}
33
+ end
34
+ request.path.unshift #{part_name}
35
+ end"
38
36
  end
39
37
  end
40
38
  end
@@ -1,52 +1,42 @@
1
1
  class HttpRouter
2
2
  class Node
3
3
  class Path < Node
4
- attr_reader :route, :param_names, :dynamic, :original_path
4
+ attr_reader :route, :param_names, :dynamic, :path
5
5
  alias_method :dynamic?, :dynamic
6
6
  def initialize(router, parent, route, path, param_names = [])
7
- @route, @original_path, @param_names, @dynamic = route, path, param_names, !param_names.empty?
7
+ @route, @path, @param_names, @dynamic = route, path, param_names, !param_names.empty?
8
+ @route.add_path(self)
8
9
  raise AmbiguousVariableException, "You have duplicate variable name present: #{param_names.join(', ')}" if param_names.uniq.size != param_names.size
9
- Util.add_path_generation(self, route, @original_path) if @original_path.respond_to?(:split)
10
10
  super router, parent
11
- root.uncompile
11
+ router.uncompile
12
12
  end
13
13
 
14
14
  def hashify_params(params)
15
15
  @dynamic && params ? Hash[param_names.zip(params)] : {}
16
16
  end
17
17
 
18
- def url(args, options)
19
- if path = raw_url(args, options)
20
- raise TooManyParametersException unless args.empty?
21
- [URI.escape(path), options]
22
- end
23
- end
24
-
25
18
  def to_code
26
19
  path_ivar = inject_root_ivar(self)
27
- "#{"if request.path_finished?" unless route.match_partially?}
28
- catch(:pass) do
29
- #{"if request.path.size == 1 && request.path.first == '' && (request.rack_request.head? || request.rack_request.get?) && request.rack_request.path_info[-1] == ?/
30
- response = ::Rack::Response.new
31
- response.redirect(request.rack_request.path_info[0, request.rack_request.path_info.size - 1], 302)
32
- throw :success, response.finish
33
- end" if router.redirect_trailing_slash?}
20
+ "#{"if !callback && request.path.size == 1 && request.path.first == '' && (request.rack_request.head? || request.rack_request.get?) && request.rack_request.path_info[-1] == ?/
21
+ response = ::Rack::Response.new
22
+ response.redirect(request.rack_request.path_info[0, request.rack_request.path_info.size - 1], 302)
23
+ return response.finish
24
+ end" if router.redirect_trailing_slash?}
34
25
 
35
- #{"if request.path.empty?#{" or (request.path.size == 1 and request.path.first == '')" if router.ignore_trailing_slash?}" unless route.match_partially?}
36
- if request.perform_call
37
- env = request.rack_request.dup.env
38
- env['router.request'] = request
39
- env['router.params'] ||= {}
40
- #{"env['router.params'].merge!(Hash[#{param_names.inspect}.zip(request.params)])" if dynamic?}
41
- @router.rewrite#{"_partial" if route.match_partially?}_path_info(env, request)
42
- response = @router.process_destination_path(#{path_ivar}, env)
43
- router.pass_on_response(response) ? throw(:pass) : throw(:success, response)
44
- else
45
- request.matched_route(Response.new(request, #{path_ivar}))
46
- end
47
- #{"end" unless route.match_partially?}
26
+ #{"if request.#{router.ignore_trailing_slash? ? 'path_finished?' : 'path.empty?'}" unless route.match_partially}
27
+ if callback
28
+ request.called = true
29
+ callback.call(Response.new(request, #{path_ivar}))
30
+ else
31
+ env = request.rack_request.dup.env
32
+ env['router.request'] = request
33
+ env['router.params'] ||= {}
34
+ #{"env['router.params'].merge!(Hash[#{param_names.inspect}.zip(request.params)])" if dynamic?}
35
+ @router.rewrite#{"_partial" if route.match_partially}_path_info(env, request)
36
+ response = @router.process_destination_path(#{path_ivar}, env)
37
+ return response unless router.pass_on_response(response)
48
38
  end
49
- #{"end" unless route.match_partially?}"
39
+ #{"end" unless route.match_partially}"
50
40
  end
51
41
 
52
42
  def usable?(other)
@@ -54,12 +44,7 @@ class HttpRouter
54
44
  end
55
45
 
56
46
  def inspect_label
57
- "Path: #{original_path.inspect} for route #{route.named || 'unnamed route'} to #{route.dest.inspect}"
58
- end
59
-
60
- private
61
- def raw_url(args, options)
62
- raise InvalidRouteException
47
+ "Path: #{path.inspect} for route #{route.name || 'unnamed route'} to #{route.dest.inspect}"
63
48
  end
64
49
  end
65
50
  end
@@ -0,0 +1,16 @@
1
+ class HttpRouter
2
+ class Node
3
+ class RequestMethod < AbstractRequestNode
4
+ def initialize(router, parent, request_methods)
5
+ super(router, parent, request_methods, :request_method)
6
+ end
7
+
8
+ def to_code
9
+ "if #{@tests.map { |test| "#{test.inspect} === request.rack_request.#{request_method}" } * ' or '}
10
+ #{super}
11
+ end
12
+ request.acceptable_methods.merge(#{@tests.inspect})"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -8,14 +8,8 @@ class HttpRouter
8
8
  @counter, @methods_module = 0, Module.new
9
9
  end
10
10
 
11
- def [](request)
12
- compile
13
- self[request]
14
- end
15
- alias_method :compiling_lookup, :[]
16
-
17
11
  def uncompile
18
- instance_eval "undef :[]; alias :[] :compiling_lookup", __FILE__, __LINE__ if compiled?
12
+ instance_eval "undef :call; def call(req); raise 'uncompiled root'; end", __FILE__, __LINE__ if compiled?
19
13
  end
20
14
 
21
15
  def next_counter
@@ -36,12 +30,112 @@ class HttpRouter
36
30
  "Root (#{@matchers.size} matchers)"
37
31
  end
38
32
 
39
- private
40
- def compile
33
+ def compile(routes)
34
+ routes.each {|route| add_route(route)}
41
35
  root.extend(root.methods_module)
42
- instance_eval "def [](request)\n#{to_code}\nnil\nend", __FILE__, __LINE__
36
+ instance_eval "def call(request, &callback)\n#{to_code}\nnil\nend"
43
37
  @compiled = true
44
38
  end
39
+
40
+ private
41
+ def add_route(route)
42
+ paths = if route.path_for_generation.nil?
43
+ route.match_partially = true
44
+ []
45
+ elsif route.path_for_generation.is_a?(Regexp)
46
+ [route.path_for_generation]
47
+ else
48
+ path_for_generation = route.path_for_generation.dup
49
+ start_index, end_index = 0, 1
50
+ raw_paths, chars = [""], path_for_generation.split('')
51
+ until chars.empty?
52
+ case chars.first[0]
53
+ when ?(
54
+ chars.shift
55
+ (start_index...end_index).each { |path_index| raw_paths << raw_paths[path_index].dup }
56
+ start_index = end_index
57
+ end_index = raw_paths.size
58
+ when ?)
59
+ chars.shift
60
+ start_index -= end_index - start_index
61
+ else
62
+ c = if chars[0][0] == ?\\ && (chars[1][0] == ?( || chars[1][0] == ?)); chars.shift; chars.shift; else; chars.shift; end
63
+ (start_index...end_index).each { |path_index| raw_paths[path_index] << c }
64
+ end
65
+ end
66
+ raw_paths
67
+ end
68
+ paths.reverse!
69
+ if paths.empty?
70
+ add_non_path_to_tree(route, @router.root, nil, [])
71
+ else
72
+ Generator.new(route, paths).each_path do |path_generator|
73
+ case path_generator.path
74
+ when Regexp
75
+ path_generator.param_names = path_generator.path.names.map(&:to_sym) if path_generator.path.respond_to?(:names)
76
+ add_non_path_to_tree(route, add_free_match(path_generator.path), path_generator.path, path_generator.param_names)
77
+ else
78
+ node = self
79
+ path_generator.path.split(/\//).each do |part|
80
+ next if part == ''
81
+ parts = part.scan(/\\.|[:*][a-z0-9_]+|[^:*\\]+/)
82
+ node = parts.size == 1 ? add_normal_part(route, node, part, path_generator) : add_complex_part(route, node, parts, path_generator)
83
+ end
84
+ add_non_path_to_tree(route, node, path_generator.path, path_generator.param_names)
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ def add_normal_part(route, node, part, path_generator)
91
+ name = part[1, part.size]
92
+ node = case part[0]
93
+ when ?\\
94
+ node.add_lookup(part[1].chr)
95
+ when ?:
96
+ path_generator.param_names << name.to_sym
97
+ route.matches_with(name) ? node.add_spanning_match(route.matches_with(name)) : node.add_variable
98
+ when ?*
99
+ path_generator.param_names << name.to_sym
100
+ route.matches_with(name) ? node.add_glob_regexp(route.matches_with(name)) : node.add_glob
101
+ else
102
+ node.add_lookup(part)
103
+ end
104
+ end
105
+
106
+ def add_complex_part(route, node, parts, path_generator)
107
+ capturing_indicies, splitting_indicies, captures, spans = [], [], 0, false
108
+ regex = parts.inject('') do |reg, part|
109
+ reg << case part[0]
110
+ when ?\\ then Regexp.quote(part[1].chr)
111
+ when ?:, ?*
112
+ spans = true if part[0] == ?*
113
+ captures += 1
114
+ (part[0] == ?* ? splitting_indicies : capturing_indicies) << captures
115
+ name = part[1, part.size].to_sym
116
+ path_generator.param_names << name.to_sym
117
+ if spans
118
+ route.matches_with(name) ? "((?:#{route.matches_with(name)}\\/?)+)" : '(.*?)'
119
+ else
120
+ "(#{(route.matches_with(name) || '[^/]*?')})"
121
+ end
122
+ else
123
+ Regexp.quote(part)
124
+ end
125
+ end
126
+ spans ? node.add_spanning_match(Regexp.new("#{regex}$"), capturing_indicies, splitting_indicies) :
127
+ node.add_match(Regexp.new("#{regex}$"), capturing_indicies, splitting_indicies)
128
+ end
129
+
130
+ def add_non_path_to_tree(route, node, path, param_names)
131
+ node = node.add_host([route.host, route.other_hosts].flatten.compact) if route.host or route.other_hosts
132
+ node = node.add_user_agent(route.user_agent) if route.user_agent
133
+ node = node.add_scheme(route.scheme) if route.scheme
134
+ node = node.add_request_method(route.request_methods) if route.request_methods
135
+ path_obj = node.add_destination(route, path, param_names)
136
+ path_obj
137
+ end
138
+
45
139
  end
46
140
  end
47
141
  end