belt 0.0.7 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +29 -1
  4. data/README.md +150 -51
  5. data/exe/belt +6 -0
  6. data/lib/belt/action_router.rb +7 -1
  7. data/lib/belt/cli/app_detection.rb +16 -0
  8. data/lib/belt/cli/bucket_security.rb +122 -0
  9. data/lib/belt/cli/env_resolver.rb +15 -0
  10. data/lib/belt/cli/environment_command.rb +77 -0
  11. data/lib/belt/cli/frontend_command.rb +85 -0
  12. data/lib/belt/cli/frontend_deploy_command.rb +125 -0
  13. data/lib/belt/cli/frontend_setup_command.rb +64 -0
  14. data/lib/belt/cli/generate_command.rb +206 -0
  15. data/lib/belt/cli/new_command.rb +126 -0
  16. data/lib/belt/cli/routes_command/route_inference.rb +100 -0
  17. data/lib/belt/cli/routes_command/schema_loader.rb +71 -0
  18. data/lib/belt/cli/routes_command.rb +307 -0
  19. data/lib/belt/cli/setup_command.rb +261 -0
  20. data/lib/belt/cli/tables_command.rb +138 -0
  21. data/lib/belt/cli/tasks_command.rb +110 -0
  22. data/lib/belt/cli/terraform_command.rb +77 -0
  23. data/lib/belt/cli/views_command.rb +134 -0
  24. data/lib/belt/cli.rb +117 -0
  25. data/lib/belt/lambda_handler.rb +16 -0
  26. data/lib/belt/root.rb +26 -0
  27. data/lib/belt/route_dsl.rb +605 -0
  28. data/lib/belt/table_inference.rb +71 -0
  29. data/lib/belt/version.rb +1 -1
  30. data/lib/belt.rb +1 -0
  31. data/lib/templates/environment/backend.tf.erb +8 -0
  32. data/lib/templates/environment/main.tf.erb +42 -0
  33. data/lib/templates/environment/terraform.tfvars.erb +1 -0
  34. data/lib/templates/environment/variables.tf.erb +16 -0
  35. data/lib/templates/frontend/react/index.html.erb +12 -0
  36. data/lib/templates/frontend/react/package.json.erb +20 -0
  37. data/lib/templates/frontend/react/src/App.jsx +14 -0
  38. data/lib/templates/frontend/react/src/index.css +10 -0
  39. data/lib/templates/frontend/react/src/lib/apiClient.js.erb +19 -0
  40. data/lib/templates/frontend/react/src/main.jsx +10 -0
  41. data/lib/templates/frontend/react/src/pages/Home.jsx.erb +10 -0
  42. data/lib/templates/frontend/react/vite.config.js +8 -0
  43. data/lib/templates/frontend_infra/frontend.tf.erb +159 -0
  44. data/lib/templates/generate/controller.rb.erb +59 -0
  45. data/lib/templates/generate/model.rb.erb +20 -0
  46. data/lib/templates/new_app/AGENTS.md.erb +130 -0
  47. data/lib/templates/new_app/Gemfile.erb +5 -0
  48. data/lib/templates/new_app/README.md.erb +25 -0
  49. data/lib/templates/new_app/Rakefile.erb +12 -0
  50. data/lib/templates/new_app/gitignore.erb +14 -0
  51. data/lib/templates/new_app/infrastructure/routes.tf.rb.erb +5 -0
  52. data/lib/templates/new_app/infrastructure/schema.tf.rb.erb +9 -0
  53. data/lib/templates/new_app/lambda/Gemfile.erb +7 -0
  54. data/lib/templates/new_app/lambda/api.rb.erb +22 -0
  55. data/lib/templates/new_app/lambda/controllers/application_controller.rb.erb +6 -0
  56. data/lib/templates/new_app/lambda/lib/routes/routes.rb.erb +11 -0
  57. data/lib/templates/new_app/lambda/models/application_record.rb.erb +6 -0
  58. data/lib/templates/new_app/lambda/models/concerns/timestampable.rb.erb +23 -0
  59. data/lib/templates/views/Edit.jsx.erb +38 -0
  60. data/lib/templates/views/Form.jsx.erb +34 -0
  61. data/lib/templates/views/Index.jsx.erb +39 -0
  62. data/lib/templates/views/New.jsx.erb +26 -0
  63. data/lib/templates/views/Show.jsx.erb +46 -0
  64. data.tar.gz.sig +0 -0
  65. metadata +73 -3
  66. metadata.gz.sig +0 -0
