rest_framework 0.3.7 → 0.5.0

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: dfa62b5fb49a4c30fabee7308e785d495f244b8aec21be6d5fc99bf0f2f8a3aa
4
- data.tar.gz: 65e70b6fd5eb3b1d06d54da6044035f1ca418f440b666e81bc7fee7d89ff0e7b
3
+ metadata.gz: 9791b0cbb0a806afa00955abf151c7d04bd1820bdbe189f237badc9782f87d6b
4
+ data.tar.gz: 69e29341f8804fac7894c0065b0f5535d88b0e8d60577bc77677fe13d633f2d5
5
5
  SHA512:
6
- metadata.gz: 2e92adef73b6fe78b9381480015c950b20e8a14e91a49f24b655e4f58a7ba1870e4d7d5b33a1983be514e50215a6c1a8e2197e1008faf4eae4b2da16072ddde0
7
- data.tar.gz: 27bc294410be0d235db8fccd20bc48b013a9814a0ac3b26fc01f5f7f46905738f6a65dd3fcdf4d54946b70cdeb9eb11c60b33139b169bd83ff8d4cbad76aaf60
6
+ metadata.gz: f04bf8059e765db9e13a9729db43c2b26a914035063fe728e9ad0bcb1ab968bb8939a4d278e239a39f64b671d183bfdd7a2487248495cd7e5bd26b93ac49a103
7
+ data.tar.gz: '0908d43c8b2188e4dde5920a84eb621c0dea2dc170891e314d77d95b823a32af6681ae9a6028277f32282b88be03f8e95da55ec310f75e3c89ec8c3a09934f09'
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021 Gregory N. Schmit
3
+ Copyright (c) 2022 Gregory N. Schmit
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.7
1
+ 0.5.0
@@ -29,14 +29,14 @@
29
29
  <ul class="nav nav-tabs">
30
30
  <% if @json_payload %>
31
31
  <li class="nav-item">
32
- <a class="nav-link active" href="#tab-json" data-toggle="tab" role="tab">
32
+ <a class="nav-link active" href="#tab-json" data-bs-toggle="tab" role="tab">
33
33
  .json
34
34
  </a>
35
35
  </li>
36
36
  <% end %>
37
37
  <% if @xml_payload %>
38
38
  <li class="nav-item">
39
- <a class="nav-link" href="#tab-xml" data-toggle="tab" role="tab">
39
+ <a class="nav-link" href="#tab-xml" data-bs-toggle="tab" role="tab">
40
40
  .xml
41
41
  </a>
42
42
  </li>
@@ -57,7 +57,7 @@
57
57
  </div>
58
58
  </div>
59
59
  <% end %>
60
- <% unless @routes.blank? %>
60
+ <% unless @route_groups.blank? %>
61
61
  <div class="row">
62
62
  <h2>Routes</h2>
63
63
  <%= render partial: 'rest_framework/routes' %>
@@ -1,11 +1,12 @@
1
1
  <meta charset="utf-8">
2
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
2
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3
3
  <%= csrf_meta_tags %>
4
4
  <%= csp_meta_tag rescue nil %>
5
5
 
6
- <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
7
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/styles/vs.min.css" integrity="sha512-aWjgJTbdG4imzxTxistV5TVNffcYGtIQQm2NBNahV6LmX14Xq9WwZTL1wPjaSglUuVzYgwrq+0EuI4+vKvQHHw==" crossorigin="anonymous" />
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous">
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/styles/vs.min.css" integrity="sha512-aWjgJTbdG4imzxTxistV5TVNffcYGtIQQm2NBNahV6LmX14Xq9WwZTL1wPjaSglUuVzYgwrq+0EuI4+vKvQHHw==" crossorigin="anonymous">
8
8
  <style>
9
+ /* Adjust headers to always take up their entire row, and tweak the sizing. */
9
10
  h1,h2,h3,h4,h5,h6 { width: 100%; font-weight: normal; }
10
11
  h1 { font-size: 2rem; }
11
12
  h2 { font-size: 1.7rem; }
@@ -13,11 +14,27 @@
13
14
  h4 { font-size: 1.3rem; }
14
15
  h5 { font-size: 1.1rem; }
15
16
  h6 { font-size: 1rem; }
17
+
18
+ /* Make route group expansion obvious to the user. */
19
+ .rrf-routes .rrf-route-group-header {
20
+ background-color: #f8f8f8;
21
+ }
22
+ .rrf-routes .rrf-route-group-header:hover {
23
+ background-color: #f0f0f0;
24
+ }
25
+ .rrf-routes .rrf-route-group-header td {
26
+ cursor: pointer;
27
+ }
28
+
29
+ /* Disable bootstrap's collapsing animation because in tables it causes delayed jerkiness. */
30
+ .rrf-routes .collapsing {
31
+ -webkit-transition: none;
32
+ transition: none;
33
+ display: none;
34
+ }
16
35
  </style>
17
36
 
