Module: TypedEAV

Extended by:
ActiveSupport::Autoload
Defined in:
lib/typed_eav.rb,
lib/typed_eav/config.rb,
lib/typed_eav/engine.rb,
lib/typed_eav/version.rb,
lib/typed_eav/registry.rb,
lib/typed_eav/bulk_read.rb,
lib/typed_eav/partition.rb,
lib/typed_eav/versioned.rb,
lib/typed_eav/bulk_write.rb,
lib/typed_eav/csv_mapper.rb,
lib/typed_eav/versioning.rb,
lib/typed_eav/scope_tuple.rb,
app/models/typed_eav/value.rb,
lib/typed_eav/entity_query.rb,
lib/typed_eav/filter_query.rb,
app/models/typed_eav/option.rb,
lib/typed_eav/has_typed_eav.rb,
lib/typed_eav/query_builder.rb,
app/models/typed_eav/section.rb,
app/models/typed_eav/field/url.rb,
lib/typed_eav/event_dispatcher.rb,
app/models/typed_eav/field/base.rb,
app/models/typed_eav/field/date.rb,
app/models/typed_eav/field/file.rb,
app/models/typed_eav/field/json.rb,
app/models/typed_eav/field/text.rb,
app/models/typed_eav/field/color.rb,
app/models/typed_eav/field/email.rb,
app/models/typed_eav/field/image.rb,
lib/typed_eav/schema_portability.rb,
app/models/typed_eav/field/select.rb,
lib/typed_eav/field/typed_storage.rb,
app/models/typed_eav/field/boolean.rb,
app/models/typed_eav/field/decimal.rb,
app/models/typed_eav/field/integer.rb,
app/models/typed_eav/value_version.rb,
app/models/typed_eav/field/currency.rb,
lib/typed_eav/versioning/subscriber.rb,
app/models/typed_eav/field/date_time.rb,
app/models/typed_eav/field/long_text.rb,
app/models/typed_eav/field/reference.rb,
app/models/typed_eav/field/date_array.rb,
app/models/typed_eav/field/optionable.rb,
app/models/typed_eav/field/percentage.rb,
app/models/typed_eav/field/text_array.rb,
app/models/typed_eav/application_record.rb,
app/models/typed_eav/field/multi_select.rb,
app/models/typed_eav/field/decimal_array.rb,
app/models/typed_eav/field/integer_array.rb,
app/models/typed_eav/field/range_bounded.rb,
app/models/typed_eav/field/validated_string.rb,
lib/typed_eav/has_typed_eav/instance_methods.rb,
lib/generators/typed_eav/install/install_generator.rb,
lib/generators/typed_eav/scaffold/scaffold_generator.rb

Defined Under Namespace

Modules: BulkWrite, CSVMapper, EntityQuery, EventDispatcher, Field, Generators, HasTypedEAV, Partition, SchemaPortability, ScopeTuple, Versioned, Versioning Classes: ApplicationRecord, BulkRead, Config, Engine, FilterQuery, Option, QueryBuilder, Registry, ScopeRequired, Section, Value, ValueVersion

Constant Summary collapse

VERSION =
"0.3.2"

Class Method Summary collapse

Class Method Details

.config {|Config| ... } ⇒ Object Also known as: configure

Yields:



50
51
52
53
# File 'lib/typed_eav.rb', line 50

def config
  yield Config if block_given?
  Config
end

.current_contextObject

Returns the current thread’s top-of-stack context Hash, or a shared frozen empty Hash when no ‘with_context` block is active. The return value is ALWAYS frozen — callers can rely on read-only semantics regardless of whether a block is active. NEVER returns nil.



192
193
194
# File 'lib/typed_eav.rb', line 192

def current_context
  Thread.current[THREAD_CONTEXT_STACK]&.last || EMPTY_FROZEN_CONTEXT
end

.current_scopeObject

Current ambient scope tuple. Resolution order:

1. Inside `unscoped { }`      → nil (hard bypass)
2. Innermost `with_scope(v)`  → tuple stored on the stack
3. Configured `scope_resolver` callable
4. nil

## Return-value contract (Phase 1, breaking change from v0.1.x)

Returns either ‘nil` (no ambient scope) or a 2-element Array `[scope, parent_scope]` where each element is a String or nil. Never returns a bare scalar.

## scope_resolver contract (strict)

The resolver lambda configured via ‘Config.scope_resolver = ->{ … }` MUST return either `nil` or a 2-element Array. Both elements may be nil. Any other shape — most importantly a bare scalar (the v0.1.x shape) — raises `ArgumentError` directly inside `current_scope`, BEFORE any normalization is applied. We deliberately do NOT auto-coerce a bare-scalar return into `[scalar, nil]`; the BC-shim path was rejected during Phase 1 design (see `.vbw-planning/phases/01-*/01-CONTEXT.md` § “Deferred Ideas”). The strict raise is the chokepoint that makes the breaking change visible — silent coercion here would hide a contract violation in user-supplied resolver code.

‘parent_scope` non-nil + `scope` nil (orphan parent) is invalid; the check belongs to model-level validators added by plans 03/04, NOT to this resolver layer. The resolver is a contract surface, not a validation surface.

