Skip to content

Rack

A Rack app is any object that responds to call(env), so a Carton can export one. Here a plain config.ru mounts two domain Cartons with Rack’s map, with no web framework. Source on GitHub.

Gemfile # carton, rack; rackup and webrick to serve
config.ru # the host: mounts each Carton's Rack app
main.rb # sends requests with Rack::MockRequest
cartons/
catalog/
lib/catalog.rb # Entrypoint: export Catalog, a Rack app
lib/catalog/products.rb # inside the Carton; private
orders/
lib/orders.rb # Entrypoint: imports catalog, exports a lambda
test/app_test.rb

config.ru routes in the Main Box and imports each Carton where it mounts it:

config.ru
require 'carton'
# The host app: plain Rack routing in the Main Box. Each Carton exports a Rack
# app, an object that responds to `call(env)`.
# Carton boundary: each import loads one Carton in its own Box. Only its
# Export comes back. Rack evaluates this file, and import_relative cannot
# infer a path from evaluated code, so import absolute paths, marked for
# `carton check`.
map '/catalog' do
run import(File.join(__dir__, 'cartons/catalog')) # carton:dynamic
end
map '/orders' do
run import(File.join(__dir__, 'cartons/orders')) # carton:dynamic
end

catalog exports its module, which is both a Rack app and the Public Surface other Cartons use. It hides Catalog::Products with private_constant, and requires JSON in its own Box:

cartons/catalog/lib/catalog.rb
# JSON loads in this Carton's Box, not the host's.
require 'json'
# A file inside this Carton: only this Entrypoint can load it.
require_relative 'catalog/products'
module Catalog
# Hidden from Importers by Ruby's own visibility, which holds across Boxes.
private_constant :Products
# Price in cents, or nil for an unknown SKU.
def self.price(sku) = Products::PRICES[sku]
# The Rack app: GET /catalog lists every product.
def self.call(_env)
[200, { 'content-type' => 'application/json' }, [Products::PRICES.to_json]]
end
end
# Export: the module is both the Rack app and the Public Surface for other
# Cartons.
export Catalog
cartons/catalog/lib/catalog/products.rb
module Catalog
module Products
PRICES = { 'tea' => 450, 'coffee' => 600 }.freeze
end
end

orders imports catalog across a boundary and exports a lambda. The host imports catalog too, so both share one Carton:

cartons/orders/lib/orders.rb
require 'json'
require 'uri'
# Carton boundary: orders reaches the catalog only through its Export. The
# host imports catalog too, so both share one Carton.
Catalog = import_relative '../../catalog'
# The Rack app: GET /orders/quote?sku=tea&quantity=2 prices an order.
Orders =
lambda do |env|
params = URI.decode_www_form(env['QUERY_STRING']).to_h
price = Catalog.price(params['sku'])
next 404, { 'content-type' => 'text/plain' }, ['Unknown SKU'] unless price
quote = { sku: params['sku'], total: price * Integer(params['quantity']) }
[200, { 'content-type' => 'application/json' }, [quote.to_json]]
end
# Export: a lambda is a Rack app too.
export Orders

main.rb builds the app from config.ru, sends requests, and checks that Catalog::Products and JSON stay out of the Main Box:

main.rb
require 'carton'
# Bundle boundary: the host's Gemfile provides rack. The Cartons use none of
# its gems.
Carton.setup_bundle
require 'rack'
# Builds the same app `rackup` serves from config.ru.
app = Rack::Builder.parse_file(File.join(__dir__, 'config.ru'))
request = Rack::MockRequest.new(app)
puts "GET /catalog => #{request.get('/catalog').body}"
puts 'GET /orders/quote?sku=tea&quantity=2 => ' \
"#{request.get('/orders/quote?sku=tea&quantity=2').body}"
puts 'GET /orders/quote?sku=cake&quantity=1 => ' \
"#{request.get('/orders/quote?sku=cake&quantity=1').status}"
puts
# Carton boundary: a Shared Import returns the Export config.ru mounted.
Catalog = import_relative 'cartons/catalog'
begin
Catalog::Products
rescue NameError => e
puts "Catalog::Products is private: #{e.message.lines.first.strip}"
end
puts "JSON loaded in the Main Box: #{!defined?(JSON).nil?}"

test/app_test.rb sends the same requests through Rack::MockRequest.

bundle exec rake example:rack
RUBY_BOX=1 ruby examples/rack/main.rb
cd examples/rack && RUBY_BOX=1 bundle exec rackup # serves on port 9292
GET /catalog => {"tea":450,"coffee":600}
GET /orders/quote?sku=tea&quantity=2 => {"sku":"tea","total":900}
GET /orders/quote?sku=cake&quantity=1 => 404
Catalog::Products is private: private constant Catalog::Products referenced
JSON loaded in the Main Box: false

See Rails and Hanami for Cartons inside a web framework.