typed_eav 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d06c0178a808cf7b67cf2b6970654fb922b02f9fa7a808eba6595f0aa3f1a38f
4
- data.tar.gz: 456c0c15222d5ede4e12778ba7c95d0977cc726ecd0506141180badceb7c395b
3
+ metadata.gz: 66997bbaca41b0302fe7a86baf9b22233f2ade442c2255c3323a900245bd78fb
4
+ data.tar.gz: 90498aa57c720504bc09a34e62fab81d0377baaeab3ea2d0598317d6d5b38a93
5
5
  SHA512:
6
- metadata.gz: 720454235c352fe0151eb533c53d172549e35e3920ae9ff730b66d8781ae3bf7cf1763afdd445cb0f36f559a21758451d336c640230c5741d58a055fd3c8c7db
7
- data.tar.gz: 91548d07b441db3f7145b2645264161a0bd0294f5bc466c045f41fef98ea8f6dd240548beb4657c59343ca337b5cb7ac7faef5e6749211634fb812034e125567
6
+ metadata.gz: a67ca56147f16ca0b6ffa02121f5783dc10f588ae36bc4d6b9df73c5de19bfac22be3b76613c693719156d15b8442723de4fd05479f72dc8074ad5b272c2599e
7
+ data.tar.gz: f4802412a11f7d5010487c2bede22037a6302653269eaf2dd4a933f98612296d38b2a6ce4bdd6244db1286c6572ac706615e9723ed73a2bc0ac582289af7ebd8
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.1] - 2026-05-25
9
+
10
+ Documentation-only release. No code or behavior changes.
11
+
12
+ ### Added
13
+
14
+ - README §"Architecture" — full overview of the post-0.3.0 internal
15
+ module layout: macro entry (`HasTypedEav`), the two-altitude query
16
+ pattern (`EntityQuery` → `FilterQuery` → `QueryBuilder`), `BulkRead`
17
+ and `BulkWrite` siblings, `InstanceMethods`, `Field::TypedStorage`
18
+ concern, family intermediate bases (`ValidatedString`, `RangeBounded`,
19
+ `Optionable`), `ScopeTuple`, `Partition`, `EventDispatcher`, and the
20
+ Phase-6 modules (`SchemaPortability`, `CSVMapper`). Anchored to ADRs
21
+ 0001–0005 throughout.
22
+
23
+ ### Removed
24
+
25
+ - `TEST_PLAN.md` — pre-0.3.0 test-sweep planning artifact (2026-04-08).
26
+ Described specs for modules deleted in #9. Git history preserves it.
27
+ - `typed_eav-enhancement-plan.md` — pre-0.3.0 phased roadmap. References
28
+ v0.1.0 line numbers and Phase-1 work that has since shipped. Git
29
+ history preserves it.
30
+
8
31
  ## [0.3.0] - 2026-05-25
9
32
 
