lennarb 1.1.0 → 1.3.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.
@@ -4,137 +4,137 @@
4
4
  # Copyright, 2023-2024, by Aristóteles Coutinho.
5
5
 
6
6
  class Lennarb
7
- class Response
8
- # @!attribute [rw] status
9
- # @returns [Integer]
10
- #
11
- attr_accessor :status
12
-
13
- # @!attribute [r] body
14
- # @returns [Array]
15
- #
16
- attr_reader :body
17
-
18
- # @!attribute [r] headers
19
- # @returns [Hash]
20
- #
21
- attr_reader :headers
22
-
23
- # @!attribute [r] length
24
- # @returns [Integer]
25
- #
26
- attr_reader :length
27
-
28
- # Constants
29
- #
30
- LOCATION = 'location'
31
- private_constant :LOCATION
32
-
33
- CONTENT_TYPE = 'content-type'
34
- private_constant :CONTENT_TYPE
35
-
36
- CONTENT_LENGTH = 'content-length'
37
- private_constant :CONTENT_LENGTH
38
-
39
- ContentType = { HTML: 'text/html', TEXT: 'text/plain', JSON: 'application/json' }.freeze
40
- private_constant :ContentType
41
-
42
- # Initialize the response object
43
- #
44
- # @returns [Response]
45
- #
46
- def initialize
47
- @status = 404
48
- @headers = {}
49
- @body = []
50
- @length = 0
51
- end
52
-
53
- # Set the response header
54
- #
55
- # @parameter [String] key
56
- #
57
- # @returns [String] value
58
- #
59
- def [](key)
60
- @headers[key]
61
- end
62
-
63
- # Get the response header
64
- #
65
- # @parameter [String] key
66
- # @parameter [String] value
67
- #
68
- # @returns [String] value
69
- #
70
- def []=(key, value)
71
- @headers[key] = value
72
- end
73
-
74
- # Write to the response body
75
- #
76
- # @parameter [String] str
77
- #
78
- # @returns [String] str
79
- #
80
- def write(str)
81
- str = str.to_s
82
- @length += str.bytesize
83
- @headers[CONTENT_LENGTH] = @length.to_s
84
- @body << str
85
- end
86
-
87
- # Set the response type to text
88
- #
89
- # @parameter [String] str
90
- #
91
- # @returns [String] str
92
- #
93
- def text(str)
94
- @headers[CONTENT_TYPE] = ContentType[:TEXT]
95
- write(str)
96
- end
97
-
98
- # Set the response type to html
99
- #
100
- # @parameter [String] str
101
- #
102
- # @returns [String] str
103
- #
104
- def html(str)
105
- @headers[CONTENT_TYPE] = ContentType[:HTML]
106
- write(str)
107
- end
108
-
109
- # Set the response type to json
110
- #
111
- # @parameter [String] str
112
- #
113
- # @returns [String] str
114
- #
115
- def json(str)
116
- @headers[CONTENT_TYPE] = ContentType[:JSON]
117
- write(str)
118
- end
119
-
120
- # Redirect the response
121
- #
122
- # @parameter [String] path
123
- # @parameter [Integer] status, default: 302
124
- #
125
- def redirect(path, status = 302)
126
- @headers[LOCATION] = path
127
- @status = status
128
-
129
- throw :halt, finish
130
- end
131
-
132
- # Finish the response
133
- #
134
- # @returns [Array] response
135
- #
136
- def finish
137
- [@status, @headers, @body]
138
- end
139
- end
7
+ class Response
8
+ # @!attribute [rw] status
9
+ # @returns [Integer]
10
+ #
11
+ attr_accessor :status
12
+
13
+ # @!attribute [r] body
14
+ # @returns [Array]
15
+ #
16
+ attr_reader :body
17
+
18
+ # @!attribute [r] headers
19
+ # @returns [Hash]
20
+ #
21
+ attr_reader :headers
22
+
23
+ # @!attribute [r] length
24
+ # @returns [Integer]
25
+ #
26
+ attr_reader :length
27
+
28
+ # Constants
29
+ #
30
+ LOCATION = 'location'
31
+ private_constant :LOCATION
32
+
33
+ CONTENT_TYPE = 'content-type'
34
+ private_constant :CONTENT_TYPE
35
+
36
+ CONTENT_LENGTH = 'content-length'
37
+ private_constant :CONTENT_LENGTH
38
+
39
+ ContentType = { HTML: 'text/html', TEXT: 'text/plain', JSON: 'application/json' }.freeze
40
+ private_constant :ContentType
41
+
42
+ # Initialize the response object
43
+ #
44
+ # @returns [Response]
45
+ #
46
+ def initialize
47
+ @status = 404
48
+ @headers = {}
49
+ @body = []
50
+ @length = 0
51
+ end
52
+
53
+ # Set the response header
54
+ #
55
+ # @parameter [String] key
56
+ #
57
+ # @returns [String] value
58
+ #
59
+ def [](key)
60
+ @headers[key]
61
+ end
62
+
63
+ # Get the response header
64
+ #
65
+ # @parameter [String] key
66
+ # @parameter [String] value
67
+ #
68
+ # @returns [String] value
69
+ #
70
+ def []=(key, value)
71
+ @headers[key] = value
72
+ end
73
+
74
+ # Write to the response body
75
+ #
76
+ # @parameter [String] str
77
+ #
78
+ # @returns [String] str
79
+ #
80
+ def write(str)
81
+ str = str.to_s
82
+ @length += str.bytesize
83
+ @headers[CONTENT_LENGTH] = @length.to_s
84
+ @body << str
85
+ end
86
+
87
+ # Set the response type to text
88
+ #
89
+ # @parameter [String] str
90
+ #
91
+ # @returns [String] str
92
+ #
93
+ def text(str)
94
+ @headers[CONTENT_TYPE] = ContentType[:TEXT]
95
+ write(str)
96
+ end
97
+
98
+ # Set the response type to html
99
+ #
100
+ # @parameter [String] str
101
+ #
102
+ # @returns [String] str
103
+ #
104
+ def html(str)
105
+ @headers[CONTENT_TYPE] = ContentType[:HTML]
106
+ write(str)
107
+ end
108
+
109
+ # Set the response type to json
110
+ #
111
+ # @parameter [String] str
112
+ #
113
+ # @returns [String] str
114
+ #
115
+ def json(str)
116
+ @headers[CONTENT_TYPE] = ContentType[:JSON]
117
+ write(str)
118
+ end
119
+
120
+ # Redirect the response
121
+ #
122
+ # @parameter [String] path
123
+ # @parameter [Integer] status, default: 302
124
+ #
125
+ def redirect(path, status = 302)
126
+ @headers[LOCATION] = path
127
+ @status = status
128
+
129
+ throw :halt, finish
130
+ end
131
+
132
+ # Finish the response
133
+ #
134
+ # @returns [Array] response
135
+ #
136
+ def finish
137
+ [@status, @headers, @body]
138
+ end
139
+ end
140
140
  end
