Module: DenormalizeFields

Defined in:
lib/denormalize_fields.rb,
lib/denormalize_fields/version.rb,
lib/denormalize_fields/association_extension.rb

Defined Under Namespace

Modules: AssociationExtension

Constant Summary collapse

CONDITIONAL_CLASSES =
[NilClass, TrueClass, FalseClass, Symbol, Proc]
VERSION =
'1.2.0'

Class Method Summary collapse

Class Method Details

.apply(changeset, to:, owner:, mapping:) ⇒ Object

Note: missing related records are ignored, and new related records are not persisted. Extra options to raise/create/persist in this case might be nice.

Raises:

  • (ActiveRecord::RecordInvalid)


90
91
92
93
94
95
96
97
98
# File 'lib/denormalize_fields.rb', line 90

def apply(changeset, to:, owner:, mapping:)
  return if to.nil?

  to.assign_attributes(changeset)
  return if to.new_record? ? to.valid? : to.save

  DenormalizeFields.copy_errors(to.errors, to_record: owner, mapping: mapping)
  raise(ActiveRecord::RecordInvalid, to)
end

.call(record:, relation_name:, mapping:, **options) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/denormalize_fields.rb', line 46

def call(record:, relation_name:, mapping:, **options)
  return unless conditional_passes?(options[:if],     record, false)
  return unless conditional_passes?(options[:unless], record, true)

  changeset = DenormalizeFields.changeset(record: record, mapping: mapping)
  return if changeset.empty?

  Array(record.send(relation_name)).each do |related_record|
    DenormalizeFields.apply(
      changeset, to: related_record, owner: record, mapping: mapping
    )
  end
end

.cast_to_mapping(fields, prefix: nil) ⇒ Object



37
38
39
40
41
42
43
44
# File 'lib/denormalize_fields.rb', line 37

def cast_to_mapping(fields, prefix: nil)
  if fields.is_a?(Hash)
    prefix && raise(ArgumentError, 'pass EITHER a fields Hash OR a prefix')
    fields
  else
    Array(fields).map { |e| [e.to_sym, [prefix, e].join.to_sym] }.to_h
  end
end

.changeset(record:, mapping:) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/denormalize_fields.rb', line 75

def changeset(record:, mapping:)
  mapping.each.with_object({}) do |(source, dest), hash|
    if source.is_a?(Array)
      if source.any? { |field| record.saved_change_to_attribute?(field) }
        current_values = record.attributes.values_at(*source.map(&:to_s))
        hash[dest] = current_values.join(' ')
      end
    elsif change = record.saved_change_to_attribute(source)
      hash[dest] = change.last
    end
  end
end

.conditional_passes?(conditional, record, inverted) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/denormalize_fields.rb', line 60

def conditional_passes?(conditional, record, inverted)
  return true if conditional.nil?

  result =
    if conditional.respond_to?(:call)
      record.instance_exec(&conditional)
    elsif conditional.class == Symbol
      record.send(conditional)
    else # true, false
      conditional
    end

  inverted ? !result : !!result
end

.copy_errors(errors, to_record:, mapping:) ⇒ Object

TODO: use Errors#import when it becomes available in rails 6.1 or 6.2



101
102
103
104
105
106
107
108
# File 'lib/denormalize_fields.rb', line 101

def copy_errors(errors, to_record:, mapping:)
  errors.details.each do |key, array|
    field = mapping.rassoc(key.to_sym).first
    array.each do |details|
      to_record.errors.add(field, details[:error], **details.except(:error))
    end
  end
end

.denormalize(fields:, from:, onto:, prefix: nil, **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
# File 'lib/denormalize_fields.rb', line 7

def denormalize(fields:, from:, onto:, prefix: nil, **options)
  mapping = cast_to_mapping(fields, prefix: prefix)
  validate_options(**options)

  from.after_save do
    DenormalizeFields.call(
      record:        self,
      relation_name: onto,
      mapping:       mapping,
      **options,
    )
  end
end

.validate_conditional(arg) ⇒ Object



30
31
32
33
34
35
# File 'lib/denormalize_fields.rb', line 30

def validate_conditional(arg)
  CONDITIONAL_CLASSES.include?(arg.class) || raise(
    ArgumentError,
    "`if:` option must be a #{CONDITIONAL_CLASSES.join('/')}, got: #{arg.class}"
  )
end

.validate_options(**options) ⇒ Object



21
22
23
24
25
26
# File 'lib/denormalize_fields.rb', line 21

def validate_options(**options)
  validate_conditional(options[:if])
  validate_conditional(options[:unless])
  unsupported = (options.keys - %i[if unless]).empty? ||
    raise(ArgumentError, "unsupported denormalize options: #{unsupported}")
end