rest_framework 0.0.0 → 0.0.6

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