rest_framework 0.1.3 → 0.2.4

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: 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