@@ -0,0 +1,42 @@
1
+ terraform {
2
+ required_providers {
3
+ aws = {
4
+ source = "hashicorp/aws"
5
+ version = "~> 5.0"
6
+ }
7
+ random = {
8
+ source = "hashicorp/random"
9
+ version = "~> 3.0"
10
+ }
11
+ dispatcher = {
12
+ source = "terraform.local/stowzilla/dispatcher"
13
+ version = "99.0.0"
14
+ }
15
+ }
16
+ }
17
+
18
+ provider "aws" {
19
+ region = var.aws_region
20
+
21
+ default_tags {
22
+ tags = {
23
+ Project = "<%= @app_name.capitalize %>"
24
+ Environment = var.environment
25
+ ManagedBy = "Terraform"
26
+ }
27
+ }
28
+ }
29
+
30
+ provider "dispatcher" {
31
+ environment = var.environment
32
+ aws_region = var.aws_region
33
+ }
34
+
35
+ resource "dispatcher" "main" {
36
+ source = "${path.module}/../routes.tf.rb"
37
+ app_name = var.app_name
38
+ lambda_source_dir = "${path.module}/../../lambda"
39
+ lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "templates"]
40
+ frontend_urls = ["http://localhost:3000"]
41
+ friendly_errors = true
42
+ }
@@ -0,0 +1 @@
1
+ environment = "<%= @env_name %>"
@@ -0,0 +1,16 @@
1
+ variable "app_name" {
2
+ description = "Name of the application"
3
+ type = string
4
+ default = "<%= @app_name %>"
5
+ }
6
+
7
+ variable "environment" {
8
+ description = "Environment name"
9
+ type = string
10
+ }
11
+
12
+ variable "aws_region" {
13
+ description = "AWS region"
14
+ type = string
15
+ default = "us-east-1"
16
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title><%= @module_name %></title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "<%= @app_name %>",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1",
14
+ "react-router-dom": "^6.28.0"
15
+ },
16
+ "devDependencies": {
17
+ "@vitejs/plugin-react": "^4.3.4",
18
+ "vite": "^6.0.0"
19
+ }
20
+ }
@@ -0,0 +1,14 @@
1
+ import { BrowserRouter, Routes, Route } from 'react-router-dom'
2
+ import Home from './pages/Home'
3
+
4
+ function App() {
5
+ return (
6
+ <BrowserRouter>
7
+ <Routes>
8
+ <Route path="/" element={<Home />} />
9
+ </Routes>
10
+ </BrowserRouter>
11
+ )
12
+ }
13
+
14
+ export default App
@@ -0,0 +1,10 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, sans-serif;
3
+ line-height: 1.5;
4
+ color: #213547;
5
+ background-color: #ffffff;
6
+ }
7
+
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ body { min-height: 100vh; }
@@ -0,0 +1,19 @@
1
+ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'
2
+
3
+ export async function apiClient(path, options = {}) {
4
+ const { method = 'GET', body, headers = {} } = options
5
+
6
+ const config = {
7
+ method,
8
+ headers: { 'Content-Type': 'application/json', ...headers }
9
+ }
10
+
11
+ if (body) config.body = JSON.stringify(body)
12
+
13
+ const response = await fetch(`${API_URL}${path}`, config)
14
+ const data = await response.json()
15
+
16
+ if (!response.ok) throw new Error(data.error || `Request failed: ${response.status}`)
17
+
18
+ return data
19
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ )
@@ -0,0 +1,10 @@
1
+ function Home() {
2
+ return (
3
+ <main style={{ padding: '2rem', maxWidth: '800px', margin: '0 auto' }}>
4
+ <h1><%= @module_name %></h1>
5
+ <p>Your Belt app is running. Edit <code>src/pages/Home.jsx</code> to get started.</p>
6
+ </main>
7
+ )
8
+ }
9
+
10
+ export default Home
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: { port: 3000 },
7
+ build: { outDir: 'dist' }
8
+ })
@@ -0,0 +1,159 @@
1
+ # Auto-generated by Belt — frontend hosting infrastructure
2
+ # Re-run `belt setup frontend <%= @env %>` to regenerate
3
+
4
+ resource "random_string" "frontend_suffix" {
5
+ length = 8
6
+ special = false
7
+ upper = false
8
+ }
9
+
10
+ # S3 bucket for frontend static assets
11
+ resource "aws_s3_bucket" "frontend" {
12
+ bucket = "<%= @app_name %>-frontend-${var.environment}-${random_string.frontend_suffix.result}"
13
+
14
+ lifecycle {
15
+ ignore_changes = [bucket]
16
+ }
17
+ }
18
+
19
+ resource "aws_s3_bucket_public_access_block" "frontend" {
20
+ bucket = aws_s3_bucket.frontend.id
21
+
22
+ block_public_acls = true
23
+ block_public_policy = true
24
+ ignore_public_acls = true
25
+ restrict_public_buckets = true
26
+ }
27
+
28
+ resource "aws_s3_bucket_server_side_encryption_configuration" "frontend" {
29
+ bucket = aws_s3_bucket.frontend.id
30
+
31
+ rule {
32
+ apply_server_side_encryption_by_default {
33
+ sse_algorithm = "AES256"
34
+ }
35
+ bucket_key_enabled = true
36
+ }
37
+ }
38
+
39
+ resource "aws_s3_bucket_website_configuration" "frontend" {
40
+ bucket = aws_s3_bucket.frontend.id
41
+
42
+ index_document {
43
+ suffix = "index.html"
44
+ }
45
+
46
+ error_document {
47
+ key = "index.html"
48
+ }
49
+ }
50
+
51
+ # CloudFront OAC — restricts S3 access to CloudFront only
52
+ resource "aws_cloudfront_origin_access_control" "frontend" {
53
+ name = "<%= @app_name %>-${var.environment}-frontend-oac"
54
+ description = "OAC for frontend bucket"
55
+ origin_access_control_origin_type = "s3"
56
+ signing_behavior = "always"
57
+ signing_protocol = "sigv4"
58
+ }
59
+
60
+ # CloudFront distribution
61
+ resource "aws_cloudfront_distribution" "frontend" {
62
+ origin {
63
+ domain_name = aws_s3_bucket.frontend.bucket_regional_domain_name
64
+ origin_id = "S3-${aws_s3_bucket.frontend.id}"
65
+ origin_access_control_id = aws_cloudfront_origin_access_control.frontend.id
66
+ }
67
+
68
+ enabled = true
69
+ is_ipv6_enabled = true
70
+ default_root_object = "index.html"
71
+
72
+ default_cache_behavior {
73
+ allowed_methods = ["GET", "HEAD", "OPTIONS"]
74
+ cached_methods = ["GET", "HEAD"]
75
+ target_origin_id = "S3-${aws_s3_bucket.frontend.id}"
76
+ compress = true
77
+ viewer_protocol_policy = "redirect-to-https"
78
+
79
+ forwarded_values {
80
+ query_string = false
81
+ cookies {
82
+ forward = "none"
83
+ }
84
+ }
85
+ }
86
+
87
+ # SPA routing — serve index.html for all 404/403 paths
88
+ custom_error_response {
89
+ error_code = 404
90
+ response_code = 200
91
+ response_page_path = "/index.html"
92
+ }
93
+
94
+ custom_error_response {
95
+ error_code = 403
96
+ response_code = 200
97
+ response_page_path = "/index.html"
98
+ }
99
+
100
+ restrictions {
101
+ geo_restriction {
102
+ restriction_type = "none"
103
+ }
104
+ }
105
+
106
+ viewer_certificate {
107
+ cloudfront_default_certificate = true
108
+ }
109
+
110
+ tags = {
111
+ Name = "<%= @app_name %>-${var.environment}-frontend"
112
+ Environment = var.environment
113
+ ManagedBy = "Terraform"
114
+ }
115
+ }
116
+
117
+ # S3 bucket policy — allow CloudFront OAC access only
118
+ resource "aws_s3_bucket_policy" "frontend" {
119
+ bucket = aws_s3_bucket.frontend.id
120
+
121
+ policy = jsonencode({
122
+ Version = "2012-10-17"
123
+ Statement = [
124
+ {
125
+ Sid = "AllowCloudFrontServicePrincipal"
126
+ Effect = "Allow"
127
+ Principal = {
128
+ Service = "cloudfront.amazonaws.com"
129
+ }
130
+ Action = "s3:GetObject"
131
+ Resource = "${aws_s3_bucket.frontend.arn}/*"
132
+ Condition = {
133
+ StringEquals = {
134
+ "AWS:SourceArn" = aws_cloudfront_distribution.frontend.arn
135
+ }
136
+ }
137
+ }
138
+ ]
139
+ })
140
+
141
+ depends_on = [aws_s3_bucket_public_access_block.frontend]
142
+ }
143
+
144
+ # Outputs for belt deploy
145
+ output "frontend_bucket_name" {
146
+ value = aws_s3_bucket.frontend.id
147
+ }
148
+
149
+ output "frontend_distribution_id" {
150
+ value = aws_cloudfront_distribution.frontend.id
151
+ }
152
+
153
+ output "frontend_url" {
154
+ value = "https://${aws_cloudfront_distribution.frontend.domain_name}"
155
+ }
156
+
157
+ output "api_url" {
158
+ value = values(dispatcher.main.api_gateway_urls)[0]
159
+ }
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'application_controller'
4
+ require_relative '../../models/<%= @singular_name %>'
5
+
6
+ module <%= @module_name %>Controllers
7
+ class <%= @class_name %>sController < ApplicationController
8
+ # GET /<%= @resource_name %>
9
+ def index
10
+ <%= @resource_name %> = <%= @class_name %>.all
11
+ success_response(<%= @resource_name %>: <%= @resource_name %>.map(&:to_h))
12
+ end
13
+
14
+ # POST /<%= @resource_name %>
15
+ def create
16
+ <%= @singular_name %> = <%= @class_name %>.new(<%= @fields.map { |f| "#{f[:name]}: params[:#{f[:name]}]" }.join(', ') %>)
17
+
18
+ if <%= @singular_name %>.save
19
+ success_response(<%= @singular_name %>: <%= @singular_name %>.to_h)
20
+ else
21
+ error_response(<%= @singular_name %>.errors.full_messages.join(', '), 422)
22
+ end
23
+ end
24
+
25
+ # GET /<%= @resource_name %>/:<%= @singular_name %>_id
26
+ def show
27
+ <%= @singular_name %> = <%= @class_name %>.find(params[:<%= @singular_name %>_id])
28
+ return error_response('<%= @class_name %> not found', 404) unless <%= @singular_name %>
29
+
30
+ success_response(<%= @singular_name %>: <%= @singular_name %>.to_h)
31
+ end
32
+
33
+ # PUT /<%= @resource_name %>/:<%= @singular_name %>_id
34
+ def update
35
+ <%= @singular_name %> = <%= @class_name %>.find(params[:<%= @singular_name %>_id])
36
+ return error_response('<%= @class_name %> not found', 404) unless <%= @singular_name %>
37
+
38
+ attrs = {}
39
+ <% @fields.each do |field| -%>
40
+ attrs[:<%= field[:name] %>] = params[:<%= field[:name] %>] if params.key?(:<%= field[:name] %>)
41
+ <% end -%>
42
+
43
+ if <%= @singular_name %>.update(attrs)
44
+ success_response(<%= @singular_name %>: <%= @singular_name %>.to_h)
45
+ else
46
+ error_response(<%= @singular_name %>.errors.full_messages.join(', '), 422)
47
+ end
48
+ end
49
+
50
+ # DELETE /<%= @resource_name %>/:<%= @singular_name %>_id
51
+ def destroy
52
+ <%= @singular_name %> = <%= @class_name %>.find(params[:<%= @singular_name %>_id])
53
+ return error_response('<%= @class_name %> not found', 404) unless <%= @singular_name %>
54
+
55
+ <%= @singular_name %>.destroy
56
+ success_response(message: '<%= @class_name %> deleted')
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @class_name %> < ApplicationRecord
4
+ include Concerns::Timestampable
5
+
6
+ <% @fields.each do |field| -%>
7
+ attr_accessor :<%= field[:name] %>
8
+ <% end -%>
9
+
10
+ def to_h
11
+ {
12
+ id: id,
13
+ <% @fields.each do |field| -%>
14
+ <%= field[:name] %>: <%= field[:name] %>,
15
+ <% end -%>
16
+ created_at: created_at,
17
+ updated_at: updated_at
18
+ }
19
+ end
20
+ end
@@ -0,0 +1,130 @@
1
+ # AGENTS.md — <%= @module_name %>
2
+
3
+ This file explains the project structure, tooling, and conventions for AI agents working in this codebase.
4
+
5
+ ## Stack
6
+
7
+ - **Belt** — CLI and runtime framework (like Rails for serverless). Provides Lambda handler, action router, controller base class, and CLI tooling.
8
+ - **ActiveItem** — ActiveRecord-like ORM for DynamoDB. Models inherit from `ActiveItem::Base`.
9
+ - **Dispatcher** — Terraform provider that reads a Ruby DSL (`routes.tf.rb`) and creates API Gateway + Lambda + IAM infrastructure.
10
+ - **Lambda Loadout** — Lambda cold-start optimizer (auto-required by Belt).
11
+
12
+ ## Project Structure
13
+
14
+ ```
15
+ <%= @app_name %>/
16
+ ├── lambda/
17
+ │ ├── <%= @app_name %>.rb # Lambda entry point
18
+ │ ├── controllers/<%= @app_name %>/ # Controllers (namespaced)
19
+ │ │ └── application_controller.rb # Base controller
20
+ │ ├── models/ # ActiveItem models
21
+ │ │ ├── application_record.rb # Base model
22
+ │ │ └── concerns/ # Shared model concerns
23
+ │ ├── lib/routes/ # Route manifests (auto-generated)
24
+ │ └── Gemfile # Lambda-specific dependencies
25
+ ├── infrastructure/
26
+ │ ├── routes.tf.rb # API routes (Dispatcher DSL)
27
+ │ ├── schema.tf.rb # Model schema definitions
28
+ │ └── <env>/ # Per-environment Terraform configs
29
+ ├── Gemfile # Project-level dependencies
30
+ └── AGENTS.md # This file
31
+ ```
32
+
33
+ ## Belt CLI Commands
34
+
35
+ ```bash
36
+ belt new <app_name> # Scaffold a new project
37
+ belt generate resource <name> <field:type ...> # Generate model + controller + routes
38
+ belt generate model <name> <field:type ...> # Generate model only
39
+ belt generate controller <name> # Generate controller only
40
+ belt generate environment <env_name> # Generate Terraform environment directory
41
+ belt setup state # Create/select S3 state bucket
42
+ belt setup tables <env> # Generate DynamoDB table definitions from schema
43
+ belt init <env> # terraform init
44
+ belt plan <env> # terraform plan
45
+ belt apply <env> # terraform apply
46
+ belt destroy <env> # terraform destroy
47
+ belt output <env> # terraform output
48
+ ```
49
+
50
+ ## How Routing Works
51
+
52
+ 1. `infrastructure/routes.tf.rb` defines routes using a DSL:
53
+ ```ruby
54
+ Belt.application.routes.draw do
55
+ namespace :<%= @app_name %> do
56
+ resources :things, tables: [:things]
57
+ end
58
+ end
59
+ ```
60
+
61
+ 2. Dispatcher creates an API Gateway where the namespace becomes a base path mapping. URLs look like:
62
+ ```
63
+ https://api.<env>.example.com/<%= @app_name %>/things
64
+ ```
65
+
66
+ 3. The Lambda entry point (`lambda/<%= @app_name %>.rb`) uses `Belt::ActionRouter` to dispatch requests to controllers based on the route manifest in `lambda/lib/routes/`.
67
+
68
+ 4. `resources :things` generates: `GET /things`, `POST /things`, `GET /things/:id`, `PUT /things/:id`, `DELETE /things/:id`. **PUT, not PATCH.**
69
+
70
+ ## How Models Work
71
+
72
+ Models use ActiveItem (DynamoDB ORM):
73
+
74
+ ```ruby
75
+ class Post < ApplicationRecord
76
+ attr_accessor :title, :content, :status
77
+ end
78
+ ```
79
+
80
+ Table names resolve as `{APP_NAME}-{ENVIRONMENT}-{pluralized_model}` (e.g., `<%= @app_name %>-wups-posts`).
81
+
82
+ Key ActiveItem methods: `create!`, `find`, `where`, `update`, `destroy`, `all`, `count`, `exists?`.
83
+
84
+ ## How Controllers Work
85
+
86
+ Controllers inherit from `BeltController::Base`:
87
+
88
+ ```ruby
89
+ module <%= @module_name %>Controllers
90
+ class ThingsController < ApplicationController
91
+ def index
92
+ items = Thing.all
93
+ success_response(things: items.map(&:to_h))
94
+ end
95
+
96
+ def show
97
+ item = Thing.find(params[:id])
98
+ success_response(thing: item.to_h)
99
+ end
100
+
101
+ def create
102
+ item = Thing.create!(params.slice(:title, :content))
103
+ success_response(thing: item.to_h, status: 201)
104
+ end
105
+ end
106
+ end
107
+ ```
108
+
109
+ `params` contains merged path parameters and parsed JSON body. Use `success_response` and `error_response` helpers.
110
+
111
+ ## Deployment Flow
112
+
113
+ ```bash
114
+ export AWS_PROFILE=<your_profile>
115
+ belt setup state # One-time: create S3 state bucket
116
+ belt generate environment <env> # One-time: scaffold Terraform configs
117
+ belt setup tables <env> # Generate DynamoDB table resources
118
+ belt init <env> # Initialize Terraform
119
+ belt apply <env> # Deploy everything
120
+ ```
121
+
122
+ ## Adding a New Resource
123
+
124
+ ```bash
125
+ belt generate resource comment body:text author:string post_id:string
126
+ belt setup tables <env>
127
+ belt apply <env>
128
+ ```
129
+
130
+ This creates the model, controller, updates routes + schema, generates the DynamoDB table definition, and deploys.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'belt'
@@ -0,0 +1,25 @@
1
+ # <%= @module_name %>
2
+
3
+ A serverless application built with [Belt](https://github.com/stowzilla/belt) and [ActiveItem](https://github.com/stowzilla/activeitem).
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ ├── infrastructure/
9
+ │ ├── routes.tf.rb # API route definitions
10
+ │ └── schema.tf.rb # Model schema definitions
11
+ └── lambda/
12
+ ├── api.rb # Lambda entry point
13
+ ├── controllers/<%= @app_name %>/ # Controllers
14
+ ├── models/ # ActiveItem models
15
+ └── lib/routes/ # Route manifest
16
+ ```
17
+
18
+ ## Development
19
+
20
+ ```bash
21
+ bundle install
22
+
23
+ # Deploy
24
+ cd infrastructure && terraform apply
25
+ ```
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ # Load rake tasks from all gems that ship them
6
+ Gem.loaded_specs.each_value do |spec|
7
+ tasks_dir = File.join(spec.full_gem_path, 'lib', 'tasks')
8
+ Dir.glob("#{tasks_dir}/**/*.rake").each { |r| load r } if Dir.exist?(tasks_dir)
9
+ end
10
+
11
+ # Load project-local tasks
12
+ Dir.glob('lib/tasks/**/*.rake').each { |r| load r }
@@ -0,0 +1,14 @@
1
+ /.bundle
2
+ /vendor/bundle
3
+ *.gem
4
+ Gemfile.lock
5
+
6
+ # Terraform
7
+ .terraform/
8
+ *.tfstate
9
+ *.tfstate.backup
10
+ .terraform.lock.hcl
11
+
12
+ # Lambda build artifacts
13
+ /lambda/vendor/
14
+ /lambda/.bundle/
@@ -0,0 +1,5 @@
1
+ Belt.application.routes.draw do
2
+ namespace :<%= @app_name %> do
3
+ # resources :posts
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ Belt.application.schema.draw do
2
+ # model :post do
3
+ # field :title, type: :string, required: true
4
+ # field :slug, type: :string, required: true
5
+ # field :content, type: :string
6
+ # field :status, type: :string, enum: %w[draft published]
7
+ # field :published_at, type: :string
8
+ # end
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'activeitem'
6
+ gem 'belt'
7
+ gem 'lambda_loadout'
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'belt'
4
+ require 'activeitem'
5
+
6
+ include Belt::LambdaHandler
7
+
8
+ ActiveItem.configure do |config|
9
+ config.table_prefix = ENV['APP_NAME']
10
+ config.environment = ENV['ENVIRONMENT']
11
+ end
12
+
13
+ require_relative 'lib/routes/<%= @app_name %>_routes'
14
+ <% @resources&.each do |r| -%>
15
+ require_relative 'controllers/<%= @app_name %>/<%= r %>_controller'
16
+ <% end -%>
17
+
18
+ ROUTER = Belt::ActionRouter.new(routes: Routes::<%= @module_name.upcase %>, namespace: '<%= @app_name %>')
19
+
20
+ def execute(path:, body:, event:)
21
+ ROUTER.route(event: event, body: body)
22
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= @module_name %>Controllers
4
+ class ApplicationController < BeltController::Base
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Routes
4
+ <%= @module_name.upcase %> = [
5
+ # { verb: 'GET', path: '/posts', controller: 'posts', action: 'index' },
6
+ # { verb: 'POST', path: '/posts', controller: 'posts', action: 'create' },
7
+ # { verb: 'GET', path: '/posts/{id}', controller: 'posts', action: 'show' },
8
+ # { verb: 'PUT', path: '/posts/{id}', controller: 'posts', action: 'update' },
9
+ # { verb: 'DELETE', path: '/posts/{id}', controller: 'posts', action: 'destroy' }
10
+ ].freeze
11
+ end