flame 5.0.0.rc4 → 5.0.0.rc7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +929 -0
  3. data/LICENSE.txt +19 -0
  4. data/README.md +135 -0
  5. data/lib/flame/application.rb +47 -46
  6. data/lib/flame/config.rb +73 -0
  7. data/lib/flame/controller/actions.rb +122 -0
  8. data/lib/flame/{dispatcher → controller}/cookies.rb +8 -3
  9. data/lib/flame/controller/path_to.rb +34 -10
  10. data/lib/flame/controller.rb +45 -78
  11. data/lib/flame/dispatcher/request.rb +22 -6
  12. data/lib/flame/dispatcher/routes.rb +22 -14
  13. data/lib/flame/dispatcher/static.rb +13 -9
  14. data/lib/flame/dispatcher.rb +15 -18
  15. data/lib/flame/errors/argument_not_assigned_error.rb +3 -8
  16. data/lib/flame/errors/config_file_not_found_error.rb +17 -0
  17. data/lib/flame/errors/controller_not_found_error.rb +19 -0
  18. data/lib/flame/errors/route_arguments_order_error.rb +3 -10
  19. data/lib/flame/errors/route_extra_arguments_error.rb +7 -20
  20. data/lib/flame/errors/route_not_found_error.rb +4 -9
  21. data/lib/flame/errors/template_not_found_error.rb +2 -8
  22. data/lib/flame/path.rb +36 -18
  23. data/lib/flame/render.rb +13 -5
  24. data/lib/flame/router/controller_finder.rb +56 -0
  25. data/lib/flame/router/route.rb +9 -0
  26. data/lib/flame/router/routes.rb +56 -9
  27. data/lib/flame/router/routes_refine/mounting.rb +57 -0
  28. data/lib/flame/router/routes_refine.rb +144 -0
  29. data/lib/flame/router.rb +7 -157
  30. data/lib/flame/validators.rb +14 -10
  31. data/lib/flame/version.rb +1 -1
  32. data/lib/flame.rb +12 -5
  33. metadata +107 -96
  34. data/bin/flame +0 -16
  35. data/lib/flame/application/config.rb +0 -49
  36. data/template/.editorconfig +0 -15
  37. data/template/.gitignore +0 -28
  38. data/template/.rubocop.yml +0 -14
  39. data/template/Gemfile +0 -55
  40. data/template/Rakefile +0 -824
  41. data/template/application.rb.erb +0 -10
  42. data/template/config/config.rb.erb +0 -56
  43. data/template/config/database.example.yml +0 -5
  44. data/template/config/deploy.example.yml +0 -2
  45. data/template/config/puma.rb +0 -56
  46. data/template/config/sequel.rb.erb +0 -22
  47. data/template/config/server.example.yml +0 -32
  48. data/template/config/session.example.yml +0 -7
  49. data/template/config.ru.erb +0 -72
  50. data/template/controllers/_controller.rb.erb +0 -14
  51. data/template/controllers/site/_controller.rb.erb +0 -18
  52. data/template/controllers/site/index_controller.rb.erb +0 -12
  53. data/template/db/.keep +0 -0
  54. data/template/filewatchers.yml +0 -12
  55. data/template/helpers/.keep +0 -0
  56. data/template/lib/.keep +0 -0
  57. data/template/locales/en.yml +0 -0
  58. data/template/models/.keep +0 -0
  59. data/template/public/.keep +0 -0
  60. data/template/server +0 -200
  61. data/template/services/.keep +0 -0
  62. data/template/views/.keep +0 -0
  63. data/template/views/site/index.html.erb.erb +0 -1
  64. data/template/views/site/layout.html.erb.erb +0 -10
data/lib/flame/path.rb CHANGED
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
+ require 'memery'
4
5
 
5
6
  require_relative 'errors/argument_not_assigned_error'
6
7
 
7
8
  module Flame
8
9
  ## Class for working with paths
9
10
  class Path
11
+ include Memery
12
+
13
+ extend Forwardable
14
+ def_delegators :to_s, :include?
15
+
10
16
  ## Merge parts of path to one path
