belt 0.2.10 → 0.2.12
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
- data/CHANGELOG.md +63 -0
- data/README.md +169 -0
- data/lib/belt/action_router.rb +32 -4
- data/lib/belt/cli/app_detection.rb +19 -5
- data/lib/belt/cli/contracts_command.rb +142 -0
- data/lib/belt/cli/destroy_command.rb +193 -10
- data/lib/belt/cli/environment_command.rb +32 -0
- data/lib/belt/cli/generate_command.rb +5 -5
- data/lib/belt/cli/new_command.rb +2 -2
- data/lib/belt/cli/plugin_command.rb +221 -0
- data/lib/belt/cli/routes_command/schema_loader.rb +17 -7
- data/lib/belt/cli/routes_command.rb +7 -3
- data/lib/belt/cli/setup_command.rb +1 -1
- data/lib/belt/cli/views_command.rb +7 -2
- data/lib/belt/cli.rb +7 -0
- data/lib/belt/helpers/cors_origin.rb +19 -2
- data/lib/belt/helpers/response.rb +13 -2
- data/lib/belt/lambda_handler.rb +14 -5
- data/lib/belt/root.rb +12 -3
- data/lib/belt/route_dsl.rb +2 -2
- data/lib/belt/version.rb +1 -1
- data/lib/templates/environment/backend.tf.erb +1 -2
- data/lib/templates/frontend_infra/frontend.tf.erb +2 -1
- data/lib/templates/module/frontend.tf.erb +2 -1
- data/lib/templates/module/main.tf.erb +1 -1
- data/lib/templates/new_app/AGENTS.md.erb +4 -4
- data/lib/templates/new_app/README.md.erb +2 -2
- data/lib/templates/plugin/AGENTS.md.erb +135 -0
- data/lib/templates/plugin/CHANGELOG.md.erb +5 -0
- data/lib/templates/plugin/Gemfile.erb +11 -0
- data/lib/templates/plugin/LICENSE.erb +21 -0
- data/lib/templates/plugin/README.md.erb +63 -0
- data/lib/templates/plugin/Rakefile.erb +7 -0
- data/lib/templates/plugin/gemspec.erb +26 -0
- data/lib/templates/plugin/gitignore.erb +11 -0
- data/lib/templates/plugin/lib/belt/generators/generator.rb.erb +122 -0
- data/lib/templates/plugin/lib/belt/module/configuration.rb.erb +15 -0
- data/lib/templates/plugin/lib/belt/module/version.rb.erb +7 -0
- data/lib/templates/plugin/lib/belt/module.rb.erb +27 -0
- data/lib/templates/plugin/lib/entry.rb.erb +3 -0
- data/lib/templates/plugin/rspec.erb +3 -0
- data/lib/templates/plugin/spec/configuration_spec.rb.erb +17 -0
- data/lib/templates/plugin/spec/spec_helper.rb.erb +19 -0
- metadata +21 -3
- /data/lib/templates/new_app/config/{schema.tf.rb.erb → contracts.rb.erb} +0 -0
- /data/lib/templates/new_app/config/{routes.tf.rb.erb → routes.rb.erb} +0 -0
data/lib/belt/cli.rb
CHANGED
|
@@ -18,10 +18,12 @@ require_relative 'cli/backup_config'
|
|
|
18
18
|
require_relative 'cli/backup_runner'
|
|
19
19
|
require_relative 'cli/server_command'
|
|
20
20
|
require_relative 'cli/routes_command'
|
|
21
|
+
require_relative 'cli/contracts_command'
|
|
21
22
|
require_relative 'cli/lambda_config_command'
|
|
22
23
|
require_relative 'cli/tasks_command'
|
|
23
24
|
require_relative 'cli/console_command'
|
|
24
25
|
require_relative 'cli/doctor_command'
|
|
26
|
+
require_relative 'cli/plugin_command'
|
|
25
27
|
|
|
26
28
|
module Belt
|
|
27
29
|
module CLI
|
|
@@ -30,11 +32,13 @@ module Belt
|
|
|
30
32
|
%w[generate g] => Belt::CLI::GenerateCommand,
|
|
31
33
|
%w[destroy d] => Belt::CLI::DestroyCommand,
|
|
32
34
|
'routes' => Belt::CLI::RoutesCommand,
|
|
35
|
+
'contracts' => Belt::CLI::ContractsCommand,
|
|
33
36
|
'lambda-config' => Belt::CLI::LambdaConfigCommand,
|
|
34
37
|
%w[console c] => Belt::CLI::ConsoleCommand,
|
|
35
38
|
%w[tasks --tasks -T] => Belt::CLI::TasksCommand,
|
|
36
39
|
'setup' => Belt::CLI::SetupCommand,
|
|
37
40
|
'doctor' => Belt::CLI::DoctorCommand,
|
|
41
|
+
'plugin' => Belt::CLI::PluginCommand,
|
|
38
42
|
'deploy' => Belt::CLI::DeployCommand,
|
|
39
43
|
'frontend' => Belt::CLI::FrontendEnvCommand,
|
|
40
44
|
%w[server s] => Belt::CLI::ServerCommand,
|
|
@@ -107,6 +111,7 @@ module Belt
|
|
|
107
111
|
deploy frontend <env> Build and deploy frontend to AWS
|
|
108
112
|
frontend env <env> Write frontend/.env from terraform outputs
|
|
109
113
|
routes [-g PATTERN] [-f json] Show route definitions
|
|
114
|
+
contracts [-g PATTERN] [-f json] Show API request/response contracts
|
|
110
115
|
lambda-config [-e ENV] [-f json|terraform] Show merged lambda configuration
|
|
111
116
|
|
|
112
117
|
console Start an interactive console (IRB)
|
|
@@ -117,6 +122,7 @@ module Belt
|
|
|
117
122
|
setup tables <env> Generate DynamoDB tables from schema
|
|
118
123
|
setup frontend <env> Generate S3 + CloudFront infrastructure
|
|
119
124
|
doctor Check system dependencies and AWS config
|
|
125
|
+
plugin new <name> Scaffold a new Belt plugin gem
|
|
120
126
|
init [environment] <env> terraform init for environment
|
|
121
127
|
plan [environment] <env> terraform plan for environment
|
|
122
128
|
apply [environment] <env> terraform apply for environment
|
|
@@ -149,6 +155,7 @@ module Belt
|
|
|
149
155
|
belt apply wups
|
|
150
156
|
belt tasks # list all rake tasks
|
|
151
157
|
belt lambda:build_layer # run a rake task directly
|
|
158
|
+
belt plugin new messaging # scaffold a belt-messaging style plugin gem
|
|
152
159
|
USAGE
|
|
153
160
|
end
|
|
154
161
|
end
|
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
module Belt
|
|
4
4
|
module Helpers
|
|
5
5
|
module CorsOrigin
|
|
6
|
+
# Maximum length for an Origin header to prevent ReDoS or memory abuse.
|
|
7
|
+
MAX_ORIGIN_LENGTH = 253
|
|
8
|
+
|
|
6
9
|
def self.resolve_origin(request_origin)
|
|
7
10
|
allowed = allowed_origins
|
|
8
11
|
return nil if allowed.empty?
|
|
9
|
-
|
|
12
|
+
|
|
13
|
+
if request_origin && valid_origin?(request_origin) && matches_allowed?(request_origin, allowed)
|
|
14
|
+
return request_origin
|
|
15
|
+
end
|
|
10
16
|
|
|
11
17
|
# Don't return a wildcard pattern as a literal origin — use '*' instead
|
|
12
18
|
first = allowed.first
|
|
@@ -18,7 +24,7 @@ module Belt
|
|
|
18
24
|
|
|
19
25
|
allowed.any? do |pattern|
|
|
20
26
|
if pattern.include?('*')
|
|
21
|
-
regex = Regexp.new("\\A#{Regexp.escape(pattern).gsub('\*', '[
|
|
27
|
+
regex = Regexp.new("\\A#{Regexp.escape(pattern).gsub('\*', '[a-z0-9\\-]+')}\\z")
|
|
22
28
|
regex.match?(origin)
|
|
23
29
|
else
|
|
24
30
|
pattern == origin
|
|
@@ -26,6 +32,17 @@ module Belt
|
|
|
26
32
|
end
|
|
27
33
|
end
|
|
28
34
|
|
|
35
|
+
# Validate that the origin looks like a legitimate URL scheme+host.
|
|
36
|
+
# Rejects origins with path components, whitespace, or unexpected characters.
|
|
37
|
+
def self.valid_origin?(origin)
|
|
38
|
+
return false if origin.nil? || origin.empty?
|
|
39
|
+
return false if origin.length > MAX_ORIGIN_LENGTH
|
|
40
|
+
return false unless origin.match?(%r{\A https?://[a-z0-9\-.:]+\z}ix)
|
|
41
|
+
|
|
42
|
+
# Reject origins with user-info, paths, queries, or fragments
|
|
43
|
+
!origin.include?('@') && !origin.include?('?') && !origin.include?('#')
|
|
44
|
+
end
|
|
45
|
+
|
|
29
46
|
def self.origin_from_event(event)
|
|
30
47
|
return nil unless event.is_a?(Hash)
|
|
31
48
|
|
|
@@ -7,6 +7,10 @@ require_relative '../http_status'
|
|
|
7
7
|
module Belt
|
|
8
8
|
module Helpers
|
|
9
9
|
module Response
|
|
10
|
+
# Maximum request body size (10 MB). Bodies larger than this are rejected
|
|
11
|
+
# to prevent memory exhaustion attacks on Lambda functions.
|
|
12
|
+
MAX_REQUEST_BODY_SIZE = 10 * 1024 * 1024
|
|
13
|
+
|
|
10
14
|
def cors_headers(event = nil)
|
|
11
15
|
event = @event if event.nil? && instance_variable_defined?(:@event)
|
|
12
16
|
origin = CorsOrigin.resolve_origin(CorsOrigin.origin_from_event(event))
|
|
@@ -15,7 +19,9 @@ module Belt
|
|
|
15
19
|
'Access-Control-Allow-Headers' => allow_headers,
|
|
16
20
|
'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
|
|
17
21
|
'Access-Control-Max-Age' => '300',
|
|
18
|
-
'Content-Type' => 'application/json'
|
|
22
|
+
'Content-Type' => 'application/json',
|
|
23
|
+
'X-Content-Type-Options' => 'nosniff',
|
|
24
|
+
'Vary' => 'Origin'
|
|
19
25
|
}
|
|
20
26
|
headers['Access-Control-Allow-Origin'] = origin if origin
|
|
21
27
|
headers
|
|
@@ -38,7 +44,12 @@ module Belt
|
|
|
38
44
|
def html_response(html, status_code = 200)
|
|
39
45
|
event = @event if instance_variable_defined?(:@event)
|
|
40
46
|
origin = CorsOrigin.resolve_origin(CorsOrigin.origin_from_event(event))
|
|
41
|
-
headers = {
|
|
47
|
+
headers = {
|
|
48
|
+
'Content-Type' => 'text/html; charset=utf-8',
|
|
49
|
+
'X-Content-Type-Options' => 'nosniff',
|
|
50
|
+
'X-Frame-Options' => 'DENY',
|
|
51
|
+
'Referrer-Policy' => 'strict-origin-when-cross-origin'
|
|
52
|
+
}
|
|
42
53
|
headers['Access-Control-Allow-Origin'] = origin if origin
|
|
43
54
|
{ statusCode: resolve_status(status_code), headers: headers, body: html }
|
|
44
55
|
end
|
data/lib/belt/lambda_handler.rb
CHANGED
|
@@ -76,11 +76,8 @@ module Belt
|
|
|
76
76
|
|
|
77
77
|
return { statusCode: 200, headers: cors_headers(event), body: '{}' } if event['httpMethod'] == 'OPTIONS'
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
rescue JSON::ParserError
|
|
82
|
-
return error_response('Invalid JSON in request body')
|
|
83
|
-
end
|
|
79
|
+
body = parse_request_body(event)
|
|
80
|
+
return body if body.is_a?(Hash) && body[:statusCode]
|
|
84
81
|
|
|
85
82
|
begin
|
|
86
83
|
result = execute(path: event['path'], body: body, event: event)
|
|
@@ -107,6 +104,18 @@ module Belt
|
|
|
107
104
|
|
|
108
105
|
private
|
|
109
106
|
|
|
107
|
+
def parse_request_body(event)
|
|
108
|
+
raw_body = event['body'] || '{}'
|
|
109
|
+
|
|
110
|
+
if raw_body.bytesize > Belt::Helpers::Response::MAX_REQUEST_BODY_SIZE
|
|
111
|
+
return error_response('Request body too large', 413)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
JSON.parse(raw_body)
|
|
115
|
+
rescue JSON::ParserError
|
|
116
|
+
error_response('Invalid JSON in request body', 400)
|
|
117
|
+
end
|
|
118
|
+
|
|
110
119
|
def init_observability(context:)
|
|
111
120
|
service_name = ENV['ACTION'] || context.function_name.split('-').last
|
|
112
121
|
|
data/lib/belt/root.rb
CHANGED
|
@@ -9,24 +9,32 @@ module Belt
|
|
|
9
9
|
@root = path
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
# Resolves the path to routes.
|
|
12
|
+
# Resolves the path to routes.rb, checking config/ first then legacy paths.
|
|
13
13
|
def self.routes_file
|
|
14
14
|
candidates = [
|
|
15
|
+
File.join(root, 'config/routes.rb'),
|
|
15
16
|
File.join(root, 'config/routes.tf.rb'),
|
|
16
17
|
File.join(root, 'infrastructure/routes.tf.rb')
|
|
17
18
|
]
|
|
18
19
|
candidates.find { |f| File.exist?(f) }
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
# Resolves the path to
|
|
22
|
-
def self.
|
|
22
|
+
# Resolves the path to contracts.rb, checking config/ first then legacy paths.
|
|
23
|
+
def self.contracts_file
|
|
23
24
|
candidates = [
|
|
25
|
+
File.join(root, 'config/contracts.rb'),
|
|
26
|
+
File.join(root, 'config/contracts.tf.rb'),
|
|
24
27
|
File.join(root, 'config/schema.tf.rb'),
|
|
25
28
|
File.join(root, 'infrastructure/schema.tf.rb')
|
|
26
29
|
]
|
|
27
30
|
candidates.find { |f| File.exist?(f) }
|
|
28
31
|
end
|
|
29
32
|
|
|
33
|
+
# Legacy alias for backward compatibility
|
|
34
|
+
def self.schema_file
|
|
35
|
+
contracts_file
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
# Resolves the lambda config directory.
|
|
31
39
|
def self.lambda_config_dir
|
|
32
40
|
File.join(root, 'config/lambda')
|
|
@@ -35,6 +43,7 @@ module Belt
|
|
|
35
43
|
def self.detect_root
|
|
36
44
|
dir = Dir.pwd
|
|
37
45
|
loop do
|
|
46
|
+
return dir if File.exist?(File.join(dir, 'config/routes.rb'))
|
|
38
47
|
return dir if File.exist?(File.join(dir, 'config/routes.tf.rb'))
|
|
39
48
|
return dir if File.exist?(File.join(dir, 'infrastructure/routes.tf.rb'))
|
|
40
49
|
|
data/lib/belt/route_dsl.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative 'inflector'
|
|
|
5
5
|
module Belt
|
|
6
6
|
# DSL for defining API Gateway routes.
|
|
7
7
|
# Ported from terraform-provider-conveyor-belt/scripts/lib/route_dsl.rb
|
|
8
|
-
# so that `belt routes` can parse routes.
|
|
8
|
+
# so that `belt routes` can parse routes.rb without external dependencies.
|
|
9
9
|
|
|
10
10
|
class Route
|
|
11
11
|
attr_reader :method, :path, :auth, :lambda, :cors, :tables, :route_type,
|
|
@@ -518,7 +518,7 @@ module Belt
|
|
|
518
518
|
end
|
|
519
519
|
end
|
|
520
520
|
|
|
521
|
-
# SchemaBuilder captures request and response model definitions from
|
|
521
|
+
# SchemaBuilder captures request and response model definitions from contracts.rb
|
|
522
522
|
class SchemaBuilder
|
|
523
523
|
SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
|
|
524
524
|
|
data/lib/belt/version.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
terraform {
|
|
2
2
|
backend "s3" {
|
|
3
|
-
|
|
4
|
-
bucket = "belt-terraform-state"
|
|
3
|
+
bucket = "<%= @state_bucket %>"
|
|
5
4
|
key = "<%= s3_safe_name(@app_name) %>/<%= @env_name %>/terraform.tfstate"
|
|
6
5
|
region = "us-east-1"
|
|
7
6
|
encrypt = true
|
|
@@ -9,7 +9,8 @@ resource "random_string" "frontend_suffix" {
|
|
|
9
9
|
|
|
10
10
|
# S3 bucket for frontend static assets
|
|
11
11
|
resource "aws_s3_bucket" "frontend" {
|
|
12
|
-
bucket
|
|
12
|
+
bucket = "<%= s3_safe_name(@app_name) %>-frontend-${var.environment}-${random_string.frontend_suffix.result}"
|
|
13
|
+
force_destroy = true
|
|
13
14
|
|
|
14
15
|
lifecycle {
|
|
15
16
|
ignore_changes = [bucket]
|
|
@@ -9,7 +9,8 @@ resource "random_string" "frontend_suffix" {
|
|
|
9
9
|
|
|
10
10
|
# S3 bucket for frontend static assets
|
|
11
11
|
resource "aws_s3_bucket" "frontend" {
|
|
12
|
-
bucket
|
|
12
|
+
bucket = "${var.app_name}-frontend-${var.environment}-${random_string.frontend_suffix.result}"
|
|
13
|
+
force_destroy = true
|
|
13
14
|
|
|
14
15
|
lifecycle {
|
|
15
16
|
ignore_changes = [bucket]
|
|
@@ -22,7 +22,7 @@ resource "conveyor_belt" "main" {
|
|
|
22
22
|
provider = conveyor-belt
|
|
23
23
|
|
|
24
24
|
# path.module is infrastructure/modules/app — climb three levels to project root
|
|
25
|
-
source = "${path.module}/../../../config/routes.
|
|
25
|
+
source = "${path.module}/../../../config/routes.rb"
|
|
26
26
|
app_name = var.app_name
|
|
27
27
|
lambda_source_dir = "${path.module}/../../../lambda"
|
|
28
28
|
lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "views"]
|
|
@@ -6,7 +6,7 @@ This file explains the project structure, tooling, and conventions for AI agents
|
|
|
6
6
|
|
|
7
7
|
- **Belt** — CLI and runtime framework (like Rails for serverless). Provides Lambda handler, action router, controller base class, and CLI tooling.
|
|
8
8
|
- **ActiveItem** — ActiveRecord-like ORM for DynamoDB. Models inherit from `ActiveItem::Base`.
|
|
9
|
-
- **Conveyor Belt** — Terraform provider that reads a Ruby DSL (`routes.
|
|
9
|
+
- **Conveyor Belt** — Terraform provider that reads a Ruby DSL (`routes.rb`) and creates API Gateway + Lambda + IAM infrastructure.
|
|
10
10
|
- **Lambda Loadout** — Lambda cold-start optimizer (auto-required by Belt).
|
|
11
11
|
|
|
12
12
|
## Project Structure
|
|
@@ -23,8 +23,8 @@ This file explains the project structure, tooling, and conventions for AI agents
|
|
|
23
23
|
│ ├── lib/routes/ # Route manifests (auto-generated)
|
|
24
24
|
│ └── Gemfile # Lambda-specific dependencies
|
|
25
25
|
├── config/
|
|
26
|
-
│ ├── routes.
|
|
27
|
-
│ ├──
|
|
26
|
+
│ ├── routes.rb # API routes (Conveyor Belt DSL)
|
|
27
|
+
│ ├── contracts.rb # API request/response contracts
|
|
28
28
|
│ └── lambda/ # Per-lambda config (like database.yml)
|
|
29
29
|
│ └── api.yml # Lambda timeout, memory, env vars
|
|
30
30
|
├── infrastructure/
|
|
@@ -53,7 +53,7 @@ belt output <env> # terraform output
|
|
|
53
53
|
|
|
54
54
|
## How Routing Works
|
|
55
55
|
|
|
56
|
-
1. `config/routes.
|
|
56
|
+
1. `config/routes.rb` defines routes using a DSL:
|
|
57
57
|
```ruby
|
|
58
58
|
Belt.application.routes.draw do
|
|
59
59
|
namespace :<%= @app_name %> do
|
|
@@ -6,8 +6,8 @@ A serverless application built with [Belt](https://github.com/stowzilla/belt) an
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
├── config/
|
|
9
|
-
│ ├── routes.
|
|
10
|
-
│ ├──
|
|
9
|
+
│ ├── routes.rb # API route definitions
|
|
10
|
+
│ ├── contracts.rb # API request/response contracts
|
|
11
11
|
│ └── lambda/ # Per-lambda config (timeout, memory, env vars)
|
|
12
12
|
│ └── api.yml
|
|
13
13
|
├── infrastructure/ # Terraform environments
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# AGENTS.md — <%= gem_name %>
|
|
2
|
+
|
|
3
|
+
This file is for AI agents (and humans) working **on this Belt plugin gem**, not on a host Belt app.
|
|
4
|
+
|
|
5
|
+
Host apps get their own `AGENTS.md` from `belt new`. Core belt docs live in the [belt](https://github.com/stowzilla/belt) repo (`README.md` + root `AGENTS.md`).
|
|
6
|
+
|
|
7
|
+
## What this gem is
|
|
8
|
+
|
|
9
|
+
**<%= gem_name %>** is a Belt plugin: optional capability that installs into Belt apps via the generator API.
|
|
10
|
+
|
|
11
|
+
- **Runtime API** lives in the gem (`<%= constant_path %>`) — apps require the gem and call it directly
|
|
12
|
+
- **Generator** copies only what the host app must own (Terraform, Lambda entrypoints, optional overrides)
|
|
13
|
+
- After the gem is in an app's Gemfile, `belt generate <%= plugin_name %>` / `belt destroy <%= plugin_name %>` work automatically
|
|
14
|
+
|
|
15
|
+
## Stack
|
|
16
|
+
|
|
17
|
+
| Piece | Role |
|
|
18
|
+
|-------|------|
|
|
19
|
+
| **belt** | Host framework + CLI; discovers this generator via `GeneratorRegistry` |
|
|
20
|
+
| **<%= gem_name %>** (this repo) | Plugin runtime + `belt generate <%= plugin_name %>` |
|
|
21
|
+
| Host Belt app | Where Terraform / Lambda config land after generation |
|
|
22
|
+
|
|
23
|
+
## Layout
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
<%= gem_name %>/
|
|
27
|
+
├── <%= gem_name %>.gemspec
|
|
28
|
+
├── lib/
|
|
29
|
+
│ ├── <%= gem_name %>.rb # require entrypoint
|
|
30
|
+
│ └── belt/
|
|
31
|
+
│ ├── <%= underscored %>.rb # <%= constant_path %> API
|
|
32
|
+
│ ├── <%= underscored %>/
|
|
33
|
+
│ │ ├── configuration.rb
|
|
34
|
+
│ │ ├── version.rb
|
|
35
|
+
│ │ └── templates/ # ERB used by the generator
|
|
36
|
+
│ └── generators/
|
|
37
|
+
│ └── <%= underscored %>_generator.rb # ← auto-discovered
|
|
38
|
+
├── spec/
|
|
39
|
+
├── README.md
|
|
40
|
+
└── AGENTS.md # This file
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Do not rename** `lib/belt/generators/<%= underscored %>_generator.rb` or the class
|
|
44
|
+
`Belt::Generators::<%= generator_class %>` — discovery depends on that path and name.
|
|
45
|
+
|
|
46
|
+
## Generator contract
|
|
47
|
+
|
|
48
|
+
Implemented in `lib/belt/generators/<%= underscored %>_generator.rb`:
|
|
49
|
+
|
|
50
|
+
| Method | Required? | Purpose |
|
|
51
|
+
|--------|-----------|---------|
|
|
52
|
+
| `.run(args)` | **Yes** | Install into the current Belt app |
|
|
53
|
+
| `.destroy(args)` | Recommended | Tear down what generate created |
|
|
54
|
+
| `.description` | Recommended | One-liner for `belt generate --help` |
|
|
55
|
+
|
|
56
|
+
Class must live in `Belt::Generators` and be named `<%= generator_class %>`.
|
|
57
|
+
|
|
58
|
+
Reference implementations (copy structure, don't invent a parallel one):
|
|
59
|
+
|
|
60
|
+
- `belt-messaging` — SMS via AWS End User Messaging
|
|
61
|
+
- `belt-pay` — Stripe payments & subscriptions
|
|
62
|
+
|
|
63
|
+
## Generator checklist
|
|
64
|
+
|
|
65
|
+
When fleshing out the generator, typically install:
|
|
66
|
+
|
|
67
|
+
1. **Terraform module** → `infrastructure/modules/<%= plugin_name %>/` (`main.tf`, `variables.tf`, `outputs.tf`)
|
|
68
|
+
2. **Lambda config** → `config/lambda/<%= plugin_name %>.yml` (timeout, memory, env, triggers)
|
|
69
|
+
3. **Lambda entrypoint** → `lambda/<%= plugin_name %>.rb` using `Belt::LambdaHandler`
|
|
70
|
+
4. **Routes / schema** → inject into `config/routes.rb` or `config/contracts.rb` when needed
|
|
71
|
+
5. **Optional overrides** → `--controllers` (or similar) for app-local subclasses — keep defaults in the gem
|
|
72
|
+
6. **Destroy path** → `belt destroy <%= plugin_name %>` removes generated artifacts
|
|
73
|
+
7. **Help** → `.description` plus `--help` / `-h` explaining files and next steps
|
|
74
|
+
|
|
75
|
+
**Principle:** runtime stays in the gem; generators copy only host-owned infra and optional overrides (same idea as `rails g devise:views`).
|
|
76
|
+
|
|
77
|
+
## Local development
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bundle install
|
|
81
|
+
bundle exec rspec
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Wire into a Belt app while iterating:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# In the app Gemfile
|
|
88
|
+
gem "<%= gem_name %>", path: "../<%= gem_name %>"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cd /path/to/belt-app
|
|
93
|
+
bundle install
|
|
94
|
+
belt generate --help # should list "<%= plugin_name %>" under Gem Generators
|
|
95
|
+
belt generate <%= plugin_name %>
|
|
96
|
+
belt destroy <%= plugin_name %>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`belt deploy` vendors `path:` gems into `vendor/cache` so Lambda packaging works without publishing.
|
|
100
|
+
|
|
101
|
+
## Configuration pattern
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
<%= constant_path %>.configure do |config|
|
|
105
|
+
# config.option = value
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Extend `lib/belt/<%= underscored %>/configuration.rb` and document options in README.
|
|
110
|
+
|
|
111
|
+
## Conventions
|
|
112
|
+
|
|
113
|
+
1. Follow patterns from `belt-messaging` / `belt-pay` before inventing new structure
|
|
114
|
+
2. Keep the generator idempotent where practical; support `--force` for overwrites
|
|
115
|
+
3. Specs under `spec/` for configuration and any non-trivial generator logic
|
|
116
|
+
4. User-facing changes → `CHANGELOG.md`
|
|
117
|
+
5. Never commit secrets, API keys, or real AWS account IDs
|
|
118
|
+
6. Prefer gem defaults + optional generate flags over dumping all code into the app
|
|
119
|
+
|
|
120
|
+
## Where to look first
|
|
121
|
+
|
|
122
|
+
| Task | Start here |
|
|
123
|
+
|------|------------|
|
|
124
|
+
| Runtime API | `lib/belt/<%= underscored %>.rb`, `lib/belt/<%= underscored %>/` |
|
|
125
|
+
| What gets installed into apps | `lib/belt/generators/<%= underscored %>_generator.rb` |
|
|
126
|
+
| Generator ERB templates | `lib/belt/<%= underscored %>/templates/` |
|
|
127
|
+
| Version / gemspec | `lib/belt/<%= underscored %>/version.rb`, `<%= gem_name %>.gemspec` |
|
|
128
|
+
| How belt discovers this | belt gem: `Belt::CLI::GeneratorRegistry` |
|
|
129
|
+
|
|
130
|
+
## Do not
|
|
131
|
+
|
|
132
|
+
- Move the generator file out of `lib/belt/generators/*_generator.rb`
|
|
133
|
+
- Require a manual initializer in the host app for registration — discovery is automatic
|
|
134
|
+
- Put host-app deploy runbooks here — those belong in the app's `AGENTS.md`
|
|
135
|
+
- Duplicate large runtime classes into generated app files unless the user opts in (e.g. `--controllers`)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) <%= Time.now.year %>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# <%= gem_name %>
|
|
2
|
+
|
|
3
|
+
<%= summary %>.
|
|
4
|
+
|
|
5
|
+
A [Belt](https://github.com/stowzilla/belt) plugin. Add the gem to a Belt app, then run the generator to install infrastructure and app wiring.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Belt app's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem '<%= gem_name %>'
|
|
13
|
+
# Or during development:
|
|
14
|
+
# gem '<%= gem_name %>', path: '../<%= gem_name %>'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
21
|
+
belt generate <%= plugin_name %>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What You Get
|
|
25
|
+
|
|
26
|
+
### From the gem (no generation needed)
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Configure
|
|
30
|
+
<%= constant_path %>.configure do |config|
|
|
31
|
+
# config.option = value
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### From the generator (`belt g <%= plugin_name %>`)
|
|
36
|
+
|
|
37
|
+
Edit `lib/belt/generators/<%= underscored %>_generator.rb` and templates under
|
|
38
|
+
`lib/belt/<%= underscored %>/templates/` to define what gets installed into apps.
|
|
39
|
+
|
|
40
|
+
Typical plugin generators create some combination of:
|
|
41
|
+
|
|
42
|
+
- **Terraform module** — `infrastructure/modules/<%= plugin_name %>/`
|
|
43
|
+
- **Lambda entry point** — `lambda/<%= plugin_name %>.rb`
|
|
44
|
+
- **Lambda config** — `config/lambda/<%= plugin_name %>.yml`
|
|
45
|
+
- **Route / schema injection** — updates to `config/routes.rb` / `config/contracts.rb`
|
|
46
|
+
- **Optional controller overrides** — `belt g <%= plugin_name %> --controllers`
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bundle install
|
|
52
|
+
bundle exec rspec
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Removing
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
belt destroy <%= plugin_name %>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/belt/<%= underscored %>/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = '<%= gem_name %>'
|
|
7
|
+
spec.version = <%= constant_path %>::VERSION
|
|
8
|
+
spec.authors = ['Your Name']
|
|
9
|
+
spec.email = ['you@example.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = '<%= summary %>'
|
|
12
|
+
spec.description = '<%= summary %>. A Belt plugin gem — add to a Belt app Gemfile, then run `belt generate <%= plugin_name %>`.'
|
|
13
|
+
spec.homepage = 'https://github.com/example/<%= gem_name %>'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.3'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
20
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
21
|
+
|
|
22
|
+
spec.files = Dir['lib/**/*', 'LICENSE', 'README.md', 'CHANGELOG.md']
|
|
23
|
+
spec.require_paths = ['lib']
|
|
24
|
+
|
|
25
|
+
spec.add_dependency 'belt', '~> 0.2'
|
|
26
|
+
end
|