Getting started
In plain Ruby, every file can reach every constant and every monkey patch applies everywhere. Carton splits an app into gem-sized or domain-sized Cartons, and each Carton is a boundary. It runs in its own Ruby::Box, so its constants, gems, and monkey patches stay inside, and only what it exports crosses. Why Carton covers the problems this solves.
You’ll build two Cartons, then watch Ruby stop the app from reaching past one into the other.
Requirements
Section titled “Requirements”- Ruby 4.0.7 or later, run with Boxes enabled:
RUBY_BOX=1 - Bundler 4.0.20 or later, if you use Bundler
require 'carton'before the app first callsimport,import_relative, orexport; every Carton Box gets Carton automatically
Quick start
Section titled “Quick start”Build a two-Carton app, then check the boundary.
-
Install
Section titled “Install”gem install cartonOr add it to your Gemfile:
Gemfile gem 'carton' -
Write a Carton
Section titled “Write a Carton”A Carton’s Entrypoint is a Ruby file that exports one value, usually its root module. Start with a tax Carton:
# tax.rb: the tax Cartonmodule Taxdef self.on(total) = total / 10endexport Tax # the only value that leaves this Carton -
Import it
Section titled “Import it”Cartons import each other. The billing Carton imports tax:
# billing.rb: the billing CartonTax = import_relative 'tax' # billing's import, invisible to Importersmodule Billingdef self.invoice(total) = total + Tax.on(total)endexport BillingThe application imports billing from the Main Box:
main.rb require 'carton'Billing = import_relative 'billing' # runs billing.rb in its own Boxputs Billing.invoice(100)import_relativereturns the exported value itself, and defines nothing in the Importer. Bind it to a constant of the same name. -
Run it
Section titled “Run it”Run it with Boxes enabled:
RUBY_BOX=1 ruby main.rb # prints 110 -
See the boundary
Section titled “See the boundary”Add a line to
main.rbthat reaches past billing:Tax # NameError: billing's importIn plain Ruby, a file’s top-level constants are global, so requiring billing would make
Taxvisible everywhere. The billing Carton keeps its imports inside, along with every other top-level constant and method it defines.Monkey patches stay inside too. Patch
Stringat the top ofbilling.rb:billing.rb class Stringdef blank? = strip.empty? # billing's patchendBilling’s code can call
''.blank?, butmain.rbcan’t:''.blank? # NoMethodError
Grow a Carton
Section titled “Grow a Carton”When only billing uses tax, make it part of billing. A Carton is its Entrypoint plus its Interior, the directory named after it, shaped like a gem’s lib/. Move tax.rb to billing/tax.rb, nest Tax under Billing as a gem would, and drop its export:
billing.rb # the Entrypointbilling/tax.rb # the Interior: inside the billing Cartonmodule Billing module Tax def self.on(total) = total / 10 end private_constant :Tax # hidden from Importers of BillingendLoad it from the Entrypoint with require_relative:
require_relative 'billing/tax' # loads inside billing's Box
module Billing def self.invoice(total) = total + Tax.on(total)end
export Billingmain.rb still prints 110. Tax is still a NameError there, and Billing::Tax raises NameError because it is private. Only the Entrypoint can be imported, so import_relative 'billing/tax' raises Carton::ImportError.
Each Carton loads once per process, and every later import returns the same value. import 'billing' searches $LOAD_PATH like require, so gems and path gems import by name.
