Minting::Rails

CI Gem Version

Store and read Active Record attributes as Mint::Money objects with a single money_attribute declaration. No manual serialization, no boilerplate.

class Product < ApplicationRecord
  money_attribute :price, currency: 'USD'   # fixed currency, single column
  money_attribute :total                    # multi-currency, two columns
end

Product.new(price: 12).price  # => [USD 12.00]

Quick start

bundle add minting-rails
bin/rails g mint:initializer
# app/models/product.rb
class Product < ApplicationRecord
  money_attribute :price, currency: 'USD'
end

That's it. Product.new(price: 12).price is a Mint::Money.

Why Minting::Rails?

  • No serialization boilerplate — declare once, read/write Mint::Money everywhere.
  • Two storage modes — single column for fixed-currency apps (simpler), amount+currency columns for multi-currency records (more flexible).
  • Integer or decimal columns — auto-detects the column type and adjusts serialization (e.g. integer stores cents, decimal stores unit value).
  • Normalizes everything — pass a number, string, or Mint::Money; always get a Mint::Money back.
  • Currency enforcement — fixed-currency attributes reject wrong currencies at assignment time.
  • Built on Rails primitives — uses ActiveRecord::Type, composed_of, and normalizes under the hood. No monkey-patching of core classes.

At a glance — vs money-rails

Feature minting-rails money-rails
Declaration money_attribute :price monetize :price_cents
Column types integer, decimal, bigint — auto-detected integer cents only
Storage modes Single column, composite (amount+currency) Single cents column, composite (cents+currency)
Decimal columns Native — t.decimal :price Not supported — must convert to cents manually
Multi-currency money_attribute :price (convention: <name>_amount + <name>_currency) monetize :price_cents, with_currency: :price_currency
Rails integration ActiveRecord::Type + composed_of — no monkey-patches monetize overrides reader/writer methods
Query (fixed) Model.where(price: money)=, IN, BETWEEN, ORDER, SUM Through cents column (price_cents)
Query (multi) Model.where(price: money) Model.where(price_cents:, price_currency:)
Internal amount Rational BigDecimal
Performance See BENCHMARKS.md — wins 9/11 cells

Requirements

  • Ruby 3.3+
  • Rails 7.1.3.2+
  • Minting 1.6.0+

Installation

# Gemfile
gem 'minting-rails'
bundle install
bin/rails g mint:initializer

The generator creates config/initializers/minting.rb.

Configuration

# config/initializers/minting.rb
Mint.configure do |config|
  config.default_currency = 'USD'
  # enabled_currencies removed — all registered currencies are valid
end

See the Minting gem for full configuration options (custom currencies, formatting, rounding).

Usage — Two modes

Decision table

Fixed currency (single column) Multi-currency (amount + currency)
Migration t.decimal :price t.decimal :price_amount + t.string :price_currency
Model money_attribute :price, currency: 'USD' money_attribute :price
When to use Column always holds the same currency Each row can hold a different currency
Column type decimal, integer, or bigint decimal, integer, or bigint for amount; string for currency
Query Product.where(price: 10.mint('USD')) — full type support Offer.where(price: 10.mint('EUR')) — equality only

Fixed currency (single column)

Migration:

class CreateProducts < ActiveRecord::Migration[7.1]
  def change
    create_table :products do |t|
      t.decimal :price
      t.decimal :discount
      t.timestamps
    end
  end
end

Model:

class Product < ApplicationRecord
  money_attribute :price, currency: 'USD'
  money_attribute :discount, currency: 'USD'
end

Assignments are normalized to Mint::Money:

product = Product.new(price: 12, discount: '3.50')
product.price    # => [USD 12.00]
product.discount # => [USD 3.50]

A currency mismatch raises ArgumentError:

Product.new(price: 12.to_money('EUR'))
# => ArgumentError: ... has different currency. Only USD allowed.

Multi-currency (amount + currency columns)

Migration:

class CreateOffers < ActiveRecord::Migration[7.1]
  def change
    create_table :offers do |t|
      t.decimal :price_amount
      t.string  :price_currency
      t.timestamps
    end
  end
end

Model:

class Offer < ApplicationRecord
  money_attribute :price
end

The attribute is composed from price_amount and price_currency:

offer = Offer.new(price: 15.to_money('EUR'))
offer.price          # => [EUR 15.00]
offer.price_amount   # => 15.0
offer.price_currency # => "EUR"

When assigning a plain number or string, Mint.default_currency is used:

offer = Offer.new(price: '12')
offer.price.currency.code # => "USD"

Column type detection

Declare the column as decimal, integer, or bigint — the gem adapts:

# Migration
create_table :orders do |t|
  t.bigint  :total_amount  # stored as cents (subunits)
  t.string  :total_currency
end

# Model
class Order < ApplicationRecord
  money_attribute :total
end

Order.new(total: 19.99.to_money('USD')).total_amount # => 1999

Same for fixed-currency attributes:

# Migration
t.bigint :price

# Model (no change needed)
money_attribute :price, currency: 'USD'

Use integer/bigint for large tables (faster, smaller). Use decimal when SQL-level readability matters.

Custom column names

If your columns don't follow the <name>_amount / <name>_currency convention:

class Invoice < ApplicationRecord
  money_attribute :total, mapping: {
    amount:   :total_amount,
    currency: :currency_code
  }
end

The mapping keys are :amount and :currency; values are your database column names.

Querying

Fixed-currency attributes support Rails-native querying through the custom type:

# Equality
Product.where(price: 10.mint('USD'))

# IN clause
Product.where(price: [10.mint('USD'), 20.mint('USD')])

# BETWEEN
Product.where(price: 10.mint('USD')..20.mint('USD'))

# Ordering
Product.order(price: :desc)

# Aggregation
Product.where(price: 10.mint('USD')).sum(:price)

Multi-currency attributes support equality queries via composed_of:

Offer.where(price: 10.mint('EUR'))

For comparisons on multi-currency attributes, use the backing columns directly:

Offer.where(price_amount: 10..20, price_currency: 'EUR')
Offer.where('price_amount > ? AND price_currency = ?', 10, 'EUR')

Convenience methods

Minting::Rails adds small helpers on Numeric and String:

12.to_money('USD')    # => [USD 12.00]
12.dollars            # => [USD 12.00]
12.euros              # => [EUR 12.00]
'12.50'.mint('BRL')   # => [BRL 12.50]

If you prefer not to extend core classes, use Mint::Money.money(12, 'USD') instead.

vs money-rails

Money-rails is the most popular money-in-Rails gem. Here's how they compare side-by-side.

Model declaration

# minting-rails
class Product < ApplicationRecord
  money_attribute :price, currency: 'USD'          # single column, fixed currency
  money_attribute :total                           # two columns, multi-currency
end

# money-rails
class Product < ApplicationRecord
  monetize :price_cents                            # single cents column, fixed currency
  monetize :total_cents, with_currency: :total_currency  # two columns, multi-currency
end

Migration

# minting-rails — any numeric column type
create_table :products do |t|
  t.decimal :price              # stores 12.34
  t.integer :discount           # stores 1234 (cents)
  t.bigint  :total_amount       # stores 1999 (cents)
  t.string  :total_currency
end

# money-rails — integer cents only
create_table :products do |t|
  t.integer :price_cents        # stores 1234 (cents)
  t.integer :discount_cents     # stores 350 (cents)
  t.integer :total_cents
  t.string  :total_currency
end

Reading & writing

# minting-rails — pass any type, always get Mint::Money
product.price = 12.34          # stores 12.34 in decimal column
product.price = 1234           # stores 1234 in integer column
product.price = '$12.34'       # parses string
product.price                  # => [USD 12.34]

# money-rails — pass any type, always get Money
product.price_cents = 1234     # stores 1234
product.price = Money.new(1234, 'USD')
product.price                  # => #<Money fractional:1234 currency:USD>