18
- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
19
- <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
20
- <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
37
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ" crossorigin="anonymous"></script>
21
38
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/highlight.min.js" integrity="sha512-TDKKr+IvoqZnPzc3l35hdjpHD0m+b2EC2SrLEgKDRWpxf2rFCxemkgvJ5kfU48ip+Y+m2XVKyOCD85ybtlZDmw==" crossorigin="anonymous"></script>
22
39
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/languages/json.min.js" integrity="sha512-FoN8JE+WWCdIGXAIT8KQXwpiavz0Mvjtfk7Rku3MDUNO0BDCiRMXAsSX+e+COFyZTcDb9HDgP+pM2RX12d4j+A==" crossorigin="anonymous"></script>
23
40
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/languages/xml.min.js" integrity="sha512-dICltIgnUP+QSJrnYGCV8943p3qSDgvcg2NU4W8IcOZP4tdrvxlXjbhIznhtVQEcXow0mOjLM0Q6/NvZsmUH4g==" crossorigin="anonymous"></script>
@@ -0,0 +1,9 @@
1
+ <tr>
2
+ <td><%= route[:path] %></td>
3
+ <td><%= route[:verb] %></td>
4
+ <% if route[:controller] && route[:action] %>
5
+ <td><%= route[:controller] %>#<%= route[:action] %></td>
6
+ <% else %>
7
+ <td><%= route[:route_app] %></td>
8
+ <% end %>
9
+ </tr>
@@ -1,18 +1,28 @@
1
- <table class="table">
2
- <thead>
3
- <tr>
4
- <th scope="col">Verb</th>
5
- <th scope="col">Path</th>
6
- <th scope="col">Action</th>
7
- </tr>
8
- </thead>
9
- <tbody>
10
- <% @routes.each do |r| %>
11
- <tr>
12
- <td><%= r[:verb] %></td>
13
- <td><%= r[:path] %></td>
14
- <td><%= r[:action] %></td>
15
- </tr>
1
+ <div class="table-responsive">
2
+ <table class="table table-responsive rrf-routes">
3
+ <thead>
4
+ <tr>
5
+ <th scope="col">Path</th>
6
+ <th scope="col">Verb</th>
7
+ <th scope="col">Controller#Action</th>
8
+ </tr>
9
+ </thead>
10
+ <%# Render first group of routes directly. %>
11
+ <tbody>
12
+ <% @route_groups.values[0].each do |route| %>
13
+ <%= render partial: "rest_framework/route", locals: {route: route} %>
14
+ <% end %>
15
+ </tbody>
16
+ <%# Render any other groups under dropdowns. %>
17
+ <% @route_groups.drop(1).each_with_index do |(name, route_group), index| %>
18
+ <tr data-bs-toggle="collapse" data-bs-target="#route-group-<%= index %>" class="rrf-route-group-header">
19
+ <td colspan="3" class="text-center user-select-none"><%= name %></td>
20
+ </tr>
21
+ <tbody id="route-group-<%= index %>" class="collapse">
22
+ <% route_group.each do |route| %>
23
+ <%= render partial: "rest_framework/route", locals: {route: route} %>
24
+ <% end %>
25
+ </tbody>
16
26
  <% end %>
17
- </tbody>
18
- </table>
27
+ </table>
28
+ </div>
@@ -1,6 +1,6 @@
1
- require_relative '../errors'
2
- require_relative '../serializers'
3
-
1
+ require_relative "../errors"
2
+ require_relative "../serializers"
3
+ require_relative "../utils"
4
4
 
5
5
  # This module provides the common functionality for any controller mixins, a `root` action, and
6
6
  # the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
@@ -29,8 +29,8 @@ module RESTFramework::BaseControllerMixin
29
29
  end
30
30
 
31
31
  def self.included(base)
32
- if base.is_a? Class
33
- base.extend ClassMethods
32
+ if base.is_a?(Class)
33
+ base.extend(ClassMethods)
34
34
 
35
35
  # Add class attributes (with defaults) unless they already exist.
36
36
  {
@@ -42,8 +42,8 @@ module RESTFramework::BaseControllerMixin
42
42
  filter_backends: nil,
43
43
  paginator_class: nil,
44
44
  page_size: 20,
45
- page_query_param: 'page',
46
- page_size_query_param: 'page_size',
45
+ page_query_param: "page",
46
+ page_size_query_param: "page_size",
47
47
  max_page_size: nil,
48
48
  serializer_class: nil,
49
49
  serialize_to_json: true,
@@ -51,13 +51,13 @@ module RESTFramework::BaseControllerMixin
51
51
  singleton_controller: nil,
52
52
  skip_actions: nil,
53
53
  }.each do |a, default|
54
- unless base.respond_to?(a)
55
- base.class_attribute(a)
54
+ next if base.respond_to?(a)
56
55
 
57
- # Set default manually so we can still support Rails 4. Maybe later we can use the default
58
- # parameter on `class_attribute`.
59
- base.send(:"#{a}=", default)
60
- end
56
+ base.class_attribute(a)
57
+
58
+ # Set default manually so we can still support Rails 4. Maybe later we can use the default
59
+ # parameter on `class_attribute`.
60
+ base.send(:"#{a}=", default)
61
61
  end
62
62
 
63
63
  # Alias `extra_actions` to `extra_collection_actions`.
@@ -67,7 +67,11 @@ module RESTFramework::BaseControllerMixin
67
67
  end
68
68
 
69
69
  # Skip csrf since this is an API.
70
- base.skip_before_action(:verify_authenticity_token) rescue nil
70
+ begin
71
+ base.skip_before_action(:verify_authenticity_token)
72
+ rescue
73
+ nil
74
+ end
71
75
 
72
76
  # Handle some common exceptions.
73
77
  base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
@@ -81,7 +85,14 @@ module RESTFramework::BaseControllerMixin
81
85
 
82
86
  # Helper to get the configured serializer class.
83
87
  def get_serializer_class
84
- return self.class.serializer_class
88
+ return nil unless serializer_class = self.class.serializer_class
89
+
90
+ # Wrap it with an adapter if it's an active_model_serializer.
91
+ if defined?(ActiveModel::Serializer) && (serializer_class < ActiveModel::Serializer)
92
+ serializer_class = RESTFramework::ActiveModelSerializerAdapterFactory.for(serializer_class)
93
+ end
94
+
95
+ return serializer_class
85
96
  end
86
97
 
87
98
  # Helper to get filtering backends, defaulting to no backends.
@@ -100,9 +111,9 @@ module RESTFramework::BaseControllerMixin
100
111
  end
101
112
 
102
113
  def record_invalid(e)
103
- return api_response({
104
- message: "Record invalid.", exception: e, errors: e.record&.errors
105
- }, status: 400)
114
+ return api_response(
115
+ {message: "Record invalid.", exception: e, errors: e.record&.errors}, status: 400
116
+ )
106
117
  end
107
118
 
108
119
  def record_not_found(e)
@@ -110,31 +121,15 @@ module RESTFramework::BaseControllerMixin
110
121
  end
111
122
 
112
123
  def record_not_saved(e)
113
- return api_response({
114
- message: "Record not saved.", exception: e, errors: e.record&.errors
115
- }, status: 406)
124
+ return api_response(
125
+ {message: "Record not saved.", exception: e, errors: e.record&.errors}, status: 406
126
+ )
116
127
  end
117
128
 
118
129
  def record_not_destroyed(e)
