rest_framework 0.0.6 → 0.0.12

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: a3e577365f00c98803ebf7f4548c10e5e0cde9208d47de71ac5d5193d0e0c776
4
- data.tar.gz: b0527bc192535edcd85a91587e11df09c30dff460701c826fe6574ec487bc003
3
+ metadata.gz: 8c3691667e86ee77e9d15a1732d0013687a2e63a062a4ff01d1c6d8cc66f45fc
4
+ data.tar.gz: 6bcab926c283feb9a0643a06eab750f941882e5b56b294c59794f64a966883bb
5
5
  SHA512:
6
- metadata.gz: 805f9fe5d4fcd15f5e360459af7246e52392115f3cf6ab3fe51a2128f16c4092e10ac0797767769992416818cdf5dc429790e128ad7a889119cdb134d739ea61
7
- data.tar.gz: d0079b9e2cb005938d95aa83cccc3971849183be3209e2c08a2ebae5c357d6c9c571663cd496f68c4a5f8855ea2b804a46fbfcb73d839693b41ce781563599c0
6
+ metadata.gz: 204e0b5ea7b46710968a6274791f9e0f676db34f4eaefa1cf831fe8f9e4ab1f076a24b0375ef0554e1c4b57db3d1787adeebc31c4201da40b504aded51ca097b
7
+ data.tar.gz: 7e46ed5bbe61832045207664d679ecf394aead2928cdd78a1b29873087b78130546fd80595fe443680d5c26eebfa8bf5f23246b56a9a4a83e801c7763c024c76
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # REST Framework
1
+ # Rails REST Framework
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rest_framework.svg)](https://badge.fury.io/rb/rest_framework)
4
4
  [![Build Status](https://travis-ci.org/gregschmit/rails-rest-framework.svg?branch=master)](https://travis-ci.org/gregschmit/rails-rest-framework)
5
5
 
6
- REST Framework helps you build awesome APIs in Ruby on Rails.
6
+ Rails REST Framework helps you build awesome Web APIs in Ruby on Rails.
7
7
 
8
8
  **The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD
9
9
  logic, and routing them can be obnoxious.
@@ -11,6 +11,8 @@ logic, and routing them can be obnoxious.
11
11
  **The Solution**: This gem handles the common logic so you can focus on the parts of your API which
12
12
  make it unique.
13
13
 
14
+ To see detailed documentation, visit https://rails-rest-framework.com.
15
+
14
16
  ## Installation
15
17
 
16
18
  Add this line to your application's Gemfile:
@@ -21,13 +23,17 @@ gem 'rest_framework'
21
23
 
22
24
  And then execute:
23
25
 
24
- $ 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>
@@ -19,13 +19,44 @@
19
19
  <div class="container py-3">
20
20
  <div class="container">
21
21
  <div class="row">
22
- <h1><%= @title %></h1>
22
+ <h1><%= (@header_text if defined? @header_text) || @title %></h1>
23
23
  </div>
24
24
  <hr/>
25
- <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)) unless @json_payload == '' %></code></pre></div>
50
+ <% end %>
51
+ </div>
52
+ <div class="tab-pane fade" id="tab-xml" role="tab">
53
+ <% if @xml_payload %>
54
+ <div><pre><code class="language-xml"><%= @xml_payload %></code></pre></div>
55
+ <% end %>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <% end %>
29
60
  <% unless @routes.blank? %>
30
61
  <div class="row">
31
62
  <h2>Routes</h2>
@@ -3,17 +3,22 @@
3
3
  <%= csrf_meta_tags %>
4
4
  <%= csp_meta_tag rescue nil %>
5
5
 
6
- <link rel="stylesheet" href="https://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.6
1
+ 0.0.12
@@ -0,0 +1,2 @@
1
+ require_relative 'controller_mixins/base'
2
+ require_relative 'controller_mixins/models'
@@ -0,0 +1,129 @@
1
+ module RESTFramework
2
+
3
+ # This module provides the common functionality for any controller mixins, a `root` action, and
4
+ # the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
5
+ # is defined.
6
+ module BaseControllerMixin
7
+ # Default action for API root.
8
+ def root
9
+ api_response({message: "This is the root of your awesome API!"})
10
+ end
11
+
12
+ module ClassMethods
13
+ def get_skip_actions(skip_undefined: true)
14
+ # first, skip explicitly skipped actions
15
+ skip = self.skip_actions || []
16
+
17
+ # now add methods which don't exist, since we don't want to route those
18
+ if skip_undefined
19
+ [:index, :new, :create, :show, :edit, :update, :destroy].each do |a|
20
+ skip << a unless self.method_defined?(a)
21
+ end
22
+ end
23
+
24
+ return skip
25
+ end
26
+ end
27
+
28
+ def self.included(base)
29
+ if base.is_a? Class
30
+ base.extend ClassMethods
31
+ base.class_attribute(*[
32
+ :singleton_controller,
33
+ :extra_actions,
34
+ :skip_actions,
35
+ :paginator_class,
36
+ ])
37
+ base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
38
+ base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
39
+ base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
40
+ base.rescue_from(ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed)
41
+ end
42
+ end
43
+
44
+ protected
45
+
46
+ def record_invalid(e)
47
+ return api_response(
48
+ {message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
49
+ )
50
+ end
51
+
52
+ def record_not_found(e)
53
+ return api_response({message: "Record not found.", exception: e}, status: 404)
54
+ end
55
+
56
+ def record_not_saved(e)
57
+ return api_response({message: "Record not saved.", exception: e}, status: 406)
58
+ end
59
+
60
+ def record_not_destroyed(e)
61
+ return api_response({message: "Record not destroyed.", exception: e}, status: 406)
62
+ end
63
+
64
+ def _get_routes
65
+ begin
66
+ formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
67
+ rescue NameError
68
+ formatter = ActionDispatch::Routing::ConsoleFormatter
69
+ end
70
+ return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
71
+ formatter.new
72
+ ).lines[1..].map { |r| r.split.last(3) }.map { |r|
73
+ {verb: r[0], path: r[1], action: r[2]}
74
+ }.select { |r| r[:path].start_with?(request.path) }
75
+ end
76
+
77
+ # Helper alias for `respond_to`/`render`. `payload` should be already serialized to Ruby
78
+ # primitives.
79
+ def api_response(payload=nil, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
80
+ html_kwargs ||= {}
81
+ json_kwargs ||= {}
82
+ xml_kwargs ||= {}
83
+
84
+ # allow blank (no-content) responses
85
+ @blank = kwargs[:blank]
86
+
87
+ respond_to do |format|
88
+ if @blank
89
+ format.json {head :no_content}
90
+ format.xml {head :no_content}
91
+ else
92
+ if payload.respond_to?(:to_json)
93
+ format.json {
94
+ kwargs = kwargs.merge(json_kwargs)
95
+ render(json: payload, layout: false, **kwargs)
96
+ }
97
+ end
98
+ if payload.respond_to?(:to_xml)
99
+ format.xml {
100
+ kwargs = kwargs.merge(xml_kwargs)
101
+ render(xml: payload, layout: false, **kwargs)
102
+ }
103
+ end
104
+ end
105
+ format.html {
106
+ @payload = payload
107
+ @json_payload = ''
108
+ @xml_payload = ''
109
+ unless @blank
110
+ @json_payload = payload.to_json if payload.respond_to?(:to_json)
111
+ @xml_payload = payload.to_xml if payload.respond_to?(:to_xml)
112
+ end
113
+ @template_logo_text ||= "Rails REST Framework"
114
+ @title ||= self.controller_name.camelize
115
+ @routes ||= self._get_routes
116
+ kwargs = kwargs.merge(html_kwargs)
117
+ begin
118
+ render(**kwargs)
119
+ rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
120
+ kwargs[:layout] = "rest_framework"
121
+ kwargs[:template] = "rest_framework/default"
122
+ end
123
+ render(**kwargs)
124
+ }
125
+ end
126
+ end
127
+ end
128
+
129
+ end
@@ -0,0 +1,222 @@
1
+ require_relative 'base'
2
+ require_relative '../serializers'
3
+
4
+ module RESTFramework
5
+
6
+ module BaseModelControllerMixin
7
+ include BaseControllerMixin
8
+ def self.included(base)
9
+ if base.is_a? Class
10
+ BaseControllerMixin.included(base)
11
+ base.class_attribute(*[
12
+ :model,
13
+ :recordset,
14
+ :fields,
15
+ :action_fields,
16
+ :native_serializer_config,
17
+ :native_serializer_action_config,
18
+ :filterset_fields,
19
+ :allowed_parameters,
20
+ :allowed_action_parameters,
21
+ :serializer_class,
22
+ :extra_member_actions,
23
+ ])
24
+ base.alias_method(:extra_collection_actions=, :extra_actions=)
25
+ end
26
+ end
27
+
28
+ protected
29
+
30
+ def get_serializer_class
31
+ return self.class.serializer_class || NativeModelSerializer
32
+ end
33
+
34
+ # Get a list of fields for the current action.
35
+ def get_fields
36
+ action_fields = self.class.action_fields || {}
37
+ action = self.action_name.to_sym
38
+
39
+ # index action should use :list fields if :index is not provided
40
+ action = :list if action == :index && !action_fields.key?(:index)
41
+
42
+ return (action_fields[action] if action) || self.class.fields || []
43
+ end
44
+
45
+ # Get a native serializer config for the current action.
46
+ def get_native_serializer_config
47
+ action_serializer_config = self.class.native_serializer_action_config || {}
48
+ action = self.action_name.to_sym
49
+
50
+ # index action should use :list serializer config if :index is not provided
51
+ action = :list if action == :index && !action_serializer_config.key?(:index)
52
+
53
+ return (action_serializer_config[action] if action) || self.class.native_serializer_config
54
+ end
55
+
56
+ # Get a list of parameters allowed for the current action.
57
+ def get_allowed_parameters
58
+ allowed_action_parameters = self.class.allowed_action_parameters || {}
59
+ action = self.action_name.to_sym
60
+
61
+ # index action should use :list allowed parameters if :index is not provided
62
+ action = :list if action == :index && !allowed_action_parameters.key?(:index)
63
+
64
+ return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
65
+ end
66
+
67
+ # Filter the request body for keys in current action's allowed_parameters/fields config.
68
+ def _get_parameter_values_from_request_body
69
+ fields = self.get_allowed_parameters || self.get_fields
70
+ return @_get_parameter_values_from_request_body ||= (request.request_parameters.select { |p|
71
+ fields.include?(p.to_sym) || fields.include?(p.to_s)
72
+ })
73
+ end
74
+ alias :get_create_params :_get_parameter_values_from_request_body
75
+ alias :get_update_params :_get_parameter_values_from_request_body
76
+
77
+ # Filter params for keys allowed by the current action's filterset_fields/fields config.
78
+ def _get_filterset_values_from_params
79
+ fields = self.filterset_fields || self.get_fields
80
+ return @_get_filterset_values_from_params ||= request.query_parameters.select { |p|
81
+ fields.include?(p.to_sym) || fields.include?(p.to_s)
82
+ }
83
+ end
84
+ alias :get_lookup_params :_get_filterset_values_from_params
85
+ alias :get_filter_params :_get_filterset_values_from_params
86
+
87
+ # Get the recordset, filtered by the filter params.
88
+ def get_filtered_recordset
89
+ filter_params = self.get_filter_params
90
+ unless filter_params.blank?
91
+ return self.get_recordset.where(**self.get_filter_params.to_hash.symbolize_keys)
92
+ end
93
+ return self.get_recordset
94
+ end
95
+
96
+ # Get a record by `id` or return a single record if recordset is filtered down to a single
97
+ # record.
98
+ def get_record
99
+ records = self.get_filtered_recordset
100
+ if params['id'] # direct lookup
101
+ return records.find(params['id'])
102
+ elsif records.length == 1
103
+ return records[0]
104
+ end
105
+ return nil
106
+ end
107
+
108
+ # Internal interface for get_model, protecting against infinite recursion with get_recordset.
109
+ def _get_model(from_internal_get_recordset: false)
110
+ return @model if instance_variable_defined?(:@model) && @model
111
+ return self.class.model if self.class.model
112
+ unless from_internal_get_recordset # prevent infinite recursion
113
+ recordset = self._get_recordset(from_internal_get_model: true)
114
+ return (@model = recordset.klass) if recordset
115
+ end
116
+ begin
117
+ return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
118
+ rescue NameError
119
+ end
120
+ return nil
121
+ end
122
+
123
+ # Internal interface for get_recordset, protecting against infinite recursion with get_model.
124
+ def _get_recordset(from_internal_get_model: false)
125
+ return @recordset if instance_variable_defined?(:@recordset) && @recordset
126
+ return self.class.recordset if self.class.recordset
127
+ unless from_internal_get_model # prevent infinite recursion
128
+ model = self._get_model(from_internal_get_recordset: true)
129
+ return (@recordset = model.all) if model
130
+ end
131
+ return nil
132
+ end
133
+
134
+ # Get the model for this controller.
135
+ def get_model
136
+ return _get_model
137
+ end
138
+
139
+ # Get the set of records this controller has access to.
140
+ def get_recordset
141
+ return _get_recordset
142
+ end
143
+ end
144
+
145
+ module ListModelMixin
146
+ # TODO: pagination classes like Django
147
+ def index
148
+ @records = self.get_filtered_recordset
149
+ @serialized_records = self.get_serializer_class.new(
150
+ object: @records, controller: self
151
+ ).serialize
152
+ return api_response(@serialized_records)
153
+ end
154
+ end
155
+
156
+ module ShowModelMixin
157
+ def show
158
+ @record = self.get_record
159
+ @serialized_record = self.get_serializer_class.new(
160
+ object: @record, controller: self
161
+ ).serialize
162
+ return api_response(@serialized_record)
163
+ end
164
+ end
165
+
166
+ module CreateModelMixin
167
+ def create
168
+ @record = self.get_model.create!(self.get_create_params)
169
+ @serialized_record = self.get_serializer_class.new(
170
+ object: @record, controller: self
171
+ ).serialize
172
+ return api_response(@serialized_record)
173
+ end
174
+ end
175
+
176
+ module UpdateModelMixin
177
+ def update
178
+ @record = self.get_record
179
+ @record.update!(self.get_update_params)
180
+ @serialized_record = self.get_serializer_class.new(
181
+ object: @record, controller: self
182
+ ).serialize
183
+ return api_response(@serialized_record)
184
+ end
185
+ end
186
+
187
+ module DestroyModelMixin
188
+ def destroy
189
+ @record = self.get_record
190
+ @record.destroy!
191
+ api_response(nil)
192
+ end
193
+ end
194
+
195
+ module ReadOnlyModelControllerMixin
196
+ include BaseModelControllerMixin
197
+ def self.included(base)
198
+ if base.is_a? Class
199
+ BaseModelControllerMixin.included(base)
200
+ end
201
+ end
202
+
203
+ include ListModelMixin
204
+ include ShowModelMixin
205
+ end
206
+
207
+ module ModelControllerMixin
208
+ include BaseModelControllerMixin
209
+ def self.included(base)
210
+ if base.is_a? Class
211
+ BaseModelControllerMixin.included(base)
212
+ end
213
+ end
214
+
215
+ include ListModelMixin
216
+ include ShowModelMixin
217
+ include CreateModelMixin
218
+ include UpdateModelMixin
219
+ include DestroyModelMixin
220
+ end
221
+
222
+ end
@@ -1,4 +1,3 @@
1
- require 'rails'
2
1
  require 'action_dispatch/routing/mapper'
3
2
 
4
3
  module ActionDispatch::Routing
@@ -67,11 +66,11 @@ module ActionDispatch::Routing
67
66
 
68
67
  # call either `resource` or `resources`, passing appropriate modifiers
69
68
  skip_undefined = kwargs.delete(:skip_undefined) || true
70
- skip = controller_class.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,108 @@
1
+ module RESTFramework
2
+ class BaseSerializer
3
+ attr_reader :errors
4
+
5
+ def initialize(object: nil, data: nil, controller: nil, **kwargs)
6
+ @object = object
7
+ @data = data
8
+ @controller = controller
9
+ end
10
+ end
11
+
12
+ # This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is a Rails
13
+ # method which converts objects to Ruby primitives (with the top-level being either an array or a
14
+ # hash).
15
+ class NativeModelSerializer < BaseSerializer
16
+ class_attribute :config
17
+ class_attribute :singular_config
18
+ class_attribute :plural_config
19
+ class_attribute :action_config
20
+
21
+ def initialize(model: nil, many: nil, **kwargs)
22
+ super(**kwargs)
23
+ @many = many
24
+ @model = model || (@controller ? @controller.send(:get_model) : nil)
25
+ end
26
+
27
+ # Get controller action, if possible.
28
+ def get_action
29
+ return @controller&.action_name&.to_sym
30
+ end
31
+
32
+ # Get a locally defined configuration, if one is defined.
33
+ def get_local_serializer_config
34
+ action = self.get_action
35
+
36
+ if action && self.action_config
37
+ # index action should use :list serializer config if :index is not provided
38
+ action = :list if action == :index && !self.action_config.key?(:index)
39
+
40
+ return self.action_config[action] if self.action_config[action]
41
+ end
42
+
43
+ # no action_config, so try singular/plural config
44
+ return self.plural_config if @many && self.plural_config
45
+ return self.singular_config if !@many && self.singular_config
46
+
47
+ # lastly, try the default config
48
+ return self.config
49
+ end
50
+
51
+ # Get a configuration passable to `as_json` for the model.
52
+ def get_serializer_config
53
+ # return a locally defined serializer config if one is defined
54
+ local_config = self.get_local_serializer_config
55
+ return local_config if local_config
56
+
57
+ # return a serializer config if one is defined
58
+ serializer_config = @controller.send(:get_native_serializer_config)
59
+ return serializer_config if serializer_config
60
+
61
+ # otherwise, build a serializer config from fields
62
+ fields = @controller.send(:get_fields) if @controller
63
+ unless fields.blank?
64
+ columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
65
+ return {only: columns, methods: methods}
66
+ end
67
+
68
+ return {}
69
+ end
70
+
71
+ # Convert the object(s) to Ruby primitives.
72
+ def serialize
73
+ if @object
74
+ @many = @object.respond_to?(:each) if @many.nil?
75
+ return @object.as_json(self.get_serializer_config)
76
+ end
77
+ return nil
78
+ end
79
+
80
+ # Allow a serializer instance to be used as a hash directly in a nested serializer config.
81
+ def [](key)
82
+ unless instance_variable_defined?(:@_nested_config)
83
+ @_nested_config = self.get_serializer_config
84
+ end
85
+ return @_nested_config[key]
86
+ end
87
+ def []=(key, value)
88
+ unless instance_variable_defined?(:@_nested_config)
89
+ @_nested_config = self.get_serializer_config
90
+ end
91
+ return @_nested_config[key] = value
92
+ end
93
+
94
+ # Allow a serializer class to be used as a hash directly in a nested serializer config.
95
+ def self.[](key)
96
+ unless instance_variable_defined?(:@_nested_config)
97
+ @_nested_config = self.new.get_serializer_config
98
+ end
99
+ return @_nested_config[key]
100
+ end
101
+ def self.[]=(key, value)
102
+ unless instance_variable_defined?(:@_nested_config)
103
+ @_nested_config = self.new.get_serializer_config
104
+ end
105
+ return @_nested_config[key] = value
106
+ end
107
+ end
108
+ end
@@ -8,10 +8,10 @@ module RESTFramework
8
8
 
9
9
  # First, attempt to get the version from git.
10
10
  begin
11
- version = `git describe`.strip
11
+ version = `git describe 2>/dev/null`.strip
12
12
  raise "blank version" if version.nil? || version.match(/^\w*$/)
13
13
  # Check for local changes.
14
- changes = `git status --porcelain`
14
+ changes = `git status --porcelain 2>/dev/null`
15
15
  version << '.localchanges' if changes.strip.length > 0
16
16
  return version
17
17
  rescue
@@ -21,7 +21,7 @@ module RESTFramework
21
21
  begin
22
22
  version = File.read(File.expand_path("VERSION_STAMP", __dir__))
23
23
  unless version.nil? || version.match(/^\w*$/)
24
- return (@_version = version) # cache VERSION_STAMP content 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.6
4
+ version: 0.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregory N. Schmit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-18 00:00:00.000000000 Z
11
+ date: 2020-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -39,11 +39,12 @@ files:
39
39
  - app/views/rest_framework/default.html.erb
40
40
  - lib/rest_framework.rb
41
41
  - lib/rest_framework/VERSION_STAMP
42
- - lib/rest_framework/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,121 +0,0 @@
1
- module RESTFramework
2
-
3
- # This module provides helpers for mixin `ClassMethods` submodules.
4
- module ClassMethodHelpers
5
- # This helper assists in providing reader interfaces for mixin properties.
6
- def _restframework_attr_reader(property, default: nil)
7
- method = <<~RUBY
8
- def #{property}
9
- return _restframework_try_class_level_variable_get(
10
- #{property.inspect},
11
- default: #{default.inspect},
12
- )
13
- end
14
- RUBY
15
- self.module_eval(method)
16
- end
17
- end
18
-
19
- # This module provides the common functionality for any controller mixins, a `root` action, and
20
- # the ability to route arbitrary actions with `@extra_actions`. This is also where `api_response`
21
- # is defined.
22
- module BaseControllerMixin
23
- # Default action for API root.
24
- def root
25
- api_response({message: "This is the root of your awesome API!"})
26
- end
27
-
28
- protected
29
-
30
- module ClassMethods
31
- extend ClassMethodHelpers
32
-
33
- # Interface for getting class-level instance/class variables. Note: we check if they are
34
- # defined first rather than rescuing NameError to prevent uninitialized variable warnings.
35
- private def _restframework_try_class_level_variable_get(name, default: nil)
36
- instance_variable = "@#{name}"
37
- if self.instance_variable_defined?(instance_variable)
38
- v = self.instance_variable_get(instance_variable)
39
- return v unless v.nil?
40
- end
41
- class_variable = "@@#{name}"
42
- if self.class_variable_defined?(class_variable)
43
- v = self.class_variable_get(class_variable)
44
- return v unless v.nil?
45
- end
46
- return default
47
- end
48
-
49
- _restframework_attr_reader(:singleton_controller)
50
- _restframework_attr_reader(:extra_actions, default: {})
51
- _restframework_attr_reader(:template_logo_text, default: 'Rails REST Framework')
52
-
53
- def skip_actions(skip_undefined: true)
54
- # first, skip explicitly skipped actions
55
- skip = _restframework_try_class_level_variable_get(:skip_actions, default: [])
56
-
57
- # now add methods which don't exist, since we don't want to route those
58
- if skip_undefined
59
- [:index, :new, :create, :show, :edit, :update, :destroy].each do |a|
60
- skip << a unless self.method_defined?(a)
61
- end
62
- end
63
-
64
- return skip
65
- end
66
- end
67
-
68
- def self.included(base)
69
- base.extend ClassMethods
70
- end
71
-
72
- def _get_routes
73
- begin
74
- formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
75
- rescue NameError
76
- formatter = ActionDispatch::Routing::ConsoleFormatter
77
- end
78
- return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
79
- formatter.new
80
- ).lines[1..].map { |r| r.split.last(3) }.map { |r|
81
- {verb: r[0], path: r[1], action: r[2]}
82
- }.select { |r| r[:path].start_with?(request.path) }
83
- end
84
-
85
- # Helper alias for `respond_to`/`render`, and replace nil responses with blank ones.
86
- def api_response(payload, html_kwargs: nil, json_kwargs: nil, **kwargs)
87
- payload ||= '' # replace nil with ''
88
- html_kwargs ||= {}
89
- json_kwargs ||= {}
90
-
91
- # serialize
92
- if self.respond_to?(:get_model_serializer_config, true)
93
- serialized_payload = payload.to_json(self.get_model_serializer_config)
94
- else
95
- serialized_payload = payload.to_json
96
- end
97
-
98
- respond_to do |format|
99
- format.html {
100
- kwargs = kwargs.merge(html_kwargs)
101
- @template_logo_text ||= self.class.template_logo_text
102
- @title ||= self.controller_name.camelize
103
- @routes ||= self._get_routes
104
- @payload = payload
105
- @serialized_payload = serialized_payload
106
- begin
107
- render(**kwargs)
108
- rescue ActionView::MissingTemplate # fallback to rest_framework default view
109
- kwargs[:template] = "rest_framework/default"
110
- end
111
- render(**kwargs)
112
- }
113
- format.json {
114
- kwargs = kwargs.merge(json_kwargs)
115
- render(json: serialized_payload || '', **kwargs)
116
- }
117
- end
118
- end
119
- end
120
-
121
- end
@@ -1,223 +0,0 @@
1
- require_relative 'base'
2
-
3
- module RESTFramework
4
-
5
- module BaseModelControllerMixin
6
- include BaseControllerMixin
7
-
8
- # By default (and for now), we will just use `as_json`, but we should consider supporting:
9
- # active_model_serializers (problem:
10
- # https://github.com/rails-api/active_model_serializers#whats-happening-to-ams)
11
- # fast_jsonapi (really good and fast serializers)
12
- #@serializer
13
- #@list_serializer
14
- #@show_serializer
15
- #@create_serializer
16
- #@update_serializer
17
-
18
- module ClassMethods
19
- extend ClassMethodHelpers
20
- include BaseControllerMixin::ClassMethods
21
-
22
- _restframework_attr_reader(:model)
23
- _restframework_attr_reader(:recordset)
24
-
25
- _restframework_attr_reader(:fields)
26
- _restframework_attr_reader(:list_fields)
27
- _restframework_attr_reader(:show_fields)
28
- _restframework_attr_reader(:create_fields)
29
- _restframework_attr_reader(:update_fields)
30
-
31
- _restframework_attr_reader(:extra_member_actions, default: {})
32
-
33
- # For model-based mixins, `@extra_collection_actions` is synonymous with `@extra_actions`.
34
- # @param skip_undefined [Boolean] whether we should skip routing undefined actions
35
- def extra_actions(skip_undefined: true)
36
- actions = (
37
- _restframework_try_class_level_variable_get(:extra_collection_actions) ||
38
- _restframework_try_class_level_variable_get(:extra_actions, default: {})
39
- )
40
- actions = actions.select { |a| self.method_defined?(a) } if skip_undefined
41
- return actions
42
- end
43
- end
44
-
45
- def self.included(base)
46
- base.extend ClassMethods
47
- end
48
-
49
- protected
50
-
51
- # Get a list of fields for the current action.
52
- def get_fields
53
- return @fields if instance_variable_defined?(:@fields) && @fields
54
-
55
- # index action should use list_fields
56
- name = (action_name == 'index') ? 'list' : action_name
57
-
58
- begin
59
- @fields = self.class.send("#{name}_fields")
60
- rescue NameError
61
- end
62
-
63
- return @fields ||= self.class.fields || []
64
- end
65
-
66
- # Get a configuration passable to `as_json` for the model.
67
- def get_model_serializer_config
68
- fields = self.get_fields
69
- unless fields.blank?
70
- columns, methods = fields.partition { |f| f.to_s.in?(self.get_model.column_names) }
71
- return {only: columns, methods: methods}
72
- end
73
- return {}
74
- end
75
-
76
- # Filter the request body for keys allowed by the current action's field config.
77
- def _get_field_values_from_request_body
78
- return @_get_field_values_from_request_body ||= (request.request_parameters.select { |p|
79
- self.get_fields.include?(p.to_sym) || self.get_fields.include?(p.to_s)
80
- })
81
- end
82
- alias :get_create_params :_get_field_values_from_request_body
83
- alias :get_update_params :_get_field_values_from_request_body
84
-
85
- # Filter params for keys allowed by the current action's field config.
86
- def _get_field_values_from_params
87
- return @_get_field_values_from_params ||= params.permit(*self.get_fields)
88
- end
89
- alias :get_lookup_params :_get_field_values_from_params
90
- alias :get_filter_params :_get_field_values_from_params
91
-
92
- # Get the recordset, filtered by the filter params.
93
- def get_filtered_recordset
94
- filter_params = self.get_filter_params
95
- unless filter_params.blank?
96
- return self.get_recordset.where(**self.get_filter_params)
97
- end
98
- return self.get_recordset
99
- end
100
-
101
- # Get a record by `id` or return a single record if recordset is filtered down to a single record.
102
- def get_record
103
- records = self.get_filtered_recordset
104
- if params['id'] # direct lookup
105
- return records.find(params['id'])
106
- elsif records.length == 1
107
- return records[0]
108
- end
109
- return nil
110
- end
111
-
112
- # Internal interface for get_model, protecting against infinite recursion with get_recordset.
113
- def _get_model(from_internal_get_recordset: false)
114
- return @model if instance_variable_defined?(:@model) && @model
115
- return self.class.model if self.class.model
116
- unless from_internal_get_recordset # prevent infinite recursion
117
- recordset = self._get_recordset(from_internal_get_model: true)
118
- return (@model = recordset.klass) if recordset
119
- end
120
- begin
121
- return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
122
- rescue NameError
123
- end
124
- return nil
125
- end
126
-
127
- # Internal interface for get_recordset, protecting against infinite recursion with get_model.
128
- def _get_recordset(from_internal_get_model: false)
129
- return @recordset if instance_variable_defined?(:@recordset) && @recordset
130
- return self.class.recordset if self.class.recordset
131
- unless from_internal_get_model # prevent infinite recursion
132
- model = self._get_model(from_internal_get_recordset: true)
133
- return (@recordset = model.all) if model
134
- end
135
- return nil
136
- end
137
-
138
- # Get the model for this controller.
139
- def get_model
140
- return _get_model
141
- end
142
-
143
- # Get the base set of records this controller has access to.
144
- def get_recordset
145
- return _get_recordset
146
- end
147
- end
148
-
149
- module ListModelMixin
150
- # TODO: pagination classes like Django
151
- def index
152
- @records = self.get_filtered_recordset
153
- api_response(@records)
154
- end
155
- end
156
-
157
- module ShowModelMixin
158
- def show
159
- @record = self.get_record
160
- api_response(@record)
161
- end
162
- end
163
-
164
- module CreateModelMixin
165
- def create
166
- begin
167
- @record = self.get_model.create!(self.get_create_params)
168
- rescue ActiveRecord::RecordInvalid => e
169
- api_response(e.record.messages, status: 400)
170
- end
171
- api_response(@record)
172
- end
173
- end
174
-
175
- module UpdateModelMixin
176
- def update
177
- @record = self.get_record
178
- if @record
179
- @record.attributes(self.get_update_params)
180
- @record.save!
181
- api_response(@record)
182
- else
183
- api_response({detail: "Record not found."}, status: 404)
184
- end
185
- end
186
- end
187
-
188
- module DestroyModelMixin
189
- def destroy
190
- @record = self.get_record
191
- if @record
192
- @record.destroy!
193
- api_response('')
194
- else
195
- api_response({detail: "Method 'DELETE' not allowed."}, status: 405)
196
- end
197
- end
198
- end
199
-
200
- module ReadOnlyModelControllerMixin
201
- include BaseModelControllerMixin
202
- def self.included(base)
203
- base.extend BaseModelControllerMixin::ClassMethods
204
- end
205
-
206
- include ListModelMixin
207
- include ShowModelMixin
208
- end
209
-
210
- module ModelControllerMixin
211
- include BaseModelControllerMixin
212
- def self.included(base)
213
- base.extend BaseModelControllerMixin::ClassMethods
214
- end
215
-
216
- include ListModelMixin
217
- include ShowModelMixin
218
- include CreateModelMixin
219
- include UpdateModelMixin
220
- include DestroyModelMixin
221
- end
222
-
223
- end