rest_framework 0.1.3 → 0.2.4

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: 96d6394bfb8dee90ee31a05289a86db5409a118a3d456b9bf927397ceea055a7
4
- data.tar.gz: 6dc5181f02dafae6d0831a7802480375097836eb3e3e799c78076d0331a2f728
3
+ metadata.gz: 8d9ce4741f59e6f68bfa18bc2e6c11202c91fbe251eeaa5562f8f718e6e8afaa
4
+ data.tar.gz: 652ad42a8d0bad99e552af9117112df707e67d4c1e6dd2dd983e7925933fcab6
5
5
  SHA512:
6
- metadata.gz: e811d984f82150ad789d1e3437fc178095dcaffa8270b15b96dd375d7dbf649e0983f8001a4fe09c4ea4abe89ba8c8cad9e44ae25c21bbecb65adf5542efd8fc
7
- data.tar.gz: 1d64e212a2bb7331cba06020e9a9405b54ab558e691f4ef349ffc156a702f352209108fc62265b2ff8369a15656bdc4b158db13201b630d0ae3e2853e3b7c553
6
+ metadata.gz: 26036468d40ccc77a203981ba442265ef82c7a991fee800ee3d7925d96d09a696f3db06930e8252c49cf63e86d2aee2fd121a735e5e06609585299ff984d6992
7
+ data.tar.gz: 3c53d8eed996e380ab3390acfde7a3cef262d38a1355d0016aa6d8129b8e329c99003c0181ec1602f5bb1ba8869355e6b1eb0ff366d8b814ff83f6bb3c9cbf4d
data/README.md CHANGED
@@ -2,6 +2,8 @@
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)
6
+ [![Maintainability](https://api.codeclimate.com/v1/badges/ba5df7706cb544d78555/maintainability)](https://codeclimate.com/github/gregschmit/rails-rest-framework/maintainability)
5
7
 
6
8
  A framework for DRY RESTful APIs in Ruby on Rails.
7
9
 
@@ -78,8 +80,8 @@ class Api::ReadOnlyMoviesController < ApiController
78
80
  end
79
81
  ```
80
82
 
81
- Note that you can also override `get_model` and `get_recordset` instance methods to override the API
82
- behavior dynamically per-request.
83
+ Note that you can also override the `get_recordset` instance method to override the API behavior
84
+ dynamically per-request.
83
85
 
84
86
  ### Routing
85
87
 
@@ -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.3
1
+ 0.2.4
@@ -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::NativeSerializer]
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 `NativeSerializer` as a default.
68
- # @return [RESTFramework::BaseSerializer]
69
- def get_serializer_class
70
- return self.class.serializer_class || RESTFramework::NativeSerializer
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
128
  if pk = params[self.get_model.primary_key]
117
- return records.find(pk)
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
137
  return (@model = 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
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
151
+
134
152
  return nil
135
153
  end
136
154
 
137
- # Internal interface for get_recordset, protecting against infinite recursion with get_model.
138
- def _get_recordset(from_internal_get_model: false)
155
+ # Get the set of records this controller has access to.
156
+ def get_recordset
139
157
  return @recordset if instance_variable_defined?(:@recordset) && @recordset
140
158
  return (@recordset = 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
- return nil
146
- end
147
159
 
148
- # Get the model for this controller.
149
- def get_model
150
- return _get_model
151
- end
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
152
164
 
153
- # Get the set of records this controller has access to.
154
- def get_recordset
155
- return _get_recordset
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,11 +1,12 @@
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.
6
7
  protected def _parse_extra_actions(extra_actions)
7
8
  return (extra_actions || {}).map do |k,v|
8
- kwargs = {}
9
+ kwargs = {action: k}
9
10
  path = k
10
11
 
11
12
  # Convert structure to path/methods/kwargs.
@@ -71,6 +72,16 @@ module ActionDispatch::Routing
71
72
  return controller
72
73
  end
73
74
 
75
+ # Interal interface for routing extra actions.
76
+ protected def _route_extra_actions(actions, &block)
77
+ actions.each do |action_config|
78
+ action_config[:methods].each do |m|
79
+ public_send(m, action_config[:path], **action_config[:kwargs])
80
+ end
81
+ yield if block_given?
82
+ end
83
+ end
84
+
74
85
  # Internal core implementation of the `rest_resource(s)` router, both singular and plural.
75
86
  # @param default_singular [Boolean] the default plurality of the resource if the plurality is
76
87
  # not otherwise defined by the controller
@@ -99,28 +110,20 @@ module ActionDispatch::Routing
99
110
  end
100
111
  resource_method = singular ? :resource : :resources
101
112
 
102
- # call either `resource` or `resources`, passing appropriate modifiers
113
+ # Call either `resource` or `resources`, passing appropriate modifiers.
103
114
  skip_undefined = kwargs.delete(:skip_undefined) || true
104
115
  skip = controller_class.get_skip_actions(skip_undefined: skip_undefined)
105
116
  public_send(resource_method, name, except: skip, **kwargs) do
106
117
  if controller_class.respond_to?(:extra_member_actions)
107
118
  member do
108
119
  actions = self._parse_extra_actions(controller_class.extra_member_actions)
109
- actions.each do |action_config|
110
- action_config[:methods].each do |m|
111
- public_send(m, action_config[:path], **action_config[:kwargs])
112
- end
113
- end
120
+ _route_extra_actions(actions)
114
121
  end
115
122
  end
116
123
 
117
124
  collection do
118
125
  actions = self._parse_extra_actions(controller_class.extra_actions)
119
- actions.each do |action_config|
120
- action_config[:methods].each do |m|
121
- public_send(m, action_config[:path], **action_config[:kwargs])
122
- end
123
- end
126
+ _route_extra_actions(actions)
124
127
  end
125
128
 
126
129
  yield if block_given?
@@ -144,6 +147,7 @@ module ActionDispatch::Routing
144
147
  # Route a controller without the default resourceful paths.
145
148
  def rest_route(name=nil, **kwargs, &block)
146
149
  controller = kwargs.delete(:controller) || name
150
+ route_root_to = kwargs.delete(:route_root_to)
147
151
  if controller.is_a?(Class)
148
152
  controller_class = controller
149
153
  else
@@ -156,42 +160,27 @@ module ActionDispatch::Routing
156
160
  # Route actions using the resourceful router, but skip all builtin actions.
157
161
  actions = self._parse_extra_actions(controller_class.extra_actions)
158
162
  public_send(:resource, name, only: [], **kwargs) do
159
- actions.each do |action_config|
160
- action_config[:methods].each do |m|
161
- public_send(m, action_config[:path], **action_config[:kwargs])
162
- end
163
- yield if block_given?
163
+ # Route a root for this resource.
164
+ if route_root_to
165
+ get '', action: route_root_to
164
166
  end
167
+
168
+ _route_extra_actions(actions, &block)
165
169
  end
166
170
  end
167
171
 
168
172
  # Route a controller's `#root` to '/' in the current scope/namespace, along with other actions.
169
- # @param name [Symbol] the snake_case name of the controller
170
173
  def rest_root(name=nil, **kwargs, &block)
171
174
  # By default, use RootController#root.
172
175
  root_action = kwargs.delete(:action) || :root
173
176
  controller = kwargs.delete(:controller) || name || :root
174
177
 
175
- # Route the root.
176
- get name.to_s, controller: controller, action: root_action
177
-
178
- # Route any additional actions.
179
- controller_class = self._get_controller_class(controller, pluralize: false)
180
- actions = self._parse_extra_actions(controller_class.extra_actions)
181
- actions.each do |action_config|
182
- # Add :action unless kwargs defines it.
183
- unless action_config[:kwargs].key?(:action)
184
- action_config[:kwargs][:action] = action_config[:path]
185
- end
178
+ # Remove path if name is nil (routing to the root of current namespace).
179
+ unless name
180
+ kwargs[:path] = ''
181
+ end
186
182
 
187
- action_config[:methods].each do |m|
188
- public_send(
189
- m,
190
- File.join(name.to_s, action_config[:path].to_s),
191
- controller: controller,
192
- **action_config[:kwargs],
193
- )
194
- end
183
+ return rest_route(controller, route_root_to: root_action, **kwargs) do
195
184
  yield if block_given?
196
185
  end
197
186
  end
@@ -1,16 +1,17 @@
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).
13
+ # This serializer uses `.serializable_hash` to convert objects to Ruby primitives (with the
14
+ # top-level being either an array or a hash).
14
15
  class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
15
16
  class_attribute :config
16
17
  class_attribute :singular_config
@@ -21,12 +22,19 @@ class RESTFramework::NativeSerializer < 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::NativeSerializer < 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::NativeSerializer < 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.
@@ -113,6 +148,18 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
113
148
  end
114
149
  end
115
150
 
151
+
152
+ # :nocov:
116
153
  # Alias NativeModelSerializer -> NativeSerializer.
117
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
118
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.3
4
+ version: 0.2.4
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-03-07 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