119
- return api_response({
120
- message: "Record not destroyed.", exception: e, errors: e.record&.errors
121
- }, status: 406)
122
- end
123
-
124
- # Helper for showing routes under a controller action, used for the browsable API.
125
- def _get_routes
126
- begin
127
- formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
128
- rescue NameError
129
- # :nocov:
130
- formatter = ActionDispatch::Routing::ConsoleFormatter
131
- # :nocov:
132
- end
133
- return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
134
- formatter.new
135
- ).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
136
- {verb: r[0], path: r[1], action: r[2]}
137
- }.select { |r| r[:path].start_with?(request.path) }
130
+ return api_response(
131
+ {message: "Record not destroyed.", exception: e, errors: e.record&.errors}, status: 406
132
+ )
138
133
  end
139
134
 
140
135
  # Helper to render a browsable API for `html` format, along with basic `json`/`xml` formats, and
@@ -153,38 +148,38 @@ module RESTFramework::BaseControllerMixin
153
148
  end
154
149
 
155
150
  respond_to do |format|
156
- if payload == ''
157
- format.json {head :no_content} if self.serialize_to_json
158
- format.xml {head :no_content} if self.serialize_to_xml
151
+ if payload == ""
152
+ format.json { head(:no_content) } if self.class.serialize_to_json
153
+ format.xml { head(:no_content) } if self.class.serialize_to_xml
159
154
  else
160
155
  format.json {
161
156
  jkwargs = kwargs.merge(json_kwargs)
162
157
  render(json: payload, layout: false, **jkwargs)
163
- } if self.serialize_to_json
158
+ } if self.class.serialize_to_json
164
159
  format.xml {
165
160
  xkwargs = kwargs.merge(xml_kwargs)
166
161
  render(xml: payload, layout: false, **xkwargs)
167
- } if self.serialize_to_xml
162
+ } if self.class.serialize_to_xml
168
163
  # TODO: possibly support more formats here if supported?
169
164
  end
170
165
  format.html {
171
166
  @payload = payload
172
- if payload == ''
173
- @json_payload = '' if self.serialize_to_json
174
- @xml_payload = '' if self.serialize_to_xml
167
+ if payload == ""
168
+ @json_payload = "" if self.class.serialize_to_json
169
+ @xml_payload = "" if self.class.serialize_to_xml
175
170
  else
176
- @json_payload = payload.to_json if self.serialize_to_json
177
- @xml_payload = payload.to_xml if self.serialize_to_xml
171
+ @json_payload = payload.to_json if self.class.serialize_to_json
172
+ @xml_payload = payload.to_xml if self.class.serialize_to_xml
178
173
  end
179
174
  @template_logo_text ||= "Rails REST Framework"
180
175
  @title ||= self.controller_name.camelize
181
- @routes ||= self._get_routes
176
+ @route_groups ||= RESTFramework::Utils.get_routes(Rails.application.routes, request)
182
177
  hkwargs = kwargs.merge(html_kwargs)
183
178
  begin
184
179
  render(**hkwargs)
185
180
  rescue ActionView::MissingTemplate # fallback to rest_framework layout
186
181
  hkwargs[:layout] = "rest_framework"
187
- hkwargs[:html] = ''
182
+ hkwargs[:html] = ""
188
183
  render(**hkwargs)
189
184
  end
190
185
  }
@@ -1,13 +1,12 @@
1
- require_relative 'base'
2
- require_relative '../filters'
3
-
1
+ require_relative "base"
2
+ require_relative "../filters"
4
3
 
5
4
  # This module provides the core functionality for controllers based on models.
6
5
  module RESTFramework::BaseModelControllerMixin
7
6
  include RESTFramework::BaseControllerMixin
8
7
 
9
8
  def self.included(base)
10
- if base.is_a? Class
9
+ if base.is_a?(Class)
11
10
  RESTFramework::BaseControllerMixin.included(base)
12
11
 
13
12
  # Add class attributes (with defaults) unless they already exist.
@@ -22,7 +21,7 @@ module RESTFramework::BaseModelControllerMixin
22
21
 
23
22
  # Attributes for finding records.
24
23
  find_by_fields: nil,
25
- find_by_query_param: 'find_by',
24
+ find_by_query_param: "find_by",
26
25
 
27
26
  # Attributes for create/update parameters.
28
27
  allowed_parameters: nil,
@@ -32,27 +31,28 @@ module RESTFramework::BaseModelControllerMixin
32
31
  native_serializer_config: nil,
33
32
  native_serializer_singular_config: nil,
34
33
  native_serializer_plural_config: nil,
34
+ native_serializer_except_query_param: "except",
35
35
 
36
36
  # Attributes for default model filtering (and ordering).
37
37
  filterset_fields: nil,
38
38
  ordering_fields: nil,
39
- ordering_query_param: 'ordering',
39
+ ordering_query_param: "ordering",
40
40
  ordering_no_reorder: false,
41
41
  search_fields: nil,
42
- search_query_param: 'search',
42
+ search_query_param: "search",
43
43
  search_ilike: false,
44
44
 
45
45
  # Other misc attributes.
46
46
  create_from_recordset: true, # Option for `recordset.create` vs `Model.create` behavior.
47
47
  filter_recordset_before_find: true, # Option to control if filtering is done before find.
48
48
  }.each do |a, default|
49
- unless base.respond_to?(a)
50
- base.class_attribute(a)
49
+ next if base.respond_to?(a)
51
50
 
52
- # Set default manually so we can still support Rails 4. Maybe later we can use the default
53
- # parameter on `class_attribute`.
54
- base.send(:"#{a}=", default)
55
- end
51
+ base.class_attribute(a)
52
+
53
+ # Set default manually so we can still support Rails 4. Maybe later we can use the default
54
+ # parameter on `class_attribute`.
55
+ base.send(:"#{a}=", default)
56
56
  end
57
57
  end
58
58
  end
@@ -105,7 +105,7 @@ module RESTFramework::BaseModelControllerMixin
105
105
 
106
106
  # Helper to get the configured serializer class, or `NativeSerializer` as a default.
107
107
  def get_serializer_class
108
- return self.class.serializer_class || RESTFramework::NativeSerializer
108
+ return super || RESTFramework::NativeSerializer
109
109
  end