‘with_scope(scalar)` block API remains BC-permissive and is a DIFFERENT surface from the resolver-callable contract — see `with_scope` doc and `normalize_scope` doc.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/typed_eav.rb', line 92

def current_scope
  return nil if Thread.current[THREAD_UNSCOPED]

  stack = Thread.current[THREAD_SCOPE_STACK]
  # The stack stores tuples already (with_scope normalized on push), so
  # reads bypass coercion entirely — no risk of double-normalization.
  return stack.last if stack.present?

  # Resolver-callable strict-contract path. We route through
  # `ScopeTuple.normalize_strict`, which raises `ArgumentError` on any
  # shape other than `nil` or a 2-element Array — most importantly on
  # the v0.1.x bare-scalar shape. The strict raise is the chokepoint
  # that makes a misshaped resolver fail loudly; the BC-shim
  # (silent coercion) alternative was rejected during Phase 1 design.
  ScopeTuple.normalize_strict(Config.scope_resolver&.call)
end

.normalize_scope(value) ⇒ Object

BC-permissive normalizer for ‘with_scope` block input and explicit tuple inputs. 1-line alias to `ScopeTuple.normalize_permissive` —the implementation moved during the 0.3.0 ScopeTuple extraction (issue #10), but the public method/return-shape stays as a v0.2.x BC surface. Always returns either `nil` or a 2-element tuple `[scope, parent_scope]` where each element is a `String` or `nil`.

## NOT a contract chokepoint for resolver returns

‘current_scope` deliberately does NOT route a custom-resolver return through this helper — it routes through `ScopeTuple.normalize_strict` instead — because the bare-scalar passthrough here would silently coerce a contract violation. This split (strict on the resolver- callable surface, permissive on the with_scope block surface) is the Phase-1 asymmetric contract.



211
# File 'lib/typed_eav.rb', line 211

def normalize_scope(value) = ScopeTuple.normalize_permissive(value)

.registryObject



57
# File 'lib/typed_eav.rb', line 57

def registry = Registry

.unscopedObject

Run the block with scope enforcement disabled. Queries return results across all scopes. Use for admin tools, migrations, and tests.



141
142
143
144
145
146
147
# File 'lib/typed_eav.rb', line 141

def unscoped
  prev = Thread.current[THREAD_UNSCOPED]
  Thread.current[THREAD_UNSCOPED] = true
  yield
ensure
  Thread.current[THREAD_UNSCOPED] = prev
end

.unscoped?Boolean

True when inside an ‘unscoped { }` block.

Returns:

  • (Boolean)


150
151
152
# File 'lib/typed_eav.rb', line 150

def unscoped?
  !!Thread.current[THREAD_UNSCOPED]
end

.with_context(**kwargs) ⇒ Object

Run the block with ‘kwargs` merged into the ambient event context, restoring the prior stack on exit (exception-safe). Nests cleanly with shallow per-key merge — outer keys remain visible inside nested blocks unless overridden by name; deep-merge of nested Hash values is NOT promised.

The pre-merged hash is FROZEN before being pushed so callbacks invoked downstream (‘Config.on_value_change` user proc, internal subscribers) cannot mutate context for the current or outer blocks. Without freeze, a callback that did `ctx = true` would corrupt the stack for every wrapping block on the same thread.

## Why **kwargs and not positional Hash

‘def with_context(**kwargs)` enforces the keyword-syntax call form. Per Ruby 3.0+ kwargs/Hash separation, `TypedEAV.with_context({ foo: 1 })` raises ArgumentError (“wrong number of arguments”) — the only accepted form is `TypedEAV.with_context(foo: 1)`. Without **kwargs, callers could push arbitrary Hash shapes (including nested Arrays or non-symbol keys) that wouldn’t merge cleanly across nesting and wouldn’t match the documented context shape that hooks read.

See ‘with_scope` (above) for the parallel ensure-pop pattern. Mirrors `with_scope`’s shape exactly except for: (a) **kwargs vs positional value, (b) merge-into-outer-on-push vs replace-on-push.



179
180
181
182
183
184
185
186
# File 'lib/typed_eav.rb', line 179

def with_context(**kwargs)
  stack  = (Thread.current[THREAD_CONTEXT_STACK] ||= [])
  merged = (stack.last || EMPTY_FROZEN_CONTEXT).merge(kwargs).freeze
  stack.push(merged)
  yield
ensure
  stack&.pop
end

.with_scope(value) ⇒ Object

Run the block with ‘value` as the ambient scope, restoring the prior stack on exit (exception-safe). Nests cleanly.

## Accepted input shapes (BC-permissive — public block API)

  • ‘with_scope(“t1”)` — single-arg BC: pushes `[“t1”, nil]`.

  • ‘with_scope(ar_record)` — pushes `[ar_record.id.to_s, nil]`.

  • ‘with_scope([“t1”, “ps1”])` — Phase 1 tuple form: pushes the tuple.

  • ‘with_scope(nil)` — pushes nil (sentinel: no scope).

The single-arg signature ‘with_scope(value)` keeps its v0.1.x meaning: `scope = value`, `parent_scope = nil`. Apps that have only ever passed a scalar do not need to update on upgrade.

The internal stack stores normalized tuples (or nil), NOT raw values, so ‘current_scope` can return `stack.last` directly without further coercion.

NOTE: this is the BC-permissive surface. The strict-contract surface is ‘Config.scope_resolver` — see `current_scope` doc. Two surfaces, two contracts: `with_scope`’s scalar-OK behavior is BC-preserving; the resolver-callable contract rejects bare scalars.



131
132
133
134
135
136
137
# File 'lib/typed_eav.rb', line 131

def with_scope(value)
  stack = (Thread.current[THREAD_SCOPE_STACK] ||= [])
  stack.push(ScopeTuple.normalize_permissive(value))
  yield
ensure
  stack&.pop
end