rest_framework 0.0.2 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 117f77ddfdeadf0ae912fe3211d78135db7f72adbb55e8531dfc4766954455cc
4
- data.tar.gz: 064f2f56125d4af20753e3a8d88eeea5e1617c58e492f1b8517ed04c2f58b730
3
+ metadata.gz: f0a9ae17435876f34d868d88ea187ae8844593a498f6d2d560c3ab2cfe1fa37a
4
+ data.tar.gz: ea04580ad618ed1e15f28cb29ed079e64beead85d77479a52896c10aed226bc8
5
5
  SHA512:
6
- metadata.gz: ab6d2e0870adee08f98c0e7c89761ce59d5e5be9e051e0045b6b0c56625d1d09225ca34ac87223e2b3d41687fe9c35235514c5b2d0479f42ef576cc47d869c9d
7
- data.tar.gz: f8f4c6e08d10cc7e7f7500412714835b67ac3aa234ecd619abba78677ea3d5055713dc12dc2993e6bce67a7b9bd10187f833b985173dcd4942f6c0e4bd417548
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
@@ -2,17 +2,7 @@
2
2
  <html>
3
3
  <head>
4
4
  <title><%= @title %></title>
5
- <meta charset="utf-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7
- <%= csrf_meta_tags %>
8
- <%= csp_meta_tag %>
9
-
10
- <%= favicon_link_tag 'rest_framework_favicon.ico' %>
11
-
12
- <%= stylesheet_link_tag 'application', media: 'all' %>
13
- <%= javascript_include_tag 'application' %>
14
- <%= stylesheet_link_tag 'rest_framework', media: 'all' %>
15
- <%= javascript_include_tag 'rest_framework' %>
5
+ <%= render partial: 'rest_framework/head' %>
16
6
  </head>
17
7
 
18
8
  <body>
@@ -21,7 +11,7 @@
21
11
  <nav class="navbar navbar-dark bg-dark">
22
12
  <div class="container">
23
13
  <span class="navbar-brand p-0">
24
- <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>
25
15
  </span>
26
16
  </div>
27
17
  </nav>
@@ -31,10 +21,42 @@
31
21
  <div class="row">
32
22
  <h1><%= @title %></h1>
33
23
  </div>
34
- <div class="row">
35
- <h2>Payload</h2><br>
36
- <div><pre><%= JSON.pretty_generate(JSON.parse(@payload.to_json)) %></pre></div>
37
- </div>
24
+ <hr/>
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 %>
38
60
  <% unless @routes.blank? %>
39
61
  <div class="row">
40
62
  <h2>Routes</h2>
@@ -0,0 +1,24 @@
1
+ <meta charset="utf-8">
2
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
3
+ <%= csrf_meta_tags %>
4
+ <%= csp_meta_tag rescue nil %>
5
+
6
+ <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/styles/vs.min.css" integrity="sha512-aWjgJTbdG4imzxTxistV5TVNffcYGtIQQm2NBNahV6LmX14Xq9WwZTL1wPjaSglUuVzYgwrq+0EuI4+vKvQHHw==" crossorigin="anonymous" />
8
+ <style>
9
+ h1,h2,h3,h4,h5,h6 { width: 100%; font-weight: normal; }
10
+ h1 { font-size: 2rem; }
11
+ h2 { font-size: 1.7rem; }
12
+ h3 { font-size: 1.5rem; }
13
+ h4 { font-size: 1.3rem; }
14
+ h5 { font-size: 1.1rem; }
15
+ h6 { font-size: 1rem; }
16
+ </style>
17
+
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.2
1
+ 0.0.8
@@ -0,0 +1,2 @@
1
+ require_relative 'controller_mixins/base'
2
+ require_relative 'controller_mixins/models'
@@ -1,63 +1,18 @@
1
1
  module RESTFramework
2
2
 