11
17
  ## @param parts [Array<String, Flame::Path>] parts of expected path
12
18
  ## @return [Flame::Path] path from parts
@@ -24,8 +30,8 @@ module Flame
24
30
 
25
31
  ## Return parts of path, splitted by slash (`/`)
26
32
  ## @return [Array<Flame::Path::Part>] array of path parts
27
- def parts
28
- @parts ||= @path.to_s.split('/').reject(&:empty?)
33
+ memoize def parts
34
+ @path.to_s.split('/').reject(&:empty?)
29
35
  .map! { |part| self.class::Part.new(part) }
30
36
  end
31
37
 
@@ -49,14 +55,10 @@ module Flame
49
55
  ## @return [-1, 0, 1] result of comparing
50
56
  def <=>(other)
51
57
  self_parts, other_parts = [self, other].map(&:parts)
52
- parts_size = self_parts.size <=> other_parts.size
53
- return parts_size unless parts_size.zero?
54
- self_parts.zip(other_parts)
55
- .reduce(0) do |result, (self_part, other_part)|
56
- break -1 if self_part.arg? && !other_part.arg?
57
- break 1 if other_part.arg? && !self_part.arg?
58
- result
59
- end
58
+ by_parts_size = self_parts.size <=> other_parts.size
59
+ return by_parts_size unless by_parts_size.zero?
60
+
61
+ compare_by_args_in_parts self_parts.zip(other_parts)
60
62
  end
61
63
 
62
64
  ## Compare with other path by parts
@@ -120,16 +122,32 @@ module Flame
120
122
  ## Not argument
121
123
  return part unless part.arg?
122
124
  ## Not required argument
123
- return args[part[2..-1].to_sym] if part.opt_arg?
125
+ return args.delete(part[2..].to_sym) if part.opt_arg?
126
+
124
127
  ## Required argument
125
- param = args[part[1..-1].to_sym]
128
+ param = args.delete(part[1..].to_sym)
126
129
  ## Required argument is nil
127
130
  error = Errors::ArgumentNotAssignedError.new(@path, part)
128
131
  raise error if param.nil?
132
+
129
133
  ## All is ok
130
134
  param
131
135
  end
132
136
 
137
+ def compare_by_args_in_parts(self_and_other_parts)
138
+ result = 0
139
+
140
+ self_and_other_parts.each do |self_part, other_part|
141
+ if self_part.arg?
142
+ break result = -1 unless other_part.arg?
143
+ elsif other_part.arg?
144
+ break result = 1
145
+ end
146
+ end
147
+
148
+ result
149
+ end
150
+
133
151
  ## Class for extracting arguments from other path
134
152
  class Extractor
135
153
  def initialize(parts, other_parts)
@@ -148,7 +166,7 @@ module Flame
148
166
 
149
167
  break if part.opt_arg? && @other_parts.count <= @other_index
150
168
 
151
- @args[part.clean.to_sym] = extract
169
+ @args[part.to_sym] = extract
152
170
  @index += 1
153
171
  end
154
172
 
@@ -178,7 +196,7 @@ module Flame
178
196
  class Part
179
197
  extend Forwardable
180
198
 
181
- def_delegators :@part, :[], :hash, :empty?, :b
199
+ def_delegators :to_s, :[], :hash, :size, :empty?, :b, :inspect
182
200
 
183
201
  ARG_CHAR = ':'
184
202
  ARG_CHAR_OPT = '?'
@@ -229,10 +247,10 @@ module Flame
229
247
  # arg? && !opt_arg?
230
248
  # end
231
249
 
232
- ## Path part as a String without arguments characters
233
- ## @return [String] clean String
234
- def clean
235
- @part.delete ARG_CHAR + ARG_CHAR_OPT
250
+ ## Path part as a Symbol without arguments characters
251
+ ## @return [Symbol] clean Symbol
252
+ def to_sym
253
+ @part.delete(ARG_CHAR + ARG_CHAR_OPT).to_sym
236
254
  end
237
255
  end
238
256
  end
