rest_framework 0.0.3 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 573511c8fd193f39ad79529ae7af61b890536bc1ba8d033a1b7e1d9ee8825ed7
4
- data.tar.gz: a46c43aaf66552a5fe5ff4f50d214dda32ce7777e1994fbe5d0fdcf8018b279f
3
+ metadata.gz: ce317c05e27ab6421b18f37595dbb846a747c8616d19fc9a1927a9ecc30fefc7
4
+ data.tar.gz: b8f88631bfca835f395677af0e0c0079b40c079aa8d9a6cc8c8ee46dc5d589c2
5
5
  SHA512:
6
- metadata.gz: 626f03645ce25dbd70fd51c19750edd846e864ac16037ba07f9e93d9525c672f14a1c5b2b8a49db24242117e538d87ce33363a7e0649a9dc3e49eedd4dfd1f69
7
- data.tar.gz: ed442d6cf44fff1e36e211c69279372efd5eebf4a74d1ab4721ef0bc6f10bb5a81a7252b857de92224c547848e9236aa9b56e28b33d6589748ce938f4b143e47
6
+ metadata.gz: abd9fc1e83d61c60af77d834d3a3e03a4a904863f3ad4bb851d8cf35a5a563a322eef508a273764ce727303e3e25f526cca03a643127957a7d9952e4988de743
7
+ data.tar.gz: 8aadf4ba5467c645da2404df61880bd741a491eea0c14a09b20e7056726022f7fc7bd4b8a3f90e9ac068b578e63870c85cfaa813296245cf8c4fddfb7d002a66
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
- $ bundle install
26
+ ```shell
27
+ $ bundle install
28
+ ```
25
29
 
26
30
  Or install it yourself with:
27
31
 
28
- $ gem install rest_framework
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
- @extra_actions = {test: [:get]}
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
- @recordset = Movie.where(enabled: true) # by default, @recordset would include all movies
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
- @model = Movie
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
- `@extra_actions` / `@extra_member_actions` to be routed automatically, then you can use the
77
- `rest_resource` / `rest_resources` routers provided by this gem. You can also use `rest_root` to route
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 :read_only_movies
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 :read_only_movies
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
- $ rake test
115
+ ```shell
116
+ $ rake test
117
+ ```
110
118
 
111
119
  To run unit tests:
112
120
 
113
- $ rake test:unit
121
+ ```shell
122
+ $ rake test:unit
123
+ ```
114
124
 
115
125
  To run integration tests:
116
126
 
117
- $ rake test:integration
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>
@@ -22,10 +22,41 @@
22
22
  <h1><%= @title %></h1>
23
23
  </div>
24
24
  <hr/>
25
- <div class="row">
26
- <h2>Payload</h2><br>
27
- <div><pre><%= JSON.pretty_generate(JSON.parse(@serialized_payload)) %></pre></div>
28
- </div>
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)) %></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>
@@ -2,19 +2,23 @@
2
2
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
3
3
  <%= csrf_meta_tags %>
4
4
  <%= csp_meta_tag rescue nil %>
5
- <%= favicon_link_tag 'rest_framework_favicon.ico' rescue nil %>
6
5
 
7
- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
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" />
8
8
  <style>
9
- h1,h2,h3,h4,h5,h6 { width: 100%; }
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; }
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; }
16
16
  </style>
17
17
 
18
- <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
19
- <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
20
- <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
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>
@@ -1,7 +1,7 @@
1
1
  module RESTFramework
2
2
  end
3
3
 
4
- require_relative "rest_framework/controllers"
4
+ require_relative "rest_framework/controller_mixins"
5
5
  require_relative "rest_framework/engine"
6
6
  require_relative "rest_framework/routers"
7
7
  require_relative "rest_framework/version"
