Module: MoneyAttribute::MigrationExtensions::Helper

Included in:
SchemaStatements, TableDefinition
Defined in:
lib/money_attribute/migration_extensions/helper.rb

Overview

Shared argument-parsing logic for migration helpers.

Resolves accessor names, column overrides, and amount type configuration into concrete column definitions. Included by both SchemaStatements and TableDefinition.

Constant Summary collapse

AMOUNT_CONFIG =
{
  crypto_decimal: { type: :decimal, precision: 36, scale: 18 },
  fiat_decimal: { type: :decimal, precision: 20, scale: 4 },
  fiat_integer: { type: :bigint }
}.freeze
CURRENCY_MIN_LIMIT =
8
CURRENCY_DEFAULT_LIMIT =
20

Instance Attribute Summary collapse

Instance Attribute Details

#AMOUNT_CONFIGHash{Symbol => Hash}

Returns mapping of symbolic type names to column type/hash pairs.

Returns:

  • (Hash{Symbol => Hash})

    mapping of symbolic type names to column type/hash pairs



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/money_attribute/migration_extensions/helper.rb', line 18

module Helper
  AMOUNT_CONFIG = {
    crypto_decimal: { type: :decimal, precision: 36, scale: 18 },
    fiat_decimal: { type: :decimal, precision: 20, scale: 4 },
    fiat_integer: { type: :bigint }
  }.freeze

  CURRENCY_MIN_LIMIT = 8
  CURRENCY_DEFAULT_LIMIT = 20

  private

  # Parses arguments for a single-column (amount-only) migration.
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param options [Hash] column options
  # @option options [Symbol] :column explicit column name override
  # @option options [Symbol] :type amount type (+:fiat_decimal+,
  #   +:crypto_decimal+, +:fiat_integer+)
  # @option options [Boolean] :null whether the column allows NULL
  # @option options [Object] :default default value for the column
  # @return [Array(String, Hash)] column name and merged options hash
  # @raise [ArgumentError] if +precision:+ or +scale:+ are given, or
  #   type is unrecognized
  # @api private
  def parse_money_amount_args(accessor, options)
    options ||= {}
    if options.key?(:precision) || options.key?(:scale)
      raise ArgumentError,
            'precision:/scale: are not configurable — money_attribute uses fixed, ' \
            'vetted values per type (:crypto_decimal, :fiat_decimal, :fiat_integer) ' \
            'to prevent under-precision bugs, particularly for crypto amounts.'
    end

    column = (options[:column] || accessor).to_s

    config = AMOUNT_CONFIG[options[:type] || :fiat_decimal]
    unless config
      raise ArgumentError, "Invalid type #{options[:type]}. Use :crypto_decimal, :fiat_decimal or :fiat_integer"
    end

    options = { null: options[:null], default: options[:default] }.compact
    [column, config.merge(options)]
  end

  # Parses arguments for the currency column in a composite migration.
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param options [Hash] currency column options
  # @option options [Symbol] :column explicit currency column name override
  # @option options [Integer] :limit string limit for the column
  # @option options [Boolean] :null whether the column allows NULL
  # @option options [Object] :default default value for the column
  # @return [Array(String, Hash)] column name and merged options hash
  # @raise [ArgumentError] if limit is below {CURRENCY_MIN_LIMIT}
  # @api private
  def parse_currency_args(accessor, options)
    options ||= {}
    limit = (options[:limit] || CURRENCY_DEFAULT_LIMIT).to_i
    if limit < CURRENCY_MIN_LIMIT
      raise ArgumentError,
            "currency limit: #{limit} is too small to hold an ISO 4217 code and crypto popular codes" \
            "(minimum #{CURRENCY_MIN_LIMIT}). Omit limit: to use the default of #{CURRENCY_DEFAULT_LIMIT}, " \
            "or pass a value >= #{CURRENCY_MIN_LIMIT}."
    end

    column = currency_column_name(accessor, options[:column])
    [column, { limit:, null: options[:null], default: options[:default] }.compact]
  end

  # Resolves the currency column name for the given accessor.
  #
  # Resolution order:
  # 1. Explicit +column_override+ → returned as-is
  # 2. Accessor is +:amount+ → +currency+
  # 3. Accessor ends with +_amount+ → strips suffix and appends +_currency+
  # 4. Otherwise → +<accessor>_currency+
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param column_override [Symbol, String, nil] explicit column name
  # @return [String] the resolved currency column name
  # @api private
  def currency_column_name(accessor, column_override)
    return column_override.to_s if column_override

    name = accessor.to_s
    return 'currency' if name == 'amount'

    radical = name.end_with?('_amount') ? name.sub(/_amount$/, '') : name
    "#{radical}_currency"
  end

  # Parses arguments for a composite (amount + currency) migration.
  #
  # Delegates to {#parse_money_amount_args} and {#parse_currency_args}
  # using the nested +:amount+ and +:currency+ option keys.
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param options [Hash] migration options
  # @option options [Hash] :amount amount column options
  # @option options [Hash] :currency currency column options
  # @return [Array(String, String, Hash, Hash)] amount column name,
  #   currency column name, amount options, currency options
  # @api private
  def parse_money_args(accessor, options = {})
    amount_column, amount_options = parse_money_amount_args(accessor, options[:amount])
    currency_column, currency_options = parse_currency_args(accessor, options[:currency])

    [amount_column, currency_column, amount_options, currency_options]
  end
