rest-easy 1.3.1 → 1.4.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 +43 -1
- data/README.md +35 -6
- data/lib/rest_easy/attribute.rb +14 -0
- data/lib/rest_easy/resource.rb +33 -6
- data/lib/rest_easy/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 567f6e8a9344027299c80aeebc330318bedf9ea0a030493b378ed3bfa9c3047d
|
|
4
|
+
data.tar.gz: 8d9015ee4dd9eeda16ed953cec2804c7a8ba723cd346dc0e1da481c4c3a9683a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e10536c6514eaca3e02acec7e303dbf085b4cef96e9fceb1d48260571087d56589aec201e3996153f6bb3dfafd235a20dda099ac634c300492d18c831ebb50d
|
|
7
|
+
data.tar.gz: c92c35d0ab63b770d2abf29e24ca8b4f7ff3a59945594e55d30bbf0ec7843f39f0f734ed2ca01aa648b94df57e4e8302d50182c3934b71343f3acfb4d9124271
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.4.0] - 2026-06-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`:required` is now enforced on serialise as well as parse.** Previously
|
|
10
|
+
the flag raised `MissingAttributeError` only when an API response omitted
|
|
11
|
+
the field; it was silently ignored when sending (`save`), letting
|
|
12
|
+
incomplete payloads reach the backend and surface as a generic
|
|
13
|
+
`RequestError`. The flag now also fires at `serialise` time (and therefore
|
|
14
|
+
on `save`) when a required attribute is `nil`, before any HTTP request,
|
|
15
|
+
with the attribute name on the exception (`#attribute_name`). Inbound
|
|
16
|
+
behavior is unchanged — `parse` still raises on missing required fields.
|
|
17
|
+
A field is treated as missing whether the API response omits the key
|
|
18
|
+
entirely or includes it as explicit `null`; both are rejected as
|
|
19
|
+
incomplete data.
|
|
20
|
+
- **`:required` is now enforced on synthetic attributes (merge / combine /
|
|
21
|
+
split patterns).** Previously the flag was silently a no-op for any
|
|
22
|
+
attribute with a multi-parameter parse or serialise block. It now
|
|
23
|
+
enforces presence on all underlying API or model fields: a merge
|
|
24
|
+
attribute requires every source API field on parse; a combine attribute
|
|
25
|
+
requires every named model attribute on serialise; a split attribute
|
|
26
|
+
requires the underlying API field on parse. `:read_only` continues to
|
|
27
|
+
skip the serialise-time check.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **The DSL auto-applies `:synthetic` to multi-parameter serialise blocks**
|
|
32
|
+
(and mapper `serialise` methods), mirroring the existing behavior for
|
|
33
|
+
multi-parameter parse. Previously only the parse side was tagged,
|
|
34
|
+
leaving the flag inconsistent with its intent of marking attributes whose
|
|
35
|
+
storage shape diverges from the standard one-slot layout.
|
|
36
|
+
- **Combine attributes no longer read from their `api_name` on parse.**
|
|
37
|
+
A combine attribute's API name does not correspond to a real inbound
|
|
38
|
+
field by design — the value is built from `target_fields` at serialise
|
|
39
|
+
time. The previous code looked up `api_data[api_name]`, stored it at
|
|
40
|
+
the model slot, and ran the standard `:required` check, raising
|
|
41
|
+
spuriously when the field was absent (the documented case). Inbound
|
|
42
|
+
values for the shadowed API key are now ignored and the model slot is
|
|
43
|
+
set to `nil`, avoiding the contradiction where `instance.address` could
|
|
44
|
+
return one value while serialise would overwrite it with another.
|
|
45
|
+
|
|
5
46
|
## [1.3.1] - 2026-05-27
|
|
6
47
|
|
|
7
48
|
### Fixed
|
|
@@ -82,7 +123,8 @@
|
|
|
82
123
|
|
|
83
124
|
Initial release.
|
|
84
125
|
|
|
85
|
-
[Unreleased]: https://github.com/accodeing/rest-easy/compare/v1.
|
|
126
|
+
[Unreleased]: https://github.com/accodeing/rest-easy/compare/v1.4.0...HEAD
|
|
127
|
+
[1.4.0]: https://github.com/accodeing/rest-easy/compare/v1.3.1...v1.4.0
|
|
86
128
|
[1.3.1]: https://github.com/accodeing/rest-easy/compare/v1.3.0...v1.3.1
|
|
87
129
|
[1.3.0]: https://github.com/accodeing/rest-easy/compare/v1.2.0...v1.3.0
|
|
88
130
|
[1.2.0]: https://github.com/accodeing/rest-easy/compare/v1.1.2...v1.2.0
|
data/README.md
CHANGED
|
@@ -326,12 +326,12 @@ attr [:tax_url, '@urlTaxReductionList'], String, :read_only
|
|
|
326
326
|
|
|
327
327
|
### Flags
|
|
328
328
|
|
|
329
|
-
| Flag | Effect
|
|
330
|
-
|
|
331
|
-
| `:required` | Raises `MissingAttributeError` if
|
|
332
|
-
| `:optional` | Documents that the field may be absent (default)
|
|
333
|
-
| `:read_only` | Excluded from serialisation (not sent back to the API)
|
|
334
|
-
| `:key` | Marks the unique identifier for CRUD operations
|
|
329
|
+
| Flag | Effect |
|
|
330
|
+
|--------------|------------------------------------------------------------------------|
|
|
331
|
+
| `:required` | Raises `MissingAttributeError` if missing or explicitly `null` in the API response (on parse) or `nil` at serialise time (`save`, `to_api`). Omitted keys and explicit `null` are treated identically — a required attribute must carry a value. See [Merge](#merge-pattern--many-api-fields-into-one-model-attribute), [Combine](#combine-pattern--many-model-attributes-into-one-api-field), and [Split](#split-pattern--one-api-field-into-many-model-attributes) patterns for how this applies to synthetic attributes. |
|
|
332
|
+
| `:optional` | Documents that the field may be absent (default) |
|
|
333
|
+
| `:read_only` | Excluded from serialisation (not sent back to the API) |
|
|
334
|
+
| `:key` | Marks the unique identifier for CRUD operations |
|
|
335
335
|
|
|
336
336
|
```ruby
|
|
337
337
|
key :id, Integer, :read_only
|
|
@@ -430,6 +430,31 @@ end
|
|
|
430
430
|
attr :full_name, String, FullNameMapper
|
|
431
431
|
```
|
|
432
432
|
|
|
433
|
+
Marking a merge attribute `:required` enforces that **all** underlying API fields are present in the response on parse. If the attribute is also round-trippable (not `:read_only`), the merged model value must additionally be non-`nil` at serialise time.
|
|
434
|
+
|
|
435
|
+
Note that string-concatenation merges like the `full_name` example above are inherently lossy on the round-trip — splitting `"Hans Erik Nilsson"` back into `FirstName`/`LastName` is ambiguous. Such attributes should normally be `:read_only`. Round-trippable merges work when the model representation preserves the structure (e.g. a tuple, a `Money` value object, or a `Date` built from year/month/day components).
|
|
436
|
+
|
|
437
|
+
### Combine pattern — many model attributes into one API field
|
|
438
|
+
|
|
439
|
+
The dual of merge: when the serialise method takes multiple parameters, RestEasy gathers the values from the corresponding model attributes and passes them in. The block returns the single combined API value:
|
|
440
|
+
|
|
441
|
+
```ruby
|
|
442
|
+
attr :street, String
|
|
443
|
+
attr :city, String
|
|
444
|
+
|
|
445
|
+
attr :address, String do
|
|
446
|
+
serialise { |street, city| "#{street}, #{city}" }
|
|
447
|
+
end
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
The parameter names (`street`, `city`) name model attributes; the framework reads them from the instance and splats them into the block. The block's return value is written under the attribute's API name (`Address`).
|
|
451
|
+
|
|
452
|
+
This also works with mapper objects whose `serialise` method takes multiple parameters.
|
|
453
|
+
|
|
454
|
+
Marking a combine attribute `:required` enforces that **all** named model attributes are non-`nil` at serialise time. The parse side does not apply — combine attributes don't read from a single API field on the way in, so users typically populate the underlying model attributes directly.
|
|
455
|
+
|
|
456
|
+
Combine attributes do not run a `parse` block. If you declare one alongside a multi-parameter `serialise`, RestEasy emits a warning at load time — the parse block would be silently ignored otherwise. If you need to read from an API field on parse, declare a separate attribute for it (or restructure as a merge pattern).
|
|
457
|
+
|
|
433
458
|
### Split pattern — one API field into many model attributes
|
|
434
459
|
|
|
435
460
|
Use a bare block with a parameter to extract from a single API field:
|
|
@@ -446,6 +471,10 @@ end
|
|
|
446
471
|
|
|
447
472
|
The parameter name (`address`) determines which API field to read from.
|
|
448
473
|
|
|
474
|
+
Marking a split attribute `:required` enforces that the underlying API field is present on parse — in the example above, the API response must include `Address`.
|
|
475
|
+
|
|
476
|
+
On serialise, split attributes are emitted under their own api_names (here, `Street` and `City`) rather than being recombined into the original source field. If the API does not accept the parts as independent top-level fields, mark them `:read_only` to keep them out of the outbound payload entirely; alternatively, reconstruct the source field in an `after_serialise` hook.
|
|
477
|
+
|
|
449
478
|
### Ignoring fields
|
|
450
479
|
|
|
451
480
|
Tell RestEasy to silently skip API fields you don't need:
|
data/lib/rest_easy/attribute.rb
CHANGED
|
@@ -36,6 +36,20 @@ module RestEasy
|
|
|
36
36
|
@flags.include?(:synthetic)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# A combine attribute is built from multiple model fields on serialise
|
|
40
|
+
# (target_fields), with no inbound source from its own api_name on parse.
|
|
41
|
+
# See the "Combine pattern" section of the README.
|
|
42
|
+
def combine?
|
|
43
|
+
@target_fields.any? && @source_fields.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_required!(*values)
|
|
47
|
+
return unless required?
|
|
48
|
+
return if values.none?(&:nil?)
|
|
49
|
+
|
|
50
|
+
raise RestEasy::MissingAttributeError.new(model_name)
|
|
51
|
+
end
|
|
52
|
+
|
|
39
53
|
def coerce(value)
|
|
40
54
|
@type[value]
|
|
41
55
|
rescue Dry::Types::ConstraintError, Dry::Types::CoercionError => e
|
data/lib/rest_easy/resource.rb
CHANGED
|
@@ -250,6 +250,7 @@ module RestEasy
|
|
|
250
250
|
|
|
251
251
|
serialise_params = serialise_block.parameters.select { |ptype, _| ptype == :opt || ptype == :req }
|
|
252
252
|
if serialise_params.length > 1
|
|
253
|
+
flags << :synthetic unless flags.include?(:synthetic)
|
|
253
254
|
target_fields = serialise_params.map { |_, pname| pname }
|
|
254
255
|
end
|
|
255
256
|
elsif block
|
|
@@ -284,9 +285,22 @@ module RestEasy
|
|
|
284
285
|
if serialise_block
|
|
285
286
|
params = serialise_block.parameters.select { |ptype, _| ptype == :opt || ptype == :req }
|
|
286
287
|
if params.length > 1
|
|
288
|
+
flags << :synthetic unless flags.include?(:synthetic)
|
|
287
289
|
target_fields = params.map { |_, pname| pname }
|
|
288
290
|
end
|
|
289
291
|
end
|
|
292
|
+
|
|
293
|
+
# Combine pattern (multi-param serialise, no multi-param parse)
|
|
294
|
+
# has no inbound api_name on parse. If the user also wrote an
|
|
295
|
+
# explicit `parse` block, it will be silently ignored — warn so
|
|
296
|
+
# the inconsistency is visible at load time.
|
|
297
|
+
if target_fields.any? && source_fields.empty? && parse_block
|
|
298
|
+
warn "RestEasy: :#{attribute_model_name} declares a combine pattern " \
|
|
299
|
+
"(serialise from #{target_fields.inspect}) and also defines a parse block. " \
|
|
300
|
+
"Combine attributes have no inbound API field to read, so the parse block " \
|
|
301
|
+
"will not run. Remove the parse block, or restructure the declaration if you " \
|
|
302
|
+
"intended to read from the API."
|
|
303
|
+
end
|
|
290
304
|
end
|
|
291
305
|
end
|
|
292
306
|
|
|
@@ -591,13 +605,15 @@ module RestEasy
|
|
|
591
605
|
# Serialise all attributes
|
|
592
606
|
klass.all_attribute_definitions.each do |_model_name, attr_def|
|
|
593
607
|
next if attr_def.read_only?
|
|
594
|
-
value = @model_attributes[attr_def.model_name]
|
|
595
608
|
|
|
596
609
|
if attr_def.target_fields.any?
|
|
597
610
|
# Multi-param serialise: gather model values by param names, splat into block
|
|
598
611
|
model_values = attr_def.target_fields.map { |fn| @model_attributes[fn] }
|
|
612
|
+
attr_def.validate_required!(*model_values)
|
|
599
613
|
result[attr_def.api_name] = attr_def.serialise_value(*model_values)
|
|
600
614
|
elsif attr_def.source_fields.any?
|
|
615
|
+
value = @model_attributes[attr_def.model_name]
|
|
616
|
+
attr_def.validate_required!(value)
|
|
601
617
|
serialised = attr_def.serialise_value(value)
|
|
602
618
|
if serialised.is_a?(::Array)
|
|
603
619
|
# Array return: zip with source field API names
|
|
@@ -613,6 +629,8 @@ module RestEasy
|
|
|
613
629
|
result[attr_def.api_name] = serialised
|
|
614
630
|
end
|
|
615
631
|
else
|
|
632
|
+
value = @model_attributes[attr_def.model_name]
|
|
633
|
+
attr_def.validate_required!(value)
|
|
616
634
|
result[attr_def.api_name] = attr_def.serialise_value(value)
|
|
617
635
|
end
|
|
618
636
|
end
|
|
@@ -683,13 +701,19 @@ module RestEasy
|
|
|
683
701
|
api_key = convention.serialise(field_name)
|
|
684
702
|
api_data[api_key]
|
|
685
703
|
end
|
|
704
|
+
|
|
705
|
+
attr_def.validate_required!(*raw_values)
|
|
706
|
+
|
|
686
707
|
@model_attributes[model_name] = attr_def.parse_value(*raw_values)
|
|
708
|
+
elsif attr_def.combine?
|
|
709
|
+
# Combine pattern: the attribute's api_name does not exist on the
|
|
710
|
+
# API side by design — the value is built from target_fields at
|
|
711
|
+
# serialise time. Nothing inbound to read or validate.
|
|
712
|
+
@model_attributes[model_name] = nil
|
|
687
713
|
else
|
|
688
714
|
raw_value = api_data[attr_def.api_name]
|
|
689
715
|
|
|
690
|
-
|
|
691
|
-
raise MissingAttributeError.new(model_name)
|
|
692
|
-
end
|
|
716
|
+
attr_def.validate_required!(raw_value)
|
|
693
717
|
|
|
694
718
|
if raw_value.nil?
|
|
695
719
|
@model_attributes[model_name] = nil
|
|
@@ -717,9 +741,12 @@ module RestEasy
|
|
|
717
741
|
end
|
|
718
742
|
end
|
|
719
743
|
|
|
720
|
-
# Warn about declared attributes missing from the API response
|
|
744
|
+
# Warn about declared attributes missing from the API response.
|
|
745
|
+
# Combine attrs have no inbound api_name by design; non-combine
|
|
746
|
+
# required attrs already raised in the parse loop above.
|
|
721
747
|
klass.all_attribute_definitions.each do |model_name, attr_def|
|
|
722
|
-
next if attr_def.
|
|
748
|
+
next if attr_def.combine?
|
|
749
|
+
next if attr_def.required?
|
|
723
750
|
|
|
724
751
|
api_keys_to_check = if attr_def.source_fields.any?
|
|
725
752
|
attr_def.source_fields.map { |sf| convention.serialise(sf) }
|
data/lib/rest_easy/version.rb
CHANGED
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
|
+
version: 1.4.0
|
|
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-
|
|
13
|
+
date: 2026-06-26 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: dry-types
|