u-attributes 3.0.2 → 3.1.0
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 +4 -4
- data/CHANGELOG.md +50 -0
- data/Gemfile +1 -1
- data/README.md +643 -634
- data/gemfiles/rails_8_1.gemfile +4 -2
- data/gemfiles/rails_edge.gemfile +4 -2
- data/lib/micro/attributes/composition.rb +108 -0
- data/lib/micro/attributes/features/accept.rb +1 -1
- data/lib/micro/attributes/features/activemodel_validations.rb +8 -0
- data/lib/micro/attributes/features.rb +68 -1
- data/lib/micro/attributes/macros.rb +187 -5
- data/lib/micro/attributes/version.rb +1 -1
- data/lib/micro/attributes.rb +72 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69f33ab594de2c8739dbdbb3a077af686ae6741d4ae365dbc7f48015e5c1d132
|
|
4
|
+
data.tar.gz: e9ce95bcb7bd7b3834487266d3d409e2fc2151e3b172abb3090aa9c08d111565
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a743e4ce7f1cd0a3bbf23b46f049fd3b5d0c9e080910c3eb551ca9e6072f3af6225a6001401c699b8decf39990929a2dd444c90244e28fd49962093910abf609
|
|
7
|
+
data.tar.gz: 112b55179e5c64226565504607164dd1f590d35e63c3ebd0fc36cd226b70bd372ce6aed5d0dde6944dabfb1f105f0cb4f8ad1617543d3dc89bbfb7a462366018
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
> **Note:** This gem was originally published as `micro-attributes` (`0.1.0`) and renamed to `u-attributes` starting with `0.2.0` on 2019-07-02.
|
|
9
9
|
|
|
10
|
+
## [3.1.0] - 2026-05-25
|
|
11
|
+
### Added
|
|
12
|
+
- **Composition baked into `Micro::Attributes`** (closes [#9](https://github.com/serradura/u-attributes/issues/9)) — every class that includes `Micro::Attributes` (directly or via `Micro::Attributes.with(...)`) now supports:
|
|
13
|
+
- **Block-form `attribute :foo do ... end`** that defines an anonymous nested class inline. The inline child inherits the host's full feature mix (strict, symbol keys, ActiveModel, etc.).
|
|
14
|
+
- **Hash → child-instance coercion** when `accept:` is another `Micro::Attributes` class. Already-built instances pass through unchanged. Nested coercion composes recursively to any depth.
|
|
15
|
+
- **Deep validation bubbling.** Any descendant's `attributes_errors?` (or AM `valid?`) is mirrored up the chain as a `'is invalid'` marker; the leaf retains the original message. For classes with `:activemodel_validations`, a `__validate_nested_entities__` validator is auto-registered so `parent.valid?` reflects deep descendant invalidity. Mixed trees (AM root + accept-only leaf) work via an `attributes_errors?` fallback.
|
|
16
|
+
- **`Micro::Attributes.new(options = {}, &block)`** — `Struct.new`-style class factory. Returns a fresh class that includes `Micro::Attributes.with(...)` with the requested features merged on top of the preset `{ initialize: true, accept: true }`. The block is `class_eval`d so attributes can be declared inline.
|
|
17
|
+
- **Hash-style configuration for `Micro::Attributes.with`** — alongside the positional symbol API, `with` now accepts a self-documenting hash:
|
|
18
|
+
```ruby
|
|
19
|
+
Micro::Attributes.with(
|
|
20
|
+
initialize: true | :strict,
|
|
21
|
+
accept: true | :strict,
|
|
22
|
+
diff: true,
|
|
23
|
+
keys_as: :symbol | :string | :indifferent,
|
|
24
|
+
active_model: :validations
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
Omit a key (or pass `false` / `nil`) to disable the feature. Both APIs can be mixed; the existing positional form (`with(:initialize, :accept)`, `with(initialize: :strict)`) is fully preserved.
|
|
28
|
+
- **`with(...)` class macro** added to every `Micro::Attributes` includer. Sugar for `include ::Micro::Attributes.with(...)`; layer extra features inline (`with :keys_as_symbol`, `with active_model: :validations`, etc.).
|
|
29
|
+
- **Multi-key hash to `Micro::Attributes.with` / `.without`** — `with(initialize: :strict, accept: :strict)` now honors every key. Previously `fetch_key` returned the first matching strict variant and silently dropped the others.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **Heads up — silent behavior shift for downstream consumers:** `Micro::Attributes.with(initialize: :strict, accept: :strict)` and `Micro::Attributes.without(initialize: :strict, accept: :strict)` now resolve to a different feature module than 3.0.x because the multi-key strict hash bug was fixed. Pre-3.1 `with(initialize: :strict, accept: :strict)` returned only `AcceptStrict`; post-3.1 it returns `AcceptStrict_InitializeStrict`. Pre-3.1 `without(initialize: :strict, accept: :strict)` only excluded `AcceptStrict`; post-3.1 it excludes both strict variants. Any code that relied on the silent drop will get a different feature mix on upgrade.
|
|
33
|
+
- **Heads up — `__validate_nested_entities__` is now auto-registered on every `Micro::Attributes` includer that mixes in `:activemodel_validations`** (pre-3.1 the registration existed only inside `Micro::Entity`, which is gone). Any class on `with(:accept, :activemodel_validations)` whose nested attribute targets are also AM-enabled will now have `valid?` recurse into descendants for the first time — descendant invalidity will surface in the parent's `errors` as `'is invalid'`. If your previous "siloed validity" was load-bearing, register your own validator that doesn't recurse, or use `accept:` without AM on the descendant types. **This affects `u-case` downstream**: any `Micro::Case` subclass with `with_activemodel_validation` and a nested `accept: SomeAMValidatedClass` now propagates descendant invalidity into `Failure(:invalid_attributes)` — previously the Case succeeded if `accept:` itself didn't reject.
|
|
34
|
+
- **Heads up — block-form inline children now always include `:initialize` and `:accept`,** even if the host class explicitly chose a minimal feature mix (e.g. `include Micro::Attributes.with(:diff)` only). Pre-3.1 the inline child mirrored the host's feature mix; post-3.1 init+accept are added on top of whatever the host has. This makes `attribute :foo do ... end` uniformly hash-constructible and accept-checking (the only behaviors that make block-form sensible), but it does add an Accept-validation surface that didn't exist before for hosts that opted out of Accept. If a parent class without Accept has a block-form child with `accept: SomeType` declarations, those declarations are now honored — `obj.child.attributes_errors?` will be true on invalid input.
|
|
35
|
+
- **Heads up — `attribute :foo, accept: X do ... end` now raises `ArgumentError`.** Pre-3.1 a block on `attribute` / `attribute!` was silently ignored; the first cut of this PR consumed the block to build an inline class and silently overwrote any explicit `options[:accept]`. The final 3.1.0 behavior raises loudly when both are passed, since "which wins?" is genuinely ambiguous. Pre-existing call sites that passed a block on accident (and saw it ignored) will surface as `ArgumentError: attribute :name: cannot pass both \`accept:\` and a block`. Drop one of the two.
|
|
36
|
+
- **Heads up — Coercion is prepended on every `Micro::Attributes` includer.** Any user who overrode `__attribute_assign` (the instance method, private internal API) on their host class would have been silently intercepted by Coercion's prepended override. To make the override less inviting and avoid even a theoretical collision, the instance-level per-attribute hook has been renamed from `__attribute_assign` (double underscore) to `___attribute_assign` (triple underscore). The class-method macro of the prior name in `Macros` keeps its name and signature (`__attribute_assign(key, can_overwrite, opt)` — different concern, no MRO collision; preserved for `u-case` v4 introspection). If you reached into the instance-level method, rename to the triple-underscore form.
|
|
37
|
+
- **Heads up — `Coercion` rescues `ArgumentError` from `klass.new(value)`.** When the hash → child coercion blows up (typically missing required keys on a strict child), the raw hash is left in place so Accept's KindOf check (`expected to be a kind of <Klass>`) rejects it into `attributes_errors` instead of letting the inner raise escape. This preserves u-case's `Failure(:invalid_attributes)` envelope for non-strict outer Cases that hold `accept:` nested entities. For strict outer Cases the rejection still raises — only the message changes (controlled "kind of" wording instead of the bare "missing keyword" from inside the child).
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- `attribute!` (subclass overwrite) with a `default:` did not clear the inherited `__attributes_required__` entry when the parent had `Initialize::Strict`. `Child.new({})` raised `ArgumentError: missing keyword: ...` even though the child gave the attribute a default. `__attributes_required_add` is now an add-or-remove sync (called from `__attributes_data_to_assign`) so the required set reflects the current options. **`default:` always wins** — when an attribute is declared with a default, `required: true` is treated as a docs hint (matches 3.0.x behavior) and the attribute is not added to the required set, regardless of strict-mode or explicit `required: true`.
|
|
41
|
+
- `attribute!` (subclass overwrite) couldn't change an attribute's Ruby visibility back from `private`/`protected` to `public` — it updated `__attributes_data__` (and so the `#attributes` hash reflected the new visibility), but the inherited reader method retained its original Ruby visibility. The class-method `__attribute_assign` macro now re-applies visibility for already-defined attributes when overwriting (via the new private class method `__attribute_reapply_visibility`).
|
|
42
|
+
- Block-form `attribute :name do ... end` and `attribute! :name do ... end` no longer silently ignore the block. The block is captured to build an anonymous inline class; passing both `accept:` and a block raises `ArgumentError` (see Changed §).
|
|
43
|
+
- Block-form nested attributes (`attribute :foo do ... end`) no longer leak the host class's user-defined attributes — or any sibling attributes added to the same class body after the block runs — into the inline nested class. The inline child is now built by replaying every `Micro::Attributes::With::*` module found on the host's ancestors, so the feature mix is reconstructed independently of `self`'s declared attributes.
|
|
44
|
+
- Block-form inline classes used in an `:activemodel_validations` host no longer raise `"Class name cannot be blank"` when ActiveModel renders error messages. The inline class now exposes a `model_name` (and lazily-resolved `to_s` / `inspect`) with an explicit name like `"Order(customer)"`, so `errors.full_messages` works and the parent's heap address never leaks into validation output — even when the host is itself an anonymous class created via `Micro::Attributes.new { ... }`.
|
|
45
|
+
- `Composition::Coercion` now gates on a precise arity check (`arity == 1 || arity == -1 || arity == -2`) instead of `klass.include?(Features::Initialize)`. The check covers `Features::Initialize` includers AND user-defined hash constructors (`def initialize(arg); self.attributes = arg; end` — the long-standing `u-case` v4 idiom). Multi-required-arg constructors (`def initialize(a, b)`, arity 2+) are correctly SKIPPED so they don't crash on `klass.new(hash)` — the value passes through to the standard accept check instead.
|
|
46
|
+
- Inline-class `inspect` now filters by `self.class.attributes_by_visibility[:public]` instead of the `@__*` prefix. This hides BOTH (a) ActiveModel internals (`@errors`, `@validation_context`, `@context_for_validation`) and (b) private/protected attribute VALUES, which the previous ivar-prefix filter let leak when AM was in the mix or when the host had private attrs.
|
|
47
|
+
- The `model_name` singleton on inline classes is now defined ONLY when the inline class includes `ActiveModel::Validations`. The previous always-define approach flipped `respond_to?(:model_name)` from false → true on AM-less hosts, breaking duck-typing feature-detection in third-party libraries.
|
|
48
|
+
- `Micro::Attributes.new` now rejects any `:initialize` value that isn't `true` or `:strict` — covers `false`, `nil`, and garbage values like `'on'`. Pre-fix only `== false` was caught, so `Micro::Attributes.new(initialize: nil)` silently built a class with no hash constructor.
|
|
49
|
+
- Layered `Micro::Attributes.with(...)` calls — two `include`s or `include` + `with` class macro — now reach block-form inline children with the **full** combined feature mix. The previous "first-include-wins" cache silently dropped features for inline children; ancestors are now scanned at build time so every layered feature is replayed.
|
|
50
|
+
- Block-form `attribute :foo do ... end` works when the host class includes `Micro::Attributes` (or `Features::*`) DIRECTLY without going through `Micro::Attributes.with(...)` — the `u-case` usage pattern. Pre-fix the inline child fell back to bare `Micro::Attributes` (no `:initialize`) and hashes weren't coerced. The build path now detects every `Features::*` module already in the host's ancestors and rebuilds an equivalent `with(...)` mix for the inline child, always including `:initialize` and `:accept` defaults so block-form has a hash constructor and accept-validation.
|
|
51
|
+
- Inline-class `model_name` singleton is now defined unconditionally with an at-call-time `defined?(::ActiveModel::Name)` check. Previously the singleton was only defined when AM was loaded at inline-class build time — gem authors who define classes eagerly and let Rails autoload AM later (a real load-order pattern) would otherwise hit the original `"Class name cannot be blank"` error.
|
|
52
|
+
- Instance-level `inspect` on a block-form inline instance no longer leaks the anonymous class's heap address. Ruby's default `Object#inspect` reads `Module#name` (still `nil` on anonymous inline classes) rather than `to_s`, so the previous fix at the class level didn't help instances. Inline classes now define `inspect` to use the stable class label.
|
|
53
|
+
- The Coercion bubble (writes `'is invalid'` to the parent's `attributes_errors`) is gated on `attribute_data[3] == :public`. Private/protected nested-entity attribute names don't surface through the new bubble path — pre-existing direct Accept reject errors are unchanged (still surface for all visibilities, matching 3.0.x).
|
|
54
|
+
- `__validate_nested_entities__` iterates `attributes_by_visibility[:public]` instead of all attributes, so private/protected nested-attribute names don't surface through the new auto-registered AM validator (the new cascade respects visibility; pre-existing direct AM validators are unchanged).
|
|
55
|
+
- `Micro::Attributes.new(active_model: :validations) { ... }` no longer raises `ActiveModel::Name#initialize: Class name cannot be blank` the first time `errors.full_messages` runs. The factory class now installs a `model_name` singleton (mirroring the inline-child fix at `Macros#__micro_attributes_build_inline_class__`) that resolves the label lazily via `self.name || self.inspect`, so AM error rendering works whether the result is assigned to a constant or kept anonymous.
|
|
56
|
+
- `__validate_nested_entities__` no longer wipes a shared child's pre-existing errors. AM's `valid?` calls `errors.clear` before re-running validators, so a child instance whose caller had added errors externally (or that was already-validated and shared across parents) would silently lose those errors as soon as one parent ran `parent.valid?`. The validator now short-circuits to "invalid" when `child.errors.any?` is already true and skips the re-validation in that case.
|
|
57
|
+
- Block-form `attribute :foo do ... end` no longer overwrites a user-defined `def inspect` placed inside the block. The macro's default `inspect` is now only installed when the inline class doesn't already define one directly (`instance_methods(false)`), so customizations declared in the block take precedence.
|
|
58
|
+
|
|
10
59
|
## [3.0.2] - 2026-05-24
|
|
11
60
|
### Added
|
|
12
61
|
- This `CHANGELOG.md`, covering the full history of the gem (from `micro-attributes 0.1.0` through `u-attributes 3.0.2`) following the [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) spec.
|
|
@@ -260,6 +309,7 @@ First stable release.
|
|
|
260
309
|
- `Micro::Attributes` mixin with the `.attribute` / `.attributes` macros for declaring attributes on a plain Ruby object.
|
|
261
310
|
- Generated reader methods plus the `with_attribute` / `with_attributes` constructors that return a new instance with the updated values (no setters).
|
|
262
311
|
|
|
312
|
+
[3.1.0]: https://github.com/serradura/u-attributes/compare/v3.0.2...v3.1.0
|
|
263
313
|
[3.0.2]: https://github.com/serradura/u-attributes/compare/v3.0.1...v3.0.2
|
|
264
314
|
[3.0.1]: https://github.com/serradura/u-attributes/compare/v3.0.0...v3.0.1
|
|
265
315
|
[3.0.0]: https://github.com/serradura/u-attributes/compare/v2.8.0...v3.0.0
|