end

#CURRENCY_DEFAULT_LIMITInteger

Returns default currency column string limit (20).

Returns:

  • (Integer)

    default currency column string limit (20)



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/money_attribute/migration_extensions/helper.rb', line 18

module Helper
  AMOUNT_CONFIG = {
    crypto_decimal: { type: :decimal, precision: 36, scale: 18 },
    fiat_decimal: { type: :decimal, precision: 20, scale: 4 },
    fiat_integer: { type: :bigint }
  }.freeze

  CURRENCY_MIN_LIMIT = 8
  CURRENCY_DEFAULT_LIMIT = 20

  private

  # Parses arguments for a single-column (amount-only) migration.
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param options [Hash] column options
  # @option options [Symbol] :column explicit column name override
  # @option options [Symbol] :type amount type (+:fiat_decimal+,
  #   +:crypto_decimal+, +:fiat_integer+)
  # @option options [Boolean] :null whether the column allows NULL
  # @option options [Object] :default default value for the column
  # @return [Array(String, Hash)] column name and merged options hash
  # @raise [ArgumentError] if +precision:+ or +scale:+ are given, or
  #   type is unrecognized
  # @api private
  def parse_money_amount_args(accessor, options)
    options ||= {}
    if options.key?(:precision) || options.key?(:scale)
      raise ArgumentError,
            'precision:/scale: are not configurable — money_attribute uses fixed, ' \
            'vetted values per type (:crypto_decimal, :fiat_decimal, :fiat_integer) ' \
            'to prevent under-precision bugs, particularly for crypto amounts.'
    end

    column = (options[:column] || accessor).to_s

    config = AMOUNT_CONFIG[options[:type] || :fiat_decimal]
    unless config
      raise ArgumentError, "Invalid type #{options[:type]}. Use :crypto_decimal, :fiat_decimal or :fiat_integer"
    end

    options = { null: options[:null], default: options[:default] }.compact
    [column, config.merge(options)]
  end

  # Parses arguments for the currency column in a composite migration.
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param options [Hash] currency column options
  # @option options [Symbol] :column explicit currency column name override
  # @option options [Integer] :limit string limit for the column
  # @option options [Boolean] :null whether the column allows NULL
  # @option options [Object] :default default value for the column
  # @return [Array(String, Hash)] column name and merged options hash
  # @raise [ArgumentError] if limit is below {CURRENCY_MIN_LIMIT}
  # @api private
  def parse_currency_args(accessor, options)
    options ||= {}
    limit = (options[:limit] || CURRENCY_DEFAULT_LIMIT).to_i
    if limit < CURRENCY_MIN_LIMIT
      raise ArgumentError,
            "currency limit: #{limit} is too small to hold an ISO 4217 code and crypto popular codes" \
            "(minimum #{CURRENCY_MIN_LIMIT}). Omit limit: to use the default of #{CURRENCY_DEFAULT_LIMIT}, " \
            "or pass a value >= #{CURRENCY_MIN_LIMIT}."
    end

    column = currency_column_name(accessor, options[:column])
    [column, { limit:, null: options[:null], default: options[:default] }.compact]
  end

  # Resolves the currency column name for the given accessor.
  #
  # Resolution order:
  # 1. Explicit +column_override+ → returned as-is
  # 2. Accessor is +:amount+ → +currency+
  # 3. Accessor ends with +_amount+ → strips suffix and appends +_currency+
  # 4. Otherwise → +<accessor>_currency+
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param column_override [Symbol, String, nil] explicit column name
  # @return [String] the resolved currency column name
  # @api private
  def currency_column_name(accessor, column_override)
    return column_override.to_s if column_override

    name = accessor.to_s
    return 'currency' if name == 'amount'

    radical = name.end_with?('_amount') ? name.sub(/_amount$/, '') : name
    "#{radical}_currency"
  end

  # Parses arguments for a composite (amount + currency) migration.
  #
  # Delegates to {#parse_money_amount_args} and {#parse_currency_args}
  # using the nested +:amount+ and +:currency+ option keys.
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param options [Hash] migration options
  # @option options [Hash] :amount amount column options
  # @option options [Hash] :currency currency column options
  # @return [Array(String, String, Hash, Hash)] amount column name,
  #   currency column name, amount options, currency options
  # @api private
  def parse_money_args(accessor, options = {})
    amount_column, amount_options = parse_money_amount_args(accessor, options[:amount])
    currency_column, currency_options = parse_currency_args(accessor, options[:currency])

    [amount_column, currency_column, amount_options, currency_options]
  end
