rage-rb 0.2.0 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4570bfdf4f86125be1d6c35b9ccba08a956fe8a20dac392e5ae914db347ad396
4
- data.tar.gz: '019e919339f97e641e65f9aecc91c02740875ebcbe71edf14a07144950e47f07'
3
+ metadata.gz: 7684c979952ee81bee165b9b667dea87199f3fa28dadadc4878b29a12211fdac
4
+ data.tar.gz: 2428443ee2456ef375990decfbf36a2952670ee517ac07cf4c994f5b9b7076f9
5
5
  SHA512:
6
- metadata.gz: 9ad7b89eb46407831ae723c1c15b87ea12b898ae1784989374b07fcffcc3afcff2a237861c94565afe4c5f486b99edb15022b539ab9cad142778e4b2718b92ea
7
- data.tar.gz: 471cb6bcbf294d9a4eb3e5fd69827f26661effafc1df20447b8e7eddd0816aff411266d22104988978c46318dc08c4fa753f1e1a4fa1327268a5f24d8851ac5a
6
+ metadata.gz: f59da6b77e69fa2b89761b30a497b4c0fa8f02cbabeed65c953b0f5e807b349e5b305bf31564bcc3eaeed4957215766e8781af5a5b8bbd9754e0461fa18f7b87
7
+ data.tar.gz: 733a15ef91abcbfc079a3bbbb83f82686038059b3c518c1c0a279e8000ad885c65cc5019cc691e2731fa1e59ca1f62e8fe95c8f454837ed0bdc65e181157ffc7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2023-10-08
4
+
5
+ ### Added
6
+
7
+ - CLI `routes` task.
8
+ - CLI `console` task.
9
+ - `:if` and `:unless` options in `before_action`.
10
+ - Allow to set response headers.
11
+ - Block version of `before_action`.
12
+
3
13
  ## [0.2.0] - 2023-09-27
4
14
 
5
15
  ### Added
data/Gemfile CHANGED
@@ -11,6 +11,3 @@ gem "rspec", "~> 3.0"
11
11
 
12
12
  gem "pg"
13
13
  gem "mysql2"
14
-
15
- gem "benchmark-ips"
16
- gem "mustache"
data/README.md CHANGED
@@ -142,7 +142,7 @@ end
142
142
  Version | Changes
143
143
  ------- |------------
144
144
  0.2 :white_check_mark: | ~~Gem configuration by env.<br>Add `skip_before_action`.<br>Add `rescue_from`.<br>Router updates:<br>&emsp;• make the `root` helper work correctly with `scope`;<br>&emsp;• support the `defaults` option;~~
145
- 0.3 | CLI updates:<br>&emsp;• `routes` task;<br>&emsp;• `console` task;<br>Support the `:if` and `:unless` options in `before_action`.<br>Allow to set response headers.
145
+ 0.3 :white_check_mark: | ~~CLI updates:<br>&emsp;• `routes` task;<br>&emsp;• `console` task;<br>Support the `:if` and `:unless` options in `before_action`.<br>Allow to set response headers.~~
146
146
  0.4 | Expose the `params` object.<br>Support header authentication with `authenticate_with_http_token`.<br>Router updates:<br>&emsp;• add the `resources` route helper;<br>&emsp;• add the `namespace` route helper;<br>&emsp;• support regexp constraints;
147
147
  0.5 | Implement Iodine-based equivalent of `ActionController::Live`.<br>Use `ActionDispatch::RemoteIp`.
148
148
  0.6 | Expose the `cookies` object.<br>Expose the `send_data` and `send_file` methods.<br>Support conditional get with `etag` and `last_modified`.
@@ -19,7 +19,7 @@ class Rage::Application
19
19
  end
20
20
 
21
21
  rescue => e
22
- [500, {}, ["#{e.class}:#{e.message}\n\n#{e.backtrace.join("\n")}"]] # TODO: check Rage.env
22
+ [500, {}, ["#{e.class}:#{e.message}\n\n#{e.backtrace.join("\n")}"]]
23
23
 
24
24
  ensure
25
25
  # notify Iodine the request can now be served
data/lib/rage/cli.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require "thor"
4
3
  require "rage"
4
+ require "irb"
5
5
 
6
6
  module Rage
7
7
  class CLI < Thor
@@ -25,11 +25,81 @@ module Rage
25
25
 
26
26
  ::Iodine.start
