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 serveconfig.ru # the host: mounts each Carton's Rack appmain.rb # sends requests with Rack::MockRequestcartons/ 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 lambdatest/app_test.rbThe host
Section titled “The host”config.ru routes in the Main Box and imports each Carton where it mounts it:
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:dynamicend
map '/orders' do run import(File.join(__dir__, 'cartons/orders')) # carton:dynamicendThe Cartons
Section titled “The Cartons”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:
# 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]] endend
# Export: the module is both the Rack app and the Public Surface for other# Cartons.export Catalogmodule Catalog module Products PRICES = { 'tea' => 450, 'coffee' => 600 }.freeze endendorders imports catalog across a boundary and exports a lambda. The host imports catalog too, so both share one Carton:
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 OrdersThe app
Section titled “The app”main.rb builds the app from config.ru, sends requests, and checks that Catalog::Products and JSON stay out of the Main Box:
require 'carton'
# Bundle boundary: the host's Gemfile provides rack. The Cartons use none of# its gems.Carton.setup_bundlerequire '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::Productsrescue 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:rackRUBY_BOX=1 ruby examples/rack/main.rbcd examples/rack && RUBY_BOX=1 bundle exec rackup # serves on port 9292GET /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 referencedJSON loaded in the Main Box: false