flame 4.18.1 → 5.0.0.rc6

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 (54) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +921 -0
  3. data/LICENSE.txt +19 -0
  4. data/README.md +135 -0
  5. data/lib/flame.rb +12 -4
  6. data/lib/flame/application.rb +93 -40
  7. data/lib/flame/config.rb +73 -0
  8. data/lib/flame/controller.rb +62 -98
  9. data/lib/flame/controller/actions.rb +122 -0
  10. data/lib/flame/controller/cookies.rb +44 -0
  11. data/lib/flame/controller/path_to.rb +63 -0
  12. data/lib/flame/dispatcher.rb +44 -73
  13. data/lib/flame/dispatcher/request.rb +33 -4
  14. data/lib/flame/dispatcher/routes.rb +66 -0
  15. data/lib/flame/dispatcher/static.rb +26 -15
  16. data/lib/flame/errors/argument_not_assigned_error.rb +7 -6
  17. data/lib/flame/errors/config_file_not_found_error.rb +17 -0
  18. data/lib/flame/errors/controller_not_found_error.rb +19 -0
  19. data/lib/flame/errors/route_arguments_order_error.rb +9 -8
  20. data/lib/flame/errors/route_extra_arguments_error.rb +18 -18
  21. data/lib/flame/errors/route_not_found_error.rb +8 -7
  22. data/lib/flame/errors/template_not_found_error.rb +6 -6
  23. data/lib/flame/path.rb +141 -55
  24. data/lib/flame/render.rb +46 -15
  25. data/lib/flame/router.rb +41 -127
  26. data/lib/flame/router/controller_finder.rb +56 -0
  27. data/lib/flame/router/route.rb +16 -54
  28. data/lib/flame/router/routes.rb +136 -0
  29. data/lib/flame/router/routes_refine.rb +144 -0
  30. data/lib/flame/router/routes_refine/mounting.rb +57 -0
  31. data/lib/flame/validators.rb +21 -11
  32. data/lib/flame/version.rb +1 -1
  33. metadata +139 -84
  34. data/bin/flame +0 -71
  35. data/lib/flame/application/config.rb +0 -43
  36. data/lib/flame/dispatcher/cookies.rb +0 -31
  37. data/template/.gitignore +0 -11
  38. data/template/Gemfile +0 -15
  39. data/template/Rakefile.erb +0 -64
  40. data/template/app.rb.erb +0 -7
  41. data/template/config.ru.erb +0 -20
  42. data/template/config/config.rb.erb +0 -14
  43. data/template/config/database.example.yml +0 -5
  44. data/template/config/sequel.rb.erb +0 -15
  45. data/template/config/thin.example.yml +0 -18
  46. data/template/controllers/_base_controller.rb.erb +0 -13
  47. data/template/db/.keep +0 -0
  48. data/template/helpers/.keep +0 -0
  49. data/template/lib/.keep +0 -0
  50. data/template/locales/en.yml +0 -0
  51. data/template/models/.keep +0 -0
  52. data/template/public/.keep +0 -0
  53. data/template/server +0 -49
  54. data/template/views/.keep +0 -0
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gorilla_patch/slice'
4
+
5
+ module Flame
6
+ class Controller
7
+ ## Module for work with actions
8
+ module Actions
9
+ include Memery
10
+ using GorillaPatch::Slice
11
+
12
+ ## Shortcut for not-inherited public methods: actions
13
+ ## @return [Array<Symbol>] array of actions (public instance methods)
14
+ def actions
15
+ public_instance_methods(false)
16
+ end
17
+
18
+ ## Re-define public instance methods (actions) from parent
19
+ ## @param actions [Array<Symbol>] Actions for inheritance
20
+ ## @param exclude [Array<Symbol>] Actions for excluding from inheritance
21
+ ## @param from [Module]
22
+ ## Module (or Class) from which actions will be inherited
23
+ ## @example Inherit all parent actions
24
+ ## class MyController < BaseController
25
+ ## inherit_actions
26
+ ## end
27
+ ## @example Inherit certain parent actions
28
+ ## class MyController < BaseController
29
+ ## inherit_actions %i[index show]
30
+ ## end
31
+ ## @example Inherit all parent actions exclude certain
32
+ ## class MyController < BaseController
33
+ ## inherit_actions exclude: %i[edit update]
34
+ ## end
35
+ ## @example Inherit certain actions from specific module
36
+ ## class MyController < BaseController
37
+ ## inherit_actions %i[index show], from: ModuleWithActions
38
+ ## end
39
+ def inherit_actions(actions = nil, exclude: [], from: superclass)
40
+ actions = from.actions if actions.nil?
41
+ actions -= exclude
42
+
43
+ actions.each do |action|
44
+ define_method action, from.public_instance_method(action)
45
+ end
46
+
47
+ return unless from.respond_to?(:refined_http_methods)
48
+
49
+ refined_http_methods.merge!(
50
+ from.refined_http_methods.slice(*actions)
51
+ )
52
+ end
53
+
54
+ ## Re-define public instance method from module
55
+ ## @param mod [Module] Module for including to controller
56
+ ## @param exclude [Array<Symbol>] Actions for excluding
57
+ ## from module public instance methods
58
+ ## @param only [Array<Symbol>] Actions for re-defining
59
+ ## from module public instance methods
60
+ ## @example Define actions from module in controller
61
+ ## class MyController < BaseController
62
+ ## include with_actions Module1
63
+ ## include with_actions Module2
64
+ ## ....
65
+ ## end
66
+ ## @example Define actions from module exclude some actions in controller
67
+ ## class MyController < BaseController
68
+ ## include with_actions Module1, exclude: %i[action1 action2 ...]
69
+ ## include with_actions Module2, exclude: %i[action1 action2 ...]
70
+ ## ....
71
+ ## end
72
+ ## @example Define actions from module according list in controller
73
+ ## class MyController < BaseController
74
+ ## include with_actions Module1, only: %i[action1 action2 ...]
75
+ ## include with_actions Module2, only: %i[action1 action2 ...]
76
+ ## ....
77
+ ## end
78
+ def with_actions(mod, exclude: [], only: nil)
79
+ Module.new do
80
+ @mod = mod
81
+ @actions = only || (@mod.public_instance_methods(false) - exclude)
82
+
83
+ extend ModuleWithActions
84
+ end
85
+ end
86
+
87
+ memoize def refined_http_methods
88
+ {}
89
+ end
90
+
91
+ private
92
+
93
+ Flame::Router::HTTP_METHODS.each do |http_method|
94
+ downcased_http_method = http_method.downcase
95
+ define_method(
96
+ downcased_http_method
97
+ ) do |action_or_action_path, action = nil|
98
+ action, action_path =
99
+ if action
100
+ [action, action_or_action_path]
101
+ else
102
+ [action_or_action_path, nil]
103
+ end
104
+ refined_http_methods[action] = [downcased_http_method, action_path]
105
+ end
106
+ end
107
+
108
+ ## Base module for module `with_actions`
109
+ module ModuleWithActions
110
+ using GorillaPatch::Slice
111
+
112
+ def included(ctrl)
113
+ ctrl.include @mod
114
+
115
+ ctrl.inherit_actions @actions, from: @mod
116
+ end
117
+ end
118
+
119
+ private_constant :ModuleWithActions
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flame
4
+ class Controller
5
+ ## Helper class for cookies
6
+ class Cookies
7
+ ## Create an instance of Cookies
8
+ ## @param request_cookies [Hash{String=>Object}] cookies from request
9
+ ## @param response [Flame::Dispatcher::Response, Rack::Response]
10
+ ## response object for cookies setting
11
+ def initialize(request_cookies, response)
12
+ @request_cookies = request_cookies
13
+ @response = response
14
+ end
15
+
16
+ ## Get request cookies
17
+ ## @param key [String, Symbol] name of cookie
18
+ ## @return [Object] value of cookie
19
+ def [](key)
20
+ @request_cookies[key.to_s]
21
+ end
22
+
23
+ ## Set (or delete) cookies for response
24
+ ## @param key [String, Symbol] name of cookie
25
+ ## @param new_value [Object, Hash, nil] value of cookie or Hash with `:value` and options
26
+ ## @example Set new value to `cat` cookie
27
+ ## cookies['cat'] = 'nice cat'
28
+ ## @example Set new value to `cat` cookie with `Max-Age` 60 seconds
29
+ ## cookies['cat'] = { value: 'nice cat', max_age: 60 }
30
+ ## @example Delete `cat` cookie
31
+ ## cookies['cat'] = nil
32
+ def []=(key, new_value)
33
+ case new_value
34
+ when NilClass
35
+ @response.delete_cookie(key.to_s, path: '/')
36
+ when Hash
37
+ @response.set_cookie(key.to_s, new_value)
38
+ else
39
+ @response.set_cookie(key.to_s, value: new_value, path: '/')
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flame
4
+ class Controller
5
+ ## Module with methods for path or URL building
6
+ module PathTo
7
+ include Memery
8
+
9
+ ## Look documentation at {Flame::Dispatcher#path_to}
10
+ def path_to(*args)
11
+ add_controller_class(args)
12
+ add_controller_arguments(args)
13
+ @dispatcher.path_to(*args)
14
+ end
15
+
16
+ ## Build a URI to the given controller and action, or path
17
+ def url_to(*args, **options)
18
+ path = build_path_for_url(*args, **options)
19
+ Addressable::URI.join(request.base_url, path).to_s
20
+ end
21
+
22
+ using GorillaPatch::Namespace
23
+
24
+ ## Path to previous page, or to index action, or to Index controller
25
+ ## @return [String] path to previous page or to index
26
+ def path_to_back
27
+ back_path = request.referer
28
+ return back_path if back_path && back_path != request.url
29
+
30
+ return path_to :index if self.class.actions.include?(:index)
31
+
32
+ '/'
33
+ end
34
+
35
+ private
36
+
37
+ def add_controller_class(args)
38
+ args.unshift(self.class) if args[0].is_a?(Symbol)
39
+ args.insert(1, :index) if args[0].is_a?(Class) && !args[1].is_a?(Symbol)
40
+ end
41
+
42
+ def add_controller_arguments(args)
43
+ if args[-1].is_a?(Hash)
44
+ args[-1] = controller_arguments.merge args[-1]
45
+ else
46
+ args.push(controller_arguments)
47
+ end
48
+ end
49
+
50
+ def build_path_for_url(*args, **options)
51
+ first_arg = args.first
52
+ if first_arg.is_a?(String) || first_arg.is_a?(Flame::Path)
53
+ find_static(first_arg).path(with_version: options[:version])
54
+ else
55
+ path_to(*args, **options)
56
+ end
57
+ end
58
+
59
+ memoize :build_path_for_url,
60
+ condition: -> { config[:environment] == 'production' }
61
+ end
62
+ end
63
+ end
@@ -1,29 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'gorilla-patch/symbolize'
3
+ require 'gorilla_patch/symbolize'
4
+ require 'rack'
4
5
 
