belt 0.1.0 → 0.1.2

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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.2'
5
5
  end
data/lib/belt.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'belt/version'
4
+ require_relative 'belt/root'
4
5
  require_relative 'belt/parameters'
5
6
  require_relative 'belt/observability'
6
7
  require_relative 'belt/lambda_handler'
@@ -51,7 +51,7 @@ belt output <env> # terraform output
51
51
 
52
52
  1. `infrastructure/routes.tf.rb` defines routes using a DSL:
53
53
  ```ruby
54
- TerraDispatch.routes.draw do
54
+ Belt.application.routes.draw do
55
55
  namespace :<%= @app_name %> do
56
56
  resources :things, tables: [:things]
57
57
  end
@@ -4,3 +4,4 @@ source 'https://rubygems.org'
4
4
 
5
5
  gem 'activeitem'
6
6
  gem 'belt'
7
+ gem 'lambda_loadout'