flame 4.18.1 → 5.0.0.rc6

Sign up to get free protection for your applications and to get access to all the features.
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