rest_framework 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []