money_attribute 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb71d942a1dd57ae30fa3d1d8941efacf402d9346192b99cca4d0eabf69246fb
4
- data.tar.gz: 70dbb86d5d328e5d42fd09593358445d931261b59212d49f3a8e6a450d275c09
3
+ metadata.gz: 4101988f3c8acc6d5a4810519fa74d1fd27feaba2114ee047ecd755da3229c91
4
+ data.tar.gz: 2a7071882d41791e33c3d189bd5db42ea39b434c597f64a3c5b6ae88facd7e82
5
5
  SHA512:
6
- metadata.gz: 93e1d50ccda57e7a535fba457e4551525923358b45ef629b381bb04c900eb1fc7b76c1aa11d18410a0669fb96f610a53df1e76e10dcc58860e1d0cd8e3dd19f6
7
- data.tar.gz: 55d8b84306bf8988e066d426e525af1d6bfb2b0f202727a9a3a38aa5a52f9461d57edac0d5c003f5996141a5e662439d661109ba6aab975fd420b882ea754a29
6
+ metadata.gz: eac6fb9ca6ef50c717fc51b8c4aac7977e425787ea98032ae02e66091f74a66158b1f19f401b5b15df3cf8dc7a402aba942e9c7b55f1dc054ec2c617114d2c87
7
+ data.tar.gz: 2216348ddfa7df7d97ddacfe12d15a895d2e1c9ac28392719c14add69741a177ab5064b5a8da5befde3ec9b28a028e37924b4f3db433a2a70af626af5e619ddc
data/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  [![CI](https://github.com/gferraz/money-attribute/actions/workflows/ci.yml/badge.svg)](https://github.com/gferraz/money-attribute/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/money_attribute.svg)](https://badge.fury.io/rb/money_attribute)
5
5
 
6
- Store and read Active Record attributes as `Mint::Money` objects with no manual serialization.
6
+ Store and read Active Record attributes as `Money` objects with no manual serialization.
7
7
 
8
- `money_attribute` uses two DB columns (amount + currency) for per-row multi-currency data. A simpler `money_amount` variant is also available for fixed-currency models (see [note](#single-column-mode-money_amount-fixed-currency)).
8
+ `money_attribute` uses two DB columns (amount + currency) for per-row multi-currency data.
9
9
 
10
10
  ```ruby
11
11
  class Product < ApplicationRecord
@@ -32,8 +32,7 @@ p.price * 2 # => [USD 24.88]
32
32
  - [Convenience methods](#convenience-methods)
33
33
  - [Form helpers](#form-helpers)
34
34
  - [Roadmap](#roadmap)
35
- - [Development & Contributing](#development)
36
- - [License](#license)
35
+ - [Development](#development)
37
36
 
38
37
  ## Quick start
39
38
 
@@ -62,23 +61,23 @@ class Product < ApplicationRecord
62
61
  end
63
62
  ```
64
63
 
65
- That's it. `Product.new(price: 12.dollars).price` is a `Mint::Money`.
64
+ That's it. `Product.new(price: 12.dollars).price` is a `Money`.
66
65
 
67
66
  ## Why MoneyAttribute?
68
67
 
69
- - **No serialization boilerplate** — declare once, read/write `Mint::Money` everywhere.
68
+ - **No serialization boilerplate** — declare once, read/write `Money` everywhere.
70
69
  - **Integer or decimal columns** — auto-detects the column type and adjusts serialization (e.g. integer stores cents, decimal stores unit value).
71
- - **Normalizes everything** — pass a number, string, or `Mint::Money`; always get a `Mint::Money` back.
72
- - **Currency enforcement** — fixed-currency attributes reject wrong currencies at assignment time.
70
+ - **Normalizes everything** — pass a number, string, or `Money`; always get a `Money` back.
71
+ - **Currency handling** — each record stores its own currency and accepts registered currencies at assignment time.
73
72
  - **Built on Rails primitives** — uses `ActiveRecord::Type`, `composed_of`, and `normalizes` under the hood. No monkey-patching of core classes.
74
73
 
75
74
  ### At a glance — vs money-rails
76
75
 
77
76
  | Feature | MoneyAttribute | money-rails |
78
77
  |---|---|---|
79
- | **Declare** | `t.money_attribute :price` / `money_attribute :price` or `t.money_amount :price` / `money_amount :price` | `monetize :price_cents` |
78
+ | **Declare** | `t.money_attribute :price` / `money_attribute :price` | `monetize :price_cents` |
80
79
  | **Column types** | `integer`, `decimal`, `bigint` — auto-detected | `integer` cents only |
81
- | **Storage modes** | Composite (amount+currency), single column | Single cents column, composite (cents+currency) |
80
+ | **Storage mode** | Composite (amount+currency) | Single cents column, composite (cents+currency) |
82
81
  | **Decimal columns** | Native — `t.decimal :price` | Not supported — must convert to cents manually |
83
82
  | **Multi-currency** | `money_attribute :price` (convention: `<name>_amount` + `<name>_currency`) | `monetize :price_cents, with_currency: :price_currency` |
84
83
  | **Rails integration** | `ActiveRecord::Type` + `composed_of` — no monkey-patches | `monetize` overrides reader/writer methods |
@@ -93,7 +92,7 @@ For a detailed side-by-side comparison, see [COMPARISON.md](COMPARISON.md).
93
92
 
94
93
  - Ruby 3.3+
95
94
  - Rails 7.1.3.2+
96
- - [Minting](https://github.com/gferraz/minting) 2.0+
95
+ - [Minting](https://github.com/gferraz/minting) 2.1+
97
96
 
98
97
  ## Installation
99
98
 
@@ -150,7 +149,33 @@ class AddPriceToProducts < ActiveRecord::Migration[8.1]
150
149
  end
151
150
  ```
152
151
 
153
- ### Naming
152
+ ### Default column names
153
+
154
+ For `money_attribute :price`, the default columns are `price` for the amount
155
+ and `price_currency` for the currency. The migration helper and model macro use
156
+ the same mapping:
157
+
158
+ ```ruby
159
+ create_table :products do |t|
160
+ t.money_attribute :price
161
+ end
162
+
163
+ class Product < ApplicationRecord
164
+ money_attribute :price
165
+ end
166
+ ```
167
+
168
+ This creates and maps to:
169
+
170
+ ```text
171
+ price
172
+ price_currency
173
+ ```
174
+
175
+ The amount column defaults to `decimal(20,4)` and the currency column defaults
176
+ to `string(16)`.
177
+
178
+ ### Naming options
154
179
 
155
180
  **`money_attribute` (composite):**
156
181
 
@@ -159,7 +184,7 @@ end
159
184
  | `t.money_attribute :price` | `price` decimal(20,4) + `price_currency` string(16) | `money_attribute :price` |
160
185
  | `t.money_attribute :price_amount` | `price_amount` decimal(20,4) + `price_currency` string(16) | `money_attribute :price` |
161
186
  | `t.money_attribute :price, amount: { type: :fiat_integer }` | `price` bigint + `price_currency` string(16) | `money_attribute :price` |
162
- | `t.money_attribute :price, amount: { column: :a }, currency: { column: :c }` | `a` + `c` | `money_attribute :price, mapping: { amount: :a, currency: :c }` |
187
+ | `t.money_attribute :price, amount: { column: :a }, currency: { column: :c }` | `a` + `c` | `money_attribute :price, amount: { column: :a }, currency: { column: :c }` |
163
188
  | `t.money_attribute :price, currency: { limit: 5 }` | `price` decimal(20,4) + `price_currency` string(5) | `money_attribute :price` |
164
189
  | `t.remove_money_attribute :price` | Removes `price` + `price_currency` | `money_attribute :price` |
165
190
 
@@ -197,7 +222,7 @@ I18n.locale = :'pt-BR'
197
222
  Money.from(1234.56, 'USD').to_s # => "$1.234,56"
198
223
  ```
199
224
 
200
- The locale backend reads `number.currency.format` from your I18n translations and maps Rails format syntax (`%n` for amount, `%u` for unit) to `Mint::Money#to_s`. If the translation key is missing (no locale file for that language), it falls back to hardcoded defaults (`.` decimal, `,` thousand, `%<symbol>s%<amount>f` format).
225
+ The locale backend reads `number.currency.format` from your I18n translations and maps Rails format syntax (`%n` for amount, `%u` for unit) to `Money#to_s`. If the translation key is missing (no locale file for that language), it falls back to hardcoded defaults (`.` decimal, `,` thousand, `%<symbol>s%<amount>f` format).
201
226
 
202
227
  You can configure per-sign formatting by adding `positive`, `negative`, and `zero` keys to your locale:
203
228
 
@@ -282,38 +307,55 @@ Order.new(total: 19.99.to_money('USD')).total_amount # => 1999
282
307
 
283
308
  ## Custom column names
284
309
 
285
- If your columns don't follow the `<name>_amount` / `<name>_currency` convention:
310
+ If your columns do not follow the default mapping, use the same nested column
311
+ options as the migration helper:
286
312
 
287
313
  ```ruby
288
314
  class Invoice < ApplicationRecord
289
- money_attribute :total, mapping: {
290
- amount: :total_amount,
291
- currency: :currency_code
292
- }
315
+ money_attribute :total,
316
+ amount: { column: :total_amount },
317
+ currency: { column: :currency_code }
293
318
  end
294
319
  ```
295
320
 
296
- The mapping keys are `:amount` and `:currency`; values are your database column names. You can provide only one key the other falls back to the `<name>_amount` / `<name>_currency` convention:
321
+ You can provide only one nested option; the other falls back to the default
322
+ mapping. The older `mapping:` syntax remains supported (but it will eventually be deprecated):
297
323
 
298
324
  ```ruby
299
325
  class Invoice < ApplicationRecord
300
- money_attribute :total, mapping: { amount: :total_amount }
301
- # currency column inferred as `total_currency`
326
+ money_attribute :total, amount: { column: :total_amount }
327
+ money_attribute :subtotal, mapping: { amount: :subtotal }
302
328
  end
303
329
  ```
304
330
 
331
+ Model declarations accept only the nested `:column` option. Migration-only
332
+ options such as `:type`, `:null`, and `:default` belong in the migration.
333
+
305
334
  ## Column resolution
306
335
 
307
- `money_attribute :name` is always composite. It resolves columns in this order:
336
+ `money_attribute :name` is always composite. Columns are resolved in two phases:
308
337
 
309
- | Step | Condition | Columns used |
310
- |---|---|---|
311
- | 1 | `mapping:` provided | As specified (missing keys fall back to `<name>_amount` / `<name>_currency`) |
312
- | 2 | `name_currency` column exists AND `name` column exists | `name` + `name_currency` |
313
- | 3 | `name == 'amount'` AND `currency` column exists | `amount` + `currency` |
314
- | 4 | None of the above | `<name>_amount` + `<name>_currency` (convention) |
338
+ **Phase 1 Default mapping** is determined by which columns exist:
339
+
340
+ | Condition | Default columns |
341
+ |---|---|
342
+ | `name_currency` exists AND `name` exists | `name` + `name_currency` |
343
+ | `name == 'amount'` AND `currency` exists | `amount` + `currency` |
344
+ | Otherwise | `<name>_amount` + `<name>_currency` |
315
345
 
316
- Step 4 raises `ArgumentError` if the convention columns don't exist. For single-column fixed-currency attributes, see [`money_amount`](#single-column-mode-money_amount-fixed-currency).
346
+ The first row is the canonical mapping used by the migration helpers. The
347
+ final row supports existing schemas that use the `<name>_amount` /
348
+ `<name>_currency` convention. For example, `money_attribute :price` also maps
349
+ to `price_amount` + `price_currency` when those are the available columns.
350
+
351
+ **Phase 2 — Override** via `mapping:` is merged on top of the default. Missing keys inherit from the default mapping:
352
+
353
+ ```ruby
354
+ money_attribute :total, mapping: { amount: :total_amount }
355
+ # currency falls back to default => :total_currency
356
+ ```
357
+
358
+ Raises `ArgumentError` if the resolved columns don't exist. For single-column fixed-currency attributes, see the [advanced `money_amount` section](#advanced-money_amount-single-column-fixed-currency).
317
359
 
318
360
  **Example**
319
361
 
@@ -325,7 +367,6 @@ create_table :financial_transactions do |t|
325
367
  t.string :discount_currency, limit: 3
326
368
  t.decimal :price_amount
327
369
  t.string :price_currency, limit: 3
328
- t.bigint :tax
329
370
  t.decimal :total_amount
330
371
  t.string :currency_code, limit: 3
331
372
  end
@@ -337,7 +378,6 @@ class FinancialTransaction < ApplicationRecord
337
378
  money_attribute :discount # step 2: discount(int) + discount_currency
338
379
  money_attribute :price # step 4: price_amount + price_currency
339
380
  money_attribute :total, mapping: { amount: :total_amount, currency: :currency_code } # step 1: explicit
340
- money_amount :tax # single-column, fixed-currency (uses default currency)
341
381
  end
342
382
  ```
343
383
 
@@ -348,7 +388,7 @@ end
348
388
  Multi-currency (`money_attribute`) attributes support equality queries via `composed_of`:
349
389
 
350
390
  ```ruby
351
- Offer.where(price: 10.to_money('EUR'))
391
+ Offer.where(price: 10.euros)
352
392
  ```
353
393
 
354
394
  For comparisons, use the backing columns directly:
@@ -358,15 +398,25 @@ Offer.where(price_amount: 10..20, price_currency: 'EUR')
358
398
  Offer.where('price_amount > ? AND price_currency = ?', 10, 'EUR')
359
399
  ```
360
400
 
361
- Fixed-currency (`money_amount`) attributes support full Rails-native querying through the custom type — equality, IN, BETWEEN, ordering, and aggregation all work:
401
+ ### Composite query matrix
362
402
 
363
- ```ruby
364
- Product.where(price: 10.to_money('USD')) # equality
365
- Product.where(price: [10.to_money('USD'), 20.to_money('USD')]) # IN
366
- Product.where(price: 10.to_money('USD')..20.to_money('USD')) # BETWEEN
367
- Product.order(price: :desc) # ordering
368
- Product.where(price: 10.to_money('USD')).sum(:price) # aggregation
369
- ```
403
+ For `money_attribute`, use native Rails queries for simple equality. Use the
404
+ money-aware helpers when working with `Money` objects or when you need an
405
+ explicit currency filter alongside an amount query:
406
+
407
+ | Operation | Native Rails | Money-aware helper |
408
+ |---|---|---|
409
+ | Equality | `where(price: 10.euros)` | `where_amount(price: 10.euros)` |
410
+ | `IN` | Query backing columns directly | `where_amount(price: [10.euros, 20.euros])` |
411
+ | Range/comparison | Query backing columns directly | `where_currency(price: 'EUR').where_amount(price: 10.euros..20.euros)` |
412
+ | Currency filter | `where(price_currency: 'EUR')` | `where_currency(price: 'EUR')` |
413
+ | Ordering | Query backing columns directly | `order_by_amount(price: :desc)` |
414
+ | Pluck | Returns backing column values | `pluck_amount(:price)` returns `Money` |
415
+ | Pick | Returns backing column values | `pick_amount(:price)` returns `Money` |
416
+ | Sum | Group and sum backing columns directly | `sum_amount(:price)` groups by currency |
417
+
418
+ `where_amount` accepts `Money` values and converts them to the backing amount
419
+ unit. Pair it with `where_currency` when the currency must be constrained.
370
420
 
371
421
  ### Money-aware query helpers
372
422
 
@@ -377,35 +427,28 @@ For multi-currency attributes, manually decomposing columns is tedious. The quer
377
427
  Filters by amount value. Accepts a scalar, Range, or Array.
378
428
 
379
429
  ```ruby
380
- Offer.where_amount(price: 10) # equality (any currency)
381
- Offer.where_amount(price: [10, 30]) # IN — matches EUR 10, USD 30
382
- Offer.where_amount(price: 10..100) # BETWEEN (inclusive)
383
- Offer.where_amount(price: 10...100) # BETWEEN (exclusive upper bound)
430
+ Offer.where_amount(price: 10.euros) # equality
431
+ Offer.where_amount(price: [10.euros, 30.euros]) # IN
432
+ Offer.where_amount(price: 10.euros..100.euros) # BETWEEN (inclusive)
433
+ Offer.where_amount(price: 10.euros...100.euros) # BETWEEN (exclusive upper bound)
384
434
  ```
385
435
 
386
- Ranges work across currencies `10..50` matches EUR 10 and USD 50:
436
+ For a currency-specific range, constrain the currency explicitly:
387
437
 
388
438
  ```ruby
389
439
  Offer.create!(price: 10.euros)
390
- Offer.create!(price: 50.dollars)
440
+ Offer.create!(price: 50.euros)
391
441
 
392
- Offer.where_amount(price: 10..50) # => both records
442
+ Offer.where_currency(price: 'EUR').where_amount(price: 10.euros..50.euros) # => both records
393
443
  ```
394
444
 
395
- For integer (subunit) columns, pass `Mint::Money` objects directly — subunit conversion is handled automatically:
445
+ For integer (subunit) columns, pass `Money` objects directly — subunit conversion is handled automatically:
396
446
 
397
447
  ```ruby
398
- FinancialTransaction.where_amount(amount: [10.dollars, 10.yens])
448
+ FinancialTransaction.where_amount(amount: [10.dollars, 20.dollars])
399
449
  FinancialTransaction.where_amount(amount: 10.dollars..100.dollars)
400
450
  ```
401
451
 
402
- For decimal columns, raw numbers work:
403
-
404
- ```ruby
405
- SimpleOffer.where_amount(price: 50)
406
- SimpleOffer.where_amount(price: 10..100)
407
- ```
408
-
409
452
  #### `where_currency`
410
453
 
411
454
  Filters by currency code. Composite attributes only — raises `ArgumentError` for single-column attributes.
@@ -456,13 +499,27 @@ SimpleOffer.sum_amount(:price)
456
499
  # => [BRL 60.00] (single-column always returns one Money)
457
500
 
458
501
  Offer.none.sum_amount(:price)
459
- # => [Mint::Money(0, 'BRL')] (empty result returns zero Money)
502
+ # => [BRL 0.00] (empty result returns zero Money)
460
503
  ```
461
504
 
462
505
  ### Notes
463
506
 
464
507
  All query helpers raise `ArgumentError` for non-money attributes. Internally, money attribute metadata is registered per model class. The same attribute name can be used safely in different models, but subclasses do not automatically inherit a parent model's registered money attributes.
465
508
 
509
+ ### Introspection
510
+
511
+ Use `money_attribute?` and `money_attribute_kind` when building generic model
512
+ or form code:
513
+
514
+ ```ruby
515
+ Product.money_attribute?(:price) # => true
516
+ Product.money_attribute_kind(:price) # => :composite or :single
517
+ Product.money_attribute?(:unknown) # => false
518
+ ```
519
+
520
+ `money_attribute_kind` returns `:composite` for `money_attribute`, `:single`
521
+ for `money_amount`, and `nil` for undeclared attributes.
522
+
466
523
  ## Convenience methods
467
524
 
468
525
  MoneyAttribute adds small helpers on `Numeric` and `String`:
@@ -477,24 +534,53 @@ MoneyAttribute adds small helpers on `Numeric` and `String`:
477
534
 
478
535
  ## Form helpers
479
536
 
480
- MoneyAttribute adds `money_field` and `money_amount_field` to Rails form builders. `money_field` renders a text input with the locale-formatted money string; `money_amount_field` renders a number input with the raw decimal value.
537
+ MoneyAttribute adds `money_field` to Rails form builders for composite money attributes. It renders a text input with the locale-formatted money string.
481
538
 
482
539
  ```erb
483
540
  <%= form_with model: @product do |form| %>
484
541
  <%= form.label :price %>
485
542
  <%= form.money_field :price %> <!-- text input, e.g. "$1,234.56" -->
486
-
487
- <%= form.label :tax %>
488
- <%= form.money_amount_field :tax %> <!-- number input, e.g. "1234.56" -->
489
543
  <% end %>
490
544
  ```
491
545
 
492
- ### Single-column mode `money_amount` (fixed-currency)
546
+ The field submits a locale-formatted string under the normal model parameter:
493
547
 
494
- `money_amount` wraps a numeric column as `Mint::Money` using the application's default currency. No per-row currency. A lighter alternative when you don't need multi-currency support.
548
+ ```ruby
549
+ # app/controllers/products_controller.rb
550
+ def create
551
+ @product = Product.new(product_params)
552
+
553
+ if @product.save
554
+ redirect_to @product
555
+ else
556
+ render :new, status: :unprocessable_entity
557
+ end
558
+ end
559
+
560
+ private
561
+
562
+ def product_params
563
+ params.require(:product).permit(:name, :price)
564
+ end
565
+ ```
566
+
567
+ The model normalizes the submitted string through `MoneyAttribute::Converter`.
568
+ For a composite attribute, provide a separate currency input or set the
569
+ currency explicitly when the form must accept a currency different from the
570
+ configured default.
571
+
572
+ ### Advanced: `money_amount` (single-column, fixed-currency)
573
+
574
+ `money_amount` is an advanced compatibility utility for schemas where one
575
+ numeric column stores amounts in one fixed currency. It is not suitable for
576
+ per-row or multi-currency data. New applications should normally use
577
+ `money_attribute`.
495
578
 
496
579
  The accessor name must match the column name. `money_amount` does not support custom column mapping.
497
580
 
581
+ Use `money_amount_field` when rendering a form for this mode; it renders a
582
+ number input with the raw decimal value.
583
+
498
584
  #### Migration helpers
499
585
 
500
586
  | Method | Action |
@@ -505,9 +591,9 @@ The accessor name must match the column name. `money_amount` does not support cu
505
591
  Default column: `decimal(20,4)`. The top-level `type:` shortcut selects the column type:
506
592
 
507
593
  ```ruby
508
- t.money_amount :price # decimal(20,4)
594
+ t.money_amount :price # decimal(20,4)
509
595
  t.money_amount :btc_balance, type: :crypto_decimal # decimal(36,18)
510
- t.money_amount :qty, type: :fiat_integer # bigint
596
+ t.money_amount :qty, type: :fiat_integer # bigint
511
597
  ```
512
598
 
513
599
  #### Naming
@@ -545,7 +631,19 @@ money_amount :price
545
631
 
546
632
  #### Querying
547
633
 
548
- Fixed-currency attributes support full Rails-native querying see [Querying](#querying) for examples.
634
+ Fixed-currency attributes support full Rails-native querying. The helper methods
635
+ are also available when a consistent query interface is useful:
636
+
637
+ | Operation | Native Rails | Money-aware helper |
638
+ |---|---|---|
639
+ | Equality | `where(price: 10.reais)` | `where_amount(price: 10.reais)` |
640
+ | `IN` | `where(price: [10.reais, 20.reais])` | `where_amount(price: [10.reais, 20.reais])` |
641
+ | Range/comparison | `where(price: 10.reais..20.reais)` | `where_amount(price: 10.reais..20.reais)` |
642
+ | Currency filter | Not applicable | Raises `ArgumentError` |
643
+ | Ordering | `order(price: :desc)` | `order_by_amount(price: :desc)` |
644
+ | Pluck | `pluck(:price)` | `pluck_amount(:price)` |
645
+ | Pick | `pick(:price)` | `pick_amount(:price)` |
646
+ | Sum | `sum(:price)` | `sum_amount(:price)` |
549
647
 
550
648
  ## Roadmap
551
649
 
@@ -564,8 +662,4 @@ The dummy Rails app under `test/dummy` exercises the engine in a full Rails envi
564
662
 
565
663
  ## Contributing
566
664
 
567
- Bug reports and pull requests welcome at [gferraz/money-attribute](https://github.com/gferraz/money-attribute).
568
-
569
- ## License
570
-
571
- [MIT](MIT-LICENSE)
665
+ Bug reports welcome at [gferraz/money-attribute](https://github.com/gferraz/money-attribute).
data/Rakefile CHANGED
@@ -15,7 +15,7 @@ Rake::TestTask.new(:test_run) do |t|
15
15
  end
16
16
 
17
17
  desc 'Migrate test database'
18
- task :test_db_migrate do # rubocop:disable Rails/RakeEnvironment
18
+ task :test_db_migrate do
19
19
  sh({ 'RAILS_ENV' => 'test' }, 'bin/rails', 'db:migrate', chdir: 'test/dummy', %i[out err] => File::NULL)
20
20
  end
21
21
 
@@ -27,13 +27,13 @@ task test: %i[test_db_migrate test_run]
27
27
  %w[postgresql mysql2].each do |adapter|
28
28
  namespace :test do
29
29
  desc "Migrate test database (#{adapter})"
30
- task "db_migrate:#{adapter}" do # rubocop:disable Rails/RakeEnvironment
30
+ task "db_migrate:#{adapter}" do
31
31
  sh({ 'RAILS_ENV' => 'test', 'DATABASE_ADAPTER' => adapter },
32
32
  'bin/rails', 'db:migrate', chdir: 'test/dummy', %i[out err] => File::NULL)
33
33
  end
34
34
 
35
35
  desc "Run tests against #{adapter}"
36
- task adapter do # rubocop:disable Rails/RakeEnvironment
36
+ task adapter do
37
37
  Rake::Task[:"test:db_migrate:#{adapter}"].invoke
38
38
  sh({ 'DATABASE_ADAPTER' => adapter }, 'bundle', 'exec', 'rake', 'test_run')
39
39
  end
@@ -41,7 +41,7 @@ task test: %i[test_db_migrate test_run]
41
41
  end
42
42
 
43
43
  desc 'Run tests against all adapters (sqlite3, postgresql, mysql2)'
44
- task 'test:all' do # rubocop:disable Rails/RakeEnvironment
44
+ task 'test:all' do
45
45
  Rake::Task[:test].invoke
46
46
  %w[postgresql mysql2].each do |adapter|
47
47
  Rake::Task[:"test:#{adapter}"].invoke
@@ -50,7 +50,7 @@ end
50
50
 
51
51
  # --- Benchmark ---
52
52
 
53
- desc 'Run money_attribute vs money-rails benchmark'
53
+ desc 'Run money_attribute vs plain Rails vs money-rails benchmark'
54
54
  task bench: :test_db_migrate do
55
55
  puts
56
56
  puts '=' * 80
@@ -59,6 +59,13 @@ task bench: :test_db_migrate do
59
59
  sh({ 'RAILS_ENV' => 'test', 'BENCH_SIDE' => 'minting' },
60
60
  'bundle', 'exec', 'ruby', 'benchmark/comparison.rb')
61
61
 
62
+ puts
63
+ puts '=' * 80
64
+ puts 'plain Rails (raw columns, no monetization)'
65
+ puts '=' * 80
66
+ sh({ 'RAILS_ENV' => 'test', 'BENCH_SIDE' => 'plain' },
67
+ 'bundle', 'exec', 'ruby', 'benchmark/comparison.rb')
68
+
62
69
  puts
63
70
  puts '=' * 80
64
71
  puts 'money-rails (money gem)'
@@ -69,6 +76,24 @@ task bench: :test_db_migrate do
69
76
  end
70
77
 
71
78
  desc 'Generate consolidated benchmark report (markdown)'
72
- task 'bench:report' do # rubocop:disable Rails/RakeEnvironment
79
+ task 'bench:report' do
73
80
  ruby 'benchmark/report.rb'
74
81
  end
82
+
83
+ desc 'Profile hot paths with stackprof (MODE=string_query|pluck|read_cached|multi_record|arithmetic|all)'
84
+ task 'bench:profile' do
85
+ mode = ENV['MODE'] || 'all'
86
+ sh({ 'RAILS_ENV' => 'test' }, 'bundle', 'exec', 'ruby', 'benchmark/profile.rb', mode)
87
+ end
88
+
89
+ desc 'Run focused performance roadmap benchmarks'
90
+ task 'bench:performance' do
91
+ sh({ 'RAILS_ENV' => 'test' }, 'bundle', 'exec', 'ruby', 'benchmark/performance.rb')
92
+ end
93
+
94
+ desc 'Compare two focused performance benchmark result files'
95
+ task 'bench:performance:compare' do
96
+ baseline = ENV.fetch('BASELINE')
97
+ current = ENV.fetch('CURRENT')
98
+ sh 'bundle', 'exec', 'ruby', 'benchmark/compare_performance.rb', baseline, current
99
+ end
@@ -1,12 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MoneyAttribute
4
+ # Generator namespace for money_attribute.
4
5
  module Generators
6
+ # Rails generator that installs the money_attribute initializer.
7
+ #
8
+ # @example
9
+ # rails generate money_attribute:initializer
10
+ #
11
+ # @api private
5
12
  class InitializerGenerator < ::Rails::Generators::Base
6
13
  source_root File.expand_path('../templates', __dir__)
7
14
 
8
15
  desc 'Creates MoneyAttribute initializer.'
9
16
 
17
+ # Copies the initializer template into the application.
18
+ #
19
+ # @return [void]
10
20
  def copy_initializer
11
21
  copy_file 'money_attribute.rb', 'config/initializers/money_attribute.rb'
12
22
  end
@@ -21,7 +21,9 @@ module MoneyAttribute
21
21
  #
22
22
  # Created by +money_attribute+ or +money_amount+ and stored in the class-level registry.
23
23
  # Used by query helpers to resolve column names, build Money values, and generate SQL.
24
- AttributeSpec = Struct.new(:name, :kind, :amount_col, :currency_col, :amount_type, keyword_init: true) do
24
+ #
25
+ # @api private
26
+ AttributeSpec = Struct.new(:name, :kind, :amount_column, :currency_column, :amount_type, keyword_init: true) do
25
27
  # @return [Boolean] +true+ when the spec describes a two-column (amount + currency) attribute.
26
28
  def composite? = kind == :composite
27
29
 
@@ -32,7 +34,7 @@ module MoneyAttribute
32
34
  #
33
35
  # @return [Array<String>] two-element array for composite, one-element for single.
34
36
  def columns
35
- @columns ||= (composite? ? [amount_col, currency_col] : [amount_col]).freeze
37
+ @columns ||= (composite? ? [amount_column, currency_column] : [amount_column]).freeze
36
38
  end
37
39
 
38
40
  # @return [Boolean] +true+ when the amount column stores subunits (bigint).
@@ -42,7 +44,9 @@ module MoneyAttribute
42
44
  def amount_extractor = integer_amount? ? :subunits : :to_d
43
45
 
44
46
  # @return [Hash{String => Symbol}] mapping suitable for +composed_of+.
45
- def composed_of_mapping = { amount_col => amount_extractor, currency_col => :currency_code }
47
+ def composed_of_mapping
48
+ @composed_of_mapping ||= { amount_column => amount_extractor, currency_column => :currency_code }.freeze
49
+ end
46
50
 
47
51
  # @return [Proc] the constructor lambda used by +composed_of+ to instantiate Money values.
48
52
  def constructor
@@ -54,6 +58,7 @@ module MoneyAttribute
54
58
  # @param amount [Integer, BigDecimal, nil] the raw column value
55
59
  # @param currency [String, Mint::Currency, nil] the currency code or object
56
60
  # @return [Mint::Money, nil] the resolved money value, or nil when amount is nil
61
+ # @api private
57
62
  def build_money(amount, currency)
58
63
  return unless amount
59
64
  return amount if amount.is_a?(Mint::Money)
@@ -68,6 +73,7 @@ module MoneyAttribute
68
73
  #
69
74
  # @param value [Mint::Money, Numeric] the query value
70
75
  # @return [Integer, BigDecimal, Mint::Money] the normalized value
76
+ # @api private
71
77
  def normalize_query_value(value)
72
78
  return value unless integer_amount?
73
79
  return value.subunits if value.is_a?(Mint::Money)