27
27
  end
28
+
29
+ desc 'routes', 'List all routes.'
30
+ option :grep, aliases: "-g", desc: "Filter routes by pattern"
31
+ def routes
32
+ # the result would be something like this:
33
+ # Action Verb Path Controller#Action
34
+ # index GET / application#index
35
+
36
+ # load config/application.rb
37
+ environment
38
+
39
+ routes = Rage.__router.routes
40
+
41
+ pattern = options[:grep]
42
+
43
+ if pattern
44
+ routes = routes.select do |route|
45
+ route[:path].match?(pattern) || route[:raw_handler].to_s.match?(pattern) || route[:method].match?(pattern)
46
+ end
47
+ end
48
+
49
+ return puts 'Action Verb Path Controller#Action' if routes.empty?
50
+
51
+ # construct a table
52
+ table = []
53
+
54
+ # longest_path is either the length of the longest path or 5
55
+ longest_path = routes.map { |route| route[:path].length }.max + 3
56
+ longest_path = longest_path > 5 ? longest_path : 5
57
+
58
+ longest_verb = routes.map { |route| route[:method].length }.max + 3
59
+ longest_verb = longest_verb > 4 ? longest_verb : 7
60
+
61
+ # longest_handler is either the length of the longest handler or 7, since DELETE is the longest HTTP method
62
+ longest_handler = routes.map { |route| route[:raw_handler].is_a?(Proc) ? 7 : route[:raw_handler].split('#').last.length }.max + 3
63
+ longest_handler = longest_handler > 7 ? longest_handler : 7
64
+
65
+ # longest_controller is either the length of the longest controller or 12, since Controller#{length} is the longest controller
66
+ longest_controller = routes.map { |route| route[:raw_handler].is_a?(Proc) ? 7 : route[:raw_handler].to_s.length }.max + 3
67
+ longest_controller = longest_controller > 12 ? longest_controller : 12
68
+
69
+ routes.each do |route|
70
+ table << [
71
+ format("%- #{longest_handler}s", route[:raw_handler].is_a?(Proc) ? 'Lambda' : route[:raw_handler].split('#').last),
72
+ format("%- #{longest_verb}s", route[:method]),
73
+ format("%- #{longest_path}s", route[:path]),
74
+ format("%- #{longest_controller}s", route[:raw_handler].is_a?(Proc) ? 'Lambda' : route[:raw_handler])
75
+ ]
76
+ end
77
+
78
+ table.unshift([format("%- #{longest_handler}s", 'Action'), format("%- #{longest_verb}s", 'Verb'), format("%- #{longest_path}s", 'Path'),
79
+ format("%- #{longest_path}s", "Controller#Action\n")])
80
+ # print the table
81
+ table.each do |row|
82
+ # this should be changed to use the main logger when added
83
+ puts row.join
84
+ end
85
+ end
86
+
87
+ desc "c", "Start the app console."
88
+ def console
89
+ environment
90
+ ARGV.clear
91
+ IRB.start
92
+ end
93
+
94
+ private
95
+
96
+ def environment
97
+ require File.expand_path("config/application.rb", Dir.pwd)
98
+ end
28
99
  end
29
100
 
30
101
  class NewAppGenerator < Thor::Group
31
102
  include Thor::Actions
32
-
33
103
  argument :path, type: :string
34
104
 
35
105
  def self.source_root
@@ -12,13 +12,21 @@ class RageController::API
12
12
 
13
13
  before_actions_chunk = if @__before_actions
14
14
  filtered_before_actions = @__before_actions.select do |h|
15
- (h[:only].nil? || h[:only].include?(action)) &&
16
- (h[:except].nil? || !h[:except].include?(action))
15
+ (!h[:only] || h[:only].include?(action)) &&
16
+ (!h[:except] || !h[:except].include?(action))
17
17
  end
18
18
 
19
19
  lines = filtered_before_actions.map do |h|
20
+ condition = if h[:if] && h[:unless]
21
+ "if #{h[:if]} && !#{h[:unless]}"
22
+ elsif h[:if]
23
+ "if #{h[:if]}"
24
+ elsif h[:unless]
25
+ "unless #{h[:unless]}"
26
+ end
27
+
20
28
  <<-RUBY
21
- #{h[:name]}
29
+ #{h[:name]} #{condition}
22
30
  return [@__status, @__headers, @__body] if @__rendered
23
31
  RUBY
