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 = 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 |