rest_framework 0.2.4 → 0.3.5

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: 8d9ce4741f59e6f68bfa18bc2e6c11202c91fbe251eeaa5562f8f718e6e8afaa
4
- data.tar.gz: 652ad42a8d0bad99e552af9117112df707e67d4c1e6dd2dd983e7925933fcab6
3
+ metadata.gz: dc5065eb969e4616e6a0e22717a4398940b3696a43182d6e8b526e225f6c81f3
4
+ data.tar.gz: 3efcfdfa6abfe7343770c8241e83d34bc1e02bcbd145826bc8a9709514b8b5f7
5
5
  SHA512:
6
- metadata.gz: 26036468d40ccc77a203981ba442265ef82c7a991fee800ee3d7925d96d09a696f3db06930e8252c49cf63e86d2aee2fd121a735e5e06609585299ff984d6992
7
- data.tar.gz: 3c53d8eed996e380ab3390acfde7a3cef262d38a1355d0016aa6d8129b8e329c99003c0181ec1602f5bb1ba8869355e6b1eb0ff366d8b814ff83f6bb3c9cbf4d
6
+ metadata.gz: 170b7b9ebb18b9527d1e9216a0456af154284ab8935d1ddcc367815bf73c86379ae83340f6a259f6b4324d5c4cf32eae740a28210a1ce963427ef96a870ae016
7
+ data.tar.gz: a6148d463fa2eda9dc6f19dc89646a78b922a93bdcf6f147707343d218a9501e8bebe67d8182d75a946968d59b27a0f030d08349268297799bc4886b780f6aa4
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.5
@@ -79,8 +79,12 @@ module RESTFramework::BaseControllerMixin
79
79
 
80
80
  protected
81
81
 
82
- # Helper to get filtering backends with a sane default.
83
- # @return [RESTFramework::BaseFilter]
82
+ # Helper to get the configured serializer class.
83
+ def get_serializer_class
84
+ return self.class.serializer_class
85
+ end
86
+
87
+ # Helper to get filtering backends, defaulting to no backends.
84
88
  def get_filter_backends
85
89
  return self.class.filter_backends || []
86
90
  end
@@ -95,16 +99,10 @@ module RESTFramework::BaseControllerMixin
95
99
  return data
96
100
  end
97
101
 
98
- # Helper to get the configured serializer class.
99
- # @return [RESTFramework::BaseSerializer]
100
- def get_serializer_class
101
- return self.class.serializer_class
102
- end
103
-
104
102
  def record_invalid(e)
105
- return api_response(
106
- {message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
107
- )
103
+ return api_response({
104
+ message: "Record invalid.", exception: e, errors: e.record&.errors
105
+ }, status: 400)
108
106
  end
109
107
 
110
108
  def record_not_found(e)
@@ -112,11 +110,15 @@ module RESTFramework::BaseControllerMixin
112
110
  end
113
111
 
114
112
  def record_not_saved(e)
115
- return api_response({message: "Record not saved.", exception: e}, status: 406)
113
+ return api_response({
114
+ message: "Record not saved.", exception: e, errors: e.record&.errors
115
+ }, status: 406)
116
116
  end
117
117
 
118
118
  def record_not_destroyed(e)
119
- return api_response({message: "Record not destroyed.", exception: e}, status: 406)
119
+ return api_response({
120
+ message: "Record not destroyed.", exception: e, errors: e.record&.errors
121
+ }, status: 406)
120
122
  end
121
123
 
122
124
  # Helper for showing routes under a controller action, used for the browsable API.
@@ -180,9 +182,9 @@ module RESTFramework::BaseControllerMixin
180
182
  hkwargs = kwargs.merge(html_kwargs)
181
183
  begin
182
184
  render(**hkwargs)
183
- rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
185
+ rescue ActionView::MissingTemplate # fallback to rest_framework layout
184
186
  hkwargs[:layout] = "rest_framework"
185
- hkwargs[:template] = "rest_framework/default"
187
+ hkwargs[:html] = ''
186
188
  render(**hkwargs)
187
189
  end
188
190
  }