3
- # This module provides helpers for mixin `ClassMethods` submodules.
4
- module ClassMethodHelpers
5
-
6
- # This helper assists in providing reader interfaces for mixin properties.
7
- def _restframework_attr_reader(property, default: nil)
8
- method = <<~RUBY
9
- def #{property}
10
- return _restframework_try_class_level_variable_get(
11
- #{property.inspect},
12
- default: #{default.inspect},
13
- )
14
- end
15
- RUBY
16
- self.module_eval(method)
17
- end
18
- end
19
-
20
3
  # This module provides the common functionality for any controller mixins, a `root` action, and
21
- # the ability to route arbitrary actions with `@extra_actions`. This is also where `api_response`
4
+ # the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
22
5
  # is defined.
23
6
  module BaseControllerMixin
24
7
  # Default action for API root.
25
- # TODO: use api_response and show sub-routes.
26
8
  def root
27
- render inline: "This is the root of your awesome API!"
9
+ api_response({message: "This is the root of your awesome API!"})
28
10
  end
29
11
 
30
- protected
31
-
32
12
  module ClassMethods
33
- extend ClassMethodHelpers
34
-
35
- # Interface for getting class-level instance/class variables.
36
- private def _restframework_try_class_level_variable_get(name, default: nil)
37
- begin
38
- v = instance_variable_get("@#{name}")
39
- return v unless v.nil?
40
- rescue NameError
41
- end
42
- begin
43
- v = class_variable_get("@@#{name}")
44
- return v unless v.nil?
45
- rescue NameError
46
- end
47
- return default
48
- end
49
-
50
- # Interface for registering exceptions handlers.
51
- # private def _restframework_register_exception_handlers
52
- # rescue_from
53
- # end
54
-
55
- _restframework_attr_reader(:extra_actions, default: {})
56
- _restframework_attr_reader(:template_logo_text, default: 'Rails REST Framework')
57
-
58
- def skip_actions(skip_undefined: true)
13
+ def get_skip_actions(skip_undefined: true)
59
14
  # first, skip explicitly skipped actions
60
- skip = _restframework_try_class_level_variable_get(:skip_actions, default: [])
15
+ skip = self.skip_actions || []
61
16
 
62
17
  # now add methods which don't exist, since we don't want to route those
63
18
  if skip_undefined
@@ -71,7 +26,29 @@ module RESTFramework
71
26
  end
72
27
 
73
28
  def self.included(base)
74
- base.extend ClassMethods
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)
75
52
  end
76
53
 
77
54
  def _get_routes
@@ -87,18 +64,34 @@ module RESTFramework
87
64
  }.select { |r| r[:path].start_with?(request.path) }
88
65
  end
89
66
 
90
- # Helper alias for `respond_to`/`render`, and replace nil responses with blank ones.
91
- def api_response(payload, html_kwargs: nil, json_kwargs: nil, **kwargs)
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)
92
70
  html_kwargs ||= {}
93
71
  json_kwargs ||= {}
72
+ xml_kwargs ||= {}
94
73
 
95
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
96
87
  format.html {
97
- kwargs = kwargs.merge(html_kwargs)
88
+ @payload = payload
89
+ @json_payload = payload.to_json
90
+ @xml_payload = payload.to_xml
98
91
  @template_logo_text ||= "Rails REST Framework"
99
92
  @title ||= self.controller_name.camelize
100
93
  @routes ||= self._get_routes
101
- @payload = payload
94
+ kwargs = kwargs.merge(html_kwargs)
102
95
  begin
103
96
  render(**kwargs)
104
97
  rescue ActionView::MissingTemplate # fallback to rest_framework default view
@@ -106,10 +99,6 @@ module RESTFramework
106
99
  end
107
100
  render(**kwargs)
108
101
  }
109
- format.json {
110
- kwargs = kwargs.merge(json_kwargs)
111
- render(json: payload || '', **kwargs)
112
- }
113
102
  end
114
103
  end
115
104
  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.2
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-17 00:00:00.000000000 Z
11
+ date: 2020-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -33,19 +33,18 @@ extra_rdoc_files: []
33
33
  files:
34
34
  - LICENSE
35
35
  - README.md
