jets 1.9.32 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/lib/jets/application/defaults.rb +6 -0
  4. data/lib/jets/cli.rb +9 -0
  5. data/lib/jets/commands/help/generate.md +25 -19
  6. data/lib/jets/commands/main.rb +3 -3
  7. data/lib/jets/commands/rake_tasks.rb +3 -0
  8. data/lib/jets/commands/templates/skeleton/app/views/layouts/application.html.erb.tt +1 -0
  9. data/lib/jets/commands/templates/skeleton/config/application.rb.tt +4 -0
  10. data/lib/jets/commands/templates/webpacker/app/javascript/src/jets/crud.js +3 -0
  11. data/lib/jets/commands/upgrade.rb +42 -109
  12. data/lib/jets/commands/upgrade/version1.rb +136 -0
  13. data/lib/jets/controller/base.rb +16 -0
  14. data/lib/jets/controller/error.rb +4 -0
  15. data/lib/jets/controller/error/invalid_authenticity_token.rb +6 -0
  16. data/lib/jets/controller/forgery_protection.rb +43 -0
  17. data/lib/jets/controller/middleware/local.rb +3 -3
  18. data/lib/jets/controller/middleware/local/route_matcher.rb +1 -1
  19. data/lib/jets/controller/middleware/main.rb +7 -1
  20. data/lib/jets/controller/rack/adapter.rb +1 -1
  21. data/lib/jets/controller/rack/env.rb +1 -1
  22. data/lib/jets/controller/rendering/rack_renderer.rb +44 -37
  23. data/lib/jets/controller/stage.rb +2 -1
  24. data/lib/jets/generator.rb +72 -8
  25. data/lib/jets/generator/templates/active_job/job/templates/application_job.rb.tt +6 -0
  26. data/lib/jets/generator/templates/active_job/job/templates/job.rb.tt +8 -0
  27. data/lib/jets/generator/templates/erb/scaffold/_form.html.erb +11 -16
  28. data/lib/jets/generator/templates/erb/scaffold/edit.html.erb +2 -2
  29. data/lib/jets/generator/templates/erb/scaffold/index.html.erb +5 -5
  30. data/lib/jets/generator/templates/erb/scaffold/new.html.erb +1 -1
  31. data/lib/jets/generator/templates/erb/scaffold/show.html.erb +3 -3
  32. data/lib/jets/generator/templates/rails/scaffold_controller/controller.rb +5 -5
  33. data/lib/jets/internal/app/controllers/jets/rack_controller.rb +1 -0
  34. data/lib/jets/overrides/rails.rb +2 -1
  35. data/lib/jets/overrides/rails/action_controller.rb +12 -0
  36. data/lib/jets/overrides/rails/url_helper.rb +66 -5
  37. data/lib/jets/resource/api_gateway/rest_api/routes/change/base.rb +1 -1
  38. data/lib/jets/resource/api_gateway/rest_api/routes/collision.rb +1 -1
  39. data/lib/jets/router.rb +32 -46
  40. data/lib/jets/router/dsl.rb +136 -0
  41. data/lib/jets/router/error.rb +4 -0
  42. data/lib/jets/router/helpers.rb +4 -0
  43. data/lib/jets/router/helpers/core_helper.rb +17 -0
  44. data/lib/jets/router/helpers/named_routes_helper.rb +8 -0
  45. data/lib/jets/router/method_creator.rb +54 -0
  46. data/lib/jets/router/method_creator/code.rb +98 -0
  47. data/lib/jets/router/method_creator/edit.rb +7 -0
  48. data/lib/jets/router/method_creator/generic.rb +11 -0
  49. data/lib/jets/router/method_creator/index.rb +42 -0
  50. data/lib/jets/router/method_creator/new.rb +7 -0
  51. data/lib/jets/router/method_creator/root.rb +15 -0
  52. data/lib/jets/router/method_creator/show.rb +7 -0
  53. data/lib/jets/router/resources/base.rb +7 -0
  54. data/lib/jets/router/resources/filter.rb +15 -0
  55. data/lib/jets/router/resources/options.rb +13 -0
  56. data/lib/jets/router/route.rb +226 -0
  57. data/lib/jets/router/scope.rb +65 -4
  58. data/lib/jets/router/util.rb +38 -0
  59. data/lib/jets/turbo/project/config/application.rb +1 -0
  60. data/lib/jets/version.rb +1 -1
  61. metadata +26 -2
  62. data/lib/jets/route.rb +0 -166
