flame 4.18.1 → 5.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/bin/flame +7 -62
  3. data/lib/flame.rb +1 -0
  4. data/lib/flame/application.rb +75 -17
  5. data/lib/flame/application/config.rb +6 -0
  6. data/lib/flame/controller.rb +36 -76
  7. data/lib/flame/controller/path_to.rb +39 -0
  8. data/lib/flame/dispatcher.rb +25 -66
  9. data/lib/flame/dispatcher/cookies.rb +10 -2
  10. data/lib/flame/dispatcher/routes.rb +53 -0
  11. data/lib/flame/dispatcher/static.rb +15 -8
  12. data/lib/flame/errors/argument_not_assigned_error.rb +6 -0
  13. data/lib/flame/errors/route_arguments_order_error.rb +6 -0
  14. data/lib/flame/errors/route_extra_arguments_error.rb +10 -0
  15. data/lib/flame/errors/route_not_found_error.rb +10 -4
  16. data/lib/flame/errors/template_not_found_error.rb +6 -0
  17. data/lib/flame/path.rb +63 -33
  18. data/lib/flame/render.rb +21 -8
  19. data/lib/flame/router.rb +112 -66
  20. data/lib/flame/router/route.rb +9 -56
  21. data/lib/flame/router/routes.rb +86 -0
  22. data/lib/flame/validators.rb +7 -1
  23. data/lib/flame/version.rb +1 -1
  24. data/template/.editorconfig +15 -0
  25. data/template/.gitignore +19 -2
  26. data/template/.rubocop.yml +14 -0
  27. data/template/Gemfile +48 -8
  28. data/template/Rakefile +824 -0
  29. data/template/{app.rb.erb → application.rb.erb} +4 -1
  30. data/template/config.ru.erb +62 -10
  31. data/template/config/config.rb.erb +44 -2
  32. data/template/config/database.example.yml +1 -1
  33. data/template/config/deploy.example.yml +2 -0
  34. data/template/config/puma.rb +56 -0
  35. data/template/config/sequel.rb.erb +13 -6
  36. data/template/config/server.example.yml +32 -0
  37. data/template/config/session.example.yml +7 -0
  38. data/template/controllers/{_base_controller.rb.erb → _controller.rb.erb} +5 -4
  39. data/template/controllers/site/_controller.rb.erb +18 -0
  40. data/template/controllers/site/index_controller.rb.erb +12 -0
  41. data/template/filewatchers.yml +12 -0
  42. data/template/server +172 -21
  43. data/template/services/.keep +0 -0
  44. data/template/views/site/index.html.erb.erb +1 -0
  45. data/template/views/site/layout.html.erb.erb +10 -0
  46. metadata +112 -54
  47. data/template/Rakefile.erb +0 -64
  48. data/template/config/thin.example.yml +0 -18
@@ -13,14 +13,26 @@ require_relative 'errors/template_not_found_error'
13
13
  module Flame
14
14
  ## Helper for render functionality
15
15
  class Render
16
+ ## Create a new instance from controller, by path and with options
17
+ ## @param controller [Flame::Controller]
18
+ ## controller for default scope, views directory and cache
19
+ ## @param path [Symbol, String] path (full or the last part) for view search
20
+ ## @param options [Hash] options for template
21
+ ## @option options [Object] :scope (controller)
22
+ ## scope of visibility in rendering
23
+ ## @option options [Symbol, String, false] :layout ('layout.*')
24
+ ## name of the layout file
25
+ ## @option options [Hash] :tilt options for Tilt
26
+ ## @option options [Hash] :locals ({}) local variables for rendering
16
27
  def initialize(controller, path, options = {})
17
28
  ## Take options for rendering
18
29
  @controller = controller
19
- @scope = options.delete(:scope) || @controller
20
- @layout = options.delete(:layout)
21
- @layout = 'layout.*' if @layout.nil?
30
+ @scope = options.delete(:scope) { @controller }
31
+ @layout = options.delete(:layout) { 'layout.*' }
32
+ ## Options for Tilt Template
33
+ @tilt_options = options.delete(:tilt)
22
34
  ## And get the rest variables to locals
23
- @locals = options.merge(options.delete(:locals) || {})
35
+ @locals = options.merge(options.delete(:locals) { {} })
24
36
  ## Find filename
25
37
  @filename = find_file(path)
26
38
  unless @filename
@@ -29,15 +41,16 @@ module Flame
29
41
  @layout = nil if File.basename(@filename)[0] == '_'
