rest_framework 0.0.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c71a3fea7904490cd834aff65231d4a18804e8cd487a07314e475a73d9a6500e
4
+ data.tar.gz: bde5421c92b4c960bc9b09b369a36b2d4df463b0722490c35cbaf00cdb6b6a66
5
+ SHA512:
6
+ metadata.gz: 21db95368e8223a1b086e63b9724eb7cb35cb5eb42db9eedb5eca557662be24363f225ab69fb98d2996152c69d58218a5156fdaba1345330660c974313b3f912
7
+ data.tar.gz: 2f32d14d77eb8f60b29019afd81f6b926e2bd97268b2cd6d4828698adf4b66debea2959f00403913a6e74a970f858d960a4d3c9268a1f6fcccd9549c1aca992e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Gregory N. Schmit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,46 @@
1
+ # REST Framework
2
+
3
+ REST Framework helps you build awesome APIs in Ruby on Rails.
4
+
5
+ **The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD
6
+ logic, and routing them can be obnoxious.
7
+
8
+ **The Solution**: This gem handles the common logic so you can focus on the parts of your API which
9
+ make it unique.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'rest_framework'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle install
22
+
23
+ Or install it yourself with:
24
+
25
+ $ gem install rest_framework
26
+
27
+ ## Development/Testing
28
+
29
+ After you clone the repository, cd'ing into the directory should create a new gemset if you are
30
+ using RVM. Then run `bundle install` to install the appropriate gems.
31
+
32
+ To run the full test suite:
33
+
34
+ $ rake test
35
+
36
+ To run unit tests:
37
+
38
+ $ rake test:unit
39
+
40
+ To run integration tests on a sample app:
41
+
42
+ $ rake test:app
43
+
44
+ To run that sample app live:
45
+
46
+ $ rake test:app:run
@@ -0,0 +1,6 @@
1
+ module RESTFramework
2
+ end
3
+
4
+ require_relative "rest_framework/controllers"
5
+ require_relative "rest_framework/routers"
6
+ require_relative "rest_framework/version"
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,2 @@
1
+ require_relative 'controllers/base'
2
+ require_relative 'controllers/models'
@@ -0,0 +1,66 @@
1
+ module RESTFramework
2
+
3
+ module ClassMethodHelpers
4
+ def _restframework_attr_reader(property, default: nil)
5
+ method = <<~RUBY
6
+ def #{property}
7
+ return _restframework_try_class_level_variable_get(
8
+ #{property.inspect},
9
+ default: #{default.inspect},
10
+ )
11
+ end
12
+ RUBY
13
+ self.module_eval(method)
14
+ end
15
+ end
16
+
17
+ module BaseControllerMixin
18
+ # Default action for API root.
19
+ # TODO: use api_response and show sub-routes.
20
+ def root
21
+ render inline: "This is the root of your awesome API!"
22
+ end
23
+
24
+ protected
25
+
26
+ module ClassMethods
27
+ extend ClassMethodHelpers
28
+
29
+ # Interface for getting class-level instance/class variables.
30
+ private def _restframework_try_class_level_variable_get(name, default: nil)
31
+ begin
32
+ v = instance_variable_get("@#{name}")
33
+ return v unless v.nil?
34
+ rescue NameError
35
+ end
36
+ begin
37
+ v = class_variable_get("@@#{name}")
38
+ return v unless v.nil?
39
+ rescue NameError
40
+ end
41
+ return default
42
+ end
43
+
44
+ # Interface for registering exceptions handlers.
45
+ # private def _restframework_register_exception_handlers
46
+ # rescue_from
47
+ # end
48
+
49
+ _restframework_attr_reader(:extra_actions, default: {})
50
+ _restframework_attr_reader(:skip_actions, default: [])
51
+ end
52
+
53
+ def self.included(base)
54
+ base.extend ClassMethods
55
+ end
56
+
57
+ # Helper alias for `respond_to`/`render`, and replace nil responses with blank ones.
58
+ def api_response(value, **kwargs)
59
+ respond_to do |format|
60
+ format.html
61
+ format.json { render json: value || '', **kwargs }
62
+ end
63
+ end
64
+ end
65
+
66
+ end
@@ -0,0 +1,222 @@
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
+ def extra_actions
36
+ return (
37
+ _restframework_try_class_level_variable_get(:extra_collection_actions) ||
38
+ _restframework_try_class_level_variable_get(:extra_actions, default: {})
39
+ )
40
+ end
41
+ end
42
+
43
+ def self.included(base)
44
+ base.extend ClassMethods
45
+ end
46
+
47
+ protected
48
+
49
+ # Get a list of fields for the current action.
50
+ def get_fields
51
+ return @fields if @fields
52
+
53
+ # index action should use list_fields
54
+ name = (action_name == 'index') ? 'list' : action_name
55
+
56
+ begin
57
+ @fields = self.class.send("#{name}_fields")
58
+ rescue NameError
59
+ end
60
+ @fields ||= self.class.fields || []
61
+
62
+ return @fields
63
+ end
64
+
65
+ # Get a configuration passable to `as_json` for the model.
66
+ def get_model_serializer_config
67
+ fields = self.get_fields
68
+ unless fields.blank?
69
+ columns, methods = fields.partition { |f| f.to_s.in?(self.get_model.column_names) }
70
+ return {only: columns, methods: methods}
71
+ end
72
+ return {}
73
+ end
74
+
75
+ # Filter the request body for keys allowed by the current action's field config.
76
+ def _get_field_values_from_request_body
77
+ return @_get_field_values_from_request_body ||= (request.request_parameters.select { |p|
78
+ self.get_fields.include?(p.to_sym) || self.get_fields.include?(p.to_s)
79
+ })
80
+ end
81
+ alias :get_create_params :_get_field_values_from_request_body
82
+ alias :get_update_params :_get_field_values_from_request_body
83
+
84
+ # Filter params for keys allowed by the current action's field config.
85
+ def _get_field_values_from_params
86
+ return @_get_field_values_from_params ||= params.permit(*self.get_fields)
87
+ end
88
+ alias :get_lookup_params :_get_field_values_from_params
89
+ alias :get_filter_params :_get_field_values_from_params
90
+
91
+ # Get the recordset, filtered by the filter params.
92
+ def get_filtered_recordset
93
+ filter_params = self.get_filter_params
94
+ unless filter_params.blank?
95
+ return self.get_recordset.where(**self.get_filter_params)
96
+ end
97
+ return self.get_recordset
98
+ end
99
+
100
+ # Get a record by `id` or return a single record if recordset is filtered down to a single record.
101
+ def get_record
102
+ records = self.get_filtered_recordset
103
+ if params['id'] # direct lookup
104
+ return records.find(params['id'])
105
+ elsif records.length == 1
106
+ return records[0]
107
+ end
108
+ return nil
109
+ end
110
+
111
+ # 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
114
+ return self.class.model if self.class.model
115
+ unless from_get_recordset # prevent infinite recursion
116
+ recordset = self._get_recordset(from_internal_get_model: true)
117
+ return (@model = recordset.klass) if recordset
118
+ end
119
+ begin
120
+ return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
121
+ rescue NameError
122
+ end
123
+ return nil
124
+ end
125
+
126
+ # 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
129
+ return self.class.recordset if self.class.recordset
130
+ unless from_get_model # prevent infinite recursion
131
+ model = self._get_model(from_internal_get_recordset: true)
132
+ return (@recordset = model.all) if model
133
+ end
134
+ return nil
135
+ end
136
+
137
+ # Get the model for this controller.
138
+ def get_model
139
+ return _get_model
140
+ end
141
+
142
+ # Get the base set of records this controller has access to.
143
+ def get_recordset
144
+ return _get_recordset
145
+ end
146
+ end
147
+
148
+ module ListModelMixin
149
+ # TODO: pagination classes like Django
150
+ def index
151
+ @records = self.get_filtered_recordset
152
+ api_response(@records, **self.get_model_serializer_config)
153
+ end
154
+ end
155
+
156
+ module ShowModelMixin
157
+ def show
158
+ @record = self.get_record
159
+ api_response(@record, **self.get_model_serializer_config)
160
+ end
161
+ end
162
+
163
+ module CreateModelMixin
164
+ def create
165
+ begin
166
+ @record = self.get_model.create!(self.get_create_params)
167
+ rescue ActiveRecord::RecordInvalid => e
168
+ api_response(e.record.messages, status: 400)
169
+ end
170
+ api_response(@record, **self.get_model_serializer_config)
171
+ end
172
+ end
173
+
174
+ module UpdateModelMixin
175
+ def update
176
+ @record = self.get_record
177
+ if @record
178
+ @record.attributes(self.get_update_params)
179
+ @record.save!
180
+ api_response(@record, **self.get_model_serializer_config)
181
+ else
182
+ api_response({detail: "Record not found."}, status: 404)
183
+ end
184
+ end
185
+ end
186
+
187
+ module DestroyModelMixin
188
+ def destroy
189
+ @record = self.get_record
190
+ if @record
191
+ @record.destroy!
192
+ api_response('')
193
+ else
194
+ api_response({detail: "Method 'DELETE' not allowed."}, status: 405)
195
+ end
196
+ end
197
+ end
198
+
199
+ module ReadOnlyModelControllerMixin
200
+ include BaseModelControllerMixin
201
+ def self.included(base)
202
+ base.extend BaseModelControllerMixin::ClassMethods
203
+ end
204
+
205
+ include ListModelMixin
206
+ include ShowModelMixin
207
+ end
208
+
209
+ module ModelControllerMixin
210
+ include BaseModelControllerMixin
211
+ def self.included(base)
212
+ base.extend BaseModelControllerMixin::ClassMethods
213
+ end
214
+
215
+ include ListModelMixin
216
+ include ShowModelMixin
217
+ include CreateModelMixin
218
+ include UpdateModelMixin
219
+ include DestroyModelMixin
220
+ end
221
+
222
+ end
@@ -0,0 +1,132 @@
1
+ require 'rails'
2
+ require 'action_dispatch/routing/mapper'
3
+
4
+ module ActionDispatch::Routing
5
+ class Mapper
6
+ # Private interface to get the controller class from the name and current scope.
7
+ protected def _get_controller_class(name, pluralize: true, fallback_reverse_pluralization: true)
8
+ # get class name
9
+ name = name.to_s.camelize # camelize to leave plural names plural
10
+ name = name.pluralize if pluralize
11
+ if name == name.pluralize
12
+ name_reverse = name.singularize
13
+ else
14
+ name_reverse = name.pluralize
15
+ end
16
+ name += "Controller"
17
+ name_reverse += "Controller"
18
+
19
+ # get scope for the class
20
+ if @scope[:module]
21
+ mod = @scope[:module].to_s.classify.constantize
22
+ else
23
+ mod = Object
24
+ end
25
+
26
+ # convert class name to class
27
+ begin
28
+ controller = mod.const_get(name, false)
29
+ rescue NameError
30
+ if fallback_reverse_pluralization
31
+ controller = mod.const_get(name_reverse, false)
32
+ else
33
+ raise
34
+ end
35
+ end
36
+
37
+ return controller
38
+ end
39
+
40
+ # Core implementation of the `rest_resource(s)` router, both singular and plural.
41
+ # @param default_singular [Boolean] the default plurality of the resource if the plurality is
42
+ # not otherwise defined by the controller
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)
45
+ controller = kwargs[:controller] || name
46
+ if controller.is_a?(Class)
47
+ controller_class = controller
48
+ else
49
+ controller_class = _get_controller_class(controller, pluralize: !default_singular)
50
+ end
51
+
52
+ # set controller if it's not explicitly set
53
+ unless kwargs[:controller]
54
+ kwargs[:controller] = name
55
+ end
56
+
57
+ # determine plural/singular resource
58
+ if kwargs.delete(:force_singular)
59
+ singular = true
60
+ elsif kwargs.delete(:force_plural)
61
+ singular = false
62
+ elsif !controller_class.singleton_controller.nil?
63
+ singular = controller_class.singleton_controller
64
+ else
65
+ singular = default_singular
66
+ end
67
+ resource_method = singular ? :resource : :resources
68
+
69
+ # call either `resource` or `resources`, passing appropriate modifiers
70
+ public_send(resource_method, name, except: controller_class.skip_actions, **kwargs) do
71
+ if controller_class.respond_to?(:extra_member_actions)
72
+ member do
73
+ controller_class.extra_member_actions.each do |action, methods|
74
+ methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
75
+ methods.each do |m|
76
+ public_send(m, action)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ collection do
83
+ controller_class.extra_actions.each do |action, methods|
84
+ methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
85
+ methods.each do |m|
86
+ public_send(m, action)
87
+ end
88
+ end
89
+ end
90
+
91
+ yield if block_given?
92
+ end
93
+ end
94
+
95
+ # Public interface for creating singular RESTful resource routes.
96
+ def rest_resource(*names, **kwargs, &block)
97
+ names.each do |n|
98
+ self._rest_resources(true, n, **kwargs, &block)
99
+ end
100
+ end
101
+
102
+ # Public interface for creating plural RESTful resource routes.
103
+ def rest_resources(*names, **kwargs, &block)
104
+ names.each do |n|
105
+ self._rest_resources(false, n, **kwargs, &block)
106
+ end
107
+ end
108
+
109
+ # Route a controller's `#root` to '/' in the current scope/namespace, along with other actions.
110
+ # @param label [Symbol] the snake_case name of the controller
111
+ def rest_root(path=nil, **kwargs, &block)
112
+ # by default, use RootController#root
113
+ root_action = kwargs.delete(:action) || :root
114
+ controller = kwargs.delete(:controller) || path || :root
115
+ path = path.to_s
116
+
117
+ # route the root
118
+ get path, controller: controller, action: root_action
119
+
120
+ # route any additional actions
121
+ controller_class = self._get_controller_class(controller, pluralize: false)
122
+ controller_class.extra_actions.each do |action, methods|
123
+ methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
124
+ methods.each do |m|
125
+ public_send(m, File.join(path, action.to_s), controller: controller, action: action)
126
+ end
127
+ yield if block_given?
128
+ end
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,35 @@
1
+ module RESTFramework
2
+ module Version
3
+ @_version = nil
4
+
5
+ def self.get_version
6
+ # Return cached @_version, if available.
7
+ return @_version if @_version
8
+
9
+ # First, attempt to get the version from git.
10
+ begin
11
+ version = `git describe`.strip
12
+ raise "blank version" if version.nil? || version.match(/^\w*$/)
13
+ # Check for local changes.
14
+ changes = `git status --porcelain`
15
+ version << '.localchanges' if changes.strip.length > 0
16
+ return version
17
+ rescue
18
+ end
19
+
20
+ # Git failed, so try to find a VERSION_STAMP.
21
+ begin
22
+ version = File.read(File.expand_path("VERSION_STAMP", __dir__))
23
+ unless version.nil? || version.match(/^\w*$/)
24
+ return (@_version = version) # cache VERSION_STAMP content in @_version
25
+ end
26
+ rescue
27
+ end
28
+
29
+ # No VERSION_STAMP, so version is unknown.
30
+ return '0.unknown'
31
+ end
32
+ end
33
+
34
+ VERSION = Version.get_version()
35
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rest_framework
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Gregory N. Schmit
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-09-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ description: A framework for DRY RESTful APIs in Ruby on Rails.
28
+ email:
29
+ - schmitgreg@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.md
36
+ - lib/rest_framework.rb
37
+ - lib/rest_framework/VERSION_STAMP
38
+ - lib/rest_framework/controllers.rb
39
+ - lib/rest_framework/controllers/base.rb
40
+ - lib/rest_framework/controllers/models.rb
41
+ - lib/rest_framework/routers.rb
42
+ - lib/rest_framework/version.rb
43
+ homepage: https://github.com/gregschmit/rails-rest-framework
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ homepage_uri: https://github.com/gregschmit/rails-rest-framework
48
+ source_code_uri: https://github.com/gregschmit/rails-rest-framework
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 2.3.0
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.0.8
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: A framework for DRY RESTful APIs in Ruby on Rails.
68
+ test_files: []