flame 4.18.1 → 5.0.0.rc1

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 (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