30
42
  end
31
43
 
32
- ## Render template
44
+ ## Render template with layout
33
45
  ## @param cache [Boolean] cache compiles or not
34
- def render(cache: true)
46
+ ## @return [String] compiled template
47
+ def render(cache: true, &block)
35
48
  @cache = cache
36
49
  ## Compile Tilt to instance hash
37
50
  return unless @filename
38
51
  tilt = compile_file
39
52
  ## Render Tilt from instance hash with new options
40
- layout_render tilt.render(@scope, @locals)
53
+ layout_render tilt.render(@scope, @locals, &block)
41
54
  end
42
55
 
43
56
  private
@@ -51,7 +64,7 @@ module Flame
51
64
  def compile_file(filename = @filename)
52
65
  cached = @controller.cached_tilts[filename]
53
66
  return cached if @cache && cached
54
- compiled = Tilt.new(filename)
67
+ compiled = Tilt.new(filename, nil, @tilt_options)
55
68
  @controller.cached_tilts[filename] ||= compiled if @cache
56
69
  compiled
57
70
  end
@@ -1,66 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'gorilla-patch/deep_merge'
4
+ require 'gorilla-patch/inflections'
5
+ require 'gorilla-patch/namespace'
6
+ require 'gorilla-patch/transform'
7
+
8
+ require_relative 'router/routes'
3
9
  require_relative 'router/route'
4
10
 
5
11
  module Flame
6
12
  ## Router class for routing
7
13
  class Router
8
- attr_reader :app, :routes
14
+ attr_reader :app, :routes, :reverse_routes
9
15
 
16
+ ## @param app [Flame::Application] host application
10
17
  def initialize(app)
11
18
  @app = app
12
- @routes = []
13
- end
14
-
15
- ## Add the controller with it's methods to routes
16
- ## @param ctrl [Flame::Controller] class of the controller which will be added
17
- ## @param path [String, nil] root path for controller's methods
18
- ## @yield block for routes refine
19
- def add_controller(ctrl, path = nil, &block)
20
- ## @todo Add Regexp paths
21
-
22
- ## Add routes from controller to glob array
23
- route_refine = RouteRefine.new(self, ctrl, path, block)
24
- concat_routes(route_refine)
25
- end
26
-
27
- ## Find route by any attributes
28
- ## @param attrs [Hash] attributes for comparing
29
- ## @return [Flame::Route, nil] return the found route, otherwise `nil`
30
- def find_route(attrs)
31
- route = routes.find { |r| r.compare_attributes(attrs) }
32
- route.dup if route
19
+ @routes = Flame::Router::Routes.new
20
+ @reverse_routes = {}
33
21
  end
34
22
 
35
23
  ## Find the nearest route by path
36
24
  ## @param path [Flame::Path] path for route finding
37
25
  ## @return [Flame::Route, nil] return the found nearest route or `nil`
38
26
  def find_nearest_route(path)
39
- path = Flame::Path.new(path) if path.is_a? String
40
27
  path_parts = path.parts.dup
41
- while path_parts.size >= 0
42
- route = find_route path: Flame::Path.new(*path_parts)
43
- break if route || path_parts.empty?
44
- path_parts.pop
28
+ loop do
29
+ route = routes.navigate(*path_parts)&.values&.grep(Route)&.first
30
+ break route if route || path_parts.pop.nil?
45
31
  end
46
- route
47
32
  end
48
33
 
49
- private
50
-
51
- ## Add `RouteRefine` routes to the routes of `Flame::Router`
52
- ## @param route_refine [Flame::Router::RouteRefine] `RouteRefine` with routes
53
- def concat_routes(route_refine)
54
- routes.concat(route_refine.routes)
34
+ ## Find the path of route
35
+ ## @param route_or_controller [Flame::Router::Route, Flame::Controller]
36
+ ## route or controller
37
+ ## @param action [Symbol, nil] action (or not for route)
38
+ ## @return [Flame::Path] mounted path to action of controller
39
+ def path_of(route_or_controller, action = nil)
40
+ if route_or_controller.is_a?(Flame::Router::Route)
41
+ route = route_or_controller
42
+ controller = route.controller
43
+ action = route.action
44
+ else
45
+ controller = route_or_controller
46
+ end
47
+ reverse_routes.dig(controller.to_s, action)
55
48
  end
56
49
 
57
50
  ## Helper class for controller routing refine
