rack-http_router 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5575554ab4629e83f2524f4fe47ff58963909b658452a3646457da97c378d744
4
+ data.tar.gz: a409c6bdd4df80abba1206e79a4adefd05d75142cbdd775e1418de33bee23ea7
5
+ SHA512:
6
+ metadata.gz: 13eac202ecb0c2c1bfc1287298cfa45ae202949e6738c8dec1689e8a674a0d8b5b60438a8d990e1dd77159eb75cfea3a90ddd60ab30376aa6f35e21bbc4f1c51
7
+ data.tar.gz: 8a43962f554a10aef1696ff990cb5096b1a211f3e8f28199b52b7ad36a6c079ad3bf070f51ad0710aec284fdb0156ff50ae7dfcfb0b6e2951cc7f0c17046de17
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erubi'
4
+ require 'json'
5
+ require 'rack'
6
+
7
+ module Rack
8
+ class HttpRouter
9
+ module Action
10
+ def self.included(base)
11
+ base.class_eval do
12
+ attr_reader :route if self != Rack::HttpRouter
13
+
14
+ def initialize(route)
15
+ @route = route
16
+ end
17
+
18
+ def view_response(a_path, a_view_params = {}, status: 200)
19
+ Rack::HttpRouter::Action.view_response(
20
+ a_path,
21
+ a_view_params,
22
+ status: status, route: route
23
+ )
24
+ end
25
+
26
+ def view(
27
+ a_path, a_view_params = {}, status: 200, response_instance: false
28
+ )
29
+ Rack::HttpRouter::Action.view(
30
+ a_path,
31
+ a_view_params,
32
+ status: status, response_instance: response_instance, route: route
33
+ )
34
+ end
35
+ end
36
+ end
37
+
38
+ def html(content, status: 200)
39
+ Rack::HttpRouter::Action.html(content, status: status)
40
+ end
41
+
42
+ def html_response(content, status: 200)
43
+ Rack::HttpRouter::Action.html_response(content, status: status)
44
+ end
45
+
46
+ def json(content = {}, status: 200)
47
+ Rack::HttpRouter::Action.json(content, status: status)
48
+ end
49
+
50
+ def json_response(content = {}, status: 200)
51
+ Rack::HttpRouter::Action.json_response(content, status: status)
52
+ end
53
+
54
+ def text(content, status: 200)
55
+ Rack::HttpRouter::Action.text(content, status: status)
56
+ end
57
+
58
+ def text_response(content, status: 200)
59
+ Rack::HttpRouter::Action.text_response(content, status: status)
60
+ end
61
+
62
+ def erb(path, view_params = {})
63
+ Rack::HttpRouter::Action.erb(path, view_params)
64
+ end
65
+
66
+ def redirect_to(url)
67
+ Rack::HttpRouter::Action.redirect_to(url)
68
+ end
69
+
70
+ def response(body = nil, status = 200, headers = {})
71
+ Rack::Response.new(body, status, headers)
72
+ end
73
+
74
+ class << self
75
+ def html(content, status: 200)
76
+ [status, { 'Content-Type' => 'text/html' }, [content]]
77
+ end
78
+
79
+ def html_response(content, status: 200)
80
+ Rack::Response.new(content, status, { 'Content-Type' => 'text/html' })
81
+ end
82
+
83
+ def view_response(paths, view_params = {}, status: 200, route: nil)
84
+ view(
85
+ paths,
86
+ view_params,
87
+ status: status, response_instance: true,
88
+ route: route
89
+ )
90
+ end
91
+
92
+ def view(
93
+ paths,
94
+ view_params = {},
95
+ status: 200,
96
+ response_instance: false,
97
+ route: nil
98
+ )
99
+ erb = if paths.is_a?(Array)
100
+ paths.map { |path| erb("views/#{path}", route, view_params) }.join
101
+ else
102
+ erb("views/#{paths}", route, view_params)
103
+ end
104
+
105
+ if response_instance
106
+ return Rack::Response.new(
107
+ erb,
108
+ status,
109
+ { 'Content-Type' => 'text/html' }
110
+ )
111
+ end
112
+
113
+ [status, { 'Content-Type' => 'text/html' }, [erb]]
114
+ end
115
+
116
+ def json(content = {}, status: 200)
117
+ [status, { 'Content-Type' => 'application/json' }, [content.to_json]]
118
+ end
119
+
120
+ def json_response(content = {}, status: 200)
121
+ Rack::Response.new(
122
+ content.to_json,
123
+ status,
124
+ { 'Content-Type' => 'application/json' }
125
+ )
126
+ end
127
+
128
+ def text(content, status: 200)
129
+ [status, { 'Content-Type' => 'text/plain' }, [content]]
130
+ end
131
+
132
+ def text_response(content, status: 200)
133
+ Rack::Response.new(
134
+ content,
135
+ status,
136
+ { 'Content-Type' => 'text/plain' }
137
+ )
138
+ end
139
+
140
+ def erb(path, _route, view_params = {})
141
+ @view = OpenStruct.new(view_params)
142
+
143
+ eval(Erubi::Engine.new(::File.read("#{path}.html.erb")).src)
144
+ end
145
+
146
+ def redirect_to(url)
147
+ [302, { 'Location' => url }, []]
148
+ end
149
+
150
+ def response(body = nil, status = 200, headers = {})
151
+ Rack::Response.new(body, status, headers)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class HttpRouter
5
+ class Router
6
+ class BuildRequest
7
+ def initialize(env)
8
+ @env = env
9
+ end
10
+
11
+ def call(route = nil)
12
+ request = Rack::Request.new(@env)
13
+
14
+ return request if route.nil?
15
+ return request unless route.has_params
16
+
17
+ update_request_params(request, route)
18
+ end
19
+
20
+ private
21
+
22
+ def update_request_params(request, route)
23
+ splitted_request_path = request.path.split('/')
24
+
25
+ route
26
+ .splitted_path
27
+ .each
28
+ .with_index do |route_word, route_word_position|
29
+ if route_word.start_with?(':')
30
+ param = splitted_request_path[route_word_position]
31
+ param = param.to_i if is_a_integer_string?(param)
32
+
33
+ update_request_param(request, route_word, param)
34
+ end
35
+ end
36
+
37
+ request
38
+ end
39
+
40
+ def is_a_integer_string?(string)
41
+ string =~ /\A[-+]?[0-9]*\.?[0-9]+\Z/
42
+ end
43
+
44
+ def update_request_param(request, word, param)
45
+ request.update_param(word.sub(':', '').to_sym, param)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class HttpRouter
5
+ class Router
6
+ class Route
7
+ attr_reader :endpoint, :splitted_path, :has_params
8
+
9
+ def initialize(path, endpoint)
10
+ @path = path
11
+ @splitted_path = @path.split('/')
12
+ @endpoint = endpoint
13
+ @params = fetch_params
14
+ @has_params = @params != []
15
+ end
16
+
17
+ def match?(env)
18
+ return match_with_params?(env) if @has_params
19
+
20
+ env['REQUEST_PATH'] == @path
21
+ end
22
+
23
+ private
24
+
25
+ def fetch_params
26
+ @splitted_path.select { |value| value.start_with? ':' }
27
+ end
28
+
29
+ def match_with_params?(env)
30
+ splitted_request_path = env['REQUEST_PATH'].split('/')
31
+
32
+ return false if @splitted_path.size != splitted_request_path.size
33
+
34
+ matched_path_pieces =
35
+ @splitted_path
36
+ .map
37
+ .with_index do |segment, i|
38
+ if segment.start_with?(':')
39
+ true
40
+ else
41
+ splitted_request_path[i] == segment
42
+ end
43
+ end
44
+
45
+ !matched_path_pieces.include?(false)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'router/route'
4
+ require_relative 'router/build_request'
5
+
6
+ module Rack
7
+ class HttpRouter
8
+ class Router
9
+ class UndefinedNamedRoute < StandardError; end
10
+
11
+ attr_writer :not_found
12
+ attr_reader :route
13
+
14
+ def initialize
15
+ @routes = {}
16
+ %w[GET POST DELETE PUT TRACE OPTIONS PATCH].each do |method|
17
+ @routes[method] = { __instances: [] }
18
+ end
19
+ @route = Hash.new do |_hash, key|
20
+ raise(UndefinedNamedRoute, "Undefined named route: '#{key}'")
21
+ end
22
+ @scopes = []
23
+ @error = proc { |_req, e| raise e }
24
+ @not_found = proc { [404, {}, ['Not found']] }
25
+ end
26
+
27
+ def call(env)
28
+ request_builder = BuildRequest.new(env)
29
+ env['REQUEST_METHOD'] = 'GET' if env['REQUEST_METHOD'] == 'HEAD'
30
+
31
+ route_instance = match_route(env)
32
+
33
+ return render_not_found(request_builder.call) if route_instance.nil?
34
+
35
+ if route_instance.endpoint.respond_to?(:call)
36
+ return route_instance.endpoint.call(request_builder.call(route_instance))
37
+ end
38
+
39
+ if route_instance.endpoint.include?(Rack::HttpRouter::Action)
40
+ return route_instance.endpoint.new(@route).call(request_builder.call(route_instance))
41
+ end
42
+
43
+ route_instance.endpoint.new.call(request_builder.call(route_instance))
44
+ rescue Exception => e
45
+ @error.call(request_builder.call, e)
46
+ end
47
+
48
+ def add(method, path, endpoint, as = nil)
49
+ method = :get if method == :head
50
+
51
+ path_with_scopes = "/#{@scopes.join('/')}#{put_path_slash(path)}"
52
+ @route[as] = path_with_scopes if as
53
+
54
+ route_instance = Route.new(path_with_scopes, endpoint)
55
+
56
+ return push_to_scope(method.to_s.upcase, route_instance) if @scopes.size >= 1
57
+
58
+ @routes[method.to_s.upcase][:__instances].push(route_instance)
59
+ end
60
+
61
+ def add_not_found(endpoint)
62
+ @not_found = endpoint
63
+ end
64
+
65
+ def add_error(endpoint)
66
+ @error = endpoint
67
+ end
68
+
69
+ def append_scope(name)
70
+ @scopes.push(name)
71
+ end
72
+
73
+ def clear_last_scope
74
+ @scopes = @scopes.first(@scopes.size - 1)
75
+ end
76
+
77
+ private
78
+
79
+ def push_to_scope(method, route_instance)
80
+ scopes_with_slash = @scopes + [:__instances]
81
+ push_it(@routes[method], *scopes_with_slash, route_instance)
82
+ end
83
+
84
+ def push_it(h, first_key, *rest_keys, val)
85
+ if rest_keys.empty?
86
+ (h[first_key] ||= []) << val
87
+ else
88
+ h[first_key] = push_it(h[first_key] ||= {}, *rest_keys, val)
89
+ end
90
+ h
91
+ end
92
+
93
+ def put_path_slash(path)
94
+ return '' if ['/', ''].include?(path) && @scopes != []
95
+ return "/#{path}" if @scopes != []
96
+
97
+ path
98
+ end
99
+
100
+ def render_not_found(env)
101
+ return @not_found.call(env) if @not_found.respond_to?(:call)
102
+
103
+ @not_found.new.call(env)
104
+ end
105
+
106
+ def match_route(env, last_tail = nil, found_scopes = [])
107
+ routes =
108
+ if last_tail.nil?
109
+ last_tail = env['REQUEST_PATH'].split('/').drop(1)
110
+
111
+ @routes[env['REQUEST_METHOD']]
112
+ else
113
+ @routes[env['REQUEST_METHOD']].dig(*found_scopes)
114
+ end
115
+
116
+ segment, *tail = last_tail
117
+
118
+ routes.each do |scope, _v|
119
+ next if scope == :__instances
120
+
121
+ if segment == scope || scope.start_with?(':')
122
+ found_scopes.push(scope)
123
+ break
124
+ end
125
+ end
126
+
127
+ if tail.empty? || found_scopes == []
128
+ return @routes[env['REQUEST_METHOD']].dig(*(found_scopes << :__instances)).detect do |route_instance|
129
+ route_instance.match?(env)
130
+ end
131
+ end
132
+
133
+ match_route(env, tail, found_scopes)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rack-http_router/router'
4
+ require_relative 'rack-http_router/action'
5
+
6
+ module Rack
7
+ class HttpRouter
8
+ include Action
9
+
10
+ def initialize(router: Router.new)
11
+ @router = router
12
+ end
13
+
14
+ def call(&block)
15
+ instance_eval(&block)
16
+
17
+ @router
18
+ end
19
+
20
+ def route
21
+ @router.route
22
+ end
23
+
24
+ def scope(name, &block)
25
+ @router.append_scope(name)
26
+ instance_eval(&block)
27
+
28
+ @router.clear_last_scope
29
+ end
30
+
31
+ def not_found(endpoint = -> {}, &block)
32
+ if block_given?
33
+ @router.add_not_found(block)
34
+ else
35
+ @router.add_not_found(endpoint)
36
+ end
37
+ end
38
+
39
+ def error(endpoint = -> {}, &block)
40
+ if block_given?
41
+ @router.add_error(block)
42
+ else
43
+ @router.add_error(endpoint)
44
+ end
45
+ end
46
+
47
+ %w[GET POST DELETE PUT TRACE OPTIONS PATCH].each do |http_method|
48
+ define_method(http_method.downcase.to_sym) do |path = '', endpoint = -> {}, as: nil, &block|
49
+ if block.respond_to?(:call)
50
+ @router.add(http_method, path, block, as)
51
+ else
52
+ @router.add(http_method, path, endpoint, as)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-http_router
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Henrique F. Teixeira
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-07-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: erubi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '4.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '2.0'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '4.0'
47
+ description: A complete http router solution that fit well with pure rack apps
48
+ email: hriqueft@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - lib/rack-http_router.rb
54
+ - lib/rack-http_router/action.rb
55
+ - lib/rack-http_router/router.rb
56
+ - lib/rack-http_router/router/build_request.rb
57
+ - lib/rack-http_router/router/route.rb
58
+ homepage: https://github.com/henriquefernandez/rack-http_router
59
+ licenses:
60
+ - MIT
61
+ metadata: {}
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.4.3
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: '"rack-http_router" come with a router and helper functions to build pure
81
+ Rack projects.'
82
+ test_files: []