rest_framework 0.0.6 → 0.0.12

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