Module: TypedEAV::CSVMapper
- Defined in:
- lib/typed_eav/csv_mapper.rb
Overview
Pure stateless CSV-to-attributes transform.
TypedEAV::CSVMapper.row_to_attributes(row, mapping, fields_by_name: nil)
turns a single CSV row (CSV::Row for header-mapped files, or a plain
Array for index-mapped headerless files) into a Result value object
with .attributes, .errors, and .success? / .failure? predicates.
Never raises on per-row content errors — cast failures land in errors,
NOT in exceptions. The only ArgumentError path is mapping-shape
validation, which fires before any row processing.
Operating modes
The 2-arg public form row_to_attributes(row, mapping) is the
passthrough mode: raw cell values flow through unchanged keyed by
the mapped field name. No coercion is attempted, no errors are possible.
This honors the public 2-arg surface in CONTEXT line 13 + ROADMAP §Phase
6 success criterion exactly. Use this when the caller only needs CSV
mapping (header → field-name) without typed coercion — e.g., when
building a preview before the host record's partition is known.
The 3-arg form row_to_attributes(row, mapping, fields_by_name: defs_by_name) is the typed mode: per-cell coercion runs through
field.cast(raw) (the existing tuple contract documented on
TypedEAV::Field::Base#cast). Cast failures (invalid? == true) land
in Result#errors keyed by the field name, with the AR-symmetric
message "is invalid". Empty cells (nil / empty string) cast to nil
per the field.cast contract and produce attributes[name] = nil with
NO error. The caller is expected to pass the result of
record.class.typed_eav_definitions(scope:, parent_scope:).index_by(&:name)
(or equivalent) — the mapper has no record context and does not resolve
fields itself.
Mapping shape
Single Hash. Keys are EITHER all String (CSV header names) OR all
Integer (column indexes for headerless files). Mixed-key mappings
raise ArgumentError immediately, before any row is touched, with a
remediation message that tells the caller how to fix it.
Mapping VALUES are field names — accepted as Symbol or String; the
mapper coerces to String before lookup in fields_by_name. This
matches the codebase convention where field.name is always a String.
Unknown field in mapping (typed mode)
When a mapping value (e.g. :foo) does NOT appear in fields_by_name,
the cell is silently SKIPPED — it does NOT produce an error and does
NOT appear in Result#attributes. Rationale: the mapper is a pure
transform and has no record context. Mapping misconfiguration is a
caller concern; callers that want to detect it can compare
result.attributes.keys against the expected set. In passthrough mode
there is no fields_by_name to look up against, so every mapped cell
flows through unconditionally.
Foundational principle
NO HARDCODED ATTRIBUTE REFERENCES. The mapper resolves field metadata
via the fields_by_name: keyword argument supplied by the caller —
the mapper itself never inspects record attributes or partition state.
Every field touch goes through field.cast(raw) which dispatches via
the existing per-type cast contract.
Defined Under Namespace
Classes: Result
Class Method Summary collapse
-
.row_to_attributes(row, mapping, fields_by_name: nil) ⇒ Object
Transform a single row into a
Result.
Class Method Details
.row_to_attributes(row, mapping, fields_by_name: nil) ⇒ Object
Transform a single row into a Result. See module-level docs for
the full contract. Returns a Result; only raises on mapping-shape
errors (mixed String + Integer keys).
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 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/typed_eav/csv_mapper.rb', line 95 def row_to_attributes(row, mapping, fields_by_name: nil) validate_mapping_keys!(mapping) attributes = {} errors = {} mapping.each do |source_key, raw_field_name| # Unified cell read: both `CSV::Row#[String]` and `Array#[Integer]` # work via `[]` — homogeneous key validation above ensures # `source_key` matches the row representation (header name vs # index). raw_cell = row[source_key] # Codebase convention: field names are always Strings on the AR # side. Mapping values may be Symbol or String — coerce here so # the lookup against `fields_by_name` and the keys in # `attributes` / `errors` are consistent regardless of caller # input style. name = raw_field_name.to_s if fields_by_name.nil? # Passthrough mode — no coercion, no errors possible. Honors # the 2-arg public surface in CONTEXT line 13 + ROADMAP §Phase # 6. Cell flows through unchanged. attributes[name] = raw_cell else # Typed mode — silently skip unknown fields (see module docs). field = fields_by_name[name] next if field.nil? casted, invalid = field.cast(raw_cell) if invalid # AR-symmetric message; matches `errors_by_record` in the # bulk-write surface and `errors.add(:value, :invalid)` in # `Value#validate_value`. Plain Hash with String keys per # RESEARCH §Open-Question Resolutions §errors_hash shape. (errors[name] ||= []) << "is invalid" else attributes[name] = casted end end end Result.new(attributes: attributes, errors: errors) end |