rest-easy 1.4.0 → 1.4.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: 567f6e8a9344027299c80aeebc330318bedf9ea0a030493b378ed3bfa9c3047d
4
- data.tar.gz: 8d9015ee4dd9eeda16ed953cec2804c7a8ba723cd346dc0e1da481c4c3a9683a
3
+ metadata.gz: a30780e0b38e31f1c183d1ec0a91ea93658a78e67d2415aeb8aef279d03359b7
4
+ data.tar.gz: 2ed7aafbcaf5e0c6fd463992a65293d1f064c693611f64b9b3256e8e4b518fdf
5
5
  SHA512:
6
- metadata.gz: 6e10536c6514eaca3e02acec7e303dbf085b4cef96e9fceb1d48260571087d56589aec201e3996153f6bb3dfafd235a20dda099ac634c300492d18c831ebb50d
7
- data.tar.gz: c92c35d0ab63b770d2abf29e24ca8b4f7ff3a59945594e55d30bbf0ec7843f39f0f734ed2ca01aa648b94df57e4e8302d50182c3934b71343f3acfb4d9124271
6
+ metadata.gz: 3ac9ea37e26b652d5fe590075d7668f70e2cf8b7290b02d7e4dd1f227bf7a7e26ea9e5faa02ed1b84b5706262d90e774db52381ea0ce2d026da40d410ae38c11
7
+ data.tar.gz: 8f8773d0cbacc6366daade22ceee18ee76de91ee5eb5cd718c392e0abfe1ed3a911658a867dab1bcd1f21b077f2296e0cb5ffbb1aa5597f6fee7e77a7b40b84d
data/CHANGELOG.md CHANGED
@@ -2,6 +2,50 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.4.1] - 2026-08-18
6
+
7
+ ### Fixed
8
+
9
+ - **`Meta` no longer claims to implement methods it has no value for.**
10
+ `Meta#respond_to_missing?` returned `true` for every name, so any caller
11
+ that asks an object what it can do before calling it got a false yes, then
12
+ received the `nil` that `method_missing` returns for an unknown key as
13
+ though it were a real answer. Ruby does this constantly and implicitly —
14
+ coercion probes when a value is splatted or interpolated, serialisation
15
+ hooks, and any library that duck-types with `respond_to?` — so the failure
16
+ surfaced far from `Meta` and looked unrelated to it. Marshalling was the
17
+ sharpest case: `dump` silently wrote an empty payload and `load` then
18
+ raised `NoMethodError` from inside `method_missing`, which broke every
19
+ consumer caching resources in a store that marshals its entries.
20
+
21
+ Bare getters are now claimed only for keys the instance actually holds.
22
+ Setters and predicates are still claimed unconditionally, since writing is
23
+ how a key comes into existence and an unset predicate is meaningfully
24
+ `false`; no core Ruby probe uses those two shapes. Library-level
25
+ duck-typing can, though — ActiveSupport's `acts_like?(:date)` asks for
26
+ `acts_like_date?`, and `Meta` still answers yes — so the guarantee is
27
+ narrower than "nothing probes for `=` or `?`", and closing it properly is
28
+ tracked in #7. Operators are no longer claimed either: the check is
29
+ anchored to identifier-shaped names, so `<=` is not mistaken for a setter
30
+ (`method_missing` still is — #6).
31
+ `Resource::MetaCollector` had the identical defect and is fixed the same
32
+ way.
33
+
34
+ **Upgrading with a warm cache:** entries written by an earlier version are
35
+ not recoverable. A payload dumped before this fix carries `nil` where the
36
+ meta state should be, and loading it now raises `TypeError: instance of
37
+ RestEasy::Meta needs to have method 'marshal_load'` — legible, but still
38
+ an error. Consumers on a cache that survives deploys (`:file_store`,
39
+ Redis, Memcached) must flush it or bump the cache key namespace when
40
+ upgrading. A process-local `:memory_store` clears itself on restart and
41
+ needs nothing.
42
+
43
+ `method_missing` is unchanged: a bare getter for a key that was never set
44
+ still returns `nil` rather than raising, as the "gem author extensions"
45
+ example in `docs/model-architecture.md` depends on. `respond_to?`
46
+ therefore under-reports for those keys; reconciling the two halves
47
+ requires declaring meta keys up front and is tracked in #7.
48
+
5
49
  ## [1.4.0] - 2026-06-26
6
50
 
7
51
  ### Fixed
@@ -123,7 +167,8 @@
123
167
 
124
168
  Initial release.
125
169
 
126
- [Unreleased]: https://github.com/accodeing/rest-easy/compare/v1.4.0...HEAD
170
+ [Unreleased]: https://github.com/accodeing/rest-easy/compare/v1.4.1...HEAD
171
+ [1.4.1]: https://github.com/accodeing/rest-easy/compare/v1.4.0...v1.4.1
127
172
  [1.4.0]: https://github.com/accodeing/rest-easy/compare/v1.3.1...v1.4.0
128
173
  [1.3.1]: https://github.com/accodeing/rest-easy/compare/v1.3.0...v1.3.1
129
174
  [1.3.0]: https://github.com/accodeing/rest-easy/compare/v1.2.0...v1.3.0
@@ -25,8 +25,46 @@ module RestEasy
25
25
  end
26
26
  end
27
27
 
28
- def respond_to_missing?(_name, _include_private = false)
29
- true
28
+ # Accessor shapes `method_missing` synthesises. Anchored, so operators
29
+ # stay out: `<=` ends with `=`, and claiming it would let `meta <= 5`
30
+ # read as a setter. `method_missing` itself is still unanchored and
31
+ # writes `@data[:<]` for that call — see issue #6.
32
+ #
33
+ # POSIX classes rather than `[a-zA-Z_]\w*`, because Ruby identifiers are
34
+ # not ASCII-only: `meta.företag = x` is a valid setter that
35
+ # `method_missing` stores, so `respond_to?` has to claim it too.
36
+ ACCESSOR_PATTERN = /\A[[:alpha:]_][[:word:]]*[=?]\z/
37
+ private_constant :ACCESSOR_PATTERN
38
+
39
+ # `method_missing` answers to any name, but this must not, because callers
40
+ # routinely ask an object what it can do before calling it — Ruby's own
41
+ # coercion probes (`to_hash` on double-splat, `to_ary`, `to_str`),
42
+ # serialisation hooks (`marshal_dump`, `init_with`), and any library that
43
+ # duck-types with `respond_to?`. Claiming those hands the caller the `nil`
44
+ # that `method_missing` returns for an unknown key as though it were an
45
+ # answer.
46
+ #
47
+ # Bare getters are therefore claimed only for keys actually held. Setters
48
+ # and predicates are claimed unconditionally: writing is how a key comes
49
+ # into existence, and an unset predicate is meaningfully `false`. No core
50
+ # Ruby probe uses those two shapes — though library-level duck-typing can,
51
+ # and ActiveSupport's `acts_like?(:date)` asking for `acts_like_date?` is
52
+ # a known false positive. Closing it properly means declaring meta keys
53
+ # up front, which is a breaking change — see issue #7.
54
+ #
55
+ # `@data` is guarded because `respond_to?` must never raise, and Ruby
56
+ # allocates an instance before initialising it: Psych's `revive` probes
57
+ # `init_with` on a bare allocation, and so does `Marshal.load` on a
58
+ # payload written before this fix.
59
+ #
60
+ # This under-claims a bare getter for a key that was never set, which
61
+ # `method_missing` still answers with `nil`. Reconciling the two halves
62
+ # means declaring meta keys up front, and that is a 2.0 — see issue #7.
63
+ def respond_to_missing?(name, include_private = false)
64
+ return true if ACCESSOR_PATTERN.match?(name.to_s)
65
+ return super unless @data
66
+
67
+ @data.key?(name.to_sym) || super
30
68
  end
31
69
  end
32
70
  end
@@ -119,8 +119,22 @@ module RestEasy
119
119
  end
120
120
  end
121
121
 
122
- def respond_to_missing?(_name, _include_private = false)
123
- true
122
+ # Anchored so operators stay out — see Meta::ACCESSOR_PATTERN and
123
+ # issue #6. Only the setter shape here; unlike Meta, `method_missing`
124
+ # above has no predicate form.
125
+ SETTER_PATTERN = /\A[[:alpha:]_][[:word:]]*=\z/
126
+ private_constant :SETTER_PATTERN
127
+
128
+ # Same reasoning as Meta#respond_to_missing? — a blanket `true` makes
129
+ # this claim Ruby's implicit protocol methods and hand them the `nil`
130
+ # from `method_missing`. `@data` is guarded for the same reason too:
131
+ # `respond_to?` must never raise, and Psych and Marshal both probe a
132
+ # bare allocation before it is initialised.
133
+ def respond_to_missing?(name, include_private = false)
134
+ return true if SETTER_PATTERN.match?(name.to_s)
135
+ return super unless @data
136
+
137
+ @data.key?(name.to_sym) || super
124
138
  end
125
139
  end
126
140
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RestEasy
4
- VERSION = "1.4.0"
4
+ VERSION = "1.4.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rest-easy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonas Schubert Erlandsson
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-06-26 00:00:00.000000000 Z
13
+ date: 2026-08-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: dry-types