rest_framework 0.1.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7cb7ba6d5e7861525dca18f908afa1f398f614af654223ac03de15f7014dafaa
4
- data.tar.gz: 7938b5b0225e089f2cd3a168b80763ebcc63dd65655ae4c87b045e923477191a
3
+ metadata.gz: '09c24135bd79001e0113aa92f21e63c5dcb67e93891db2a5df36b4d83d1c5da2'
4
+ data.tar.gz: ba3bd5829fdb9fd3c61a6050f5800c91a0077c76e78aedb049f850e581027f67
5
5
  SHA512:
6
- metadata.gz: cb1583fe183387972e1537bc2b90ebe9b224381e46515ffd202f3ad86cb4a545f906264644fff537b5937f8e4f985f39eb3123c6d47e2e8e5c1be31b425aad54
7
- data.tar.gz: 3a8b3a6e63a4ca03250d8e6697eda885abd6940f8b580f073ffa0c237a59a535d4997aa4591651c01cda61fd3f021508ed69d298a19512f783a9604b6dbcdd7c
6
+ metadata.gz: c89c8af64c2eeb36dba79747770df5497d8505f2a4ced03ed6e25e81804a3a8a8f656e87bb0739a31d26be8e1f0fdcdd9e3df25721766e2e5d4bb2a01d40746f
7
+ data.tar.gz: 4bd5bdf9f2735622a07795245f605137c0310d16b3596fc544e9ade2046744a499ea0fdf1e7f11c7f5fc49f137650cceefadcf805884e6a6df5361ed919211d4
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rest_framework.svg)](https://badge.fury.io/rb/rest_framework)
4
4
  [![Build Status](https://travis-ci.org/gregschmit/rails-rest-framework.svg?branch=master)](https://travis-ci.org/gregschmit/rails-rest-framework)
5
+ [![Coverage Status](https://coveralls.io/repos/github/gregschmit/rails-rest-framework/badge.svg?branch=master)](https://coveralls.io/github/gregschmit/rails-rest-framework?branch=master)
5
6
 
6
7
  A framework for DRY RESTful APIs in Ruby on Rails.
7
8
 
@@ -78,8 +79,8 @@ class Api::ReadOnlyMoviesController < ApiController
78
79
  end
79
80
  ```
80
81
 
81
- Note that you can also override `get_model` and `get_recordset` instance methods to override the API
82
- behavior dynamically per-request.
82
+ Note that you can also override the `get_recordset` instance method to override the API behavior
83
+ dynamically per-request.
83
84
 
84
85
  ### Routing
85
86
 
@@ -4,8 +4,10 @@ end
4
4
 
5
5
  require_relative "rest_framework/controller_mixins"
6
6
  require_relative "rest_framework/engine"
7
+ require_relative "rest_framework/errors"
7
8
  require_relative "rest_framework/filters"
8
9
  require_relative "rest_framework/paginators"
9
10
  require_relative "rest_framework/routers"
10
11
  require_relative "rest_framework/serializers"
11
12
  require_relative "rest_framework/version"
13
+ require_relative "rest_framework/generators"
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.3
@@ -1,3 +1,4 @@
1
+ require_relative '../errors'
1
2
  require_relative '../serializers'
2
3
 
3
4
 
@@ -33,6 +34,9 @@ module RESTFramework::BaseControllerMixin
33
34
 
34
35
  # Add class attributes (with defaults) unless they already exist.
35
36
  {
37
+ filter_pk_from_request_body: true,
38
+ exclude_body_fields: [:created_at, :created_by, :updated_at, :updated_by],
39
+ accept_generic_params_as_body_params: true,
36
40
  extra_actions: nil,
37
41
  extra_member_actions: nil,
38
42
  filter_backends: nil,
@@ -42,14 +46,16 @@ module RESTFramework::BaseControllerMixin
42
46
  page_size_query_param: 'page_size',
43
47
  max_page_size: nil,
44
48
  serializer_class: nil,
49
+ serialize_to_json: true,
50
+ serialize_to_xml: true,
45
51
  singleton_controller: nil,
46
52
  skip_actions: nil,
47
53
  }.each do |a, default|
48
54
  unless base.respond_to?(a)
49
55
  base.class_attribute(a)
50
56
 
51
- # Set default manually so we can still support Rails 4. Maybe later we can use the
52
- # default parameter on `class_attribute`.
57
+ # Set default manually so we can still support Rails 4. Maybe later we can use the default
58
+ # parameter on `class_attribute`.
53
59
  base.send(:"#{a}=", default)
54
60
  end
55
61
  end
@@ -60,10 +66,10 @@ module RESTFramework::BaseControllerMixin
60
66
  base.alias_method(:extra_collection_actions=, :extra_actions=)
61
67
  end
62
68
 
63
- # skip csrf since this is an API
69
+ # Skip csrf since this is an API.
64
70
  base.skip_before_action(:verify_authenticity_token) rescue nil
65
71
 
66
- # handle some common exceptions
72
+ # Handle some common exceptions.
67
73
  base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
68
74
  base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
69
75
  base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
@@ -118,7 +124,9 @@ module RESTFramework::BaseControllerMixin
118
124
  begin
119
125
  formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
120
126
  rescue NameError
127
+ # :nocov:
121
128
  formatter = ActionDispatch::Routing::ConsoleFormatter
129
+ # :nocov:
122
130
  end
123
131
  return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
124
132
  formatter.new
@@ -129,39 +137,42 @@ module RESTFramework::BaseControllerMixin
129
137
 
130
138
  # Helper to render a browsable API for `html` format, along with basic `json`/`xml` formats, and
131
139
  # with support or passing custom `kwargs` to the underlying `render` calls.
132
- def api_response(payload, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
140
+ def api_response(payload, html_kwargs: nil, **kwargs)
133
141
  html_kwargs ||= {}
134
- json_kwargs ||= {}
135
- xml_kwargs ||= {}
136
-
137
- # allow blank (no-content) responses
138
- @blank = kwargs[:blank]
142
+ json_kwargs = kwargs.delete(:json_kwargs) || {}
143
+ xml_kwargs = kwargs.delete(:xml_kwargs) || {}
144
+
145
+ # Raise helpful error if payload is nil. Usually this happens when a record is not found (e.g.,
146
+ # when passing something like `User.find_by(id: some_id)` to `api_response`). The caller should
147
+ # actually be calling `find_by!` to raise ActiveRecord::RecordNotFound and allowing the REST
148
+ # framework to catch this error and return an appropriate error response.
149
+ if payload.nil?
150
+ raise RESTFramework::NilPassedToAPIResponseError
151
+ end
139
152
 
140
153
  respond_to do |format|
141
- if @blank
142
- format.json {head :no_content}
143
- format.xml {head :no_content}
154
+ if payload == ''
155
+ format.json {head :no_content} if self.serialize_to_json
156
+ format.xml {head :no_content} if self.serialize_to_xml
144
157
  else
145
- if payload.respond_to?(:to_json)
146
- format.json {
147
- jkwargs = kwargs.merge(json_kwargs)
148
- render(json: payload, layout: false, **jkwargs)
149
- }
150
- end
151
- if payload.respond_to?(:to_xml)
152
- format.xml {
153
- xkwargs = kwargs.merge(xml_kwargs)
154
- render(xml: payload, layout: false, **xkwargs)
155
- }
156
- end
158
+ format.json {
159
+ jkwargs = kwargs.merge(json_kwargs)
160
+ render(json: payload, layout: false, **jkwargs)
161
+ } if self.serialize_to_json
162
+ format.xml {
163
+ xkwargs = kwargs.merge(xml_kwargs)
164
+ render(xml: payload, layout: false, **xkwargs)
165
+ } if self.serialize_to_xml
166
+ # TODO: possibly support more formats here if supported?
157
167
  end
158
168
  format.html {
159
169
  @payload = payload
160
- @json_payload = ''
161
- @xml_payload = ''
162
- unless @blank
163
- @json_payload = payload.to_json if payload.respond_to?(:to_json)
164
- @xml_payload = payload.to_xml if payload.respond_to?(:to_xml)
170
+ if payload == ''
171
+ @json_payload = '' if self.serialize_to_json
172
+ @xml_payload = '' if self.serialize_to_xml
173
+ else
174
+ @json_payload = payload.to_json if self.serialize_to_json
175
+ @xml_payload = payload.to_xml if self.serialize_to_xml
165
176
  end
166
177
  @template_logo_text ||= "Rails REST Framework"
167
178
  @title ||= self.controller_name.camelize
@@ -16,17 +16,18 @@ module RESTFramework::BaseModelControllerMixin
16
16
  model: nil,
17
17
  recordset: nil,
18
18
 
19
- # Attributes for create/update parameters.
20
- allowed_parameters: nil,
21
- allowed_action_parameters: nil,
22
-
23
19
  # Attributes for configuring record fields.
24
20
  fields: nil,
25
21
  action_fields: nil,
26
22
 
23
+ # Attributes for create/update parameters.
24
+ allowed_parameters: nil,
25
+ allowed_action_parameters: nil,
26
+
27
27
  # Attributes for the default native serializer.
28
28
  native_serializer_config: nil,
29
- native_serializer_action_config: nil,
29
+ native_serializer_singular_config: nil,
30
+ native_serializer_plural_config: nil,
30
31
 
31
32
  # Attributes for default model filtering (and ordering).
32
33
  filterset_fields: nil,
@@ -34,13 +35,13 @@ module RESTFramework::BaseModelControllerMixin
34
35
  ordering_query_param: 'ordering',
35
36
 
36
37
  # Other misc attributes.
37
- disable_creation_from_recordset: nil, # Option to disable `recordset.create` behavior.
38
+ disable_creation_from_recordset: false, # Option to disable `recordset.create` behavior.
38
39
  }.each do |a, default|
39
40
  unless base.respond_to?(a)
40
41
  base.class_attribute(a)
41
42
 
42
- # Set default manually so we can still support Rails 4. Maybe later we can use the
43
- # default parameter on `class_attribute`.
43
+ # Set default manually so we can still support Rails 4. Maybe later we can use the default
44
+ # parameter on `class_attribute`.
44
45
  base.send(:"#{a}=", default)
45
46
  end
46
47
  end
@@ -49,36 +50,34 @@ module RESTFramework::BaseModelControllerMixin
49
50
 
50
51
  protected
51
52
 
52
- # Get a native serializer config for the current action.
53
- # @return [RESTFramework::NativeModelSerializer]
54
- def get_native_serializer_config
55
- action_serializer_config = self.class.native_serializer_action_config || {}
56
- action = self.action_name.to_sym
57
-
58
- # Handle case where :index action is not defined.
59
- if action == :index && !action_serializer_config.key?(:index)
60
- # Default is :show if `singleton_controller`, otherwise :list.
61
- action = self.class.singleton_controller ? :show : :list
62
- end
53
+ def _get_specific_action_config(action_config_key, generic_config_key)
54
+ action_config = self.class.send(action_config_key) || {}
55
+ action = self.action_name&.to_sym
63
56
 
64
- return (action_serializer_config[action] if action) || self.class.native_serializer_config
65
- end
57
+ # Index action should use :list serializer if :index is not provided.
58
+ action = :list if action == :index && !action_config.key?(:index)
66
59
 
67
- # Helper to get the configured serializer class, or `NativeModelSerializer` as a default.
68
- # @return [RESTFramework::BaseSerializer]
69
- def get_serializer_class
70
- return self.class.serializer_class || RESTFramework::NativeModelSerializer
60
+ return (action_config[action] if action) || self.class.send(generic_config_key)
71
61
  end
72
62
 
73
63
  # Get a list of parameters allowed for the current action.
74
64
  def get_allowed_parameters
75
- allowed_action_parameters = self.class.allowed_action_parameters || {}
76
- action = self.action_name.to_sym
65
+ return _get_specific_action_config(:allowed_action_parameters, :allowed_parameters)&.map(&:to_s)
66
+ end
77
67
 
78
- # index action should use :list allowed parameters if :index is not provided
79
- action = :list if action == :index && !allowed_action_parameters.key?(:index)
68
+ # Get a list of fields for the current action.
69
+ def get_fields
70
+ return (
71
+ _get_specific_action_config(:action_fields, :fields)&.map(&:to_s) ||
72
+ self.get_model&.column_names ||
73
+ []
74
+ )
75
+ end
80
76
 
81
- return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
77
+ # Helper to get the configured serializer class, or `NativeSerializer` as a default.
78
+ # @return [RESTFramework::BaseSerializer]
79
+ def get_serializer_class
80
+ return self.class.serializer_class || RESTFramework::NativeSerializer
82
81
  end
83
82
 
84
83
  # Get the list of filtering backends to use.
@@ -89,70 +88,81 @@ module RESTFramework::BaseModelControllerMixin
89
88
  ]
90
89
  end
91
90
 
92
- # Get a list of fields for the current action.
93
- def get_fields
94
- action_fields = self.class.action_fields || {}
95
- action = self.action_name.to_sym
91
+ # Filter the request body for keys in current action's allowed_parameters/fields config.
92
+ def get_body_params
93
+ return @_get_body_params ||= begin
94
+ fields = self.get_allowed_parameters || self.get_fields
95
+
96
+ # Filter the request body.
97
+ body_params = request.request_parameters.select { |p|
98
+ fields.include?(p)
99
+ }
100
+
101
+ # Add query params in place of missing body params, if configured.
102
+ if self.class.accept_generic_params_as_body_params
103
+ (fields - body_params.keys).each do |k|
104
+ if (value = params[k])
105
+ body_params[k] = value
106
+ end
107
+ end
108
+ end
96
109
 
97
- # index action should use :list fields if :index is not provided
98
- action = :list if action == :index && !action_fields.key?(:index)
110
+ # Filter primary key if configured.
111
+ if self.class.filter_pk_from_request_body
112
+ body_params.delete(self.get_model&.primary_key)
113
+ end
99
114
 
100
- return (action_fields[action] if action) || self.class.fields || []
101
- end
115
+ # Filter fields in exclude_body_fields.
116
+ (self.class.exclude_body_fields || []).each do |f|
117
+ body_params.delete(f.to_s)
118
+ end
102
119
 
103
- # Filter the request body for keys in current action's allowed_parameters/fields config.
104
- def get_body_params
105
- fields = self.get_allowed_parameters || self.get_fields
106
- return @get_body_params ||= (request.request_parameters.select { |p|
107
- fields.include?(p.to_sym) || fields.include?(p.to_s)
108
- })
120
+ body_params
121
+ end
109
122
  end
110
123
  alias :get_create_params :get_body_params
111
124
  alias :get_update_params :get_body_params
112
125
 
113
- # Get a record by the primary key from the filtered recordset.
126
+ # Get a record by the primary key from the (non-filtered) recordset.
114
127
  def get_record
115
- records = self.get_filtered_data(self.get_recordset)
116
- if pk = params[self.model.primary_key]
117
- return records.find(pk)
128
+ if pk = params[self.get_model.primary_key]
129
+ return self.get_recordset.find(pk)
118
130
  end
119
131
  return nil
120
132
  end
121
133
 
122
- # Internal interface for get_model, protecting against infinite recursion with get_recordset.
123
- def _get_model(from_internal_get_recordset: false)
134
+ # Get the model for this controller.
135
+ def get_model(from_get_recordset: false)
124
136
  return @model if instance_variable_defined?(:@model) && @model
125
- return self.class.model if self.class.model
126
- unless from_internal_get_recordset # prevent infinite recursion
127
- recordset = self._get_recordset(from_internal_get_model: true)
128
- return (@model = recordset.klass) if recordset
137
+ return (@model = self.class.model) if self.class.model
138
+
139
+ # Delegate to the recordset's model, if it's defined.
140
+ unless from_get_recordset # prevent infinite recursion
141
+ if (recordset = self.get_recordset)
142
+ return @model = recordset.klass
143
+ end
129
144
  end
145
+
146
+ # Try to determine model from controller name.
130
147
  begin
131
148
  return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
132
149
  rescue NameError
133
150
  end
134
- return nil
135
- end
136
151
 
137
- # Internal interface for get_recordset, protecting against infinite recursion with get_model.
138
- def _get_recordset(from_internal_get_model: false)
139
- return @recordset if instance_variable_defined?(:@recordset) && @recordset
140
- return self.class.recordset if self.class.recordset
141
- unless from_internal_get_model # prevent infinite recursion
142
- model = self._get_model(from_internal_get_recordset: true)
143
- return (@recordset = model.all) if model
144
- end
145
152
  return nil
146
153
  end
147
154
 
148
- # Get the model for this controller.
149
- def get_model
150
- return _get_model
151
- end
152
-
153
155
  # Get the set of records this controller has access to.
154
156
  def get_recordset
155
- return _get_recordset
157
+ return @recordset if instance_variable_defined?(:@recordset) && @recordset
158
+ return (@recordset = self.class.recordset) if self.class.recordset
159
+
160
+ # If there is a model, return that model's default scope (all records by default).
161
+ if (model = self.get_model(from_get_recordset: true))
162
+ return @recordset = model.all
163
+ end
164
+
165
+ return nil
156
166
  end
157
167
  end
158
168
 
@@ -219,7 +229,7 @@ module RESTFramework::DestroyModelMixin
219
229
  def destroy
220
230
  @record = self.get_record
221
231
  @record.destroy!
222
- api_response(nil)
232
+ api_response('')
223
233
  end
224
234
  end
225
235
 
@@ -0,0 +1,26 @@
1
+ # Top-level class for all REST Framework errors.
2
+ class RESTFramework::Error < StandardError
3
+ end
4
+
5
+ class RESTFramework::NilPassedToAPIResponseError < RESTFramework::Error
6
+ def message
7
+ return <<~MSG.split("\n").join(' ')
8
+ Payload of `nil` was passed to `api_response`; this is unsupported. If you want a blank
9
+ response, pass `''` (an empty string) as the payload. If this was the result of a `find_by`
10
+ (or similar Active Record method) not finding a record, you should use the bang version (e.g.,
11
+ `find_by!`) to raise `ActiveRecord::RecordNotFound`, which the REST controller will catch and
12
+ return an appropriate error response.
13
+ MSG
14
+ end
15
+ end
16
+
17
+ class RESTFramework::UnserializableError < RESTFramework::Error
18
+ def initialize(object)
19
+ @object = object
20
+ return super
21
+ end
22
+
23
+ def message
24
+ return "Unable to serialize `#{@object.inspect}` (of type `#{@object.class}`)."
25
+ end
26
+ end
@@ -14,10 +14,10 @@ end
14
14
  class RESTFramework::ModelFilter < RESTFramework::BaseFilter
15
15
  # Filter params for keys allowed by the current action's filterset_fields/fields config.
16
16
  def _get_filter_params
17
- fields = @controller.class.filterset_fields || @controller.send(:get_fields)
17
+ fields = @controller.class.filterset_fields&.map(&:to_s) || @controller.send(:get_fields)
18
18
  return @controller.request.query_parameters.select { |p|
19
- fields.include?(p.to_sym) || fields.include?(p.to_s)
20
- }.to_hash.symbolize_keys
19
+ fields.include?(p)
20
+ }.to_h.symbolize_keys # convert from HashWithIndifferentAccess to Hash w/keys
21
21
  end
22
22
 
23
23
  # Filter data according to the request query parameters.
@@ -0,0 +1,5 @@
1
+ module RESTFramework::Generators
2
+ end
3
+
4
+
5
+ require_relative 'generators/controller_generator'
@@ -0,0 +1,67 @@
1
+ require 'rails/generators'
2
+
3
+
4
+ # Some projects don't have the inflection "REST" as an acronym, so this is a helper class to prevent
5
+ # this generator from being namespaced under `r_e_s_t_framework`.
6
+ # :nocov:
7
+ class RESTFrameworkCustomGeneratorControllerNamespace < String
8
+ def camelize
9
+ return "RESTFramework"
10
+ end
11
+ end
12
+ # :nocov:
13
+
14
+
15
+ class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
16
+ PATH_REGEX = /^\/*([a-z0-9_\/]*[a-z0-9_])(?:[\.a-z\/]*)$/
17
+
18
+ desc <<~END
19
+ Description:
20
+ Generates a new REST Framework controller.
21
+
22
+ Specify the controller as a path, including the module, if needed, like:
23
+ 'parent_module/controller_name'.
24
+
25
+ Example:
26
+ `rails generate rest_framework:controller user_api/groups`
27
+
28
+ Generates a controller at `app/controllers/user_api/groups_controller.rb` named
29
+ `UserApi::GroupsController`.
30
+ END
31
+
32
+ argument :path, type: :string
33
+ class_option(
34
+ :parent_class,
35
+ type: :string,
36
+ default: 'ApplicationController',
37
+ desc: "Inheritance parent",
38
+ )
39
+ class_option(
40
+ :include_base,
41
+ type: :boolean,
42
+ default: false,
43
+ desc: "Include `BaseControllerMixin`, not `ModelControllerMixin`",
44
+ )
45
+
46
+ # Some projects may not have the inflection "REST" as an acronym, which changes this generator to
47
+ # be namespaced in `r_e_s_t_framework`, which is weird.
48
+ def self.namespace
49
+ return RESTFrameworkCustomGeneratorControllerNamespace.new("rest_framework:controller")
50
+ end
51
+
52
+ def create_rest_controller_file
53
+ unless (path_match = PATH_REGEX.match(self.path))
54
+ raise StandardError.new("Path isn't correct.")
55
+ end
56
+
57
+ cleaned_path = path_match[1]
58
+ content = <<~END
59
+ class #{cleaned_path.camelize}Controller < #{options[:parent_class]}
60
+ include RESTFramework::#{
61
+ options[:include_base] ? "BaseControllerMixin" : "ModelControllerMixin"
62
+ }
63
+ end
64
+ END
65
+ create_file("app/controllers/#{path}_controller.rb", content)
66
+ end
67
+ end
@@ -1,5 +1,6 @@
1
1
  require 'action_dispatch/routing/mapper'
2
2
 
3
+
3
4
  module ActionDispatch::Routing
4
5
  class Mapper
5
6
  # Internal helper to take extra_actions hash and convert to a consistent format.
@@ -24,6 +25,11 @@ module ActionDispatch::Routing
24
25
  path = v.delete(:path)
25
26
  end
26
27
 
28
+ # Set the action to be the action key unless it's already defined.
29
+ if !kwargs[:action]
30
+ kwargs[:action] = k
31
+ end
32
+
27
33
  # Pass any further kwargs to the underlying Rails interface.
28
34
  kwargs = kwargs.merge(v)
29
35
  elsif v.is_a?(Symbol) || v.is_a?(String)
@@ -1,17 +1,18 @@
1
1
  class RESTFramework::BaseSerializer
2
- attr_reader :errors
3
-
4
2
  def initialize(object: nil, controller: nil, **kwargs)
5
3
  @object = object
6
4
  @controller = controller
7
5
  end
6
+
7
+ def serialize
8
+ raise NotImplementedError
9
+ end
8
10
  end
9
11
 
10
12
 
11
- # This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is an
12
- # `ActiveModel` method which converts objects to Ruby primitives (with the top-level being either
13
- # an array or a hash).
14
- class RESTFramework::NativeModelSerializer < RESTFramework::BaseSerializer
13
+ # This serializer uses `.serializable_hash` to convert objects to Ruby primitives (with the
14
+ # top-level being either an array or a hash).
15
+ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
15
16
  class_attribute :config
16
17
  class_attribute :singular_config
17
18
  class_attribute :plural_config
@@ -21,12 +22,19 @@ class RESTFramework::NativeModelSerializer < RESTFramework::BaseSerializer
21
22
  super(**kwargs)
22
23
 
23
24
  if many.nil?
24
- @many = @object.respond_to?(:count) ? @object.count : nil
25
+ # Determine if we are dealing with many objects or just one.
26
+ @many = @object.is_a?(Enumerable)
25
27
  else
26
28
  @many = many
27
29
  end
28
30
 
29
- @model = model || (@controller ? @controller.send(:get_model) : nil)
31
+ # Determine model either explicitly, or by inspecting @object or @controller.
32
+ @model = model
33
+ @model ||= @object.class if @object.is_a?(ActiveRecord::Base)
34
+ @model ||= @object[0].class if (
35
+ @many && @object.is_a?(Enumerable) && @object.is_a?(ActiveRecord::Base)
36
+ )
37
+ @model ||= @controller.send(:get_model) if @controller
30
38
  end
31
39
 
32
40
  # Get controller action, if possible.
@@ -34,8 +42,8 @@ class RESTFramework::NativeModelSerializer < RESTFramework::BaseSerializer
34
42
  return @controller&.action_name&.to_sym
35
43
  end
36
44
 
37
- # Get a locally defined configuration, if one is defined.
38
- def get_local_serializer_config
45
+ # Get a locally defined native serializer configuration, if one is defined.
46
+ def get_local_native_serializer_config
39
47
  action = self.get_action
40
48
 
41
49
  if action && self.action_config
@@ -45,43 +53,70 @@ class RESTFramework::NativeModelSerializer < RESTFramework::BaseSerializer
45
53
  return self.action_config[action] if self.action_config[action]
46
54
  end
47
55
 
48
- # No action_config, so try singular/plural config.
49
- return self.plural_config if @many && self.plural_config
50
- return self.singular_config if !@many && self.singular_config
56
+ # No action_config, so try singular/plural config if explicitly instructed to via @many.
57
+ return self.plural_config if @many == true && self.plural_config
58
+ return self.singular_config if @many == false && self.singular_config
51
59
 
52
- # Lastly, try returning the default config.
53
- return self.config
60
+ # Lastly, try returning the default config, or singular/plural config in that order.
61
+ return self.config || self.singular_config || self.plural_config
54
62
  end
55
63
 
56
- # Get a configuration passable to `as_json` for the object.
64
+ # Helper to get a native serializer configuration from the controller.
65
+ def get_controller_native_serializer_config
66
+ return nil unless @controller
67
+
68
+ if @many == true
69
+ controller_serializer = @controller.try(:native_serializer_plural_config)
70
+ elsif @many == false
71
+ controller_serializer = @controller.try(:native_serializer_singular_config)
72
+ end
73
+
74
+ return controller_serializer || @controller.try(:native_serializer_config)
75
+ end
76
+
77
+ # Get a configuration passable to `serializable_hash` for the object.
57
78
  def get_serializer_config
58
79
  # Return a locally defined serializer config if one is defined.
59
- if local_config = self.get_local_serializer_config
80
+ if local_config = self.get_local_native_serializer_config
60
81
  return local_config
61
82
  end
62
83
 
63
- # Return a serializer config if one is defined.
64
- if serializer_config = @controller.send(:get_native_serializer_config)
84
+ # Return a serializer config if one is defined on the controller.
85
+ if serializer_config = get_controller_native_serializer_config
65
86
  return serializer_config
66
87
  end
67
88
 
68
89
  # If the config wasn't determined, build a serializer config from model fields.
69
- fields = @controller.try(:get_fields) if @controller
70
- unless fields.blank?
71
- columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
90
+ fields = @controller.send(:get_fields) if @controller
91
+ if fields
92
+ if @model
93
+ columns, methods = fields.partition { |f| f.in?(@model.column_names) }
94
+ else
95
+ columns = fields
96
+ methods = []
97
+ end
98
+
72
99
  return {only: columns, methods: methods}
73
100
  end
74
101
 
102
+ # By default, pass an empty configuration, allowing the serialization of all columns.
75
103
  return {}
76
104
  end
77
105
 
78
106
  # Convert the object (record or recordset) to Ruby primitives.
79
107
  def serialize
80
108
  if @object
81
- @many = @object.respond_to?(:each) if @many.nil?
82
- return @object.as_json(self.get_serializer_config)
109
+ begin
110
+ if @object.is_a?(Enumerable)
111
+ return @object.map { |r| r.serializable_hash(self.get_serializer_config) }
112
+ end
113
+ return @object.serializable_hash(self.get_serializer_config)
114
+ rescue NoMethodError
115
+ end
83
116
  end
84
- return nil
117
+
118
+ # Raise an error if we cannot serialize the object.
119
+ raise RESTFramework::UnserializableError.new(@object)
85
120
  end
86
121
 
87
122
  # Allow a serializer instance to be used as a hash directly in a nested serializer config.
@@ -112,3 +147,19 @@ class RESTFramework::NativeModelSerializer < RESTFramework::BaseSerializer
112
147
  return @_nested_config[key] = value
113
148
  end
114
149
  end
150
+
151
+
152
+ # :nocov:
153
+ # Alias NativeModelSerializer -> NativeSerializer.
154
+ class RESTFramework::NativeModelSerializer < RESTFramework::NativeSerializer
155
+ def initialize(**kwargs)
156
+ super
157
+ ActiveSupport::Deprecation.warn(
158
+ <<~MSG.split("\n").join(' ')
159
+ RESTFramework::NativeModelSerializer is deprecated and will be removed in future versions of
160
+ REST Framework; you should use RESTFramework::NativeSerializer instead.
161
+ MSG
162
+ )
163
+ end
164
+ end
165
+ # :nocov:
@@ -2,19 +2,21 @@ module RESTFramework
2
2
  module Version
3
3
  @_version = nil
4
4
 
5
- def self.get_version
5
+ def self.get_version(skip_git: false)
6
6
  # Return cached @_version, if available.
7
7
  return @_version if @_version
8
8
 
9
9
  # First, attempt to get the version from git.
10
- begin
11
- version = `git describe 2>/dev/null`.strip
12
- raise "blank version" if version.nil? || version.match(/^\w*$/)
13
- # Check for local changes.
14
- changes = `git status --porcelain 2>/dev/null`
15
- version << '.localchanges' if changes.strip.length > 0
16
- return version
17
- rescue
10
+ unless skip_git
11
+ begin
12
+ version = `git describe 2>/dev/null`.strip
13
+ raise "blank version" if version.nil? || version.match(/^\w*$/)
14
+ # Check for local changes.
15
+ changes = `git status --porcelain 2>/dev/null`
16
+ version << '.localchanges' if changes.strip.length > 0
17
+ return version
18
+ rescue
19
+ end
18
20
  end
19
21
 
20
22
  # Git failed, so try to find a VERSION_STAMP.
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: 0.1.1
4
+ version: 0.2.3
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: 2021-02-28 00:00:00.000000000 Z
11
+ date: 2021-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -43,7 +43,10 @@ files:
43
43
  - lib/rest_framework/controller_mixins/base.rb
44
44
  - lib/rest_framework/controller_mixins/models.rb
45
45
  - lib/rest_framework/engine.rb
46
+ - lib/rest_framework/errors.rb
46
47
  - lib/rest_framework/filters.rb
48
+ - lib/rest_framework/generators.rb
49
+ - lib/rest_framework/generators/controller_generator.rb
47
50
  - lib/rest_framework/paginators.rb
48
51
  - lib/rest_framework/routers.rb
49
52
  - lib/rest_framework/serializers.rb