rest_framework 2.0.0.beta6 → 2.0.0.rc1
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/README.md +24 -1
- data/lib/rest_framework/controller/actions.rb +18 -10
- data/lib/rest_framework/controller/openapi.rb +8 -2
- data/lib/rest_framework/controller.rb +81 -30
- data/lib/rest_framework/filters/query_filter.rb +1 -7
- data/lib/rest_framework/routers.rb +15 -8
- data/lib/rest_framework/serializers/native_serializer.rb +18 -6
- data/lib/rest_framework/utils.rb +27 -3
- data/lib/rest_framework/version.rb +1 -39
- metadata +2 -3
- data/VERSION +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 827c588ec16bebf728f1bc186d0b716488be2a13d7d653fec82ff3c1bd3d7a75
|
|
4
|
+
data.tar.gz: f1447d4d393e9ab18fc82a2edeb1b366d7236ad46a00aa5e1adedb294262f3a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 818d459d7b6897e777edb7d403e23087c19d2a18c8002cb8fc343381c514b7aa49da625f460d0fd06a181d76fe55308ec2642f493f4627aef82d98d341775014
|
|
7
|
+
data.tar.gz: 50226b18ddd51c61a88952c2637c3ea0c622ef59385ddcaff8b231dc0b2aabcacbe6fb5dd9a7db5d4d49bc67600c1d543781e9065cb37faa2344998186df3018
|
data/README.md
CHANGED
|
@@ -67,7 +67,7 @@ controllers/
|
|
|
67
67
|
└─ users_controller.rb
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
### Serving the
|
|
70
|
+
### Serving the Base API Index
|
|
71
71
|
|
|
72
72
|
A controller without a `model` renders its `index_content` at its index path, which serves as the
|
|
73
73
|
API root. Because declared actions are local by default (they don't propagate to subclasses), you
|
|
@@ -168,6 +168,29 @@ web server and the job queue, which serves the test app and coverage/brakeman re
|
|
|
168
168
|
- API: [http://127.0.0.1:3000/api](http://127.0.0.1:3000/api)
|
|
169
169
|
- Reports: [http://127.0.0.1:3000/reports](http://127.0.0.1:3000/reports)
|
|
170
170
|
|
|
171
|
+
### Releasing
|
|
172
|
+
|
|
173
|
+
The gem version is in `lib/rest_framework/version.rb`. Cutting a release means bumping that
|
|
174
|
+
constant, tagging, and pushing; the pipeline builds the gem from the constant and pushes it to
|
|
175
|
+
RubyGems when it sees the tag.
|
|
176
|
+
|
|
177
|
+
Use `bin/release` with the exact version. It bumps the version constant, refreshes `Gemfile.lock`,
|
|
178
|
+
folds both into a single commit, and creates the annotated tag `v<version>` (via the
|
|
179
|
+
[gem-release](https://github.com/svenfuchs/gem-release) gem). The lock refresh matters because
|
|
180
|
+
`rest_framework` is a path gem: if the tagged commit's lock doesn't match the bumped gemspec, CI's
|
|
181
|
+
frozen `bundle install` fails with "the gemspecs for path gems changed". Run it from the `master`
|
|
182
|
+
branch with a clean working tree:
|
|
183
|
+
|
|
184
|
+
```shell
|
|
185
|
+
bin/release 2.0.0.rc1 # cut a release candidate
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Review, then push to publish:
|
|
189
|
+
|
|
190
|
+
```shell
|
|
191
|
+
git push origin master --follow-tags
|
|
192
|
+
```
|
|
193
|
+
|
|
171
194
|
## Version 2
|
|
172
195
|
|
|
173
196
|
Version 2 is a substantial overhaul. The highlights below cover the major additions and behavior
|
|
@@ -50,7 +50,10 @@ module RESTFramework::Controller
|
|
|
50
50
|
# Route an action, choosing the collection/member scope. Pass `type:` on a plural model
|
|
51
51
|
# controller, where the scopes differ; elsewhere the scope is implied — a singular controller's
|
|
52
52
|
# sole resource is a member (so `delegate` targets the record), a modelless one has only a
|
|
53
|
-
# collection — and `type:` warns as unnecessary (unless the action is delegated).
|
|
53
|
+
# collection — and `type:` warns as unnecessary (unless the action is delegated). `propagate:`
|
|
54
|
+
# also accepts a `->(controller) { ... }` predicate to gate the action: it applies to this
|
|
55
|
+
# controller and its descendants wherever the predicate holds — e.g. `->(c) { c.model }` routes
|
|
56
|
+
# it only on controllers with a model.
|
|
54
57
|
def add_action(name, methods, type: nil, **opts)
|
|
55
58
|
singular_model = self.model && self.singular
|
|
56
59
|
|
|
@@ -112,9 +115,10 @@ module RESTFramework::Controller
|
|
|
112
115
|
_rrf_action_removes(type)[name] = { propagate: _rrf_normalize_propagate(propagate) }
|
|
113
116
|
end
|
|
114
117
|
|
|
115
|
-
# Normalize `propagate:` to `false` (local), `true` (self + descendants),
|
|
116
|
-
# (descendants only)
|
|
117
|
-
# becomes `
|
|
118
|
+
# Normalize `propagate:` to `false` (local), `true` (self + descendants), `:exclude_self`
|
|
119
|
+
# (descendants only), or a `->(controller) { ... }` predicate (self + descendants, wherever it
|
|
120
|
+
# returns truthy). Non-standard values warn: `nil` becomes `false`, anything else truthy becomes
|
|
121
|
+
# `true`.
|
|
118
122
|
def _rrf_normalize_propagate(value)
|
|
119
123
|
case value
|
|
120
124
|
when false
|
|
@@ -125,21 +129,25 @@ module RESTFramework::Controller
|
|
|
125
129
|
Rails.logger.warn("RRF: `propagate: nil` is nonstandard; treating as `false`.")
|
|
126
130
|
false
|
|
127
131
|
else
|
|
132
|
+
return value if value.respond_to?(:call)
|
|
133
|
+
|
|
128
134
|
Rails.logger.warn("RRF: invalid `propagate:` value #{value.inspect}; treating as `true`.")
|
|
129
135
|
true
|
|
130
136
|
end
|
|
131
137
|
end
|
|
132
138
|
|
|
133
|
-
# Whether an entry with the given `propagate`, declared on some class, reaches the
|
|
134
|
-
# we're composing for. `is_self` is true when
|
|
135
|
-
def _rrf_reaches?(propagate, is_self)
|
|
139
|
+
# Whether an entry with the given `propagate`, declared on some class, reaches `controller` (the
|
|
140
|
+
# class we're composing for). `is_self` is true when the declaring class is `controller` itself.
|
|
141
|
+
def _rrf_reaches?(propagate, is_self, controller)
|
|
136
142
|
case propagate
|
|
137
143
|
when :exclude_self
|
|
138
144
|
!is_self
|
|
139
145
|
when true
|
|
140
146
|
true
|
|
141
|
-
|
|
147
|
+
when false
|
|
142
148
|
is_self
|
|
149
|
+
else # a `->(controller) { ... }` predicate: applies wherever it holds (self and descendants)
|
|
150
|
+
propagate.call(controller)
|
|
143
151
|
end
|
|
144
152
|
end
|
|
145
153
|
|
|
@@ -172,11 +180,11 @@ module RESTFramework::Controller
|
|
|
172
180
|
is_self = klass.equal?(self)
|
|
173
181
|
|
|
174
182
|
klass._rrf_action_removes(type).each do |name, remove|
|
|
175
|
-
effective.delete(name) if _rrf_reaches?(remove[:propagate], is_self)
|
|
183
|
+
effective.delete(name) if _rrf_reaches?(remove[:propagate], is_self, self)
|
|
176
184
|
end
|
|
177
185
|
|
|
178
186
|
klass._rrf_action_adds(type).each do |name, add|
|
|
179
|
-
effective[name] = add[:spec] if _rrf_reaches?(add[:propagate], is_self)
|
|
187
|
+
effective[name] = add[:spec] if _rrf_reaches?(add[:propagate], is_self, self)
|
|
180
188
|
end
|
|
181
189
|
end
|
|
182
190
|
|
|
@@ -196,15 +196,21 @@ module RESTFramework::Controller
|
|
|
196
196
|
|
|
197
197
|
if cfg[:reflection]
|
|
198
198
|
ref = cfg[:reflection]
|
|
199
|
+
|
|
200
|
+
# A polymorphic `belongs_to` has no single target class, so class-derived properties
|
|
201
|
+
# (`association_primary_key`, `join_table`) raise; expose the `*_type` column instead.
|
|
202
|
+
polymorphic = ref.respond_to?(:polymorphic?) && ref.polymorphic?
|
|
199
203
|
v[:"x-rrf-reflection"] = {
|
|
204
|
+
polymorphic: polymorphic || nil,
|
|
200
205
|
class_name: ref.respond_to?(:class_name) ? ref.class_name : nil,
|
|
201
206
|
foreign_key: ref.respond_to?(:foreign_key) ? ref.foreign_key : nil,
|
|
207
|
+
foreign_type: polymorphic ? ref.foreign_type : nil,
|
|
202
208
|
association_foreign_key: ref.respond_to?(:association_foreign_key) ?
|
|
203
209
|
ref.association_foreign_key : nil,
|
|
204
|
-
association_primary_key: ref.respond_to?(:association_primary_key) ?
|
|
210
|
+
association_primary_key: !polymorphic && ref.respond_to?(:association_primary_key) ?
|
|
205
211
|
ref.association_primary_key : nil,
|
|
206
212
|
inverse_of: ref.respond_to?(:inverse_of) ? ref.inverse_of&.name : nil,
|
|
207
|
-
join_table: ref.respond_to?(:join_table) ? ref.join_table : nil,
|
|
213
|
+
join_table: !polymorphic && ref.respond_to?(:join_table) ? ref.join_table : nil,
|
|
208
214
|
}.compact
|
|
209
215
|
v[:"x-rrf-association_pk"] = cfg[:association_pk]
|
|
210
216
|
v[:"x-rrf-association_fields"] = cfg[:fields]
|
|
@@ -301,34 +301,23 @@ module RESTFramework::Controller
|
|
|
301
301
|
cfg = field_config[f]&.dup || {}
|
|
302
302
|
cfg[:label] ||= self.label_for(f)
|
|
303
303
|
|
|
304
|
+
# An explicit `read_only`/`write_only` in `field_config` wins over every framework default
|
|
305
|
+
# below (primary key, readonly attributes, the read/write-only config lists, and the
|
|
306
|
+
# method-field default), so those only apply when the developer set neither.
|
|
307
|
+
read_write_only_set = cfg.key?(:read_only) || cfg.key?(:write_only)
|
|
308
|
+
|
|
304
309
|
# Annotate primary key.
|
|
305
310
|
if self.model.primary_key == f
|
|
306
311
|
cfg[:primary_key] = true
|
|
307
|
-
|
|
308
|
-
unless cfg.key?(:read_only)
|
|
309
|
-
cfg[:read_only] = true
|
|
310
|
-
end
|
|
312
|
+
cfg[:read_only] = true unless read_write_only_set
|
|
311
313
|
end
|
|
312
314
|
|
|
313
315
|
# Annotate field mutability and display properties.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
# Raise warnings on some bad combinations of properties.
|
|
319
|
-
if cfg[:write_only]
|
|
320
|
-
if cfg[:read_only]
|
|
321
|
-
Rails.logger.warn("RRF: `#{f}` write_only conflicts with read_only.")
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
if cfg[:hidden]
|
|
325
|
-
Rails.logger.warn("RRF: `#{f}` write_only implies hidden.")
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
if cfg[:hidden_from_index]
|
|
329
|
-
Rails.logger.warn("RRF: `#{f}` write_only implies hidden_from_index.")
|
|
330
|
-
end
|
|
316
|
+
unless read_write_only_set
|
|
317
|
+
cfg[:read_only] = true if f.in?(readonly_attributes) || f.in?(read_only_fields)
|
|
318
|
+
cfg[:write_only] = true if f.in?(write_only_fields)
|
|
331
319
|
end
|
|
320
|
+
cfg[:hidden] = true if f.in?(hidden_fields)
|
|
332
321
|
|
|
333
322
|
# Annotate column data.
|
|
334
323
|
if column = columns[f]
|
|
@@ -432,7 +421,8 @@ module RESTFramework::Controller
|
|
|
432
421
|
# Determine if this is just a method.
|
|
433
422
|
if !cfg[:kind] && self.model.method_defined?(f)
|
|
434
423
|
cfg[:kind] = "method"
|
|
435
|
-
|
|
424
|
+
# Methods are read-only by default, unless the field was marked read/write-only.
|
|
425
|
+
cfg[:read_only] = true unless read_write_only_set || cfg[:write_only]
|
|
436
426
|
end
|
|
437
427
|
|
|
438
428
|
# Collect validator options into a hash on their type, while also updating `required` based
|
|
@@ -453,6 +443,21 @@ module RESTFramework::Controller
|
|
|
453
443
|
cfg[:validators][kind] << options
|
|
454
444
|
end
|
|
455
445
|
|
|
446
|
+
# Warn on bad combinations, once every property has been resolved.
|
|
447
|
+
if cfg[:write_only]
|
|
448
|
+
if cfg[:read_only]
|
|
449
|
+
Rails.logger.warn("RRF: `#{f}` write_only conflicts with read_only.")
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
if cfg[:hidden]
|
|
453
|
+
Rails.logger.warn("RRF: `#{f}` write_only implies hidden.")
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
if cfg[:hidden_from_index]
|
|
457
|
+
Rails.logger.warn("RRF: `#{f}` write_only implies hidden_from_index.")
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
456
461
|
next [ f, cfg ]
|
|
457
462
|
}.to_h.compact.with_indifferent_access
|
|
458
463
|
|
|
@@ -648,8 +653,10 @@ module RESTFramework::Controller
|
|
|
648
653
|
end
|
|
649
654
|
|
|
650
655
|
def readable_fields
|
|
651
|
-
|
|
652
|
-
|
|
656
|
+
@_readable_fields ||= begin
|
|
657
|
+
cfg = self.class.field_configuration
|
|
658
|
+
self.get_fields.reject { |f| cfg[f]&.[](:write_only) }
|
|
659
|
+
end
|
|
653
660
|
end
|
|
654
661
|
|
|
655
662
|
# The fields a client may write (create/update): `get_fields` minus read_only fields. Excluding
|
|
@@ -657,23 +664,35 @@ module RESTFramework::Controller
|
|
|
657
664
|
# keeps a read_only association from ever producing a permitted (and otherwise unstrippable, since
|
|
658
665
|
# those keys don't match a field name) assignment key.
|
|
659
666
|
def writable_fields
|
|
660
|
-
|
|
661
|
-
|
|
667
|
+
@_writable_fields ||= begin
|
|
668
|
+
cfg = self.class.field_configuration
|
|
669
|
+
self.get_fields.reject { |f| cfg[f]&.[](:read_only) }
|
|
670
|
+
end
|
|
662
671
|
end
|
|
663
672
|
|
|
664
673
|
# `readable_fields` restricted to real columns, for query surfaces that build SQL directly
|
|
665
674
|
# (find_by, search) and would raise on a virtual/method field.
|
|
666
675
|
def readable_columns
|
|
667
|
-
self.readable_fields & self.class.model.column_names
|
|
676
|
+
@_readable_columns ||= self.readable_fields & self.class.model.column_names
|
|
668
677
|
end
|
|
669
678
|
|
|
670
679
|
# `readable_fields` restricted to columns and associations, for surfaces that also resolve dotted
|
|
671
680
|
# `association.sub_field` paths (filtering, ordering). Excludes virtual/method fields, which have
|
|
672
681
|
# no column to order or filter by.
|
|
673
682
|
def readable_columns_or_associations
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
683
|
+
@_readable_columns_or_associations ||= begin
|
|
684
|
+
cfg = self.class.field_configuration
|
|
685
|
+
columns = self.class.model.column_names
|
|
686
|
+
self.readable_fields.select do |f|
|
|
687
|
+
next true if f.in?(columns)
|
|
688
|
+
|
|
689
|
+
# Skip polymorphic associations: they can't be JOINed, so filtering or ordering through them
|
|
690
|
+
# (e.g. `?favorite.name=x` or `?ordering=favorite.name`) would raise. This method is the
|
|
691
|
+
# safe field surface for those query features.
|
|
692
|
+
field = cfg[f]
|
|
693
|
+
field&.[](:kind) == "association" && !field[:reflection]&.polymorphic?
|
|
694
|
+
end
|
|
695
|
+
end
|
|
677
696
|
end
|
|
678
697
|
|
|
679
698
|
# Get a hash of strong parameters for the current action.
|
|
@@ -711,6 +730,14 @@ module RESTFramework::Controller
|
|
|
711
730
|
next nil
|
|
712
731
|
end
|
|
713
732
|
|
|
733
|
+
# JSON/JSONB columns hold opaque structured data, so permit hash (and nested) values here.
|
|
734
|
+
# Scalar and array values can't share this slot in strong params, so `get_body_params`
|
|
735
|
+
# re-injects them after filtering.
|
|
736
|
+
if config[:type].in?(%i[json jsonb])
|
|
737
|
+
hash_variations[f] = {}
|
|
738
|
+
next nil
|
|
739
|
+
end
|
|
740
|
+
|
|
714
741
|
if config[:reflection]
|
|
715
742
|
# Add `_id`/`_ids` variations for associations.
|
|
716
743
|
if id_field = config[:id_field]
|
|
@@ -742,6 +769,15 @@ module RESTFramework::Controller
|
|
|
742
769
|
@_get_allowed_parameters
|
|
743
770
|
end
|
|
744
771
|
|
|
772
|
+
# Writable JSON/JSONB columns, whose values are opaque and may arrive as any JSON type.
|
|
773
|
+
def get_json_columns
|
|
774
|
+
return @_get_json_columns if defined?(@_get_json_columns)
|
|
775
|
+
|
|
776
|
+
@_get_json_columns = self.writable_fields.map(&:to_s).select { |f|
|
|
777
|
+
self.class.field_configuration[f]&.[](:type).in?(%i[json jsonb])
|
|
778
|
+
}
|
|
779
|
+
end
|
|
780
|
+
|
|
745
781
|
# Use strong parameters to filter the request body.
|
|
746
782
|
def get_body_params(bulk_action: nil)
|
|
747
783
|
data = self.request.request_parameters
|
|
@@ -808,6 +844,16 @@ module RESTFramework::Controller
|
|
|
808
844
|
end
|
|
809
845
|
end
|
|
810
846
|
|
|
847
|
+
# JSON/JSONB columns accept any JSON value. Strong params permit a hash for such a key (via
|
|
848
|
+
# `key: {}`; see `get_allowed_parameters`), but can't also accept a scalar or array in that
|
|
849
|
+
# slot, so remember non-hash values now and re-inject them after filtering.
|
|
850
|
+
json_scalar_or_array_data = {}
|
|
851
|
+
if !bulk_action && self.class.model
|
|
852
|
+
self.get_json_columns.each do |f|
|
|
853
|
+
json_scalar_or_array_data[f] = data[f] if data.key?(f) && !data[f].is_a?(Hash)
|
|
854
|
+
end
|
|
855
|
+
end
|
|
856
|
+
|
|
811
857
|
# Filter the request body with strong params. If `bulk` is true, then we apply allowed
|
|
812
858
|
# parameters to the `_json` key of the request body.
|
|
813
859
|
body_params = if allowed_params == true
|
|
@@ -835,6 +881,11 @@ module RESTFramework::Controller
|
|
|
835
881
|
body_params[k].unshift(*v)
|
|
836
882
|
end
|
|
837
883
|
|
|
884
|
+
# Re-inject scalar/array JSON column values that strong params dropped (see above).
|
|
885
|
+
json_scalar_or_array_data.each do |k, v|
|
|
886
|
+
body_params[k] = v
|
|
887
|
+
end
|
|
888
|
+
|
|
838
889
|
body_params
|
|
839
890
|
end
|
|
840
891
|
alias_method :get_create_params, :get_body_params
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
# A simple filtering backend that supports filtering a recordset based on query parameters.
|
|
2
2
|
class RESTFramework::Filters::QueryFilter < RESTFramework::Filters::BaseFilter
|
|
3
3
|
# Wrapper to indicate a type of query that must be negated with `where.not(...)`.
|
|
4
|
-
|
|
5
|
-
attr_reader :q
|
|
6
|
-
|
|
7
|
-
def initialize(q)
|
|
8
|
-
@q = q
|
|
9
|
-
end
|
|
10
|
-
end
|
|
4
|
+
Not = Struct.new(:q)
|
|
11
5
|
|
|
12
6
|
PREDICATES = {
|
|
13
7
|
true: true,
|
|
@@ -9,8 +9,9 @@ module ActionDispatch::Routing
|
|
|
9
9
|
mod.const_get("#{name.to_s.camelize}Controller")
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
# Route each action from a controller's action store.
|
|
13
|
-
|
|
12
|
+
# Route each action from a controller's action store. `helpers: false` routes every action
|
|
13
|
+
# unnamed (`as: nil`), so the resource contributes no URL/path helpers.
|
|
14
|
+
def _rrf_route_actions(actions, helpers: true)
|
|
14
15
|
actions.each_value do |spec|
|
|
15
16
|
# Delegated actions keep their declared action name (so routing and OpenAPI show the real
|
|
16
17
|
# name); `method_for_action` redirects dispatch to `rrf_delegate`, which needs the scope.
|
|
@@ -18,6 +19,7 @@ module ActionDispatch::Routing
|
|
|
18
19
|
if !spec.builtin && spec.metadata&.[](:delegate)
|
|
19
20
|
kwargs = kwargs.merge(rrf_delegate_scope: spec.type)
|
|
20
21
|
end
|
|
22
|
+
kwargs = kwargs.merge(as: nil) unless helpers
|
|
21
23
|
|
|
22
24
|
spec.methods.each do |m|
|
|
23
25
|
public_send(m, spec.path, action: spec.name, **kwargs)
|
|
@@ -36,7 +38,9 @@ module ActionDispatch::Routing
|
|
|
36
38
|
# a plural `resources` is decided by the controller's own config (`singular`, and whether it has
|
|
37
39
|
# a `model`) — not by the method name: a plural model controller gets collection/member scopes,
|
|
38
40
|
# while singular and non-model controllers route everything at the root. Pass a block to nest
|
|
39
|
-
# resources like Rails' `resources` (the nested controller resolves in the current scope).
|
|
41
|
+
# resources like Rails' `resources` (the nested controller resolves in the current scope). Pass
|
|
42
|
+
# `helpers: false` to route the resource without URL/path helpers — handy when a singular and a
|
|
43
|
+
# plural resource of the same model would otherwise claim the same helper name.
|
|
40
44
|
def rest_resource(name, **kwargs)
|
|
41
45
|
controller = kwargs.delete(:controller) || name
|
|
42
46
|
if controller.is_a?(Class)
|
|
@@ -48,6 +52,9 @@ module ActionDispatch::Routing
|
|
|
48
52
|
# Set controller if it's not explicitly set.
|
|
49
53
|
kwargs[:controller] = name unless kwargs[:controller]
|
|
50
54
|
|
|
55
|
+
# `helpers:` is ours, not Rails' — pull it out before forwarding the rest to the router.
|
|
56
|
+
helpers = kwargs.delete(:helpers) != false
|
|
57
|
+
|
|
51
58
|
has_model = !!controller_class.model
|
|
52
59
|
singular = controller_class.singular
|
|
53
60
|
actions = controller_class.actions
|
|
@@ -61,16 +68,16 @@ module ActionDispatch::Routing
|
|
|
61
68
|
if has_model
|
|
62
69
|
if singular
|
|
63
70
|
# Singular model controller: actions and member actions are the same.
|
|
64
|
-
self._rrf_route_actions(actions)
|
|
65
|
-
self._rrf_route_actions(member_actions)
|
|
71
|
+
self._rrf_route_actions(actions, helpers: helpers)
|
|
72
|
+
self._rrf_route_actions(member_actions, helpers: helpers)
|
|
66
73
|
else
|
|
67
74
|
# Plural model controller: route collection/member actions separately.
|
|
68
|
-
collection { self._rrf_route_actions(actions) }
|
|
69
|
-
member { self._rrf_route_actions(member_actions) }
|
|
75
|
+
collection { self._rrf_route_actions(actions, helpers: helpers) }
|
|
76
|
+
member { self._rrf_route_actions(member_actions, helpers: helpers) }
|
|
70
77
|
end
|
|
71
78
|
else
|
|
72
79
|
# Non-model controller: only actions (there is no member `:id` scope).
|
|
73
|
-
self._rrf_route_actions(actions)
|
|
80
|
+
self._rrf_route_actions(actions, helpers: helpers)
|
|
74
81
|
end
|
|
75
82
|
|
|
76
83
|
yield if block_given?
|
|
@@ -241,14 +241,26 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
241
241
|
if f.in?(column_names)
|
|
242
242
|
columns << f
|
|
243
243
|
elsif ref = reflections[f]
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
244
|
+
# A polymorphic `belongs_to` has no single target class to introspect, so serialize it via a
|
|
245
|
+
# method that always emits the `type` (from the `*_type` column) alongside the id, plus a
|
|
246
|
+
# label when the target has one. Filtering/ordering skip polymorphic associations (see
|
|
247
|
+
# `readable_columns_or_associations`), so consumer-driven field/limit requests don't apply.
|
|
248
|
+
if ref.polymorphic?
|
|
249
|
+
foreign_type = ref.foreign_type
|
|
250
|
+
serializer_methods[f] = f
|
|
251
|
+
includes_map[f] = f.to_sym
|
|
252
|
+
self.define_singleton_method(f) do |record|
|
|
253
|
+
next nil unless target = record.send(f)
|
|
254
|
+
|
|
255
|
+
RESTFramework::Utils.serialize_polymorphic(target, record.send(foreign_type))
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
next
|
|
250
259
|
end
|
|
251
260
|
|
|
261
|
+
effective_fields = self._effective_association_fields(f, ref, field_config)
|
|
262
|
+
sub_config = self._build_association_config(ref.klass, effective_fields, field_config)
|
|
263
|
+
|
|
252
264
|
# Apply certain rules regarding collection associations.
|
|
253
265
|
if ref.collection?
|
|
254
266
|
# A finite limit needs a per-record `.limit` query, since eager `includes` can't cap rows
|
data/lib/rest_framework/utils.rb
CHANGED
|
@@ -122,8 +122,14 @@ module RESTFramework::Utils
|
|
|
122
122
|
# Get the fields for a given model, including not just columns (which includes foreign keys), but
|
|
123
123
|
# also associations. Note that we always return an array of strings, not symbols.
|
|
124
124
|
def self.fields_for(model, exclude_associations:, action_text:, active_storage:)
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
belongs_to = model.reflect_on_all_associations(:belongs_to)
|
|
126
|
+
|
|
127
|
+
# A polymorphic `belongs_to` is backed by both a foreign key and a `*_type` column; the
|
|
128
|
+
# association represents both, so drop them from the plain column fields (as we already do for
|
|
129
|
+
# every foreign key).
|
|
130
|
+
excluded_columns = belongs_to.map(&:foreign_key)
|
|
131
|
+
excluded_columns += belongs_to.select(&:polymorphic?).map(&:foreign_type)
|
|
132
|
+
base_fields = model.column_names.reject { |c| c.in?(excluded_columns) }
|
|
127
133
|
|
|
128
134
|
return base_fields if exclude_associations
|
|
129
135
|
|
|
@@ -173,7 +179,25 @@ module RESTFramework::Utils
|
|
|
173
179
|
return fields
|
|
174
180
|
end
|
|
175
181
|
|
|
176
|
-
|
|
182
|
+
# A polymorphic association has no single target class, so we can't resolve a label column ahead
|
|
183
|
+
# of time. The id and type together identify the record; a per-record label is added at
|
|
184
|
+
# serialization time when the target has one (see `serialize_polymorphic`).
|
|
185
|
+
[ "id", "type" ]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Serialize a polymorphic association's target as `{<pk> => id, "type" => type}`, plus a label
|
|
189
|
+
# entry when the target responds to one of the configured `label_fields`. The type comes from the
|
|
190
|
+
# parent's `*_type` column, so it matches exactly what is stored (and what a reverse lookup uses).
|
|
191
|
+
def self.serialize_polymorphic(target, type)
|
|
192
|
+
result = {}
|
|
193
|
+
Array(target.class.primary_key).each { |pk| result[pk] = target.public_send(pk) }
|
|
194
|
+
result["type"] = type
|
|
195
|
+
|
|
196
|
+
if label = RESTFramework.config.label_fields.find { |f| target.respond_to?(f) }
|
|
197
|
+
result[label] = target.public_send(label)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
result
|
|
177
201
|
end
|
|
178
202
|
|
|
179
203
|
# Get a field's id/ids variation.
|
|
@@ -1,41 +1,3 @@
|
|
|
1
1
|
module RESTFramework
|
|
2
|
-
|
|
3
|
-
module Version
|
|
4
|
-
VERSION_FILEPATH = File.expand_path("../../VERSION", __dir__)
|
|
5
|
-
UNKNOWN = "0-unknown"
|
|
6
|
-
|
|
7
|
-
def self.get_version(skip_git: false)
|
|
8
|
-
# First, attempt to get the version from git.
|
|
9
|
-
unless skip_git
|
|
10
|
-
version = `git describe --dirty 2>/dev/null`&.strip
|
|
11
|
-
return version unless !version || version.empty?
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# Git failed or was skipped, so try to find a VERSION file.
|
|
15
|
-
begin
|
|
16
|
-
version = File.read(VERSION_FILEPATH)&.strip
|
|
17
|
-
return version unless !version || version.empty?
|
|
18
|
-
rescue SystemCallError
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# No VERSION file, so version is unknown.
|
|
22
|
-
UNKNOWN
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.stamp_version(version = nil)
|
|
26
|
-
# Stamp the given version into the VERSION file, deriving it from git when none is provided.
|
|
27
|
-
# Returns the stamped version so callers don't rely on the (require-time) `VERSION` constant.
|
|
28
|
-
version ||= self.get_version
|
|
29
|
-
if version != UNKNOWN
|
|
30
|
-
File.write(VERSION_FILEPATH, version)
|
|
31
|
-
end
|
|
32
|
-
version
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.unstamp_version
|
|
36
|
-
File.delete(VERSION_FILEPATH) if File.exist?(VERSION_FILEPATH)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
VERSION = Version.get_version(skip_git: true)
|
|
2
|
+
VERSION = "2.0.0.rc1"
|
|
41
3
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rest_framework
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.0.
|
|
4
|
+
version: 2.0.0.rc1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gregory N. Schmit
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -34,7 +34,6 @@ files:
|
|
|
34
34
|
- ".yardopts"
|
|
35
35
|
- LICENSE
|
|
36
36
|
- README.md
|
|
37
|
-
- VERSION
|
|
38
37
|
- app/views/layouts/rest_framework.html.erb
|
|
39
38
|
- app/views/rest_framework/_breadcrumbs.html.erb
|
|
40
39
|
- app/views/rest_framework/_head.html.erb
|
data/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.0.0.beta6
|