lennarb 1.2.0 → 1.4.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.
@@ -1,140 +1,133 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2023-2024, by Aristóteles Coutinho.
5
-
6
1
  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
2
+ class Response
3
+ # @!attribute [rw] status
4
+ # @returns [Integer]
5
+ #
6
+ attr_accessor :status
7
+
8
+ # @!attribute [r] body
9
+ # @returns [Array]
10
+ #
11
+ attr_reader :body
12
+
13
+ # @!attribute [r] headers
14
+ # @returns [Hash]
15
+ #
16
+ attr_reader :headers
17
+
18
+ # @!attribute [r] length
19
+ # @returns [Integer]
20
+ #
21
+ attr_reader :length
22
+
23
+ # Constants
24
+ #
25
+ LOCATION = "location"
26
+ private_constant :LOCATION
27
+
28
+ CONTENT_TYPE = "content-type"
29
+ private_constant :CONTENT_TYPE
30
+
31
+ CONTENT_LENGTH = "content-length"
32
+ private_constant :CONTENT_LENGTH
33
+
34
+ ContentType = {HTML: "text/html", TEXT: "text/plain", JSON: "application/json"}.freeze
35
+ # Initialize the response object
36
+ #
37
+ # @returns [Response]
38
+ #
39
+ def initialize
40
+ @status = 404
41
+ @headers = {}
42
+ @body = []
43
+ @length = 0
44
+ end
45
+
46
+ # Set the response header
47
+ #
48
+ # @parameter [String] key
49
+ #
50
+ # @returns [String] value
51
+ #
52
+ def [](key)
53
+ @headers[key]
54
+ end
55
+
56
+ # Get the response header
57
+ #
58
+ # @parameter [String] key
59
+ # @parameter [String] value
60
+ #
61
+ # @returns [String] value
62
+ #
63
+ def []=(key, value)
64
+ @headers[key] = value
65
+ end
66
+
67
+ # Write to the response body
68
+ #
69
+ # @parameter [String] str
70
+ #
71
+ # @returns [String] str
72
+ #
73
+ def write(str)
74
+ str = str.to_s
75
+ @length += str.bytesize
76
+ @headers[CONTENT_LENGTH] = @length.to_s
77
+ @body << str
78
+ end
79
+
80
+ # Set the response type to text
81
+ #
82
+ # @parameter [String] str
83
+ #
84
+ # @returns [String] str
85
+ #
86
+ def text(str)
87
+ @headers[CONTENT_TYPE] = ContentType[:TEXT]
88
+ write(str)
89
+ end
90
+
91
+ # Set the response type to html
92
+ #
93
+ # @parameter [String] str
94
+ #
95
+ # @returns [String] str
96
+ #
97
+ def html(str)
98
+ @headers[CONTENT_TYPE] = ContentType[:HTML]
99
+ write(str)
100
+ end
101
+
102
+ # Set the response type to json
103
+ #
104
+ # @parameter [String] str
105
+ #
106
+ # @returns [String] str
107
+ #
108
+ def json(str)
109
+ @headers[CONTENT_TYPE] = ContentType[:JSON]
110
+ write(str)
111
+ end
112
+
113
+ # Redirect the response
114
+ #
115
+ # @parameter [String] path
116
+ # @parameter [Integer] status, default: 302
117
+ #
118
+ def redirect(path, status = 302)
119
+ @headers[LOCATION] = path
120
+ @status = status
121
+
122
+ throw :halt, finish
123
+ end
124
+
125
+ # Finish the response
126
+ #
127
+ # @returns [Array] response
128
+ #
129
+ def finish
130
+ [@status, @headers, @body]
131
+ end
132
+ end
140
133
  end
@@ -1,84 +1,61 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2023-2024, by Aristóteles Coutinho.
5
-
6
1
  class Lennarb