58
- class RouteRefine
59
- attr_accessor :rest_routes
60
- attr_reader :ctrl, :routes
51
+ class RoutesRefine
52
+ attr_reader :routes, :reverse_routes
61
53
 
62
54
  ## Defaults REST routes (methods, pathes, controllers actions)
63
- def rest_routes
55
+ def self.rest_routes
64
56
  @rest_routes ||= [
65
57
  { method: :GET, path: '/', action: :index },
66
58
  { method: :POST, path: '/', action: :create },
@@ -70,15 +62,33 @@ module Flame
70
62
  ]
71
63
  end
72
64
 
73
- def initialize(router, ctrl, path, block)
65
+ def initialize(router, namespace_name, controller_name, path, &block)
74
66
  @router = router
75
- @ctrl = ctrl
76
- @path = path || @ctrl.default_path
77
- @routes = []
67
+ @controller = constantize_controller namespace_name, controller_name
68
+ @path = Flame::Path.new(path || @controller.default_path)
69
+ @routes, @endpoint = @path.to_routes_with_endpoint
70
+ @reverse_routes = {}
78
71
  execute(&block)
79
72
  end
80
73
 
81
- %i[GET POST PUT PATCH DELETE].each do |request_method|
74
+ private
75
+
76
+ using GorillaPatch::Inflections
77
+
78
+ def constantize_controller(namespace_name, controller_name)
79
+ controller_name = controller_name.to_s.camelize
80
+ namespace =
81
+ namespace_name.empty? ? Object : Object.const_get(namespace_name)
82
+ if namespace.const_defined?(controller_name)
83
+ controller = namespace.const_get(controller_name)
84
+ return controller if controller < Flame::Controller
85
+ controller::IndexController
86
+ else
87
+ namespace.const_get("#{controller_name}Controller")
88
+ end
89
+ end
90
+
91
+ %i[GET POST PUT PATCH DELETE].each do |http_method|
82
92
  ## Define refine methods for all HTTP methods
83
93
  ## @overload post(path, action)
84
94
  ## Execute action on requested path and HTTP method
@@ -91,19 +101,24 @@ module Flame
91
101
  ## @param action [Symbol] name of method for the request
92
102
  ## @example Set method to :POST for action `goodbye`
93
103
  ## post :goodbye
94
- method = request_method.downcase
95
- define_method(method) do |path, action = nil|
104
+ define_method(http_method.downcase) do |action_path, action = nil|
96
105
  ## Swap arguments if action in path variable
97
106
  unless action
98
- action = path.to_sym
99
- path = nil
107
+ action = action_path.to_sym
108
+ action_path = nil
100
109
  end
101
- ## Init new Route
102
- route = Route.new(@ctrl, action, method, @path, path)
103
- ## Try to find route with the same action
104
- index = find_route_index(action: action)
105
- ## Overwrite route if needed
106
- index ? @routes[index] = route : @routes.push(route)
110
+ ## Initialize new route
111
+ route = Route.new(@controller, action)
112
+ ## Make path by controller method with parameners
113
+ action_path = Flame::Path.new(action_path).adapt(@controller, action)
114
+ ## Validate action path
115
+ validate_action_path(action, action_path)
116
+ ## Merge action path with controller path
117
+ path = Flame::Path.new(@path, action_path)
118
+ ## Remove the same route if needed
119
+ remove_old_routes(action, route)
120
+ ## Add new route
121
+ add_new_route(route, action, path, http_method)
107
122
  end
108
123
  end
109
124
 
@@ -111,42 +126,73 @@ module Flame
111
126
  ## to defaults pathes and HTTP methods
112
127
  def defaults
113
128
  rest
114
- @ctrl.actions.each do |action|
115
- next if find_route_index(action: action)
129
+ @controller.actions.each do |action|
130
+ next if find_reverse_route(action)
116
131
  send(:GET.downcase, action)
117
132
  end
118
133
  end
119
134
 
120
135
  ## Assign methods of the controller to REST architecture
121
136
  def rest
122
- rest_routes.each do |rest_route|
137
+ self.class.rest_routes.each do |rest_route|
123
138
  action = rest_route[:action]
124
- next if !@ctrl.actions.include?(action) ||
125
- find_route_index(action: action)
139
+ next if !@controller.actions.include?(action) ||
140
+ find_reverse_route(action)
126
141
  send(*rest_route.values.map(&:downcase))
127
142
  end
128
143
  end
