rest_framework 0.0.3 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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