oxidizer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ed6b93890738ae60feb4c4d579bb291eda9086fe768b8adb5487a124f7629539
4
+ data.tar.gz: 46efbd146e216d26325c1fb5024781cce988599b192035a8ad53bb4f00acf508
5
+ SHA512:
6
+ metadata.gz: cfb69290fbeb108ed02fbabbed97370d3a4fca008a27be406421ba7fb85305896cdb8b22ef926d0ae298d7c6d0eb988e45ba170496ab08a57a936800b066fb75
7
+ data.tar.gz: 5de271eb48c4ef95ed18e353270d3359b1e12ef1e5c1a0b7a85dce8efe66f16a2b86689a1b6202a1a0fab039f313d11a2c92be30d865ddaf46f3c786db5b16d4
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Brad Gessler
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Oxidizer
2
+
3
+ Rails controllers require a lot of boilerplate for non-trivial Rails applications. Oxidizer resources a lot of the boilerplate and spaghetti code typically seen in Rails controllers by:
4
+
5
+ 1. Moves authorization out of controller methods and callbacks and into policy objects via Pundit.
6
+ 2. Encourage the use of more, but smaller, controllers to handle various interactions with ActiveRecord objects and other Resources.
7
+ 3. Utiliziers PORO and inheritance for making controller code less verbose, as opposed to a DSL approach, which can be difficult to extend and obscufates how Rails controllers work.
8
+ 4. Encourages keeping business logic out of ActiveRecord objects **and** controllers by utilizing Resource objects.
9
+
10
+ Putting that all together, a typical Oxidizer controller that handles CRUD actions for a blog comment feature would look like this:
11
+
12
+ ```ruby
13
+ # Example Oxidizer controller for comments in a blog post.
14
+ class CommentsController < Oxidizer::NestedResourcesController
15
+ protected
16
+ def self.resource
17
+ Comment
18
+ end
19
+
20
+ def self.parent_resource
21
+ Post
22
+ end
23
+
24
+ def assign_attributes
25
+ @comment.user = current_user
26
+ @comment.blog = @post
27
+ end
28
+
29
+ def permitted_params
30
+ [:post_id, :body]
31
+ end
32
+ end
33
+ ```
34
+
35
+ Since there's no DSLs, its easy to extend Oxidizer controllers to implement any type of behavior you need.
36
+
37
+ ## Installation
38
+
39
+ Add to your Rails application Gemfile by executing:
40
+
41
+ ```bash
42
+ bundle add "oxidizer"
43
+ ```
44
+
45
+ Then run:
46
+
47
+ ```bash
48
+ # TODO: Not implemented yet
49
+ rails generate oxidizer:install
50
+ ```
51
+
52
+ This will create the folders and files needed to get going with Oxidizer.
53
+
54
+ ```txt
55
+ # TODO: Not implemented yet
56
+ app/controllers/application_resources_controller.rb
57
+ ```
58
+
59
+ ## Concepts
60
+
61
+ Oxidizer makes it easy to build RESTful Rails applications that follow the CRUD controller pattern and shallow routes.
62
+
63
+ ## Controller types
64
+
65
+ There's a few types of controllers you'll want to use:
66
+
67
+ ### ResourcesController
68
+
69
+ The most common type of controller is a resources controller. Its very much like a vanilla RESTful Rails controller where `index` is the collection of resources and `new`, `create`, `show`, `edit`, `update`, and `destroy` operate on the singular resource.
70
+
71
+ For example, a blog web application might have a `Posts` Resources controller.
72
+
73
+ ### ResourceController
74
+
75
+ Similar to above, but does not have an `index` action. Singular resources are commonly used in web applications for managing the current users profile and associated resources.
76
+
77
+ For example, a blog web application might have a `Session` Resource controller that the user can create when they login and destroy when they log out.
78
+
79
+ ### NestedResources
80
+
81
+ Nested resources are designed to be scoped within a `Resources`. They have `new`, `create`, and `index` actions, but do not have the remaining actions. The remaining CRUD actions for a nested resource should be `Resources` controller.
82
+
83
+ For example, a blog's `Post` resources might have many `Comment` resources per post. The creation of the comment is within the context of the `Post` resource. After the `Comment` resource is created, the `Post` should be persisted in the `Comment` (probably as `comments.post_id`) if it needs to be accessible after its persisted.
84
+
85
+ It's possible to have the other CRUD actions in a nested resource, but its discourage since nesting controller scopes can be difficult to maintain as dependencies and business logic change. Best to keep themn flat.
86
+
87
+ ### NestedResource
88
+
89
+ A nested resources is similar to the nested resources, but is singular. For example, a `Post` may have an `Author` resource at `posts/:id/author`. The singular nested resource supports the full range of CRUD actions, but does not have `index`.
90
+
91
+ ### NestedWeakResource
92
+
93
+ A nested weak resource is on where the underlying resource is the same as the parent resource.
94
+
95
+ For example, a `Post` may require a confirmation screen before its deleted available at `posts/:id/delete_confirmation/new`. The user would press the `Confirm deletion` button on that screen which would `POST` to `/posts/:id/delete_confirmation` and destroy the object.
96
+
97
+ ## Contributing
98
+
99
+ Open issues with reproducable steps.
100
+
101
+ ## License
102
+
103
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
File without changes
@@ -0,0 +1,32 @@
1
+ module Oxidizer
2
+ # Controller for nesting a singular resource within a parent resource. This
3
+ # should only be used for creating a new resource within the scope of the parent
4
+ # resource, or to redirect to the un-nested resource location if it exists.
5
+ class NestedResourceController < NestedResourcesController
6
+ def show
7
+ # Disabled for show because show will only redirect to either
8
+ # the new resource or to the existing resource, which both have
9
+ # authorizations of their own. I did this here and not as a `skip_after_filter`
10
+ # to add safety for a future engineer who might try to incorrectly override
11
+ # this action and render the resource. If that happened, authorization would
12
+ # be disabled for them, which wouldn't be great.
13
+ skip_authorization
14
+ redirect_to resource.present? ? existing_resource_url : new_resource_url
15
+ end
16
+
17
+ protected
18
+ def existing_resource_url
19
+ url_for resource
20
+ end
21
+
22
+ def new_resource_url
23
+ url_for action: :new
24
+ end
25
+
26
+ # A single nested resource should only have one resource under
27
+ # the scope of the parent resources.
28
+ def find_resource
29
+ resources.first
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,91 @@
1
+ module Oxidizer
2
+ class NestedResourcesController < ResourcesController
3
+ before_action :assign_parent_resource_instance_variable
4
+ before_action :assign_resources_instance_variable, only: :index
5
+ before_action :authorize_parent_resource
6
+
7
+ helper_method :parent_resource
8
+
9
+ protected
10
+ def self.parent_resource
11
+ raise NotImplementedError, "NestedResourcesController.parent_resource must be an ActiveModel or ActiveRecord class"
12
+ end
13
+
14
+ def resources
15
+ @_resources ||= nested_resource_scope
16
+ end
17
+
18
+ # Use callbacks to share common setup or constraints between actions.
19
+ def assign_parent_resource_instance_variable
20
+ instance_variable_set("@#{parent_resource_name}", parent_resource)
21
+ end
22
+
23
+ def parent_resource
24
+ @_parent_resource ||= find_parent_resource
25
+ end
26
+
27
+ def find_parent_resource
28
+ self.class.parent_resource.find_resource params[parent_resource_id_param]
29
+ end
30
+
31
+ # Finds the account of the resource depending on the request type and
32
+ # the parent resource.
33
+ def find_account
34
+ if member_request?
35
+ resource.account
36
+ elsif parent_resource.is_a? Account
37
+ parent_resource
38
+ else
39
+ parent_resource.account
40
+ end
41
+ end
42
+
43
+ # If we're deep, we want to show only members that are scoped
44
+ # from within an index.
45
+ def nested_resource_scope
46
+ query = {}
47
+ query[parent_resource_foreign_key] = parent_resource
48
+ resource_scope.where(**query)
49
+ end
50
+
51
+ # Assumes the route key is the foreign key, which is usually the case.
52
+ # This can be overridden if its not the case or the `nested_resource_scope`
53
+ # can be over-ridden.
54
+ def parent_resource_id_param
55
+ parent_resource_foreign_key
56
+ end
57
+
58
+ # Key used to find the parent resource via ActiveRecord. Typically this is the primary key of the record,
59
+ # but it would be a different field if you don't want to expose users to primary keys.
60
+ def parent_active_record_id
61
+ :id
62
+ end
63
+
64
+ # If the user doesn't have `show?` priviledge on the parent resource,
65
+ # then its highly likely they won't be authorized to do anything with
66
+ # the child resource. This isn't 100% true, but I'm having a hard time
67
+ # thinking of a practical edge case.
68
+ def authorize_parent_resource
69
+ authorize parent_resource, :show?
70
+ end
71
+
72
+ private
73
+ def resource_params
74
+ # Optionally allow the resource params because nested resources usually
75
+ # allow a POST request with no params that create a resource.
76
+ if params.key? resource_name
77
+ params.require(resource_name).permit(permitted_params)
78
+ end
79
+ end
80
+
81
+ # Gets the resource name of the ActiveRecord model for use by
82
+ # instance methods in this controller.
83
+ def parent_resource_name
84
+ self.class.parent_resource.model_name.singular
85
+ end
86
+
87
+ def parent_resource_foreign_key
88
+ "#{parent_resource_name}_id".to_sym
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,33 @@
1
+ module Oxidizer
2
+ # This controller is designed for a "Weak" resource entity, or the `parent_resource`
3
+ # is the same as the `resource`. This is useful if you want to have complex view logic
4
+ # in place to change a few parameters on a model.
5
+ class NestedWeakResourceController < NestedResourcesController
6
+ def self.resource
7
+ parent_resource
8
+ end
9
+
10
+ protected
11
+ # Prevents callbacks from superclasses from firing that `resources`, not `resource`
12
+ # routes fire. This is a non-obvios way to keep the inheritance on the simpler side.
13
+ # TODO: Can I infer a plural vs singular controller in a better way from params? I
14
+ # could look at action names, but those aren't great. Hmm.
15
+ def member_request?
16
+ true
17
+ end
18
+
19
+ def find_resource
20
+ parent_resource
21
+ end
22
+
23
+ # This is for the `account_layout` helper in all sub-classes.
24
+ def find_account
25
+ parent_resource.account
26
+ end
27
+
28
+ def assign_new_resource
29
+ # Do nothing; resource is already created, you're
30
+ # just doing something to it.
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,253 @@
1
+ module Oxidizer
2
+ class ResourcesController < ApplicationController
3
+ before_action :authenticate_user
4
+ before_action :assign_resource_instance_variable, if: :member_request?
5
+ before_action :authorize_resource, if: :member_request?
6
+ before_action :assign_resources_instance_variable, only: :index
7
+
8
+ helper_method \
9
+ :resource_name,
10
+ :resource_class,
11
+ :resource,
12
+ :resources,
13
+ :created_resource,
14
+ :updated_resource
15
+
16
+ # These are used to contain the serialized IDs for resources so they can
17
+ # be accessed from views on the other side of the action that's performed.
18
+ add_flash_types :created_resource, :updated_resource
19
+
20
+ def self.resource
21
+ raise NotImplementedError, "ResourcesController.resource must be an ActiveModel or ActiveRecord class"
22
+ end
23
+
24
+ def index
25
+ end
26
+
27
+ def show
28
+ end
29
+
30
+ def new
31
+ assign_new_resource
32
+ assign_attributes
33
+ authorize_resource
34
+ end
35
+
36
+ def edit
37
+ end
38
+
39
+ def create
40
+ self.resource = resource_class.new(resource_params)
41
+ assign_attributes
42
+ authorize_resource
43
+
44
+ respond_to do |format|
45
+ if resource.save
46
+ flash_created_resource
47
+ format.html { redirect_to create_redirect_url, notice: create_notice }
48
+ format.json { render :show, status: :created, location: resource }
49
+ create_success_formats format
50
+ else
51
+ format.html { render :new, status: :unprocessable_entity }
52
+ format.json { render json: resource.errors, status: :unprocessable_entity }
53
+ end
54
+ end
55
+ end
56
+
57
+ def update
58
+ resource.assign_attributes(resource_params)
59
+ assign_attributes
60
+ authorize_resource
61
+
62
+ respond_to do |format|
63
+ if resource.save
64
+ flash_updated_resource
65
+ format.html { redirect_to update_redirect_url, notice: update_notice }
66
+ format.json { render :show, status: :ok, location: resource }
67
+ else
68
+ format.html { render :edit, status: :unprocessable_entity }
69
+ format.json { render json: resource.errors, status: :unprocessable_entity }
70
+ end
71
+ end
72
+ end
73
+
74
+ def destroy
75
+ resource.destroy
76
+
77
+ respond_to do |format|
78
+ format.html { redirect_to destroy_redirect_url, notice: destroy_notice }
79
+ format.json { head :no_content }
80
+ end
81
+ end
82
+
83
+ protected
84
+ def member_request?
85
+ params.key? resource_id_param
86
+ end
87
+
88
+ def collection_request?
89
+ !member_request?
90
+ end
91
+
92
+ # Gets the resource name of the ActiveRecord model for use by
93
+ # instance methods in this controller.
94
+ def resource_name
95
+ resource_class.model_name.singular
96
+ end
97
+
98
+ def resources_name
99
+ resource_class.model_name.plural
100
+ end
101
+
102
+ def resource_class
103
+ self.class.resource
104
+ end
105
+
106
+ # Permitted params the resource controller allows
107
+ def permitted_params
108
+ []
109
+ end
110
+
111
+ # A scope used for collections scoped by Pundit auth.
112
+ def resource_scope
113
+ policy_scope.joins(:user)
114
+ end
115
+
116
+ # `policy_scope` is defined by Pundit.
117
+ def policy_scope(scope = resource_class)
118
+ super(scope)
119
+ end
120
+
121
+ # Sets instance variable for templates to match the model name. For
122
+ # example, `Account` model name would set the `@accounts` instance variable
123
+ # for template access.
124
+ def assign_resources_instance_variable
125
+ instance_variable_set("@#{resources_name}", resources)
126
+ end
127
+
128
+ # A hook that allows sub-classes to assign attributes to a model.
129
+ def assign_attributes
130
+ resource.user = current_user if resource.respond_to? :user=
131
+ end
132
+
133
+ # Redirect to this url after a resource is created
134
+ def create_redirect_url
135
+ resource
136
+ end
137
+
138
+ # Redirect to this url after a resource is updated
139
+ def update_redirect_url
140
+ resource
141
+ end
142
+
143
+ # Redirect to this url after a resource is destroyed
144
+ def destroy_redirect_url
145
+ resources_name.to_sym
146
+ end
147
+
148
+ # Key rails routing uses to find resource. Rails resources defaults to the `:id` value.
149
+ def resource_id_param
150
+ :id
151
+ end
152
+
153
+ # Use callbacks to share common setup or constraints between actions.
154
+ def assign_resource_instance_variable
155
+ instance_variable_set("@#{resource_name}", resource)
156
+ end
157
+
158
+ def resource
159
+ @_resource ||= find_resource
160
+ end
161
+
162
+ # Sometimes we need to set the resource to hack the controller from another method, such as the
163
+ # case of setting the instance variable to be an instance of a model.
164
+ def resource=(value)
165
+ @_resource = value
166
+ assign_resource_instance_variable
167
+ end
168
+
169
+ # Finds resource, which is called by `assign_resource` to assign it to the right variables.
170
+ def find_resource
171
+ resource_class.find_resource params[resource_id_param]
172
+ end
173
+
174
+ # Initializes a model for the `new` action.
175
+ def assign_new_resource
176
+ self.resource = resource_class.new
177
+ end
178
+
179
+ def resources
180
+ @_resources ||= resource_scope
181
+ end
182
+
183
+ # Additional formats can be specified for successful response creations
184
+ def create_success_formats(format)
185
+ end
186
+
187
+ # Authorizse resource with Pundit.
188
+ def authorize_resource(*args)
189
+ authorize resource, *args
190
+ end
191
+
192
+ # `flash[:notice]` message when a resource is successfully created
193
+ def create_notice
194
+ "#{notice_resource_name} created"
195
+ end
196
+
197
+ # `flash[:notice]` message when a resource is successfully updated
198
+ def update_notice
199
+ "#{notice_resource_name} updated"
200
+ end
201
+
202
+ # `flash[:notice]` message when a resource is successfully deleted
203
+ def destroy_notice
204
+ "#{notice_resource_name} deleted"
205
+ end
206
+
207
+ # What do you call the things that are created?
208
+ def notice_resource_name
209
+ resource_name.humanize.capitalize
210
+ end
211
+
212
+ # Get the current account of the resource, if possible.
213
+ def find_account
214
+ resource.account if member_request?
215
+ end
216
+
217
+ private
218
+ # Never trust parameters from the scary internet, only allow the white list through.
219
+ def resource_params
220
+ params.require(resource_name).permit(permitted_params)
221
+ end
222
+
223
+ # Assign global_id to resource that was just created. This is used in subsequent
224
+ # screens to display a link or information about the resource that was just created.
225
+ def flash_created_resource
226
+ flash[:created_resource] = resource_global_uri
227
+ end
228
+
229
+ # Resource that was created from the last action.
230
+ def created_resource
231
+ @created_resource ||= GlobalID::Locator.locate flash[:created_resource]
232
+ end
233
+
234
+ # Assign global_id to resource that was just created. This is used in subsequent
235
+ # screens to display a link or information about the resource that was just created.
236
+ def flash_updated_resource
237
+ flash[:updated_resource] = resource_global_uri
238
+ end
239
+
240
+ # Generates a GlobalID object that can be serialized into flash for the next action to
241
+ # pickup and find the object as an updated or created resource. The reason this check
242
+ # exists is because some resources are ApplicationModels (NOT Records) and don't have
243
+ # a way to find via ActiveRecord queries.
244
+ def resource_global_uri
245
+ resource.to_global_id.uri if resource.respond_to? :to_global_id
246
+ end
247
+
248
+ # Resource that was updated from the last action.
249
+ def updated_resource
250
+ @updated_resource ||= GlobalID::Locator.locate flash[:updated_resource]
251
+ end
252
+ end
253
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -0,0 +1,4 @@
1
+ module Oxidizer
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module Oxidizer
2
+ VERSION = "0.1.0"
3
+ end
data/lib/oxidizer.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "oxidizer/version"
2
+ require "oxidizer/engine"
3
+
4
+ module Oxidizer
5
+ # Your code goes here...
6
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :oxidizer do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oxidizer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Brad Gessler
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-06-07 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: 7.0.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.3
27
+ description: Rapidly build Rails controllers.
28
+ email:
29
+ - bradgessler@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - app/assets/config/resourcefully_manifest.js
38
+ - app/controllers/oxidizer/nested_resource_controller.rb
39
+ - app/controllers/oxidizer/nested_resources_controller.rb
40
+ - app/controllers/oxidizer/nested_weak_resource_controller.rb
41
+ - app/controllers/oxidizer/resources_controller.rb
42
+ - config/routes.rb
43
+ - lib/oxidizer.rb
44
+ - lib/oxidizer/engine.rb
45
+ - lib/oxidizer/version.rb
46
+ - lib/tasks/oxidizer_tasks.rake
47
+ homepage: https://github.com/rocketshipio/oxidizer
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ allowed_push_host: https://rubygems.org
52
+ homepage_uri: https://github.com/rocketshipio/oxidizer
53
+ source_code_uri: https://github.com/rocketshipio/oxidizer
54
+ changelog_uri: https://github.com/rocketshipio/oxidizer
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.2.3
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Rapidly build Rails controllers.
74
+ test_files: []