Class: YamlExporter::Nodes::ManyBase
- Inherits:
-
Object
- Object
- YamlExporter::Nodes::ManyBase
- Defined in:
- lib/yaml_exporter/nodes/many_base.rb
Overview
Shared base for many flavors that take a block:
- ManyPositional (no find_by, no through)
- ManyFindBy (find_by, no through)
- ManyThrough (through + find_by)
Holds the block's sub-structure and the common accessors. Subclasses override how each YAML entry maps to an existing-or-new child record. ManyReference is NOT a subclass — it has no block and different semantics.
Target-class resolution is deferred: we store the owner class and the
association name, and resolve the reflected class on demand. Eager
resolution breaks anonymous ActiveRecord classes whose inverse
associations (often referenced via string class_name:) can't be
resolved at declaration time.
Direct Known Subclasses
Instance Attribute Summary collapse
-
#find_by ⇒ Object
readonly
Returns the value of attribute find_by.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#owner_class ⇒ Object
readonly
Returns the value of attribute owner_class.
-
#positioned_by ⇒ Object
readonly
Returns the value of attribute positioned_by.
-
#sub_structure ⇒ Object
readonly
Returns the value of attribute sub_structure.
Instance Method Summary collapse
-
#build_child(parent) ⇒ Object
Build a new child instance bound to
parent. -
#current_entries(parent) ⇒ Object
Return the child records currently associated with
parent. -
#default_export_order(records) ⇒ Object
Subclasses override to express their "natural" order (find_by for reference-style flavors).
-
#destroy_missing(existing, kept) ⇒ Object
Destroy children from
existingthat weren't touched this round. -
#entry_class ⇒ Object
Class against which the block is evaluated.
- #export(parent, exporter:) ⇒ Object
-
#extra_entry_keys ⇒ Object
Additional entry-level keys the subclass considers legal but that aren't declared in the block (e.g. the
find_bydiscriminator). -
#extra_entry_schema ⇒ Object
Schema fragment for those extra entry keys.
-
#find_or_build_child(_parent, _entry, _index, existing:) ⇒ Object
Child lookup/construction for a single YAML entry.
- #import(parent, data, path:, importer:) ⇒ Object
-
#initialize(name:, owner_class:, find_by: nil, positioned_by: nil, &block) ⇒ ManyBase
constructor
A new instance of ManyBase.
- #phase ⇒ Object
- #schema_fragment ⇒ Object
-
#sort_for_export(records) ⇒ Object
Used by Exporter to produce a stable list order.
- #target_class ⇒ Object
- #yaml_keys ⇒ Object
Constructor Details
#initialize(name:, owner_class:, find_by: nil, positioned_by: nil, &block) ⇒ ManyBase
Returns a new instance of ManyBase.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 23 def initialize(name:, owner_class:, find_by: nil, positioned_by: nil, &block) raise ArgumentError, "`many #{name.inspect}`: block required" unless block @name = name.to_sym @owner_class = owner_class @find_by = find_by&.to_sym @positioned_by = positioned_by&.to_sym reflection = owner_class.reflect_on_association(@name) unless reflection raise ArgumentError, "`many #{name.inspect}`: #{owner_class} has no association `#{name}`" end # Pass a lazy class resolver into the inner Builder: attribute-only # blocks never trigger it. @sub_structure = Builder.new(-> { entry_class }, &block).build validate_positioned_by! end |
Instance Attribute Details
#find_by ⇒ Object (readonly)
Returns the value of attribute find_by.
21 22 23 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 21 def find_by @find_by end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
21 22 23 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 21 def name @name end |
#owner_class ⇒ Object (readonly)
Returns the value of attribute owner_class.
21 22 23 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 21 def owner_class @owner_class end |
#positioned_by ⇒ Object (readonly)
Returns the value of attribute positioned_by.
21 22 23 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 21 def positioned_by @positioned_by end |
#sub_structure ⇒ Object (readonly)
Returns the value of attribute sub_structure.
21 22 23 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 21 def sub_structure @sub_structure end |
Instance Method Details
#build_child(parent) ⇒ Object
Build a new child instance bound to parent. We delegate to the
collection's .build, which is the PK-agnostic path: AR fills in the
FK (and any polymorphic *_type column) by consulting the
association's association_primary_key, so composite/custom primary
keys keep working. Subclasses may override for join-table semantics.
131 132 133 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 131 def build_child(parent) parent.public_send(@name).build end |
#current_entries(parent) ⇒ Object
Return the child records currently associated with parent.
Order is whatever the association's default scope produces — if
you care (positional semantics), set -> { order(...) } on the
has_many. ManyFindBy / ManyThrough match by column or
association object, so order doesn't affect their correctness.
122 123 124 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 122 def current_entries(parent) parent.public_send(@name).to_a end |
#default_export_order(records) ⇒ Object
Subclasses override to express their "natural" order (find_by for
reference-style flavors). Default uses the record's primary key so
we don't bake in :id.
158 159 160 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 158 def default_export_order(records) records.sort_by { |r| r.to_key || [] } end |
#destroy_missing(existing, kept) ⇒ Object
Destroy children from existing that weren't touched this round.
kept is a compare_by_identity Set of Ruby objects; no PK columns
are consulted.
138 139 140 141 142 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 138 def destroy_missing(existing, kept) existing.each do |child| child.destroy! unless kept.include?(child) end end |
#entry_class ⇒ Object
Class against which the block is evaluated. For vanilla many ... do end this is the target class. ManyThrough overrides to return the
join class (the block's attributes attach to the join row).
59 60 61 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 59 def entry_class target_class end |
#export(parent, exporter:) ⇒ Object
103 104 105 106 107 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 103 def export(parent, exporter:) records = sort_for_export(Array(parent.public_send(@name))) list = records.map { |child| exporter.build_hash(child, @sub_structure) } [@name.to_s, list] end |
#extra_entry_keys ⇒ Object
Additional entry-level keys the subclass considers legal but that
aren't declared in the block (e.g. the find_by discriminator).
164 165 166 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 164 def extra_entry_keys [] end |
#extra_entry_schema ⇒ Object
Schema fragment for those extra entry keys. Default: infer the type
from the entry_class (the class the block is evaluated against) —
correct for ManyFindBy (entry_class == target_class). ManyThrough
overrides because its extra key lives on target_class, not the join.
TypeInference gracefully falls back to 'string' for unknown columns.
173 174 175 176 177 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 173 def extra_entry_schema extra_entry_keys.each_with_object({}) do |k, acc| acc[k.to_sym] = { type: TypeInference.schema_type_for(entry_class, k) } end end |
#find_or_build_child(_parent, _entry, _index, existing:) ⇒ Object
Child lookup/construction for a single YAML entry. Must return a record (new or existing) that will be passed through Importer#apply. Abstract here; subclasses must implement.
182 183 184 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 182 def find_or_build_child(_parent, _entry, _index, existing:) # rubocop:disable Lint/UnusedMethodArgument raise NotImplementedError, "#{self.class} must implement #find_or_build_child" end |
#import(parent, data, path:, importer:) ⇒ Object
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 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 63 def import(parent, data, path:, importer:) entries = data.key?(@name.to_s) ? data[@name.to_s] : nil entries = [] if entries.nil? unless entries.is_a?(Array) raise UnknownAttributeError, "#{list_path(path)}: expected a list, got #{entries.class}" end parent.association(@name).reset existing = current_entries(parent) # Object-identity set: `find_or_build_child` returns the exact same # Ruby object from `existing` when it matches, so we don't have to # invent a primary-key-based identifier (keeps this working for # composite PKs, custom primary keys, and other non-id models). kept = Set.new.compare_by_identity entries.each_with_index do |entry, index| unless entry.is_a?(Hash) raise UnknownAttributeError, "#{entry_path(path, index)}: expected a mapping, got #{entry.class}" end child = find_or_build_child(parent, entry, index, existing: existing) # positioned_by is DSL-owned: the column value is derived from the # array index (1-based), never read from the YAML entry. We set it # on the child before #apply so the pre-save phase persists it in # the same round-trip as the block's attributes. child.public_send("#{@positioned_by}=", index + 1) if @positioned_by importer.apply(child, @sub_structure, entry, path: entry_path(path, index), extra_keys: extra_entry_keys) kept << child end destroy_missing(existing, kept) end |
#phase ⇒ Object
44 45 46 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 44 def phase :post_save end |
#schema_fragment ⇒ Object
109 110 111 112 113 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 109 def schema_fragment props = @sub_structure.nodes.each_with_object({}) { |n, acc| acc.merge!(n.schema_fragment) } extra_entry_schema.each { |k, v| props[k.to_sym] = v } { @name => { type: 'array', items: { type: 'object', properties: props } } } end |
#sort_for_export(records) ⇒ Object
Used by Exporter to produce a stable list order. positioned_by,
when present, wins over every other ordering rule. The to_key
fallback is PK-agnostic (returns [pk_value] for surrogate-id rows
and the composite array for composite-PK rows); nil (unsaved) folds
to [] and leaves Ruby's stable sort to preserve incoming order.
149 150 151 152 153 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 149 def sort_for_export(records) return records.sort_by { |r| [positioned_value(r), r.to_key || []] } if @positioned_by default_export_order(records) end |
#target_class ⇒ Object
52 53 54 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 52 def target_class @target_class ||= @owner_class.reflect_on_association(@name).klass end |
#yaml_keys ⇒ Object
48 49 50 |
# File 'lib/yaml_exporter/nodes/many_base.rb', line 48 def yaml_keys [@name.to_s] end |