rest_framework 0.0.2 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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