129
144
 
145
+ using GorillaPatch::Namespace
146
+ using GorillaPatch::Transform
147
+ using GorillaPatch::DeepMerge
148
+
130
149
  ## Mount controller inside other (parent) controller
131
- ## @param ctrl [Flame::Controller] class of mounting controller
150
+ ## @param controller [Flame::Controller] class of mounting controller
132
151
  ## @param path [String, nil] root path for mounting controller
133
152
  ## @yield Block of code for routes refine
134
- def mount(ctrl, path = nil, &block)
135
- path = Flame::Path.merge(@path, path || ctrl.default_path)
136
- @router.add_controller(ctrl, path, &block)
153
+ def mount(controller_name, path = nil, &block)
154
+ routes_refine = self.class.new(
155
+ @router, @controller.deconstantize, controller_name, path, &block
156
+ )
157
+
158
+ @endpoint.deep_merge! routes_refine.routes
159
+
160
+ @reverse_routes.merge!(
161
+ routes_refine.reverse_routes.transform_values do |hash|
162
+ hash.transform_values { |action_path| @path + action_path }
163
+ end
164
+ )
137
165
  end
138
166
 
139
- private
167
+ # private
140
168
 
141
169
  ## Execute block of refinings end sorting routes
142
170
  def execute(&block)
143
171
  instance_exec(&block) if block
144
172
  defaults
145
- @routes.sort!
146
173
  end
147
174
 
148
- def find_route_index(attrs)
149
- @routes.find_index { |route| route.compare_attributes(attrs) }
175
+ def find_reverse_route(action)
176
+ @reverse_routes.dig(@controller.to_s, action)
177
+ end
178
+
179
+ def validate_action_path(action, action_path)
180
+ Validators::RouteArgumentsValidator.new(
181
+ @controller, action_path, action
182
+ ).valid?
183
+ end
184
+
185
+ def remove_old_routes(action, new_route)
186
+ return unless (old_path = @reverse_routes[@controller.to_s]&.delete(action))
187
+ @routes.dig(*old_path.parts)
188
+ .delete_if { |_method, old_route| old_route == new_route }
189
+ end
190
+
191
+ def add_new_route(route, action, path, http_method)
192
+ path_routes, endpoint = path.to_routes_with_endpoint
193
+ endpoint[http_method] = route
194
+ @routes.deep_merge!(path_routes)
195
+ (@reverse_routes[@controller.to_s] ||= {})[action] = path
150
196
  end
151
197
  end
152
198
  end
@@ -7,74 +7,27 @@ module Flame
7
7
  class Router
8
8
  ## Class for Route in Router.routes
9
9
  class Route
10
- attr_reader :method, :controller, :action, :path
10
+ attr_reader :controller, :action
11
11
 
12
- def initialize(controller, action, method, ctrl_path, action_path)
12
+ ## Create a new instance
13
+ ## @param controller [Flame::Controller] controller
14
+ ## @param action [Symbol] action
15
+ def initialize(controller, action)
13
16
  @controller = controller
14
17
  @action = action
15
- @method = method.to_sym.upcase
16
- ## Make path by controller method with parameners
17
- action_path = Flame::Path.new(action_path).adapt(controller, action)
18
- ## Merge action path with controller path
19
- @path = Flame::Path.new(ctrl_path, action_path)
20
- Validators::RouteArgumentsValidator.new(
21
- @controller, action_path, @action
22
- ).valid?
23
- freeze
24
- end
25
-
26
- def freeze
27
- @path.freeze
28
- super
29
- end
30
-
31
- ## Compare attributes for `Router.find_route`
32
- ## @param attrs [Hash] Hash of attributes for comparing
33
- def compare_attributes(attrs)
34
- attrs.each do |name, value|
35
- next true if compare_attribute(name, value)
36
- break false
37
- end
38
18
  end
39
19
 
40
20
  ## Method for Routes comparison
21
+ ## @param other [Flame::Router::Route] other route
22
+ ## @return [true, false] equal or not
41
23
  def ==(other)
42
- %i[controller action method path].reduce(true) do |result, method|
24
+ return false unless other.is_a? self.class
25
+ %i[controller action].reduce(true) do |result, method|
43
26
  result && (
44
27
  public_send(method) == other.public_send(method)
45
28
  )
46
29
  end
47
30
  end