7
- class RouteNode
8
- attr_accessor :static_children, :dynamic_children, :blocks, :param_key
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
20
-
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
31
-
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
44
-
45
- current_node.blocks[http_method] = block
46
- end
47
-
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..]
54
-
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
59
-
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)
64
-
65
- return [result_block, result_params] if result_block
66
- end
67
- end
68
-
69
- [nil, nil]
70
- end
71
-
72
- # Merge the other RouteNode into the current one
73
- #
74
- # @parameter other [RouteNode] The other RouteNode to merge into the current one
75
- #
76
- # @return [void]
77
- #
78
- def merge!(other)
79
- self.static_children.merge!(other.static_children)
80
- self.dynamic_children.merge!(other.dynamic_children)
81
- self.blocks.merge!(other.blocks)
82
- end
83
- end
2
+ class RouteNode
3
+ attr_accessor :static_children, :dynamic_children, :blocks, :param_key
4
+
5
+ def initialize
6
+ @blocks = {}
7
+ @param_key = nil
8
+ @static_children = {}
9
+ @dynamic_children = {}
10
+ end
11
+
12
+ def add_route(parts, http_method, block)
13
+ current_node = self
14
+
15
+ parts.each do |part|
16
+ if part.start_with?(":")
17
+ param_sym = part[1..].to_sym
18
+ current_node.dynamic_children[param_sym] ||= RouteNode.new
19
+ dynamic_node = current_node.dynamic_children[param_sym]
20
+ dynamic_node.param_key = param_sym
21
+ current_node = dynamic_node
22
+ else
23
+ current_node.static_children[part] ||= RouteNode.new
24
+ current_node = current_node.static_children[part]
25
+ end
26
+ end
27
+
28
+ current_node.blocks[http_method] = block
29
+ end
30
+
31
+ def match_route(parts, http_method, params: {})
32
+ if parts.empty?
33
+ return [blocks[http_method], params] if blocks[http_method]
34
+ else
35
+ part = parts.first
36
+ rest = parts[1..]
37
+
38
+ if static_children.key?(part)
39
+ result_block, result_params = static_children[part].match_route(rest, http_method, params:)
40
+ return [result_block, result_params] if result_block
41
+ end
42
+
43
+ dynamic_children.each_value do |dyn_node|
44
+ new_params = params.dup
45
+ new_params[dyn_node.param_key] = part
46
+ result_block, result_params = dyn_node.match_route(rest, http_method, params: new_params)
47
+
48
+ return [result_block, result_params] if result_block
49
+ end
50
+ end
51
+
52
+ [nil, nil]
53
+ end
54
+
55
+ def merge!(other)
56
+ static_children.merge!(other.static_children)
57
+ dynamic_children.merge!(other.dynamic_children)
58
+ blocks.merge!(other.blocks)
59
+ end
60
+ end
84
61
  end
@@ -1,10 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2023-2024, by Aristóteles Coutinho.
5
-
6
1
  class Lennarb
7
- VERSION = '1.2.0'
2
+ VERSION = "1.4.0"
8
3
 
9
- public_constant :VERSION
4
+ public_constant :VERSION
10
5
  end
data/lib/lennarb.rb CHANGED
@@ -1,144 +1,72 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2023-2024, by Aristóteles Coutinho.
5
-
6
1
  # Core extensions
7
2
  #
8
- require 'pathname'
9
- require 'rack'
3
+ require "pathname"
4
+ require "rack"
5
+ require "bundler"
10
6
 
11
- # Base class for Lennarb
12
- #
13
- require_relative 'lennarb/application/base'
14
- require_relative 'lennarb/plugin'
15
- require_relative 'lennarb/request'
16
- require_relative 'lennarb/response'
17
- require_relative 'lennarb/route_node'
18
- require_relative 'lennarb/version'
7
+ require_relative "lennarb/request"
8
+ require_relative "lennarb/response"
9
+ require_relative "lennarb/route_node"
10
+ require_relative "lennarb/version"
11
+ require_relative "lennarb/constansts"
19
12
 
