rest_framework 0.0.7 → 0.0.8

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: 5a70372b29a6a1f71c62ecfe54f87836c185986e0786be23c73d8da52c73e3d3
4
- data.tar.gz: fa48c80ce2d183f31c3e9791a454462fd157f803bd3f77f620ebad13bb9b86a2
3
+ metadata.gz: f0a9ae17435876f34d868d88ea187ae8844593a498f6d2d560c3ab2cfe1fa37a
4
+ data.tar.gz: ea04580ad618ed1e15f28cb29ed079e64beead85d77479a52896c10aed226bc8
5
5
  SHA512:
6
- metadata.gz: 4c7819ea220f5a41890320d5f7c4667d9989d9a23a46c6d9e53b4c8fe558f187cbfa094616ade0013b2ed8a28732024403d416449589167d759eaa8f0df432b0
7
- data.tar.gz: cf92ba584de58531b31ed33a1855270324559a4484c29a4ea953e4b2093080db3923e1ded3be6fedac867985febf11dbdfaf07600879b02ff6a17f5b87cef473
6
+ metadata.gz: 3b596fb77e6bf656ce0fd7613ed2498019670b8576b022f6bda349d4f945d1502a2704bffb3d82a8cf2910ee0779f8fbcf2abae69bbd90e7734d59af1f2a8323
7
+ data.tar.gz: 9c11d18a0805d17adb55b15d2e5b75e563c7e065bf0734e330c2584991ba0301a542466d9ee5d87387ecd97848451656c0740fa5488d77cd3f187b1f8988aacb
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)) unless @serialized_payload.blank? %></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>
@@ -3,17 +3,22 @@
3
3
  <%= csrf_meta_tags %>
4
4
  <%= csp_meta_tag rescue nil %>
5
5
 