data/lib/flame/render.rb CHANGED
@@ -29,15 +29,17 @@ module Flame
29
29
  @controller = controller
30
30
  @scope = options.delete(:scope) { @controller }
31
31
  @layout = options.delete(:layout) { 'layout.*' }
32
+
32
33
  ## Options for Tilt Template
33
34
  @tilt_options = options.delete(:tilt)
35
+
34
36
  ## And get the rest variables to locals
35
37
  @locals = options.merge(options.delete(:locals) { {} })
38
+
36
39
  ## Find filename
37
40
  @filename = find_file(path)
38
- unless @filename
39
- raise Flame::Errors::TemplateNotFoundError.new(controller, path)
40
- end
41
+ raise Flame::Errors::TemplateNotFoundError.new(controller, path) unless @filename
42
+
41
43
  @layout = nil if File.basename(@filename)[0] == '_'
42
44
  end
43
45
 
@@ -48,6 +50,7 @@ module Flame
48
50
  @cache = cache
49
51
  ## Compile Tilt to instance hash
50
52
  return unless @filename
53
+
51
54
  tilt = compile_file
52
55
  ## Render Tilt from instance hash with new options
53
56
  layout_render tilt.render(@scope, @locals, &block)
@@ -64,6 +67,7 @@ module Flame
64
67
  def compile_file(filename = @filename)
65
68
  cached = @controller.cached_tilts[filename]
66
69
  return cached if @cache && cached
70
+
67
71
  compiled = Tilt.new(filename, nil, @tilt_options)
68
72
  @controller.cached_tilts[filename] ||= compiled if @cache
69
73
  compiled
@@ -111,11 +115,13 @@ module Flame
111
115
 
112
116
  using GorillaPatch::Inflections
113
117
 
118
+ CONTROLLER_SUFFIXES = %w[_controller _ctrl].freeze
119
+ private_constant :CONTROLLER_SUFFIXES
120
+
114
121
  ## Find possible directories for the controller
115
122
  def controller_dirs
116
123
  parts = @controller.class.underscore.split('/').map do |part|
117
- %w[_controller _ctrl]
118
- .find { |suffix| part.chomp! suffix }
124
+ CONTROLLER_SUFFIXES.find { |suffix| part.chomp! suffix }
119
125
  part
120
126
  ## Alternative, but slower by ~50%:
121
127
  # part.sub(/_(controller|ctrl)$/, '')
@@ -151,8 +157,10 @@ module Flame
151
157
  ## @param result [String] result of template rendering
152
158
  def layout_render(content)
153
159
  return content unless @layout
160
+
154
161
  layout_files = find_layouts(@layout)
155
162
  return content if layout_files.empty?
163
+
156
164
  layout_files.each_with_object(content.dup) do |layout_file, result|
157
165
  layout = compile_file(layout_file)
158
166
  result.replace layout.render(@scope, @locals) { result }
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flame
4
+ ## Comment due to `private_constant`
5
+ class Router
6
+ ## Class for controller constant finding in namespace by names
7
+ class ControllerFinder
8
+ attr_reader :controller
9
+
10
+ def initialize(namespace_name, controller_or_name)
11
+ @namespace =
12
+ namespace_name.empty? ? Object : Object.const_get(namespace_name)
13
+
14
+ if controller_or_name.is_a?(Class)
15
+ @controller = controller_or_name
16
+ @controller_name = controller_or_name.name
17
+ else
18
+ @controller_name = controller_or_name
19
+ @controller = find
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def find
26
+ found_controller_name = controller_name_variations.find do |variation|
27
+ @namespace.const_defined?(variation) unless variation.empty?
28
+ end
29
+
30
+ raise_controller_not_found_error unless found_controller_name
31
+
32
+ controller = @namespace.const_get(found_controller_name)
33
+ return controller if controller < Flame::Controller
34
+
35
+ controller::IndexController
36
+ end
37
+
38
+ using GorillaPatch::Inflections
39
+
40
+ TRASNFORMATION_METHODS = %i[camelize upcase].freeze
41
+
42
+ def controller_name_variations
43
+ TRASNFORMATION_METHODS.each_with_object([]) do |method, result|
44
+ transformed = @controller_name.to_s.send(method)
45
+ result.push transformed, "#{transformed}Controller"
46
+ end
47
+ end
48
+
49
+ def raise_controller_not_found_error
50
+ raise Errors::ControllerNotFoundError.new(@controller_name, @namespace)
51
+ end
52
+ end
53
+
54
+ private_constant :ControllerFinder
55
+ end
56
+ end
@@ -7,6 +7,10 @@ module Flame
7
7
  class Router
