rest_framework 0.0.6 → 0.0.12
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 +4 -4
- data/README.md +34 -22
- data/app/views/layouts/rest_framework.html.erb +37 -6
- data/app/views/rest_framework/_head.html.erb +16 -11
- data/lib/rest_framework.rb +1 -1
- data/lib/rest_framework/VERSION_STAMP +1 -1
- data/lib/rest_framework/controller_mixins.rb +2 -0
- data/lib/rest_framework/controller_mixins/base.rb +129 -0
- data/lib/rest_framework/controller_mixins/models.rb +222 -0
- data/lib/rest_framework/routers.rb +25 -5
- data/lib/rest_framework/serializers.rb +108 -0
- data/lib/rest_framework/version.rb +3 -3
- metadata +6 -5
- data/lib/rest_framework/controllers.rb +0 -2
- data/lib/rest_framework/controllers/base.rb +0 -121
- data/lib/rest_framework/controllers/models.rb +0 -223
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c3691667e86ee77e9d15a1732d0013687a2e63a062a4ff01d1c6d8cc66f45fc
|
4
|
+
data.tar.gz: 6bcab926c283feb9a0643a06eab750f941882e5b56b294c59794f64a966883bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 204e0b5ea7b46710968a6274791f9e0f676db34f4eaefa1cf831fe8f9e4ab1f076a24b0375ef0554e1c4b57db3d1787adeebc31c4201da40b504aded51ca097b
|
7
|
+
data.tar.gz: 7e46ed5bbe61832045207664d679ecf394aead2928cdd78a1b29873087b78130546fd80595fe443680d5c26eebfa8bf5f23246b56a9a4a83e801c7763c024c76
|
data/README.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
# REST Framework
|
1
|
+
# Rails REST Framework
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/rest_framework)
|
4
4
|
[](https://travis-ci.org/gregschmit/rails-rest-framework)
|
5
5
|
|
6
|
-
REST Framework helps you build awesome APIs in Ruby on Rails.
|
6
|
+
Rails REST Framework helps you build awesome Web APIs in Ruby on Rails.
|
7
7
|
|
8
8
|
**The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD
|
9
9
|
logic, and routing them can be obnoxious.
|
@@ -11,6 +11,8 @@ logic, and routing them can be obnoxious.
|
|
11
11
|
**The Solution**: This gem handles the common logic so you can focus on the parts of your API which
|
12
12
|
make it unique.
|
13
13
|
|
14
|
+
To see detailed documentation, visit https://rails-rest-framework.com.
|
15
|
+
|
14
16
|
## Installation
|
15
17
|
|
16
18
|
Add this line to your application's Gemfile:
|
@@ -21,13 +23,17 @@ gem 'rest_framework'
|
|
21
23
|
|
22
24
|
And then execute:
|
23
25
|
|
24
|
-
|
26
|
+
```shell
|
27
|
+
$ bundle install
|
28
|
+
```
|
25
29
|
|
26
30
|
Or install it yourself with:
|
27
31
|
|
28
|
-
|
32
|
+
```shell
|
33
|
+
$ gem install rest_framework
|
34
|
+
```
|
29
35
|
|
30
|
-
## Usage
|
36
|
+
## Quick Usage Tutorial
|
31
37
|
|
32
38
|
### Controller Mixins
|
33
39
|
|
@@ -35,10 +41,10 @@ To transform a controller into a RESTful controller, you can either include `Bas
|
|
35
41
|
`ReadOnlyModelControllerMixin`, or `ModelControllerMixin`. `BaseControllerMixin` provides a `root`
|
36
42
|
action and a simple interface for routing arbitrary additional actions:
|
37
43
|
|
38
|
-
```
|
44
|
+
```ruby
|
39
45
|
class ApiController < ApplicationController
|
40
46
|
include RESTFramework::BaseControllerMixin
|
41
|
-
|
47
|
+
self.extra_actions = {test: [:get]}
|
42
48
|
|
43
49
|
def test
|
44
50
|
render inline: "Test successful!"
|
@@ -46,24 +52,24 @@ class ApiController < ApplicationController
|
|
46
52
|
end
|
47
53
|
```
|
48
54
|
|
49
|
-
`ModelControllerMixin` assists with providing the standard CRUD for your controller.
|
55
|
+
`ModelControllerMixin` assists with providing the standard model CRUD for your controller.
|
50
56
|
|
51
|
-
```
|
57
|
+
```ruby
|
52
58
|
class Api::MoviesController < ApiController
|
53
59
|
include RESTFramework::ModelControllerMixin
|
54
60
|
|
55
|
-
|
61
|
+
self.recordset = Movie.where(enabled: true)
|
56
62
|
end
|
57
63
|
```
|
58
64
|
|
59
65
|
`ReadOnlyModelControllerMixin` only enables list/show actions, but since we're naming this
|
60
66
|
controller in a way that doesn't make the model obvious, we can set that explicitly:
|
61
67
|
|
62
|
-
```
|
68
|
+
```ruby
|
63
69
|
class Api::ReadOnlyMoviesController < ApiController
|
64
70
|
include RESTFramework::ReadOnlyModelControllerMixin
|
65
71
|
|
66
|
-
|
72
|
+
self.model = Movie
|
67
73
|
end
|
68
74
|
```
|
69
75
|
|
@@ -73,28 +79,28 @@ behavior dynamically per-request.
|
|
73
79
|
### Routing
|
74
80
|
|
75
81
|
You can use Rails' `resource`/`resources` routers to route your API, however if you want
|
76
|
-
|
77
|
-
`rest_resource` / `rest_resources` routers
|
78
|
-
the root of your API:
|
82
|
+
`extra_actions` / `extra_member_actions` to be routed automatically, then you can use `rest_route`
|
83
|
+
for non-resourceful controllers, or `rest_resource` / `rest_resources` resourceful routers. You can
|
84
|
+
also use `rest_root` to route the root of your API:
|
79
85
|
|
80
|
-
```
|
86
|
+
```ruby
|
81
87
|
Rails.application.routes.draw do
|
82
88
|
rest_root :api # will find `api_controller` and route the `root` action to '/api'
|
83
89
|
namespace :api do
|
84
90
|
rest_resources :movies
|
85
|
-
rest_resources :
|
91
|
+
rest_resources :users
|
86
92
|
end
|
87
93
|
end
|
88
94
|
```
|
89
95
|
|
90
96
|
Or if you want the API root to be routed to `Api::RootController#root`:
|
91
97
|
|
92
|
-
```
|
98
|
+
```ruby
|
93
99
|
Rails.application.routes.draw do
|
94
100
|
namespace :api do
|
95
101
|
rest_root # will route `Api::RootController#root` to '/' in this namespace ('/api')
|
96
102
|
rest_resources :movies
|
97
|
-
rest_resources :
|
103
|
+
rest_resources :users
|
98
104
|
end
|
99
105
|
end
|
100
106
|
```
|
@@ -106,15 +112,21 @@ using RVM. Then run `bundle install` to install the appropriate gems.
|
|
106
112
|
|
107
113
|
To run the full test suite:
|
108
114
|
|
109
|
-
|
115
|
+
```shell
|
116
|
+
$ rake test
|
117
|
+
```
|
110
118
|
|
111
119
|
To run unit tests:
|
112
120
|
|
113
|
-
|
121
|
+
```shell
|
122
|
+
$ rake test:unit
|
123
|
+
```
|
114
124
|
|
115
125
|
To run integration tests:
|
116
126
|
|
117
|
-
|
127
|
+
```shell
|
128
|
+
$ rake test:integration
|
129
|
+
```
|
118
130
|
|
119
131
|
To interact with the integration app, you can `cd test/integration` and operate it via the normal
|
120
132
|
Rails interfaces. Ensure you run `rake db:schema:load` before running `rails server` or
|
@@ -11,7 +11,7 @@
|
|
11
11
|
<nav class="navbar navbar-dark bg-dark">
|
12
12
|
<div class="container">
|
13
13
|
<span class="navbar-brand p-0">
|
14
|
-
<h1 class="text-light font-weight-light m-0 p-0" style="font-size: 1em"><%= @template_logo_text %></h1>
|
14
|
+
<h1 class="text-light font-weight-light m-0 p-0" style="font-size: 1em"><%= @template_logo_text || 'Rails REST Framework' %></h1>
|
15
15
|
</span>
|
16
16
|
</div>
|
17
17
|
</nav>
|
@@ -19,13 +19,44 @@
|
|
19
19
|
<div class="container py-3">
|
20
20
|
<div class="container">
|
21
21
|
<div class="row">
|
22
|
-
<h1><%= @title %></h1>
|
22
|
+
<h1><%= (@header_text if defined? @header_text) || @title %></h1>
|
23
23
|
</div>
|
24
24
|
<hr/>
|
25
|
-
|
26
|
-
<
|
27
|
-
|
28
|
-
|
25
|
+
<% if @json_payload || @xml_payload %>
|
26
|
+
<div class="row">
|
27
|
+
<h2>Payload</h2>
|
28
|
+
<div class="w-100">
|
29
|
+
<ul class="nav nav-tabs">
|
30
|
+
<% if @json_payload %>
|
31
|
+
<li class="nav-item">
|
32
|
+
<a class="nav-link active" href="#tab-json" data-toggle="tab" role="tab">
|
33
|
+
JSON
|
34
|
+
</a>
|
35
|
+
</li>
|
36
|
+
<% end %>
|
37
|
+
<% if @xml_payload %>
|
38
|
+
<li class="nav-item">
|
39
|
+
<a class="nav-link" href="#tab-xml" data-toggle="tab" role="tab">
|
40
|
+
XML
|
41
|
+
</a>
|
42
|
+
</li>
|
43
|
+
<% end %>
|
44
|
+
</ul>
|
45
|
+
</div>
|
46
|
+
<div class="tab-content w-100 pt-3">
|
47
|
+
<div class="tab-pane fade show active" id="tab-json" role="tab">
|
48
|
+
<% if @json_payload %>
|
49
|
+
<div><pre><code class="language-json"><%= JSON.pretty_generate(JSON.parse(@json_payload)) unless @json_payload == '' %></code></pre></div>
|
50
|
+
<% end %>
|
51
|
+
</div>
|
52
|
+
<div class="tab-pane fade" id="tab-xml" role="tab">
|
53
|
+
<% if @xml_payload %>
|
54
|
+
<div><pre><code class="language-xml"><%= @xml_payload %></code></pre></div>
|
55
|
+
<% end %>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
<% end %>
|
29
60
|
<% unless @routes.blank? %>
|
30
61
|
<div class="row">
|
31
62
|
<h2>Routes</h2>
|
@@ -3,17 +3,22 @@
|
|
3
3
|
<%= csrf_meta_tags %>
|
4
4
|
<%= csp_meta_tag rescue nil %>
|
5
5
|
|
6
|
-
<link rel="stylesheet" href="https://
|
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" />
|
7
8
|
<style>
|
8
|
-
h1,h2,h3,h4,h5,h6 { width: 100%; }
|
9
|
-
h1 { font-size: 2rem; }
|
10
|
-
h2 { font-size: 1.7rem; }
|
11
|
-
h3 { font-size: 1.5rem; }
|
12
|
-
h4 { font-size: 1.3rem; }
|
13
|
-
h5 { font-size: 1.1rem; }
|
14
|
-
h6 { font-size: 1rem; }
|
9
|
+
h1,h2,h3,h4,h5,h6 { width: 100%; font-weight: normal; }
|
10
|
+
h1 { font-size: 2rem; }
|
11
|
+
h2 { font-size: 1.7rem; }
|
12
|
+
h3 { font-size: 1.5rem; }
|
13
|
+
h4 { font-size: 1.3rem; }
|
14
|
+
h5 { font-size: 1.1rem; }
|
15
|
+
h6 { font-size: 1rem; }
|
15
16
|
</style>
|
16
17
|
|
17
|
-
<script src="https://code.jquery.com/jquery-3.
|
18
|
-
<script src="https://
|
19
|
-
<script src="https://
|
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>
|
21
|
+
<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
|
+
<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
|
+
<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>
|
24
|
+
<script>hljs.initHighlightingOnLoad();</script>
|
data/lib/rest_framework.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.12
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module RESTFramework
|
2
|
+
|
3
|
+
# This module provides the common functionality for any controller mixins, a `root` action, and
|
4
|
+
# the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
|
5
|
+
# is defined.
|
6
|
+
module BaseControllerMixin
|
7
|
+
# Default action for API root.
|
8
|
+
def root
|
9
|
+
api_response({message: "This is the root of your awesome API!"})
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def get_skip_actions(skip_undefined: true)
|
14
|
+
# first, skip explicitly skipped actions
|
15
|
+
skip = self.skip_actions || []
|
16
|
+
|
17
|
+
# now add methods which don't exist, since we don't want to route those
|
18
|
+
if skip_undefined
|
19
|
+
[:index, :new, :create, :show, :edit, :update, :destroy].each do |a|
|
20
|
+
skip << a unless self.method_defined?(a)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
return skip
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.included(base)
|
29
|
+
if base.is_a? Class
|
30
|
+
base.extend ClassMethods
|
31
|
+
base.class_attribute(*[
|
32
|
+
:singleton_controller,
|
33
|
+
:extra_actions,
|
34
|
+
:skip_actions,
|
35
|
+
:paginator_class,
|
36
|
+
])
|
37
|
+
base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
|
38
|
+
base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
|
39
|
+
base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
|
40
|
+
base.rescue_from(ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def record_invalid(e)
|
47
|
+
return api_response(
|
48
|
+
{message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def record_not_found(e)
|
53
|
+
return api_response({message: "Record not found.", exception: e}, status: 404)
|
54
|
+
end
|
55
|
+
|
56
|
+
def record_not_saved(e)
|
57
|
+
return api_response({message: "Record not saved.", exception: e}, status: 406)
|
58
|
+
end
|
59
|
+
|
60
|
+
def record_not_destroyed(e)
|
61
|
+
return api_response({message: "Record not destroyed.", exception: e}, status: 406)
|
62
|
+
end
|
63
|
+
|
64
|
+
def _get_routes
|
65
|
+
begin
|
66
|
+
formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
|
67
|
+
rescue NameError
|
68
|
+
formatter = ActionDispatch::Routing::ConsoleFormatter
|
69
|
+
end
|
70
|
+
return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
|
71
|
+
formatter.new
|
72
|
+
).lines[1..].map { |r| r.split.last(3) }.map { |r|
|
73
|
+
{verb: r[0], path: r[1], action: r[2]}
|
74
|
+
}.select { |r| r[:path].start_with?(request.path) }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Helper alias for `respond_to`/`render`. `payload` should be already serialized to Ruby
|
78
|
+
# primitives.
|
79
|
+
def api_response(payload=nil, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
|
80
|
+
html_kwargs ||= {}
|
81
|
+
json_kwargs ||= {}
|
82
|
+
xml_kwargs ||= {}
|
83
|
+
|
84
|
+
# allow blank (no-content) responses
|
85
|
+
@blank = kwargs[:blank]
|
86
|
+
|
87
|
+
respond_to do |format|
|
88
|
+
if @blank
|
89
|
+
format.json {head :no_content}
|
90
|
+
format.xml {head :no_content}
|
91
|
+
else
|
92
|
+
if payload.respond_to?(:to_json)
|
93
|
+
format.json {
|
94
|
+
kwargs = kwargs.merge(json_kwargs)
|
95
|
+
render(json: payload, layout: false, **kwargs)
|
96
|
+
}
|
97
|
+
end
|
98
|
+
if payload.respond_to?(:to_xml)
|
99
|
+
format.xml {
|
100
|
+
kwargs = kwargs.merge(xml_kwargs)
|
101
|
+
render(xml: payload, layout: false, **kwargs)
|
102
|
+
}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
format.html {
|
106
|
+
@payload = payload
|
107
|
+
@json_payload = ''
|
108
|
+
@xml_payload = ''
|
109
|
+
unless @blank
|
110
|
+
@json_payload = payload.to_json if payload.respond_to?(:to_json)
|
111
|
+
@xml_payload = payload.to_xml if payload.respond_to?(:to_xml)
|
112
|
+
end
|
113
|
+
@template_logo_text ||= "Rails REST Framework"
|
114
|
+
@title ||= self.controller_name.camelize
|
115
|
+
@routes ||= self._get_routes
|
116
|
+
kwargs = kwargs.merge(html_kwargs)
|
117
|
+
begin
|
118
|
+
render(**kwargs)
|
119
|
+
rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
|
120
|
+
kwargs[:layout] = "rest_framework"
|
121
|
+
kwargs[:template] = "rest_framework/default"
|
122
|
+
end
|
123
|
+
render(**kwargs)
|
124
|
+
}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require_relative '../serializers'
|
3
|
+
|
4
|
+
module RESTFramework
|
5
|
+
|
6
|
+
module BaseModelControllerMixin
|
7
|
+
include BaseControllerMixin
|
8
|
+
def self.included(base)
|
9
|
+
if base.is_a? Class
|
10
|
+
BaseControllerMixin.included(base)
|
11
|
+
base.class_attribute(*[
|
12
|
+
:model,
|
13
|
+
:recordset,
|
14
|
+
:fields,
|
15
|
+
:action_fields,
|
16
|
+
:native_serializer_config,
|
17
|
+
:native_serializer_action_config,
|
18
|
+
:filterset_fields,
|
19
|
+
:allowed_parameters,
|
20
|
+
:allowed_action_parameters,
|
21
|
+
:serializer_class,
|
22
|
+
:extra_member_actions,
|
23
|
+
])
|
24
|
+
base.alias_method(:extra_collection_actions=, :extra_actions=)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def get_serializer_class
|
31
|
+
return self.class.serializer_class || NativeModelSerializer
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get a list of fields for the current action.
|
35
|
+
def get_fields
|
36
|
+
action_fields = self.class.action_fields || {}
|
37
|
+
action = self.action_name.to_sym
|
38
|
+
|
39
|
+
# index action should use :list fields if :index is not provided
|
40
|
+
action = :list if action == :index && !action_fields.key?(:index)
|
41
|
+
|
42
|
+
return (action_fields[action] if action) || self.class.fields || []
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get a native serializer config for the current action.
|
46
|
+
def get_native_serializer_config
|
47
|
+
action_serializer_config = self.class.native_serializer_action_config || {}
|
48
|
+
action = self.action_name.to_sym
|
49
|
+
|
50
|
+
# index action should use :list serializer config if :index is not provided
|
51
|
+
action = :list if action == :index && !action_serializer_config.key?(:index)
|
52
|
+
|
53
|
+
return (action_serializer_config[action] if action) || self.class.native_serializer_config
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get a list of parameters allowed for the current action.
|
57
|
+
def get_allowed_parameters
|
58
|
+
allowed_action_parameters = self.class.allowed_action_parameters || {}
|
59
|
+
action = self.action_name.to_sym
|
60
|
+
|
61
|
+
# index action should use :list allowed parameters if :index is not provided
|
62
|
+
action = :list if action == :index && !allowed_action_parameters.key?(:index)
|
63
|
+
|
64
|
+
return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
|
65
|
+
end
|
66
|
+
|
67
|
+
# Filter the request body for keys in current action's allowed_parameters/fields config.
|
68
|
+
def _get_parameter_values_from_request_body
|
69
|
+
fields = self.get_allowed_parameters || self.get_fields
|
70
|
+
return @_get_parameter_values_from_request_body ||= (request.request_parameters.select { |p|
|
71
|
+
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
72
|
+
})
|
73
|
+
end
|
74
|
+
alias :get_create_params :_get_parameter_values_from_request_body
|
75
|
+
alias :get_update_params :_get_parameter_values_from_request_body
|
76
|
+
|
77
|
+
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
78
|
+
def _get_filterset_values_from_params
|
79
|
+
fields = self.filterset_fields || self.get_fields
|
80
|
+
return @_get_filterset_values_from_params ||= request.query_parameters.select { |p|
|
81
|
+
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
82
|
+
}
|
83
|
+
end
|
84
|
+
alias :get_lookup_params :_get_filterset_values_from_params
|
85
|
+
alias :get_filter_params :_get_filterset_values_from_params
|
86
|
+
|
87
|
+
# Get the recordset, filtered by the filter params.
|
88
|
+
def get_filtered_recordset
|
89
|
+
filter_params = self.get_filter_params
|
90
|
+
unless filter_params.blank?
|
91
|
+
return self.get_recordset.where(**self.get_filter_params.to_hash.symbolize_keys)
|
92
|
+
end
|
93
|
+
return self.get_recordset
|
94
|
+
end
|
95
|
+
|
96
|
+
# Get a record by `id` or return a single record if recordset is filtered down to a single
|
97
|
+
# record.
|
98
|
+
def get_record
|
99
|
+
records = self.get_filtered_recordset
|
100
|
+
if params['id'] # direct lookup
|
101
|
+
return records.find(params['id'])
|
102
|
+
elsif records.length == 1
|
103
|
+
return records[0]
|
104
|
+
end
|
105
|
+
return nil
|
106
|
+
end
|
107
|
+
|
108
|
+
# Internal interface for get_model, protecting against infinite recursion with get_recordset.
|
109
|
+
def _get_model(from_internal_get_recordset: false)
|
110
|
+
return @model if instance_variable_defined?(:@model) && @model
|
111
|
+
return self.class.model if self.class.model
|
112
|
+
unless from_internal_get_recordset # prevent infinite recursion
|
113
|
+
recordset = self._get_recordset(from_internal_get_model: true)
|
114
|
+
return (@model = recordset.klass) if recordset
|
115
|
+
end
|
116
|
+
begin
|
117
|
+
return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
|
118
|
+
rescue NameError
|
119
|
+
end
|
120
|
+
return nil
|
121
|
+
end
|
122
|
+
|
123
|
+
# Internal interface for get_recordset, protecting against infinite recursion with get_model.
|
124
|
+
def _get_recordset(from_internal_get_model: false)
|
125
|
+
return @recordset if instance_variable_defined?(:@recordset) && @recordset
|
126
|
+
return self.class.recordset if self.class.recordset
|
127
|
+
unless from_internal_get_model # prevent infinite recursion
|
128
|
+
model = self._get_model(from_internal_get_recordset: true)
|
129
|
+
return (@recordset = model.all) if model
|
130
|
+
end
|
131
|
+
return nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# Get the model for this controller.
|
135
|
+
def get_model
|
136
|
+
return _get_model
|
137
|
+
end
|
138
|
+
|
139
|
+
# Get the set of records this controller has access to.
|
140
|
+
def get_recordset
|
141
|
+
return _get_recordset
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
module ListModelMixin
|
146
|
+
# TODO: pagination classes like Django
|
147
|
+
def index
|
148
|
+
@records = self.get_filtered_recordset
|
149
|
+
@serialized_records = self.get_serializer_class.new(
|
150
|
+
object: @records, controller: self
|
151
|
+
).serialize
|
152
|
+
return api_response(@serialized_records)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
module ShowModelMixin
|
157
|
+
def show
|
158
|
+
@record = self.get_record
|
159
|
+
@serialized_record = self.get_serializer_class.new(
|
160
|
+
object: @record, controller: self
|
161
|
+
).serialize
|
162
|
+
return api_response(@serialized_record)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
module CreateModelMixin
|
167
|
+
def create
|
168
|
+
@record = self.get_model.create!(self.get_create_params)
|
169
|
+
@serialized_record = self.get_serializer_class.new(
|
170
|
+
object: @record, controller: self
|
171
|
+
).serialize
|
172
|
+
return api_response(@serialized_record)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
module UpdateModelMixin
|
177
|
+
def update
|
178
|
+
@record = self.get_record
|
179
|
+
@record.update!(self.get_update_params)
|
180
|
+
@serialized_record = self.get_serializer_class.new(
|
181
|
+
object: @record, controller: self
|
182
|
+
).serialize
|
183
|
+
return api_response(@serialized_record)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
module DestroyModelMixin
|
188
|
+
def destroy
|
189
|
+
@record = self.get_record
|
190
|
+
@record.destroy!
|
191
|
+
api_response(nil)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
module ReadOnlyModelControllerMixin
|
196
|
+
include BaseModelControllerMixin
|
197
|
+
def self.included(base)
|
198
|
+
if base.is_a? Class
|
199
|
+
BaseModelControllerMixin.included(base)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
include ListModelMixin
|
204
|
+
include ShowModelMixin
|
205
|
+
end
|
206
|
+
|
207
|
+
module ModelControllerMixin
|
208
|
+
include BaseModelControllerMixin
|
209
|
+
def self.included(base)
|
210
|
+
if base.is_a? Class
|
211
|
+
BaseModelControllerMixin.included(base)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
include ListModelMixin
|
216
|
+
include ShowModelMixin
|
217
|
+
include CreateModelMixin
|
218
|
+
include UpdateModelMixin
|
219
|
+
include DestroyModelMixin
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'rails'
|
2
1
|
require 'action_dispatch/routing/mapper'
|
3
2
|
|
4
3
|
module ActionDispatch::Routing
|
@@ -67,11 +66,11 @@ module ActionDispatch::Routing
|
|
67
66
|
|
68
67
|
# call either `resource` or `resources`, passing appropriate modifiers
|
69
68
|
skip_undefined = kwargs.delete(:skip_undefined) || true
|
70
|
-
skip = controller_class.
|
69
|
+
skip = controller_class.get_skip_actions(skip_undefined: skip_undefined)
|
71
70
|
public_send(resource_method, name, except: skip, **kwargs) do
|
72
71
|
if controller_class.respond_to?(:extra_member_actions)
|
73
72
|
member do
|
74
|
-
actions = controller_class.extra_member_actions
|
73
|
+
actions = controller_class.extra_member_actions || {}
|
75
74
|
actions = actions.select { |k,v| controller_class.method_defined?(k) } if skip_undefined
|
76
75
|
actions.each do |action, methods|
|
77
76
|
methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
|
@@ -83,8 +82,9 @@ module ActionDispatch::Routing
|
|
83
82
|
end
|
84
83
|
|
85
84
|
collection do
|
86
|
-
actions = controller_class.extra_actions
|
85
|
+
actions = controller_class.extra_actions || {}
|
87
86
|
actions = actions.select { |k,v| controller_class.method_defined?(k) } if skip_undefined
|
87
|
+
actions.reject! { |k,v| skip.include? k }
|
88
88
|
actions.each do |action, methods|
|
89
89
|
methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
|
90
90
|
methods.each do |m|
|
@@ -111,6 +111,26 @@ module ActionDispatch::Routing
|
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
114
|
+
# Route a controller without the default resourceful paths.
|
115
|
+
def rest_route(path=nil, skip_undefined: true, **kwargs, &block)
|
116
|
+
controller = kwargs.delete(:controller) || path
|
117
|
+
path = path.to_s
|
118
|
+
|
119
|
+
# route actions
|
120
|
+
controller_class = self._get_controller_class(controller, pluralize: false)
|
121
|
+
skip = controller_class.get_skip_actions(skip_undefined: skip_undefined)
|
122
|
+
actions = controller_class.extra_actions || {}
|
123
|
+
actions = actions.select { |k,v| controller_class.method_defined?(k) } if skip_undefined
|
124
|
+
actions.reject! { |k,v| skip.include? k }
|
125
|
+
actions.each do |action, methods|
|
126
|
+
methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
|
127
|
+
methods.each do |m|
|
128
|
+
public_send(m, File.join(path, action.to_s), controller: controller, action: action)
|
129
|
+
end
|
130
|
+
yield if block_given?
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
114
134
|
# Route a controller's `#root` to '/' in the current scope/namespace, along with other actions.
|
115
135
|
# @param label [Symbol] the snake_case name of the controller
|
116
136
|
def rest_root(path=nil, **kwargs, &block)
|
@@ -124,7 +144,7 @@ module ActionDispatch::Routing
|
|
124
144
|
|
125
145
|
# route any additional actions
|
126
146
|
controller_class = self._get_controller_class(controller, pluralize: false)
|
127
|
-
controller_class.extra_actions.each do |action, methods|
|
147
|
+
(controller_class.extra_actions || {}).each do |action, methods|
|
128
148
|
methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
|
129
149
|
methods.each do |m|
|
130
150
|
public_send(m, File.join(path, action.to_s), controller: controller, action: action)
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module RESTFramework
|
2
|
+
class BaseSerializer
|
3
|
+
attr_reader :errors
|
4
|
+
|
5
|
+
def initialize(object: nil, data: nil, controller: nil, **kwargs)
|
6
|
+
@object = object
|
7
|
+
@data = data
|
8
|
+
@controller = controller
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is a Rails
|
13
|
+
# method which converts objects to Ruby primitives (with the top-level being either an array or a
|
14
|
+
# hash).
|
15
|
+
class NativeModelSerializer < BaseSerializer
|
16
|
+
class_attribute :config
|
17
|
+
class_attribute :singular_config
|
18
|
+
class_attribute :plural_config
|
19
|
+
class_attribute :action_config
|
20
|
+
|
21
|
+
def initialize(model: nil, many: nil, **kwargs)
|
22
|
+
super(**kwargs)
|
23
|
+
@many = many
|
24
|
+
@model = model || (@controller ? @controller.send(:get_model) : nil)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get controller action, if possible.
|
28
|
+
def get_action
|
29
|
+
return @controller&.action_name&.to_sym
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get a locally defined configuration, if one is defined.
|
33
|
+
def get_local_serializer_config
|
34
|
+
action = self.get_action
|
35
|
+
|
36
|
+
if action && self.action_config
|
37
|
+
# index action should use :list serializer config if :index is not provided
|
38
|
+
action = :list if action == :index && !self.action_config.key?(:index)
|
39
|
+
|
40
|
+
return self.action_config[action] if self.action_config[action]
|
41
|
+
end
|
42
|
+
|
43
|
+
# no action_config, so try singular/plural config
|
44
|
+
return self.plural_config if @many && self.plural_config
|
45
|
+
return self.singular_config if !@many && self.singular_config
|
46
|
+
|
47
|
+
# lastly, try the default config
|
48
|
+
return self.config
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get a configuration passable to `as_json` for the model.
|
52
|
+
def get_serializer_config
|
53
|
+
# return a locally defined serializer config if one is defined
|
54
|
+
local_config = self.get_local_serializer_config
|
55
|
+
return local_config if local_config
|
56
|
+
|
57
|
+
# return a serializer config if one is defined
|
58
|
+
serializer_config = @controller.send(:get_native_serializer_config)
|
59
|
+
return serializer_config if serializer_config
|
60
|
+
|
61
|
+
# otherwise, build a serializer config from fields
|
62
|
+
fields = @controller.send(:get_fields) if @controller
|
63
|
+
unless fields.blank?
|
64
|
+
columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
|
65
|
+
return {only: columns, methods: methods}
|
66
|
+
end
|
67
|
+
|
68
|
+
return {}
|
69
|
+
end
|
70
|
+
|
71
|
+
# Convert the object(s) to Ruby primitives.
|
72
|
+
def serialize
|
73
|
+
if @object
|
74
|
+
@many = @object.respond_to?(:each) if @many.nil?
|
75
|
+
return @object.as_json(self.get_serializer_config)
|
76
|
+
end
|
77
|
+
return nil
|
78
|
+
end
|
79
|
+
|
80
|
+
# Allow a serializer instance to be used as a hash directly in a nested serializer config.
|
81
|
+
def [](key)
|
82
|
+
unless instance_variable_defined?(:@_nested_config)
|
83
|
+
@_nested_config = self.get_serializer_config
|
84
|
+
end
|
85
|
+
return @_nested_config[key]
|
86
|
+
end
|
87
|
+
def []=(key, value)
|
88
|
+
unless instance_variable_defined?(:@_nested_config)
|
89
|
+
@_nested_config = self.get_serializer_config
|
90
|
+
end
|
91
|
+
return @_nested_config[key] = value
|
92
|
+
end
|
93
|
+
|
94
|
+
# Allow a serializer class to be used as a hash directly in a nested serializer config.
|
95
|
+
def self.[](key)
|
96
|
+
unless instance_variable_defined?(:@_nested_config)
|
97
|
+
@_nested_config = self.new.get_serializer_config
|
98
|
+
end
|
99
|
+
return @_nested_config[key]
|
100
|
+
end
|
101
|
+
def self.[]=(key, value)
|
102
|
+
unless instance_variable_defined?(:@_nested_config)
|
103
|
+
@_nested_config = self.new.get_serializer_config
|
104
|
+
end
|
105
|
+
return @_nested_config[key] = value
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -8,10 +8,10 @@ module RESTFramework
|
|
8
8
|
|
9
9
|
# First, attempt to get the version from git.
|
10
10
|
begin
|
11
|
-
version = `git describe`.strip
|
11
|
+
version = `git describe 2>/dev/null`.strip
|
12
12
|
raise "blank version" if version.nil? || version.match(/^\w*$/)
|
13
13
|
# Check for local changes.
|
14
|
-
changes = `git status --porcelain`
|
14
|
+
changes = `git status --porcelain 2>/dev/null`
|
15
15
|
version << '.localchanges' if changes.strip.length > 0
|
16
16
|
return version
|
17
17
|
rescue
|
@@ -21,7 +21,7 @@ module RESTFramework
|
|
21
21
|
begin
|
22
22
|
version = File.read(File.expand_path("VERSION_STAMP", __dir__))
|
23
23
|
unless version.nil? || version.match(/^\w*$/)
|
24
|
-
return (@_version = version) # cache VERSION_STAMP content
|
24
|
+
return (@_version = version) # cache VERSION_STAMP content
|
25
25
|
end
|
26
26
|
rescue
|
27
27
|
end
|
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.0.
|
4
|
+
version: 0.0.12
|
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: 2020-09
|
11
|
+
date: 2020-10-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -39,11 +39,12 @@ files:
|
|
39
39
|
- app/views/rest_framework/default.html.erb
|
40
40
|
- lib/rest_framework.rb
|
41
41
|
- lib/rest_framework/VERSION_STAMP
|
42
|
-
- lib/rest_framework/
|
43
|
-
- lib/rest_framework/
|
44
|
-
- lib/rest_framework/
|
42
|
+
- lib/rest_framework/controller_mixins.rb
|
43
|
+
- lib/rest_framework/controller_mixins/base.rb
|
44
|
+
- lib/rest_framework/controller_mixins/models.rb
|
45
45
|
- lib/rest_framework/engine.rb
|
46
46
|
- lib/rest_framework/routers.rb
|
47
|
+
- lib/rest_framework/serializers.rb
|
47
48
|
- lib/rest_framework/version.rb
|
48
49
|
homepage: https://github.com/gregschmit/rails-rest-framework
|
49
50
|
licenses:
|
@@ -1,121 +0,0 @@
|
|
1
|
-
module RESTFramework
|
2
|
-
|
3
|
-
# This module provides helpers for mixin `ClassMethods` submodules.
|
4
|
-
module ClassMethodHelpers
|
5
|
-
# This helper assists in providing reader interfaces for mixin properties.
|
6
|
-
def _restframework_attr_reader(property, default: nil)
|
7
|
-
method = <<~RUBY
|
8
|
-
def #{property}
|
9
|
-
return _restframework_try_class_level_variable_get(
|
10
|
-
#{property.inspect},
|
11
|
-
default: #{default.inspect},
|
12
|
-
)
|
13
|
-
end
|
14
|
-
RUBY
|
15
|
-
self.module_eval(method)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
# This module provides the common functionality for any controller mixins, a `root` action, and
|
20
|
-
# the ability to route arbitrary actions with `@extra_actions`. This is also where `api_response`
|
21
|
-
# is defined.
|
22
|
-
module BaseControllerMixin
|
23
|
-
# Default action for API root.
|
24
|
-
def root
|
25
|
-
api_response({message: "This is the root of your awesome API!"})
|
26
|
-
end
|
27
|
-
|
28
|
-
protected
|
29
|
-
|
30
|
-
module ClassMethods
|
31
|
-
extend ClassMethodHelpers
|
32
|
-
|
33
|
-
# Interface for getting class-level instance/class variables. Note: we check if they are
|
34
|
-
# defined first rather than rescuing NameError to prevent uninitialized variable warnings.
|
35
|
-
private def _restframework_try_class_level_variable_get(name, default: nil)
|
36
|
-
instance_variable = "@#{name}"
|
37
|
-
if self.instance_variable_defined?(instance_variable)
|
38
|
-
v = self.instance_variable_get(instance_variable)
|
39
|
-
return v unless v.nil?
|
40
|
-
end
|
41
|
-
class_variable = "@@#{name}"
|
42
|
-
if self.class_variable_defined?(class_variable)
|
43
|
-
v = self.class_variable_get(class_variable)
|
44
|
-
return v unless v.nil?
|
45
|
-
end
|
46
|
-
return default
|
47
|
-
end
|
48
|
-
|
49
|
-
_restframework_attr_reader(:singleton_controller)
|
50
|
-
_restframework_attr_reader(:extra_actions, default: {})
|
51
|
-
_restframework_attr_reader(:template_logo_text, default: 'Rails REST Framework')
|
52
|
-
|
53
|
-
def skip_actions(skip_undefined: true)
|
54
|
-
# first, skip explicitly skipped actions
|
55
|
-
skip = _restframework_try_class_level_variable_get(:skip_actions, default: [])
|
56
|
-
|
57
|
-
# now add methods which don't exist, since we don't want to route those
|
58
|
-
if skip_undefined
|
59
|
-
[:index, :new, :create, :show, :edit, :update, :destroy].each do |a|
|
60
|
-
skip << a unless self.method_defined?(a)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
return skip
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def self.included(base)
|
69
|
-
base.extend ClassMethods
|
70
|
-
end
|
71
|
-
|
72
|
-
def _get_routes
|
73
|
-
begin
|
74
|
-
formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
|
75
|
-
rescue NameError
|
76
|
-
formatter = ActionDispatch::Routing::ConsoleFormatter
|
77
|
-
end
|
78
|
-
return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
|
79
|
-
formatter.new
|
80
|
-
).lines[1..].map { |r| r.split.last(3) }.map { |r|
|
81
|
-
{verb: r[0], path: r[1], action: r[2]}
|
82
|
-
}.select { |r| r[:path].start_with?(request.path) }
|
83
|
-
end
|
84
|
-
|
85
|
-
# Helper alias for `respond_to`/`render`, and replace nil responses with blank ones.
|
86
|
-
def api_response(payload, html_kwargs: nil, json_kwargs: nil, **kwargs)
|
87
|
-
payload ||= '' # replace nil with ''
|
88
|
-
html_kwargs ||= {}
|
89
|
-
json_kwargs ||= {}
|
90
|
-
|
91
|
-
# serialize
|
92
|
-
if self.respond_to?(:get_model_serializer_config, true)
|
93
|
-
serialized_payload = payload.to_json(self.get_model_serializer_config)
|
94
|
-
else
|
95
|
-
serialized_payload = payload.to_json
|
96
|
-
end
|
97
|
-
|
98
|
-
respond_to do |format|
|
99
|
-
format.html {
|
100
|
-
kwargs = kwargs.merge(html_kwargs)
|
101
|
-
@template_logo_text ||= self.class.template_logo_text
|
102
|
-
@title ||= self.controller_name.camelize
|
103
|
-
@routes ||= self._get_routes
|
104
|
-
@payload = payload
|
105
|
-
@serialized_payload = serialized_payload
|
106
|
-
begin
|
107
|
-
render(**kwargs)
|
108
|
-
rescue ActionView::MissingTemplate # fallback to rest_framework default view
|
109
|
-
kwargs[:template] = "rest_framework/default"
|
110
|
-
end
|
111
|
-
render(**kwargs)
|
112
|
-
}
|
113
|
-
format.json {
|
114
|
-
kwargs = kwargs.merge(json_kwargs)
|
115
|
-
render(json: serialized_payload || '', **kwargs)
|
116
|
-
}
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
end
|
@@ -1,223 +0,0 @@
|
|
1
|
-
require_relative 'base'
|
2
|
-
|
3
|
-
module RESTFramework
|
4
|
-
|
5
|
-
module BaseModelControllerMixin
|
6
|
-
include BaseControllerMixin
|
7
|
-
|
8
|
-
# By default (and for now), we will just use `as_json`, but we should consider supporting:
|
9
|
-
# active_model_serializers (problem:
|
10
|
-
# https://github.com/rails-api/active_model_serializers#whats-happening-to-ams)
|
11
|
-
# fast_jsonapi (really good and fast serializers)
|
12
|
-
#@serializer
|
13
|
-
#@list_serializer
|
14
|
-
#@show_serializer
|
15
|
-
#@create_serializer
|
16
|
-
#@update_serializer
|
17
|
-
|
18
|
-
module ClassMethods
|
19
|
-
extend ClassMethodHelpers
|
20
|
-
include BaseControllerMixin::ClassMethods
|
21
|
-
|
22
|
-
_restframework_attr_reader(:model)
|
23
|
-
_restframework_attr_reader(:recordset)
|
24
|
-
|
25
|
-
_restframework_attr_reader(:fields)
|
26
|
-
_restframework_attr_reader(:list_fields)
|
27
|
-
_restframework_attr_reader(:show_fields)
|
28
|
-
_restframework_attr_reader(:create_fields)
|
29
|
-
_restframework_attr_reader(:update_fields)
|
30
|
-
|
31
|
-
_restframework_attr_reader(:extra_member_actions, default: {})
|
32
|
-
|
33
|
-
# For model-based mixins, `@extra_collection_actions` is synonymous with `@extra_actions`.
|
34
|
-
# @param skip_undefined [Boolean] whether we should skip routing undefined actions
|
35
|
-
def extra_actions(skip_undefined: true)
|
36
|
-
actions = (
|
37
|
-
_restframework_try_class_level_variable_get(:extra_collection_actions) ||
|
38
|
-
_restframework_try_class_level_variable_get(:extra_actions, default: {})
|
39
|
-
)
|
40
|
-
actions = actions.select { |a| self.method_defined?(a) } if skip_undefined
|
41
|
-
return actions
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.included(base)
|
46
|
-
base.extend ClassMethods
|
47
|
-
end
|
48
|
-
|
49
|
-
protected
|
50
|
-
|
51
|
-
# Get a list of fields for the current action.
|
52
|
-
def get_fields
|
53
|
-
return @fields if instance_variable_defined?(:@fields) && @fields
|
54
|
-
|
55
|
-
# index action should use list_fields
|
56
|
-
name = (action_name == 'index') ? 'list' : action_name
|
57
|
-
|
58
|
-
begin
|
59
|
-
@fields = self.class.send("#{name}_fields")
|
60
|
-
rescue NameError
|
61
|
-
end
|
62
|
-
|
63
|
-
return @fields ||= self.class.fields || []
|
64
|
-
end
|
65
|
-
|
66
|
-
# Get a configuration passable to `as_json` for the model.
|
67
|
-
def get_model_serializer_config
|
68
|
-
fields = self.get_fields
|
69
|
-
unless fields.blank?
|
70
|
-
columns, methods = fields.partition { |f| f.to_s.in?(self.get_model.column_names) }
|
71
|
-
return {only: columns, methods: methods}
|
72
|
-
end
|
73
|
-
return {}
|
74
|
-
end
|
75
|
-
|
76
|
-
# Filter the request body for keys allowed by the current action's field config.
|
77
|
-
def _get_field_values_from_request_body
|
78
|
-
return @_get_field_values_from_request_body ||= (request.request_parameters.select { |p|
|
79
|
-
self.get_fields.include?(p.to_sym) || self.get_fields.include?(p.to_s)
|
80
|
-
})
|
81
|
-
end
|
82
|
-
alias :get_create_params :_get_field_values_from_request_body
|
83
|
-
alias :get_update_params :_get_field_values_from_request_body
|
84
|
-
|
85
|
-
# Filter params for keys allowed by the current action's field config.
|
86
|
-
def _get_field_values_from_params
|
87
|
-
return @_get_field_values_from_params ||= params.permit(*self.get_fields)
|
88
|
-
end
|
89
|
-
alias :get_lookup_params :_get_field_values_from_params
|
90
|
-
alias :get_filter_params :_get_field_values_from_params
|
91
|
-
|
92
|
-
# Get the recordset, filtered by the filter params.
|
93
|
-
def get_filtered_recordset
|
94
|
-
filter_params = self.get_filter_params
|
95
|
-
unless filter_params.blank?
|
96
|
-
return self.get_recordset.where(**self.get_filter_params)
|
97
|
-
end
|
98
|
-
return self.get_recordset
|
99
|
-
end
|
100
|
-
|
101
|
-
# Get a record by `id` or return a single record if recordset is filtered down to a single record.
|
102
|
-
def get_record
|
103
|
-
records = self.get_filtered_recordset
|
104
|
-
if params['id'] # direct lookup
|
105
|
-
return records.find(params['id'])
|
106
|
-
elsif records.length == 1
|
107
|
-
return records[0]
|
108
|
-
end
|
109
|
-
return nil
|
110
|
-
end
|
111
|
-
|
112
|
-
# Internal interface for get_model, protecting against infinite recursion with get_recordset.
|
113
|
-
def _get_model(from_internal_get_recordset: false)
|
114
|
-
return @model if instance_variable_defined?(:@model) && @model
|
115
|
-
return self.class.model if self.class.model
|
116
|
-
unless from_internal_get_recordset # prevent infinite recursion
|
117
|
-
recordset = self._get_recordset(from_internal_get_model: true)
|
118
|
-
return (@model = recordset.klass) if recordset
|
119
|
-
end
|
120
|
-
begin
|
121
|
-
return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
|
122
|
-
rescue NameError
|
123
|
-
end
|
124
|
-
return nil
|
125
|
-
end
|
126
|
-
|
127
|
-
# Internal interface for get_recordset, protecting against infinite recursion with get_model.
|
128
|
-
def _get_recordset(from_internal_get_model: false)
|
129
|
-
return @recordset if instance_variable_defined?(:@recordset) && @recordset
|
130
|
-
return self.class.recordset if self.class.recordset
|
131
|
-
unless from_internal_get_model # prevent infinite recursion
|
132
|
-
model = self._get_model(from_internal_get_recordset: true)
|
133
|
-
return (@recordset = model.all) if model
|
134
|
-
end
|
135
|
-
return nil
|
136
|
-
end
|
137
|
-
|
138
|
-
# Get the model for this controller.
|
139
|
-
def get_model
|
140
|
-
return _get_model
|
141
|
-
end
|
142
|
-
|
143
|
-
# Get the base set of records this controller has access to.
|
144
|
-
def get_recordset
|
145
|
-
return _get_recordset
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
module ListModelMixin
|
150
|
-
# TODO: pagination classes like Django
|
151
|
-
def index
|
152
|
-
@records = self.get_filtered_recordset
|
153
|
-
api_response(@records)
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
module ShowModelMixin
|
158
|
-
def show
|
159
|
-
@record = self.get_record
|
160
|
-
api_response(@record)
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
module CreateModelMixin
|
165
|
-
def create
|
166
|
-
begin
|
167
|
-
@record = self.get_model.create!(self.get_create_params)
|
168
|
-
rescue ActiveRecord::RecordInvalid => e
|
169
|
-
api_response(e.record.messages, status: 400)
|
170
|
-
end
|
171
|
-
api_response(@record)
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
module UpdateModelMixin
|
176
|
-
def update
|
177
|
-
@record = self.get_record
|
178
|
-
if @record
|
179
|
-
@record.attributes(self.get_update_params)
|
180
|
-
@record.save!
|
181
|
-
api_response(@record)
|
182
|
-
else
|
183
|
-
api_response({detail: "Record not found."}, status: 404)
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
module DestroyModelMixin
|
189
|
-
def destroy
|
190
|
-
@record = self.get_record
|
191
|
-
if @record
|
192
|
-
@record.destroy!
|
193
|
-
api_response('')
|
194
|
-
else
|
195
|
-
api_response({detail: "Method 'DELETE' not allowed."}, status: 405)
|
196
|
-
end
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
module ReadOnlyModelControllerMixin
|
201
|
-
include BaseModelControllerMixin
|
202
|
-
def self.included(base)
|
203
|
-
base.extend BaseModelControllerMixin::ClassMethods
|
204
|
-
end
|
205
|
-
|
206
|
-
include ListModelMixin
|
207
|
-
include ShowModelMixin
|
208
|
-
end
|
209
|
-
|
210
|
-
module ModelControllerMixin
|
211
|
-
include BaseModelControllerMixin
|
212
|
-
def self.included(base)
|
213
|
-
base.extend BaseModelControllerMixin::ClassMethods
|
214
|
-
end
|
215
|
-
|
216
|
-
include ListModelMixin
|
217
|
-
include ShowModelMixin
|
218
|
-
include CreateModelMixin
|
219
|
-
include UpdateModelMixin
|
220
|
-
include DestroyModelMixin
|
221
|
-
end
|
222
|
-
|
223
|
-
end
|