rest_framework 0.0.6 → 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/rest_framework.svg)](https://badge.fury.io/rb/rest_framework)
|
4
4
|
[![Build Status](https://travis-ci.org/gregschmit/rails-rest-framework.svg?branch=master)](https://travis-ci.org/gregschmit/rails-rest-framework)
|
5
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
|