5
6
  require_relative 'dispatcher/request'
6
7
  require_relative 'dispatcher/response'
7
8
 
8
- require_relative 'dispatcher/cookies'
9
9
  require_relative 'dispatcher/static'
10
+ require_relative 'dispatcher/routes'
10
11
 
11
12
  require_relative 'errors/route_not_found_error'
12
13
 
13
14
  module Flame
14
15
  ## Helpers for dispatch Flame::Application#call
15
16
  class Dispatcher
16
- GEM_STATIC_FILES = File.join __dir__, '..', '..', 'public'
17
+ include Memery
18
+
19
+ GEM_STATIC_FILES = File.join(__dir__, '../../public').freeze
20
+
21
+ extend Forwardable
22
+ def_delegators :@app_class, :router, :path_to
17
23
 
18
24
  attr_reader :request, :response
19
25
 
20
26
  include Flame::Dispatcher::Static
27
+ include Flame::Dispatcher::Routes
21
28
 
22
29
  ## Initialize Dispatcher from Application#call
23
- ## @param app [Flame::Application] application object
30
+ ## @param app_class [Class] application class
24
31
  ## @param env Rack-environment object
25
- def initialize(app, env)
26
- @app = app
32
+ def initialize(app_class, env)
33
+ @app_class = app_class
27
34
  @env = env
