belt 0.1.5 → 0.1.7

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.
@@ -12,69 +12,17 @@ module Belt
12
12
  ASSETS_DIR = File.expand_path('../assets', __dir__)
13
13
 
14
14
  def show
15
- title = ENV.fetch('WELCOME_TITLE', 'Welcome to Belt')
16
- subtitle = ENV.fetch('WELCOME_SUBTITLE', 'API Gateway → Lambda → DynamoDB — all connected.')
17
- app_name = ENV.fetch('APP_NAME', 'my-app')
15
+ @title = ENV.fetch('WELCOME_TITLE', 'Welcome to Belt')
16
+ @subtitle = ENV.fetch('WELCOME_SUBTITLE', 'API Gateway → Lambda → DynamoDB — all connected.')
17
+ @css = welcome_css
18
+ @background_image = background_image_base64
19
+ @dynamodb_connected = dynamodb_connected?
18
20
 
19
- html_response(render_welcome_page(title: title, subtitle: subtitle, app_name: app_name))
21
+ render
20
22
  end
21
23
 
22
24
  private
23
25
 
24
- def render_welcome_page(title:, subtitle:, app_name:)
25
- <<~HTML
26
- <!DOCTYPE html>
27
- <html lang="en">
28
- <head>
29
- <meta charset="UTF-8">
30
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
31
- <title>#{escape_html(title)}</title>
32
- <style>#{welcome_css}</style>
33
- </head>
34
- <body>
35
- <div class="hero">
36
- <img src="data:image/png;base64,#{background_image_base64}" alt="Belt" class="hero-bg" />
37
- <div class="overlay">
38
- <h1>#{escape_html(title)}</h1>
39
- <p class="subtitle">#{escape_html(subtitle)}</p>
40
- </div>
41
- </div>
42
- <div class="container">
43
- <div class="stack-check">
44
- <div class="check-item check-pass">
45
- <span class="icon">✓</span>
46
- <span>API Gateway</span>
47
- </div>
48
- <div class="arrow">→</div>
49
- <div class="check-item check-pass">
50
- <span class="icon">✓</span>
51
- <span>Lambda</span>
52
- </div>
53
- <div class="arrow">→</div>
54
- #{dynamodb_check_html}
55
- </div>
56
- <div class="next-steps">
57
- <h2>Next Steps</h2>
58
- <ol>
59
- <li>Generate a resource: <code>belt g resource post title:string body:text</code></li>
60
- <li>Deploy: <code>belt deploy</code></li>
61
- <li>This page will be replaced once you define your own root route.</li>
62
- </ol>
63
- </div>
64
- </div>
65
- </body>
66
- </html>
67
- HTML
68
- end
69
-
70
- def dynamodb_check_html
71
- if dynamodb_connected?
72
- '<div class="check-item check-pass"><span class="icon">✓</span><span>DynamoDB</span></div>'
73
- else
74
- '<div class="check-item check-warn"><span class="icon">⚠</span><span>DynamoDB</span></div>'
75
- end
76
- end
77
-
78
26
  def dynamodb_connected?
79
27
  return false unless defined?(Aws::DynamoDB::Client)
80
28
 
@@ -85,17 +33,13 @@ module Belt
85
33
  false
86
34
  end
87
35
 
88
- def escape_html(text)
89
- text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
90
- end
91
-
92
36
  def welcome_css
93
- @welcome_css ||= File.read(File.join(ASSETS_DIR, 'welcome.css'))
37
+ @welcome_css_content ||= File.read(File.join(ASSETS_DIR, 'welcome.css'))
94
38
  end
95
39
 
96
40
  def background_image_base64