6
- <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" />
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.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
18
- <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>
19
- <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.7
1
+ 0.0.8
@@ -0,0 +1,2 @@
1
+ require_relative 'controller_mixins/base'
2
+ require_relative 'controller_mixins/models'
@@ -0,0 +1,106 @@
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
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def record_invalid(e)
45
+ return api_response(
46
+ {message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
47
+ )
48
+ end
49
+
50
+ def record_not_found(e)
51
+ return api_response({message: "Record not found.", exception: e}, status: 404)
52
+ end
53
+
54
+ def _get_routes
55
+ begin
56
+ formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
57
+ rescue NameError
58
+ formatter = ActionDispatch::Routing::ConsoleFormatter
59
+ end
60
+ return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
61
+ formatter.new
62
+ ).lines[1..].map { |r| r.split.last(3) }.map { |r|
63
+ {verb: r[0], path: r[1], action: r[2]}
64
+ }.select { |r| r[:path].start_with?(request.path) }
65
+ end
66
+
67
+ # Helper alias for `respond_to`/`render`, and replace nil responses with blank ones. `payload`
68
+ # must be already serialized to Ruby primitives.
69
+ def api_response(payload, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
70
+ html_kwargs ||= {}
71
+ json_kwargs ||= {}
72
+ xml_kwargs ||= {}
73
+
74
+ respond_to do |format|
75
+ if payload.respond_to?(:to_json)
76
+ format.json {
77
+ kwargs = kwargs.merge(json_kwargs)
78
+ render(json: payload || '', **kwargs)
79
+ }
80
+ end
81
+ if payload.respond_to?(:to_xml)
82
+ format.xml {
83
+ kwargs = kwargs.merge(xml_kwargs)
84
+ render(xml: payload || '', **kwargs)
85
+ }
86
+ end
87
+ format.html {
88
+ @payload = payload
89
+ @json_payload = payload.to_json
90
+ @xml_payload = payload.to_xml
91
+ @template_logo_text ||= "Rails REST Framework"
92
+ @title ||= self.controller_name.camelize
93
+ @routes ||= self._get_routes
94
+ kwargs = kwargs.merge(html_kwargs)
95
+ begin
96
+ render(**kwargs)
97
+ rescue ActionView::MissingTemplate # fallback to rest_framework default view
98
+ kwargs[:template] = "rest_framework/default"
99
+ end
100
+ render(**kwargs)
101
+ }
102
+ end
103
+ end
104
+ end
105
+
106
+ end
@@ -0,0 +1,217 @@
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
+ :filterset_fields,
17
+ :allowed_parameters,
18
+ :allowed_action_parameters,
19
+ :serializer_class,
20
+ :extra_member_actions,
21
+ ])
22
+ base.alias_method(:extra_collection_actions=, :extra_actions=)
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ def get_serializer_class
29
+ return self.class.serializer_class || NativeModelSerializer
30
+ end
31
+
32
+ # Get a list of fields for an action (or the current action if none given).
33
+ def get_fields(action: nil)
34
+ action_fields = self.class.action_fields || {}
35
+
36
+ # action will, by default, be the current action name
37
+ action = action_name.to_sym unless action
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] || self.class.fields || []
43
+ end
44
+
45
+ # Get a list of parameters allowed for an action (or the current action if none given).
46
+ def get_allowed_parameters(action: nil)
47
+ allowed_action_parameters = self.class.allowed_action_parameters || {}
48
+
49
+ # action will, by default, be the current action name
50
+ action = action_name.to_sym unless action
51
+
52
+ # index action should use :list allowed parameters if :index is not provided
53
+ action = :list if action == :index && !allowed_action_parameters.key?(:index)
54
+
55
+ return allowed_action_parameters[action] || self.class.allowed_parameters
56
+ end
57
+
58
+ # Filter the request body for keys in current action's allowed_parameters/fields config.
59
+ def _get_parameter_values_from_request_body
60
+ fields = self.get_allowed_parameters || self.get_fields
61
+ return @_get_field_values_from_request_body ||= (request.request_parameters.select { |p|
62
+ fields.include?(p.to_sym) || fields.include?(p.to_s)
63
+ })
64
+ end
65
+ alias :get_create_params :_get_parameter_values_from_request_body
66
+ alias :get_update_params :_get_parameter_values_from_request_body
67
+
68
+ # Filter params for keys allowed by the current action's filterset_fields/fields config.
69
+ def _get_filterset_values_from_params
70
+ fields = self.filterset_fields || self.get_fields
71
+ return @_get_field_values_from_params ||= request.query_parameters.select { |p|
72
+ fields.include?(p.to_sym) || fields.include?(p.to_s)
73
+ }
74
+ end
75
+ alias :get_lookup_params :_get_filterset_values_from_params
76
+ alias :get_filter_params :_get_filterset_values_from_params
77
+
78
+ # Get the recordset, filtered by the filter params.
79
+ def get_filtered_recordset
80
+ filter_params = self.get_filter_params
81
+ unless filter_params.blank?
82
+ return self.get_recordset.where(**self.get_filter_params.to_hash.symbolize_keys)
83
+ end
84
+ return self.get_recordset
85
+ end
86
+
87
+ # Get a record by `id` or return a single record if recordset is filtered down to a single
88
+ # record.
89
+ def get_record
90
+ records = self.get_filtered_recordset
91
+ if params['id'] # direct lookup
92
+ return records.find(params['id'])
93
+ elsif records.length == 1
94
+ return records[0]
95
+ end
96
+ return nil
97
+ end
98
+
99
+ # Internal interface for get_model, protecting against infinite recursion with get_recordset.
100
+ def _get_model(from_internal_get_recordset: false)
101
+ return @model if instance_variable_defined?(:@model) && @model
102
+ return self.class.model if self.class.model
103
+ unless from_internal_get_recordset # prevent infinite recursion
104
+ recordset = self._get_recordset(from_internal_get_model: true)
105
+ return (@model = recordset.klass) if recordset
106
+ end
107
+ begin
108
+ return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
109
+ rescue NameError
110
+ end
111
+ return nil
112
+ end
113
+
114
+ # Internal interface for get_recordset, protecting against infinite recursion with get_model.
115
+ def _get_recordset(from_internal_get_model: false)
116
+ return @recordset if instance_variable_defined?(:@recordset) && @recordset
117
+ return self.class.recordset if self.class.recordset
118
+ unless from_internal_get_model # prevent infinite recursion
119
+ model = self._get_model(from_internal_get_recordset: true)
120
+ return (@recordset = model.all) if model
121
+ end
122
+ return nil
123
+ end
124
+
125
+ # Get the model for this controller.
126
+ def get_model
127
+ return _get_model
128
+ end
129
+
130
+ # Get the set of records this controller has access to.
131
+ def get_recordset
132
+ return _get_recordset
133
+ end
134
+ end
135
+
136
+ module ListModelMixin
137
+ # TODO: pagination classes like Django
138
+ def index
139
+ @records = self.get_filtered_recordset
140
+ @serialized_records = self.get_serializer_class.new(
141
+ object: @records, controller: self
142
+ ).serialize
143
+ return api_response(@serialized_records)
144
+ end
145
+ end
146
+
147
+ module ShowModelMixin
148
+ def show
149
+ @record = self.get_record
150
+ @serialized_record = self.get_serializer_class.new(
151
+ object: @record, controller: self
152
+ ).serialize
153
+ return api_response(@serialized_record)
154
+ end
155
+ end
156
+
157
+ module CreateModelMixin
158
+ def create
159
+ @record = self.get_model.create!(self.get_create_params)
160
+ @serialized_record = self.get_serializer_class.new(
161
+ object: @record, controller: self
162
+ ).serialize
163
+ return api_response(@serialized_record)
164
+ end
165
+ end
166
+
167
+ module UpdateModelMixin
168
+ def update
169
+ @record = self.get_record
170
+ @record.update!(self.get_update_params)
171
+ @serialized_record = self.get_serializer_class.new(
172
+ object: @record, controller: self
173
+ ).serialize
174
+ return api_response(@serialized_record)
175
+ end
176
+ end
177
+
178
+ module DestroyModelMixin
179
+ def destroy
180
+ @record = self.get_record
181
+ if @record
182
+ @record.destroy!
183
+ api_response('')
184
+ else
185
+ api_response({detail: "Method 'DELETE' not allowed."}, status: 405)
186
+ end
187
+ end
188
+ end
189
+
190
+ module ReadOnlyModelControllerMixin
191
+ include BaseModelControllerMixin
192
+ def self.included(base)
193
+ if base.is_a? Class
194
+ BaseModelControllerMixin.included(base)
195
+ end
196
+ end
197
+
198
+ include ListModelMixin
199
+ include ShowModelMixin
200
+ end
201
+
202
+ module ModelControllerMixin
203
+ include BaseModelControllerMixin
204
+ def self.included(base)
205
+ if base.is_a? Class
206
+ BaseModelControllerMixin.included(base)
207
+ end
208
+ end
209
+
210
+ include ListModelMixin
211
+ include ShowModelMixin
212
+ include CreateModelMixin
213
+ include UpdateModelMixin
214
+ include DestroyModelMixin
215
+ end
216
+
217
+ 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,40 @@
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
+ fields = @controller.send(:get_fields)
28
+ unless fields.blank?
29
+ columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
30
+ return {only: columns, methods: methods}
31
+ end
32
+ return {}
33
+ end
34
+
35
+ # Convert the object(s) to Ruby primitives.
36
+ def serialize
37
+ return @object.as_json(self.get_native_serializer_config)
38
+ end
39
+ end
40
+ 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.7
4
+ version: 0.0.8
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
@@ -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/controllers.rb
43
- - lib/rest_framework/controllers/base.rb
44
- - 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
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,2 +0,0 @@
1
- require_relative 'controllers/base'
2
- require_relative 'controllers/models'
@@ -1,124 +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
- html_kwargs ||= {}
88
- json_kwargs ||= {}
89
-
90
- # serialize the payload if it's not blank
91
- if payload.blank?
92
- serialized_payload = ''
93
- else
94
- if self.respond_to?(:get_model_serializer_config, true)
95
- serialized_payload = payload.to_json(self.get_model_serializer_config)
96
- else
97
- serialized_payload = payload.to_json
98
- end
99
- end
100
-
101
- respond_to do |format|
102
- format.html {
103
- kwargs = kwargs.merge(html_kwargs)
104
- @template_logo_text ||= self.class.template_logo_text
105
- @title ||= self.controller_name.camelize
106
- @routes ||= self._get_routes
107
- @payload = payload
108
- @serialized_payload = serialized_payload
109
- begin
110
- render(**kwargs)
111
- rescue ActionView::MissingTemplate # fallback to rest_framework default view
112
- kwargs[:template] = "rest_framework/default"
113
- end
114
- render(**kwargs)
115
- }
116
- format.json {
117
- kwargs = kwargs.merge(json_kwargs)
118
- render(json: serialized_payload || '', **kwargs)
119
- }
120
- end
121
- end
122
- end
123
-
124
- 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