@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,611 lines (1,327 loc) • 38.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sinatraTemplate = void 0;
exports.sinatraTemplate = {
id: 'sinatra',
name: 'sinatra',
displayName: 'Sinatra',
description: 'Lightweight Ruby web framework for building APIs and microservices with minimal overhead',
language: 'ruby',
framework: 'sinatra',
version: '3.1.0',
tags: ['ruby', 'sinatra', 'api', 'microservices', 'lightweight', 'rest', 'minimal'],
port: 4567,
dependencies: {},
features: ['authentication', 'database', 'validation', 'logging', 'documentation', 'testing'],
files: {
// Gemfile
'Gemfile': `source 'https://rubygems.org'
ruby '3.3.0'
# Web framework
gem 'sinatra', '~> 3.1.0'
gem 'sinatra-contrib', '~> 3.1.0'
gem 'puma', '~> 6.4'
# Database
gem 'activerecord', '~> 7.1.2'
gem 'sinatra-activerecord', '~> 2.0'
gem 'pg', '~> 1.5'
gem 'rake', '~> 13.1'
# Authentication
gem 'bcrypt', '~> 3.1.19'
gem 'jwt', '~> 2.7'
# JSON
gem 'json', '~> 2.7'
gem 'multi_json', '~> 1.15'
# Validation
gem 'sinatra-param', '~> 1.6'
# Environment
gem 'dotenv', '~> 2.8'
# Logging
gem 'sinatra-logger', '~> 0.3'
# CORS
gem 'sinatra-cross_origin', '~> 0.4'
# Redis
gem 'redis', '~> 5.0'
gem 'hiredis', '~> 0.6'
# Background jobs
gem 'sidekiq', '~> 7.2'
gem 'sinatra-sidekiq', '~> 1.0'
# Pagination
gem 'will_paginate', '~> 4.0'
gem 'will_paginate-sinatra', '~> 1.0'
# API Documentation
gem 'sinatra-swagger-exposer', '~> 0.5'
# Rate limiting
gem 'rack-throttle', '~> 0.7'
# Health checks
gem 'health_check', '~> 3.1'
group :development do
gem 'rerun', '~> 0.14'
gem 'tux', '~> 0.3'
end
group :test do
gem 'rspec', '~> 3.12'
gem 'rack-test', '~> 2.1'
gem 'factory_bot', '~> 6.4'
gem 'faker', '~> 3.2'
gem 'database_cleaner-active_record', '~> 2.1'
gem 'simplecov', '~> 0.22', require: false
gem 'shoulda-matchers', '~> 5.3'
gem 'timecop', '~> 0.9'
gem 'webmock', '~> 3.19'
gem 'vcr', '~> 6.2'
end
group :development, :test do
gem 'pry', '~> 0.14'
gem 'rubocop', '~> 1.59', require: false
gem 'rubocop-rspec', '~> 2.25', require: false
end
`,
// Ruby version
'.ruby-version': `3.3.0
`,
// Main application file
'app.rb': `require 'sinatra/base'
require 'sinatra/json'
require 'sinatra/activerecord'
require 'sinatra/namespace'
require 'sinatra/cross_origin'
require 'sinatra/custom_logger'
require 'sinatra/param'
require 'sinatra/swagger-exposer/swagger-exposer'
require 'bcrypt'
require 'jwt'
require 'json'
require 'logger'
require 'redis'
# Load environment variables
require 'dotenv/load'
# Load application files
Dir['./config/*.rb'].sort.each { |file| require file }
Dir['./app/models/*.rb'].sort.each { |file| require file }
Dir['./app/helpers/*.rb'].sort.each { |file| require file }
Dir['./app/controllers/*.rb'].sort.each { |file| require file }
class {{projectName}}App < Sinatra::Base
register Sinatra::ActiveRecordExtension
register Sinatra::Namespace
register Sinatra::CrossOrigin
register Sinatra::Swagger::Exposer
helpers Sinatra::Param
helpers Sinatra::CustomLogger
helpers AuthHelper
helpers JsonHelper
# Configuration
configure do
set :app_name, '{{projectName}}'
set :server, :puma
set :database_file, 'config/database.yml'
set :show_exceptions, false
set :raise_errors, false
set :dump_errors, true
set :logging, true
# Logger
logger = Logger.new(STDOUT)
logger.level = Logger::INFO
set :logger, logger
# CORS
enable :cross_origin
set :allow_origin, ENV.fetch('CORS_ORIGINS', '*').split(',')
set :allow_methods, [:get, :post, :put, :patch, :delete, :options]
set :allow_headers, ['Content-Type', 'Authorization', 'X-Requested-With']
set :expose_headers, ['X-Total-Count', 'X-Total-Pages', 'X-Current-Page']
# JSON
set :json_encoder, :to_json
# Redis
set :redis, Redis.new(url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0'))
end
configure :development do
set :show_exceptions, true
logger.level = Logger::DEBUG
end
configure :production do
logger.level = Logger::INFO
end
# Swagger documentation
general_info(
info: {
version: '1.0.0',
title: '{{projectName}} API',
description: 'Lightweight API built with Sinatra'
}
)
# Error handling
error ActiveRecord::RecordNotFound do
error_response(404, 'Record not found')
end
error ActiveRecord::RecordInvalid do |e|
error_response(422, 'Validation failed', errors: e.record.errors.full_messages)
end
error Sinatra::Param::InvalidParameterError do |e|
error_response(400, 'Invalid parameter', error: e.message)
end
error JWT::DecodeError do
error_response(401, 'Invalid token')
end
error do |e|
logger.error "#{e.class}: #{e.message}"
logger.error e.backtrace.join("\\n")
error_response(500, 'Internal server error')
end
# Middleware
use Rack::Deflater
use Rack::Throttle::Minute, max: 60, cache: settings.redis, key_prefix: :throttle
# Before filters
before do
content_type :json
# Log request
logger.info "#{request.request_method} #{request.path_info} - #{request.ip}"
end
# Root endpoint
get '/' do
json(
name: settings.app_name,
version: '1.0.0',
status: 'running',
timestamp: Time.now.iso8601
)
end
# Health check
get '/health' do
health = {
status: 'ok',
timestamp: Time.now.iso8601,
database: database_healthy?,
redis: redis_healthy?,
version: '1.0.0'
}
status health[:database] && health[:redis] ? 200 : 503
json health
end
# Mount controllers
use AuthController
use UsersController
use ProductsController
use OrdersController
private
def database_healthy?
ActiveRecord::Base.connection.active?
rescue StandardError
false
end
def redis_healthy?
settings.redis.ping == 'PONG'
rescue StandardError
false
end
def error_response(status_code, message, additional = {})
halt status_code, json({ error: message }.merge(additional))
end
end
`,
// Config files
'config.ru': `require './app'
run {{projectName}}App
`,
'Rakefile': `require 'sinatra/activerecord'
require 'sinatra/activerecord/rake'
require './app'
namespace :db do
task :load_config do
require './app'
end
end
desc "Run the application"
task :run do
exec "bundle exec puma"
end
desc "Run the console"
task :console do
require 'pry'
require './app'
Pry.start
end
desc "Run tests"
task :test do
exec "bundle exec rspec"
end
desc "Run linter"
task :lint do
exec "bundle exec rubocop"
end
`,
'config/database.yml': `default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: {{projectName}}_development
username: <%= ENV.fetch("DATABASE_USERNAME", "postgres") %>
password: <%= ENV.fetch("DATABASE_PASSWORD", "") %>
host: <%= ENV.fetch("DATABASE_HOST", "localhost") %>
port: <%= ENV.fetch("DATABASE_PORT", 5432) %>
test:
<<: *default
database: {{projectName}}_test
username: <%= ENV.fetch("DATABASE_USERNAME", "postgres") %>
password: <%= ENV.fetch("DATABASE_PASSWORD", "") %>
host: <%= ENV.fetch("DATABASE_HOST", "localhost") %>
port: <%= ENV.fetch("DATABASE_PORT", 5432) %>
production:
<<: *default
database: {{projectName}}_production
username: {{projectName}}
password: <%= ENV["{{projectName}}_DATABASE_PASSWORD"] %>
url: <%= ENV["DATABASE_URL"] %>
`,
'config/puma.rb': `# Puma configuration file
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
port ENV.fetch("PORT") { 4567 }
environment ENV.fetch("RACK_ENV") { "development" }
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
preload_app!
plugin :tmp_restart
`,
// Helpers
'app/helpers/auth_helper.rb': `module AuthHelper
def jwt_encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, jwt_secret)
end
def jwt_decode(token)
JWT.decode(token, jwt_secret)[0]
rescue JWT::DecodeError
nil
end
def jwt_secret
ENV.fetch('JWT_SECRET', 'your-secret-key')
end
def authenticate!
token = extract_token
return halt_unauthorized unless token
payload = jwt_decode(token)
return halt_unauthorized unless payload
@current_user = User.find_by(id: payload['user_id'])
return halt_unauthorized unless @current_user && @current_user.active?
@current_user
rescue StandardError
halt_unauthorized
end
def current_user
@current_user
end
def extract_token
auth_header = request.env['HTTP_AUTHORIZATION']
return nil unless auth_header
auth_header.split(' ').last
end
def halt_unauthorized
halt 401, json(error: 'Unauthorized')
end
def admin_only!
authenticate!
halt 403, json(error: 'Forbidden') unless current_user.admin?
end
end
`,
'app/helpers/json_helper.rb': `module JsonHelper
def json(data, status = 200)
content_type :json
status status
if data.respond_to?(:to_json)
data.to_json
else
JSON.generate(data)
end
end
def parse_json_body
request.body.rewind
JSON.parse(request.body.read, symbolize_names: true)
rescue JSON::ParserError
halt 400, json(error: 'Invalid JSON')
end
def paginate(collection, page = 1, per_page = 20)
page = [page.to_i, 1].max
per_page = [[per_page.to_i, 100].min, 1].max
total = collection.count
collection = collection.limit(per_page).offset((page - 1) * per_page)
headers['X-Total-Count'] = total.to_s
headers['X-Total-Pages'] = ((total.to_f / per_page).ceil).to_s
headers['X-Current-Page'] = page.to_s
headers['X-Next-Page'] = (page < (total.to_f / per_page).ceil ? page + 1 : nil).to_s
headers['X-Prev-Page'] = (page > 1 ? page - 1 : nil).to_s
collection
end
end
`,
// Models
'app/models/user.rb': `class User < ActiveRecord::Base
has_secure_password
# Associations
has_many :orders, dependent: :destroy
# Validations
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true
validates :password, length: { minimum: 8 }, if: :password_required?
validates :role, inclusion: { in: %w[user admin moderator] }
# Scopes
scope :active, -> { where(active: true) }
scope :admins, -> { where(role: 'admin') }
scope :recent, -> { order(created_at: :desc) }
# Callbacks
before_validation :downcase_email
before_create :set_defaults
def admin?
role == 'admin'
end
def moderator?
role == 'moderator'
end
def as_json(options = {})
super(options.merge(except: [:password_digest]))
end
private
def downcase_email
self.email = email&.downcase
end
def set_defaults
self.role ||= 'user'
self.active = true if active.nil?
end
def password_required?
new_record? || password.present?
end
end
`,
'app/models/product.rb': `class Product < ActiveRecord::Base
# Associations
has_many :order_items
has_many :orders, through: :order_items
# Validations
validates :name, presence: true
validates :price, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :stock, presence: true, numericality: { greater_than_or_equal_to: 0, only_integer: true }
validates :sku, presence: true, uniqueness: true
validates :category, presence: true
# Scopes
scope :active, -> { where(active: true) }
scope :in_stock, -> { where('stock > 0') }
scope :by_category, ->(category) { where(category: category) }
scope :price_range, ->(min, max) { where(price: min..max) }
scope :search, ->(query) { where('name ILIKE ? OR description ILIKE ?', "%#{query}%", "%#{query}%") }
# Callbacks
before_validation :generate_sku, on: :create
before_create :set_defaults
def in_stock?
stock > 0
end
def update_stock!(quantity)
with_lock do
if stock >= quantity
update!(stock: stock - quantity)
true
else
errors.add(:stock, 'insufficient stock')
false
end
end
end
private
def generate_sku
self.sku ||= "SKU-#{SecureRandom.hex(4).upcase}"
end
def set_defaults
self.active = true if active.nil?
end
end
`,
'app/models/order.rb': `class Order < ActiveRecord::Base
# Associations
belongs_to :user
has_many :order_items, dependent: :destroy
has_many :products, through: :order_items
# Validations
validates :status, inclusion: { in: %w[pending processing shipped delivered cancelled] }
validates :total_amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
# Scopes
scope :recent, -> { order(created_at: :desc) }
scope :by_status, ->(status) { where(status: status) }
scope :completed, -> { where(status: %w[shipped delivered]) }
# Callbacks
before_create :set_defaults
before_save :calculate_total
def cancel!
return false unless can_cancel?
transaction do
order_items.each do |item|
item.product.update!(stock: item.product.stock + item.quantity)
end
update!(status: 'cancelled')
end
end
def complete!
return false unless status == 'processing'
update!(status: 'shipped')
end
def can_cancel?
%w[pending processing].include?(status)
end
private
def set_defaults
self.status ||= 'pending'
self.total_amount ||= 0
end
def calculate_total
self.total_amount = order_items.sum { |item| item.quantity * item.price }
end
end
`,
'app/models/order_item.rb': `class OrderItem < ActiveRecord::Base
# Associations
belongs_to :order
belongs_to :product
# Validations
validates :quantity, presence: true, numericality: { greater_than: 0, only_integer: true }
validates :price, presence: true, numericality: { greater_than_or_equal_to: 0 }
# Callbacks
before_validation :set_price
def subtotal
quantity * price
end
private
def set_price
self.price ||= product&.price
end
end
`,
// Controllers
'app/controllers/application_controller.rb': `class ApplicationController < Sinatra::Base
helpers AuthHelper
helpers JsonHelper
configure do
set :show_exceptions, false
set :raise_errors, false
end
before do
content_type :json
end
error ActiveRecord::RecordNotFound do
halt 404, json(error: 'Record not found')
end
error ActiveRecord::RecordInvalid do |e|
halt 422, json(errors: e.record.errors.full_messages)
end
end
`,
'app/controllers/auth_controller.rb': `class AuthController < ApplicationController
post '/api/v1/auth/register' do
param :email, String, required: true, format: URI::MailTo::EMAIL_REGEXP
param :password, String, required: true, min_length: 8
param :name, String, required: true
user = User.new(
email: params[:email],
password: params[:password],
name: params[:name]
)
if user.save
token = jwt_encode(user_id: user.id)
refresh_token = jwt_encode({ user_id: user.id, type: 'refresh' }, 7.days.from_now)
status 201
json(
user: user,
access_token: token,
refresh_token: refresh_token
)
else
halt 422, json(errors: user.errors.full_messages)
end
end
post '/api/v1/auth/login' do
param :email, String, required: true
param :password, String, required: true
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
if user.active?
token = jwt_encode(user_id: user.id)
refresh_token = jwt_encode({ user_id: user.id, type: 'refresh' }, 7.days.from_now)
json(
user: user,
access_token: token,
refresh_token: refresh_token
)
else
halt 403, json(error: 'Account is inactive')
end
else
halt 401, json(error: 'Invalid credentials')
end
end
post '/api/v1/auth/refresh' do
token = extract_token
halt 401, json(error: 'Missing token') unless token
payload = jwt_decode(token)
halt 401, json(error: 'Invalid token') unless payload && payload['type'] == 'refresh'
user = User.find_by(id: payload['user_id'])
halt 401, json(error: 'User not found') unless user
new_token = jwt_encode(user_id: user.id)
json(access_token: new_token)
end
delete '/api/v1/auth/logout' do
authenticate!
# In a real app, you might want to blacklist the token
json(message: 'Logged out successfully')
end
end
`,
'app/controllers/users_controller.rb': `class UsersController < ApplicationController
before '/api/v1/users*' do
authenticate!
end
get '/api/v1/users' do
param :search, String
param :role, String, in: %w[user admin moderator]
param :page, Integer, default: 1, min: 1
param :per_page, Integer, default: 20, min: 1, max: 100
users = User.all
users = users.where('name ILIKE ? OR email ILIKE ?', "%#{params[:search]}%", "%#{params[:search]}%") if params[:search]
users = users.where(role: params[:role]) if params[:role]
users = paginate(users, params[:page], params[:per_page])
json(users: users)
end
get '/api/v1/users/:id' do
user = User.find(params[:id])
json(user)
end
put '/api/v1/users/:id' do
user = User.find(params[:id])
param :email, String, format: URI::MailTo::EMAIL_REGEXP
param :name, String
param :role, String, in: %w[user admin moderator]
param :active, Boolean
update_params = {}
update_params[:email] = params[:email] if params.key?(:email)
update_params[:name] = params[:name] if params.key?(:name)
update_params[:role] = params[:role] if params.key?(:role)
update_params[:active] = params[:active] if params.key?(:active)
if user.update(update_params)
json(user)
else
halt 422, json(errors: user.errors.full_messages)
end
end
delete '/api/v1/users/:id' do
user = User.find(params[:id])
user.destroy
status 204
end
patch '/api/v1/users/:id/activate' do
user = User.find(params[:id])
user.update(active: true)
json(user)
end
patch '/api/v1/users/:id/deactivate' do
user = User.find(params[:id])
user.update(active: false)
json(user)
end
end
`,
'app/controllers/products_controller.rb': `class ProductsController < ApplicationController
before '/api/v1/products*' do
authenticate! unless %w[GET].include?(request.request_method)
end
get '/api/v1/products' do
param :search, String
param :category, String
param :min_price, Float, min: 0
param :max_price, Float, min: 0
param :page, Integer, default: 1, min: 1
param :per_page, Integer, default: 20, min: 1, max: 100
products = Product.active
products = products.search(params[:search]) if params[:search]
products = products.by_category(params[:category]) if params[:category]
products = products.where('price >= ?', params[:min_price]) if params[:min_price]
products = products.where('price <= ?', params[:max_price]) if params[:max_price]
products = paginate(products, params[:page], params[:per_page])
json(products: products)
end
get '/api/v1/products/:id' do
product = Product.find(params[:id])
json(product)
end
post '/api/v1/products' do
param :name, String, required: true
param :description, String
param :price, Float, required: true, min: 0
param :stock, Integer, required: true, min: 0
param :category, String, required: true
param :sku, String
product = Product.new(
name: params[:name],
description: params[:description],
price: params[:price],
stock: params[:stock],
category: params[:category],
sku: params[:sku]
)
if product.save
status 201
json(product)
else
halt 422, json(errors: product.errors.full_messages)
end
end
put '/api/v1/products/:id' do
product = Product.find(params[:id])
param :name, String
param :description, String
param :price, Float, min: 0
param :stock, Integer, min: 0
param :category, String
param :sku, String
param :active, Boolean
update_params = {}
%i[name description price stock category sku active].each do |attr|
update_params[attr] = params[attr] if params.key?(attr)
end
if product.update(update_params)
json(product)
else
halt 422, json(errors: product.errors.full_messages)
end
end
delete '/api/v1/products/:id' do
product = Product.find(params[:id])
product.destroy
status 204
end
end
`,
'app/controllers/orders_controller.rb': `class OrdersController < ApplicationController
before '/api/v1/orders*' do
authenticate!
end
get '/api/v1/orders' do
param :status, String, in: %w[pending processing shipped delivered cancelled]
param :page, Integer, default: 1, min: 1
param :per_page, Integer, default: 20, min: 1, max: 100
orders = current_user.orders
orders = orders.by_status(params[:status]) if params[:status]
orders = orders.recent
orders = paginate(orders, params[:page], params[:per_page])
json(orders: orders.map { |o| order_with_items(o) })
end
get '/api/v1/orders/:id' do
order = current_user.orders.find(params[:id])
json(order_with_items(order))
end
post '/api/v1/orders' do
param :items, Array, required: true do |items|
items.each do |item|
item.param :product_id, Integer, required: true
item.param :quantity, Integer, required: true, min: 1
end
end
order = current_user.orders.build
ActiveRecord::Base.transaction do
order.save!
params[:items].each do |item|
product = Product.find(item[:product_id])
unless product.update_stock!(item[:quantity])
raise ActiveRecord::Rollback
end
order.order_items.create!(
product: product,
quantity: item[:quantity],
price: product.price
)
end
order.reload
end
status 201
json(order_with_items(order))
rescue ActiveRecord::Rollback
halt 422, json(error: 'Insufficient stock for one or more products')
end
patch '/api/v1/orders/:id/cancel' do
order = current_user.orders.find(params[:id])
if order.cancel!
json(order_with_items(order))
else
halt 422, json(error: 'Order cannot be cancelled')
end
end
patch '/api/v1/orders/:id/complete' do
admin_only!
order = Order.find(params[:id])
if order.complete!
json(order_with_items(order))
else
halt 422, json(error: 'Order cannot be completed')
end
end
private
def order_with_items(order)
order.as_json.merge(
items: order.order_items.map do |item|
item.as_json.merge(product: item.product)
end
)
end
end
`,
// Database migrations
'db/migrate/001_create_users.rb': `class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :email, null: false
t.string :password_digest, null: false
t.string :name, null: false
t.string :role, null: false, default: 'user'
t.boolean :active, null: false, default: true
t.timestamps
end
add_index :users, :email, unique: true
add_index :users, :role
add_index :users, :active
end
end
`,
'db/migrate/002_create_products.rb': `class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
t.string :name, null: false
t.text :description
t.decimal :price, precision: 10, scale: 2, null: false
t.integer :stock, null: false, default: 0
t.string :category, null: false
t.string :sku, null: false
t.boolean :active, null: false, default: true
t.timestamps
end
add_index :products, :sku, unique: true
add_index :products, :category
add_index :products, :active
end
end
`,
'db/migrate/003_create_orders.rb': `class CreateOrders < ActiveRecord::Migration[7.1]
def change
create_table :orders do |t|
t.references :user, null: false, foreign_key: true
t.string :status, null: false, default: 'pending'
t.decimal :total_amount, precision: 10, scale: 2, null: false, default: 0
t.timestamps
end
add_index :orders, :status
add_index :orders, :created_at
end
end
`,
'db/migrate/004_create_order_items.rb': `class CreateOrderItems < ActiveRecord::Migration[7.1]
def change
create_table :order_items do |t|
t.references :order, null: false, foreign_key: true
t.references :product, null: false, foreign_key: true
t.integer :quantity, null: false
t.decimal :price, precision: 10, scale: 2, null: false
t.timestamps
end
end
end
`,
// Seeds
'db/seeds.rb': `require 'faker'
# Clear existing data
OrderItem.destroy_all
Order.destroy_all
Product.destroy_all
User.destroy_all
# Create admin user
admin = User.create!(
email: 'admin@example.com',
password: 'password123',
name: 'Admin User',
role: 'admin'
)
# Create regular users
5.times do |i|
User.create!(
email: "user#{i+1}@example.com",
password: 'password123',
name: Faker::Name.name,
role: 'user'
)
end
# Create categories
categories = ['Electronics', 'Books', 'Clothing', 'Home & Garden', 'Sports']
# Create products
categories.each do |category|
10.times do
Product.create!(
name: Faker::Commerce.product_name,
description: Faker::Lorem.paragraph(sentence_count: 3),
price: Faker::Commerce.price(range: 10.0..500.0),
stock: rand(0..100),
category: category,
sku: "SKU-#{SecureRandom.hex(4).upcase}",
active: [true, true, true, false].sample
)
end
end
puts "Seeded #{User.count} users and #{Product.count} products"
`,
// Test setup
'.rspec': `--require spec_helper
--format documentation
--color
`,
'spec/spec_helper.rb': `ENV['RACK_ENV'] = 'test'
require 'simplecov'
SimpleCov.start
require File.expand_path '../app.rb', __dir__
require 'rspec'
require 'rack/test'
require 'factory_bot'
require 'faker'
require 'database_cleaner/active_record'
require 'shoulda/matchers'
require 'timecop'
require 'webmock/rspec'
require 'vcr'
# Include test helpers
Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
RSpec.configure do |config|
config.include Rack::Test::Methods
config.include FactoryBot::Syntax::Methods
config.include RequestHelpers
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.filter_run_when_matching :focus
config.example_status_persistence_file_path = "spec/examples.txt"
config.disable_monkey_patching!
config.warnings = true
config.order = :random
Kernel.srand config.seed
# Database cleaner
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
# Configure Shoulda Matchers
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
end
end
# Configure VCR
VCR.configure do |config|
config.cassette_library_dir = "spec/vcr_cassettes"
config.hook_into :webmock
config.configure_rspec_metadata!
end
def app
{{projectName}}App
end
`,
'spec/support/request_helpers.rb': `module RequestHelpers
def json_response
JSON.parse(last_response.body, symbolize_names: true)
end
def auth_headers(user)
token = JWT.encode({ user_id: user.id }, ENV.fetch('JWT_SECRET', 'your-secret-key'))
{ 'HTTP_AUTHORIZATION' => "Bearer #{token}" }
end
def post_json(path, data = {}, headers = {})
post path, data.to_json, headers.merge('CONTENT_TYPE' => 'application/json')
end
def put_json(path, data = {}, headers = {})
put path, data.to_json, headers.merge('CONTENT_TYPE' => 'application/json')
end
def patch_json(path, data = {}, headers = {})
patch path, data.to_json, headers.merge('CONTENT_TYPE' => 'application/json')
end
end
`,
// Factories
'spec/factories/users.rb': `FactoryBot.define do
factory :user do
email { Faker::Internet.unique.email }
password { 'password123' }
name { Faker::Name.name }
role { 'user' }
active { true }
trait :admin do
role { 'admin' }
end
trait :inactive do
active { false }
end
end
end
`,
'spec/factories/products.rb': `FactoryBot.define do
factory :product do
name { Faker::Commerce.product_name }
description { Faker::Lorem.paragraph(sentence_count: 3) }
price { Faker::Commerce.price(range: 10.0..500.0) }
stock { rand(10..100) }
category { %w[Electronics Books Clothing Home Sports].sample }
sku { "SKU-#{SecureRandom.hex(4).upcase}" }
active { true }
trait :out_of_stock do
stock { 0 }
end
trait :inactive do
active { false }
end
end
end
`,
// Sample spec
'spec/api/auth_spec.rb': `require 'spec_helper'
RSpec.describe 'Authentication API' do
describe 'POST /api/v1/auth/register' do
let(:valid_params) do
{
email: 'newuser@example.com',
password: 'password123',
name: 'New User'
}
end
context 'with valid parameters' do
it 'creates a new user' do
expect {
post_json '/api/v1/auth/register', valid_params
}.to change(User, :count).by(1)
expect(last_response.status).to eq(201)
expect(json_response[:user][:email]).to eq('newuser@example.com')
expect(json_response[:access_token]).to be_present
expect(json_response[:refresh_token]).to be_present
end
end
context 'with invalid parameters' do
it 'returns error for missing email' do
post_json '/api/v1/auth/register', valid_params.except(:email)
expect(last_response.status).to eq(400)
expect(json_response[:error]).to include('email')
end
it 'returns error for duplicate email' do
create(:user, email: 'newuser@example.com')
post_json '/api/v1/auth/register', valid_params
expect(last_response.status).to eq(422)
expect(json_response[:errors]).to include('Email has already been taken')
end
end
end
describe 'POST /api/v1/auth/login' do
let!(:user) { create(:user, email: 'user@example.com', password: 'password123') }
context 'with valid credentials' do
it 'returns user data with tokens' do
post_json '/api/v1/auth/login', { email: 'user@example.com', password: 'password123' }
expect(last_response.status).to eq(200)
expect(json_response[:user][:email]).to eq('user@example.com')
expect(json_response[:access_token]).to be_present
expect(json_response[:refresh_token]).to be_present
end
end
context 'with invalid credentials' do
it 'returns error for wrong password' do
post_json '/api/v1/auth/login', { email: 'user@example.com', password: 'wrongpassword' }
expect(last_response.status).to eq(401)
expect(json_response[:error]).to eq('Invalid credentials')
end
it 'returns error for inactive account' do
user.update(active: false)
post_json '/api/v1/auth/login', { email: 'user@example.com', password: 'password123' }
expect(last_response.status).to eq(403)
expect(json_response[:error]).to eq('Account is inactive')
end
end
end
end
`,
// Docker configuration
'Dockerfile': `FROM ruby:3.3.0-alpine
# Install dependencies
RUN apk add --no-cache \
build-base \
postgresql-dev \
tzdata \
git
# Set working directory
WORKDIR /app
# Install bundler
RUN gem install bundler:2.5.3
# Copy Gemfile
COPY Gemfile Gemfile.lock ./
# Install gems
RUN bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install --jobs 4 --retry 3
# Copy application code
COPY . .
# Create non-root user
RUN addgroup -g 1000 -S app && \
adduser -u 1000 -S app -G app && \
chown -R app:app /app
# Switch to non-root user
USER app
# Expose port
EXPOSE 4567
# Start server
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
`,
'docker-compose.yml': `version: '3.8'
services:
app:
build: .
ports:
- "4567:4567"
environment:
- RACK_ENV=development
- DATABASE_HOST=postgres
- DATABASE_USERNAME=postgres
- DATABASE_PASSWORD=password
- REDIS_URL=redis://redis:6379/0
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- .:/app
- bundle:/usr/local/bundle
command: bundle exec rerun 'puma -C config/puma.rb'
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_DB={{projectName}}_development
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
sidekiq:
build: .
depends_on:
- postgres
- redis
environment:
- RACK_ENV=development
- DATABASE_HOST=postgres
- DATABASE_USERNAME=postgres
- DATABASE_PASSWORD=password
- REDIS_URL=redis://redis:6379/0
volumes:
- .:/app
- bundle:/usr/local/bundle
command: bundle exec sidekiq
volumes:
postgres_data:
bundle:
`,
// Environment configuration
'.env.example': `# Environment
RACK_ENV=development
# Server
PORT=4567
# Database
DATABASE_HOST=localhost
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=
DATABASE_PORT=5432
# Redis
REDIS_URL=redis://localhost:6379/0
# JWT
JWT_SECRET=your-jwt-secret
# CORS
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
`,
'.gitignore': `# Ruby
*.gem
*.rbc
/.config
/coverage/
/InstalledFiles
/pkg/
/spec/reports/
/spec/examples.txt
/test/tmp/
/test/version_tmp/
/tmp/
# Documentation
/.yardoc/
/_yardoc/
/doc/
/rdoc/
# Environment
/.bundle/
/vendor/bundle
/lib/bundler/man/
.env
.env.*
# Database
*.sqlite3
*.sqlite3-journal
# Logs
*.log
# OS
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
*.swp
*.swo
# Test coverage
/coverage/
`,
'README.md': `# {{projectName}}
Lightweight Ruby web API built with Sinatra, featuring JWT authentication, ActiveRecord ORM, and comprehensive testing.
## Features
- Lightweight Sinatra framework
- JWT authentication with refresh tokens
- ActiveRecord ORM with PostgreSQL
- Request parameter validation
- API documentation with Swagger
- Background job processing with Sidekiq
- Rate limiting with Rack::Throttle
- Comprehensive testing with RSpec
- Docker and Docker Compose setup
- Auto-reloading in development
## Prerequisites
- Ruby 3.3.0
- PostgreSQL 14+
- Redis 7+
- Docker and Docker Compose (optional)
## Quick Start
### Using Docker
1. Clone the repository:
\`\`\`bash
git clone <repository-url>
cd {{projectName}}
\`\`\`
2. Copy environment variables:
\`\`\`bash
cp .env.example .env
\`\`\`
3. Start with Docker Compose:
\`\`\`bash
docker-compose up
\`\`\`
The API will be available at http://localhost:4567.
### Local Development
1. Install dependencies:
\`\`\`bash
bundle install
\`\`\`
2. Setup database:
\`\`\`bash
bundle exec rake db:create
bundle exec rake db:migrate
bundle exec rake db:seed
\`\`\`
3. Start the server:
\`\`\`bash
bundle exec rerun 'puma'
\`\`\`
## Testing
Run the test suite:
\`\`\`bash
bundle exec rspec
\`\`\`
With coverage:
\`\`\`bash
COVERAGE=true bundle exec rspec
\`\`\`
## API Documentation
The API follows RESTful conventions. Swagger documentation can be accessed at the API endpoints.
## API Endpoints
### Authentication
- \`POST /api/v1/auth/register\` - Register new user
- \`POST /api/v1/auth/login\` - Login
- \`POST /api/v1/auth/refresh\` - Refresh token
- \`DELETE /api/v1/auth/logout\` - Logout
### Users
- \`GET /api/v1/users\` - List users
- \`GET /api/v1/users/:id\` - Get user
- \`PUT /api/v1/users/:id\` - Update user
- \`DELETE /api/v1/users/:id\` - Delete user
- \`PATCH /api/v1/users/:id/activate\` - Activate user
- \`PATCH /api/v1/users/:id/deactivate\` - Deactivate user
### Products
- \`GET /api/v1/products\` - List products
- \`GET /api/v1/products/:id\` - Get product
- \`POST /api/v1/products\` - Create product
- \`PUT /api/v1/products/:id\` - Update product
- \`DELETE /api/v1/products/:id\` - Delete product
### Orders
- \`GET /api/v1/orders\` - List orders
- \`GET /api/v1/orders/:id\` - Get order
- \`POST /api/v1/orders\` - Create order
- \`PATCH /api/v1/orders/:id/cancel\` - Cancel order
- \`PATCH /api/v1/orders/:id/complete\` - Complete order
## Development
### Console
Access the Ruby console with loaded application:
\`\`\`bash
bundle exec rake console
\`\`\`
### Linting
Run RuboCop:
\`\`\`bash
bundle exec rubocop
\`\`\`
### Database Tasks
Create database:
\`\`\`bash
bundle exec rake db:create
\`\`\`
Run migrations:
\`\`\`bash
bundle exec rake db:migrate
\`\`\`
Rollback migration:
\`\`\`bash
bundle exec rake db:rollback
\`\`\`
## License
[Your License]
`
}
};