110
110
 
111
111
  # Helper to get filtering backends, defaulting to using `ModelFilter` and `ModelOrderingFilter`.
@@ -143,8 +143,8 @@ module RESTFramework::BaseModelControllerMixin
143
143
  body_params
144
144
  end
145
145
  end
146
- alias :get_create_params :get_body_params
147
- alias :get_update_params :get_body_params
146
+ alias_method :get_create_params, :get_body_params
147
+ alias_method :get_update_params, :get_body_params
148
148
 
149
149
  # Get the model for this controller.
150
150
  def get_model(from_get_recordset: false)
@@ -160,7 +160,7 @@ module RESTFramework::BaseModelControllerMixin
160
160
 
161
161
  # Try to determine model from controller name.
162
162
  begin
163
- return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
163
+ return @model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize
164
164
  rescue NameError
165
165
  end
166
166
 
@@ -196,48 +196,51 @@ module RESTFramework::BaseModelControllerMixin
196
196
  recordset = self.get_filtered_data(recordset)
197
197
  end
198
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
199
+ # Return the record. Route key is always :id by Rails convention.
200
+ return recordset.find_by!(find_by_key => params[:id])
204
201
  end
205
202
  end
206
203
 
207
-
208
204
  # Mixin for listing records.
209
205
  module RESTFramework::ListModelMixin
210
206
  def index
207
+ api_response(self._index)
208
+ end
209
+
210
+ def _index
211
211
  @records = self.get_filtered_data(self.get_recordset)
212
212
 
213
213
  # Handle pagination, if enabled.
214
214
  if self.class.paginator_class
215
215
  paginator = self.class.paginator_class.new(data: @records, controller: self)
216
216
  page = paginator.get_page
217
- serialized_page = self.get_serializer_class.new(object: page, controller: self).serialize
218
- data = paginator.get_paginated_response(serialized_page)
217
+ serialized_page = self.get_serializer_class.new(page, controller: self).serialize
218
+ return paginator.get_paginated_response(serialized_page)
219
219
  else
220
- data = self.get_serializer_class.new(object: @records, controller: self).serialize
220
+ return self.get_serializer_class.new(@records, controller: self).serialize
221
221
  end
222
-
223
- return api_response(data)
224
222
  end
225
223
  end
226
224
 
227
-
228
225
  # Mixin for showing records.
229
226
  module RESTFramework::ShowModelMixin
230
227
  def show
228
+ api_response(self._show)
229
+ end
230
+
231
+ def _show
231
232
  @record = self.get_record
232
- serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
233
- return api_response(serialized_record)
233
+ return self.get_serializer_class.new(@record, controller: self).serialize
234
234
  end
235
235
  end
236
236
 
237
-
238
237
  # Mixin for creating records.
239
238
  module RESTFramework::CreateModelMixin
240
239
  def create
240
+ api_response(self._create)
241
+ end
242
+
243
+ def _create
241
244
  if self.get_recordset.respond_to?(:create!) && self.create_from_recordset
242
245
  # Create with any properties inherited from the recordset.
243
246
  @record = self.get_recordset.create!(self.get_create_params)
@@ -245,39 +248,43 @@ module RESTFramework::CreateModelMixin
245
248
  # Otherwise, perform a "bare" create.
246
249
  @record = self.get_model.create!(self.get_create_params)
247
250
  end
248
- serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
249
- return api_response(serialized_record)
251
+
252
+ return self.get_serializer_class.new(@record, controller: self).serialize
250
253
  end
251
254
  end
252
255
 
253
-
254
256
  # Mixin for updating records.
255
257
  module RESTFramework::UpdateModelMixin
256
258
  def update
259
+ api_response(self._update)
260
+ end
261
+
262
+ def _update
257
263
  @record = self.get_record
258
264
  @record.update!(self.get_update_params)
259
- serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
260
- return api_response(serialized_record)
265
+ return self.get_serializer_class.new(@record, controller: self).serialize
261
266
  end
262
267
  end
263
268
 
264
-
265
269
  # Mixin for destroying records.
266
270
  module RESTFramework::DestroyModelMixin
267
271
  def destroy
272
+ self._destroy
273
+ api_response("")
274
+ end
275
+
276
+ def _destroy
268
277
  @record = self.get_record
269
278
  @record.destroy!
270
- api_response('')
271
279
  end
272
280
  end
273
281
 
274
-
275
282
  # Mixin that includes show/list mixins.
276
283
  module RESTFramework::ReadOnlyModelControllerMixin
277
284
  include RESTFramework::BaseModelControllerMixin
278
285
 
279
286
  def self.included(base)
280
- if base.is_a? Class
287
+ if base.is_a?(Class)
281
288
  RESTFramework::BaseModelControllerMixin.included(base)
282
289
  end
283
290
  end
@@ -286,13 +293,12 @@ module RESTFramework::ReadOnlyModelControllerMixin
286
293
  include RESTFramework::ShowModelMixin
287
294
  end
288
295
 
289
-
290
296
  # Mixin that includes all the CRUD mixins.
291
297
  module RESTFramework::ModelControllerMixin
292
298
  include RESTFramework::BaseModelControllerMixin
293
299
 
294
300
  def self.included(base)
295
- if base.is_a? Class
301
+ if base.is_a?(Class)
296
302
  RESTFramework::BaseModelControllerMixin.included(base)
297
303
  end
298
304
  end
@@ -1,6 +1,5 @@
1
1
  module RESTFramework::ControllerMixins
2
2
  end
3
3
 
4
-
5
- require_relative 'controller_mixins/base'
6
- require_relative 'controller_mixins/models'
4
+ require_relative "controller_mixins/base"
5
+ require_relative "controller_mixins/models"
@@ -2,10 +2,9 @@
2
2
  class RESTFramework::Error < StandardError
3
3
  end
4
4
 
5
-
6
5
  class RESTFramework::NilPassedToAPIResponseError < RESTFramework::Error
7
6
  def message
8
- return <<~MSG.split("\n").join(' ')
7
+ return <<~MSG.split("\n").join(" ")
9
8
  Payload of `nil` was passed to `api_response`; this is unsupported. If you want a blank
