rest_framework 0.0.7 → 0.0.8

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: 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