jets 1.9.32 → 2.0.0

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