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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7be6812ae478ca58b5f352bf561a57384d309e2a91037d25cf1ad3dbe64ff09
4
- data.tar.gz: a2295b59abd1f6e15fa0736b3e5255f00492a0cdc794583b87b85634533cb003
3
+ metadata.gz: 827c588ec16bebf728f1bc186d0b716488be2a13d7d653fec82ff3c1bd3d7a75
4
+ data.tar.gz: f1447d4d393e9ab18fc82a2edeb1b366d7236ad46a00aa5e1adedb294262f3a9
5
5
  SHA512:
6
- metadata.gz: 6a92158c48314c5fb62470cde91d5b21d264e60b8d3d367315b20014965cceaee9bef5ff019c32cc3ef75fb9a45a410cc63373d2dc9c00fa295f6626ec279a6b
7
- data.tar.gz: a332502f06c34126962890d6e4b395d5788c8dd1c07d5bd0cfbbcb33d49ec209efad4986d99f1f69ec0f86569b175bb902a11aee76cb21f4942546e256e0239b
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 Root API Index
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), or `:exclude_self`
116
- # (descendants only). Non-standard values warn: `nil` becomes `false`, anything else truthy
117
- # becomes `true`.
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 controller
134
- # we're composing for. `is_self` is true when that class is the controller itself.
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
- else # false
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
- cfg[:read_only] = true if f.in?(readonly_attributes) || f.in?(read_only_fields)
315
- cfg[:write_only] = true if f.in?(write_only_fields)
316
- cfg[:hidden] = true if f.in?(hidden_fields)
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
- cfg[:read_only] = true if cfg[:read_only].nil?
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
- cfg = self.class.field_configuration
652
- self.get_fields.reject { |f| cfg[f]&.[](:write_only) }
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
- cfg = self.class.field_configuration
661
- self.get_fields.reject { |f| cfg[f]&.[](:read_only) }
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
- cfg = self.class.field_configuration
675
- columns = self.class.model.column_names
676
- self.readable_fields.select { |f| f.in?(columns) || cfg[f]&.[](:kind) == "association" }
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
- class Not
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
- def _rrf_route_actions(actions)
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
- effective_fields = self._effective_association_fields(f, ref, field_config)
245
- sub_config = if ref.polymorphic?
246
- # No single target class to introspect, so serialize every field as a method.
247
- { only: [], methods: effective_fields }
248
- else
249
- self._build_association_config(ref.klass, effective_fields, field_config)
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
@@ -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
- foreign_keys = model.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
126
- base_fields = model.column_names.reject { |c| c.in?(foreign_keys) }
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
- [ "id", "name" ]
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
- # Do not use Rails-specific helper methods here (e.g., `blank?`) so the module can run standalone.
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.beta6
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-04 00:00:00.000000000 Z
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