@@ -20,6 +20,10 @@ module RESTFramework::BaseModelControllerMixin
20
20
  fields: nil,
21
21
  action_fields: nil,
22
22
 
23
+ # Attributes for finding records.
24
+ find_by_fields: nil,
25
+ find_by_query_param: 'find_by',
26
+
23
27
  # Attributes for create/update parameters.
24
28
  allowed_parameters: nil,
25
29
  allowed_action_parameters: nil,
@@ -33,9 +37,14 @@ module RESTFramework::BaseModelControllerMixin
33
37
  filterset_fields: nil,
34
38
  ordering_fields: nil,
35
39
  ordering_query_param: 'ordering',
40
+ ordering_no_reorder: false,
41
+ search_fields: nil,
42
+ search_query_param: 'search',
43
+ search_ilike: false,
36
44
 
37
45
  # Other misc attributes.
38
- disable_creation_from_recordset: false, # Option to disable `recordset.create` behavior.
46
+ create_from_recordset: true, # Option for `recordset.create` vs `Model.create` behavior.
47
+ filter_recordset_before_find: true, # Option to control if filtering is done before find.
39
48
  }.each do |a, default|
40
49
  unless base.respond_to?(a)
41
50
  base.class_attribute(a)
@@ -60,11 +69,6 @@ module RESTFramework::BaseModelControllerMixin
60
69
  return (action_config[action] if action) || self.class.send(generic_config_key)
61
70
  end
62
71
 
63
- # Get a list of parameters allowed for the current action.
64
- def get_allowed_parameters
65
- return _get_specific_action_config(:allowed_action_parameters, :allowed_parameters)&.map(&:to_s)
66
- end
67
-
68
72
  # Get a list of fields for the current action.
69
73
  def get_fields
70
74
  return (
@@ -74,14 +78,37 @@ module RESTFramework::BaseModelControllerMixin
74
78
  )
75
79
  end
76
80
 
81
+ # Get a list of find_by fields for the current action.
82
+ def get_find_by_fields
83
+ return self.class.find_by_fields&.map(&:to_s) || self.get_fields
84
+ end
85
+
86
+ # Get a list of find_by fields for the current action.
87
+ def get_filterset_fields
88
+ return self.class.filterset_fields&.map(&:to_s) || self.get_fields
89
+ end
90
+
91
+ # Get a list of ordering fields for the current action.
92
+ def get_ordering_fields
93
+ return self.class.ordering_fields&.map(&:to_s) || self.get_fields
94
+ end
95
+
96
+ # Get a list of search fields for the current action.
97
+ def get_search_fields
98
+ return self.class.search_fields&.map(&:to_s) || self.get_fields
99
+ end
100
+
101
+ # Get a list of parameters allowed for the current action.
102
+ def get_allowed_parameters
103
+ return _get_specific_action_config(:allowed_action_parameters, :allowed_parameters)&.map(&:to_s)
104
+ end
105
+
77
106
  # Helper to get the configured serializer class, or `NativeSerializer` as a default.
78
- # @return [RESTFramework::BaseSerializer]
79
107
  def get_serializer_class
80
108
  return self.class.serializer_class || RESTFramework::NativeSerializer
81
109
  end
82
110
 
83
- # Get the list of filtering backends to use.
84
- # @return [RESTFramework::BaseFilter]
111
+ # Helper to get filtering backends, defaulting to using `ModelFilter` and `ModelOrderingFilter`.
85
112
  def get_filter_backends