24
32
  end
@@ -65,6 +73,16 @@ class RageController::API
65
73
  klass.__rescue_handlers = @__rescue_handlers.freeze
66
74
  end
67
75
 
76
+ # @private
77
+ @@__tmp_name_seed = ("a".."i").to_a.permutation
78
+
79
+ # @private
80
+ # define temporary method based on a block
81
+ def define_tmp_method(block)
82
+ name = @@__tmp_name_seed.next.join
83
+ define_method("__rage_tmp_#{name}", block)
84
+ end
85
+
68
86
  ############
69
87
  #
70
88
  # PUBLIC API
@@ -89,8 +107,7 @@ class RageController::API
89
107
  def rescue_from(*klasses, with: nil, &block)
90
108
  unless with
91
109
  if block_given?
92
- name = ("a".."z").to_a.sample(15).join
93
- with = define_method("__#{name}", &block)
110
+ with = define_tmp_method(block)
94
111
  else
95
112
  raise "No handler provided. Pass the `with` keyword argument or provide a block."
96
113
  end
@@ -107,21 +124,53 @@ class RageController::API
107
124
 
108
125
  # Register a new `before_action` hook. Calls with the same `action_name` will overwrite the previous ones.
109
126
  #
110
- # @param action_name [String] the name of the callback to add
111
- # @param only [Symbol, Array<Symbol>] restrict the callback to run only for specific actions
112
- # @param except [Symbol, Array<Symbol>] restrict the callback to run for all actions except specified
127
+ # @param action_name [String, nil] the name of the callback to add
128
+ # @param [Hash] opts action options
129
+ # @option opts [Symbol, Array<Symbol>] :only restrict the callback to run only for specific actions
130
+ # @option opts [Symbol, Array<Symbol>] :except restrict the callback to run for all actions except specified
131
+ # @option opts [Symbol, Proc] :if only run the callback if the condition is true
132
+ # @option opts [Symbol, Proc] :unless only run the callback if the condition is false
113
133
  # @example
114
134
  # before_action :find_photo, only: :show
115
135
  #
116
136
  # def find_photo
117
137
  # Photo.first
118
138
  # end
119
- def before_action(action_name, only: nil, except: nil)
139
+ # @example
140
+ # before_action :require_user, unless: :logged_in?
141
+ # @example
142
+ # before_action :set_locale, if: -> { params[:locale] != "en-US" }
143
+ # @example
144
+ # before_action do
145
+ # unless logged_in? # would be `controller.send(:logged_in?)` in Rails
146
+ # head :unauthorized
147
+ # end
148
+ # end
149
+ # @note The block form doesn't receive an argument and is executed on the controller level as if it was a regular method.
150
+ def before_action(action_name = nil, **opts, &block)
151
+ if block_given?
152
+ action_name = define_tmp_method(block)
153
+ elsif action_name.nil?
154
+ raise "No handler provided. Pass the `action_name` parameter or provide a block."
155
+ end
156
+
157
+ _only, _except, _if, _unless = opts.values_at(:only, :except, :if, :unless)
158
+
120
159
  if @__before_actions && @__before_actions.frozen?
121
160
  @__before_actions = @__before_actions.dup
122
161
  end
123
162
 
124
- action = { name: action_name, only: only && Array(only), except: except && Array(except) }
163
+ action = {
164
+ name: action_name,
165
+ only: _only && Array(_only),
166
+ except: _except && Array(_except),
167
+ if: _if,
168
+ unless: _unless
169
+ }
170
+
171
+ action[:if] = define_tmp_method(action[:if]) if action[:if].is_a?(Proc)
172
+ action[:unless] = define_tmp_method(action[:unless]) if action[:unless].is_a?(Proc)
173
+
125
174
  if @__before_actions.nil?
126
175
  @__before_actions = [action]
127
176
  elsif i = @__before_actions.find_index { |a| a[:name] == action_name }
@@ -192,7 +241,7 @@ class RageController::API
192
241
  @__body << if json
193
242
  json.is_a?(String) ? json : json.to_json
194
243
  else
195
- __set_header("content-type", "text/plain; charset=utf-8")
244
+ headers["content-type"] = "text/plain; charset=utf-8"
196
245
  plain.to_s
197
246
  end
198
247
 
@@ -225,11 +274,13 @@ class RageController::API
225
274
  end
226
275
  end
227
276
 
