belt 0.0.7 → 0.1.1
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +29 -1
- data/README.md +150 -51
- data/exe/belt +6 -0
- data/lib/belt/action_router.rb +7 -1
- data/lib/belt/cli/app_detection.rb +16 -0
- data/lib/belt/cli/bucket_security.rb +122 -0
- data/lib/belt/cli/env_resolver.rb +15 -0
- data/lib/belt/cli/environment_command.rb +77 -0
- data/lib/belt/cli/frontend_command.rb +85 -0
- data/lib/belt/cli/frontend_deploy_command.rb +125 -0
- data/lib/belt/cli/frontend_setup_command.rb +64 -0
- data/lib/belt/cli/generate_command.rb +206 -0
- data/lib/belt/cli/new_command.rb +126 -0
- data/lib/belt/cli/routes_command/route_inference.rb +100 -0
- data/lib/belt/cli/routes_command/schema_loader.rb +71 -0
- data/lib/belt/cli/routes_command.rb +307 -0
- data/lib/belt/cli/setup_command.rb +261 -0
- data/lib/belt/cli/tables_command.rb +138 -0
- data/lib/belt/cli/tasks_command.rb +110 -0
- data/lib/belt/cli/terraform_command.rb +77 -0
- data/lib/belt/cli/views_command.rb +134 -0
- data/lib/belt/cli.rb +117 -0
- data/lib/belt/lambda_handler.rb +16 -0
- data/lib/belt/root.rb +26 -0
- data/lib/belt/route_dsl.rb +605 -0
- data/lib/belt/table_inference.rb +71 -0
- data/lib/belt/version.rb +1 -1
- data/lib/belt.rb +1 -0
- data/lib/templates/environment/backend.tf.erb +8 -0
- data/lib/templates/environment/main.tf.erb +42 -0
- data/lib/templates/environment/terraform.tfvars.erb +1 -0
- data/lib/templates/environment/variables.tf.erb +16 -0
- data/lib/templates/frontend/react/index.html.erb +12 -0
- data/lib/templates/frontend/react/package.json.erb +20 -0
- data/lib/templates/frontend/react/src/App.jsx +14 -0
- data/lib/templates/frontend/react/src/index.css +10 -0
- data/lib/templates/frontend/react/src/lib/apiClient.js.erb +19 -0
- data/lib/templates/frontend/react/src/main.jsx +10 -0
- data/lib/templates/frontend/react/src/pages/Home.jsx.erb +10 -0
- data/lib/templates/frontend/react/vite.config.js +8 -0
- data/lib/templates/frontend_infra/frontend.tf.erb +159 -0
- data/lib/templates/generate/controller.rb.erb +59 -0
- data/lib/templates/generate/model.rb.erb +20 -0
- data/lib/templates/new_app/AGENTS.md.erb +130 -0
- data/lib/templates/new_app/Gemfile.erb +5 -0
- data/lib/templates/new_app/README.md.erb +25 -0
- data/lib/templates/new_app/Rakefile.erb +12 -0
- data/lib/templates/new_app/gitignore.erb +14 -0
- data/lib/templates/new_app/infrastructure/routes.tf.rb.erb +5 -0
- data/lib/templates/new_app/infrastructure/schema.tf.rb.erb +9 -0
- data/lib/templates/new_app/lambda/Gemfile.erb +7 -0
- data/lib/templates/new_app/lambda/api.rb.erb +22 -0
- data/lib/templates/new_app/lambda/controllers/application_controller.rb.erb +6 -0
- data/lib/templates/new_app/lambda/lib/routes/routes.rb.erb +11 -0
- data/lib/templates/new_app/lambda/models/application_record.rb.erb +6 -0
- data/lib/templates/new_app/lambda/models/concerns/timestampable.rb.erb +23 -0
- data/lib/templates/views/Edit.jsx.erb +38 -0
- data/lib/templates/views/Form.jsx.erb +34 -0
- data/lib/templates/views/Index.jsx.erb +39 -0
- data/lib/templates/views/New.jsx.erb +26 -0
- data/lib/templates/views/Show.jsx.erb +46 -0
- data.tar.gz.sig +0 -0
- metadata +73 -3
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
# DSL for defining API Gateway routes.
|
|
5
|
+
# Ported from terraform-provider-conveyor-belt/scripts/lib/route_dsl.rb
|
|
6
|
+
# so that `belt routes` can parse routes.tf.rb without external dependencies.
|
|
7
|
+
|
|
8
|
+
class Route
|
|
9
|
+
attr_reader :method, :path, :auth, :lambda, :cors, :tables, :route_type,
|
|
10
|
+
:controller, :action, :request_model, :response_model, :response_context
|
|
11
|
+
|
|
12
|
+
def initialize(method, path, options = {})
|
|
13
|
+
@method = method.to_s.upcase
|
|
14
|
+
@path = normalize_path(path)
|
|
15
|
+
@auth = options[:auth]
|
|
16
|
+
@lambda = options[:lambda]
|
|
17
|
+
@cors = options.fetch(:cors, true)
|
|
18
|
+
@tables = options[:tables] || []
|
|
19
|
+
@route_type = options[:route_type] || :action
|
|
20
|
+
@controller = options[:controller]
|
|
21
|
+
@action = options[:action]
|
|
22
|
+
@request_model = options[:request_model]&.to_s
|
|
23
|
+
@response_model = options[:response_model]&.to_s
|
|
24
|
+
@response_context = options[:response_context]&.to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def resource?
|
|
28
|
+
@route_type == :resource || @route_type == :resources
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def singular_resource?
|
|
32
|
+
@route_type == :resource
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def plural_resource?
|
|
36
|
+
@route_type == :resources
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def action?
|
|
40
|
+
@route_type == :action
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def normalize_path(path)
|
|
46
|
+
path = "/#{path}" unless path.start_with?('/')
|
|
47
|
+
path
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class NestedResourceBuilder
|
|
52
|
+
def initialize(gateway, prefix, collection_prefix, inherited_tables: [], inherited_auth: nil)
|
|
53
|
+
@gateway = gateway
|
|
54
|
+
@prefix = prefix
|
|
55
|
+
@collection_prefix = collection_prefix
|
|
56
|
+
@inherited_tables = inherited_tables
|
|
57
|
+
@inherited_auth = inherited_auth
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def resources(name, options = {})
|
|
61
|
+
resource_name = name.to_s
|
|
62
|
+
singular = @gateway.send(:singularize, resource_name)
|
|
63
|
+
param_name = options[:param] || "#{singular}_id"
|
|
64
|
+
options = merge_inherited_options(options)
|
|
65
|
+
options = @gateway.send(:auto_infer_tables, resource_name, options)
|
|
66
|
+
resource_options = options.merge(route_type: :resources)
|
|
67
|
+
actions = @gateway.send(:determine_actions, options)
|
|
68
|
+
|
|
69
|
+
@gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}", resource_options) if actions.include?(:index)
|
|
70
|
+
@gateway.send(:add_route, :post, "#{@prefix}/#{resource_name}", resource_options) if actions.include?(:create)
|
|
71
|
+
if actions.include?(:show)
|
|
72
|
+
@gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}/{#{param_name}}",
|
|
73
|
+
resource_options)
|
|
74
|
+
end
|
|
75
|
+
if actions.include?(:update)
|
|
76
|
+
@gateway.send(:add_route, :put, "#{@prefix}/#{resource_name}/{#{param_name}}",
|
|
77
|
+
resource_options)
|
|
78
|
+
end
|
|
79
|
+
return unless actions.include?(:destroy)
|
|
80
|
+
|
|
81
|
+
@gateway.send(:add_route, :delete, "#{@prefix}/#{resource_name}/{#{param_name}}",
|
|
82
|
+
resource_options)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def member(&)
|
|
86
|
+
MemberCollectionBuilder.new(@gateway, @prefix, @inherited_tables, @inherited_auth).instance_eval(&)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def collection(&)
|
|
90
|
+
MemberCollectionBuilder.new(@gateway, @collection_prefix, @inherited_tables,
|
|
91
|
+
@inherited_auth).instance_eval(&)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
%i[get post put delete patch].each do |method|
|
|
95
|
+
define_method(method) do |path, options = {}|
|
|
96
|
+
full_path = options[:on] == :collection ? "#{@collection_prefix}#{path}" : "#{@prefix}#{path}"
|
|
97
|
+
options = merge_inherited_options(options)
|
|
98
|
+
route_options = options.except(:on)
|
|
99
|
+
@gateway.send(:add_route, method, full_path, route_options)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def merge_inherited_options(options)
|
|
106
|
+
result = options.dup
|
|
107
|
+
if @inherited_tables.any?
|
|
108
|
+
explicit_tables = Array(result[:tables] || [])
|
|
109
|
+
result[:tables] = (@inherited_tables + explicit_tables).uniq
|
|
110
|
+
end
|
|
111
|
+
result[:auth] ||= @inherited_auth if @inherited_auth
|
|
112
|
+
result
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class MemberCollectionBuilder
|
|
117
|
+
def initialize(gateway, prefix, inherited_tables, inherited_auth)
|
|
118
|
+
@gateway = gateway
|
|
119
|
+
@prefix = prefix
|
|
120
|
+
@inherited_tables = inherited_tables
|
|
121
|
+
@inherited_auth = inherited_auth
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
%i[get post put delete patch].each do |method|
|
|
125
|
+
define_method(method) do |path, options = {}|
|
|
126
|
+
full_path = "#{@prefix}#{path}"
|
|
127
|
+
options = merge_inherited_options(options)
|
|
128
|
+
@gateway.send(:add_route, method, full_path, options)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def merge_inherited_options(options)
|
|
135
|
+
result = options.dup
|
|
136
|
+
if @inherited_tables.any?
|
|
137
|
+
explicit_tables = Array(result[:tables] || [])
|
|
138
|
+
result[:tables] = (@inherited_tables + explicit_tables).uniq
|
|
139
|
+
end
|
|
140
|
+
result[:auth] ||= @inherited_auth if @inherited_auth
|
|
141
|
+
result
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class ApiGateway
|
|
146
|
+
attr_reader :name, :routes, :default_auth, :default_lambda, :default_cors, :default_tables
|
|
147
|
+
|
|
148
|
+
def initialize(name, options = {})
|
|
149
|
+
@name = name.to_s
|
|
150
|
+
@routes = []
|
|
151
|
+
@default_auth = options[:auth] || :cognito
|
|
152
|
+
@default_lambda = options[:lambda] || name
|
|
153
|
+
@default_cors = options.fetch(:cors, true)
|
|
154
|
+
@default_tables = Array(options[:tables] || [])
|
|
155
|
+
@current_lambda_context = nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def lambda(name, &)
|
|
159
|
+
previous_context = @current_lambda_context
|
|
160
|
+
@current_lambda_context = name.to_sym
|
|
161
|
+
instance_eval(&) if block_given?
|
|
162
|
+
@current_lambda_context = previous_context
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
%i[get post put delete patch].each do |method|
|
|
166
|
+
define_method(method) do |path, options = {}|
|
|
167
|
+
add_route(method, path, options)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def resources(name, options = {}, &)
|
|
172
|
+
resource_name = name.to_s
|
|
173
|
+
singular = singularize(resource_name)
|
|
174
|
+
param_name = options[:param] || "#{singular}_id"
|
|
175
|
+
options = auto_infer_tables(resource_name, options)
|
|
176
|
+
resource_options = options.merge(route_type: :resources)
|
|
177
|
+
actions = determine_actions(options)
|
|
178
|
+
|
|
179
|
+
add_route(:get, "/#{resource_name}", resource_options) if actions.include?(:index)
|
|
180
|
+
add_route(:post, "/#{resource_name}", resource_options) if actions.include?(:create)
|
|
181
|
+
add_route(:get, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:show)
|
|
182
|
+
add_route(:put, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:update)
|
|
183
|
+
add_route(:delete, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:destroy)
|
|
184
|
+
|
|
185
|
+
return unless block_given?
|
|
186
|
+
|
|
187
|
+
collection_prefix = "/#{resource_name}"
|
|
188
|
+
member_prefix = "/#{resource_name}/{#{param_name}}"
|
|
189
|
+
resource_tables = Array(options[:tables] || [])
|
|
190
|
+
inherited_tables = (@default_tables + resource_tables).uniq
|
|
191
|
+
inherited_auth = options[:auth] || @default_auth
|
|
192
|
+
nested_builder = NestedResourceBuilder.new(self, member_prefix, collection_prefix,
|
|
193
|
+
inherited_tables: inherited_tables,
|
|
194
|
+
inherited_auth: inherited_auth)
|
|
195
|
+
nested_builder.instance_eval(&)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def resource(name, options = {})
|
|
199
|
+
resource_name = name.to_s
|
|
200
|
+
actions = determine_actions(options, default: %i[show update destroy])
|
|
201
|
+
resource_options = options.merge(route_type: :resource)
|
|
202
|
+
|
|
203
|
+
add_route(:get, "/#{resource_name}", resource_options) if actions.include?(:show)
|
|
204
|
+
add_route(:put, "/#{resource_name}", resource_options) if actions.include?(:update)
|
|
205
|
+
add_route(:delete, "/#{resource_name}", resource_options) if actions.include?(:destroy)
|
|
206
|
+
add_route(:post, "/#{resource_name}", resource_options) if actions.include?(:create)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
def add_route(method, path, options = {})
|
|
212
|
+
lambda_to_use = options[:lambda] || @current_lambda_context || @default_lambda
|
|
213
|
+
route_tables = Array(options[:tables] || [])
|
|
214
|
+
merged_tables = (@default_tables + route_tables).uniq
|
|
215
|
+
|
|
216
|
+
controller = options[:controller]
|
|
217
|
+
action = options[:action]
|
|
218
|
+
if options[:to]
|
|
219
|
+
parts = options[:to].to_s.split('#')
|
|
220
|
+
if parts.length == 2
|
|
221
|
+
controller ||= parts[0]
|
|
222
|
+
action ||= parts[1]
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
route_options = {
|
|
227
|
+
auth: options[:auth] || @default_auth,
|
|
228
|
+
lambda: lambda_to_use,
|
|
229
|
+
cors: options.fetch(:cors, @default_cors),
|
|
230
|
+
tables: merged_tables,
|
|
231
|
+
route_type: options[:route_type] || :action,
|
|
232
|
+
controller: controller,
|
|
233
|
+
action: action,
|
|
234
|
+
request_model: options[:request_model],
|
|
235
|
+
response_model: options[:response_model],
|
|
236
|
+
response_context: options[:response_context]
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@routes << Belt::Route.new(method, path, route_options)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def auto_infer_tables(resource_name, options)
|
|
243
|
+
return options if options.key?(:tables)
|
|
244
|
+
|
|
245
|
+
options.merge(tables: [resource_name.to_sym])
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def determine_actions(options, default: %i[index create show update destroy])
|
|
249
|
+
if options[:only]
|
|
250
|
+
Array(options[:only])
|
|
251
|
+
elsif options[:except]
|
|
252
|
+
default - Array(options[:except])
|
|
253
|
+
else
|
|
254
|
+
default
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def singularize(word)
|
|
259
|
+
if word.end_with?('ies')
|
|
260
|
+
"#{word[0..-4]}y"
|
|
261
|
+
elsif word.end_with?('xes') || word.end_with?('zes') || word.end_with?('ses')
|
|
262
|
+
word[0..-3]
|
|
263
|
+
elsif word.end_with?('ches') || word.end_with?('shes')
|
|
264
|
+
word[0..-3]
|
|
265
|
+
elsif word.end_with?('s') && !word.end_with?('ss')
|
|
266
|
+
word[0..-2]
|
|
267
|
+
else
|
|
268
|
+
word
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Application object providing Rails-style `Belt.application.routes.draw` DSL.
|
|
274
|
+
class Application
|
|
275
|
+
class Routes
|
|
276
|
+
attr_reader :dsl
|
|
277
|
+
|
|
278
|
+
def initialize
|
|
279
|
+
@dsl = RouteDSL.new
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def draw(&)
|
|
283
|
+
instance_eval(&) if block_given?
|
|
284
|
+
@dsl
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def namespace(name, options = {}, &)
|
|
288
|
+
gateway = Belt::ApiGateway.new(name, options)
|
|
289
|
+
RouteBuilder.new(gateway).instance_eval(&) if block_given?
|
|
290
|
+
@dsl.api_gateways << gateway
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def routes
|
|
295
|
+
Routes.new
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def schema
|
|
299
|
+
@schema ||= SchemaBuilder.new
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
class RouteBuilder
|
|
303
|
+
def initialize(gateway)
|
|
304
|
+
@gateway = gateway
|
|
305
|
+
@scope_prefix = ''
|
|
306
|
+
@scope_module = nil
|
|
307
|
+
@scope_auth = nil
|
|
308
|
+
@scope_tables = []
|
|
309
|
+
@scope_controller = nil
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def scope(options = {}, &)
|
|
313
|
+
previous_prefix = @scope_prefix
|
|
314
|
+
previous_module = @scope_module
|
|
315
|
+
previous_auth = @scope_auth
|
|
316
|
+
previous_tables = @scope_tables
|
|
317
|
+
previous_controller = @scope_controller
|
|
318
|
+
|
|
319
|
+
@scope_prefix = options[:path] || @scope_prefix
|
|
320
|
+
@scope_module = options[:module] || @scope_module
|
|
321
|
+
@scope_auth = options[:auth] || @scope_auth
|
|
322
|
+
@scope_tables = (@scope_tables + Array(options[:tables] || [])).uniq
|
|
323
|
+
@scope_controller = options[:controller] || @scope_controller
|
|
324
|
+
|
|
325
|
+
instance_eval(&) if block_given?
|
|
326
|
+
|
|
327
|
+
@scope_prefix = previous_prefix
|
|
328
|
+
@scope_module = previous_module
|
|
329
|
+
@scope_auth = previous_auth
|
|
330
|
+
@scope_tables = previous_tables
|
|
331
|
+
@scope_controller = previous_controller
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
%i[get post put delete patch].each do |method|
|
|
335
|
+
define_method(method) do |path, options = {}|
|
|
336
|
+
full_path = build_path(path)
|
|
337
|
+
route_options = options.dup
|
|
338
|
+
route_options[:lambda] ||= @scope_module if @scope_module
|
|
339
|
+
route_options[:auth] ||= @scope_auth if @scope_auth
|
|
340
|
+
route_options[:controller] ||= @scope_controller if @scope_controller
|
|
341
|
+
if @scope_tables.any? || route_options[:tables]
|
|
342
|
+
route_options[:tables] =
|
|
343
|
+
(@scope_tables + Array(route_options[:tables] || [])).uniq
|
|
344
|
+
end
|
|
345
|
+
@gateway.send(method, full_path, route_options)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def resources(name, options = {}, &)
|
|
350
|
+
options = apply_scope_options(options)
|
|
351
|
+
@gateway.resources(name, options, &)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def resource(name, options = {})
|
|
355
|
+
options = apply_scope_options(options)
|
|
356
|
+
@gateway.resource(name, options)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def lambda(name, &)
|
|
360
|
+
name
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def mount(mountable, options = {})
|
|
364
|
+
prefix = options[:at]&.to_s&.gsub(%r{^/|/$}, '') || ''
|
|
365
|
+
extra_tables = Array(options[:tables] || [])
|
|
366
|
+
auth_override = options[:auth]
|
|
367
|
+
route_definitions = mountable.respond_to?(:routes) ? mountable.routes : []
|
|
368
|
+
|
|
369
|
+
route_definitions.each do |route_def|
|
|
370
|
+
mount_route(route_def, prefix, extra_tables, auth_override)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def mount_route(route_def, prefix, extra_tables, auth_override)
|
|
375
|
+
method = route_def[:method].to_sym
|
|
376
|
+
path = route_def[:path].to_s.gsub(/:([a-zA-Z_]\w*)/) { "{#{::Regexp.last_match(1)}}" }
|
|
377
|
+
full_path = mount_full_path(path, prefix)
|
|
378
|
+
route_options = mount_route_options(route_def, path, prefix, extra_tables, auth_override)
|
|
379
|
+
|
|
380
|
+
@gateway.send(method, full_path, route_options)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def mount_full_path(path, prefix)
|
|
384
|
+
full_path = prefix.empty? ? path : "/#{prefix}#{path}"
|
|
385
|
+
full_path = full_path.chomp('/') unless full_path == '/'
|
|
386
|
+
build_path(full_path)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def mount_route_options(route_def, path, prefix, extra_tables, auth_override)
|
|
390
|
+
route_options = (route_def[:options] || {}).dup
|
|
391
|
+
route_options[:tables] = (extra_tables + Array(route_options[:tables] || [])).uniq
|
|
392
|
+
route_options[:auth] = auth_override if auth_override
|
|
393
|
+
route_options[:auth] ||= @scope_auth if @scope_auth
|
|
394
|
+
route_options[:tables] = (@scope_tables + route_options[:tables]).uniq if @scope_tables.any?
|
|
395
|
+
route_options[:controller] ||= prefix.gsub('-', '_') unless prefix.empty?
|
|
396
|
+
stripped = path.gsub(%r{^/|/$}, '')
|
|
397
|
+
route_options[:action] ||= stripped.empty? ? 'index' : stripped.gsub('-', '_')
|
|
398
|
+
route_options
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
private
|
|
402
|
+
|
|
403
|
+
def build_path(path)
|
|
404
|
+
@scope_prefix.empty? ? path : "/#{@scope_prefix}#{path}"
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def apply_scope_options(options)
|
|
408
|
+
result = options.dup
|
|
409
|
+
result[:auth] ||= @scope_auth if @scope_auth
|
|
410
|
+
result[:lambda] ||= @scope_module if @scope_module
|
|
411
|
+
result[:tables] = (@scope_tables + Array(result[:tables] || [])).uniq if @scope_tables.any? || result[:tables]
|
|
412
|
+
result
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
class << self
|
|
418
|
+
def application
|
|
419
|
+
@application ||= Application.new
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Minimal RouteDSL for legacy api_gateway style
|
|
424
|
+
class RouteDSL
|
|
425
|
+
attr_reader :api_gateways
|
|
426
|
+
|
|
427
|
+
def initialize
|
|
428
|
+
@api_gateways = []
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def api_gateway(name, options = {}, &)
|
|
432
|
+
gateway = Belt::ApiGateway.new(name, options)
|
|
433
|
+
gateway.instance_eval(&) if block_given?
|
|
434
|
+
@api_gateways << gateway
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def self.load_from_file(filename)
|
|
438
|
+
dsl = new
|
|
439
|
+
dsl.instance_eval(File.read(filename), filename)
|
|
440
|
+
dsl
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# SchemaBuilder captures request and response model definitions from schema.tf.rb
|
|
445
|
+
class SchemaBuilder
|
|
446
|
+
SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
|
|
447
|
+
|
|
448
|
+
attr_reader :request_models, :response_models
|
|
449
|
+
|
|
450
|
+
def initialize
|
|
451
|
+
@request_models = {}
|
|
452
|
+
@response_models = {}
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def define(&)
|
|
456
|
+
instance_eval(&) if block_given?
|
|
457
|
+
self
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
alias draw define
|
|
461
|
+
|
|
462
|
+
def request(name, &)
|
|
463
|
+
builder = RequestModelBuilder.new(name)
|
|
464
|
+
builder.instance_eval(&) if block_given?
|
|
465
|
+
@request_models[name] = builder
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def model(name, &)
|
|
469
|
+
builder = ResponseModelBuilder.new(name)
|
|
470
|
+
builder.instance_eval(&) if block_given?
|
|
471
|
+
@response_models[name] = builder
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def to_h
|
|
475
|
+
{
|
|
476
|
+
request_models: @request_models.transform_values(&:to_h),
|
|
477
|
+
response_models: @response_models.transform_values(&:to_h)
|
|
478
|
+
}
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
class RequestModelBuilder
|
|
483
|
+
SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
|
|
484
|
+
|
|
485
|
+
attr_reader :name, :fields
|
|
486
|
+
|
|
487
|
+
def initialize(name)
|
|
488
|
+
@name = name
|
|
489
|
+
@fields = []
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
SUPPORTED_TYPES.each do |type|
|
|
493
|
+
define_method(type) do |field_name, options = {}|
|
|
494
|
+
@fields << { name: field_name, type: type, required: options[:required] == true }
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def to_h
|
|
499
|
+
{
|
|
500
|
+
name: @name.to_s,
|
|
501
|
+
properties: fields_to_properties,
|
|
502
|
+
required: @fields.select { |f| f[:required] }.map { |f| f[:name].to_s }
|
|
503
|
+
}
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
private
|
|
507
|
+
|
|
508
|
+
def fields_to_properties
|
|
509
|
+
@fields.to_h do |field|
|
|
510
|
+
[field[:name].to_s, { type: map_type(field[:type]) }]
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def map_type(dsl_type)
|
|
515
|
+
case dsl_type
|
|
516
|
+
when :map then 'object'
|
|
517
|
+
when :list then 'array'
|
|
518
|
+
else dsl_type.to_s
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
class ResponseModelBuilder
|
|
524
|
+
SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
|
|
525
|
+
|
|
526
|
+
attr_reader :name, :contexts, :fields
|
|
527
|
+
|
|
528
|
+
def initialize(name)
|
|
529
|
+
@name = name
|
|
530
|
+
@contexts = {}
|
|
531
|
+
@fields = []
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
SUPPORTED_TYPES.each do |type|
|
|
535
|
+
define_method(type) do |field_name, _options = {}|
|
|
536
|
+
@fields << { name: field_name, type: type }
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def context(name, &)
|
|
541
|
+
builder = ContextBuilder.new(name)
|
|
542
|
+
builder.instance_eval(&) if block_given?
|
|
543
|
+
@contexts[name] = builder
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def to_h
|
|
547
|
+
result = { name: @name.to_s, contexts: @contexts.transform_values(&:to_h) }
|
|
548
|
+
result[:properties] = fields_to_properties unless @fields.empty?
|
|
549
|
+
result
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
private
|
|
553
|
+
|
|
554
|
+
def fields_to_properties
|
|
555
|
+
@fields.to_h do |field|
|
|
556
|
+
[field[:name].to_s, { type: map_type(field[:type]) }]
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def map_type(dsl_type)
|
|
561
|
+
case dsl_type
|
|
562
|
+
when :map then 'object'
|
|
563
|
+
when :list then 'array'
|
|
564
|
+
else dsl_type.to_s
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
class ContextBuilder
|
|
570
|
+
SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
|
|
571
|
+
|
|
572
|
+
attr_reader :name, :fields
|
|
573
|
+
|
|
574
|
+
def initialize(name)
|
|
575
|
+
@name = name
|
|
576
|
+
@fields = []
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
SUPPORTED_TYPES.each do |type|
|
|
580
|
+
define_method(type) do |field_name, _options = {}|
|
|
581
|
+
@fields << { name: field_name, type: type }
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def to_h
|
|
586
|
+
{ name: @name.to_s, properties: fields_to_properties }
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
private
|
|
590
|
+
|
|
591
|
+
def fields_to_properties
|
|
592
|
+
@fields.to_h do |field|
|
|
593
|
+
[field[:name].to_s, { type: map_type(field[:type]) }]
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def map_type(dsl_type)
|
|
598
|
+
case dsl_type
|
|
599
|
+
when :map then 'object'
|
|
600
|
+
when :list then 'array'
|
|
601
|
+
else dsl_type.to_s
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
# Infers DynamoDB table names from route paths by matching against
|
|
5
|
+
# tables defined in a Terraform file containing aws_dynamodb_table resources.
|
|
6
|
+
class TableInference
|
|
7
|
+
attr_reader :available_tables
|
|
8
|
+
|
|
9
|
+
def initialize(dynamodb_tables_file)
|
|
10
|
+
@available_tables = if dynamodb_tables_file && File.exist?(dynamodb_tables_file)
|
|
11
|
+
parse_available_tables(dynamodb_tables_file)
|
|
12
|
+
else
|
|
13
|
+
[]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def infer_tables_from_route(route)
|
|
18
|
+
path_segments = route.path.split('/').reject(&:empty?)
|
|
19
|
+
return [] if path_segments.empty?
|
|
20
|
+
|
|
21
|
+
resource_segment = path_segments.find { |seg| !seg.start_with?('{') }
|
|
22
|
+
return [] unless resource_segment
|
|
23
|
+
|
|
24
|
+
inferred = find_matching_table(resource_segment)
|
|
25
|
+
inferred ? [inferred] : []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def parse_available_tables(file_path)
|
|
31
|
+
content = File.read(file_path)
|
|
32
|
+
content.scan(/resource\s+"aws_dynamodb_table"\s+"(\w+)"\s*\{/).flatten
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def find_matching_table(resource_name)
|
|
36
|
+
return resource_name if @available_tables.include?(resource_name)
|
|
37
|
+
|
|
38
|
+
plural = pluralize(resource_name)
|
|
39
|
+
return plural if @available_tables.include?(plural)
|
|
40
|
+
|
|
41
|
+
singular = singularize(resource_name)
|
|
42
|
+
return singular if @available_tables.include?(singular)
|
|
43
|
+
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def pluralize(word)
|
|
48
|
+
if word.end_with?('y')
|
|
49
|
+
"#{word[0..-2]}ies"
|
|
50
|
+
elsif word.end_with?('s', 'x', 'z', 'ch', 'sh')
|
|
51
|
+
"#{word}es"
|
|
52
|
+
else
|
|
53
|
+
"#{word}s"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def singularize(word)
|
|
58
|
+
if word.end_with?('ies')
|
|
59
|
+
"#{word[0..-4]}y"
|
|
60
|
+
elsif word.end_with?('xes', 'zes', 'ses')
|
|
61
|
+
word[0..-3]
|
|
62
|
+
elsif word.end_with?('ches', 'shes')
|
|
63
|
+
word[0..-3]
|
|
64
|
+
elsif word.end_with?('s') && !word.end_with?('ss')
|
|
65
|
+
word[0..-2]
|
|
66
|
+
else
|
|
67
|
+
word
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/belt/version.rb
CHANGED
data/lib/belt.rb
CHANGED