97
- @background_image_base64 ||= Base64.strict_encode64(
98
- File.binread(File.join(ASSETS_DIR, 'belt-default.png'))
41
+ @background_image_content ||= Base64.strict_encode64(
42
+ File.binread(File.join(ASSETS_DIR, 'belt-default.jpg'))
99
43
  )
100
44
  end
101
45
  end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'cgi'
5
+
6
+ module Belt
7
+ # Provides Rails-like ERB rendering for BeltController.
8
+ #
9
+ # Convention: templates live at `views/<controller_name>/<action>.html.erb`
10
+ # relative to the controller's gem or app directory.
11
+ #
12
+ # Usage:
13
+ # class WelcomeController < BeltController::Base
14
+ # def show
15
+ # @title = "Hello"
16
+ # render # auto-resolves to views/welcome/show.html.erb
17
+ # end
18
+ # end
19
+ #
20
+ # Or explicit:
21
+ # render template: "welcome/show"
22
+ # render inline: "<h1><%= @title %></h1>"
23
+ #
24
+ module Rendering
25
+ def self.included(base)
26
+ base.extend(ClassMethods)
27
+ end
28
+
29
+ module ClassMethods
30
+ # Override in subclasses to set a custom view path.
31
+ # Defaults to `views/` relative to the file that defines the controller.
32
+ def view_paths
33
+ @view_paths ||= []
34
+ end
35
+
36
+ def prepend_view_path(path)
37
+ view_paths.unshift(path)
38
+ end
39
+
40
+ def append_view_path(path)
41
+ view_paths << path
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # Render an ERB template and return an html_response.
48
+ #
49
+ # Options:
50
+ # render — auto-resolve from controller/action
51
+ # render template: "ctrl/action" — explicit template path (no extension)
52
+ # render inline: "<%= @x %>" — render a string as ERB
53
+ # render status: 201 — custom status code
54
+ # render layout: false — skip layout (default: no layout)
55
+ #
56
+ def render(options = {})
57
+ status = options.fetch(:status, 200)
58
+
59
+ html = if options.key?(:inline)
60
+ render_erb_string(options[:inline])
61
+ else
62
+ template_path = resolve_template_path(options[:template])
63
+ render_erb_file(template_path)
64
+ end
65
+
66
+ html_response(html, status)
67
+ end
68
+
69
+ def render_erb_string(source)
70
+ erb = ERB.new(source, trim_mode: '-')
71
+ erb.result(binding)
72
+ end
73
+
74
+ def render_erb_file(path)
75
+ raise Belt::TemplateNotFound, "Template not found: #{path}" unless File.exist?(path)
76
+
77
+ source = File.read(path)
78
+ erb = ERB.new(source, trim_mode: '-')
79
+ erb.filename = path
80
+ erb.result(binding)
81
+ end
82
+
83
+ # HTML-escape helper, available in templates as `h(value)`
84
+ def h(text)
85
+ CGI.escapeHTML(text.to_s)
86
+ end
87
+
88
+ def resolve_template_path(explicit_template = nil)
89
+ template_name = explicit_template || default_template_name
90
+ search_paths = build_view_search_paths
91
+
92
+ search_paths.each do |base|
93
+ full_path = File.join(base, "#{template_name}.html.erb")
94
+ return full_path if File.exist?(full_path)
95
+ end
96
+
97
+ # If nothing found, return the first candidate so the error message is useful
98
+ File.join(search_paths.first || '.', "#{template_name}.html.erb")
99
+ end
100
+
101
+ def default_template_name
102
+ # PostsController → posts, Belt::WelcomeController → welcome
103
+ controller = self.class.name.split('::').last
104
+ .sub(/Controller$/, '')
105
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
106
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
107
+ .downcase
108
+ "#{controller}/#{action_name}"
109
+ end
110
+
111
+ def build_view_search_paths
112
+ paths = []
113
+
114
+ # 1. Class-level view paths (set by the controller or gem)
115
+ paths.concat(self.class.view_paths) if self.class.respond_to?(:view_paths)
116
+
117
+ # 2. Walk up the class hierarchy for inherited view paths
118
+ klass = self.class.superclass
119
+ while klass.respond_to?(:view_paths)
120
+ paths.concat(klass.view_paths)
121
+ klass = klass.superclass
122
+ end
123
+
124
+ # 3. App-level views (convention: lambda/views/ from Belt.root)
125
+ if defined?(Belt.root) && Belt.root
126
+ paths << File.join(Belt.root, 'lambda', 'views')
127
+ paths << File.join(Belt.root, 'views')
128
+ end
129
+
130
+ # 4. Gem-level fallback (views/ relative to this file — belt gem's own views)
131
+ paths << File.expand_path('views', __dir__)
132
+
133
+ paths.uniq
134
+ end
135
+ end
136
+ end
data/lib/belt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.1.5'
4
+ VERSION = '0.1.7'
5
5
  end
@@ -0,0 +1,51 @@
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><%= h(@title) %></title>
7
+ <style><%= @css %></style>
8
+ </head>
9
+ <body>
10
+ <div class="hero">
11
+ <img src="data:image/jpeg;base64,<%= @background_image %>" alt="Belt" class="hero-bg" />
12
+ <div class="overlay">
13
+ <h1><%= h(@title) %></h1>
14
+ <p class="subtitle"><%= h(@subtitle) %></p>
15
+ </div>
16
+ </div>
17
+ <div class="container">
18
+ <div class="stack-check">
19
+ <div class="check-item check-pass">
20
+ <span class="icon">✓</span>
21
+ <span>API Gateway</span>
22
+ </div>
23
+ <div class="arrow">→</div>
24
+ <div class="check-item check-pass">
25
+ <span class="icon">✓</span>
26
+ <span>Lambda</span>
27
+ </div>
28
+ <div class="arrow">→</div>
29
+ <%- if @dynamodb_connected -%>
30
+ <div class="check-item check-pass">
31
+ <span class="icon">✓</span>
32
+ <span>DynamoDB</span>
33
+ </div>
34
+ <%- else -%>
35
+ <div class="check-item check-warn">
36
+ <span class="icon">⚠</span>
37
+ <span>DynamoDB</span>
38
+ </div>
39
+ <%- end -%>
40
+ </div>
41
+ <div class="next-steps">
42
+ <h2>Next Steps</h2>
43
+ <ol>
44
+ <li>Generate a resource: <code>belt g resource post title:string body:text</code></li>
45
+ <li>Deploy: <code>belt deploy</code></li>
46
+ <li>This page will be replaced once you define your own root route.</li>
47
+ </ol>
48
+ </div>
49
+ </div>
50
+ </body>
51
+ </html>
data/lib/belt.rb CHANGED
@@ -11,6 +11,7 @@ module Belt
11
11
  class AuthenticationError < StandardError; end
12
12
  class RecordNotFound < StandardError; end
13
13
  class ActionNotFound < StandardError; end
14
+ class TemplateNotFound < StandardError; end
14
15
 
15
16
  @controller_paths = []
16
17
 
@@ -6,10 +6,12 @@ require_relative '../belt/parameters'
6
6
  require_relative '../belt/helpers/response'
7
7
  require_relative '../belt/helpers/error_logging'
8
8
  require_relative '../belt/helpers/cors_origin'
9
+ require_relative '../belt/rendering'
9
10
 
10
11
  module BeltController
11
12
  class Base
12
13
  include Belt::Helpers::Response
14
+ include Belt::Rendering
13
15
 
14
16
  attr_reader :event, :body
15
17
 
@@ -20,7 +20,7 @@ provider "aws" {
20
20
 
21
21
  default_tags {
22
22
  tags = {
23
- Project = "<%= @app_name.capitalize %>"
23
+ Project = var.app_name
24
24
  Environment = var.environment
25
25
  ManagedBy = "Terraform"
26
26
  }
@@ -37,7 +37,20 @@ resource "conveyor_belt" "main" {
37
37
  source = "${path.module}/../routes.tf.rb"
38
38
  app_name = var.app_name
39
39
  lambda_source_dir = "${path.module}/../../lambda"
40
- lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "templates"]
40
+ lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "views"]
41
41
  frontend_urls = ["http://localhost:3000"]
42
- friendly_errors = true
42
+ friendly_errors = var.environment != "prod"
43
+
44
+ # Per-lambda configuration: env vars, timeouts, memory
45
+ lambda_config = {
46
+ api = {
47
+ # timeout = 30
48
+ # memory_size = 256
49
+
50
+ env_vars = {
51
+ WELCOME_TITLE = "Welcome to ${var.app_name}"
52
+ WELCOME_SUBTITLE = "API Gateway → Lambda → DynamoDB — all connected."
53
+ }
54
+ }
55
+ }
43
56
  }
@@ -1,5 +1,10 @@
1
1
  output "api_url" {
2
2
  description = "API Gateway base URL"
3
+ value = values(conveyor_belt.main.api_gateway_urls)[0]
4
+ }
5
+
6
+ output "api_urls" {
7
+ description = "Map of gateway names to API Gateway URLs"
3
8
  value = conveyor_belt.main.api_gateway_urls
4
9
  }
5
10
 
@@ -153,7 +153,3 @@ output "frontend_distribution_id" {
153
153
  output "frontend_url" {
154
154
  value = "https://${aws_cloudfront_distribution.frontend.domain_name}"
155
155
  }
156
-
157
- output "api_url" {
158
- value = values(conveyor_belt.main.api_gateway_urls)[0]
159
- }
@@ -1,5 +1,5 @@
1
1
  Belt.application.routes.draw do
2
- namespace :<%= @app_name %> do
2
+ namespace :api do
3
3
  get "/", action: :show, controller: :welcome
4
4
  # resources :posts
5
5
  end
@@ -10,12 +10,12 @@ ActiveItem.configure do |config|
10
10
  config.environment = ENV['ENVIRONMENT']
11
11
  end
12
12
 
13
- require_relative 'lib/routes/<%= @app_name %>_routes'
13
+ require_relative 'lib/routes/api_routes'
14
14
  <% @resources&.each do |r| -%>
15
15
  require_relative 'controllers/<%= @app_name %>/<%= r %>_controller'
16
16
  <% end -%>
17
17
 
18
- ROUTER = Belt::ActionRouter.new(routes: Routes::<%= @module_name.upcase %>, namespace: '<%= @app_name %>')
18
+ ROUTER = Belt::ActionRouter.new(routes: Routes::API, namespace: 'api')
19
19
 
20
20
  def execute(path:, body:, event:)
21
21
  ROUTER.route(event: event, body: body)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module <%= @module_name %>Controllers
3
+ module ApiControllers
4
4
  class ApplicationController < BeltController::Base
5
5
  end
6
6
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Routes
4
- <%= @module_name.upcase %> = [
4
+ API = [
5
5
  { verb: 'GET', path: '/', controller: 'welcome', action: 'show' },
6
6
  # { verb: 'GET', path: '/posts', controller: 'posts', action: 'index' },
7
7
  # { verb: 'POST', path: '/posts', controller: 'posts', action: 'create' },
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: belt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
@@ -52,13 +52,14 @@ files:
52
52
  - exe/belt
53
53
  - lib/belt.rb
54
54
  - lib/belt/action_router.rb
55
- - lib/belt/assets/belt-default.png
55
+ - lib/belt/assets/belt-default.jpg
56
56
  - lib/belt/assets/welcome.css
57
57
  - lib/belt/cli.rb
58
58
  - lib/belt/cli/app_detection.rb
59
59
  - lib/belt/cli/bucket_security.rb
60
60
  - lib/belt/cli/console_command.rb
61
61
  - lib/belt/cli/deploy_command.rb
62
+ - lib/belt/cli/destroy_command.rb
62
63
  - lib/belt/cli/env_resolver.rb
63
64
  - lib/belt/cli/environment_command.rb
64
65
  - lib/belt/cli/frontend_command.rb
@@ -82,10 +83,12 @@ files:
82
83
  - lib/belt/lambda_handler.rb
83
84
  - lib/belt/observability.rb
84
85
  - lib/belt/parameters.rb
86
+ - lib/belt/rendering.rb
85
87
  - lib/belt/root.rb
86
88
  - lib/belt/route_dsl.rb
87
89
  - lib/belt/table_inference.rb
88
90
  - lib/belt/version.rb
91
+ - lib/belt/views/welcome/show.html.erb
89
92
  - lib/belt_controller/base.rb
90
93
  - lib/templates/environment/backend.tf.erb
91
94
  - lib/templates/environment/main.tf.erb
Binary file