10
9
  response, pass `''` (an empty string) as the payload. If this was the result of a `find_by`
11
10
  (or similar Active Record method) not finding a record, you should use the bang version (e.g.,
@@ -8,7 +8,6 @@ class RESTFramework::BaseFilter
8
8
  end
9
9
  end
10
10
 
11
-
12
11
  # A simple filtering backend that supports filtering a recordset based on fields defined on the
13
12
  # controller class.
14
13
  class RESTFramework::ModelFilter < RESTFramework::BaseFilter
@@ -31,19 +30,19 @@ class RESTFramework::ModelFilter < RESTFramework::BaseFilter
31
30
  end
32
31
  end
33
32
 
34
-
35
33
  # A filter backend which handles ordering of the recordset.
36
34
  class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
37
35
  # Convert ordering string to an ordering configuration.
38
36
  def _get_ordering
39
37
  return nil if @controller.class.ordering_query_param.blank?
38
+
40
39
  ordering_fields = @controller.send(:get_ordering_fields)
41
40
  order_string = @controller.params[@controller.class.ordering_query_param]
42
41
 
43
42
  unless order_string.blank?
44
43
  ordering = {}
45
- order_string.split(',').each do |field|
46
- if field[0] == '-'
44
+ order_string.split(",").each do |field|
45
+ if field[0] == "-"
47
46
  column = field[1..-1]
48
47
  direction = :desc
49
48
  else
@@ -63,7 +62,7 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
63
62
  # Order data according to the request query parameters.
64
63
  def get_filtered_data(data)
65
64
  ordering = self._get_ordering
66
- reorder = !@controller.send(:ordering_no_reorder)
65
+ reorder = !@controller.class.ordering_no_reorder
67
66
 
68
67
  if ordering && !ordering.empty?
69
68
  return data.send(reorder ? :reorder : :order, _get_ordering)
@@ -73,19 +72,21 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
73
72
  end
74
73
  end
75
74
 
76
-
77
75
  # Multi-field text searching on models.
78
76
  class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
79
77
  # Filter data according to the request query parameters.
80
78
  def get_filtered_data(data)
81
79
  fields = @controller.send(:get_search_fields)
82
- search = @controller.request.query_parameters[@controller.send(:search_query_param)]
80
+ search = @controller.request.query_parameters[@controller.class.search_query_param]
83
81
 
84
82
  # Ensure we use array conditions to prevent SQL injection.
85
83
  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))
84
+ return data.where(
85
+ fields.map { |f|
86
+ "CAST(#{f} AS CHAR) #{@controller.class.search_ilike ? "ILIKE" : "LIKE"} ?"
87
+ }.join(" OR "),
88
+ *(["%#{search}%"] * fields.length),
89
+ )
89
90
  end
90
91
 
91
92
  return data
@@ -1,7 +1,6 @@
1
- require 'rails/generators'
1
+ require "rails/generators"
2
2
 
3
-
4
- # Some projects don't have the inflection "REST" as an acronym, so this is a helper class to prevent
3
+ # Most projects don't have the inflection "REST" as an acronym, so this is a helper class to prevent
5
4
  # this generator from being namespaced under `r_e_s_t_framework`.
6
5
  # :nocov:
7
6
  class RESTFrameworkCustomGeneratorControllerNamespace < String
@@ -11,18 +10,17 @@ class RESTFrameworkCustomGeneratorControllerNamespace < String
11
10
  end
12
11
  # :nocov:
13
12
 
14
-
15
13
  class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