10
33
  Pre-1.0 architecture cleanup arc (issues #9–#13). No public-API breakage
@@ -205,6 +228,7 @@ worked examples.
205
228
 
206
229
  Initial release.
207
230
 
231
+ [0.3.1]: https://github.com/dchuk/typed_eav/releases/tag/v0.3.1
208
232
  [0.3.0]: https://github.com/dchuk/typed_eav/releases/tag/v0.3.0
209
233
  [0.2.1]: https://github.com/dchuk/typed_eav/releases/tag/v0.2.1
210
234
  [0.2.0]: https://github.com/dchuk/typed_eav/releases/tag/v0.2.0
data/README.md CHANGED
@@ -1239,6 +1239,128 @@ The gem creates four tables:
1239
1239
  - `typed_eav_options` - allowed values for select/multi-select fields
1240
1240
  - `typed_eav_sections` - optional UI grouping
1241
1241
 
1242
+ ## Architecture
1243
+
1244
+ Internal module layout as of 0.3.0. Most consumers never reach for these directly — the public surface is the `has_typed_eav` macro and the instance/class methods it installs — but the split matters if you're extending the gem, debugging an integration, or evaluating it for production. Decisions are anchored by [ADR-0001](docs/adr/0001-collapse-column-mapping-stack.md) through [ADR-0005](docs/adr/0005-keep-phase-six-modules-independent.md).
1245
+
1246
+ ### Macro entry: `HasTypedEav`
1247
+
1248
+ `lib/typed_eav/has_typed_eav.rb` (~120 LOC) is the macro shell. When you call `has_typed_eav` on an AR model, it:
1249
+
1250
+ 1. `extend`s `TypedEAV::EntityQuery` onto the class (class-level query methods).
1251
+ 2. `include`s `TypedEAV::HasTypedEav::InstanceMethods` (per-record accessors).
1252
+ 3. Wires scope/parent-scope kwargs into the model's class-level configuration.
1253
+ 4. Registers the model with `TypedEAV::Registry`.
1254
+
1255
+ The macro is intentionally thin. All real behavior lives in the modules it pulls in.
1256
+
1257
+ ### Class-level reads: two-altitude query pattern
1258
+
1259
+ ```
1260
+ Contact.where_typed_eav(...) ← public class method
1261
+
1262
+
1263
+ TypedEAV::EntityQuery ← high altitude: orchestrator
1264
+ • resolves scope/parent_scope from ambient context or explicit kwargs
1265
+ • owns the UNSET_SCOPE / ALL_SCOPES sentinels
1266
+ • delegates to FilterQuery
1267
+
1268
+
1269
+ TypedEAV::FilterQuery ← multi-filter composition
1270
+ • normalizes filter input shapes (positional, hash, hash-of-hashes)
1271
+ • looks up field definitions via TypedEAV::Partition
1272
+ • per filter, asks QueryBuilder for the SQL fragment
1273
+ • unions/intersects per-field entity-id sets
1274
+ • returns an ActiveRecord::Relation scoped to the host model
1275
+
1276
+
1277
+ TypedEAV::QueryBuilder ← low altitude: per-field SQL primitive
1278
+ • turns a single (field, op, value) into a WHERE clause against typed_eav_values
1279
+ • knows about typed-column projections (integer_value, string_value, etc.)
1280
+ • knows about operator-specific column choice (currency-cents vs currency-code)
1281
+ ```
1282
+
1283
+ `QueryBuilder` is the single place that decides "given this field and this operator, which column and which SQL fragment?" `FilterQuery` never builds SQL fragments directly; `EntityQuery` never touches columns. Splitting the two altitudes keeps custom field types extending only the column-mapping surface (`value_column`, `operators`, `operator_column`) without ever subclassing `FilterQuery`.
1284
+
1285
+ ### Bulk reads: `BulkRead`
1286
+
1287
+ `typed_eav_hash_for(records)` (the plural read) routes through `TypedEAV::BulkRead`. Given a record collection and an effective `(scope, parent_scope)`, it:
1288
+
1289
+ 1. Groups records by partition tuple via `TypedEAV::Partition.definitions_by_name`.
1290
+ 2. Issues one batched `WHERE entity_id IN (...) AND field_id IN (...)` query per partition.
1291
+ 3. Returns a `{record_id => {field_name => value}}` map.
1292
+
1293
+ Single-record reads (`typed_eav_value`, `typed_eav_hash`) live on `InstanceMethods` and use the same partition helpers but without batching.
1294
+
1295
+ ### Bulk writes: `BulkWrite`
1296
+
1297
+ `bulk_set_typed_eav_values(records, attrs)` routes through `TypedEAV::BulkWrite`, an executor that:
1298
+
1299
+ 1. Memoizes field definitions for the call via `Thread.current[:typed_eav_bulk_defs_memo]`.
1300
+ 2. Validates each attribute against its field type's cast contract.
1301
+ 3. Upserts in a single SQL round trip per typed column.
1302
+
1303
+ `BulkWrite` and `BulkRead` are siblings — one read path, one write path — but they don't share a base class. Per [ADR-0005](docs/adr/0005-keep-phase-six-modules-independent.md), keeping them independent preserves the option to evolve each on its own schedule.
1304
+
1305
+ ### Per-record reads/writes: `InstanceMethods`
1306
+
1307
+ `lib/typed_eav/has_typed_eav/instance_methods.rb` (~250 LOC) holds the per-record API:
1308
+
1309
+ - `typed_eav_value(name)` / `typed_eav_hash` — reads
1310
+ - `set_typed_eav_value(name, value)` / `typed_eav_attributes=` — writes
1311
+ - `typed_eav_changes` — dirty tracking
1312
+ - `typed_eav_scope` / `typed_eav_parent_scope` — scope resolution per record
1313
+
1314
+ Every method uses `TypedEAV::Partition.definitions_by_name` so the collision-precedence rules for ambient/explicit/parent scopes are computed in one place.
1315
+
1316
+ ### Field types and storage: `Field::TypedStorage`
1317
+
1318
+ `TypedEAV::Field::Base` is the STI parent of every field type. The shared storage surface lives in the `TypedEAV::Field::TypedStorage` concern (`lib/typed_eav/field/typed_storage.rb`, ~200 LOC), auto-included on `Field::Base`. Per [ADR-0001](docs/adr/0001-collapse-column-mapping-stack.md), it provides:
1319
+
1320
+ - **Class DSL**: `value_column`, `value_columns`, `operators`, `operator_column`, `supported_operators` — describe where typed values live and which operators they support.
1321
+ - **Instance override points**: `read_value(record)`, `write_value(record, casted)`, `apply_default(record)` — the three methods a multi-cell field type overrides.
1322
+ - **Concrete snapshot helpers**: `value_changed?`, `before_snapshot`, `after_snapshot` — derived automatically from `value_columns`; not overridable.
1323
+
1324
+ Custom multi-cell field types subclass `Field::Base` directly and override only the three instance methods. See §[Multi-cell field types](#multi-cell-field-types) for `Currency` as the canonical worked example.
1325
+
1326
+ ### Field families: intermediate STI bases
1327
+
1328
+ Per [ADR-0004](docs/adr/0004-field-family-intermediate-bases.md), three intermediate STI parents factor shared validation behavior out of `Field::Base`:
1329
+
1330
+ - **`TypedEAV::Field::ValidatedString`** — parent of `Text`, `Email`, `Url`. Owns string-length and pattern-validation helpers including `max_gte_min_length` (which now covers Email/Url, not just Text).
1331
+ - **`TypedEAV::Field::RangeBounded`** — parent of `Integer`, `Decimal`, `Date`, `DateTime` (and `Percentage < Decimal`). Owns range-validation helpers including `validates :max, comparison: { greater_than_or_equal_to: :min }` (which now covers Date/DateTime, not just Integer/Decimal).
1332
+ - **`TypedEAV::Field::Optionable`** — a Rails concern included by `Select` and `MultiSelect`. Owns the public-facing sorted `allowed_values` reader and the option-inclusion validators.
1333
+
1334
+ `Color`, `Boolean`, `Json`, and the array field types (`TextArray`, `IntegerArray`, `DecimalArray`, `DateArray`) remain direct children of `Field::Base`. See §[Family intermediate bases](#family-intermediate-bases-extension-points) for extension examples.
1335
+
1336
+ ### Scope tuple normalization: `ScopeTuple`
1337
+
1338
+ `TypedEAV::ScopeTuple` (`lib/typed_eav/scope_tuple.rb`, ~120 LOC) is the canonical source of truth for the `(scope, parent_scope)` partition tuple. It provides:
1339
+
1340
+ - `normalize_permissive(scope)` — coerces input to a tuple; tolerates bare scalars (used by `with_scope`, `normalize_scope`, `Field#validate_parent_scope_invariant`).
1341
+ - `normalize_strict(scope)` — same shape, but raises on bare-scalar input (used by `current_scope`; preserves Phase-1's asymmetric contract that `Config.scope_resolver` must return a tuple).
1342
+ - `invariant_satisfied?(scope, parent_scope)` — Boolean check for the orphan-parent invariant (`parent_scope` set without `scope` = invalid).
1343
+
1344
+ Each calling site keeps its own response policy (raise / AR error / silent narrow) using the Boolean return — `ScopeTuple` is a predicate, not an enforcer.
1345
+
1346
+ ### Partition tuple helpers: `Partition`
1347
+
1348
+ `TypedEAV::Partition` (`lib/typed_eav/partition.rb`, ~100 LOC) owns the `(entity_type, scope, parent_scope)` precedence rules:
1349
+
1350
+ - `definitions_by_name(model, scope, parent_scope)` — returns the field-definitions map for a single resolved partition.
1351
+ - `definitions_multimap_by_name(model)` — returns the cross-partition multimap used by `unscoped { }` blocks.
1352
+ - `visible_fields(model, scope, parent_scope)` / `visible_sections(...)` — scope-respecting field/section iteration with the orphan-parent invariant inlined via `ScopeTuple.invariant_satisfied?`.
1353
+
1354
+ The definitions helpers used to live as class methods on `HasTypedEav` before 0.3.0. They moved to `Partition` per [ADR-0002](docs/adr/0002-entity-query-orchestration.md) because they describe the partition domain, not the macro.
1355
+
1356
+ ### Events: `EventDispatcher`
1357
+
1358
+ `TypedEAV::EventDispatcher` (`lib/typed_eav/event_dispatcher.rb`, ~150 LOC) is the broker for `on_value_change` and `on_field_change` callbacks. Per [ADR-0003](docs/adr/0003-keep-event-dispatcher-broker.md), it intentionally stays a broker rather than getting absorbed into either `Value` or `Field` — its multi-publisher / multi-subscriber shape doesn't belong on either model. See §[Event hooks](#event-hooks) for the public callback contract.
1359
+
1360
+ ### Schema portability and CSV: independent modules
1361
+
1362
+ `TypedEAV::SchemaPortability` and `TypedEAV::CSVMapper` (Phase-6 modules) are deliberately decoupled from the core read/write path per [ADR-0005](docs/adr/0005-keep-phase-six-modules-independent.md). They depend on the public `has_typed_eav` macro surface, never on internal modules.
1363
+
1242
1364
  ## License
1243
1365
 
1244
1366
  MIT
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypedEAV
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typed_eav
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - dchuk