jets-responders 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: 3d3a01e41bd6fe4be4e5302b20b73fe91585aab6e92d72a285947d2ce561b94f
4
+ data.tar.gz: 834e78db1644e8c86dc959e3966091b9e0bcf3f128812de98bb7d4b0d87ad1dd
5
+ SHA512:
6
+ metadata.gz: 4be0dd3874fddd4bfe1ac5ec9e486d41c621e6da244e71b1aef34c29aed8911f9d7d6d97a0f64f99aad7737d5c78a8cb8693ae65412524753af05a2f68a4d524
7
+ data.tar.gz: ffb85dc800ebd0ab1c86238ec46e5312a01f4f70271eb12104e9699a358b66c4660032fed771be0f5d16d6e3b3348cd6101fecc63cb7589344788d9bffccc845
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ /.bundle
4
+ /.config
5
+ /.yardoc
6
+ /_yardoc
7
+ /coverage
8
+ /doc/
9
+ /Gemfile.lock
10
+ /InstalledFiles
11
+ /lib/bundler/man
12
+ /pkg
13
+ /rdoc
14
+ /spec/reports
15
+ /test/tmp
16
+ /test/version_tmp
17
+ /tmp
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ This project *loosely tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
+
6
+ ## [0.1.0]
7
+ - Initial release.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem dependencies in gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,19 @@
1
+ guard "bundler", cmd: "bundle" do
2
+ watch("Gemfile")
3
+ watch(/^.+\.gemspec/)
4
+ end
5
+
6
+ guard :rspec, cmd: "bundle exec rspec" do
7
+ require "guard/rspec/dsl"
8
+ dsl = Guard::RSpec::Dsl.new(self)
9
+
10
+ # RSpec files
11
+ rspec = dsl.rspec
12
+ watch(rspec.spec_helper) { rspec.spec_dir }
13
+ watch(rspec.spec_support) { rspec.spec_dir }
14
+ watch(rspec.spec_files)
15
+
16
+ # Ruby files
17
+ ruby = dsl.ruby
18
+ dsl.watch_spec_files_for(ruby.lib_files)
19
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Tung Nguyen
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.
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # Responders
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/jets-responders.png)](http://badge.fury.io/rb/jets-responders)
4
+
5
+ [![BoltOps Badge](https://img.boltops.com/boltops/badges/boltops-badge.png)](https://www.boltops.com)
6
+
7
+ [![BoltOps Learn Badge](https://img.boltops.com/boltops-learn/boltops-learn.png)](https://learn.boltops.com)
8
+
9
+ A set of responders modules to dry up your Jets app.
10
+
11
+ This is a humble port of the original [responders](https://github.com/heartcombo/responders) to support the [Ruby on Jets](https://rubyonjets.com/) framework. A big thanks to the original authors of responders.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task default: :spec
5
+
6
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "responders/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jets-responders"
8
+ spec.version = Responders::VERSION
9
+ spec.authors = ["Tung Nguyen"]
10
+ spec.email = ["tung@boltops.com"]
11
+ spec.summary = "A set of Jets responders to dry up your application"
12
+ spec.homepage = "https://github.com/rubyonjets/jets-responders"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = File.directory?('.git') ? `git ls-files`.split($/) : Dir.glob("**/*")
16
+ spec.bindir = "exe"
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+ spec.add_dependency "memoist"
23
+ spec.add_dependency "rainbow"
24
+ spec.add_dependency "zeitwerk"
25
+
26
+ spec.add_development_dependency "bundler"
27
+ spec.add_development_dependency "byebug"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "rspec"
30
+ end
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/extract_options"
4
+ require "action_controller/metal/mime_responds"
5
+
6
+ module Jets::Controller # :nodoc:
7
+ module RespondWith
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class_attribute :responder, :mimes_for_respond_to
12
+ self.responder = Jets::Controller::Responder
13
+ clear_respond_to
14
+ end
15
+
16
+ module ClassMethods
17
+ # Defines mime types that are rendered by default when invoking
18
+ # <tt>respond_with</tt>.
19
+ #
20
+ # respond_to :html, :xml, :json
21
+ #
22
+ # Specifies that all actions in the controller respond to requests
23
+ # for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>.
24
+ #
25
+ # To specify on per-action basis, use <tt>:only</tt> and
26
+ # <tt>:except</tt> with an array of actions or a single action:
27
+ #
28
+ # respond_to :html
29
+ # respond_to :xml, :json, except: [ :edit ]
30
+ #
31
+ # This specifies that all actions respond to <tt>:html</tt>
32
+ # and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and
33
+ # <tt>:json</tt>.
34
+ #
35
+ # respond_to :json, only: :create
36
+ #
37
+ # This specifies that the <tt>:create</tt> action and no other responds
38
+ # to <tt>:json</tt>.
39
+ def respond_to(*mimes)
40
+ options = mimes.extract_options!
41
+
42
+ only_actions = Array(options.delete(:only)).map(&:to_sym)
43
+ except_actions = Array(options.delete(:except)).map(&:to_sym)
44
+
45
+ hash = mimes_for_respond_to.dup
46
+ mimes.each do |mime|
47
+ mime = mime.to_sym
48
+ hash[mime] = {}
49
+ hash[mime][:only] = only_actions unless only_actions.empty?
50
+ hash[mime][:except] = except_actions unless except_actions.empty?
51
+ end
52
+ self.mimes_for_respond_to = hash.freeze
53
+ end
54
+
55
+ # Clear all mime types in <tt>respond_to</tt>.
56
+ #
57
+ def clear_respond_to
58
+ self.mimes_for_respond_to = Hash.new.freeze
59
+ end
60
+ end
61
+
62
+ # For a given controller action, respond_with generates an appropriate
63
+ # response based on the mime-type requested by the client.
64
+ #
65
+ # If the method is called with just a resource, as in this example -
66
+ #
67
+ # class PeopleController < ApplicationController
68
+ # respond_to :html, :xml, :json
69
+ #
70
+ # def index
71
+ # @people = Person.all
72
+ # respond_with @people
73
+ # end
74
+ # end
75
+ #
76
+ # then the mime-type of the response is typically selected based on the
77
+ # request's Accept header and the set of available formats declared
78
+ # by previous calls to the controller's class method +respond_to+. Alternatively
79
+ # the mime-type can be selected by explicitly setting <tt>request.format</tt> in
80
+ # the controller.
81
+ #
82
+ # If an acceptable format is not identified, the application returns a
83
+ # '406 - not acceptable' status. Otherwise, the default response is to render
84
+ # a template named after the current action and the selected format,
85
+ # e.g. <tt>index.html.erb</tt>. If no template is available, the behavior
86
+ # depends on the selected format:
87
+ #
88
+ # * for an html response - if the request method is +get+, an exception
89
+ # is raised but for other requests such as +post+ the response
90
+ # depends on whether the resource has any validation errors (i.e.
91
+ # assuming that an attempt has been made to save the resource,
92
+ # e.g. by a +create+ action) -
93
+ # 1. If there are no errors, i.e. the resource
94
+ # was saved successfully, the response +redirect+'s to the resource
95
+ # i.e. its +show+ action.
96
+ # 2. If there are validation errors, the response
97
+ # renders a default action, which is <tt>:new</tt> for a
98
+ # +post+ request or <tt>:edit</tt> for +patch+ or +put+,
99
+ # and the status is set based on the configured `error_status`.
100
+ # (defaults to `422 Unprocessable Entity` on new apps,
101
+ # `200 OK` for compatibility reasons on old apps.)
102
+ # Thus an example like this -
103
+ #
104
+ # respond_to :html, :xml
105
+ #
106
+ # def create
107
+ # @user = User.new(params[:user])
108
+ # flash[:notice] = 'User was successfully created.' if @user.save
109
+ # respond_with(@user)
110
+ # end
111
+ #
112
+ # is equivalent, in the absence of <tt>create.html.erb</tt>, to -
113
+ #
114
+ # def create
115
+ # @user = User.new(params[:user])
116
+ # respond_to do |format|
117
+ # if @user.save
118
+ # flash[:notice] = 'User was successfully created.'
119
+ # format.html { redirect_to(@user) }
120
+ # format.xml { render xml: @user }
121
+ # else
122
+ # format.html { render action: "new", status: :unprocessable_entity }
123
+ # format.xml { render xml: @user, status: :unprocessable_entity }
124
+ # end
125
+ # end
126
+ # end
127
+ #
128
+ # * for a JavaScript request - if the template isn't found, an exception is
129
+ # raised.
130
+ # * for other requests - i.e. data formats such as xml, json, csv etc, if
131
+ # the resource passed to +respond_with+ responds to <code>to_<format></code>,
132
+ # the method attempts to render the resource in the requested format
133
+ # directly, e.g. for an xml request, the response is equivalent to calling
134
+ # <code>render xml: resource</code>.
135
+ #
136
+ # === Nested resources
137
+ #
138
+ # As outlined above, the +resources+ argument passed to +respond_with+
139
+ # can play two roles. It can be used to generate the redirect url
140
+ # for successful html requests (e.g. for +create+ actions when
141
+ # no template exists), while for formats other than html and JavaScript
142
+ # it is the object that gets rendered, by being converted directly to the
143
+ # required format (again assuming no template exists).
144
+ #
145
+ # For redirecting successful html requests, +respond_with+ also supports
146
+ # the use of nested resources, which are supplied in the same way as
147
+ # in <code>form_for</code> and <code>polymorphic_url</code>. For example -
148
+ #
149
+ # def create
150
+ # @project = Project.find(params[:project_id])
151
+ # @task = @project.comments.build(params[:task])
152
+ # flash[:notice] = 'Task was successfully created.' if @task.save
153
+ # respond_with(@project, @task)
154
+ # end
155
+ #
156
+ # This would cause +respond_with+ to redirect to <code>project_task_url</code>
157
+ # instead of <code>task_url</code>. For request formats other than html or
158
+ # JavaScript, if multiple resources are passed in this way, it is the last
159
+ # one specified that is rendered.
160
+ #
161
+ # === Customizing response behavior
162
+ #
163
+ # Like +respond_to+, +respond_with+ may also be called with a block that
164
+ # can be used to overwrite any of the default responses, e.g. -
165
+ #
166
+ # def create
167
+ # @user = User.new(params[:user])
168
+ # flash[:notice] = "User was successfully created." if @user.save
169
+ #
170
+ # respond_with(@user) do |format|
171
+ # format.html { render }
172
+ # end
173
+ # end
174
+ #
175
+ # The argument passed to the block is an ActionController::MimeResponds::Collector
176
+ # object which stores the responses for the formats defined within the
177
+ # block. Note that formats with responses defined explicitly in this way
178
+ # do not have to first be declared using the class method +respond_to+.
179
+ #
180
+ # Also, a hash passed to +respond_with+ immediately after the specified
181
+ # resource(s) is interpreted as a set of options relevant to all
182
+ # formats. Any option accepted by +render+ can be used, e.g.
183
+ #
184
+ # respond_with @people, status: 200
185
+ #
186
+ # However, note that these options are ignored after an unsuccessful attempt
187
+ # to save a resource, e.g. when automatically rendering <tt>:new</tt>
188
+ # after a post request.
189
+ #
190
+ # Three additional options are relevant specifically to +respond_with+ -
191
+ # 1. <tt>:location</tt> - overwrites the default redirect location used after
192
+ # a successful html +post+ request.
193
+ # 2. <tt>:action</tt> - overwrites the default render action used after an
194
+ # unsuccessful html +post+ request.
195
+ # 3. <tt>:render</tt> - allows to pass any options directly to the <tt>:render<tt/>
196
+ # call after unsuccessful html +post+ request. Useful if for example you
197
+ # need to render a template which is outside of controller's path or you
198
+ # want to override the default http <tt>:status</tt> code, e.g.
199
+ #
200
+ # respond_with(resource, render: { template: 'path/to/template', status: 418 })
201
+ def respond_with(*resources, &block)
202
+ if self.class.mimes_for_respond_to.empty?
203
+ raise "In order to use respond_with, first you need to declare the " \
204
+ "formats your controller responds to in the class level."
205
+ end
206
+
207
+ mimes = collect_mimes_from_class_level
208
+ collector = ActionController::MimeResponds::Collector.new(mimes, request.variant)
209
+ block.call(collector) if block_given?
210
+
211
+ if format = collector.negotiate_format(request)
212
+ _process_format(format)
213
+ options = resources.size == 1 ? {} : resources.extract_options!
214
+ options = options.clone
215
+
216
+ options[:default_response] = collector.response
217
+ (options.delete(:responder) || self.class.responder).call(self, resources, options)
218
+ else
219
+ raise ActionController::UnknownFormat
220
+ end
221
+ end
222
+
223
+ protected
224
+
225
+ # Before action callback that can be used to prevent requests that do not
226
+ # match the mime types defined through <tt>respond_to</tt> from being executed.
227
+ #
228
+ # class PeopleController < ApplicationController
229
+ # respond_to :html, :xml, :json
230
+ #
231
+ # before_action :verify_requested_format!
232
+ # end
233
+ def verify_requested_format!
234
+ mimes = collect_mimes_from_class_level
235
+ collector = ActionController::MimeResponds::Collector.new(mimes, request.variant)
236
+
237
+ unless collector.negotiate_format(request)
238
+ raise ActionController::UnknownFormat
239
+ end
240
+ end
241
+
242
+ alias :verify_request_format! :verify_requested_format!
243
+
244
+ # Collect mimes declared in the class method respond_to valid for the
245
+ # current action.
246
+ def collect_mimes_from_class_level # :nodoc:
247
+ action = action_name.to_sym
248
+
249
+ self.class.mimes_for_respond_to.keys.select do |mime|
250
+ config = self.class.mimes_for_respond_to[mime]
251
+
252
+ if config[:except]
253
+ !config[:except].include?(action)
254
+ elsif config[:only]
255
+ config[:only].include?(action)
256
+ else
257
+ true
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/json"
4
+
5
+ module Jets::Controller # :nodoc:
6
+ # Responsible for exposing a resource to different mime requests,
7
+ # usually depending on the HTTP verb. The responder is triggered when
8
+ # <code>respond_with</code> is called. The simplest case to study is a GET request:
9
+ #
10
+ # class PeopleController < ApplicationController
11
+ # respond_to :html, :xml, :json
12
+ #
13
+ # def index
14
+ # @people = Person.all
15
+ # respond_with(@people)
16
+ # end
17
+ # end
18
+ #
19
+ # When a request comes in, for example for an XML response, three steps happen:
20
+ #
21
+ # 1) the responder searches for a template at people/index.xml;
22
+ #
23
+ # 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource;
24
+ #
25
+ # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it.
26
+ #
27
+ # === Built-in HTTP verb semantics
28
+ #
29
+ # The default \Rails responder holds semantics for each HTTP verb. Depending on the
30
+ # content type, verb and the resource status, it will behave differently.
31
+ #
32
+ # Using \Rails default responder, a POST request for creating an object could
33
+ # be written as:
34
+ #
35
+ # def create
36
+ # @user = User.new(params[:user])
37
+ # flash[:notice] = 'User was successfully created.' if @user.save
38
+ # respond_with(@user)
39
+ # end
40
+ #
41
+ # Which is exactly the same as:
42
+ #
43
+ # def create
44
+ # @user = User.new(params[:user])
45
+ #
46
+ # respond_to do |format|
47
+ # if @user.save
48
+ # flash[:notice] = 'User was successfully created.'
49
+ # format.html { redirect_to(@user) }
50
+ # format.xml { render xml: @user, status: :created, location: @user }
51
+ # else
52
+ # format.html { render action: "new", status: :unprocessable_entity }
53
+ # format.xml { render xml: @user.errors, status: :unprocessable_entity }
54
+ # end
55
+ # end
56
+ # end
57
+ #
58
+ # The same happens for PATCH/PUT and DELETE requests.
59
+ #
60
+ # === Nested resources
61
+ #
62
+ # You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>.
63
+ # Consider the project has many tasks example. The create action for
64
+ # TasksController would be like:
65
+ #
66
+ # def create
67
+ # @project = Project.find(params[:project_id])
68
+ # @task = @project.tasks.build(params[:task])
69
+ # flash[:notice] = 'Task was successfully created.' if @task.save
70
+ # respond_with(@project, @task)
71
+ # end
72
+ #
73
+ # Giving several resources ensures that the responder will redirect to
74
+ # <code>project_task_url</code> instead of <code>task_url</code>.
75
+ #
76
+ # Namespaced and singleton resources require a symbol to be given, as in
77
+ # polymorphic urls. If a project has one manager which has many tasks, it
78
+ # should be invoked as:
79
+ #
80
+ # respond_with(@project, :manager, @task)
81
+ #
82
+ # Note that if you give an array, it will be treated as a collection,
83
+ # so the following is not equivalent:
84
+ #
85
+ # respond_with [@project, :manager, @task]
86
+ #
87
+ # === Custom options
88
+ #
89
+ # <code>respond_with</code> also allows you to pass options that are forwarded
90
+ # to the underlying render call. Those options are only applied for success
91
+ # scenarios. For instance, you can do the following in the create method above:
92
+ #
93
+ # def create
94
+ # @project = Project.find(params[:project_id])
95
+ # @task = @project.tasks.build(params[:task])
96
+ # flash[:notice] = 'Task was successfully created.' if @task.save
97
+ # respond_with(@project, @task, status: 201)
98
+ # end
99
+ #
100
+ # This will return status 201 if the task was saved successfully. If not,
101
+ # it will simply ignore the given options and return status 422 and the
102
+ # resource errors. You can also override the location to redirect to:
103
+ #
104
+ # respond_with(@project, location: root_path)
105
+ #
106
+ # To customize the failure scenario, you can pass a block to
107
+ # <code>respond_with</code>:
108
+ #
109
+ # def create
110
+ # @project = Project.find(params[:project_id])
111
+ # @task = @project.tasks.build(params[:task])
112
+ # respond_with(@project, @task, status: 201) do |format|
113
+ # if @task.save
114
+ # flash[:notice] = 'Task was successfully created.'
115
+ # else
116
+ # format.html { render "some_special_template", status: :unprocessable_entity }
117
+ # end
118
+ # end
119
+ # end
120
+ #
121
+ # Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>.
122
+ class Responder
123
+ class_attribute :error_status, default: :ok, instance_writer: false, instance_predicate: false
124
+ class_attribute :redirect_status, default: :found, instance_writer: false, instance_predicate: false
125
+
126
+ attr_reader :controller, :request, :format, :resource, :resources, :options
127
+
128
+ DEFAULT_ACTIONS_FOR_VERBS = {
129
+ post: :new,
130
+ patch: :edit,
131
+ put: :edit
132
+ }
133
+
134
+ def initialize(controller, resources, options = {})
135
+ @controller = controller
136
+ @request = @controller.request
137
+ @format = @controller.formats.first
138
+ @resource = resources.last
139
+ @resources = resources
140
+ @options = options
141
+ @action = options.delete(:action)
142
+ @default_response = options.delete(:default_response)
143
+
144
+ if options[:location].respond_to?(:call)
145
+ location = options.delete(:location)
146
+ options[:location] = location.call unless has_errors?
147
+ end
148
+ end
149
+
150
+ delegate :head, :render, :redirect_to, to: :controller
151
+ delegate :get?, :post?, :patch?, :put?, :delete?, to: :request
152
+
153
+ # Undefine :to_json and :to_yaml since it's defined on Object
154
+ undef_method(:to_json) if method_defined?(:to_json)
155
+ undef_method(:to_yaml) if method_defined?(:to_yaml)
156
+
157
+ # Initializes a new responder and invokes the proper format. If the format is
158
+ # not defined, call to_format.
159
+ #
160
+ def self.call(*args)
161
+ new(*args).respond
162
+ end
163
+
164
+ # Main entry point for responder responsible to dispatch to the proper format.
165
+ #
166
+ def respond
167
+ method = "to_#{format}"
168
+ respond_to?(method) ? send(method) : to_format
169
+ end
170
+
171
+ # HTML format does not render the resource, it always attempt to render a
172
+ # template.
173
+ #
174
+ def to_html
175
+ default_render
176
+ rescue ActionView::MissingTemplate => e
177
+ navigation_behavior(e)
178
+ end
179
+
180
+ # to_js simply tries to render a template. If no template is found, raises the error.
181
+ def to_js
182
+ default_render
183
+ end
184
+
185
+ # All other formats follow the procedure below. First we try to render a
186
+ # template, if the template is not available, we verify if the resource
187
+ # responds to :to_format and display it.
188
+ #
189
+ def to_format
190
+ if !get? && has_errors? && !response_overridden?
191
+ display_errors
192
+ elsif has_view_rendering? || response_overridden?
193
+ default_render
194
+ else
195
+ api_behavior
196
+ end
197
+ rescue ActionView::MissingTemplate
198
+ api_behavior
199
+ end
200
+
201
+ protected
202
+
203
+ # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth.
204
+ def navigation_behavior(error)
205
+ if get?
206
+ raise error
207
+ elsif has_errors? && default_action
208
+ render error_rendering_options
209
+ else
210
+ redirect_to navigation_location, status: redirect_status
211
+ end
212
+ end
213
+
214
+ # This is the common behavior for formats associated with APIs, such as :xml and :json.
215
+ def api_behavior
216
+ raise MissingRenderer.new(format) unless has_renderer?
217
+
218
+ if get?
219
+ display resource
220
+ elsif post?
221
+ display resource, status: :created, location: api_location
222
+ else
223
+ head :no_content
224
+ end
225
+ end
226
+
227
+ # Returns the resource location by retrieving it from the options or
228
+ # returning the resources array.
229
+ #
230
+ def resource_location
231
+ options[:location] || resources
232
+ end
233
+ alias :navigation_location :resource_location
234
+ alias :api_location :resource_location
235
+
236
+ # If a response block was given, use it, otherwise call render on
237
+ # controller.
238
+ #
239
+ def default_render
240
+ if @default_response
241
+ @default_response.call(options)
242
+ elsif !get? && has_errors?
243
+ controller.render({ status: error_status }.merge!(options))
244
+ else
245
+ controller.render(options)
246
+ end
247
+ end
248
+
249
+ # Display is just a shortcut to render a resource with the current format.
250
+ #
251
+ # display @user, status: :ok
252
+ #
253
+ # For XML requests it's equivalent to:
254
+ #
255
+ # render xml: @user, status: :ok
256
+ #
257
+ # Options sent by the user are also used:
258
+ #
259
+ # respond_with(@user, status: :created)
260
+ # display(@user, status: :ok)
261
+ #
262
+ # Results in:
263
+ #
264
+ # render xml: @user, status: :created
265
+ #
266
+ def display(resource, given_options = {})
267
+ controller.render given_options.merge!(options).merge!(format => resource)
268
+ end
269
+
270
+ def display_errors
271
+ # TODO: use `error_status` once we switch the default to be `unprocessable_entity`,
272
+ # otherwise we'd be changing this behavior here now.
273
+ controller.render format => resource_errors, :status => :unprocessable_entity
274
+ end
275
+
276
+ # Check whether the resource has errors.
277
+ #
278
+ def has_errors?
279
+ resource.respond_to?(:errors) && !resource.errors.empty?
280
+ end
281
+
282
+ # Check whether the necessary Renderer is available
283
+ def has_renderer?
284
+ Renderers::RENDERERS.include?(format)
285
+ end
286
+
287
+ def has_view_rendering?
288
+ controller.class.include? ActionView::Rendering
289
+ end
290
+
291
+ # By default, render the <code>:edit</code> action for HTML requests with errors, unless
292
+ # the verb was POST.
293
+ #
294
+ def default_action
295
+ @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol]
296
+ end
297
+
298
+ def resource_errors
299
+ respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors
300
+ end
301
+
302
+ def json_resource_errors
303
+ { errors: resource.errors }
304
+ end
305
+
306
+ def response_overridden?
307
+ @default_response.present?
308
+ end
309
+
310
+ def error_rendering_options
311
+ if options[:render]
312
+ options[:render]
313
+ else
314
+ { action: default_action, status: error_status }
315
+ end
316
+ end
317
+ end
318
+ end
@@ -0,0 +1 @@
1
+ require_relative "responders"
@@ -0,0 +1,23 @@
1
+ require "zeitwerk"
2
+
3
+ module Responders
4
+ class Autoloader
5
+ class Inflector < Zeitwerk::Inflector
6
+ def camelize(basename, _abspath)
7
+ map = { version: "VERSION" }
8
+ map[basename.to_sym] || super
9
+ end
10
+ end
11
+
12
+ class << self
13
+ def setup
14
+ loader = Zeitwerk::Loader.new
15
+ loader.inflector = Inflector.new
16
+ lib = File.dirname(__dir__)
17
+ loader.push_dir(lib)
18
+ loader.ignore("#{lib}/jets-responders.rb")
19
+ loader.setup
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Responders
4
+ # This responder modifies your current responder to redirect
5
+ # to the collection page on POST/PUT/DELETE.
6
+ module CollectionResponder
7
+ protected
8
+
9
+ # Returns the collection location for redirecting after POST/PUT/DELETE.
10
+ # This method, converts the following resources array to the following:
11
+ #
12
+ # [:admin, @post] #=> [:admin, :posts]
13
+ # [@user, @post] #=> [@user, :posts]
14
+ #
15
+ # When these new arrays are given to redirect_to, it will generate the
16
+ # proper URL pointing to the index action.
17
+ #
18
+ # [:admin, @post] #=> admin_posts_url
19
+ # [@user, @post] #=> user_posts_url(@user.to_param)
20
+ #
21
+ def navigation_location
22
+ return options[:location] if options[:location]
23
+
24
+ klass = resources.last.class
25
+
26
+ if klass.respond_to?(:model_name)
27
+ resources[0...-1] << klass.model_name.route_key.to_sym
28
+ else
29
+ resources
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Responders
4
+ module ControllerMethod
5
+ # Adds the given responders to the current controller's responder, allowing you to cherry-pick
6
+ # which responders you want per controller.
7
+ #
8
+ # class InvitationsController < ApplicationController
9
+ # responders :flash, :http_cache
10
+ # end
11
+ #
12
+ # Takes symbols and strings and translates them to VariableResponder (eg. :flash becomes FlashResponder).
13
+ # Also allows passing in the responders modules in directly, so you could do:
14
+ #
15
+ # responders FlashResponder, HttpCacheResponder
16
+ #
17
+ # Or a mix of both methods:
18
+ #
19
+ # responders :flash, MyCustomResponder
20
+ #
21
+ def responders(*responders)
22
+ self.responder = responders.inject(Class.new(responder)) do |klass, responder|
23
+ responder = \
24
+ case responder
25
+ when Module
26
+ responder
27
+ when String, Symbol
28
+ Responders.const_get("#{responder.to_s.camelize}Responder")
29
+ else
30
+ raise "responder has to be a string, a symbol or a module"
31
+ end
32
+
33
+ klass.send(:include, responder)
34
+ klass
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ ActiveSupport.on_load(:action_controller_base) do
41
+ Jets::Controller::Base.extend Responders::ControllerMethod
42
+ end
@@ -0,0 +1,22 @@
1
+ module Responders
2
+ require "responders/controller_method"
3
+
4
+ class Engine < ::Jets::Engine
5
+ config.responders = ActiveSupport::OrderedOptions.new
6
+ config.responders.flash_keys = [:notice, :alert]
7
+ config.responders.namespace_lookup = false
8
+ config.responders.error_status = :ok
9
+ config.responders.redirect_status = :found
10
+
11
+ # Add load paths straight to I18n, so engines and application can overwrite it.
12
+ require "active_support/i18n"
13
+ I18n.load_path << File.expand_path("../responders/locales/en.yml", __dir__)
14
+
15
+ initializer "responders.flash_responder" do |app|
16
+ Responders::FlashResponder.flash_keys = app.config.responders.flash_keys
17
+ Responders::FlashResponder.namespace_lookup = app.config.responders.namespace_lookup
18
+ Jets::Controller::Responder.error_status = app.config.responders.error_status
19
+ Jets::Controller::Responder.redirect_status = app.config.responders.redirect_status
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Responders
4
+ # Responder to automatically set flash messages based on I18n API. It checks for
5
+ # message based on the current action, but also allows defaults to be set, using
6
+ # the following order:
7
+ #
8
+ # flash.controller_name.action_name.status
9
+ # flash.actions.action_name.status
10
+ #
11
+ # So, if you have a CarsController, create action, it will check for:
12
+ #
13
+ # flash.cars.create.status
14
+ # flash.actions.create.status
15
+ #
16
+ # The statuses by default are :notice (when the object can be created, updated
17
+ # or destroyed with success) and :alert (when the object cannot be created
18
+ # or updated).
19
+ #
20
+ # On I18n, the resource_name given is available as interpolation option,
21
+ # this means you can set:
22
+ #
23
+ # flash:
24
+ # actions:
25
+ # create:
26
+ # notice: "Hooray! %{resource_name} was successfully created!"
27
+ #
28
+ # But sometimes, flash messages are not that simple. Going back
29
+ # to cars example, you might want to say the brand of the car when it's
30
+ # updated. Well, that's easy also:
31
+ #
32
+ # flash:
33
+ # cars:
34
+ # update:
35
+ # notice: "Hooray! You just tuned your %{car_brand}!"
36
+ #
37
+ # Since :car_name is not available for interpolation by default, you have
38
+ # to overwrite `flash_interpolation_options` in your controller.
39
+ #
40
+ # def flash_interpolation_options
41
+ # { :car_brand => @car.brand }
42
+ # end
43
+ #
44
+ # Then you will finally have:
45
+ #
46
+ # 'Hooray! You just tuned your Aston Martin!'
47
+ #
48
+ # If your controller is namespaced, for example Admin::CarsController,
49
+ # the messages will be checked in the following order:
50
+ #
51
+ # flash.admin.cars.create.status
52
+ # flash.admin.actions.create.status
53
+ # flash.cars.create.status
54
+ # flash.actions.create.status
55
+ #
56
+ # You can also have flash messages with embedded HTML. Just create a scope that
57
+ # ends with <tt>_html</tt> as the scopes below:
58
+ #
59
+ # flash.actions.create.notice_html
60
+ # flash.cars.create.notice_html
61
+ #
62
+ # == Options
63
+ #
64
+ # FlashResponder also accepts some options through respond_with API.
65
+ #
66
+ # * :flash - When set to false, no flash message is set.
67
+ #
68
+ # respond_with(@user, :flash => true)
69
+ #
70
+ # * :notice - Supply the message to be set if the record has no errors.
71
+ # * :alert - Supply the message to be set if the record has errors.
72
+ #
73
+ # respond_with(@user, :notice => "Hooray! Welcome!", :alert => "Woot! You failed.")
74
+ #
75
+ # * :flash_now - Sets the flash message using flash.now. Accepts true, :on_failure or :on_success.
76
+ #
77
+ # == Configure status keys
78
+ #
79
+ # As said previously, FlashResponder by default use :notice and :alert
80
+ # keys. You can change that by setting the status_keys:
81
+ #
82
+ # Responders::FlashResponder.flash_keys = [ :success, :failure ]
83
+ #
84
+ # However, the options :notice and :alert to respond_with are kept :notice
85
+ # and :alert.
86
+ #
87
+ module FlashResponder
88
+ class << self
89
+ attr_accessor :flash_keys, :namespace_lookup
90
+ end
91
+
92
+ self.flash_keys = [ :notice, :alert ]
93
+ self.namespace_lookup = false
94
+
95
+ def initialize(controller, resources, options = {})
96
+ super
97
+ @flash = options.delete(:flash)
98
+ @notice = options.delete(:notice)
99
+ @alert = options.delete(:alert)
100
+ @flash_now = options.delete(:flash_now) { :on_failure }
101
+ end
102
+
103
+ def to_html
104
+ set_flash_message! if set_flash_message?
105
+ super
106
+ end
107
+
108
+ def to_js
109
+ set_flash_message! if set_flash_message?
110
+ defined?(super) ? super : to_format
111
+ end
112
+
113
+ protected
114
+
115
+ def set_flash_message!
116
+ if has_errors?
117
+ status = Responders::FlashResponder.flash_keys.last
118
+ set_flash(status, @alert)
119
+ else
120
+ status = Responders::FlashResponder.flash_keys.first
121
+ set_flash(status, @notice)
122
+ end
123
+
124
+ return if controller.flash[status].present?
125
+
126
+ options = mount_i18n_options(status)
127
+ message = controller.helpers.t options[:default].shift, **options
128
+ set_flash(status, message)
129
+ end
130
+
131
+ def set_flash(key, value)
132
+ return if value.blank?
133
+ flash = controller.flash
134
+ flash = flash.now if set_flash_now?
135
+ flash[key] ||= value
136
+ end
137
+
138
+ def set_flash_now?
139
+ @flash_now == true || format == :js ||
140
+ (default_action && (has_errors? ? @flash_now == :on_failure : @flash_now == :on_success))
141
+ end
142
+
143
+ def set_flash_message? # :nodoc:
144
+ !get? && @flash != false
145
+ end
146
+
147
+ def mount_i18n_options(status) # :nodoc:
148
+ options = {
149
+ default: flash_defaults_by_namespace(status),
150
+ resource_name: resource_name,
151
+ downcase_resource_name: resource_name.downcase
152
+ }
153
+
154
+ controller_options = controller_interpolation_options
155
+ if controller_options
156
+ options.merge!(controller_options)
157
+ end
158
+
159
+ options
160
+ end
161
+
162
+ def controller_interpolation_options
163
+ controller.send(:flash_interpolation_options) if controller.respond_to?(:flash_interpolation_options, true)
164
+ end
165
+
166
+ def resource_name
167
+ if resource.class.respond_to?(:model_name)
168
+ resource.class.model_name.human
169
+ else
170
+ resource.class.name.underscore.humanize
171
+ end
172
+ end
173
+
174
+ def flash_defaults_by_namespace(status) # :nodoc:
175
+ defaults = []
176
+ slices = controller.controller_path.split("/")
177
+ lookup = Responders::FlashResponder.namespace_lookup
178
+
179
+ begin
180
+ controller_scope = :"flash.#{slices.fill(controller.controller_name, -1).join(".")}.#{controller.action_name}.#{status}"
181
+
182
+ actions_scope = lookup ? slices.fill("actions", -1).join(".") : :actions
183
+ actions_scope = :"flash.#{actions_scope}.#{controller.action_name}.#{status}"
184
+
185
+ defaults << :"#{controller_scope}_html"
186
+ defaults << controller_scope
187
+
188
+ defaults << :"#{actions_scope}_html"
189
+ defaults << actions_scope
190
+
191
+ slices.shift
192
+ end while slices.size > 0 && lookup
193
+
194
+ defaults << ""
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Responders
4
+ # Set HTTP Last-Modified headers based on the given resource. It's used only
5
+ # on API behavior (to_format) and is useful for a client to check in the server
6
+ # if a resource changed after a specific date or not.
7
+ #
8
+ # This is not usually not used in html requests because pages contains a lot
9
+ # information besides the resource information, as current_user, flash messages,
10
+ # widgets... that are better handled with other strategies, as fragment caches and
11
+ # the digest of the body.
12
+ #
13
+ module HttpCacheResponder
14
+ def initialize(controller, resources, options = {})
15
+ super
16
+ @http_cache = options.delete(:http_cache)
17
+ end
18
+
19
+ def to_format
20
+ return if do_http_cache? && do_http_cache!
21
+ super
22
+ end
23
+
24
+ protected
25
+
26
+ def do_http_cache!
27
+ timestamp = resources.map do |resource|
28
+ resource.updated_at.try(:utc) if resource.respond_to?(:updated_at)
29
+ end.compact.max
30
+
31
+ controller.response.last_modified ||= timestamp if timestamp
32
+
33
+ head :not_modified if fresh = request.fresh?(controller.response)
34
+ fresh
35
+ end
36
+
37
+ def do_http_cache?
38
+ get? && @http_cache != false && Jets::Controller::Base.perform_caching &&
39
+ persisted? && resource.respond_to?(:updated_at)
40
+ end
41
+
42
+ def persisted?
43
+ resource.respond_to?(:persisted?) ? resource.persisted? : true
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ en:
2
+ flash:
3
+ actions:
4
+ create:
5
+ notice: '%{resource_name} was successfully created.'
6
+ # alert: '%{resource_name} could not be created.'
7
+ update:
8
+ notice: '%{resource_name} was successfully updated.'
9
+ # alert: '%{resource_name} could not be updated.'
10
+ destroy:
11
+ notice: '%{resource_name} was successfully destroyed.'
12
+ alert: '%{resource_name} could not be destroyed.'
@@ -0,0 +1,3 @@
1
+ module Responders
2
+ VERSION = "0.1.0"
3
+ end
data/lib/responders.rb ADDED
@@ -0,0 +1,17 @@
1
+ $:.unshift(File.expand_path("../", __FILE__))
2
+
3
+ require "responders/autoloader"
4
+ Responders::Autoloader.setup
5
+
6
+ require "memoist"
7
+ require "rainbow/ext/string"
8
+
9
+ module Responders
10
+ class Error < StandardError; end
11
+ end
12
+
13
+ ActiveSupport.on_load(:jets_controller) do
14
+ include Jets::Controller::RespondWith
15
+ end
16
+
17
+ require "responders/engine"
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jets-responders
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tung Nguyen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-12-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: memoist
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rainbow
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description:
126
+ email:
127
+ - tung@boltops.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - CHANGELOG.md
134
+ - Gemfile
135
+ - Guardfile
136
+ - LICENSE.txt
137
+ - README.md
138
+ - Rakefile
139
+ - jets-responders.gemspec
140
+ - lib/jets-responders.rb
141
+ - lib/jets/controller/respond_with.rb
142
+ - lib/jets/controller/responder.rb
143
+ - lib/responders.rb
144
+ - lib/responders/autoloader.rb
145
+ - lib/responders/collection_responder.rb
146
+ - lib/responders/controller_method.rb
147
+ - lib/responders/engine.rb
148
+ - lib/responders/flash_responder.rb
149
+ - lib/responders/http_cache_responder.rb
150
+ - lib/responders/locales/en.yml
151
+ - lib/responders/version.rb
152
+ homepage: https://github.com/rubyonjets/jets-responders
153
+ licenses:
154
+ - MIT
155
+ metadata: {}
156
+ post_install_message:
157
+ rdoc_options: []
158
+ require_paths:
159
+ - lib
160
+ required_ruby_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ required_rubygems_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ requirements: []
171
+ rubygems_version: 3.4.20
172
+ signing_key:
173
+ specification_version: 4
174
+ summary: A set of Jets responders to dry up your application
175
+ test_files: []