belt 0.3.2 → 0.3.3
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.
Potentially problematic release.
This version of belt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/belt/action_router.rb +13 -2
- data/lib/belt/cli/explain_command.rb +110 -0
- data/lib/belt/cli/routes_command.rb +24 -0
- data/lib/belt/cli.rb +4 -1
- data/lib/belt/docs/backups.md +101 -0
- data/lib/belt/docs/console.md +65 -0
- data/lib/belt/docs/controllers.md +155 -0
- data/lib/belt/docs/deployment.md +141 -0
- data/lib/belt/docs/generators.md +128 -0
- data/lib/belt/docs/lambda_handler.md +105 -0
- data/lib/belt/docs/models.md +160 -0
- data/lib/belt/docs/observability.md +94 -0
- data/lib/belt/docs/plugins.md +94 -0
- data/lib/belt/docs/routing.md +138 -0
- data/lib/belt/docs/structure.md +93 -0
- data/lib/belt/route_dsl.rb +17 -0
- data/lib/belt/version.rb +1 -1
- data/lib/templates/new_app/AGENTS.md.erb +140 -19
- data/lib/templates/new_app/config/routes.rb.erb +5 -2
- metadata +13 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Generators
|
|
2
|
+
|
|
3
|
+
Belt generators scaffold code and infrastructure for common patterns.
|
|
4
|
+
Run `belt generate --help` to see all available generators (built-in + plugins).
|
|
5
|
+
|
|
6
|
+
## Built-in Generators
|
|
7
|
+
|
|
8
|
+
### Resource (scaffold)
|
|
9
|
+
|
|
10
|
+
Creates model + controller + routes + schema entry:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
belt generate resource post title:string body:text user_id:string status:string
|
|
14
|
+
belt g resource comment body:text author:string post_id:string
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This is the most common generator — it sets up everything for a new REST endpoint.
|
|
18
|
+
|
|
19
|
+
### Model
|
|
20
|
+
|
|
21
|
+
Creates just the model file + schema entry:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
belt generate model post title:string body:text user_id:string
|
|
25
|
+
belt g model payment amount:number currency:string
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Controller
|
|
29
|
+
|
|
30
|
+
Creates just the controller:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
belt generate controller posts
|
|
34
|
+
belt g controller admin/users
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Environment
|
|
38
|
+
|
|
39
|
+
Scaffolds a new Terraform environment directory:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
belt generate environment staging
|
|
43
|
+
belt g environment prod
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Frontend
|
|
47
|
+
|
|
48
|
+
Adds a frontend framework to the project:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
belt generate frontend react
|
|
52
|
+
belt generate frontend vue
|
|
53
|
+
belt generate frontend svelte
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Views
|
|
57
|
+
|
|
58
|
+
Generates React pages for a resource's REST actions:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
belt generate views post title:string body:text status:string
|
|
62
|
+
belt g views comment body:text author:string
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Auth
|
|
66
|
+
|
|
67
|
+
Sets up Cognito authentication:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
belt generate auth
|
|
71
|
+
belt g auth --provider cognito
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Field Types
|
|
75
|
+
|
|
76
|
+
When specifying fields, use these types:
|
|
77
|
+
|
|
78
|
+
| Type | DynamoDB Type | Notes |
|
|
79
|
+
|------|---------------|-------|
|
|
80
|
+
| `string` | S | Default if no type given |
|
|
81
|
+
| `text` | S | Same as string (semantic hint) |
|
|
82
|
+
| `number` | N | Numeric values |
|
|
83
|
+
| `boolean` | BOOL | True/false |
|
|
84
|
+
| `list` | L | Array values |
|
|
85
|
+
| `map` | M | Nested objects |
|
|
86
|
+
|
|
87
|
+
## Destroying Generated Code
|
|
88
|
+
|
|
89
|
+
Every generator has a matching destroy command:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
belt destroy resource post
|
|
93
|
+
belt destroy model comment
|
|
94
|
+
belt destroy controller admin/users
|
|
95
|
+
belt destroy environment staging
|
|
96
|
+
belt destroy frontend
|
|
97
|
+
belt destroy views post
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Plugin Generators
|
|
101
|
+
|
|
102
|
+
Gems that follow the Belt plugin contract are auto-discovered:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
belt generate messaging # from belt-messaging gem
|
|
106
|
+
belt generate pay # from belt-pay gem
|
|
107
|
+
belt generate --help # lists all available generators
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Generator Workflow (Common Pattern)
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# 1. Generate the resource
|
|
114
|
+
belt generate resource order item_id:string quantity:number total:number status:string
|
|
115
|
+
|
|
116
|
+
# 2. Generate DynamoDB table from schema
|
|
117
|
+
belt setup tables dev01
|
|
118
|
+
|
|
119
|
+
# 3. Deploy
|
|
120
|
+
belt deploy dev01
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## See Also
|
|
124
|
+
|
|
125
|
+
- `belt explain models` — how generated models work
|
|
126
|
+
- `belt explain controllers` — how generated controllers work
|
|
127
|
+
- `belt explain routing` — how generated routes work
|
|
128
|
+
- `belt explain deployment` — deploying generated code
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Lambda Handler
|
|
2
|
+
|
|
3
|
+
`Belt::LambdaHandler` is the module you include in your Lambda entry point.
|
|
4
|
+
It provides the `lambda_handler` method that AWS Lambda invokes, wrapping
|
|
5
|
+
your application logic with observability, CORS, and error handling.
|
|
6
|
+
|
|
7
|
+
## Basic Usage
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "belt"
|
|
11
|
+
|
|
12
|
+
include Belt::LambdaHandler
|
|
13
|
+
|
|
14
|
+
ROUTER = Belt::ActionRouter.new(routes: Routes::API, gateway: "api")
|
|
15
|
+
|
|
16
|
+
def execute(path:, body:, event:)
|
|
17
|
+
ROUTER.route(event: event, body: body)
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What It Does
|
|
22
|
+
|
|
23
|
+
When a request arrives, `lambda_handler` automatically:
|
|
24
|
+
|
|
25
|
+
1. **Initializes observability** — structured logging + CloudWatch metrics
|
|
26
|
+
2. **Handles OPTIONS preflight** — returns CORS headers immediately
|
|
27
|
+
3. **Parses the request body** — JSON string → Ruby hash
|
|
28
|
+
4. **Calls your `execute` method** — with `path:`, `body:`, and `event:`
|
|
29
|
+
5. **Catches unhandled errors** — returns a CORS-enabled error response
|
|
30
|
+
6. **Emits metrics** — request count, latency, error count via EMF
|
|
31
|
+
|
|
32
|
+
## The `execute` Method
|
|
33
|
+
|
|
34
|
+
You must define `execute` in your Lambda file. It receives:
|
|
35
|
+
|
|
36
|
+
| Param | Description |
|
|
37
|
+
|-------|-------------|
|
|
38
|
+
| `path:` | The request path (e.g., `/posts/123`) |
|
|
39
|
+
| `body:` | Parsed request body (Hash or nil) |
|
|
40
|
+
| `event:` | Full API Gateway event (for headers, query params, auth context) |
|
|
41
|
+
|
|
42
|
+
Return value should be a response hash: `{ statusCode:, headers:, body: }`.
|
|
43
|
+
Typically you just call `ROUTER.route(...)` which returns the correct format.
|
|
44
|
+
|
|
45
|
+
## Multiple Lambdas
|
|
46
|
+
|
|
47
|
+
If your app has multiple Lambda functions (via `function` in routes):
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# lambda/worker.rb
|
|
51
|
+
require "belt"
|
|
52
|
+
|
|
53
|
+
include Belt::LambdaHandler
|
|
54
|
+
|
|
55
|
+
ROUTER = Belt::ActionRouter.new(routes: Routes::WORKER, gateway: "worker")
|
|
56
|
+
|
|
57
|
+
def execute(path:, body:, event:)
|
|
58
|
+
ROUTER.route(event: event, body: body)
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Each Lambda entry point gets its own route manifest and gateway name.
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
Configure via `config/lambda/<name>.yml`:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
handler: lambda/api.lambda_handler
|
|
70
|
+
runtime: ruby3.3
|
|
71
|
+
timeout: 30
|
|
72
|
+
memory: 256
|
|
73
|
+
layers:
|
|
74
|
+
- ${var.ruby_layer_arn}
|
|
75
|
+
environment:
|
|
76
|
+
ENVIRONMENT: ${var.environment}
|
|
77
|
+
APP_NAME: my-app
|
|
78
|
+
ERROR_NOTIFICATION_TOPIC_ARN: ${var.sns_topic_arn}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Observability
|
|
82
|
+
|
|
83
|
+
The handler sets up `Belt::Observability::Logger` and `Belt::Observability::Metrics`
|
|
84
|
+
automatically. Use them anywhere in your app:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
Belt::Observability::Logger.info("Order created", order_id: order.id)
|
|
88
|
+
Belt::Observability::Metrics.track_event("OrderCreated", model: "Order")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Environment Variables
|
|
92
|
+
|
|
93
|
+
| Variable | Purpose |
|
|
94
|
+
|----------|---------|
|
|
95
|
+
| `ENVIRONMENT` | Controls error verbosity (`dev*`, `local`, `test` = verbose) |
|
|
96
|
+
| `BELT_METRICS_NAMESPACE` | CloudWatch namespace (default: `Belt`) |
|
|
97
|
+
| `ACTION` | Service name for logging |
|
|
98
|
+
| `ERROR_NOTIFICATION_TOPIC_ARN` | SNS topic for error alerts |
|
|
99
|
+
| `CORS_ALLOWED_ORIGINS` | Comma-separated allowed origins |
|
|
100
|
+
|
|
101
|
+
## See Also
|
|
102
|
+
|
|
103
|
+
- `belt explain routing` — how ActionRouter dispatches requests
|
|
104
|
+
- `belt explain controllers` — how dispatched requests are handled
|
|
105
|
+
- `belt explain deployment` — Lambda packaging and configuration
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Models
|
|
2
|
+
|
|
3
|
+
Belt uses **ActiveItem** as its ORM for DynamoDB. Models inherit from
|
|
4
|
+
`ActiveItem::Base` (typically via an `ApplicationRecord` base class).
|
|
5
|
+
|
|
6
|
+
## Basic Model
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
class Post < ApplicationRecord
|
|
10
|
+
attr_accessor :id, :user_id, :title, :body, :status, :created_at, :updated_at
|
|
11
|
+
|
|
12
|
+
validates :title, presence: true
|
|
13
|
+
validates :status, inclusion: { in: %w[draft published] }, allow_nil: true
|
|
14
|
+
|
|
15
|
+
before_create { self.id ||= SecureRandom.uuid }
|
|
16
|
+
before_save { self.updated_at = Time.now.iso8601 }
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Table Naming
|
|
21
|
+
|
|
22
|
+
Table names are derived from the environment:
|
|
23
|
+
`{APP_NAME}-{ENVIRONMENT}-{pluralized_model}`
|
|
24
|
+
|
|
25
|
+
Example: app "blog", environment "prod", model "Post" → `blog-prod-posts`
|
|
26
|
+
|
|
27
|
+
## Schema Definition
|
|
28
|
+
|
|
29
|
+
Define table structure in `infrastructure/schema.tf.rb`:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
Belt.application.schema.define do
|
|
33
|
+
model :post do
|
|
34
|
+
partition_key :id, :string
|
|
35
|
+
global_secondary_index :UserIndex, partition_key: :user_id
|
|
36
|
+
global_secondary_index :StatusIndex, partition_key: :status, sort_key: :created_at
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## CRUD Operations
|
|
42
|
+
|
|
43
|
+
### Create
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
post = Post.create!(title: "Hello", body: "World", user_id: "u-123")
|
|
47
|
+
# or
|
|
48
|
+
post = Post.new(title: "Hello")
|
|
49
|
+
post.save!
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Read
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
post = Post.find("post-id-123") # by primary key
|
|
56
|
+
posts = Post.all # scan (use sparingly)
|
|
57
|
+
posts = Post.where(user_id: "u-123", index: "UserIndex")
|
|
58
|
+
post = Post.find_by(user_id: "u-123", index: "UserIndex") # first match
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Update
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
post = Post.find("post-id-123")
|
|
65
|
+
post.update(title: "New Title")
|
|
66
|
+
# or
|
|
67
|
+
post.title = "New Title"
|
|
68
|
+
post.save!
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Delete
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
post = Post.find("post-id-123")
|
|
75
|
+
post.destroy
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Query Patterns
|
|
79
|
+
|
|
80
|
+
### Using Indexes
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Query a GSI
|
|
84
|
+
Post.where(user_id: "u-123", index: "UserIndex")
|
|
85
|
+
|
|
86
|
+
# With sort key conditions
|
|
87
|
+
Post.where(
|
|
88
|
+
status: "published",
|
|
89
|
+
created_at: { gte: "2024-01-01" },
|
|
90
|
+
index: "StatusIndex"
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Count and Existence
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
Post.count # total items (scan)
|
|
98
|
+
Post.exists?("post-id-123") # check by primary key
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Validations
|
|
102
|
+
|
|
103
|
+
ActiveItem supports ActiveModel-style validations:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
validates :title, presence: true
|
|
107
|
+
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
108
|
+
validates :status, inclusion: { in: %w[active inactive] }
|
|
109
|
+
validates :age, numericality: { greater_than: 0 }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Callbacks
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
before_create { self.id ||= SecureRandom.uuid }
|
|
116
|
+
before_save { self.updated_at = Time.now.iso8601 }
|
|
117
|
+
after_create { notify_subscribers }
|
|
118
|
+
before_destroy { cleanup_associations }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Associations (Manual)
|
|
122
|
+
|
|
123
|
+
DynamoDB doesn't have joins. Model relationships manually:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
class Post < ApplicationRecord
|
|
127
|
+
def comments
|
|
128
|
+
Comment.where(post_id: id, index: "PostIndex")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def author
|
|
132
|
+
User.find(user_id)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Transactions
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
ActiveItem::Transaction.write do |tx|
|
|
141
|
+
tx.put(post)
|
|
142
|
+
tx.put(comment)
|
|
143
|
+
tx.delete(draft)
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Generating Models
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
belt generate model post title:string body:text user_id:string
|
|
151
|
+
belt generate resource comment body:text author:string post_id:string
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The resource generator creates model + controller + routes + schema entry.
|
|
155
|
+
|
|
156
|
+
## See Also
|
|
157
|
+
|
|
158
|
+
- `belt explain controllers` — using models in controllers
|
|
159
|
+
- `belt explain deployment` — how schema becomes infrastructure
|
|
160
|
+
- `belt explain queries` — advanced DynamoDB query patterns
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Observability
|
|
2
|
+
|
|
3
|
+
Belt provides structured logging and CloudWatch metrics out of the box.
|
|
4
|
+
These are initialized automatically by `Belt::LambdaHandler` — no setup required.
|
|
5
|
+
|
|
6
|
+
## Logging
|
|
7
|
+
|
|
8
|
+
Access the logger from anywhere:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
Belt::Observability::Logger.info("Order placed", order_id: "o-123", total: 49.99)
|
|
12
|
+
Belt::Observability::Logger.warn("Retry attempt", attempt: 3, service: "payments")
|
|
13
|
+
Belt::Observability::Logger.error("Payment failed", error: e.message, order_id: "o-123")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Logs are structured JSON, compatible with CloudWatch Logs Insights:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"level": "INFO",
|
|
21
|
+
"message": "Order placed",
|
|
22
|
+
"order_id": "o-123",
|
|
23
|
+
"total": 49.99,
|
|
24
|
+
"timestamp": "2024-01-15T10:30:00Z",
|
|
25
|
+
"service": "api"
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Metrics
|
|
30
|
+
|
|
31
|
+
Track custom events via CloudWatch Embedded Metric Format (EMF):
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Belt::Observability::Metrics.track_event("OrderCreated", model: "Order")
|
|
35
|
+
Belt::Observability::Metrics.track_event("PaymentProcessed", amount: 49.99)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Built-in Metrics
|
|
39
|
+
|
|
40
|
+
The Lambda handler automatically emits:
|
|
41
|
+
- `RequestCount` — per invocation
|
|
42
|
+
- `ErrorCount` — on unhandled exceptions
|
|
43
|
+
- `Latency` — request duration in milliseconds
|
|
44
|
+
|
|
45
|
+
### Namespace
|
|
46
|
+
|
|
47
|
+
Metrics are published under the namespace set by `BELT_METRICS_NAMESPACE`
|
|
48
|
+
(defaults to `Belt`). View them in CloudWatch → Metrics → Custom namespaces.
|
|
49
|
+
|
|
50
|
+
## Error Alerting
|
|
51
|
+
|
|
52
|
+
Set `ERROR_NOTIFICATION_TOPIC_ARN` to an SNS topic ARN. Unhandled errors
|
|
53
|
+
will publish a notification with error details (message, backtrace, request context).
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
# config/lambda/api.yml
|
|
57
|
+
environment:
|
|
58
|
+
ERROR_NOTIFICATION_TOPIC_ARN: ${var.sns_topic_arn}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## CloudWatch Logs Insights
|
|
62
|
+
|
|
63
|
+
Query structured logs:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
fields @timestamp, message, order_id
|
|
67
|
+
| filter level = "ERROR"
|
|
68
|
+
| sort @timestamp desc
|
|
69
|
+
| limit 20
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Viewing Logs Locally
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
belt logs api # tail logs for the "api" Lambda
|
|
76
|
+
belt logs api -f # follow (live tail)
|
|
77
|
+
belt logs api -s 30m # last 30 minutes
|
|
78
|
+
belt logs worker -e prod # specific environment
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Environment Variables
|
|
82
|
+
|
|
83
|
+
| Variable | Purpose |
|
|
84
|
+
|----------|---------|
|
|
85
|
+
| `BELT_METRICS_NAMESPACE` | CloudWatch metrics namespace (default: `Belt`) |
|
|
86
|
+
| `ACTION` | Service name in log entries (falls back to function name) |
|
|
87
|
+
| `ERROR_NOTIFICATION_TOPIC_ARN` | SNS topic for error alerts |
|
|
88
|
+
| `ENVIRONMENT` | Controls verbose errors (`dev*`/`local`/`test` = verbose) |
|
|
89
|
+
|
|
90
|
+
## See Also
|
|
91
|
+
|
|
92
|
+
- `belt explain lambda_handler` — how observability is initialized
|
|
93
|
+
- `belt explain deployment` — configuring environment variables
|
|
94
|
+
- `belt logs --help` — full CLI options for log viewing
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Plugins
|
|
2
|
+
|
|
3
|
+
Belt plugins are separate gems that extend the CLI and runtime. They're
|
|
4
|
+
discovered automatically — no registration file or initializer hook needed.
|
|
5
|
+
|
|
6
|
+
## Using a Plugin
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
# Gemfile
|
|
10
|
+
gem "belt-messaging"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle install
|
|
15
|
+
belt generate messaging # run the plugin's generator
|
|
16
|
+
belt destroy messaging # remove what the generator created
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Available Plugins
|
|
20
|
+
|
|
21
|
+
| Gem | Purpose |
|
|
22
|
+
|-----|---------|
|
|
23
|
+
| `belt-messaging` | Two-way SMS via AWS End User Messaging |
|
|
24
|
+
| `belt-pay` | Stripe payments & subscriptions |
|
|
25
|
+
|
|
26
|
+
## Creating a Plugin
|
|
27
|
+
|
|
28
|
+
Scaffold a new plugin gem:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
belt plugin new notifications
|
|
32
|
+
belt plugin new pay --path ~/Code --summary "Stripe payments for Belt"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This creates a gem with the correct structure and Belt integration points.
|
|
36
|
+
|
|
37
|
+
## Plugin Discovery Contract
|
|
38
|
+
|
|
39
|
+
Belt discovers plugins automatically when:
|
|
40
|
+
|
|
41
|
+
1. Gem is in the app's `Gemfile` and bundled
|
|
42
|
+
2. Gem has a file at `lib/belt/generators/<name>_generator.rb`
|
|
43
|
+
3. Class is `Belt::Generators::<Name>Generator`
|
|
44
|
+
4. Class implements `.run(args)` (required)
|
|
45
|
+
|
|
46
|
+
Optional: `.destroy(args)` and `.description` for destroy path and help text.
|
|
47
|
+
|
|
48
|
+
## Generator Checklist
|
|
49
|
+
|
|
50
|
+
A good plugin generator typically installs:
|
|
51
|
+
|
|
52
|
+
1. **Terraform module** → `infrastructure/modules/<name>/`
|
|
53
|
+
2. **Lambda config** → `config/lambda/<name>.yml`
|
|
54
|
+
3. **Lambda entrypoint** → `lambda/<name>.rb` using `Belt::LambdaHandler`
|
|
55
|
+
4. **Routes / schema** → inject into routes or schema when needed
|
|
56
|
+
5. **Optional overrides** → `--controllers` flag for app-local subclasses
|
|
57
|
+
6. **Destroy path** → removes everything the generator created
|
|
58
|
+
7. **Help text** → `.description` + `--help`
|
|
59
|
+
|
|
60
|
+
## Plugin Layout
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
belt-messaging/
|
|
64
|
+
├── belt-messaging.gemspec
|
|
65
|
+
├── lib/
|
|
66
|
+
│ ├── belt-messaging.rb # require entrypoint
|
|
67
|
+
│ └── belt/
|
|
68
|
+
│ ├── messaging.rb # Runtime API
|
|
69
|
+
│ ├── messaging/
|
|
70
|
+
│ │ ├── configuration.rb
|
|
71
|
+
│ │ ├── version.rb
|
|
72
|
+
│ │ ├── controllers/ # Default controllers
|
|
73
|
+
│ │ └── templates/ # ERB templates for generator
|
|
74
|
+
│ └── generators/
|
|
75
|
+
│ └── messaging_generator.rb # ← auto-discovered
|
|
76
|
+
└── spec/
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Development Workflow
|
|
80
|
+
|
|
81
|
+
Point a Belt app at your plugin during development:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# In the app Gemfile
|
|
85
|
+
gem "belt-notifications", path: "../belt-notifications"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`belt deploy` detects `path:` gems and vendors them into `vendor/cache`
|
|
89
|
+
for Lambda packaging.
|
|
90
|
+
|
|
91
|
+
## See Also
|
|
92
|
+
|
|
93
|
+
- `belt explain generators` — built-in generators
|
|
94
|
+
- `belt plugin new --help` — scaffold options
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
Belt routes map HTTP requests to controller actions. Routes are defined in
|
|
4
|
+
`config/routes.rb` using a Ruby DSL that mirrors infrastructure (API Gateway + Lambda).
|
|
5
|
+
|
|
6
|
+
## Defining Routes
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
Belt.application.routes.draw do
|
|
10
|
+
gateway :api do
|
|
11
|
+
resources :posts
|
|
12
|
+
resources :comments, only: [:index, :create]
|
|
13
|
+
resource :profile, only: [:show, :update]
|
|
14
|
+
get "health", action: :health
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Route DSL Keywords
|
|
20
|
+
|
|
21
|
+
| Keyword | Purpose | Creates Lambda? |
|
|
22
|
+
|---------|---------|-----------------|
|
|
23
|
+
| `gateway` | Creates an API Gateway + default Lambda | Yes |
|
|
24
|
+
| `function` | Routes to a different Lambda (overrides gateway default) | Yes |
|
|
25
|
+
| `namespace` | Adds path prefix + controller module nesting | No |
|
|
26
|
+
| `scope` | Flexible path/module/auth grouping | No |
|
|
27
|
+
|
|
28
|
+
## How `resources` Maps to Verbs
|
|
29
|
+
|
|
30
|
+
`resources :posts` generates:
|
|
31
|
+
|
|
32
|
+
| Verb | Path | Action |
|
|
33
|
+
|------|------|--------|
|
|
34
|
+
| GET | /posts | index |
|
|
35
|
+
| POST | /posts | create |
|
|
36
|
+
| GET | /posts/{post_id} | show |
|
|
37
|
+
| PUT | /posts/{post_id} | update |
|
|
38
|
+
| DELETE | /posts/{post_id} | destroy |
|
|
39
|
+
|
|
40
|
+
**Note:** Belt uses PUT, not PATCH, for updates.
|
|
41
|
+
|
|
42
|
+
Use `only:` or `except:` to limit generated routes:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
resources :posts, only: [:index, :show, :create]
|
|
46
|
+
resources :comments, except: [:destroy]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Singular Resources
|
|
50
|
+
|
|
51
|
+
`resource :profile` (no `:id` in the path):
|
|
52
|
+
|
|
53
|
+
| Verb | Path | Action |
|
|
54
|
+
|------|------|--------|
|
|
55
|
+
| GET | /profile | show |
|
|
56
|
+
| PUT | /profile | update |
|
|
57
|
+
| POST | /profile | create |
|
|
58
|
+
| DELETE | /profile | destroy |
|
|
59
|
+
|
|
60
|
+
## Namespace and Scope
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
gateway :api do
|
|
64
|
+
# Namespace: adds path prefix AND controller module
|
|
65
|
+
namespace :admin do
|
|
66
|
+
resources :users # → /admin/users → Admin::UsersController
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Scope: flexible grouping without full nesting
|
|
70
|
+
scope path: 'v2', module: 'legacy' do
|
|
71
|
+
resources :widgets # → /v2/widgets → Legacy::WidgetsController
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Multiple Lambdas
|
|
77
|
+
|
|
78
|
+
Use `function` when routes should be handled by a separate Lambda:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
gateway :api do
|
|
82
|
+
resources :posts # → handled by "api" Lambda
|
|
83
|
+
|
|
84
|
+
function :worker do
|
|
85
|
+
resources :jobs # → handled by "worker" Lambda
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Authentication
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
gateway :api, auth: :cognito do
|
|
94
|
+
resources :posts # requires cognito auth
|
|
95
|
+
get "health", action: :health, auth: :none # public
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Table Access
|
|
100
|
+
|
|
101
|
+
Declare which DynamoDB tables a route accesses (used by Terraform for IAM):
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
resources :posts, tables: [:posts, :comments]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Inspecting Routes
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
belt routes # display all routes
|
|
111
|
+
belt routes -g posts # filter by pattern
|
|
112
|
+
belt routes -f json # machine-readable output
|
|
113
|
+
belt routes --namespace api # generate Ruby route manifest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Runtime Routing
|
|
117
|
+
|
|
118
|
+
The Lambda entry point uses `Belt::ActionRouter` with the generated route manifest:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
require "belt"
|
|
122
|
+
include Belt::LambdaHandler
|
|
123
|
+
|
|
124
|
+
ROUTER = Belt::ActionRouter.new(routes: Routes::API, gateway: "api")
|
|
125
|
+
|
|
126
|
+
def execute(path:, body:, event:)
|
|
127
|
+
ROUTER.route(event: event, body: body)
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The router matches the incoming HTTP method + path against the manifest and
|
|
132
|
+
dispatches to the appropriate controller and action.
|
|
133
|
+
|
|
134
|
+
## See Also
|
|
135
|
+
|
|
136
|
+
- `belt explain controllers` — how controllers handle requests
|
|
137
|
+
- `belt explain deployment` — how routes become infrastructure
|
|
138
|
+
- `belt routes --help` — full CLI options
|