@@ -0,0 +1,4 @@
1
+ class Jets::Router
2
+ class Error < RuntimeError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Jets::Router::Helpers
2
+ include CoreHelper
3
+ include NamedRoutesHelper
4
+ end
@@ -0,0 +1,17 @@
1
+ module Jets::Router::Helpers
2
+ module CoreHelper
3
+ # Used for form_for helper
4
+ def polymorphic_path(record, _)
5
+ url_for(record)
6
+ end
7
+
8
+ # override helper delegates to point to jets controller
9
+ # TODO: params is weird
10
+ CONTROLLER_DELEGATES = %w[session response headers]
11
+ CONTROLLER_DELEGATES.each do |meth|
12
+ define_method meth do
13
+ @_jets[:controller].send(meth)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ module Jets::Router::Helpers
2
+ module NamedRoutesHelper
3
+ def self.clear!
4
+ meths = public_instance_methods(false)
5
+ meths.each { |m| remove_method(m) }
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,54 @@
1
+ class Jets::Router
2
+ class MethodCreator
3
+ include Util
4
+
5
+ def initialize(options, scope)
6
+ @options, @scope = options, scope
7
+ @controller, @action = get_controller_action(@options)
8
+ end
9
+
10
+ def define_url_helper!
11
+ return unless @options[:method] == :get
12
+
13
+ if %w[index new show edit].include?(@action)
14
+ create_method(@action)
15
+ else
16
+ create_method("generic")
17
+ end
18
+ end
19
+
20
+ # Examples:
21
+ #
22
+ # posts_path: path: 'posts'
23
+ # admin_posts_path: prefix: 'admin', path: 'posts'
24
+ # new_post_path
25
+ #
26
+ def create_method(action)
27
+ # Code eventually does this:
28
+ #
29
+ # code = Jets::Router::MethodCreator::Edit.new
30
+ # def_meth code.path_method
31
+ #
32
+ class_name = "Jets::Router::MethodCreator::#{action.camelize}"
33
+ klass = class_name.constantize # Index, Show, Edit, New
34
+ code = klass.new(@options, @scope, @controller)
35
+
36
+ # puts "define_#{action}_methods:".color(:yellow) if code.path_method
37
+ # puts code.path_method.color(:blue) if code.path_method
38
+ # puts code.url_method.color(:blue) if code.url_method
39
+
40
+ def_meth(code.path_method) if code.path_method
41
+ def_meth(code.url_method) if code.url_method
42
+ end
43
+
44
+ def create_root_helper
45
+ code = Jets::Router::MethodCreator::Root.new(@options, @scope, @controller)
46
+ def_meth(code.path_method)
47
+ def_meth(code.url_method)
48
+ end
49
+
50
+ def def_meth(str)
51
+ Jets::Router::Helpers::NamedRoutesHelper.class_eval(str)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,98 @@
1
+ class Jets::Router::MethodCreator
2
+ class Code
3
+ include Jets::Router::Util
4
+
5
+ def initialize(options, scope, controller, action=nil)
6
+ @options, @scope, @controller, @action = options, scope, controller, action
7
+ @path, @as = options[:path], options[:as]
8
+ end
9
+
10
+ def meth_args
11
+ params = full_path.split('/').select { |x| x.include?(':') }
12
+ items = params.map { |x| x.sub(':','') }
13
+
14
+ items.empty? ? nil : "("+items.join(', ')+")"
15
+ end
16
+
17
+ def meth_result
18
+ results = full_path.split('/').map do |x|
19
+ if x.include?(':')
20
+ variable = x.sub(':','')
21
+ "\#{#{variable}.to_param}"
22
+ else
23
+ x
24
+ end
25
+ end
26
+
27
+ '/' + results.join('/') unless results.empty?
28
+ end
29
+
30
+ def full_path
31
+ route = Jets::Router::Route.new(@options, @scope)
32
+ route.compute_path
33
+ end
34
+
35
+ def action
36
+ @action || self.class.name.split('::').last.downcase
37
+ end
38
+
39
+ def full_as
40
+ @scope&.full_as
41
+ end
42
+
43
+ # The method_name_leaf is used to generate method names.
44
+ # Can be nil because sometimes the name is fully acquired from the scope.
45
+ def method_name_leaf
46
+ unless %w[resource resources].include?(@scope.from.to_s) && @options[:from_scope]
47
+ @controller
48
+ end
49
+ end
50
+
51
+ def full_meth_name(suffix=nil)
52
+ as = @as || meth_name
53
+ name = [as, suffix].compact.join('_')
54
+ underscore(name)
55
+ end
56
+
57
+ def path_method
58
+ <<~EOL
59
+ def #{full_meth_name(:path)}#{meth_args}
60
+ "#{meth_result}"
61
+ end
62
+ EOL
63
+ end
64
+
65
+ def url_method
66
+ path_method_call = "#{full_meth_name(:path)}#{meth_args}"
67
+ # Note: It is important lazily get the value of ENV['JETS_HOST'] within the method.
68
+ # Since it is not set until the requrest goes through the main middleware.
69
+ <<~EOL
70
+ def #{full_meth_name(:url)}#{meth_args}
71
+ "\#{ENV['JETS_HOST']}\#{#{path_method_call}}"
72
+ end
73
+ EOL
74
+ end
75
+
76
+ def param_name(name)
77
+ # split('/').last for case:
78
+ #
79
+ # resources :posts, prefix: "articles", only: :index do
80
+ # resources :comments, only: :new
81
+ # end
82
+ #
83
+ # Since the prefix at the scope level is added to the posts item, which results in:
84
+ #
85
+ # param_name("articles/posts")
86
+ #
87
+ # We drop the articles prefix portion. The resources items can only be words with no /.
88
+ #
89
+ name.to_s.split('/').last.singularize + "_id"
90
+ end
91
+
92
+ private
93
+ def singularize(s)
94
+ return unless s # nil
95
+ s.singularize
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,7 @@
1
+ class Jets::Router::MethodCreator
2
+ class Edit < Code
3
+ def meth_name
4
+ join(action, singularize(full_as), singularize(method_name_leaf))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ class Jets::Router::MethodCreator
2
+ class Generic < Code
3
+ def meth_name
4
+ @options[:as]
5
+ end
6
+
7
+ def path_method
8
+ super if @options[:as]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,42 @@
1
+ class Jets::Router::MethodCreator
2
+ class Index < Code
3
+ def meth_name
4
+ # Well this is pretty confusing and tough to follow. TODO: figure out how to improve this.
5
+ #
6
+ # Example 1:
7
+ #
8
+ # resources :users, only: [] do
9
+ # resources :posts, only: :index
10
+ # end
11
+ #
12
+ # Results in:
13
+ #
14
+ # full_as: user_posts
15
+ # method_name_leaf: nil
16
+ #
17
+ # Example 2:
18
+ #
19
+ # resources :users, only: [] do
20
+ # get "posts", to: "posts#index"
21
+ # end
22
+ #
23
+ # Results in:
24
+ #
25
+ # full_as: users
26
+ # method_name_leaf: posts
27
+ #
28
+ # This is because using resources contains all the info we need in parent scopes.
29
+ # The scope.full_as already has the desired meth_name.
30
+ #
31
+ # However, when using the simple create_route methods like get, the parent scope does not contain
32
+ # all the info we need. In this tricky case, the method_name_leaf is set.
33
+ # We then have to reconstruct the meth_name.
34
+ #
35
+ if method_name_leaf
36
+ join(singularize(full_as), method_name_leaf) # reconstruct
37
+ else
38
+ join(full_as) # construct entirely from scope info
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,7 @@
1
+ class Jets::Router::MethodCreator
2
+ class New < Code
3
+ def meth_name
4
+ join(action, singularize(full_as), singularize(method_name_leaf))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ class Jets::Router::MethodCreator
2
+ class Root < Code
3
+ def meth_name
4
+ "root"
5
+ end
6
+
7
+ def meth_args
8
+ nil
9
+ end
10
+
11
+ def meth_result
12
+ nil
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ class Jets::Router::MethodCreator
2
+ class Show < Code
3
+ def meth_name
4
+ join(singularize(full_as), singularize(method_name_leaf))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Jets::Router::Resources
2
+ class Base
3
+ def initialize(name, options)
4
+ @name, @options = name, options
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module Jets::Router::Resources
2
+ class Filter < Base
3
+ def yes?(action)
4
+ return true unless @options[:only] || @options[:except]
5
+
6
+ if @options[:only]
7
+ only = [@options[:only]].flatten.map(&:to_s)
8
+ only.include?(action.to_s)
9
+ else # except
10
+ except = [@options[:except]].flatten.map(&:to_s)
11
+ !except.include?(action.to_s)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ module Jets::Router::Resources
2
+ class Options < Base
3
+ def build(action)
4
+ controller = @options[:singular_resource] ? @name.to_s.pluralize : @name
5
+ options = @options.merge(to: "#{controller}##{action}") # important to create a copy of the options
6
+ # remove special options from getting to create_route. For some reason .slice! doesnt work
7
+ options.delete(:only)
8
+ options.delete(:except)
9
+ options[:from_scope] = true # flag to drop the prefix later in Route#compute_path
10
+ options
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,226 @@
1
+ # route = Jets::Router::Route.new(
2
+ # path: "posts",
3
+ # method: :get,
4
+ # to: "posts#index",
5
+ # )
6
+ class Jets::Router
7
+ class Route
8
+ include Util
9
+
10
+ CAPTURE_REGEX = "([^/]*)" # as string
11
+
12
+ attr_reader :to, :as
13
+ def initialize(options, scope=Scope.new)
14
+ @options, @scope = options, scope
15
+ @path = compute_path
16
+ @to = compute_to
17
+ @as = compute_as
18
+ end
19
+
20
+ def compute_path
21
+ # Note: The @options[:prefix] is missing prefix and is not support via direct create_route.
22
+ # This is because it can be added directly to the path. IE:
23
+ #
24
+ # get "myprefix/posts", to: "posts#index"
25
+ #
26
+ # Also, this helps to keep the method creator logic more simple.
27
+ #
28
+ prefix = @scope.full_prefix
29
+ prefix = account_scope(prefix)
30
+ prefix = account_on(prefix)
31
+
32
+ [prefix, @options[:path]].compact.join('/')
33
+ end
34
+
35
+ def account_scope(prefix)
36
+ return unless prefix
37
+ return prefix unless @options[:from_scope]
38
+
39
+ if @options[:singular_resource]
40
+ prefix.split('/')[0..-2].join('/')
41
+ else
42
+ prefix.split('/')[0..-3].join('/')
43
+ end
44
+ end
45
+
46
+ def account_on(prefix)
47
+ # Tricky @scope.from == :resources since the account_scope already has accounted for it
48
+ if @options[:on] == :collection && @scope.from == :resources
49
+ prefix = prefix.split('/')[0..-2].join('/')
50
+ end
51
+ prefix == '' ? nil : prefix
52
+ end
53
+
54
+ def compute_to
55
+ controller, action = get_controller_action(@options)
56
+ mod = @options[:module] || @scope.full_module
57
+ controller = [mod, controller].compact.join('/') # add module
58
+ "#{controller}##{action}"
59
+ end
60
+
61
+ def compute_as
62
+ return unless @options[:method] == :get || @options[:root]
63
+
64
+ controller, action = get_controller_action(@options)
65
+ klass = if @options[:root]
66
+ Jets::Router::MethodCreator::Root
67
+ elsif %w[index edit show new].include?(action.to_s)
68
+ class_name = "Jets::Router::MethodCreator::#{action.camelize}"
69
+ class_name.constantize # Index, Show, Edit, New
70
+ else
71
+ Jets::Router::MethodCreator::Generic
72
+ end
73
+
74
+ klass.new(@options, @scope, controller).full_meth_name(nil)
75
+ end
76
+
77
+ # IE: standard: posts/:id/edit
78
+ # api_gateway: posts/{id}/edit
79
+ def path(format=:jets)
80
+ case format
81
+ when :api_gateway
82
+ api_gateway_format(@path)
83
+ when :raw
84
+ @path
85
+ else # jets format
86
+ ensure_jets_format(@path)
87
+ end
88
+ end
89
+
90
+ def method
91
+ @options[:method].to_s.upcase
92
+ end
93
+
94
+ def internal?
95
+ !!@options[:internal]
96
+ end
97
+
98
+ def homepage?
99
+ path == ''
100
+ end
101
+
102
+ # IE: PostsController
103
+ def controller_name
104
+ to.sub(/#.*/,'').camelize + "Controller"
105
+ end
106
+
107
+ # IE: index
108
+ def action_name
109
+ to.sub(/.*#/,'')
110
+ end
111
+
112
+ # Checks to see if the corresponding controller exists. Useful to validate routes
113
+ # before deploying to CloudFormation and then rolling back.
114
+ def valid?
115
+ controller_class = begin
116
+ controller_name.constantize
117
+ rescue NameError
118
+ return false
119
+ end
120
+ controller_class.lambda_functions.include?(action_name.to_sym)
121
+ end
122
+
123
+ # Extracts the path parameters from the actual path
124
+ # Only supports extracting 1 parameter. So:
125
+ #
126
+ # actual_path: posts/tung/edit
127
+ # route.path: posts/:id/edit
128
+ #
129
+ # Returns:
130
+ # { id: "tung" }
131
+ def extract_parameters(actual_path)
132
+ if path.include?(':')
133
+ extract_parameters_capture(actual_path)
134
+ elsif path.include?('*')
135
+ extract_parameters_proxy(actual_path)
136
+ else
137
+ # Lambda AWS_PROXY sets null to the input request when there are no path parmeters
138
+ nil
139
+ end
140
+ end
141
+
142
+ def extract_parameters_proxy(actual_path)
143
+ # changes path to a string used for a regexp
144
+ # others/*proxy => others\/(.*)
145
+ # nested/others/*proxy => nested/others\/(.*)
146
+ if path.include?('/')
147
+ leading_path = path.split('/')[0..-2].join('/') # drop last segment
148
+ # leading_path: nested/others
149
+ # capture everything after the leading_path as the value
150
+ regexp = Regexp.new("#{leading_path}/(.*)")
151
+ value = actual_path.match(regexp)[1]
152
+ else
153
+ value = actual_path
154
+ end
155
+
156
+ # the last segment without the '*' is the key
157
+ proxy_segment = path.split('/').last # last segment is the proxy segment
158
+ # proxy_segment: *proxy
159
+ key = proxy_segment.sub('*','')
160
+
161
+ { key => value }
162
+ end
163
+
164
+ def extract_parameters_capture(actual_path)
165
+ # changes path to a string used for a regexp
166
+ # posts/:id/edit => posts\/(.*)\/edit
167
+ labels = []
168
+ regexp_string = path.split('/').map do |s|
169
+ if s.start_with?(':')
170
+ labels << s.delete_prefix(':')
171
+ CAPTURE_REGEX
172
+ else
173
+ s
174
+ end
175
+ end.join('\/')
176
+ # make sure beginning and end of the string matches
177
+ regexp_string = "^#{regexp_string}$"
178
+ regexp = Regexp.new(regexp_string)
179
+
180
+ values = regexp.match(actual_path).captures
181
+ labels.map do |next_label|
182
+ [next_label, values.delete_at(0)]
183
+ end.to_h
184
+ end
185
+
186
+ def authorization_type
187
+ @options[:authorization_type]
188
+ end
189
+
190
+ private
191
+ def ensure_jets_format(path)
192
+ path.split('/').map do |s|
193
+ if s =~ /^\{/ and s =~ /\+\}$/
194
+ s.sub(/^\{/, '*').sub(/\+\}$/,'') # {proxy+} => *proxy
195
+ elsif s =~ /^\{/ and s =~ /\}$/
196
+ s.sub('{',':').sub(/\}$/,'') # {id} => :id
197
+ else
198
+ s
199
+ end
200
+ end.join('/')
201
+ end
202
+
203
+ def api_gateway_format(path)
204
+ path.split('/')
205
+ .map {|s| transform_capture(s) }
206
+ .map {|s| transform_proxy(s) }
207
+ .join('/')
208
+ end
209
+
210
+ def transform_capture(text)
211
+ if text.starts_with?(':')
212
+ text = text.sub(':','')
213
+ text = "{#{text}}"
214
+ end
215
+ text
216
+ end
217
+
218
+ def transform_proxy(text)
219
+ if text.starts_with?('*')
220
+ text = text.sub('*','')
221
+ text = "{#{text}+}"
222
+ end
223
+ text
224
+ end
225
+ end
226
+ end