end

#CURRENCY_MIN_LIMITInteger

Returns minimum allowed currency column string limit (8).

Returns:

  • (Integer)

    minimum allowed currency column string limit (8)



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/money_attribute/migration_extensions/helper.rb', line 18

module Helper
  AMOUNT_CONFIG = {
    crypto_decimal: { type: :decimal, precision: 36, scale: 18 },
    fiat_decimal: { type: :decimal, precision: 20, scale: 4 },
    fiat_integer: { type: :bigint }
  }.freeze

  CURRENCY_MIN_LIMIT = 8
  CURRENCY_DEFAULT_LIMIT = 20

  private

  # Parses arguments for a single-column (amount-only) migration.
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param options [Hash] column options
  # @option options [Symbol] :column explicit column name override
  # @option options [Symbol] :type amount type (+:fiat_decimal+,
  #   +:crypto_decimal+, +:fiat_integer+)
  # @option options [Boolean] :null whether the column allows NULL
  # @option options [Object] :default default value for the column
  # @return [Array(String, Hash)] column name and merged options hash
  # @raise [ArgumentError] if +precision:+ or +scale:+ are given, or
  #   type is unrecognized
  # @api private
  def parse_money_amount_args(accessor, options)
    options ||= {}
    if options.key?(:precision) || options.key?(:scale)
      raise ArgumentError,
            'precision:/scale: are not configurable — money_attribute uses fixed, ' \
            'vetted values per type (:crypto_decimal, :fiat_decimal, :fiat_integer) ' \
            'to prevent under-precision bugs, particularly for crypto amounts.'
    end

    column = (options[:column] || accessor).to_s

    config = AMOUNT_CONFIG[options[:type] || :fiat_decimal]
    unless config
      raise ArgumentError, "Invalid type #{options[:type]}. Use :crypto_decimal, :fiat_decimal or :fiat_integer"
    end

    options = { null: options[:null], default: options[:default] }.compact
    [column, config.merge(options)]
  end

  # Parses arguments for the currency column in a composite migration.
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param options [Hash] currency column options
  # @option options [Symbol] :column explicit currency column name override
  # @option options [Integer] :limit string limit for the column
  # @option options [Boolean] :null whether the column allows NULL
  # @option options [Object] :default default value for the column
  # @return [Array(String, Hash)] column name and merged options hash
  # @raise [ArgumentError] if limit is below {CURRENCY_MIN_LIMIT}
  # @api private
  def parse_currency_args(accessor, options)
    options ||= {}
    limit = (options[:limit] || CURRENCY_DEFAULT_LIMIT).to_i
    if limit < CURRENCY_MIN_LIMIT
      raise ArgumentError,
            "currency limit: #{limit} is too small to hold an ISO 4217 code and crypto popular codes" \
            "(minimum #{CURRENCY_MIN_LIMIT}). Omit limit: to use the default of #{CURRENCY_DEFAULT_LIMIT}, " \
            "or pass a value >= #{CURRENCY_MIN_LIMIT}."
    end

    column = currency_column_name(accessor, options[:column])
    [column, { limit:, null: options[:null], default: options[:default] }.compact]
  end

  # Resolves the currency column name for the given accessor.
  #
  # Resolution order:
  # 1. Explicit +column_override+ → returned as-is
  # 2. Accessor is +:amount+ → +currency+
  # 3. Accessor ends with +_amount+ → strips suffix and appends +_currency+
  # 4. Otherwise → +<accessor>_currency+
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param column_override [Symbol, String, nil] explicit column name
  # @return [String] the resolved currency column name
  # @api private
  def currency_column_name(accessor, column_override)
    return column_override.to_s if column_override

    name = accessor.to_s
    return 'currency' if name == 'amount'

    radical = name.end_with?('_amount') ? name.sub(/_amount$/, '') : name
    "#{radical}_currency"
  end

  # Parses arguments for a composite (amount + currency) migration.
  #
  # Delegates to {#parse_money_amount_args} and {#parse_currency_args}
  # using the nested +:amount+ and +:currency+ option keys.
  #
  # @param accessor [Symbol, String] the money attribute name
  # @param options [Hash] migration options
  # @option options [Hash] :amount amount column options
  # @option options [Hash] :currency currency column options
  # @return [Array(String, String, Hash, Hash)] amount column name,
  #   currency column name, amount options, currency options
  # @api private
  def parse_money_args(accessor, options = {})
    amount_column, amount_options = parse_money_amount_args(accessor, options[:amount])
    currency_column, currency_options = parse_currency_args(accessor, options[:currency])

    [amount_column, currency_column, amount_options, currency_options]
  end
end