48
-
49
- ## Compare by:
50
- ## 1. path parts count (more is matter);
51
- ## 2. args position (father is matter);
52
- ## 3. HTTP-method (default).
53
- def <=>(other)
54
- path_result = other.path <=> path
55
- return path_result unless path_result.zero?
56
- method <=> other.method
57
- end
58
-
59
- private
60
-
61
- ## Helpers for `compare_attributes`
62
- def compare_attribute(name, value)
63
- case name
64
- when :method
65
- compare_method(value)
66
- when :path
67
- path.match? value
68
- else
69
- send(name) == value
70
- end
71
- end
72
-
73
- def compare_method(request_method)
74
- request_method = request_method.upcase.to_sym
75
- request_method = :GET if request_method == :HEAD
76
- method.upcase.to_sym == request_method
77
- end
78
31
  end
79
32
  end
80
33
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flame
4
+ class Router
5
+ ## Custom Hash for routes
6
+ class Routes < Hash
7
+ ## @param path_parts [Array<String, Flame::Path, Flame::Path::Part>]
8
+ ## path parts for nested keys
9
+ ## @example Initialize without keys
10
+ ## Flame::Router::Routes.new # => {}
11
+ ## @example Initialize with nested keys
12
+ ## Flame::Router::Routes.new('/foo/bar/baz')
13
+ ## # => { 'foo' => { 'bar' => { 'baz' => {} } } }
14
+ def initialize(*path_parts)
15
+ path = Flame::Path.new(*path_parts)
16
+ return if path.parts.empty?
17
+ nested_routes = self.class.new Flame::Path.new(*path.parts[1..-1])
18
+ # path.parts.reduce(result) { |hash, part| hash[part] ||= self.class.new }
19
+ self[path.parts.first] = nested_routes
20
+ end
21
+
22
+ ## Move into Hash by equal key
23
+ ## @param path_part [String, Flame::Path::Part, Symbol] requested key
24
+ ## @return [Flame::Router::Routes, Flame::Router::Route, nil] found value
25
+ ## @example Move by static path part
26
+ ## routes = Flame::Router::Routes.new('/foo/bar/baz')
27
+ ## routes['foo'] # => { 'bar' => { 'baz' => {} } }
28
+ ## @example Move by HTTP-method
29
+ ## routes = Flame::Router::Routes.new('/foo/bar')
30
+ ## routes['foo']['bar'][:GET] = 42
31
+ ## routes['foo']['bar'][:GET] # => 42
32
+ def [](key)
33
+ if key.is_a? String
34
+ key = Flame::Path::Part.new(key)
35
+ elsif !key.is_a?(Flame::Path::Part) && !key.is_a?(Symbol)
36
+ return
37
+ end
38
+ super
39
+ end
40
+
41
+ ## Navigate to Routes or Route through static parts or arguments
42
+ ## @param path_parts [Array<String, Flame::Path, Flame::Path::Part>]
43
+ ## path or path parts as keys for navigating
44
+ ## @return [Flame::Router::Routes, Flame::Router::Route, nil] found value
45
+ ## @example Move by static path part and argument
46
+ ## routes = Flame::Router::Routes.new('/foo/:first/bar')
47
+ ## routes.navigate('foo', 'value') # => { 'bar' => {} }
48
+ def navigate(*path_parts)
49
+ path_parts = Flame::Path.new(*path_parts).parts
50
+ return dig_through_opt_args if path_parts.empty?
51
+ endpoint =
52
+ self[path_parts.first] ||
53
+ dig(first_opt_arg_key, path_parts.first) ||
54
+ self[first_arg_key]
55
+ endpoint&.navigate(*path_parts[1..-1])
56
+ end
57
+
58
+ ## Dig through optional arguments as keys
59
+ ## @return [Flame::Router::Routes] return most nested end-point
60
+ ## without optional arguments
61
+ def dig_through_opt_args
62
+ self[first_opt_arg_key]&.dig_through_opt_args || self
63
+ end
64
+
65
+ def allow
66
+ methods = keys.select { |key| key.is_a? Symbol }
67
+ return if methods.empty?
68
+ methods.push(:OPTIONS).join(', ')
69
+ end
70
+
71
+ private
72
+
73
+ def first_arg_key
74
+ keys.find do |key|
75
+ key.is_a?(Flame::Path::Part) && key.arg?
76
+ end
77
+ end
78
+
79
+ def first_opt_arg_key
80
+ keys.find do |key|
81
+ key.is_a?(Flame::Path::Part) && key.opt_arg?
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end