@@ -4,69 +4,63 @@
4
4
  # Copyright, 2023-2024, by Aristóteles Coutinho.
5
5
 
6
6
  class Lennarb
7
- class RouteNode
8
- attr_accessor :static_children, :dynamic_children, :blocks, :param_key
7
+ class RouteNode
8
+ attr_accessor :static_children, :dynamic_children, :blocks, :param_key
9
9
 
10
- # Initializes the RouteNode class.
11
- #
12
- # @return [RouteNode]
13
- #
14
- def initialize
15
- @blocks = {}
16
- @param_key = nil
17
- @static_children = {}
18
- @dynamic_children = {}
19
- end
10
+ def initialize
11
+ @blocks = {}
12
+ @param_key = nil
13
+ @static_children = {}
14
+ @dynamic_children = {}
15
+ end
20
16
 
21
- # Add a route to the route node
22
- #
23
- # @parameter parts [Array<String>] The parts of the route
24
- # @parameter http_method [Symbol] The HTTP method of the route
25
- # @parameter block [Proc] The block to be executed when the route is matched
26
- #
27
- # @return [void]
28
- #
29
- def add_route(parts, http_method, block)
30
- current_node = self
17
+ def add_route(parts, http_method, block)
18
+ current_node = self
31
19
 
32
- parts.each do |part|
33
- if part.start_with?(':')
34
- param_sym = part[1..].to_sym
35
- current_node.dynamic_children[param_sym] ||= RouteNode.new
36
- dynamic_node = current_node.dynamic_children[param_sym]
37
- dynamic_node.param_key = param_sym
38
- current_node = dynamic_node
39
- else
40
- current_node.static_children[part] ||= RouteNode.new
41
- current_node = current_node.static_children[part]
42
- end
43
- end
20
+ parts.each do |part|
21
+ if part.start_with?(':')
22
+ param_sym = part[1..].to_sym
23
+ current_node.dynamic_children[param_sym] ||= RouteNode.new
24
+ dynamic_node = current_node.dynamic_children[param_sym]
25
+ dynamic_node.param_key = param_sym
26
+ current_node = dynamic_node
27
+ else
28
+ current_node.static_children[part] ||= RouteNode.new
29
+ current_node = current_node.static_children[part]
30
+ end
31
+ end
44
32
 