28
35
  @request = Flame::Dispatcher::Request.new(env)
29
36
  @response = Flame::Dispatcher::Response.new
@@ -32,12 +39,15 @@ module Flame
32
39
  ## Start of execution the request
33
40
  def run!
34
41
  catch :halt do
35
- try_static ||
42
+ validate_request
43
+
44
+ try_options ||
45
+ try_static ||
36
46
  try_static(dir: GEM_STATIC_FILES) ||
37
47
  try_route ||
38
48
  halt(404)
39
49
  end
40
- response.write body unless request.http_method == :HEAD
50
+ response.write body unless request.head?
41
51
  response.finish
42
52
  end
43
53
 
@@ -65,43 +75,27 @@ module Flame
65
75
 
66
76
  ## Parameters of the request
67
77
  def params
68
- @params ||= request.params.symbolize_keys(deep: true)
78
+ request.params.symbolize_keys(deep: true)
79
+ rescue ArgumentError => e
80
+ raise unless e.message.include?('invalid %-encoding')
81
+
82
+ {}
69
83
  end
84
+ memoize :params
70
85
 
71
86
  ## Session object as Hash
72
87
  def session
73
88
  request.session
74
89
  end
75
90
 
76
- ## Cookies object as Hash
77
- def cookies
78
- @cookies ||= Cookies.new(request.cookies, response)
79
- end
80
-
81
91
  ## Application-config object as Hash