36
- - app/assets/images/rest_framework_favicon.ico
37
- - app/assets/javascripts/rest_framework.js
38
- - app/assets/stylesheets/rest_framework.css
39
36
  - app/views/layouts/rest_framework.html.erb
37
+ - app/views/rest_framework/_head.html.erb
40
38
  - app/views/rest_framework/_routes.html.erb
41
39
  - app/views/rest_framework/default.html.erb
42
40
  - lib/rest_framework.rb
43
41
  - lib/rest_framework/VERSION_STAMP
44
- - lib/rest_framework/controllers.rb
45
- - lib/rest_framework/controllers/base.rb
46
- - lib/rest_framework/controllers/models.rb
42
+ - lib/rest_framework/controller_mixins.rb
43
+ - lib/rest_framework/controller_mixins/base.rb
44
+ - lib/rest_framework/controller_mixins/models.rb
47
45
  - lib/rest_framework/engine.rb
48
46
  - lib/rest_framework/routers.rb
47
+ - lib/rest_framework/serializers.rb
49
48
  - lib/rest_framework/version.rb
50
49
  homepage: https://github.com/gregschmit/rails-rest-framework
51
50
  licenses:
File without changes
@@ -1,3 +0,0 @@
1
- h1, h2, h3, h4, h5, h6 {
2
- width: 100%;
3
- }
@@ -1,2 +0,0 @@
1
- require_relative 'controllers/base'
2
- require_relative 'controllers/models'
@@ -1,225 +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
- _restframework_attr_reader(:singleton_controller)
25
-
26
- _restframework_attr_reader(:fields)
27
- _restframework_attr_reader(:list_fields)
28
- _restframework_attr_reader(:show_fields)
29
- _restframework_attr_reader(:create_fields)
30
- _restframework_attr_reader(:update_fields)
31
-
32
- _restframework_attr_reader(:extra_member_actions, default: {})
33
-
34
- # For model-based mixins, `@extra_collection_actions` is synonymous with `@extra_actions`.
35
- # @param skip_undefined [Boolean] whether we should skip routing undefined actions
36
- def extra_actions(skip_undefined: true)
37
- actions = (
38
- _restframework_try_class_level_variable_get(:extra_collection_actions) ||
39
- _restframework_try_class_level_variable_get(:extra_actions, default: {})
40
- )
41
- actions = actions.select { |a| self.method_defined?(a) } if skip_undefined
42
- return actions
43
- end
44
- end
45
-
46
- def self.included(base)
47
- base.extend ClassMethods
48
- end
49
-
50
- protected
51
-
52
- # Get a list of fields for the current action.
53
- def get_fields
54
- return @fields if @fields
55
-
56
- # index action should use list_fields
57
- name = (action_name == 'index') ? 'list' : action_name
58
-
59
- begin
60
- @fields = self.class.send("#{name}_fields")
61
- rescue NameError
62
- end
63
- @fields ||= self.class.fields || []
64
-
65
- return @fields
66
- end
67
-
68
- # Get a configuration passable to `as_json` for the model.
69
- def get_model_serializer_config
70
- fields = self.get_fields
71
- unless fields.blank?
72
- columns, methods = fields.partition { |f| f.to_s.in?(self.get_model.column_names) }
73
- return {only: columns, methods: methods}
74
- end
75
- return {}
76
- end
77
-
78
- # Filter the request body for keys allowed by the current action's field config.
79
- def _get_field_values_from_request_body
80
- return @_get_field_values_from_request_body ||= (request.request_parameters.select { |p|
81
- self.get_fields.include?(p.to_sym) || self.get_fields.include?(p.to_s)
82
- })
83
- end
84
- alias :get_create_params :_get_field_values_from_request_body
85
- alias :get_update_params :_get_field_values_from_request_body
86
-
87
- # Filter params for keys allowed by the current action's field config.
88
- def _get_field_values_from_params
89
- return @_get_field_values_from_params ||= params.permit(*self.get_fields)
90
- end
91
- alias :get_lookup_params :_get_field_values_from_params
92
- alias :get_filter_params :_get_field_values_from_params
93
-
94
- # Get the recordset, filtered by the filter params.
95
- def get_filtered_recordset
96
- filter_params = self.get_filter_params
97
- unless filter_params.blank?
98
- return self.get_recordset.where(**self.get_filter_params)
99
- end
100
- return self.get_recordset
101
- end
102
-
103
- # Get a record by `id` or return a single record if recordset is filtered down to a single record.
104
- def get_record
105
- records = self.get_filtered_recordset
106
- if params['id'] # direct lookup
107
- return records.find(params['id'])
108
- elsif records.length == 1
109
- return records[0]
110
- end
111
- return nil
112
- end
113
-
114
- # Internal interface for get_model, protecting against infinite recursion with get_recordset.
115
- def _get_model(from_internal_get_recordset: false)
116
- return @model if @model
117
- return self.class.model if self.class.model
118
- unless from_internal_get_recordset # prevent infinite recursion
119
- recordset = self._get_recordset(from_internal_get_model: true)
120
- return (@model = recordset.klass) if recordset
121
- end
122
- begin
123
- return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
124
- rescue NameError
125
- end
126
- return nil
127
- end
128
-
129
- # Internal interface for get_recordset, protecting against infinite recursion with get_model.
130
- def _get_recordset(from_internal_get_model: false)
131
- return @recordset if @recordset
132
- return self.class.recordset if self.class.recordset
133
- unless from_internal_get_model # prevent infinite recursion
134
- model = self._get_model(from_internal_get_recordset: true)
135
- return (@recordset = model.all) if model
136
- end
137
- return nil
138
- end
139
-
140
- # Get the model for this controller.
141
- def get_model
142
- return _get_model
143
- end
144
-
145
- # Get the base set of records this controller has access to.
146
- def get_recordset
147
- return _get_recordset
148
- end
149
- end
150
-
151
- module ListModelMixin
152
- # TODO: pagination classes like Django
153
- def index
154
- @records = self.get_filtered_recordset
155
- api_response(@records, **self.get_model_serializer_config)
156
- end
157
- end
158
-
159
- module ShowModelMixin
160
- def show
161
- @record = self.get_record
162
- api_response(@record, **self.get_model_serializer_config)
163
- end
164
- end
165
-
166
- module CreateModelMixin
167
- def create
168
- begin
169
- @record = self.get_model.create!(self.get_create_params)
170
- rescue ActiveRecord::RecordInvalid => e
171
- api_response(e.record.messages, status: 400)
172
- end
173
- api_response(@record, **self.get_model_serializer_config)
174
- end
175
- end
176
-
177
- module UpdateModelMixin
178
- def update
179
- @record = self.get_record
180
- if @record
181
- @record.attributes(self.get_update_params)
182
- @record.save!
183
- api_response(@record, **self.get_model_serializer_config)
184
- else
185
- api_response({detail: "Record not found."}, status: 404)
186
- end
187
- end
188
- end
189
-
190
- module DestroyModelMixin
191
- def destroy
192
- @record = self.get_record
193
- if @record
194
- @record.destroy!
195
- api_response('')
196
- else
197
- api_response({detail: "Method 'DELETE' not allowed."}, status: 405)
198
- end
199
- end
200
- end
201
-
202
- module ReadOnlyModelControllerMixin
203
- include BaseModelControllerMixin
204
- def self.included(base)
205
- base.extend BaseModelControllerMixin::ClassMethods
206
- end
207
-
208
- include ListModelMixin
209
- include ShowModelMixin
210
- end
211
-
212
- module ModelControllerMixin
213
- include BaseModelControllerMixin
214
- def self.included(base)
215
- base.extend BaseModelControllerMixin::ClassMethods
216
- end
217
-
218
- include ListModelMixin
219
- include ShowModelMixin
220
- include CreateModelMixin
221
- include UpdateModelMixin
222
- include DestroyModelMixin
223
- end
224
-
225
- end