flame 5.0.0.rc4 → 5.0.0.rc7

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