rest_framework 0.0.0 → 0.0.6

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: c71a3fea7904490cd834aff65231d4a18804e8cd487a07314e475a73d9a6500e
4
- data.tar.gz: bde5421c92b4c960bc9b09b369a36b2d4df463b0722490c35cbaf00cdb6b6a66
3
+ metadata.gz: a3e577365f00c98803ebf7f4548c10e5e0cde9208d47de71ac5d5193d0e0c776
4
+ data.tar.gz: b0527bc192535edcd85a91587e11df09c30dff460701c826fe6574ec487bc003
5
5
  SHA512:
6
- metadata.gz: 21db95368e8223a1b086e63b9724eb7cb35cb5eb42db9eedb5eca557662be24363f225ab69fb98d2996152c69d58218a5156fdaba1345330660c974313b3f912
7
- data.tar.gz: 2f32d14d77eb8f60b29019afd81f6b926e2bd97268b2cd6d4828698adf4b66debea2959f00403913a6e74a970f858d960a4d3c9268a1f6fcccd9549c1aca992e
6
+ metadata.gz: 805f9fe5d4fcd15f5e360459af7246e52392115f3cf6ab3fe51a2128f16c4092e10ac0797767769992416818cdf5dc429790e128ad7a889119cdb134d739ea61
7
+ data.tar.gz: d0079b9e2cb005938d95aa83cccc3971849183be3209e2c08a2ebae5c357d6c9c571663cd496f68c4a5f8855ea2b804a46fbfcb73d839693b41ce781563599c0
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # REST Framework
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/rest_framework.svg)](https://badge.fury.io/rb/rest_framework)
4
+ [![Build Status](https://travis-ci.org/gregschmit/rails-rest-framework.svg?branch=master)](https://travis-ci.org/gregschmit/rails-rest-framework)
5
+
3
6
  REST Framework helps you build awesome APIs in Ruby on Rails.
4
7
 
5
8
  **The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD
@@ -24,6 +27,78 @@ Or install it yourself with:
24
27
 
25
28
  $ gem install rest_framework
26
29
 
30
+ ## Usage
31
+
32
+ ### Controller Mixins
33
+
34
+ To transform a controller into a RESTful controller, you can either include `BaseControllerMixin`,
35
+ `ReadOnlyModelControllerMixin`, or `ModelControllerMixin`. `BaseControllerMixin` provides a `root`
36
+ action and a simple interface for routing arbitrary additional actions:
37
+
38
+ ```
39
+ class ApiController < ApplicationController
40
+ include RESTFramework::BaseControllerMixin
41
+ @extra_actions = {test: [:get]}
42
+
43
+ def test
44
+ render inline: "Test successful!"
45
+ end
46
+ end
47
+ ```
48
+
49
+ `ModelControllerMixin` assists with providing the standard CRUD for your controller.
50
+
51
+ ```
52
+ class Api::MoviesController < ApiController
53
+ include RESTFramework::ModelControllerMixin
54
+
55
+ @recordset = Movie.where(enabled: true) # by default, @recordset would include all movies
56
+ end
57
+ ```
58
+
59
+ `ReadOnlyModelControllerMixin` only enables list/show actions, but since we're naming this
60
+ controller in a way that doesn't make the model obvious, we can set that explicitly:
61
+
62
+ ```
63
+ class Api::ReadOnlyMoviesController < ApiController
64
+ include RESTFramework::ReadOnlyModelControllerMixin
65
+
66
+ @model = Movie
67
+ end
68
+ ```
69
+
70
+ Note that you can also override `get_model` and `get_recordset` instance methods to override the API
71
+ behavior dynamically per-request.
72
+
73
+ ### Routing
74
+
75
+ 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:
79
+
80
+ ```
81
+ Rails.application.routes.draw do
82
+ rest_root :api # will find `api_controller` and route the `root` action to '/api'
83
+ namespace :api do
84
+ rest_resources :movies
85
+ rest_resources :read_only_movies
86
+ end
87
+ end
88
+ ```
89
+
90
+ Or if you want the API root to be routed to `Api::RootController#root`:
91
+
92
+ ```
93
+ Rails.application.routes.draw do
94
+ namespace :api do
95
+ rest_root # will route `Api::RootController#root` to '/' in this namespace ('/api')
96
+ rest_resources :movies
97
+ rest_resources :read_only_movies
98
+ end
99
+ end
100
+ ```
101
+
27
102
  ## Development/Testing
28
103
 
29
104
  After you clone the repository, cd'ing into the directory should create a new gemset if you are
@@ -37,10 +112,10 @@ To run unit tests:
37
112
 
38
113
  $ rake test:unit
39
114
 
40
- To run integration tests on a sample app:
41
-
42
- $ rake test:app
115
+ To run integration tests:
43
116
 
44
- To run that sample app live:
117
+ $ rake test:integration
45
118
 
46
- $ rake test:app:run
119
+ To interact with the integration app, you can `cd test/integration` and operate it via the normal
120
+ Rails interfaces. Ensure you run `rake db:schema:load` before running `rails server` or
121
+ `rails console`.
@@ -0,0 +1,38 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title><%= @title %></title>
5
+ <%= render partial: 'rest_framework/head' %>
6
+ </head>
7
+
8
+ <body>
9
+ <div class="bg-dark">
10
+ <div class="w-100 m-0 p-0" style="height: .3em; background-color: #a00;"></div>
11
+ <nav class="navbar navbar-dark bg-dark">
12
+ <div class="container">
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>
15
+ </span>
16
+ </div>
17
+ </nav>
18
+ </div>
19
+ <div class="container py-3">
20
+ <div class="container">
21
+ <div class="row">
22
+ <h1><%= @title %></h1>
23
+ </div>
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>
29
+ <% unless @routes.blank? %>
30
+ <div class="row">
31
+ <h2>Routes</h2>
32
+ <%= render partial: 'rest_framework/routes' %>
33
+ </div>
34
+ <% end %>
35
+ </div>
36
+ </div>
37
+ </body>
38
+ </html>
@@ -0,0 +1,19 @@
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://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
7
+ <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; }
15
+ </style>
16
+
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>
@@ -0,0 +1,18 @@
1
+ <table class="table">
2
+ <thead>
3
+ <tr>
4
+ <th scope="col">Verb</th>
5
+ <th scope="col">Path</th>
6
+ <th scope="col">Action</th>
7
+ </tr>
8
+ </thead>
9
+ <tbody>
10
+ <% @routes.each do |r| %>
11
+ <tr>
12
+ <td><%= r[:verb] %></td>
13
+ <td><%= r[:path] %></td>
14
+ <td><%= r[:action] %></td>
15
+ </tr>
16
+ <% end %>
17
+ </tbody>
18
+ </table>
@@ -2,5 +2,6 @@ module RESTFramework
2
2
  end
3
3
 
4
4
  require_relative "rest_framework/controllers"
5
+ require_relative "rest_framework/engine"
5
6
  require_relative "rest_framework/routers"
6
7
  require_relative "rest_framework/version"
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.0.6
@@ -1,6 +1,8 @@
1
1
  module RESTFramework
2
2
 
3
+ # This module provides helpers for mixin `ClassMethods` submodules.
3
4
  module ClassMethodHelpers
5
+ # This helper assists in providing reader interfaces for mixin properties.
4
6
  def _restframework_attr_reader(property, default: nil)
5
7
  method = <<~RUBY
6
8
  def #{property}
@@ -14,11 +16,13 @@ module RESTFramework
14
16
  end
15
17
  end
16
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.
17
22
  module BaseControllerMixin
18
23
  # Default action for API root.
19
- # TODO: use api_response and show sub-routes.
20
24
  def root
21
- render inline: "This is the root of your awesome API!"
25
+ api_response({message: "This is the root of your awesome API!"})
22
26
  end
23
27
 
24
28
  protected
@@ -26,39 +30,90 @@ module RESTFramework
26
30
  module ClassMethods
27
31
  extend ClassMethodHelpers
28
32
 
29
- # Interface for getting class-level instance/class variables.
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.
30
35
  private def _restframework_try_class_level_variable_get(name, default: nil)
31
- begin
32
- v = instance_variable_get("@#{name}")
36
+ instance_variable = "@#{name}"
37
+ if self.instance_variable_defined?(instance_variable)
38
+ v = self.instance_variable_get(instance_variable)
33
39
  return v unless v.nil?
34
- rescue NameError
35
40
  end
36
- begin
37
- v = class_variable_get("@@#{name}")
41
+ class_variable = "@@#{name}"
42
+ if self.class_variable_defined?(class_variable)
43
+ v = self.class_variable_get(class_variable)
38
44
  return v unless v.nil?
39
- rescue NameError
40
45
  end
41
46
  return default
42
47
  end
43
48
 
44
- # Interface for registering exceptions handlers.
45
- # private def _restframework_register_exception_handlers
46
- # rescue_from
47
- # end
48
-
49
+ _restframework_attr_reader(:singleton_controller)
49
50
  _restframework_attr_reader(:extra_actions, default: {})
50
- _restframework_attr_reader(:skip_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
51
66
  end
52
67
 
53
68
  def self.included(base)
54
69
  base.extend ClassMethods
55
70
  end
56
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
+
57
85
  # Helper alias for `respond_to`/`render`, and replace nil responses with blank ones.
58
- def api_response(value, **kwargs)
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
+
59
98
  respond_to do |format|
60
- format.html
61
- format.json { render json: value || '', **kwargs }
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
+ }
62
117
  end
63
118
  end
64
119
  end
@@ -21,7 +21,6 @@ module RESTFramework
21
21
 
22
22
  _restframework_attr_reader(:model)
23
23
  _restframework_attr_reader(:recordset)
24
- _restframework_attr_reader(:singleton_controller)
25
24
 
26
25
  _restframework_attr_reader(:fields)
27
26
  _restframework_attr_reader(:list_fields)
@@ -32,11 +31,14 @@ module RESTFramework
32
31
  _restframework_attr_reader(:extra_member_actions, default: {})
33
32
 
34
33
  # For model-based mixins, `@extra_collection_actions` is synonymous with `@extra_actions`.
35
- def extra_actions
36
- return (
34
+ # @param skip_undefined [Boolean] whether we should skip routing undefined actions
35
+ def extra_actions(skip_undefined: true)
36
+ actions = (
37
37
  _restframework_try_class_level_variable_get(:extra_collection_actions) ||
38
38
  _restframework_try_class_level_variable_get(:extra_actions, default: {})
39
39
  )
40
+ actions = actions.select { |a| self.method_defined?(a) } if skip_undefined
41
+ return actions
40
42
  end
41
43
  end
42
44
 
@@ -48,7 +50,7 @@ module RESTFramework
48
50
 
49
51
  # Get a list of fields for the current action.
50
52
  def get_fields
51
- return @fields if @fields
53
+ return @fields if instance_variable_defined?(:@fields) && @fields
52
54
 
53
55
  # index action should use list_fields
54
56
  name = (action_name == 'index') ? 'list' : action_name
@@ -57,9 +59,8 @@ module RESTFramework
57
59
  @fields = self.class.send("#{name}_fields")
58
60
  rescue NameError
59
61
  end
60
- @fields ||= self.class.fields || []
61
62
 
62
- return @fields
63
+ return @fields ||= self.class.fields || []
63
64
  end
64
65
 
65
66
  # Get a configuration passable to `as_json` for the model.
@@ -109,10 +110,10 @@ module RESTFramework
109
110
  end
110
111
 
111
112
  # Internal interface for get_model, protecting against infinite recursion with get_recordset.
112
- private def _get_model(from_internal_get_recordset: false)
113
- return @model if @model
113
+ def _get_model(from_internal_get_recordset: false)
114
+ return @model if instance_variable_defined?(:@model) && @model
114
115
  return self.class.model if self.class.model
115
- unless from_get_recordset # prevent infinite recursion
116
+ unless from_internal_get_recordset # prevent infinite recursion
116
117
  recordset = self._get_recordset(from_internal_get_model: true)
117
118
  return (@model = recordset.klass) if recordset
118
119
  end
@@ -124,10 +125,10 @@ module RESTFramework
124
125
  end
125
126
 
126
127
  # Internal interface for get_recordset, protecting against infinite recursion with get_model.
127
- private def _get_recordset(from_internal_get_model: false)
128
- return @recordset if @recordset
128
+ def _get_recordset(from_internal_get_model: false)
129
+ return @recordset if instance_variable_defined?(:@recordset) && @recordset
129
130
  return self.class.recordset if self.class.recordset
130
- unless from_get_model # prevent infinite recursion
131
+ unless from_internal_get_model # prevent infinite recursion
131
132
  model = self._get_model(from_internal_get_recordset: true)
132
133
  return (@recordset = model.all) if model
133
134
  end
@@ -149,14 +150,14 @@ module RESTFramework
149
150
  # TODO: pagination classes like Django
150
151
  def index
151
152
  @records = self.get_filtered_recordset
152
- api_response(@records, **self.get_model_serializer_config)
153
+ api_response(@records)
153
154
  end
154
155
  end
155
156
 
156
157
  module ShowModelMixin
157
158
  def show
158
159
  @record = self.get_record
159
- api_response(@record, **self.get_model_serializer_config)
160
+ api_response(@record)
160
161
  end
161
162
  end
162
163
 
@@ -167,7 +168,7 @@ module RESTFramework
167
168
  rescue ActiveRecord::RecordInvalid => e
168
169
  api_response(e.record.messages, status: 400)
169
170
  end
170
- api_response(@record, **self.get_model_serializer_config)
171
+ api_response(@record)
171
172
  end
172
173
  end
173
174
 
@@ -177,7 +178,7 @@ module RESTFramework
177
178
  if @record
178
179
  @record.attributes(self.get_update_params)
179
180
  @record.save!
180
- api_response(@record, **self.get_model_serializer_config)
181
+ api_response(@record)
181
182
  else
182
183
  api_response({detail: "Record not found."}, status: 404)
183
184
  end
@@ -0,0 +1,2 @@
1
+ class RESTFramework::Engine < ::Rails::Engine
2
+ end
@@ -25,10 +25,10 @@ module ActionDispatch::Routing
25
25
 
26
26
  # convert class name to class
27
27
  begin
28
- controller = mod.const_get(name, false)
28
+ controller = mod.const_get(name)
29
29
  rescue NameError
30
30
  if fallback_reverse_pluralization
31
- controller = mod.const_get(name_reverse, false)
31
+ controller = mod.const_get(name_reverse)
32
32
  else
33
33
  raise
34
34
  end
@@ -41,7 +41,8 @@ module ActionDispatch::Routing
41
41
  # @param default_singular [Boolean] the default plurality of the resource if the plurality is
42
42
  # not otherwise defined by the controller
43
43
  # @param name [Symbol] the resource name, from which path and controller are deduced by default
44
- protected def _rest_resources(default_singular, name, **kwargs, &block)
44
+ # @param skip_undefined [Boolean] whether we should skip routing undefined actions
45
+ protected def _rest_resources(default_singular, name, skip_undefined: true, **kwargs, &block)
45
46
  controller = kwargs[:controller] || name
46
47
  if controller.is_a?(Class)
47
48
  controller_class = controller
@@ -50,9 +51,7 @@ module ActionDispatch::Routing
50
51
  end
51
52
 
52
53
  # set controller if it's not explicitly set
53
- unless kwargs[:controller]
54
- kwargs[:controller] = name
55
- end
54
+ kwargs[:controller] = name unless kwargs[:controller]
56
55
 
57
56
  # determine plural/singular resource
58
57
  if kwargs.delete(:force_singular)
@@ -67,10 +66,14 @@ module ActionDispatch::Routing
67
66
  resource_method = singular ? :resource : :resources
68
67
 
69
68
  # call either `resource` or `resources`, passing appropriate modifiers
70
- public_send(resource_method, name, except: controller_class.skip_actions, **kwargs) do
69
+ skip_undefined = kwargs.delete(:skip_undefined) || true
70
+ skip = controller_class.skip_actions(skip_undefined: skip_undefined)
71
+ public_send(resource_method, name, except: skip, **kwargs) do
71
72
  if controller_class.respond_to?(:extra_member_actions)
72
73
  member do
73
- controller_class.extra_member_actions.each do |action, methods|
74
+ actions = controller_class.extra_member_actions
75
+ actions = actions.select { |k,v| controller_class.method_defined?(k) } if skip_undefined
76
+ actions.each do |action, methods|
74
77
  methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
75
78
  methods.each do |m|
76
79
  public_send(m, action)
@@ -80,7 +83,9 @@ module ActionDispatch::Routing
80
83
  end
81
84
 
82
85
  collection do
83
- controller_class.extra_actions.each do |action, methods|
86
+ actions = controller_class.extra_actions
87
+ actions = actions.select { |k,v| controller_class.method_defined?(k) } if skip_undefined
88
+ actions.each do |action, methods|
84
89
  methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
85
90
  methods.each do |m|
86
91
  public_send(m, action)
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.0
4
+ version: 0.0.6
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-16 00:00:00.000000000 Z
11
+ date: 2020-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -33,11 +33,16 @@ extra_rdoc_files: []
33
33
  files:
34
34
  - LICENSE
35
35
  - README.md
36
+ - app/views/layouts/rest_framework.html.erb
37
+ - app/views/rest_framework/_head.html.erb
38
+ - app/views/rest_framework/_routes.html.erb
39
+ - app/views/rest_framework/default.html.erb
36
40
  - lib/rest_framework.rb
37
41
  - lib/rest_framework/VERSION_STAMP
38
42
  - lib/rest_framework/controllers.rb
39
43
  - lib/rest_framework/controllers/base.rb
40
44
  - lib/rest_framework/controllers/models.rb
45
+ - lib/rest_framework/engine.rb
41
46
  - lib/rest_framework/routers.rb
42
47
  - lib/rest_framework/version.rb
43
48
  homepage: https://github.com/gregschmit/rails-rest-framework
@@ -50,6 +55,7 @@ post_install_message:
50
55
  rdoc_options: []
51
56
  require_paths:
52
57
  - lib
58
+ - app
53
59
  required_ruby_version: !ruby/object:Gem::Requirement
54
60
  requirements:
55
61
  - - ">="