45
- current_node.blocks[http_method] = block
46
- end
33
+ current_node.blocks[http_method] = block
34
+ end
47
35
 
48
- def match_route(parts, http_method, params: {})
49
- if parts.empty?
50
- return [blocks[http_method], params] if blocks[http_method]
51
- else
52
- part = parts.first
53
- rest = parts[1..]
36
+ def match_route(parts, http_method, params: {})
37
+ if parts.empty?
38
+ return [blocks[http_method], params] if blocks[http_method]
39
+ else
40
+ part = parts.first
41
+ rest = parts[1..]
54
42
 
55
- if static_children.key?(part)
56
- result_block, result_params = static_children[part].match_route(rest, http_method, params:)
57
- return [result_block, result_params] if result_block
58
- end
43
+ if static_children.key?(part)
44
+ result_block, result_params = static_children[part].match_route(rest, http_method, params:)
45
+ return [result_block, result_params] if result_block
46
+ end
59
47
 
60
- dynamic_children.each_value do |dyn_node|
61
- new_params = params.dup
62
- new_params[dyn_node.param_key] = part
63
- result_block, result_params = dyn_node.match_route(rest, http_method, params: new_params)
48
+ dynamic_children.each_value do |dyn_node|
49
+ new_params = params.dup
50
+ new_params[dyn_node.param_key] = part
51
+ result_block, result_params = dyn_node.match_route(rest, http_method, params: new_params)
64
52
 
65
- return [result_block, result_params] if result_block
66
- end
67
- end
53
+ return [result_block, result_params] if result_block
54
+ end
55
+ end
68
56
 
69
- [nil, nil]
70
- end
71
- end
57
+ [nil, nil]
58
+ end
59
+
60
+ def merge!(other)
61
+ static_children.merge!(other.static_children)
62
+ dynamic_children.merge!(other.dynamic_children)
63
+ blocks.merge!(other.blocks)
64
+ end
65
+ end
72
66
  end
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2023-2024, by Aristóteles Coutinho.
5
5
 
6
6
  class Lennarb
7
- VERSION = '1.1.0'
7
+ VERSION = '1.3.0'
8
8
 
9
- public_constant :VERSION
9
+ public_constant :VERSION
10
10
  end
data/lib/lennarb.rb CHANGED
@@ -8,9 +8,6 @@
8
8
  require 'pathname'
9
9
  require 'rack'
10
10
 
11
- # Base class for Lennarb
12
- #
13
- require_relative 'lennarb/application/base'
14
11
  require_relative 'lennarb/plugin'
15
12
  require_relative 'lennarb/request'
16
13
  require_relative 'lennarb/response'
@@ -18,115 +15,144 @@ require_relative 'lennarb/route_node'
18
15
  require_relative 'lennarb/version'
19
16
 
20
17
  class Lennarb