82
92
  def config
83
- @app.config
93
+ @app_class.config
84
94
  end
85
95
 
86
- ## Build a path to the given controller and action, with any expected params
87
- ##
88
- ## @param ctrl [Flame::Controller] class of controller
89
- ## @param action [Symbol] method of controller
90
- ## @param args [Hash] parameters for method of controller
91
- ## @return [String] path for requested method, controller and parameters
92
- ## @example Path for `show(id)` method of `ArticlesController` with `id: 2`
93
- ## path_to ArticlesController, :show, id: 2 # => "/articles/show/2"
94
- ## @example Path for `new` method of `ArticlesController` with params
95
- ## path_to ArticlesController, :new, params: { author_id: 1 }
96
- ## # => "/articles/new?author_id=1"
97
- def path_to(ctrl, action = :index, args = {})
98
- route = @app.class.router.find_route(controller: ctrl, action: action)
99
- raise Errors::RouteNotFoundError.new(ctrl, action) unless route
100
- query = Rack::Utils.build_nested_query args.delete(:params)
101
- query = nil if query&.empty?
102
- path = route.path.assign_arguments(args)
103
- path = '/' if path.empty?
104
- URI::Generic.build(path: path, query: query).to_s
96
+ ## Available routes endpoint
97
+ memoize def available_endpoint
98
+ router.navigate(*request.path.parts)
105
99
  end
106
100
 
107
101
  ## Interrupt the execution of route, and set new optional data
@@ -117,7 +111,7 @@ module Flame
117
111
  ## @example Halt with 404, render template
118
112
  ## halt 404, render('errors/404')
119
113
  ## @example Halt with 200, set new headers
120
- ## halt 200, 'Cats!', 'Content-Type' => 'animal/cat'
114
+ ## halt 200, 'Cats!', 'Content-Type' # => 'animal/cat'
121
115
  def halt(new_status = nil, new_body = nil, new_headers = {})
122
116
  status new_status if new_status
123
117
  body new_body || (default_body_of_nearest_route if body.empty?)
@@ -139,55 +133,32 @@ module Flame
139
133
  ## Generate default body of error page
140
134
  def default_body
141
135
  # response.headers[Rack::CONTENT_TYPE] = 'text/html'