@@ -1 +1 @@
1
- 0.0.3
1
+ 0.0.9
@@ -0,0 +1,2 @@
1
+ require_relative 'controller_mixins/base'
2
+ require_relative 'controller_mixins/models'
@@ -0,0 +1,122 @@
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`, and replace nil responses with blank ones. `payload`
78
+ # must be already serialized to Ruby primitives.
79
+ def api_response(payload, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
80
+ html_kwargs ||= {}
81
+ json_kwargs ||= {}
82
+ xml_kwargs ||= {}
83
+
84
+ # make empty responses status 204 unless a status is already explicitly defined
85
+ if (payload.nil? || payload == '') && !kwargs.key?(:status)
86
+ kwargs[:status] = 204
87
+ end
88
+
89
+ respond_to do |format|
90
+ if payload.respond_to?(:to_json)
91
+ format.json {
92
+ kwargs = kwargs.merge(json_kwargs)
93
+ render(json: payload || '', **kwargs)
94
+ }
95
+ end
96
+ if payload.respond_to?(:to_xml)
97
+ format.xml {
98
+ kwargs = kwargs.merge(xml_kwargs)
99
+ render(xml: payload || '', **kwargs)
100
+ }
101
+ end
102
+ format.html {
103
+ @payload = payload
104
+ @json_payload = payload.to_json
105
+ @xml_payload = payload.to_xml
106
+ @template_logo_text ||= "Rails REST Framework"
107
+ @title ||= self.controller_name.camelize
108
+ @routes ||= self._get_routes
109
+ kwargs = kwargs.merge(html_kwargs)
110
+ begin
111
+ render(**kwargs)
112
+ rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
113
+ kwargs[:layout] = "rest_framework"
114
+ kwargs[:template] = "rest_framework/default"
115
+ end
116
+ render(**kwargs)
117
+ }
118
+ end
119
+ end
120
+ end
121
+
122
+ end
@@ -0,0 +1,228 @@
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 an action (or the current action if none given).
35
+ def get_fields(action: nil)
36
+ action_fields = self.class.action_fields || {}
37
+
38
+ # action will, by default, be the current action name
39
+ action = action_name.to_sym unless action
40
+
41
+ # index action should use :list fields if :index is not provided
42
+ action = :list if action == :index && !action_fields.key?(:index)
43
+
44
+ return action_fields[action] || self.class.fields || []
45
+ end
46
+
47
+ # Get a native serializer config for an action (or the current action if none given).
48
+ def get_native_serializer_config(action: nil)
49
+ native_serializer_action_config = self.class.native_serializer_action_config || {}
50
+
51
+ # action will, by default, be the current action name
52
+ action = action_name.to_sym unless action
53
+
54
+ # index action should use :list serializer config if :index is not provided
55
+ action = :list if action == :index && !native_serializer_action_config.key?(:index)
56
+
57
+ return native_serializer_action_config[action] || self.class.native_serializer_config
58
+ end
59
+
60
+ # Get a list of parameters allowed for an action (or the current action if none given).
61
+ def get_allowed_parameters(action: nil)
62
+ allowed_action_parameters = self.class.allowed_action_parameters || {}
63
+
64
+ # action will, by default, be the current action name
65
+ action = action_name.to_sym unless action
66
+
67
+ # index action should use :list allowed parameters if :index is not provided
68
+ action = :list if action == :index && !allowed_action_parameters.key?(:index)
69
+
70
+ return allowed_action_parameters[action] || self.class.allowed_parameters
71
+ end
72
+
73
+ # Filter the request body for keys in current action's allowed_parameters/fields config.
74
+ def _get_parameter_values_from_request_body
75
+ fields = self.get_allowed_parameters || self.get_fields
76
+ return @_get_field_values_from_request_body ||= (request.request_parameters.select { |p|
77
+ fields.include?(p.to_sym) || fields.include?(p.to_s)
78
+ })
79
+ end
80
+ alias :get_create_params :_get_parameter_values_from_request_body
81
+ alias :get_update_params :_get_parameter_values_from_request_body
82
+
83
+ # Filter params for keys allowed by the current action's filterset_fields/fields config.
84
+ def _get_filterset_values_from_params
85
+ fields = self.filterset_fields || self.get_fields
86
+ return @_get_field_values_from_params ||= request.query_parameters.select { |p|
87
+ fields.include?(p.to_sym) || fields.include?(p.to_s)
88
+ }
89
+ end
90
+ alias :get_lookup_params :_get_filterset_values_from_params
91
+ alias :get_filter_params :_get_filterset_values_from_params
92
+
93
+ # Get the recordset, filtered by the filter params.
94
+ def get_filtered_recordset
95
+ filter_params = self.get_filter_params
96
+ unless filter_params.blank?
97
+ return self.get_recordset.where(**self.get_filter_params.to_hash.symbolize_keys)
98
+ end
99
+ return self.get_recordset
100
+ end
101
+
102
+ # Get a record by `id` or return a single record if recordset is filtered down to a single
103
+ # record.
104
+ def get_record
105
+ records = self.get_filtered_recordset
106
+ if params['id'] # direct lookup
107
+ return records.find(params['id'])
108
+ elsif records.length == 1
109
+ return records[0]
110
+ end
111
+ return nil
112
+ end
113
+
114
+ # Internal interface for get_model, protecting against infinite recursion with get_recordset.
115
+ def _get_model(from_internal_get_recordset: false)
116
+ return @model if instance_variable_defined?(:@model) && @model
117
+ return self.class.model if self.class.model
118
+ unless from_internal_get_recordset # prevent infinite recursion
119
+ recordset = self._get_recordset(from_internal_get_model: true)
120
+ return (@model = recordset.klass) if recordset
121
+ end
122
+ begin
123
+ return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
124
+ rescue NameError
125
+ end
126
+ return nil
127
+ end
128
+
129
+ # Internal interface for get_recordset, protecting against infinite recursion with get_model.
130
+ def _get_recordset(from_internal_get_model: false)
131
+ return @recordset if instance_variable_defined?(:@recordset) && @recordset
132
+ return self.class.recordset if self.class.recordset
133
+ unless from_internal_get_model # prevent infinite recursion
134
+ model = self._get_model(from_internal_get_recordset: true)
135
+ return (@recordset = model.all) if model
136
+ end
137
+ return nil
138
+ end
139
+
140
+ # Get the model for this controller.
141
+ def get_model
142
+ return _get_model
143
+ end
144
+
145
+ # Get the set of records this controller has access to.
146
+ def get_recordset
147
+ return _get_recordset
148
+ end
149
+ end
150
+
151
+ module ListModelMixin
152
+ # TODO: pagination classes like Django
153
+ def index
154
+ @records = self.get_filtered_recordset
155
+ @serialized_records = self.get_serializer_class.new(
156
+ object: @records, controller: self
157
+ ).serialize
158
+ return api_response(@serialized_records)
159
+ end
160
+ end
161
+
162
+ module ShowModelMixin
163
+ def show
164
+ @record = self.get_record
165
+ @serialized_record = self.get_serializer_class.new(
166
+ object: @record, controller: self
167
+ ).serialize
168
+ return api_response(@serialized_record)
169
+ end
170
+ end
171
+
172
+ module CreateModelMixin
173
+ def create
174
+ @record = self.get_model.create!(self.get_create_params)
175
+ @serialized_record = self.get_serializer_class.new(
176
+ object: @record, controller: self
177
+ ).serialize
178
+ return api_response(@serialized_record)
179
+ end
180
+ end
181
+
182
+ module UpdateModelMixin
183
+ def update
184
+ @record = self.get_record
185
+ @record.update!(self.get_update_params)
186
+ @serialized_record = self.get_serializer_class.new(
187
+ object: @record, controller: self
188
+ ).serialize
189
+ return api_response(@serialized_record)
190
+ end
191
+ end
192
+
193
+ module DestroyModelMixin
194
+ def destroy
195
+ @record = self.get_record
196
+ @record.destroy!
197
+ api_response(nil)
198
+ end
199
+ end
200
+
201
+ module ReadOnlyModelControllerMixin
202
+ include BaseModelControllerMixin
203
+ def self.included(base)
204
+ if base.is_a? Class
205
+ BaseModelControllerMixin.included(base)
206
+ end
207
+ end
208
+
209
+ include ListModelMixin
210
+ include ShowModelMixin
211
+ end
212
+
213
+ module ModelControllerMixin
214
+ include BaseModelControllerMixin
215
+ def self.included(base)
216
+ if base.is_a? Class
217
+ BaseModelControllerMixin.included(base)
218
+ end
219
+ end
220
+
221
+ include ListModelMixin
222
+ include ShowModelMixin
223
+ include CreateModelMixin
224
+ include UpdateModelMixin
225
+ include DestroyModelMixin
226
+ end
227
+
228
+ 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.skip_actions(skip_undefined: skip_undefined)
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,49 @@
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
+
11
+ def is_valid
12
+ return true
13
+ end
14
+ end
15
+
16
+ # This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is a Rails
17
+ # method which converts objects to Ruby primitives (with the top-level being either an array or a
18
+ # hash).
19
+ class NativeModelSerializer < BaseSerializer
20
+ def initialize(model: nil, **kwargs)
21
+ super(**kwargs)
22
+ @model = model || @controller.send(:get_model)
23
+ end
24
+
25
+ # Get a configuration passable to `as_json` for the model.
26
+ def get_native_serializer_config
27
+ # return a serializer config if one is defined
28
+ serializer_config = @controller.send(:get_native_serializer_config)
29
+ return serializer_config if serializer_config
30
+
31
+ # build serializer config from fields
32
+ fields = @controller.send(:get_fields)
33
+ unless fields.blank?
34
+ columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
35
+ return {only: columns, methods: methods}
36
+ end
37
+
38
+ return {}
39
+ end
40
+
41
+ # Convert the object(s) to Ruby primitives.
42
+ def serialize
43
+ if @object
44
+ return @object.as_json(self.get_native_serializer_config)
45
+ end
46
+ return nil
47
+ end
48
+ end
49
+ 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 in @_version
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.3
4
+ version: 0.0.9
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-18 00:00:00.000000000 Z
11
+ date: 2020-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -33,19 +33,18 @@ extra_rdoc_files: []
33
33
  files:
34
34
  - LICENSE
35
35
  - README.md
36
- - app/assets/images/rest_framework_favicon.ico
37
- - app/assets/stylesheets/rest_framework.css
38
36
  - app/views/layouts/rest_framework.html.erb
39
37
  - app/views/rest_framework/_head.html.erb
40
38
  - app/views/rest_framework/_routes.html.erb
41
39
  - app/views/rest_framework/default.html.erb
42
40
  - lib/rest_framework.rb
43
41
  - lib/rest_framework/VERSION_STAMP
44
- - lib/rest_framework/controllers.rb
45
- - lib/rest_framework/controllers/base.rb
46
- - lib/rest_framework/controllers/models.rb
42
+ - lib/rest_framework/controller_mixins.rb
43
+ - lib/rest_framework/controller_mixins/base.rb
44
+ - lib/rest_framework/controller_mixins/models.rb
47
45
  - lib/rest_framework/engine.rb
48
46
  - lib/rest_framework/routers.rb
47
+ - lib/rest_framework/serializers.rb
49
48
  - lib/rest_framework/version.rb
50
49
  homepage: https://github.com/gregschmit/rails-rest-framework
51
50
  licenses:
@@ -1 +0,0 @@
1
- h1, h2, h3, h4, h5, h6 { width: 100%; }
@@ -1,2 +0,0 @@
1
- require_relative 'controllers/base'
2
- require_relative 'controllers/models'
@@ -1,125 +0,0 @@
1
- module RESTFramework
2
-
3
- # This module provides helpers for mixin `ClassMethods` submodules.
4
- module ClassMethodHelpers
5
-
6
- # This helper assists in providing reader interfaces for mixin properties.
7
- def _restframework_attr_reader(property, default: nil)
8
- method = <<~RUBY
9
- def #{property}
10
- return _restframework_try_class_level_variable_get(
11
- #{property.inspect},
12
- default: #{default.inspect},
13
- )
14
- end
15
- RUBY
16
- self.module_eval(method)
17
- end
18
- end
19
-
20
- # This module provides the common functionality for any controller mixins, a `root` action, and
21
- # the ability to route arbitrary actions with `@extra_actions`. This is also where `api_response`
22
- # is defined.
23
- module BaseControllerMixin
24
- # Default action for API root.
25
- def root
26
- api_response({message: "This is the root of your awesome API!"})
27
- end
28
-
29
- protected
30
-
31
- module ClassMethods
32
- extend ClassMethodHelpers
33
-
34
- # Interface for getting class-level instance/class variables.
35
- private def _restframework_try_class_level_variable_get(name, default: nil)
36
- begin
37
- v = instance_variable_get("@#{name}")
38
- return v unless v.nil?
39
- rescue NameError
40
- end
41
- begin
42
- v = class_variable_get("@@#{name}")
43
- return v unless v.nil?
44
- rescue NameError
45
- end
46
- return default
47
- end
48
-
49
- # Interface for registering exceptions handlers.
50
- # private def _restframework_register_exception_handlers
51
- # rescue_from
52
- # end
53
-
54
- _restframework_attr_reader(:singleton_controller)
55
- _restframework_attr_reader(:extra_actions, default: {})
56
- _restframework_attr_reader(:template_logo_text, default: 'Rails REST Framework')
57
-
58
- def skip_actions(skip_undefined: true)
59
- # first, skip explicitly skipped actions
60
- skip = _restframework_try_class_level_variable_get(:skip_actions, default: [])
61
-
62
- # now add methods which don't exist, since we don't want to route those
63
- if skip_undefined
64
- [:index, :new, :create, :show, :edit, :update, :destroy].each do |a|
65
- skip << a unless self.method_defined?(a)
66
- end
67
- end
68
-
69
- return skip
70
- end
71
- end
72
-
73
- def self.included(base)
74
- base.extend ClassMethods
75
- end
76
-
77
- def _get_routes
78
- begin
79
- formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
80
- rescue NameError
81
- formatter = ActionDispatch::Routing::ConsoleFormatter
82
- end
83
- return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
84
- formatter.new
85
- ).lines[1..].map { |r| r.split.last(3) }.map { |r|
86
- {verb: r[0], path: r[1], action: r[2]}
87
- }.select { |r| r[:path].start_with?(request.path) }
88
- end
89
-
90
- # Helper alias for `respond_to`/`render`, and replace nil responses with blank ones.
91
- def api_response(payload, html_kwargs: nil, json_kwargs: nil, **kwargs)
92
- html_kwargs ||= {}
93
- json_kwargs ||= {}
94
-
95
- # serialize
96
- if self.respond_to?(:get_model_serializer_config, true)
97
- serialized_payload = payload.to_json(**self.get_model_serializer_config)
98
- else
99
- serialized_payload = payload.to_json
100
- end
101
-
102
- respond_to do |format|
103
- format.html {
104
- kwargs = kwargs.merge(html_kwargs)
105
- @template_logo_text ||= self.class.template_logo_text
106
- @title ||= self.controller_name.camelize
107
- @routes ||= self._get_routes
108
- @payload = payload
109
- @serialized_payload = serialized_payload
110
- begin
111
- render(**kwargs)
112
- rescue ActionView::MissingTemplate # fallback to rest_framework default view
113
- kwargs[:template] = "rest_framework/default"
114
- end
115
- render(**kwargs)
116
- }
117
- format.json {
118
- kwargs = kwargs.merge(json_kwargs)
119
- render(json: serialized_payload || '', **kwargs)
120
- }
121
- end
122
- end
123
- end
124
-
125
- end
@@ -1,224 +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 @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
- @fields ||= self.class.fields || []
63
-
64
- return @fields
65
- end
66
-
67
- # Get a configuration passable to `as_json` for the model.
68
- def get_model_serializer_config
69
- fields = self.get_fields
70
- unless fields.blank?
71
- columns, methods = fields.partition { |f| f.to_s.in?(self.get_model.column_names) }
72
- return {only: columns, methods: methods}
73
- end
74
- return {}
75
- end
76
-
77
- # Filter the request body for keys allowed by the current action's field config.
78
- def _get_field_values_from_request_body
79
- return @_get_field_values_from_request_body ||= (request.request_parameters.select { |p|
80
- self.get_fields.include?(p.to_sym) || self.get_fields.include?(p.to_s)
81
- })
82
- end
83
- alias :get_create_params :_get_field_values_from_request_body
84
- alias :get_update_params :_get_field_values_from_request_body
85
-
86
- # Filter params for keys allowed by the current action's field config.
87
- def _get_field_values_from_params
88
- return @_get_field_values_from_params ||= params.permit(*self.get_fields)
89
- end
90
- alias :get_lookup_params :_get_field_values_from_params
91
- alias :get_filter_params :_get_field_values_from_params
92
-
93
- # Get the recordset, filtered by the filter params.
94
- def get_filtered_recordset
95
- filter_params = self.get_filter_params
96
- unless filter_params.blank?
97
- return self.get_recordset.where(**self.get_filter_params)
98
- end
99
- return self.get_recordset
100
- end
101
-
102
- # Get a record by `id` or return a single record if recordset is filtered down to a single record.
103
- def get_record
104
- records = self.get_filtered_recordset
105
- if params['id'] # direct lookup
106
- return records.find(params['id'])
107
- elsif records.length == 1
108
- return records[0]
109
- end
110
- return nil
111
- end
112
-
113
- # Internal interface for get_model, protecting against infinite recursion with get_recordset.
114
- def _get_model(from_internal_get_recordset: false)
115
- return @model if @model
116
- return self.class.model if self.class.model
117
- unless from_internal_get_recordset # prevent infinite recursion
118
- recordset = self._get_recordset(from_internal_get_model: true)
119
- return (@model = recordset.klass) if recordset
120
- end
121
- begin
122
- return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
123
- rescue NameError
124
- end
125
- return nil
126
- end
127
-
128
- # Internal interface for get_recordset, protecting against infinite recursion with get_model.
129
- def _get_recordset(from_internal_get_model: false)
130
- return @recordset if @recordset
131
- return self.class.recordset if self.class.recordset
132
- unless from_internal_get_model # prevent infinite recursion
133
- model = self._get_model(from_internal_get_recordset: true)
134
- return (@recordset = model.all) if model
135
- end
136
- return nil
137
- end
138
-
139
- # Get the model for this controller.
140
- def get_model
141
- return _get_model
142
- end
143
-
144
- # Get the base set of records this controller has access to.
145
- def get_recordset
146
- return _get_recordset
147
- end
148
- end
149
-
150
- module ListModelMixin
151
- # TODO: pagination classes like Django
152
- def index
153
- @records = self.get_filtered_recordset
154
- api_response(@records)
155
- end
156
- end
157
-
158
- module ShowModelMixin
159
- def show
160
- @record = self.get_record
161
- api_response(@record)
162
- end
163
- end
164
-
165
- module CreateModelMixin
166
- def create
167
- begin
168
- @record = self.get_model.create!(self.get_create_params)
169
- rescue ActiveRecord::RecordInvalid => e
170
- api_response(e.record.messages, status: 400)
171
- end
172
- api_response(@record)
173
- end
174
- end
175
-
176
- module UpdateModelMixin
177
- def update
178
- @record = self.get_record
179
- if @record
180
- @record.attributes(self.get_update_params)
181
- @record.save!
182
- api_response(@record)
183
- else
184
- api_response({detail: "Record not found."}, status: 404)
185
- end
186
- end
187
- end
188
-
189
- module DestroyModelMixin
190
- def destroy
191
- @record = self.get_record
192
- if @record
193
- @record.destroy!
194
- api_response('')
195
- else
196
- api_response({detail: "Method 'DELETE' not allowed."}, status: 405)
197
- end
198
- end
199
- end
200
-
201
- module ReadOnlyModelControllerMixin
202
- include BaseModelControllerMixin
203
- def self.included(base)
204
- base.extend BaseModelControllerMixin::ClassMethods
205
- end
206
-
207
- include ListModelMixin
208
- include ShowModelMixin
209
- end
210
-
211
- module ModelControllerMixin
212
- include BaseModelControllerMixin
213
- def self.included(base)
214
- base.extend BaseModelControllerMixin::ClassMethods
215
- end
216
-
217
- include ListModelMixin
218
- include ShowModelMixin
219
- include CreateModelMixin
220
- include UpdateModelMixin
221
- include DestroyModelMixin
222
- end
223
-
224
- end