belt 0.1.0 → 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/lib/belt/cli/new_command.rb +1 -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/tables_command.rb +2 -2
- data/lib/belt/cli/tasks_command.rb +110 -0
- data/lib/belt/cli.rb +24 -5
- 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/new_app/AGENTS.md.erb +1 -1
- data/lib/templates/new_app/Gemfile.erb +0 -1
- data/lib/templates/new_app/Rakefile.erb +12 -0
- data/lib/templates/new_app/infrastructure/routes.tf.rb.erb +1 -1
- data/lib/templates/new_app/infrastructure/schema.tf.rb.erb +1 -1
- data.tar.gz.sig +0 -0
- metadata +23 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9236fac00b7efcb43f17c02fb347626f768cf7dd5d6c1d2162470176043cdfde
|
|
4
|
+
data.tar.gz: d7998c44bfcdfa42eb4d22acca3fabf62e0005753b355f4112bbdff108682527
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ec40527e1d14fe89e8e5f3dc412e803354630126d40e9bed1a8c1dc32703ecd5a1fbffec1690ff0262ee2d1ece1895751441fc554f2cc38b86cb1d36e8b4ea8
|
|
7
|
+
data.tar.gz: 3eaff9879994c9d2b7c3eb6b454a833bc4ae433bd544ac0c8fa3578045de96263a2871915b60d8707eaed8b512e497c88125554b45068d597d19d612a6b87209
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### `belt routes` CLI command
|
|
6
|
+
|
|
7
|
+
- Added `belt routes` — displays all routes defined in `infrastructure/routes.tf.rb`
|
|
8
|
+
- Concise table output with VERB, PATH, CONTROLLER#ACTION (shows GATEWAY/LAMBDA columns when multiple namespaces exist)
|
|
9
|
+
- JSON output via `--format json` includes routes array and optional schema models
|
|
10
|
+
- Filter routes with `--grep PATTERN` (case-insensitive, matches verb, path, gateway, lambda, controller, or action)
|
|
11
|
+
- Generate Ruby route manifest files with `--namespace NAMESPACE` (or `all` for every gateway/lambda)
|
|
12
|
+
- `--output-dir DIR` controls where generated files are written (warns if used without `--namespace`)
|
|
13
|
+
- Added `Belt.root` — project root detection by walking up to find `infrastructure/routes.tf.rb`, with fallback to `pwd`
|
|
14
|
+
- Default output directory for generated routes: `#{Belt.root}/lambda/lib/routes`
|
|
15
|
+
|
|
16
|
+
### `belt tasks` CLI command
|
|
17
|
+
|
|
18
|
+
- Added `belt tasks` — lists available rake tasks from the project's Rakefile
|
|
19
|
+
- Filter tasks with `--grep PATTERN`
|
|
20
|
+
- Show all tasks (including undescribed) with `--all`
|
|
21
|
+
- Run rake tasks directly: `belt lambda:build_layer` invokes `bundle exec rake lambda:build_layer`
|
|
22
|
+
|
|
23
|
+
### Other changes
|
|
24
|
+
|
|
25
|
+
- Added `Belt::RouteDSL` — full route DSL parser (resources, nested resources, scopes, mounts, schemas)
|
|
26
|
+
- Added `Belt::TableInference` — infers DynamoDB table access from Terraform definitions
|
|
27
|
+
- Renamed `TerraDispatch` references to `Belt` in templates and DSL entry points
|
|
28
|
+
- Removed `activeitem` dependency from generated Gemfile template
|
|
29
|
+
- Added Rakefile template to `belt new` scaffolding
|
|
30
|
+
|
|
3
31
|
## 0.0.7
|
|
4
32
|
|
|
5
33
|
- Fixed `discover_gem_paths` to use `Gem.loaded_specs` instead of `Gem::Specification.each` — the latter silently returns nothing on Lambda's vendored bundle layout, causing gem controllers/models to not be found
|
|
@@ -26,4 +54,4 @@
|
|
|
26
54
|
- Added `BeltController::Base` with callbacks, strong params, CORS, error handling
|
|
27
55
|
- Added `ActionController::Parameters` (strong params without Rails)
|
|
28
56
|
- Added response helpers and CORS origin resolution
|
|
29
|
-
- Bundled dependencies: activeitem, lambda_loadout
|
|
57
|
+
- Bundled dependencies: activeitem, lambda_loadout
|
data/README.md
CHANGED
|
@@ -9,7 +9,6 @@ Belt bundles everything you need to go from zero to production:
|
|
|
9
9
|
- **Belt::ActionRouter** — request routing to controllers from route manifests
|
|
10
10
|
- **ActiveItem** — DynamoDB ORM (queries, validations, associations, transactions)
|
|
11
11
|
- **Lambda Loadout** — structured logging, CloudWatch metrics (EMF), error alerting
|
|
12
|
-
- **S3arch** — full-text search via SQLite FTS5, stored on S3, queried from Lambda
|
|
13
12
|
|
|
14
13
|
## Installation
|
|
15
14
|
|
|
@@ -129,7 +128,7 @@ terraform {
|
|
|
129
128
|
Define routes in `infrastructure/routes.tf.rb`:
|
|
130
129
|
|
|
131
130
|
```ruby
|
|
132
|
-
|
|
131
|
+
Belt.application.routes.draw do
|
|
133
132
|
namespace :api do
|
|
134
133
|
resources :posts, only: [:index, :show, :create]
|
|
135
134
|
end
|
|
@@ -139,7 +138,7 @@ end
|
|
|
139
138
|
Define tables in `infrastructure/schema.tf.rb`:
|
|
140
139
|
|
|
141
140
|
```ruby
|
|
142
|
-
|
|
141
|
+
Belt.application.schema.define do
|
|
143
142
|
model :post do
|
|
144
143
|
partition_key :id, :string
|
|
145
144
|
global_secondary_index :UserIndex, partition_key: :user_id
|
|
@@ -201,80 +200,180 @@ error_response("Not found", 404) # 404 JSON error
|
|
|
201
200
|
html_response("<h1>Hello</h1>") # 200 HTML with CORS
|
|
202
201
|
```
|
|
203
202
|
|
|
204
|
-
##
|
|
203
|
+
## Controller Discovery
|
|
205
204
|
|
|
206
|
-
|
|
205
|
+
Belt discovers controllers from the app's namespace module first, then searches `Belt.all_controller_paths` — which includes app-defined paths. No registration needed.
|
|
207
206
|
|
|
208
|
-
|
|
207
|
+
## Belt::Observability
|
|
209
208
|
|
|
210
|
-
|
|
209
|
+
Belt provides global `Belt::Observability::Logger` and `Belt::Observability::Metrics` facades that are set automatically by `Belt::LambdaHandler`. Access them from anywhere:
|
|
211
210
|
|
|
212
211
|
```ruby
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
Belt::Observability::Logger.info("Something happened", user_id: "123")
|
|
213
|
+
Belt::Observability::Metrics.track_event("OrderCreated", model: "Order")
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Environment Variables
|
|
217
|
+
|
|
218
|
+
| Variable | Purpose |
|
|
219
|
+
|----------|---------|
|
|
220
|
+
| `ENVIRONMENT` | Controls verbose error responses (`dev*`, `local`, `test`) |
|
|
221
|
+
| `BELT_METRICS_NAMESPACE` | CloudWatch metrics namespace (default: `Belt`) |
|
|
222
|
+
| `ACTION` | Service name for logging (falls back to function name) |
|
|
223
|
+
| `ERROR_NOTIFICATION_TOPIC_ARN` | SNS topic for error alerts |
|
|
224
|
+
| `CORS_ALLOWED_ORIGINS` | Comma-separated origins (overrides domain vars) |
|
|
225
|
+
| `CUSTOMER_APP_DOMAIN` | Primary app domain for CORS |
|
|
226
|
+
| `OPS_APP_DOMAIN` | Internal tools domain for CORS |
|
|
227
|
+
|
|
228
|
+
## CLI
|
|
229
|
+
|
|
230
|
+
Belt includes a command-line interface for project management.
|
|
231
|
+
|
|
232
|
+
### `belt routes`
|
|
233
|
+
|
|
234
|
+
Display route definitions from your `infrastructure/routes.tf.rb`. This is the primary way to inspect what endpoints your app exposes.
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
belt routes
|
|
218
238
|
```
|
|
219
239
|
|
|
220
|
-
|
|
240
|
+
Output (single namespace):
|
|
221
241
|
|
|
222
242
|
```
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
└── models/ # Holster's models
|
|
243
|
+
VERB PATH CONTROLLER#ACTION
|
|
244
|
+
------------------------------------------------------------------
|
|
245
|
+
GET /posts posts#index
|
|
246
|
+
GET /posts/{post_id} posts#show
|
|
247
|
+
POST /posts posts#create
|
|
248
|
+
DELETE /posts/{post_id} posts#destroy
|
|
230
249
|
```
|
|
231
250
|
|
|
232
|
-
|
|
251
|
+
When multiple namespaces (API Gateways) exist, GATEWAY and LAMBDA columns are added automatically:
|
|
233
252
|
|
|
234
|
-
|
|
253
|
+
```
|
|
254
|
+
VERB PATH GATEWAY LAMBDA CONTROLLER#ACTION
|
|
255
|
+
---------------------------------------------------------------
|
|
256
|
+
GET /posts blog blog posts#index
|
|
257
|
+
POST /posts blog blog posts#create
|
|
258
|
+
GET /posts ops ops posts#index
|
|
259
|
+
POST /posts ops ops posts#create
|
|
260
|
+
```
|
|
235
261
|
|
|
236
|
-
|
|
262
|
+
#### Options
|
|
237
263
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
264
|
+
| Flag | Description |
|
|
265
|
+
|------|-------------|
|
|
266
|
+
| `-g, --grep PATTERN` | Filter routes matching pattern (case-insensitive, matches verb, path, gateway, lambda, controller, or action) |
|
|
267
|
+
| `-f, --format FORMAT` | Output format: `concise` (default) or `json` |
|
|
268
|
+
| `--namespace NAMESPACE` | Generate Ruby route files for NAMESPACE (or "all") |
|
|
269
|
+
| `--output-dir DIR` | Output directory for generated Ruby files (default: `lambda/lib/routes/`) |
|
|
270
|
+
| `--schema FILE` | Path to `schema.tf.rb` for model definitions (default: same directory as routes file) |
|
|
271
|
+
| `--tables-file FILE` | Path to Terraform file with `aws_dynamodb_table` resources for table inference |
|
|
272
|
+
| `-h, --help` | Show help |
|
|
273
|
+
|
|
274
|
+
#### Examples
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# Filter routes by pattern
|
|
278
|
+
belt routes -g posts
|
|
279
|
+
|
|
280
|
+
# JSON output (for tooling/CI)
|
|
281
|
+
belt routes -f json
|
|
282
|
+
|
|
283
|
+
# Generate Ruby route constant for the "api" namespace
|
|
284
|
+
belt routes --namespace api
|
|
285
|
+
|
|
286
|
+
# Generate to a custom directory
|
|
287
|
+
belt routes --namespace api --output-dir lib/routes
|
|
288
|
+
|
|
289
|
+
# Include schema models in JSON output
|
|
290
|
+
belt routes -f json --schema infrastructure/schema.tf.rb
|
|
291
|
+
|
|
292
|
+
# Infer DynamoDB table access from Terraform
|
|
293
|
+
belt routes -f json --tables-file infrastructure/main.tf
|
|
245
294
|
```
|
|
246
295
|
|
|
247
|
-
|
|
296
|
+
#### JSON Output
|
|
297
|
+
|
|
298
|
+
With `--format json`, the output includes a `routes` array and optionally a `models` array (when a schema file is found):
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"routes": [
|
|
303
|
+
{
|
|
304
|
+
"name": "posts",
|
|
305
|
+
"verb": "GET",
|
|
306
|
+
"path": "/posts",
|
|
307
|
+
"gateway": "api",
|
|
308
|
+
"lambda": "api",
|
|
309
|
+
"controller": "posts",
|
|
310
|
+
"action": "index",
|
|
311
|
+
"auth": "cognito",
|
|
312
|
+
"tables": ["posts"],
|
|
313
|
+
"request_model": "",
|
|
314
|
+
"response_model": ""
|
|
315
|
+
}
|
|
316
|
+
],
|
|
317
|
+
"models": [
|
|
318
|
+
{
|
|
319
|
+
"name": "CreatePost",
|
|
320
|
+
"kind": "request",
|
|
321
|
+
"description": "Request model: CreatePost",
|
|
322
|
+
"properties": {
|
|
323
|
+
"title": { "type": "string" },
|
|
324
|
+
"body": { "type": "string" }
|
|
325
|
+
},
|
|
326
|
+
"required": ["title"]
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
```
|
|
248
331
|
|
|
249
|
-
|
|
250
|
-
- **Routes**: `Belt.all_routes_paths` collects all holster `routes.tf.rb` files for the Terraform provider
|
|
251
|
-
- **Schema**: `Belt.all_schema_paths` collects all holster `schema.tf.rb` files for the Terraform provider
|
|
252
|
-
- **Models**: `Belt.all_models_paths` collects all holster model directories
|
|
332
|
+
#### Ruby Output
|
|
253
333
|
|
|
254
|
-
|
|
334
|
+
With `--namespace NAMESPACE`, Belt generates a frozen Ruby constant file at `lambda/lib/routes/<namespace>_routes.rb`:
|
|
255
335
|
|
|
256
|
-
|
|
336
|
+
```ruby
|
|
337
|
+
# frozen_string_literal: true
|
|
338
|
+
|
|
339
|
+
# Auto-generated by: belt routes --namespace api
|
|
340
|
+
# Do not edit manually
|
|
341
|
+
|
|
342
|
+
module Routes
|
|
343
|
+
API = [
|
|
344
|
+
{
|
|
345
|
+
verb: "GET",
|
|
346
|
+
path: "/posts",
|
|
347
|
+
gateway: "api",
|
|
348
|
+
lambda: "api",
|
|
349
|
+
controller: "posts",
|
|
350
|
+
action: "index",
|
|
351
|
+
auth: "cognito",
|
|
352
|
+
tables: ["posts"]
|
|
353
|
+
}
|
|
354
|
+
].freeze
|
|
355
|
+
end
|
|
356
|
+
```
|
|
257
357
|
|
|
258
|
-
|
|
358
|
+
This is used by `Belt::ActionRouter` at runtime for request routing.
|
|
259
359
|
|
|
260
|
-
|
|
360
|
+
#### Route File Location
|
|
361
|
+
|
|
362
|
+
The command expects `infrastructure/routes.tf.rb` in the current working directory. Routes are defined using the same DSL as the Belt Terraform provider:
|
|
261
363
|
|
|
262
364
|
```ruby
|
|
263
|
-
Belt
|
|
264
|
-
|
|
365
|
+
Belt.application.routes.draw do
|
|
366
|
+
namespace :api do
|
|
367
|
+
resources :posts, only: [:index, :show, :create, :destroy]
|
|
368
|
+
resource :profile, only: [:show, :update]
|
|
369
|
+
get "health", action: :health
|
|
370
|
+
end
|
|
371
|
+
end
|
|
265
372
|
```
|
|
266
373
|
|
|
267
|
-
|
|
374
|
+
#### Table Inference
|
|
268
375
|
|
|
269
|
-
|
|
270
|
-
|----------|---------|
|
|
271
|
-
| `ENVIRONMENT` | Controls verbose error responses (`dev*`, `local`, `test`) |
|
|
272
|
-
| `BELT_METRICS_NAMESPACE` | CloudWatch metrics namespace (default: `Belt`) |
|
|
273
|
-
| `ACTION` | Service name for logging (falls back to function name) |
|
|
274
|
-
| `ERROR_NOTIFICATION_TOPIC_ARN` | SNS topic for error alerts |
|
|
275
|
-
| `CORS_ALLOWED_ORIGINS` | Comma-separated origins (overrides domain vars) |
|
|
276
|
-
| `CUSTOMER_APP_DOMAIN` | Primary app domain for CORS |
|
|
277
|
-
| `OPS_APP_DOMAIN` | Internal tools domain for CORS |
|
|
376
|
+
When `--tables-file` is provided, Belt parses `aws_dynamodb_table` resource blocks from your Terraform files and infers which tables each route accesses based on the resource name in the route path. Routes can also declare tables explicitly in the DSL via `tables: [:posts, :comments]`.
|
|
278
377
|
|
|
279
378
|
## License
|
|
280
379
|
|
data/lib/belt/cli/new_command.rb
CHANGED
|
@@ -79,6 +79,7 @@ module Belt
|
|
|
79
79
|
def files
|
|
80
80
|
{
|
|
81
81
|
'Gemfile.erb' => "#{@app_name}/Gemfile",
|
|
82
|
+
'Rakefile.erb' => "#{@app_name}/Rakefile",
|
|
82
83
|
'lambda/Gemfile.erb' => "#{@app_name}/lambda/Gemfile",
|
|
83
84
|
'lambda/api.rb.erb' => "#{@app_name}/lambda/#{@app_name}.rb",
|
|
84
85
|
'lambda/models/application_record.rb.erb' => "#{@app_name}/lambda/models/application_record.rb",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module CLI
|
|
5
|
+
class RoutesCommand
|
|
6
|
+
# Extracts route controller/action inference logic from RoutesCommand.
|
|
7
|
+
module RouteInference
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def infer_controller(route, gateway)
|
|
11
|
+
return route.controller.to_s if route.controller
|
|
12
|
+
|
|
13
|
+
segments = route.path.split('/').reject(&:empty?)
|
|
14
|
+
non_param = segments.reject { |s| s.start_with?(':', '{') }
|
|
15
|
+
return gateway.name if non_param.empty?
|
|
16
|
+
|
|
17
|
+
return non_param.map { |s| s.gsub('-', '_') }.join('/') if route.resource? && nested_resource?(segments)
|
|
18
|
+
|
|
19
|
+
if route.resource?
|
|
20
|
+
non_param.first.gsub('-', '_')
|
|
21
|
+
elsif non_param.length == 1 && segments.length == 1
|
|
22
|
+
route.lambda.to_s == gateway.name.to_s ? gateway.name.to_s : route.lambda.to_s
|
|
23
|
+
else
|
|
24
|
+
non_param.first.gsub('-', '_')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def infer_action(route, _gateway)
|
|
29
|
+
return route.action.to_s if route.action
|
|
30
|
+
|
|
31
|
+
segments = route.path.split('/').reject(&:empty?)
|
|
32
|
+
verb = route.method
|
|
33
|
+
|
|
34
|
+
if route.singular_resource?
|
|
35
|
+
infer_singular_resource_action(verb)
|
|
36
|
+
elsif route.plural_resource?
|
|
37
|
+
infer_plural_resource_action(verb, segments)
|
|
38
|
+
else
|
|
39
|
+
infer_plain_action(verb, segments)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def infer_singular_resource_action(verb)
|
|
44
|
+
case verb
|
|
45
|
+
when 'GET' then 'show'
|
|
46
|
+
when 'PUT', 'PATCH' then 'update'
|
|
47
|
+
when 'DELETE' then 'destroy'
|
|
48
|
+
when 'POST' then 'create'
|
|
49
|
+
else 'show'
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def infer_plural_resource_action(verb, segments)
|
|
54
|
+
has_id = segments.any? { |s| s.start_with?(':', '{') }
|
|
55
|
+
last_is_param = segments.last&.start_with?(':', '{')
|
|
56
|
+
|
|
57
|
+
if nested_resource?(segments)
|
|
58
|
+
child_idx = segments.rindex { |s| !s.start_with?(':', '{') }
|
|
59
|
+
has_child_id = child_idx && segments[(child_idx + 1)..]&.any? { |s| s.start_with?(':', '{') }
|
|
60
|
+
restful_action(verb, has_child_id || false)
|
|
61
|
+
else
|
|
62
|
+
restful_action(verb, has_id && last_is_param)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def infer_plain_action(verb, segments)
|
|
67
|
+
non_param = segments.reject { |s| s.start_with?(':', '{') }
|
|
68
|
+
has_id = segments.any? { |s| s.start_with?(':', '{') }
|
|
69
|
+
last_is_param = segments.last&.start_with?(':', '{')
|
|
70
|
+
|
|
71
|
+
if non_param.length <= 1 && !has_id
|
|
72
|
+
non_param.first&.gsub('-', '_') || 'index'
|
|
73
|
+
elsif non_param.length > 1
|
|
74
|
+
non_param.last.gsub('-', '_')
|
|
75
|
+
else
|
|
76
|
+
restful_action(verb, has_id && last_is_param)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def nested_resource?(segments)
|
|
81
|
+
segments.length >= 3 &&
|
|
82
|
+
!segments[0].start_with?(':', '{') &&
|
|
83
|
+
segments[1]&.start_with?(':', '{') &&
|
|
84
|
+
!segments[2]&.start_with?(':', '{')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def restful_action(verb, is_member)
|
|
88
|
+
case [verb, is_member]
|
|
89
|
+
when ['GET', false] then 'index'
|
|
90
|
+
when ['GET', true] then 'show'
|
|
91
|
+
when ['POST', false] then 'create'
|
|
92
|
+
when ['PUT', true], ['PATCH', true] then 'update'
|
|
93
|
+
when ['DELETE', true] then 'destroy'
|
|
94
|
+
else 'index'
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module CLI
|
|
5
|
+
class RoutesCommand
|
|
6
|
+
# Extracts schema model loading logic from RoutesCommand.
|
|
7
|
+
module SchemaLoader
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def load_schema_models(routes_file)
|
|
11
|
+
schema_file = resolve_schema_file(routes_file)
|
|
12
|
+
return [] unless schema_file && File.exist?(schema_file)
|
|
13
|
+
|
|
14
|
+
Belt.instance_variable_set(:@application, nil)
|
|
15
|
+
begin
|
|
16
|
+
eval(File.read(schema_file), binding, schema_file) # rubocop:disable Security/Eval
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
warn "Warning: Failed to load schema file #{schema_file}: #{e.message}"
|
|
19
|
+
return []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
schema = Belt.application.schema.to_h
|
|
23
|
+
build_models_from_schema(schema)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def resolve_schema_file(routes_file)
|
|
27
|
+
schema_file = @options[:schema_file]
|
|
28
|
+
unless schema_file
|
|
29
|
+
routes_dir = File.dirname(File.expand_path(routes_file))
|
|
30
|
+
schema_file = File.join(routes_dir, 'schema.tf.rb')
|
|
31
|
+
end
|
|
32
|
+
schema_file
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build_models_from_schema(schema)
|
|
36
|
+
models = []
|
|
37
|
+
|
|
38
|
+
(schema[:request_models] || {}).each_value do |model|
|
|
39
|
+
models << {
|
|
40
|
+
name: model[:name],
|
|
41
|
+
kind: 'request',
|
|
42
|
+
description: "Request model: #{model[:name]}",
|
|
43
|
+
properties: stringify_properties(model[:properties] || {}),
|
|
44
|
+
required: (model[:required] || []).map(&:to_s)
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
(schema[:response_models] || {}).each_value do |model|
|
|
49
|
+
(model[:contexts] || {}).each do |ctx_name, ctx|
|
|
50
|
+
models << {
|
|
51
|
+
name: "#{model[:name]}_#{ctx_name}_response",
|
|
52
|
+
kind: 'response',
|
|
53
|
+
description: "Response model: #{model[:name]} (#{ctx_name} context)",
|
|
54
|
+
properties: stringify_properties(ctx[:properties] || {}),
|
|
55
|
+
required: []
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
models
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def stringify_properties(properties)
|
|
64
|
+
properties.each_with_object({}) do |(key, value), hash|
|
|
65
|
+
hash[key.to_s] = value.transform_keys(&:to_s)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|