21
- # Error class
22
- #
23
- class LennarbError < StandardError; end
24
-
25
- # @attribute [r] root
26
- # @returns [RouteNode]
27
- #
28
- attr_reader :_root
29
-
30
- # @attribute [r] applied_plugins
31
- # @returns [Array]
32
- #
33
- attr_reader :_applied_plugins
34
-
35
- # Initialize the application
36
- #
37
- # @yield { ... } The application
38
- #
39
- # @returns [Lennarb]
40
- #
41
- def initialize
42
- @_root = RouteNode.new
43
- @_applied_plugins = []
44
- yield self if block_given?
45
- end
46
-
47
- # Split a path into parts
48
- #
49
- # @parameter [String] path
50
- #
51
- # @returns [Array] parts. Ex. ['users', ':id']
52
- #
53
- SplitPath = ->(path) { path.split('/').reject(&:empty?) }
54
- private_constant :SplitPath
55
-
56
- # Call the application
57
- #
58
- # @parameter [Hash] env
59
- #
60
- # @returns [Array] response
61
- #
62
- def call(env)
63
- http_method = env[Rack::REQUEST_METHOD].to_sym
64
- parts = SplitPath[env[Rack::PATH_INFO]]
65
-
66
- block, params = @_root.match_route(parts, http_method)
67
- return [404, { 'content-type' => 'text/plain' }, ['Not Found']] unless block
68
-
69
- @res = Response.new
70
- req = Request.new(env, params)
71
-
72
- catch(:halt) do
73
- instance_exec(req, @res, &block)
74
- @res.finish
75
- end
76
- end
77
-
78
- # Freeze the routes
79
- #
80
- # @returns [void]
81
- #
82
- def freeze! = @_root.freeze
83
-
84
- # Add a routes
85
- #
86
- # @parameter [String] path
87
- # @parameter [Proc] block
88
- #
89
- # @returns [void]
90
- #
91
- def get(path, &block) = add_route(path, :GET, block)
92
- def put(path, &block) = add_route(path, :PUT, block)
93
- def post(path, &block) = add_route(path, :POST, block)
94
- def head(path, &block) = add_route(path, :HEAD, block)
95
- def patch(path, &block) = add_route(path, :PATCH, block)
96
- def delete(path, &block) = add_route(path, :DELETE, block)
97
- def options(path, &block) = add_route(path, :OPTIONS, block)
98
-
99
- # Add plugin to extend the router
100
- #
101
- # @parameter [String] plugin_name
102
- # @parameter [args] *args
103
- # @parameter [Block] block
104
- #
105
- # @returns [void]
106
- #
107
- def plugin(plugin_name, *, &)
108
- return if @_applied_plugins.include?(plugin_name)
109
-
110
- plugin_module = Plugin.load(plugin_name)
111
- extend plugin_module::InstanceMethods if defined?(plugin_module::InstanceMethods)
112
- self.class.extend plugin_module::ClassMethods if defined?(plugin_module::ClassMethods)
113
- plugin_module.setup(self.class, *, &) if plugin_module.respond_to?(:setup)
114
-
115
- @_applied_plugins << plugin_name
116
- end
117
-
118
- private
119
-
120
- # Add a route
121
- #
122
- # @parameter [String] path
123
- # @parameter [String] http_method
124
- # @parameter [Proc] block
125
- #
126
- # @returns [void]
127
- #
128
- def add_route(path, http_method, block)
129
- parts = SplitPath[path]
130
- @_root.add_route(parts, http_method, block)
131
- end
18
+ class LennarbError < StandardError; end
19
+
20
+ attr_reader :_root, :_plugins, :_loaded_plugins, :_middlewares, :_app
21
+
22
+ def self.use(middleware, *args, &block)
23
+ @_middlewares ||= []
24
+ @_middlewares << [middleware, args, block]
25
+ end
26
+
27
+ def self.get(path, &block) = add_route(path, :GET, block)
28
+ def self.put(path, &block) = add_route(path, :PUT, block)
29
+ def self.post(path, &block) = add_route(path, :POST, block)
30
+ def self.head(path, &block) = add_route(path, :HEAD, block)
31
+ def self.patch(path, &block) = add_route(path, :PATCH, block)
32
+ def self.delete(path, &block) = add_route(path, :DELETE, block)
33
+ def self.options(path, &block) = add_route(path, :OPTIONS, block)
34
+
35
+ def self.inherited(subclass)
36
+ super
37
+ subclass.instance_variable_set(:@_root, RouteNode.new)
38
+ subclass.instance_variable_set(:@_plugins, [])
39
+ subclass.instance_variable_set(:@_middlewares, @_middlewares&.dup || [])
40
+
41
+ Plugin.load_defaults! if Plugin.load_defaults?
42
+ end
43
+
44
+ def self.plugin(plugin_name, *, &)
45
+ @_loaded_plugins ||= {}
46
+ @_plugins ||= []
47
+
48
+ return if @_loaded_plugins.key?(plugin_name)
49
+
50
+ plugin_module = Plugin.load(plugin_name)
51
+ plugin_module.configure(self, *, &) if plugin_module.respond_to?(:configure)
52
+
53
+ @_loaded_plugins[plugin_name] = plugin_module
54
+ @_plugins << plugin_name
55
+ end
56
+
57
+ def self.freeze!
58
+ app = new
59
+ app.freeze!
60
+ app
61
+ end
62
+
63
+ def self.add_route(path, http_method, block)
64
+ @_root ||= RouteNode.new
65
+ parts = path.split('/').reject(&:empty?)
66
+ @_root.add_route(parts, http_method, block)
67
+ end
68
+
69
+ private_class_method :add_route
70
+
71
+ def initialize
72
+ @_mutex = Mutex.new
73
+ @_root = self.class.instance_variable_get(:@_root)&.dup || RouteNode.new
74
+ @_plugins = self.class.instance_variable_get(:@_plugins)&.dup || []
75
+ @_loaded_plugins = self.class.instance_variable_get(:@_loaded_plugins)&.dup || {}
76
+ @_middlewares = self.class.instance_variable_get(:@_middlewares)&.dup || []
77
+
78
+ build_app
79
+
80
+ yield self if block_given?
81
+ end
82
+
83
+ def call(env) = @_mutex.synchronize { @_app.call(env) }
84
+
85
+ def freeze!
86
+ return self if @_mounted
87
+
88
+ @_root.freeze
89
+ @_plugins.freeze
90
+ @_loaded_plugins.freeze
91
+ @_middlewares.freeze
92
+ @_app.freeze if @_app.respond_to?(:freeze)
93
+ self
94
+ end
95
+
96
+ def get(path, &block) = add_route(path, :GET, block)
97
+ def put(path, &block) = add_route(path, :PUT, block)
98
+ def post(path, &block) = add_route(path, :POST, block)
99
+ def head(path, &block) = add_route(path, :HEAD, block)
100
+ def patch(path, &block) = add_route(path, :PATCH, block)
101
+ def delete(path, &block) = add_route(path, :DELETE, block)
102
+ def options(path, &block) = add_route(path, :OPTIONS, block)
103
+
104
+ def plugin(plugin_name, *, &)
105
+ return if @_loaded_plugins.key?(plugin_name)
106
+
107
+ plugin_module = Plugin.load(plugin_name)
108
+ self.class.extend plugin_module::ClassMethods if plugin_module.const_defined?(:ClassMethods)
109
+ self.class.include plugin_module::InstanceMethods if plugin_module.const_defined?(:InstanceMethods)
110
+ plugin_module.configure(self, *, &) if plugin_module.respond_to?(:configure)
111
+ @_loaded_plugins[plugin_name] = plugin_module
112
+ @_plugins << plugin_name
113
+ end
114
+
115
+ private
116
+
117
+ def build_app
118
+ @_app = method(:process_request)
119
+
120
+ @_middlewares.reverse_each do |middleware, args, block|
121
+ @_app = middleware.new(@_app, *args, &block)
122
+ end
123
+ end
124
+
125
+ def process_request(env)
126
+ http_method = env[Rack::REQUEST_METHOD].to_sym
127
+ parts = env[Rack::PATH_INFO].split('/').reject(&:empty?)
128
+
129
+ block, params = @_root.match_route(parts, http_method)
130
+ return not_found unless block
131
+
132
+ res = Response.new
133
+ req = Request.new(env, params)
134
+
135
+ catch(:halt) do
136
+ instance_exec(req, res, &block)
137
+ res.finish
138
+ end
139
+ rescue StandardError => e
140
+ handle_error(e)
141
+ end
142
+
143
+ def handle_error(error)
144
+ case error
145
+ when ArgumentError
146
+ [400, { 'content-type' => 'text/plain' }, ["Bad Request: #{error.message}"]]
147
+ else
148
+ [500, { 'content-type' => 'text/plain' }, ["Internal Server Error: #{error.message}"]]
149
+ end
150
+ end
151
+
152
+ def not_found = [404, { 'content-type' => 'text/plain' }, ['Not Found']]
153
+
154
+ def add_route(path, http_method, block)
155
+ parts = path.split('/').reject(&:empty?)
156
+ @_root.add_route(parts, http_method, block)
157
+ end
132
158
  end