228
- private
229
-
230
- # copy-on-write implementation for the headers object
231
- def __set_header(key, value)
232
- @__headers = @__headers.dup if DEFAULT_HEADERS.equal?(@__headers)
233
- @__headers[key] = value
277
+ # Set response headers.
278
+ #
279
+ # @example
280
+ # headers["Content-Type"] = "application/pdf"
281
+ def headers
282
+ # copy-on-write implementation for the headers object
283
+ @__headers = {}.merge!(@__headers) if DEFAULT_HEADERS.equal?(@__headers)
284
+ @__headers
234
285
  end
235
286
  end
@@ -3,6 +3,8 @@
3
3
  require "uri"
4
4
 
5
5
  class Rage::Router::Backend
6
+ attr_reader :routes
7
+
6
8
  OPTIONAL_PARAM_REGEXP = /\/?\(\/?(:\w+)\/?\)/
7
9
  STRING_HANDLER_REGEXP = /^([a-z0-9_\/]+)#([a-z_]+)$/
8
10
 
@@ -13,6 +15,7 @@ class Rage::Router::Backend
13
15
  end
14
16
 
15
17
  def on(method, path, handler, constraints: {}, defaults: nil)
18
+ raw_handler = handler
16
19
  raise "Path could not be empty" if path&.empty?
17
20
 
18
21
  if match_index = (path =~ OPTIONAL_PARAM_REGEXP)
@@ -42,7 +45,7 @@ class Rage::Router::Backend
42
45
  handler = ->(env, _params) { orig_handler.call(env) }
43
46
  end
44
47
 
45
- __on(method, path, handler, constraints, defaults)
48
+ __on(method, path, handler, raw_handler, constraints, defaults)
46
49
  end
47
50
 
48
51
  def lookup(env)
@@ -52,7 +55,7 @@ class Rage::Router::Backend
52
55
 
53
56
  private
54
57
 
55
- def __on(method, path, handler, constraints, defaults)
58
+ def __on(method, path, handler, raw_handler, constraints, defaults)
56
59
  @constrainer.validate_constraints(constraints)
57
60
  # Let the constrainer know if any constraints are being used now
58
61
  @constrainer.note_usage(constraints)
@@ -159,7 +162,7 @@ class Rage::Router::Backend
159
162
  end
160
163
  end
161
164
 
162
- route = { method: method, path: path, pattern: pattern, params: params, constraints: constraints, handler: handler, defaults: defaults }
165
+ route = { method: method, path: path, pattern: pattern, params: params, constraints: constraints, handler: handler, raw_handler: raw_handler, defaults: defaults }
163
166
  @routes << route
164
167
  current_node.add_route(route, @constrainer)
165
168
  end
data/lib/rage/setup.rb CHANGED
@@ -1,7 +1,23 @@
1
1
  Iodine.patch_rack
2
2
 
3
- project_root = Pathname.new(".").expand_path
3
+ require_relative "#{Rage.root}/config/environments/#{Rage.env}"
4
4
 
5
- require_relative "#{project_root}/config/environments/#{Rage.env}"
6
- Dir["#{project_root}/app/**/*.rb"].each { |path| require_relative path }
7
- require_relative "#{project_root}/config/routes"
5
+
6
+ # load application files
7
+ app, bad = Dir["#{Rage.root}/app/**/*.rb"], []
8
+
9
+ loop do
10
+ path = app.shift
11
+ break if path.nil?
12
+
13
+ require_relative path
14
+
15
+ # push the file to the end of the list in case it depends on another file that has not yet been required;
16
+ # re-raise if only errored out files are left
17
+ rescue NameError
18
+ raise if (app - bad).empty?
19
+ app << path
20
+ bad << path
21
+ end
22
+
23
+ require_relative "#{Rage.root}/config/routes"
data/lib/rage/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rage
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/rage-rb.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "rack"
4
4
  require "json"
5
5
  require "iodine"
6
+ require "pathname"
6
7
 
7
8
  module Rage
8
9
  def self.application
@@ -33,6 +34,10 @@ module Rage
33
34
  [:default, Rage.env.to_sym]
34
35
  end
35
36
 
37
+ def self.root
38
+ @root ||= Pathname.new(".").expand_path
39
+ end
40
+
36
41
  module Router
37
42
  module Strategies
38
43
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rage-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Samoilov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-27 00:00:00.000000000 Z
11
+ date: 2023-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor