oxidizer 0.1.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.
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: []