20
13
  class Lennarb
21
- # Error class
22
- #
23
- class LennarbError < StandardError; end
24
-
25
- # @attribute [r] root
26
- # @returns [RouteNode]
27
- #
28
- attr_reader :_root
14
+ class LennarbError < StandardError; end
29
15
 
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
16
+ def initialize
17
+ @_mutex ||= Mutex.new
18
+ yield self if block_given?
19
+ end
46
20
 
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
21
+ HTTP_METHODS.each do |http_method|
22
+ define_method(http_method.downcase) do |path, &block|
23
+ add_route(path, http_method, block)
24
+ end
25
+ end
55
26
 
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]]
27
+ def root
28
+ @root ||= RouteNode.new
29
+ end
65
30
 
66
- block, params = @_root.match_route(parts, http_method)
67
- return [404, { 'content-type' => 'text/plain' }, ['Not Found']] unless block
31
+ def app
32
+ @app ||= begin
33
+ request_handler = ->(env) { process_request(env) }
68
34
 
69
- @res = Response.new
70
- req = Request.new(env, params)
35
+ Rack::Builder.app do
36
+ run request_handler
37
+ end
38
+ end
39
+ end
71
40
 
72
- catch(:halt) do
73
- instance_exec(req, @res, &block)
74
- @res.finish
75
- end
76
- end
41
+ def initializer!
42
+ Bundler.require(:default, ENV["LENNA_ENV"] || "development")
77
43
 
78
- # Freeze the routes
79
- #
80
- # @returns [void]
81
- #
82
- def freeze! = @_root.freeze
44
+ root.freeze
45
+ app.freeze
46
+ end
83
47
 
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)
48
+ def call(env) = @_mutex.synchronize { app.call(env) }
98
49
 
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)
50
+ def add_route(path, http_method, block)
51
+ parts = path.split("/").reject(&:empty?)
52
+ root.add_route(parts, http_method, block)
53
+ end
109
54
 
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)
55
+ private def process_request(env)
56
+ http_method = env[Rack::REQUEST_METHOD].to_sym
57
+ parts = env[Rack::PATH_INFO].split("/").reject(&:empty?)
114
58
 
115
- @_applied_plugins << plugin_name
116
- end
59
+ block, params = root.match_route(parts, http_method)
60
+ return [404, {"content-type" => Response::ContentType[:TEXT]}, ["Not Found"]] unless block
117
61
 
118
- # Merge the other RouteNode into the current one
119
- #
120
- # @parameter other [RouteNode] The other RouteNode to merge into the current one
121
- #
122
- # @return [void]
123
- #
124
- def merge!(other)
125
- raise "Expected a Lennarb instance, got #{other.class}" unless other.is_a?(Lennarb)
62
+ res = Response.new
63
+ req = Request.new(env, params)
126
64
 
127
- @_root.merge!(other._root)
65
+ catch(:halt) do
66
+ instance_exec(req, res, &block)
67
+ res.finish
68
+ end
69
+ rescue => e
70
+ [500, {"content-type" => Response::ContentType[:TEXT]}, ["Internal Server Error - #{e.message}"]]
128
71
  end
129
-
130
- private
131
-
132
- # Add a route
133
- #
134
- # @parameter [String] path
135
- # @parameter [String] http_method
136
- # @parameter [Proc] block
137
- #
138
- # @returns [void]
139
- #
140
- def add_route(path, http_method, block)
141
- parts = SplitPath[path]
142
- @_root.add_route(parts, http_method, block)
143
- end
144
72
  end
data/license.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2023-2024, by Aristóteles Coutinho.
4
- Copyright, 2023, by aristotelesbr.
3
+ Copyright (c) 2023-2025 Aristótels Coutinho
5
4
 
6
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
6
  of this software and associated documentation files (the "Software"), to deal
data/logo/lennarb.png ADDED
Binary file