142
- "<h1>#{Rack::Utils::HTTP_STATUS_CODES[status]}</h1>"
136
+ Rack::Utils::HTTP_STATUS_CODES[status]
143
137
  end
144
138
 
145
139
  ## All cached tilts (views) for application by Flame::Render
146
140
  def cached_tilts
147
- @app.class.cached_tilts
141
+ @app_class.cached_tilts
148
142
  end
149
143
 
150
144
  private
151
145
 
152
- ## Find route and try execute it
153
- def try_route
154
- route = @app.class.router.find_route(
155
- method: request.http_method,
156
- path: request.path
157
- )
158
- return nil unless route
159
- status 200
160
- execute_route(route)
161
- end
146
+ def validate_request
147
+ ## https://github.com/rack/rack/issues/337#issuecomment-48555831
148
+ request.params
149
+ rescue ArgumentError => e
150
+ raise unless e.message.include?('invalid %-encoding')
162
151
 
163
- ## Execute route
164
- ## @param route [Flame::Route] route that must be executed
165
- def execute_route(route, action = route.action)
166
- params.merge! route.path.extract_arguments(request.path)
167
- # route.execute(self)
168
- controller = route.controller.new(self)
169
- controller.send(:execute, action)
170
- rescue => exception
171
- # p 'rescue from dispatcher'
172
- dump_error(exception)
173
- status 500
174
- controller&.send(:server_error, exception)
175
- # p 're raise exception from dispatcher'
176
- # raise exception
152
+ halt 400
177
153
  end
178
154
 
179
- ## Generate a default body of nearest route
180
- def default_body_of_nearest_route
181
- ## Return nil if must be no body for current HTTP status
182
- return if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
183
- ## Find the nearest route by the parts of requested path
184
- route = @app.router.find_nearest_route(request.path)
185
- ## Return nil if the route not found
186
- ## or it's `default_body` method not defined
187
- return default_body unless route
188
- ## Execute `default_body` method for the founded route
189
- execute_route(route, :default_body)
190
- default_body if body.empty?
155
+ ## Return response if HTTP-method is OPTIONS
156
+ def try_options
157
+ return unless request.http_method == :OPTIONS
158
+
159
+ allow = available_endpoint&.allow
160
+ halt 404 unless allow
161
+ response.headers['Allow'] = allow
191
162
  end
192
163
  end
193
164
  end
@@ -4,14 +4,43 @@ module Flame
4
4
  class Dispatcher
5
5
  ## Class for requests
6
6
  class Request < Rack::Request
7
+ include Memery
8
+
7
9
  ## Initialize Flame::Path
8
- def path
9
- @path ||= Flame::Path.new path_info
10
+ memoize def path
11
+ Flame::Path.new path_info
10
12
  end
11
13
 
12
14
  ## Override HTTP-method of the request if the param '_method' found
13
- def http_method
14
- @http_method ||= (params['_method'] || request_method).upcase.to_sym
15
+ memoize def http_method
16
+ method_from_method =
17
+ begin
18
+ params['_method']
19
+ rescue ArgumentError => e
20
+ ## https://github.com/rack/rack/issues/337#issuecomment-48555831
21
+ raise unless e.message.include?('invalid %-encoding')
22
+ end
23
+
24
+ (method_from_method || request_method).upcase.to_sym
25
+ end
26
+
27
+ using GorillaPatch::Inflections
28
+
29
+ HEADER_PREFIX = 'HTTP_'
30
+
31
+ ## Helper method for comfortable Camel-Cased Hash of headers
32
+ memoize def headers
33
+ env.each_with_object({}) do |(key, value), result|
34
+ next unless key.start_with?(HEADER_PREFIX)
35
+
36
+ ## TODO: Replace `String#[]` with `#delete_prefix`
37
+ ## after Ruby < 2.5 dropping
38
+ camelized_key =
39
+ key[HEADER_PREFIX.size..-1].downcase.tr('_', '/')
40
+ .camelize.gsub('::', '-')
41
+
42
+ result[camelized_key] = value
43
+ end
15
44
  end
16
45
  end
17
46
  end