8
8
  ## Class for Route in Router.routes
9
9
  class Route
10
+ extend Forwardable
11
+
12
+ def_delegators :to_s, :inspect
13
+
10
14
  attr_reader :controller, :action
11
15
 
12
16
  ## Create a new instance
@@ -22,12 +26,17 @@ module Flame
22
26
  ## @return [true, false] equal or not
23
27
  def ==(other)
24
28
  return false unless other.is_a? self.class
29
+
25
30
  %i[controller action].reduce(true) do |result, method|
26
31
  result && (
27
32
  public_send(method) == other.public_send(method)
28
33
  )
29
34
  end
30
35
  end
36
+
37
+ def to_s
38
+ "#{controller}##{action}"
39
+ end
31
40
  end
32
41
  end
33
42
  end
@@ -12,10 +12,15 @@ module Flame
12
12
  ## Flame::Router::Routes.new('/foo/bar/baz')
13
13
  ## # => { 'foo' => { 'bar' => { 'baz' => {} } } }
14
14
  def initialize(*path_parts)
15
+ super()
16
+
15
17
  path = Flame::Path.new(*path_parts)
16
18
  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
+
20
+ nested_routes = self.class.new Flame::Path.new(*path.parts[1..])
21
+ # path.parts.reduce(result) do |hash, part|
22
+ # hash[part] ||= self.class.new
23
+ # end
19
24
  self[path.parts.first] = nested_routes
20
25
  end
21
26
 
@@ -38,6 +43,12 @@ module Flame
38
43
  super
39
44
  end
40
45
 
46
+ ## Return the first available route (at the first level).
47
+ ## @return [Flame::Router::Route] the first route
48
+ def first_route
49
+ values.find { |value| value.is_a?(Route) }
50
+ end
51
+
41
52
  ## Navigate to Routes or Route through static parts or arguments
42
53
  ## @param path_parts [Array<String, Flame::Path, Flame::Path::Part>]
43
54
  ## path or path parts as keys for navigating
@@ -48,31 +59,67 @@ module Flame
48
59
  def navigate(*path_parts)
49
60
  path_parts = Flame::Path.new(*path_parts).parts
50
61
  return dig_through_opt_args if path_parts.empty?
62
+
51
63
  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])
64
+ self[path_parts.first] || dig(first_opt_arg_key, path_parts.first)
65
+
66
+ endpoint&.navigate(*path_parts[1..]) ||
67
+ find_among_arg_keys(path_parts[1..])
56
68
  end
57
69
 
58
70
  ## Dig through optional arguments as keys
59
71
  ## @return [Flame::Router::Routes] return most nested end-point
60
72
  ## without optional arguments
61
73
  def dig_through_opt_args
62
- self[first_opt_arg_key]&.dig_through_opt_args || self
74
+ [
75
+ self[first_opt_arg_key]&.dig_through_opt_args,
76
+ self
77
+ ]
78
+ .compact.find(&:first_route)
63
79
  end
64
80
 
65
81
  def allow
66
82
  methods = keys.select { |key| key.is_a? Symbol }
67
83
  return if methods.empty?
84
+
68
85
  methods.push(:OPTIONS).join(', ')
69
86
  end
70
87
 