Querying

# minting-rails (fixed-currency) — full type-aware querying
Product.where(price: 10.mint('USD'))
Product.where(price: [5.mint('USD'), 10.mint('USD')])
Product.where(price: 5.mint('USD')..15.mint('USD'))
Product.order(price: :desc)
Product.where(price: 10.mint('USD')).sum(:price)

# money-rails — query through cents column
Product.where(price_cents: 1000)
Product.where(price_cents: [500, 1000])
Product.where(price_cents: 500..1500)
Product.order(:price_cents)

Decimal columns

# minting-rails — works with decimal columns out of the box
# migration: t.decimal :price
money_attribute :price, currency: 'USD'

product.price = 12.34
product.price           # => [USD 12.34]
product.read_attribute(:price)  # => [USD 12.34]

# money-rails — no decimal column support
# migration: t.decimal :price  ← not supported
# Must use integer cents:
# migration: t.integer :price_cents
monetize :price_cents
product.price_cents = 1234
product.price           # => #<Money fractional:1234 currency:USD>

Multi-currency

# minting-rails
money_attribute :price   # expects price_amount + price_currency columns

offer = Offer.new(price: 15.to_money('EUR'))
offer.price             # => [EUR 15.00]
offer.price_amount      # => 15.0
offer.price_currency    # => "EUR"

# money-rails
monetize :price_cents, with_currency: :price_currency

offer = Offer.new(price: Money.new(1500, 'EUR'))
offer.price             # => #<Money fractional:1500 currency:EUR>
offer.price_cents       # => 1500
offer.price_currency    # => "EUR"

Column type auto-detection

# minting-rails — same declaration works with any column type
money_attribute :price, currency: 'USD'

# t.decimal :price   → stores human-readable value (12.34)
# t.integer :price   → stores cents (1234)
# t.bigint  :price   → stores cents (1234)

# money-rails — must always match the column name
monetize :price_cents   # column must be price_cents
monetize :price         # column must be price — no support for other types

Performance

See BENCHMARKS.md for detailed results across instantiation, persistence, reads, queries, arithmetic, and mass inserts. Minting-rails wins 9 of 11 benchmark cells, with the largest advantages in reads (up to 14× faster), arithmetic (6.6×), and mass inserts (1.6×).

What money-rails has (and minting-rails doesn't)

Minting-rails is intentionally minimal — it focuses on storing and reading money attributes with Rails primitives. Money-rails is a more mature gem (12+ years, 1.9k stars) with a broader feature set that minting-rails does not currently provide:

Feature money-rails minting-rails
Mongoid support Yes ActiveRecord only
Migration helpers add_monetize :products, :price None
View helpers humanized_money, money_without_cents, etc. None
I18n / locale files Built-in locale-aware formatting None
Test matcher monetize(:price_cents) RSpec matcher None
Currency exchange default_bank, add_rate, EuCentralBank None
Custom currencies register_currency for non-ISO codes Via minting gem config
Validation integration validates_numericality_of auto-added Must add manually
Rounding mode Configurable rounding_mode None
Per-request currency Lambda-based for multi-tenant apps Static default only
Allow nil monetize :x, allow_nil: true Must handle nil manually
Parse error control raise_error_on_money_parsing option Always raises
Community 1.9k stars, 386 forks, 897 commits New gem

If you need any of these features today, money-rails may be a better fit. minting-rails fills a specific niche: a lightweight, performant money-in-Rails solution built on standard Rails primitives.

Roadmap

  1. Allow nilmoney_attribute :price, currency: 'USD', allow_nil: true
  2. Method-level currency — lambda-based currency resolution for multi-tenant and instance-level scenarios
  3. Migration helper
  4. Internationalization

Contributions and suggestions are welcome — open an issue or PR at gferraz/minting-rails.

Development

bundle install
bundle exec rake test

The dummy Rails app under test/dummy exercises the engine in a full Rails environment.

Contributing

Bug reports and pull requests welcome at gferraz/minting-rails.

License

MIT