Class: YamlExporter::Nodes::ManyThrough
- Defined in:
- lib/yaml_exporter/nodes/many_through.rb
Overview
many :assoc, through: :join, find_by: :column do ... end.
With a block, its attributes (and any positioned_by column) attach to the JOIN row, not the target record. find_by resolves the target globally.
With NO block at all the association is a bare reference list: each
entry is just the target's find_by value (a string), exactly like
ManyReference, but routed through a join model. The join rows are still
created/destroyed by the DSL, and positioned_by: — when given — derives
the join's position column from the YAML order. An empty block is NOT
the reference flavor: passing a block (empty or not) opts into the
hash-shaped entries, same as the other many flavors.
Instance Attribute Summary collapse
-
#through ⇒ Object
readonly
Returns the value of attribute through.
Attributes inherited from ManyBase
#find_by, #name, #owner_class, #positioned_by, #sub_structure
Instance Method Summary collapse
-
#current_entries(parent) ⇒ Object
Existing "children" for destroy-missing bookkeeping are the join rows, not the targets: removing a reviewer must drop the join row, never the Reviewer itself.
- #default_export_order(records) ⇒ Object
-
#entry_class ⇒ Object
Block attributes live on the join row.
-
#export(parent, exporter:) ⇒ Object
The collection the Exporter should walk is the join rows, since block attributes belong to those.
- #extra_entry_keys ⇒ Object
-
#extra_entry_schema ⇒ Object
find_by is a column on the target (e.g. Reviewer.slug), not on the join row — so resolve the type against target_class.
- #find_or_build_child(parent, entry, _index, existing:) ⇒ Object
-
#import(parent, data, path:, importer:) ⇒ Object
Bare reference-list import: entries are find_by values, not hashes.
-
#initialize(name:, owner_class:, through:, find_by:, positioned_by: nil, &block) ⇒ ManyThrough
constructor
A new instance of ManyThrough.
- #join_class ⇒ Object
-
#reference_list? ⇒ Boolean
No block was passed → the YAML is a flat list of find_by values rather than a list of hashes.
-
#schema_fragment ⇒ Object
A bare reference list is a flat array of find_by values; the block-driven object-array schema from ManyBase doesn't apply.
Methods inherited from ManyBase
#build_child, #destroy_missing, #phase, #sort_for_export, #target_class, #yaml_keys
Constructor Details
#initialize(name:, owner_class:, through:, find_by:, positioned_by: nil, &block) ⇒ ManyThrough
Returns a new instance of ManyThrough.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 21 def initialize(name:, owner_class:, through:, find_by:, positioned_by: nil, &block) # Whether a block was passed (even an empty one) decides the YAML # shape: block → hash entries, no block → bare reference list. @reference_list = block.nil? block ||= proc {} @through = through.to_sym join_reflection = owner_class.reflect_on_association(@through) unless join_reflection raise ArgumentError, "`many #{name.inspect}`: #{owner_class} has no join association `#{through}`" end super(name: name, owner_class: owner_class, find_by: find_by, positioned_by: positioned_by, &block) end |
Instance Attribute Details
#through ⇒ Object (readonly)
Returns the value of attribute through.
19 20 21 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 19 def through @through end |
Instance Method Details
#current_entries(parent) ⇒ Object
Existing "children" for destroy-missing bookkeeping are the join rows, not the targets: removing a reviewer must drop the join row, never the Reviewer itself. Order follows the through association's default scope (matching is by source-association object, not index, so order doesn't affect correctness here).
59 60 61 62 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 59 def current_entries(parent) parent.association(@through).reset parent.public_send(@through).to_a end |
#default_export_order(records) ⇒ Object
87 88 89 90 91 92 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 87 def default_export_order(records) records.sort_by do |join| target = join.public_send(source_association_name) target.public_send(@find_by).to_s end end |
#entry_class ⇒ Object
Block attributes live on the join row.
50 51 52 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 50 def entry_class join_class end |
#export(parent, exporter:) ⇒ Object
The collection the Exporter should walk is the join rows, since block attributes belong to those.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 96 def export(parent, exporter:) records = sort_for_export(Array(parent.public_send(@through))) if reference_list? keys = records.map { |join| join.public_send(source_association_name).public_send(@find_by) } return [@name.to_s, keys] end list = records.map do |join| target = join.public_send(source_association_name) hash = { @find_by.to_s => target.public_send(@find_by) } hash.merge!(exporter.build_hash(join, @sub_structure)) hash end [@name.to_s, list] end |
#extra_entry_keys ⇒ Object
146 147 148 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 146 def extra_entry_keys [@find_by.to_s] end |
#extra_entry_schema ⇒ Object
find_by is a column on the target (e.g. Reviewer.slug), not on the join row — so resolve the type against target_class.
152 153 154 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 152 def extra_entry_schema { @find_by => { type: TypeInference.schema_type_for(target_class, @find_by) } } end |
#find_or_build_child(parent, entry, _index, existing:) ⇒ Object
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 64 def find_or_build_child(parent, entry, _index, existing:) key = entry[@find_by.to_s] target = target_class.find_by(@find_by => key) unless target raise ActiveRecord::RecordNotFound, "no #{target_class} with #{@find_by}=#{key.inspect}" end # Match existing join rows by comparing the source association # object (AR `==` compares primary keys internally, so this works # for surrogate ids and composite PKs alike — no `target.id` needed). assoc = source_association_name match = existing.find { |join| join.public_send(assoc) == target } return match if match # `parent.<through>.build` sets the through-side FK for us (respects # `association_primary_key`); then we assign the target via its own # association setter rather than poking `*_id` columns. parent.public_send(@through).build.tap do |join| join.public_send("#{assoc}=", target) end end |
#import(parent, data, path:, importer:) ⇒ Object
Bare reference-list import: entries are find_by values, not hashes. The block-driven flow in ManyBase#import only applies when there are block attributes to write onto the join row.
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 115 def import(parent, data, path:, importer:) return super unless reference_list? raw = data.key?(@name.to_s) ? data[@name.to_s] : nil values = raw.nil? ? [] : raw unless values.is_a?(Array) raise UnknownAttributeError, "#{list_path(path)}: expected a list, got #{values.class}" end unless values.all? { |v| v.is_a?(String) || v.is_a?(Symbol) || v.is_a?(Numeric) } raise UnknownAttributeError, "#{list_path(path)}: expected a list of #{@find_by} values, got #{raw.inspect}" end parent.association(@through).reset existing = current_entries(parent) kept = Set.new.compare_by_identity values.each_with_index do |value, index| join = find_or_build_child(parent, { @find_by.to_s => value }, index, existing: existing) # positioned_by is DSL-owned: derived from the 1-based array index, # mirroring ManyBase#import. join.public_send("#{@positioned_by}=", index + 1) if @positioned_by join.save! kept << join end destroy_missing(existing, kept) end |
#join_class ⇒ Object
45 46 47 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 45 def join_class @join_class ||= @owner_class.reflect_on_association(@through).klass end |
#reference_list? ⇒ Boolean
No block was passed → the YAML is a flat list of find_by values rather than a list of hashes. Mirrors ManyReference, but join rows (and their positioned_by column) are still managed by the DSL.
41 42 43 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 41 def reference_list? @reference_list end |
#schema_fragment ⇒ Object
A bare reference list is a flat array of find_by values; the block-driven object-array schema from ManyBase doesn't apply.
158 159 160 161 162 163 |
# File 'lib/yaml_exporter/nodes/many_through.rb', line 158 def schema_fragment return super unless reference_list? item_type = TypeInference.schema_type_for(target_class, @find_by) { @name => { type: 'array', items: { type: item_type } } } end |