88
+ PADDING_SIZE = Router::HTTP_METHODS.map(&:size).max
89
+ PADDING_FORMAT = "%#{PADDING_SIZE}.#{PADDING_SIZE}s"
90
+
91
+ ## Output routes in human readable format
92
+ def to_s(prefix = '/')
93
+ sort.map do |key, value|
94
+ if key.is_a?(Symbol)
95
+ <<~OUTPUT
96
+ \e[1m#{format PADDING_FORMAT, key} #{prefix}\e[22m
97
+ #{' ' * PADDING_SIZE} \e[3m\e[36m#{value}\e[0m\e[23m
98
+ OUTPUT
99
+ else
100
+ value.to_s(Flame::Path.new(prefix, key))
101
+ end
102
+ end.join
103
+ end
104
+
105
+ ## Sort routes for human readability
106
+ def sort
107
+ sort_by do |key, _value|
108
+ [
109
+ key.is_a?(Symbol) ? Router::HTTP_METHODS.index(key) : Float::INFINITY,
110
+ key.to_s
111
+ ]
112
+ end
113
+ end
114
+
71
115
  private
72
116
 
73
- def first_arg_key
117
+ def find_among_arg_keys(path_parts)
74
118
  keys.find do |key|
75
- key.is_a?(Flame::Path::Part) && key.arg?
119
+ next unless key.is_a?(Flame::Path::Part) && key.arg?
120
+
121
+ result = self[key].navigate(*path_parts)
122
+ break result if result
76
123
  end
77
124
  end
78
125
 
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flame
4
+ class Router
5
+ class RoutesRefine
6
+ ## Module for mounting in RoutesRefine
7
+ module Mounting
8
+ private
9
+
10
+ using GorillaPatch::DeepMerge
11
+
12
+ ## Mount controller inside other (parent) controller
13
+ ## @param controller [Flame::Controller] class of mounting controller
14
+ ## @param path [String, nil] root path for mounting controller
15
+ ## @yield Block of code for routes refine
16
+ def mount(controller_name, path = nil, &block)
17
+ routes_refine = self.class.new(
18
+ @namespace_name, controller_name, path, &block
19
+ )
20
+
21
+ @endpoint.deep_merge! routes_refine.routes
22
+
23
+ @reverse_routes.merge!(
24
+ routes_refine.reverse_routes.transform_values do |hash|
25
+ hash.transform_values { |action_path| @path + action_path }
26
+ end
27
+ )
28
+ end
29
+
30
+ using GorillaPatch::Namespace
31
+
32
+ def mount_nested_controllers
33
+ namespace = Object.const_get(@namespace_name)
34
+
35
+ namespace.constants.each do |constant_name|
36
+ constant = namespace.const_get(constant_name)
37
+ if constant < Flame::Controller || constant.instance_of?(Module)
38
+ mount_nested_controller constant
39
+ end
40
+ end
41
+ end
42
+
43
+ def mount_nested_controller(nested_controller)
44
+ mount nested_controller if should_be_mounted? nested_controller
45
+ end
46
+
47
+ def should_be_mounted?(controller)
48
+ if controller.instance_of?(Module)
49
+ controller.const_defined?(:IndexController, false)
50
+ else
51
+ controller.actions.any? && !@reverse_routes.key?(controller.to_s)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'routes_refine/mounting'
4
+
5
+ module Flame
6
+ class Router
7
+ ## Helper class for controller routing refine
8
+ class RoutesRefine
9
+ attr_reader :routes, :reverse_routes
10
+
11
+ class << self
12
+ include Memery
13
+
14
+ ## Defaults REST routes (methods, pathes, controllers actions)
15
+ def rest_routes
16
+ [
17
+ { method: :GET, path: '/', action: :index },
18
+ { method: :POST, path: '/', action: :create },
19
+ { method: :GET, path: '/', action: :show },
20
+ { method: :PUT, path: '/', action: :update },
21
+ { method: :DELETE, path: '/', action: :delete }
22
+ ]
23
+ end
24
+ end
25
+
26
+ using GorillaPatch::Namespace
27
+
28
+ def initialize(
29
+ namespace_name, controller_or_name, path, nested: true, &block
30
+ )
31
+ @controller =
32
+ ControllerFinder.new(namespace_name, controller_or_name).controller
33
+ @namespace_name = @controller.deconstantize
34
+ @path = Flame::Path.new(path || @controller.path)
35
+ @controller.path_arguments = @path.parts.select(&:arg?).map(&:to_sym)
36
+ @routes, @endpoint = @path.to_routes_with_endpoint
37
+ @reverse_routes = {}
38
+ @mount_nested = nested
39
+ execute(&block)
40
+ end
41
+
42
+ private
43
+
44
+ HTTP_METHODS.each do |http_method|
45
+ ## Define refine methods for all HTTP methods
46
+ ## @overload post(path, action)
47
+ ## Execute action on requested path and HTTP method
48
+ ## @param path [String] path of method for the request
49
+ ## @param action [Symbol] name of method for the request
50
+ ## @example
51
+ ## Set path to '/bye' and method to :POST for action `goodbye`
52
+ ## post '/bye', :goodbye
53
+ ## @overload post(action)
54
+ ## Execute action on requested HTTP method
55
+ ## @param action [Symbol] name of method for the request
56
+ ## @example Set method to :POST for action `goodbye`
57
+ ## post :goodbye
58
+ define_method(http_method.downcase) do |action_path, action = nil|
59
+ ## Swap arguments if action in path variable
60
+ unless action
61
+ action = action_path.to_sym
62
+ action_path = nil
63
+ end
64
+ ## Initialize new route
65
+ route = Route.new(@controller, action)
66
+ ## Make path by controller method with parameners
67
+ action_path = Flame::Path.new(action_path).adapt(@controller, action)
68
+ ## Validate action path
69
+ validate_action_path(action, action_path)
70
+ ## Merge action path with controller path
71
+ path = Flame::Path.new(@path, action_path)
72
+ ## Remove the same route if needed
73
+ remove_old_routes(action, route)
74
+ ## Add new route
75
+ add_new_route(route, action, path, http_method)
76
+ end
77
+ end
78
+
79
+ ## Assign remaining methods of the controller
80
+ ## to defaults pathes and HTTP methods
81
+ def defaults
82
+ rest
83
+
84
+ @controller.actions.each do |action|
85
+ get action unless find_reverse_route(action)
86
+ end
87
+
88
+ mount_nested_controllers if @mount_nested && (
89
+ @controller.demodulize == 'IndexController' &&
90
+ !@namespace_name.empty?
91
+ )
92
+ end
93
+
94
+ ## Assign methods of the controller to REST architecture
95
+ def rest
96
+ self.class.rest_routes.each do |rest_route|
97
+ action = rest_route[:action]
98
+ next if !@controller.actions.include?(action) || find_reverse_route(action)
99
+
100
+ send(*rest_route.values.map(&:downcase))
101
+ end
102
+ end
103
+
104
+ include Mounting
105
+
106
+ ## Execute block of refinings end sorting routes
107
+ def execute(&block)
108
+ @controller.refined_http_methods
109
+ .each do |action, (http_method, action_path)|
110
+ send(http_method, action_path, action)
111
+ end
112
+ instance_exec(&block) if block
113
+ defaults
114
+ end
115
+
116
+ def find_reverse_route(action)
117
+ @reverse_routes.dig(@controller.to_s, action)
118
+ end
119
+
120
+ def validate_action_path(action, action_path)
121
+ Validators::RouteArgumentsValidator
122
+ .new(@controller, action_path, action)
123
+ .valid?
124
+ end
125
+
126
+ def remove_old_routes(action, new_route)
127
+ old_path = @reverse_routes[@controller.to_s]&.delete(action)
128
+ return unless old_path
129
+
130
+ @routes.dig(*old_path.parts)
131
+ .delete_if { |_method, old_route| old_route == new_route }
132
+ end
133
+
134
+ using GorillaPatch::DeepMerge
135
+
136
+ def add_new_route(route, action, path, http_method)
137
+ path_routes, endpoint = path.to_routes_with_endpoint
138
+ endpoint[http_method] = route
139
+ @routes.deep_merge!(path_routes)
140
+ (@reverse_routes[@controller.to_s] ||= {})[action] = path
141
+ end
142
+ end
143
+ end
144
+ end