16
- PATH_REGEX = /^\/*([a-z0-9_\/]*[a-z0-9_])(?:[\.a-z\/]*)$/
14
+ PATH_REGEX = %r{^[a-z0-9][a-z0-9_/]+$}
17
15
 
18
16
  desc <<~END
19
- Description:
17
+ Description:
20
18
  Generates a new REST Framework controller.
21
19
 
22
20
  Specify the controller as a path, including the module, if needed, like:
23
21
  'parent_module/controller_name'.
24
22
 
25
- Example:
23
+ Example:
26
24
  `rails generate rest_framework:controller user_api/groups`
27
25
 
28
26
  Generates a controller at `app/controllers/user_api/groups_controller.rb` named
@@ -31,10 +29,7 @@ class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
31
29
 
32
30
  argument :path, type: :string
33
31
  class_option(
34
- :parent_class,
35
- type: :string,
36
- default: 'ApplicationController',
37
- desc: "Inheritance parent",
32
+ :parent_class, type: :string, default: "ApplicationController", desc: "Inheritance parent"
38
33
  )
39
34
  class_option(
40
35
  :include_base,
@@ -50,11 +45,13 @@ class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
50
45
  end
51
46
 
52
47
  def create_rest_controller_file
53
- unless (path_match = PATH_REGEX.match(self.path))
54
- raise StandardError.new("Path isn't correct.")
48
+ unless PATH_REGEX.match?(self.path)
49
+ raise StandardError, "Path isn't valid."
55
50
  end
56
51
 
57
- cleaned_path = path_match[1]
52
+ # Remove '_controller' from end of path, if it exists.
53
+ cleaned_path = self.path.delete_suffix("_controller")
54
+
58
55
  content = <<~END
59
56
  class #{cleaned_path.camelize}Controller < #{options[:parent_class]}
60
57
  include RESTFramework::#{
@@ -62,6 +59,6 @@ class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
62
59
  }
63
60
  end
64
61
  END
65
- create_file("app/controllers/#{path}_controller.rb", content)
62
+ create_file("app/controllers/#{cleaned_path}_controller.rb", content)
66
63
  end
67
64
  end
@@ -1,5 +1,4 @@
1
1
  module RESTFramework::Generators
2
2
  end
3
3
 
4
-
5
- require_relative 'generators/controller_generator'
4
+ require_relative "generators/controller_generator"
@@ -15,7 +15,6 @@ class RESTFramework::BasePaginator
15
15
  end
16
16
  end
17
17
 
18
-
19
18
  # A simple paginator based on page numbers.
20
19
  #
21
20
  # Example: http://example.com/api/users/?page=3&page_size=50
@@ -26,7 +25,7 @@ class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
26
25
  @page_size = self._page_size
27
26
 
28
27
  @total_pages = @count / @page_size
29
- @total_pages += 1 if (@count % @page_size != 0)
28
+ @total_pages += 1 if @count % @page_size != 0
30
29
  end
31
30
 
32
31
  def _page_size
@@ -60,7 +59,7 @@ class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
60
59
  # Get the page and return it so the caller can serialize it.
61
60
  def get_page(page_number=nil)
62
61
  # If page number isn't provided, infer from the params or use 1 as a fallback value.
63
- if !page_number
62
+ unless page_number
64
63
  page_number = @controller&.params&.[](self._page_query_param)
65
64
  if page_number.blank?
66
65
  page_number = 1
@@ -90,7 +89,6 @@ class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
90
89
  end
91
90
  end
92
91
 
93
-
94
92
  # TODO: implement this
95
93
  # class RESTFramework::CountOffsetPaginator
96
94
  # end
@@ -1,46 +1,11 @@
1
- require 'action_dispatch/routing/mapper'
2
-
1
+ require "action_dispatch/routing/mapper"
2
+ require_relative "utils"
3
3
 
4
4
  module ActionDispatch::Routing
5
5
  class Mapper
6
- # Internal helper to take extra_actions hash and convert to a consistent format.
7
- protected def _parse_extra_actions(extra_actions)
8
- return (extra_actions || {}).map do |k,v|
9
- kwargs = {action: k}
10
- path = k
11
-
12
- # Convert structure to path/methods/kwargs.
13
- if v.is_a?(Hash) # allow kwargs
14
- v = v.symbolize_keys
15
-
16
- # Ensure methods is an array.
17
- if v[:methods].is_a?(String) || v[:methods].is_a?(Symbol)
18
- methods = [v.delete(:methods)]
19
- else
20
- methods = v.delete(:methods)
21
- end
22
-
23
- # Override path if it's provided.
24
- if v.key?(:path)
25
- path = v.delete(:path)
26
- end
27
-
28
- # Pass any further kwargs to the underlying Rails interface.
29
- kwargs = kwargs.merge(v)
30
- elsif v.is_a?(Symbol) || v.is_a?(String)
31
- methods = [v]
32
- else
33
- methods = v
34
- end
35
-
36
- # Return a hash with keys: :path, :methods, :kwargs.
37
- {path: path, methods: methods, kwargs: kwargs}
38
- end
39
- end
40
-
41
6
  # Internal interface to get the controller class from the name and current scope.
42
7
  protected def _get_controller_class(name, pluralize: true, fallback_reverse_pluralization: true)
43
- # get class name
8
+ # Get class name.
44
9
  name = name.to_s.camelize # camelize to leave plural names plural
45
10
  name = name.pluralize if pluralize
46
11
  if name == name.pluralize
@@ -51,14 +16,14 @@ module ActionDispatch::Routing
51
16
  name += "Controller"
52
17
  name_reverse += "Controller"
53
18
 
54
- # get scope for the class
19
+ # Get scope for the class.
55
20
  if @scope[:module]
56
21
  mod = @scope[:module].to_s.classify.constantize
57
22
  else
58
23
  mod = Object
59
24
  end
60
25
 
61
- # convert class name to class
26
+ # Convert class name to class.
62
27
  begin
63
28
  controller = mod.const_get(name)
64
29
  rescue NameError
@@ -92,13 +57,13 @@ module ActionDispatch::Routing
92
57
  if controller.is_a?(Class)
93
58
  controller_class = controller
94
59
  else
95
- controller_class = _get_controller_class(controller, pluralize: !default_singular)
60
+ controller_class = self._get_controller_class(controller, pluralize: !default_singular)
96
61
  end
97
62
 
98
63
  # Set controller if it's not explicitly set.
99
64
  kwargs[:controller] = name unless kwargs[:controller]
100
65
 
101
- # determine plural/singular resource
66
+ # Determine plural/singular resource.
102
67
  force_singular = kwargs.delete(:force_singular)
103
68
  force_plural = kwargs.delete(:force_plural)
104
69
  if force_singular
@@ -118,14 +83,16 @@ module ActionDispatch::Routing
118
83
  public_send(resource_method, name, except: skip, **kwargs) do
119
84
  if controller_class.respond_to?(:extra_member_actions)
120
85
  member do
121
- actions = self._parse_extra_actions(controller_class.extra_member_actions)
122
- _route_extra_actions(actions)
86
+ actions = RESTFramework::Utils.parse_extra_actions(
87
+ controller_class.extra_member_actions,
88
+ )
89
+ self._route_extra_actions(actions)
123
90
  end
124
91
  end
125
92
 
126
93
  collection do
127
- actions = self._parse_extra_actions(controller_class.extra_actions)
128
- _route_extra_actions(actions)
94
+ actions = RESTFramework::Utils.parse_extra_actions(controller_class.extra_actions)
95
+ self._route_extra_actions(actions)
129
96
  end
130
97
 
131
98
  yield if block_given?
@@ -160,14 +127,14 @@ module ActionDispatch::Routing
160
127
  kwargs[:controller] = name unless kwargs[:controller]
161
128
 
162
129
  # Route actions using the resourceful router, but skip all builtin actions.
163
- actions = self._parse_extra_actions(controller_class.extra_actions)
130
+ actions = RESTFramework::Utils.parse_extra_actions(controller_class.extra_actions)
164
131
  public_send(:resource, name, only: [], **kwargs) do
165
132
  # Route a root for this resource.
166
133
  if route_root_to
167
- get '', action: route_root_to
134
+ get("", action: route_root_to)
168
135
  end
169
136
 
170
- _route_extra_actions(actions, &block)
137
+ self._route_extra_actions(actions, &block)
171
138
  end
172
139
  end
173
140
 
@@ -179,13 +146,12 @@ module ActionDispatch::Routing
179
146
 
180
147
  # Remove path if name is nil (routing to the root of current namespace).
181
148
  unless name
182
- kwargs[:path] = ''
149
+ kwargs[:path] = ""
183
150
  end
184
151
 
185
152
  return rest_route(controller, route_root_to: root_action, **kwargs) do
186
153
  yield if block_given?
187
154
  end
188
155
  end
189
-
190
156
  end
191
157
  end
@@ -1,14 +1,23 @@
1
+ # The base serializer defines the interface for all REST Framework serializers.
1
2
  class RESTFramework::BaseSerializer
2
- def initialize(object: nil, controller: nil, **kwargs)
3
+ attr_accessor :object
4
+
5
+ def initialize(object=nil, controller: nil, **kwargs)
3
6
  @object = object
4
7
  @controller = controller
5
8
  end
6
9
 
7
- def serialize
10
+ # The primary interface for extracting a native Ruby types. This works both for records and
11
+ # collections.
12
+ def serialize(**kwargs)
8
13
  raise NotImplementedError
9
14
  end
10
- end
11
15
 
16
+ # Synonym for `serializable_hash` or compatibility with ActiveModelSerializers.
17
+ def serializable_hash(**kwargs)
18
+ return self.serialize(**kwargs)
19
+ end
20
+ end
12
21
 
13
22
  # This serializer uses `.serializable_hash` to convert objects to Ruby primitives (with the
14
23
  # top-level being either an array or a hash).
@@ -18,8 +27,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
18
27
  class_attribute :plural_config
19
28
  class_attribute :action_config
20
29
 
21
- def initialize(many: nil, model: nil, **kwargs)
22
- super(**kwargs)
30
+ def initialize(object=nil, many: nil, model: nil, **kwargs)
31
+ super(object, **kwargs)
23
32
 
24
33
  if many.nil?
25
34
  # Determine if we are dealing with many objects or just one.
@@ -31,9 +40,9 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
31
40
  # Determine model either explicitly, or by inspecting @object or @controller.
32
41
  @model = model
33
42
  @model ||= @object.class if @object.is_a?(ActiveRecord::Base)
34
- @model ||= @object[0].class if (
43
+ @model ||= @object[0].class if
35
44
  @many && @object.is_a?(Enumerable) && @object.is_a?(ActiveRecord::Base)
36
- )
45
+
37
46
  @model ||= @controller.send(:get_model) if @controller
38
47
  end
39
48
 
@@ -66,23 +75,66 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
66
75
  return nil unless @controller
67
76
 
68
77
  if @many == true
69
- controller_serializer = @controller.try(:native_serializer_plural_config)
78
+ controller_serializer = @controller.class.try(:native_serializer_plural_config)
70
79
  elsif @many == false
71
- controller_serializer = @controller.try(:native_serializer_singular_config)
80
+ controller_serializer = @controller.class.try(:native_serializer_singular_config)
72
81
  end
73
82
 
74
- return controller_serializer || @controller.try(:native_serializer_config)
83
+ return controller_serializer || @controller.class.try(:native_serializer_config)
75
84
  end
76
85
 
77
- # Get a configuration passable to `serializable_hash` for the object.
78
- def get_serializer_config
86
+ # Helper to filter (mutate) a single subconfig for specific keys.
87
+ def self.filter_subconfig(subconfig, except, additive: false)
88
+ return subconfig unless subconfig
89
+
90
+ if subconfig.is_a?(Array)
91
+ subconfig = subconfig.map(&:to_sym)
92
+ if additive
93
+ # Only add fields which are not already included.
94
+ subconfig += except - subconfig
95
+ else
96
+ subconfig -= except
97
+ end
98
+ elsif subconfig.is_a?(Hash)
99
+ subconfig.symbolize_keys!
100
+ subconfig.reject! { |k, _v| k.in?(except) }
101
+ end
102
+
103
+ return subconfig
104
+ end
105
+
106
+ # Helper to filter out configuration properties based on the :except query parameter.
107
+ def filter_except(config)
108
+ return config unless @controller
109
+
110
+ except_query_param = @controller.class.try(:native_serializer_except_query_param)
111
+ if except = @controller.request.query_parameters[except_query_param]
112
+ except = except.split(",").map(&:strip).map(&:to_sym)
113
+
114
+ unless except.empty?
115
+ # Duplicate the config to avoid mutating class state.
116
+ config = config.deep_dup
117
+
118
+ # Filter `only`, `except` (additive), `include`, and `methods`.
119
+ self.class.filter_subconfig(config[:only], except)
120
+ self.class.filter_subconfig(config[:except], except, additive: true)
121
+ self.class.filter_subconfig(config[:include], except)
122
+ self.class.filter_subconfig(config[:methods], except)
123
+ end
124
+ end
125
+
126
+ return config
127
+ end
128
+
129
+ # Get the raw serializer config.
130
+ def _get_raw_serializer_config
79
131
  # Return a locally defined serializer config if one is defined.
80
132
  if local_config = self.get_local_native_serializer_config
81
133
  return local_config
82
134
  end
83
135
 
84
136
  # Return a serializer config if one is defined on the controller.
85
- if serializer_config = get_controller_native_serializer_config
137
+ if serializer_config = self.get_controller_native_serializer_config
86
138
  return serializer_config
87
139
  end
88
140
 
@@ -103,13 +155,16 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
103
155
  return {}
104
156
  end
105
157
 
106
- # Convert the object (record or recordset) to Ruby primitives.
107
- def serialize
108
- raise "No object available to serialize!" unless @object
158
+ # Get a configuration passable to `serializable_hash` for the object, filtered if required.
159
+ def get_serializer_config
160
+ return filter_except(self._get_raw_serializer_config)
161
+ end
109
162
 
110
- if @object.is_a?(Enumerable)
163
+ def serialize(**kwargs)
164
+ if @object.respond_to?(:to_ary)
111
165
  return @object.map { |r| r.serializable_hash(self.get_serializer_config) }
112
166
  end
167
+
113
168
  return @object.serializable_hash(self.get_serializer_config)
114
169
  end
115
170
 
@@ -118,6 +173,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
118
173
  @_nested_config ||= self.get_serializer_config
119
174
  return @_nested_config[key]
120
175
  end
176
+
121
177
  def []=(key, value)
122
178
  @_nested_config ||= self.get_serializer_config
123
179
  return @_nested_config[key] = value
@@ -128,20 +184,20 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
128
184
  @_nested_config ||= self.new.get_serializer_config
129
185
  return @_nested_config[key]
130
186
  end
187
+
131
188
  def self.[]=(key, value)
132
189
  @_nested_config ||= self.new.get_serializer_config
133
190
  return @_nested_config[key] = value
134
191
  end
135
192
  end
136
193
 
137
-
138
194
  # :nocov:
139
195
  # Alias NativeModelSerializer -> NativeSerializer.
140
196
  class RESTFramework::NativeModelSerializer < RESTFramework::NativeSerializer
141
197
  def initialize(**kwargs)
142
198
  super
143
199
  ActiveSupport::Deprecation.warn(
144
- <<~MSG.split("\n").join(' ')
200
+ <<~MSG.split("\n").join(" "),
145
201
  RESTFramework::NativeModelSerializer is deprecated and will be removed in future versions of
146
202
  REST Framework; you should use RESTFramework::NativeSerializer instead.
147
203
  MSG
@@ -149,3 +205,19 @@ class RESTFramework::NativeModelSerializer < RESTFramework::NativeSerializer
149
205
  end
150
206
  end
151
207
  # :nocov:
208
+
209
+ # This is a helper factory to wrap an ActiveModelSerializer to provide a `serialize` method which
210
+ # accepts both collections and individual records. Use `.for` to build adapters.
211
+ class RESTFramework::ActiveModelSerializerAdapterFactory
212
+ def self.for(active_model_serializer)
213
+ return Class.new(active_model_serializer) do
214
+ def serialize
215
+ if self.object.respond_to?(:to_ary)
216
+ return self.object.map { |r| self.class.superclass.new(r).serializable_hash }
217
+ end
218
+
219
+ return self.serializable_hash
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,64 @@
1
+ module RESTFramework::Utils
2
+ # Helper to take extra_actions hash and convert to a consistent format:
3
+ # `{paths:, methods:, kwargs:}`.
4
+ def self.parse_extra_actions(extra_actions)
5
+ return (extra_actions || {}).map do |k, v|
6
+ kwargs = {action: k}
7
+ path = k
8
+
9
+ # Convert structure to path/methods/kwargs.
10
+ if v.is_a?(Hash) # allow kwargs
11
+ v = v.symbolize_keys
12
+
13
+ # Ensure methods is an array.
14
+ if v[:methods].is_a?(String) || v[:methods].is_a?(Symbol)
15
+ methods = [v.delete(:methods)]
16
+ else
17
+ methods = v.delete(:methods)
18
+ end
19
+
20
+ # Override path if it's provided.
21
+ if v.key?(:path)
22
+ path = v.delete(:path)
23
+ end
24
+
25
+ # Pass any further kwargs to the underlying Rails interface.
26
+ kwargs = kwargs.merge(v)
27
+ elsif v.is_a?(Symbol) || v.is_a?(String)
28
+ methods = [v]
29
+ else
30
+ methods = v
31
+ end
32
+
33
+ # Return a hash with keys: :path, :methods, :kwargs.
34
+ {path: path, methods: methods, kwargs: kwargs}
35
+ end
36
+ end
37
+
38
+ # Helper to get the current route pattern, stripped of the `(:format)` segment.
39
+ def self.get_route_pattern(application_routes, request)
40
+ application_routes.router.recognize(request) do |route, _, _|
41
+ return route.path.spec.to_s.gsub(/\(\.:format\)$/, "")
42
+ end
43
+ end
44
+
45
+ # Helper for showing routes under a controller action, used for the browsable API.
46
+ def self.get_routes(application_routes, request)
47
+ current_pattern = self.get_route_pattern(application_routes, request)
48
+ current_subdomain = request.subdomain.presence
49
+
50
+ # Return routes that match our current route subdomain/pattern, grouped by controller.
51
+ return application_routes.routes.map { |r|
52
+ {
53
+ verb: r.verb,
54
+ path: r.path.spec.to_s,
55
+ action: r.defaults[:action].presence,
56
+ controller: r.defaults[:controller].presence,
57
+ subdomain: r.defaults[:subdomain].presence,
58
+ route_app: r.app&.app&.inspect&.presence,
59
+ }
60
+ }.select { |r|
61
+ r[:subdomain] == current_subdomain && r[:path].start_with?(current_pattern)
62
+ }.group_by { |r| r[:controller] }
63
+ end
64
+ end
@@ -18,7 +18,7 @@ module RESTFramework
18
18
  end
19
19
 
20
20
  # No VERSION file, so version is unknown.
21
- return 'unknown'
21
+ return "unknown"
22
22
  end
23
23
 
24
24
  def self.stamp_version
@@ -30,5 +30,5 @@ module RESTFramework
30
30
  end
31
31
  end
32
32
 
33
- VERSION = Version.get_version()
33
+ VERSION = Version.get_version
34
34
  end
@@ -1,7 +1,6 @@
1
1
  module RESTFramework
2
2
  end
3
3
 
4
-
5
4
  require_relative "rest_framework/controller_mixins"
6
5
  require_relative "rest_framework/engine"
7
6
  require_relative "rest_framework/errors"
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.3.7
4
+ version: 0.5.0
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-08-25 00:00:00.000000000 Z
11
+ date: 2022-03-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -36,6 +36,7 @@ files:
36
36
  - VERSION
37
37
  - app/views/layouts/rest_framework.html.erb
38
38
  - app/views/rest_framework/_head.html.erb
39
+ - app/views/rest_framework/_route.html.erb
39
40
  - app/views/rest_framework/_routes.html.erb
40
41
  - lib/rest_framework.rb
41
42
  - lib/rest_framework/controller_mixins.rb
@@ -49,6 +50,7 @@ files:
49
50
  - lib/rest_framework/paginators.rb
50
51
  - lib/rest_framework/routers.rb
51
52
  - lib/rest_framework/serializers.rb
53
+ - lib/rest_framework/utils.rb
52
54
  - lib/rest_framework/version.rb
53
55
  homepage: https://rails-rest-framework.com
54
56
  licenses:
@@ -72,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
74
  - !ruby/object:Gem::Version
73
75
  version: '0'
74
76
  requirements: []
75
- rubygems_version: 3.1.4
77
+ rubygems_version: 3.2.22
76
78
  signing_key:
77
79
  specification_version: 4
78
80
  summary: A framework for DRY RESTful APIs in Ruby on Rails.