86
113
  return self.class.filter_backends || [
87
114
  RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter
@@ -94,9 +121,7 @@ module RESTFramework::BaseModelControllerMixin
94
121
  fields = self.get_allowed_parameters || self.get_fields
95
122
 
96
123
  # Filter the request body.
97
- body_params = request.request_parameters.select { |p|
98
- fields.include?(p)
99
- }
124
+ body_params = request.request_parameters.select { |p| fields.include?(p) }
100
125
 
101
126
  # Add query params in place of missing body params, if configured.
102
127
  if self.class.accept_generic_params_as_body_params
@@ -113,9 +138,7 @@ module RESTFramework::BaseModelControllerMixin
113
138
  end
114
139
 
115
140
  # Filter fields in exclude_body_fields.
116
- (self.class.exclude_body_fields || []).each do |f|
117
- body_params.delete(f.to_s)
118
- end
141
+ (self.class.exclude_body_fields || []).each { |f| body_params.delete(f.to_s) }
119
142
 
120
143
  body_params
121
144
  end
@@ -123,14 +146,6 @@ module RESTFramework::BaseModelControllerMixin
123
146
  alias :get_create_params :get_body_params
124
147
  alias :get_update_params :get_body_params
125
148
 
126
- # Get a record by the primary key from the (non-filtered) recordset.
127
- def get_record
128
- if pk = params[self.get_model.primary_key]
129
- return self.get_recordset.find(pk)
130
- end
131
- return nil
132
- end
133
-
134
149
  # Get the model for this controller.
135
150
  def get_model(from_get_recordset: false)
136
151
  return @model if instance_variable_defined?(:@model) && @model
@@ -164,6 +179,29 @@ module RESTFramework::BaseModelControllerMixin
164
179
 
165
180
  return nil
166
181
  end
182
+
183
+ # Get a single record by primary key or another column, if allowed.
184
+ def get_record
185
+ recordset = self.get_recordset
186
+ find_by_fields = self.get_find_by_fields
187
+ find_by_key = self.get_model.primary_key
188
+
189
+ # Find by another column if it's permitted.
190
+ if find_by_query_param && params[find_by_query_param].in?(find_by_fields)
191
+ find_by_key = params[find_by_query_param]
192
+ end
193
+
194
+ # Filter recordset, if configured.
195
+ if self.filter_recordset_before_find
196
+ recordset = self.get_filtered_data(recordset)
197
+ end
198
+
199
+ # Return the record.
200
+ if find_by_value = params[:id] # Route key is always :id by Rails convention.
201
+ return self.get_recordset.find_by!(find_by_key => find_by_value)
202
+ end
203
+ return nil
204
+ end
167
205
  end
168
206
 
169
207
 
@@ -200,8 +238,8 @@ end
200
238
  # Mixin for creating records.
201
239
  module RESTFramework::CreateModelMixin
202
240
  def create
203
- if self.get_recordset.respond_to?(:create!) && !self.disable_creation_from_recordset
204
- # Create with any properties inherited from the recordset (like associations).
241
+ if self.get_recordset.respond_to?(:create!) && self.create_from_recordset
242
+ # Create with any properties inherited from the recordset.
205
243
  @record = self.get_recordset.create!(self.get_create_params)
206
244
  else
207
245
  # Otherwise, perform a "bare" create.
@@ -2,6 +2,7 @@
2
2
  class RESTFramework::Error < StandardError
3
3
  end
4
4
 
5
+
5
6
  class RESTFramework::NilPassedToAPIResponseError < RESTFramework::Error
6
7
  def message
7
8
  return <<~MSG.split("\n").join(' ')
@@ -14,6 +15,7 @@ class RESTFramework::NilPassedToAPIResponseError < RESTFramework::Error
14
15
  end
15
16
  end
16
17
 
18
+
17
19
  class RESTFramework::UnserializableError < RESTFramework::Error
18
20
  def initialize(object)
19
21
  @object = object
@@ -14,8 +14,8 @@ 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&.map(&:to_s) || @controller.send(:get_fields)
18
- return @controller.request.query_parameters.select { |p|
17
+ fields = @controller.send(:get_filterset_fields)
18
+ return @controller.request.query_parameters.select { |p, _|
19
19
  fields.include?(p)
20
20
  }.to_h.symbolize_keys # convert from HashWithIndifferentAccess to Hash w/keys
21
21
  end
@@ -36,17 +36,25 @@ end
36
36
  class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
37
37
  # Convert ordering string to an ordering configuration.
38
38
  def _get_ordering
39
- return nil unless @controller.class.ordering_query_param
40
-
39
+ return nil if @controller.class.ordering_query_param.blank?
40
+ ordering_fields = @controller.send(:get_ordering_fields)
41
41
  order_string = @controller.params[@controller.class.ordering_query_param]
42
+
42
43
  unless order_string.blank?
43
- return order_string.split(',').map { |field|
44
+ ordering = {}
45
+ order_string.split(',').each do |field|
44
46
  if field[0] == '-'
45
- [field[1..-1].to_sym, :desc]
47
+ column = field[1..-1]
48
+ direction = :desc
46
49
  else
47
- [field.to_sym, :asc]
50
+ column = field
51
+ direction = :asc
48
52
  end
49
- }.to_h
53
+ if column.in?(ordering_fields)
54
+ ordering[column.to_sym] = direction
55
+ end
56
+ end
57
+ return ordering
50
58
  end
51
59
 
52
60
  return nil
@@ -55,14 +63,31 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
55
63
  # Order data according to the request query parameters.
56
64
  def get_filtered_data(data)
57
65
  ordering = self._get_ordering
66
+ reorder = !@controller.send(:ordering_no_reorder)
67
+
58
68
  if ordering && !ordering.empty?
59
- return data.order(_get_ordering)
69
+ return data.send(reorder ? :reorder : :order, _get_ordering)
60
70
  end
71
+
61
72
  return data
62
73
  end
63
74
  end
64
75
 
65
76
 
66
- # TODO: implement searching within fields rather than exact match filtering (ModelFilter)
67
- # class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
68
- # end
77
+ # Multi-field text searching on models.
78
+ class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
79
+ # Filter data according to the request query parameters.
80
+ def get_filtered_data(data)
81
+ fields = @controller.send(:get_search_fields)
82
+ search = @controller.request.query_parameters[@controller.send(:search_query_param)]
83
+
84
+ # Ensure we use array conditions to prevent SQL injection.
85
+ unless search.blank?
86
+ return data.where(fields.map { |f|
87
+ "CAST(#{f} AS CHAR) #{@controller.send(:search_ilike) ? "ILIKE" : "LIKE"} ?"
88
+ }.join(' OR '), *(["%#{search}%"] * fields.length))
89
+ end
90
+
91
+ return data
92
+ end
93
+ end
@@ -83,6 +83,7 @@ class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
83
83
  return {
84
84
  count: @count,
85
85
  page: @page_number,
86
+ page_size: @page_size,
86
87
  total_pages: @total_pages,
87
88
  results: serialized_page,
88
89
  }
@@ -99,9 +99,11 @@ module ActionDispatch::Routing
99
99
  kwargs[:controller] = name unless kwargs[:controller]
100
100
 
101
101
  # determine plural/singular resource
102
- if kwargs.delete(:force_singular)
102
+ force_singular = kwargs.delete(:force_singular)
103
+ force_plural = kwargs.delete(:force_plural)
104
+ if force_singular
103
105
  singular = true
104
- elsif kwargs.delete(:force_plural)
106
+ elsif force_plural
105
107
  singular = false
106
108
  elsif !controller_class.singleton_controller.nil?
107
109
  singular = controller_class.singleton_controller
@@ -121,29 +121,21 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
121
121
 
122
122
  # Allow a serializer instance to be used as a hash directly in a nested serializer config.
123
123
  def [](key)
124
- unless instance_variable_defined?(:@_nested_config)
125
- @_nested_config = self.get_serializer_config
126
- end
124
+ @_nested_config ||= self.get_serializer_config
127
125
  return @_nested_config[key]
128
126
  end
129
127
  def []=(key, value)
130
- unless instance_variable_defined?(:@_nested_config)
131
- @_nested_config = self.get_serializer_config
132
- end
128
+ @_nested_config ||= self.get_serializer_config
133
129
  return @_nested_config[key] = value
134
130
  end
135
131
 
136
132
  # Allow a serializer class to be used as a hash directly in a nested serializer config.
137
133
  def self.[](key)
138
- unless instance_variable_defined?(:@_nested_config)
139
- @_nested_config = self.new.get_serializer_config
140
- end
134
+ @_nested_config ||= self.new.get_serializer_config
141
135
  return @_nested_config[key]
142
136
  end
143
137
  def self.[]=(key, value)
144
- unless instance_variable_defined?(:@_nested_config)
145
- @_nested_config = self.new.get_serializer_config
146
- end
138
+ @_nested_config ||= self.new.get_serializer_config
147
139
  return @_nested_config[key] = value
148
140
  end
149
141
  end
@@ -1,35 +1,32 @@
1
+ # Do not use Rails-specific helper methods here (e.g., `blank?`) so the module can run standalone.
1
2
  module RESTFramework
2
3
  module Version
3
- @_version = nil
4
+ VERSION_FILEPATH = File.expand_path("../../VERSION", __dir__)
4
5
 
5
6
  def self.get_version(skip_git: false)
6
- # Return cached @_version, if available.
7
- return @_version if @_version
8
-
9
7
  # First, attempt to get the version from git.
10
8
  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
9
+ version = `git describe --dirty --broken 2>/dev/null`&.strip
10
+ return version unless !version || version.empty?
20
11
  end
21
12
 
22
- # Git failed, so try to find a VERSION_STAMP.
13
+ # Git failed or was skipped, so try to find a VERSION file.
23
14
  begin
24
- version = File.read(File.expand_path("VERSION_STAMP", __dir__))
25
- unless version.nil? || version.match(/^\w*$/)
26
- return (@_version = version) # cache VERSION_STAMP content
27
- end
28
- rescue
15
+ version = File.read(VERSION_FILEPATH)&.strip
16
+ return version unless !version || version.blank?
17
+ rescue SystemCallError
29
18
  end
30
19
 
31
- # No VERSION_STAMP, so version is unknown.
32
- return '0.unknown'
20
+ # No VERSION file, so version is unknown.
21
+ return 'unknown'
22
+ end
23
+
24
+ def self.stamp_version
25
+ File.write(VERSION_FILEPATH, RESTFramework::VERSION)
26
+ end
27
+
28
+ def self.unstamp_version
29
+ File.delete(VERSION_FILEPATH) if File.exist?(VERSION_FILEPATH)
33
30
  end
34
31
  end
35
32
 
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.2.4
4
+ version: 0.3.5
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-29 00:00:00.000000000 Z
11
+ date: 2021-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -33,12 +33,11 @@ extra_rdoc_files: []
33
33
  files:
34
34
  - LICENSE
35
35
  - README.md
36
+ - VERSION
36
37
  - app/views/layouts/rest_framework.html.erb
37
38
  - app/views/rest_framework/_head.html.erb
38
39
  - app/views/rest_framework/_routes.html.erb
39
- - app/views/rest_framework/default.html.erb
40
40
  - lib/rest_framework.rb
41
- - lib/rest_framework/VERSION_STAMP
42
41
  - lib/rest_framework/controller_mixins.rb
43
42
  - lib/rest_framework/controller_mixins/base.rb
44
43
  - lib/rest_framework/controller_mixins/models.rb
@@ -73,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
72
  - !ruby/object:Gem::Version
74
73
  version: '0'
75
74
  requirements: []
76
- rubygems_version: 3.0.9
75
+ rubygems_version: 3.1.4
77
76
  signing_key:
78
77
  specification_version: 4
79
78
  summary: A framework for DRY RESTful APIs in Ruby on Rails.
File without changes
@@ -1 +0,0 @@
1
- 0.2.4